From b3acf8eac22be0972b220aee550bc96a4e78fcff Mon Sep 17 00:00:00 2001 From: btea <2356281422@qq.com> Date: Sun, 1 Nov 2026 16:02:25 +0800 Subject: [PATCH] feat(pluginutils): add wordRegex helper for token-boundary matching --- packages/pluginutils/README.md | 21 ++++++++++++- .../src/filter/simple-filters.test.ts | 31 ++++++++++++++++++- .../pluginutils/src/filter/simple-filters.ts | 25 +++++++++++++++ 3 files changed, 75 insertions(+), 2 deletions(-) diff --git a/packages/pluginutils/README.md b/packages/pluginutils/README.md index 7d4d5a5..2006487 100644 --- a/packages/pluginutils/README.md +++ b/packages/pluginutils/README.md @@ -13,7 +13,7 @@ pnpm add -D @rolldown/pluginutils ## Usage ```ts -import { exactRegex, prefixRegex, makeIdFiltersToMatchWithQuery } from '@rolldown/pluginutils' +import { exactRegex, prefixRegex, wordRegex, makeIdFiltersToMatchWithQuery } from '@rolldown/pluginutils' ``` All filter helpers are also exposed via the `/filter` subpath: @@ -60,6 +60,25 @@ const plugin = { } ``` +### `wordRegex` + +- **Type:** `(str: string, flags?: string) => RegExp` + +Constructs a `RegExp` that matches a standalone word-like token. +Useful when you want to match `node_modules` but not `node_modulesa`. + +```ts +import { wordRegex } from '@rolldown/pluginutils' + +const plugin = { + name: 'plugin', + resolveId: { + filter: { id: wordRegex('node_modules') }, + handler(id) {}, // called for IDs containing standalone `node_modules` + }, +} +``` + ### `makeIdFiltersToMatchWithQuery` - **Type:** `(input: string | RegExp | (string | RegExp)[]) => string | RegExp | (string | RegExp)[]` diff --git a/packages/pluginutils/src/filter/simple-filters.test.ts b/packages/pluginutils/src/filter/simple-filters.test.ts index adabf91..4986ef6 100644 --- a/packages/pluginutils/src/filter/simple-filters.test.ts +++ b/packages/pluginutils/src/filter/simple-filters.test.ts @@ -1,6 +1,6 @@ import picomatch from 'picomatch' import { describe, expect, test } from 'vitest' -import { exactRegex, makeIdFiltersToMatchWithQuery, prefixRegex } from './simple-filters.js' +import { exactRegex, makeIdFiltersToMatchWithQuery, prefixRegex, wordRegex } from './simple-filters.js' describe('exactRegex', () => { test('supports without flag parameter', () => { @@ -82,6 +82,35 @@ describe('makeIdFiltersToMatchWithQuery', () => { expect(matcher('/foo/bar.txt?.js')).toBe(true) }) + describe('wordRegex', () => { + test('matches word-like token but not suffix extension', () => { + const regex = wordRegex('node_modules') + expect(regex).toStrictEqual(/\bnode_modules\b/) + + expect(regex.test('node_modules')).toBe(true) + expect(regex.test('/node_modules/')).toBe(true) + expect(regex.test('foo/node_modules/bar')).toBe(true) + expect(regex.test('node_modulesa')).toBe(false) + expect(regex.test('anode_modules')).toBe(false) + }) + + test('supports with flag parameter', () => { + const regex = wordRegex('node_modules', 'i') + expect(regex).toStrictEqual(/\bnode_modules\b/i) + + expect(regex.test('NODE_MODULES')).toBe(true) + expect(regex.test('NODE_MODULESa')).toBe(false) + }) + + test('escapes special characters for Regex', () => { + const regex = wordRegex('foo.bar') + expect(regex).toStrictEqual(/\bfoo\.bar\b/) + + expect(regex.test('foo.bar')).toBe(true) + expect(regex.test('foo.barx')).toBe(false) + }) + }) + test('supports regex patterns without `$`', () => { const input = /\/foo\// const output = makeIdFiltersToMatchWithQuery(input) diff --git a/packages/pluginutils/src/filter/simple-filters.ts b/packages/pluginutils/src/filter/simple-filters.ts index 990d517..58b70ee 100644 --- a/packages/pluginutils/src/filter/simple-filters.ts +++ b/packages/pluginutils/src/filter/simple-filters.ts @@ -46,6 +46,31 @@ export function prefixRegex(str: string, flags?: string): RegExp { return new RegExp(`^${escapeRegex(str)}`, flags) } +/** + * Constructs a RegExp that matches the specified string with word boundaries. + * + * This is useful for plugin hook filters when you want to match a complete word-like token, + * such as matching `node_modules` but not `node_modulesa`. + * + * @param str the string to match. + * @param flags flags for the RegExp. + * + * @example + * ```ts + * import { wordRegex } from '@rolldown/pluginutils'; + * const plugin = { + * name: 'plugin', + * resolveId: { + * filter: { id: wordRegex('node_modules') }, + * handler(id) {} // called for IDs containing `node_modules` as a standalone token + * } + * } + * ``` + */ +export function wordRegex(str: string, flags?: string): RegExp { + return new RegExp(`\\b${escapeRegex(str)}\\b`, flags) +} + const escapeRegexRE = /[-/\\^$*+?.()|[\]{}]/g function escapeRegex(str: string): string { return str.replace(escapeRegexRE, '\\$&')