Skip to content

Commit 4200731

Browse files
committed
feat: support regex pattern
1 parent aa03543 commit 4200731

File tree

11 files changed

+172
-71
lines changed

11 files changed

+172
-71
lines changed

src/core/options.ts

Lines changed: 27 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,17 @@
11
import type { FilterPattern } from '@rollup/pluginutils'
22

3-
type Replacement = string | ((id: string) => string)
3+
export type Replacement =
4+
| string
5+
| ((id: string, match: RegExpExecArray) => string)
6+
export type ReplaceItem<F = string | RegExp> = {
7+
find: F
8+
replacement: Replacement
9+
}
10+
export type ReplaceMap = {
11+
[str: string]: Replacement
12+
}
413

5-
interface BaseOptions {
14+
export interface BaseOptions {
615
/**
716
* A picomatch pattern, or array of patterns, of files that should be
817
* processed by this plugin (if omitted, all files are included by default)
@@ -34,8 +43,7 @@ interface BaseOptions {
3443
/**
3544
* You can separate values to replace from other options.
3645
*/
37-
values?: { [str: string]: Replacement }
38-
46+
values?: ReplaceMap | ReplaceItem[]
3947
enforce?: 'pre' | 'post' | undefined
4048
}
4149

@@ -56,10 +64,11 @@ export type OptionsResolved = Overwrite<
5664
Required<BaseOptions>,
5765
{
5866
enforce: BaseOptions['enforce']
67+
values: ReplaceItem[]
5968
}
6069
>
6170

62-
export function resolveOption(options: Options): OptionsResolved {
71+
export function resolveOptions(options: Options): OptionsResolved {
6372
return {
6473
include: options.include || [/\.[cm]?[jt]sx?$/],
6574
exclude: options.exclude || [/node_modules/],
@@ -73,15 +82,26 @@ export function resolveOption(options: Options): OptionsResolved {
7382

7483
function getReplacements() {
7584
if (options.values) {
76-
return Object.assign({}, options.values)
85+
if (Array.isArray(options.values)) {
86+
return options.values
87+
}
88+
return normalizeObjectValues(options.values)
7789
}
90+
7891
const values = Object.assign({}, options)
7992
delete values.delimiters
8093
delete values.include
8194
delete values.exclude
8295
delete values.sourcemap
8396
delete values.sourceMap
8497
delete values.objectGuards
85-
return values as OptionsResolved['values']
98+
return normalizeObjectValues(values as ReplaceMap)
99+
}
100+
101+
function normalizeObjectValues(values: ReplaceMap): ReplaceItem[] {
102+
return Object.entries(values).map(([find, replacement]) => ({
103+
find,
104+
replacement,
105+
}))
86106
}
87107
}

src/index.ts

Lines changed: 81 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { type TransformResult, createUnplugin } from 'unplugin'
22
import { createFilter } from '@rollup/pluginutils'
33
import MagicString from 'magic-string'
4-
import { type Options, resolveOption } from './core/options'
4+
import { type Options, type ReplaceItem, resolveOptions } from './core/options'
55

66
export default createUnplugin<Options | undefined, false>((rawOptions = {}) => {
77
let {
@@ -13,16 +13,25 @@ export default createUnplugin<Options | undefined, false>((rawOptions = {}) => {
1313
delimiters,
1414
values,
1515
enforce,
16-
} = resolveOption(rawOptions)
16+
} = resolveOptions(rawOptions)
1717
const filter = createFilter(include, exclude)
1818

19-
if (objectGuards) expandTypeofReplacements(values)
20-
const functionValues = mapToFunctions(values)
21-
// eslint-disable-next-line unicorn/no-array-callback-reference
22-
const keys = Object.keys(functionValues).sort(longest).map(escape)
19+
const stringValues = values.filter(
20+
(value): value is ReplaceItem<string> => typeof value.find === 'string',
21+
)
22+
const regexpValues = values.filter(
23+
(value): value is ReplaceItem<RegExp> => value.find instanceof RegExp,
24+
)
25+
26+
if (objectGuards) expandTypeofReplacements(stringValues)
27+
const escapedKeys = stringValues
28+
.map(({ find }) => find)
29+
.sort(longest)
30+
// eslint-disable-next-line unicorn/no-array-callback-reference
31+
.map(escape)
2332
const lookahead = preventAssignment ? '(?!\\s*(=[^=]|:[^:]))' : ''
2433
const pattern = new RegExp(
25-
`${delimiters[0]}(${keys.join('|')})${delimiters[1]}${lookahead}`,
34+
`${delimiters[0]}(${escapedKeys.join('|')})${delimiters[1]}${lookahead}`,
2635
'g',
2736
)
2837

@@ -40,7 +49,7 @@ export default createUnplugin<Options | undefined, false>((rawOptions = {}) => {
4049
},
4150

4251
transformInclude(id) {
43-
if (keys.length === 0) return false
52+
if (escapedKeys.length === 0 && regexpValues.length === 0) return false
4453
return filter(id)
4554
},
4655

@@ -73,18 +82,32 @@ export default createUnplugin<Options | undefined, false>((rawOptions = {}) => {
7382
id: string,
7483
magicString: MagicString,
7584
) {
76-
let result = false
77-
let match
85+
let has = false
86+
let match: RegExpExecArray | null
87+
88+
const values: ReplaceItem<RegExp>[] = [...regexpValues]
89+
if (escapedKeys.length > 0)
90+
values.push({ find: pattern, replacement: null! })
91+
92+
for (const { find, replacement } of values) {
93+
while ((match = find.exec(code))) {
94+
has = true
95+
96+
const start = match.index
97+
const end = start + match[0].length
7898

79-
while ((match = pattern.exec(code))) {
80-
result = true
99+
const finalReplacement =
100+
find === pattern
101+
? stringValues.find(({ find }) => find === match![1])!.replacement
102+
: replacement
103+
const result = String(ensureFunction(finalReplacement)(id, match))
104+
magicString.overwrite(start, end, result)
81105

82-
const start = match.index
83-
const end = start + match[0].length
84-
const replacement = String(functionValues[match[1]](id))
85-
magicString.overwrite(start, end, replacement)
106+
if (!find.global) break
107+
}
86108
}
87-
return result
109+
110+
return has
88111
}
89112
})
90113

@@ -93,7 +116,9 @@ function escape(str: string) {
93116
return str.replace(/[$()*+./?[\\\]^{|}-]/g, '\\$&')
94117
}
95118

96-
function ensureFunction(functionOrValue: any): () => any {
119+
function ensureFunction(
120+
functionOrValue: any,
121+
): (id: string, match: RegExpExecArray) => any {
97122
if (typeof functionOrValue === 'function') return functionOrValue
98123
return () => functionOrValue
99124
}
@@ -102,47 +127,51 @@ function longest(a: string, b: string) {
102127
return b.length - a.length
103128
}
104129

105-
function mapToFunctions(object: Record<string, any>): Record<string, Function> {
106-
return Object.keys(object).reduce((fns, key) => {
107-
const functions: Record<string, Function> = Object.assign({}, fns)
108-
functions[key] = ensureFunction(object[key])
109-
return functions
110-
}, {})
111-
}
112-
113130
const objKeyRegEx =
114131
/^([$A-Z_a-z\u00A0-\uFFFF][\w$\u00A0-\uFFFF]*)(\.([$A-Z_a-z\u00A0-\uFFFF][\w$\u00A0-\uFFFF]*))+$/
115-
function expandTypeofReplacements(replacements: Record<string, any>) {
116-
Object.keys(replacements).forEach((key) => {
117-
const objMatch = key.match(objKeyRegEx)
132+
function expandTypeofReplacements(values: ReplaceItem<string>[]) {
133+
values.forEach(({ find }) => {
134+
const objMatch = find.match(objKeyRegEx)
118135
if (!objMatch) return
119136
let dotIndex = objMatch[1].length
120137
let lastIndex = 0
121138
do {
122-
// eslint-disable-next-line no-param-reassign
123-
replacements[`typeof ${key.slice(lastIndex, dotIndex)} ===`] =
124-
'"object" ==='
125-
// eslint-disable-next-line no-param-reassign
126-
replacements[`typeof ${key.slice(lastIndex, dotIndex)} !==`] =
127-
'"object" !=='
128-
// eslint-disable-next-line no-param-reassign
129-
replacements[`typeof ${key.slice(lastIndex, dotIndex)}===`] =
130-
'"object"==='
131-
// eslint-disable-next-line no-param-reassign
132-
replacements[`typeof ${key.slice(lastIndex, dotIndex)}!==`] =
133-
'"object"!=='
134-
// eslint-disable-next-line no-param-reassign
135-
replacements[`typeof ${key.slice(lastIndex, dotIndex)} ==`] =
136-
'"object" ==='
137-
// eslint-disable-next-line no-param-reassign
138-
replacements[`typeof ${key.slice(lastIndex, dotIndex)} !=`] =
139-
'"object" !=='
140-
// eslint-disable-next-line no-param-reassign
141-
replacements[`typeof ${key.slice(lastIndex, dotIndex)}==`] = '"object"==='
142-
// eslint-disable-next-line no-param-reassign
143-
replacements[`typeof ${key.slice(lastIndex, dotIndex)}!=`] = '"object"!=='
139+
values.push(
140+
{
141+
find: `typeof ${find.slice(lastIndex, dotIndex)} ===`,
142+
replacement: '"object" ===',
143+
},
144+
{
145+
find: `typeof ${find.slice(lastIndex, dotIndex)} !==`,
146+
replacement: '"object" !==',
147+
},
148+
{
149+
find: `typeof ${find.slice(lastIndex, dotIndex)}===`,
150+
replacement: '"object"===',
151+
},
152+
{
153+
find: `typeof ${find.slice(lastIndex, dotIndex)}!==`,
154+
replacement: '"object"!==',
155+
},
156+
{
157+
find: `typeof ${find.slice(lastIndex, dotIndex)} ==`,
158+
replacement: '"object" ===',
159+
},
160+
{
161+
find: `typeof ${find.slice(lastIndex, dotIndex)} !=`,
162+
replacement: '"object" !==',
163+
},
164+
{
165+
find: `typeof ${find.slice(lastIndex, dotIndex)}==`,
166+
replacement: '"object"===',
167+
},
168+
{
169+
find: `typeof ${find.slice(lastIndex, dotIndex)}!=`,
170+
replacement: '"object"!==',
171+
},
172+
)
144173
lastIndex = dotIndex + 1
145-
dotIndex = key.indexOf('.', lastIndex)
174+
dotIndex = find.indexOf('.', lastIndex)
146175
} while (dotIndex !== -1)
147176
})
148177
}

tests/__snapshots__/esbuild.test.ts.snap

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ exports[`esbuild 1`] = `
44
"// tests/fixtures/main.js
55
var platform = "darwin";
66
console.log('hello "darwin"', platform);
7+
console.log(DEV);
78
export {
89
platform
910
};

tests/__snapshots__/rollup.test.ts.snap

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,24 @@
11
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
22

3-
exports[`rollup 1`] = `
3+
exports[`rollup > find regexp 1`] = `
4+
"import 'node:process';
5+
6+
const platform = null;
7+
8+
console.info('hello null', platform);
9+
console.info(dEV);
10+
11+
export { platform };
12+
"
13+
`;
14+
15+
exports[`rollup > find string 1`] = `
416
"import 'node:process';
517
618
const platform = "darwin";
719
820
console.log('hello "darwin"', platform);
21+
console.log(DEV);
922
1023
export { platform };
1124
"

tests/__snapshots__/vite.test.ts.snap

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,5 +4,6 @@ exports[`vite 1`] = `
44
"import "node:process";
55
const platform = "darwin";
66
console.log('hello "darwin"', platform);
7+
console.log(DEV);
78
"
89
`;

tests/__snapshots__/webpack.test.ts.snap

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ const external_node_process_namespaceObject = require("node:process");
1515
const platform = "darwin"
1616
1717
console.log('hello "darwin"', platform)
18+
console.log(DEV)
1819
1920
/******/ })()
2021
;"

tests/esbuild.test.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,4 +17,6 @@ test('esbuild', async () => {
1717
],
1818
})
1919
expect(outputFiles[0].text).toMatchSnapshot()
20+
expect(outputFiles[0].text).not.contains('process.platform')
21+
expect(outputFiles[0].text).contains('"darwin"')
2022
})

tests/fixtures/main.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,4 @@ import process from 'node:process'
33
export const platform = process.platform
44

55
console.log('hello process.platform', platform)
6+
console.log(DEV)

tests/rollup.test.ts

Lines changed: 40 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,45 @@
11
import path from 'node:path'
2-
import { expect, test } from 'vitest'
2+
import { describe, expect, test } from 'vitest'
33
import { rollupBuild } from '@vue-macros/test-utils'
4+
45
import UnpluginReplace from '../src/rollup'
56

6-
test('rollup', async () => {
7-
const result = await rollupBuild(
8-
path.resolve(__dirname, 'fixtures/main.js'),
9-
[
10-
UnpluginReplace({
11-
'process.platform': '"darwin"',
12-
}),
13-
],
14-
)
15-
expect(result).toMatchSnapshot()
7+
describe('rollup', () => {
8+
test('find string', async () => {
9+
const result = await rollupBuild(
10+
path.resolve(__dirname, 'fixtures/main.js'),
11+
[
12+
UnpluginReplace({
13+
'process.platform': '"darwin"',
14+
}),
15+
],
16+
)
17+
expect(result).toMatchSnapshot()
18+
expect(result).not.contains('process.platform')
19+
expect(result).contains('"darwin"')
20+
})
21+
22+
test('find regexp', async () => {
23+
const result = await rollupBuild(
24+
path.resolve(__dirname, 'fixtures/main.js'),
25+
[
26+
UnpluginReplace({
27+
values: [
28+
{ find: /process\.\w+/g, replacement: 'null' },
29+
{
30+
find: /[A-Z]/,
31+
replacement: (id, match) => match[0].toLowerCase(),
32+
},
33+
{
34+
find: /console\.\w+/g,
35+
replacement: () => `console.info`,
36+
},
37+
],
38+
}),
39+
],
40+
)
41+
expect(result).toMatchSnapshot()
42+
expect(result).contains('console.info(dEV)')
43+
expect(result).contains(`console.info('hello null', platform)`)
44+
})
1645
})

tests/vite.test.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,4 +25,6 @@ test('vite', async () => {
2525
],
2626
})) as RollupOutput
2727
expect(output[0].code).toMatchSnapshot()
28+
expect(output[0].code).not.contains('process.platform')
29+
expect(output[0].code).contains('"darwin"')
2830
})

0 commit comments

Comments
 (0)