Skip to content

Commit 199da7e

Browse files
committed
fix(transform): use mlly to respect how users import lib
1 parent 45cba26 commit 199da7e

File tree

4 files changed

+90
-16
lines changed

4 files changed

+90
-16
lines changed

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
"dependencies": {
3636
"estree-walker": "^3.0.1",
3737
"magic-string": "^0.26.2",
38+
"mlly": "^0.5.4",
3839
"ufo": "^0.8.5",
3940
"unplugin": "^0.7.2"
4041
},

pnpm-lock.yaml

Lines changed: 17 additions & 7 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/transform.ts

Lines changed: 40 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { createContext, runInContext } from 'node:vm'
1+
import { Context, createContext, runInContext } from 'node:vm'
22
import { pathToFileURL } from 'node:url'
33

44
import { walk } from 'estree-walker'
@@ -7,14 +7,14 @@ import type { SimpleCallExpression } from 'estree'
77
import { createUnplugin } from 'unplugin'
88
import MagicString from 'magic-string'
99
import { parseURL, parseQuery } from 'ufo'
10+
import { findStaticImports, parseStaticImport } from 'mlly'
1011

1112
import * as magicRegExp from 'magic-regexp'
1213

1314
export const MagicRegExpTransformPlugin = createUnplugin(() => {
14-
const context = createContext(magicRegExp)
15-
1615
return {
1716
name: 'MagicRegExpTransformPlugin',
17+
enforce: 'post',
1818
transformInclude(id) {
1919
const { pathname, search } = parseURL(decodeURIComponent(pathToFileURL(id).href))
2020
const { type } = parseQuery(search)
@@ -30,14 +30,49 @@ export const MagicRegExpTransformPlugin = createUnplugin(() => {
3030
}
3131
},
3232
transform(code, id) {
33-
if (!code.includes('createRegExp')) return
33+
if (!code.includes('magic-regexp')) return
34+
35+
const statements = findStaticImports(code).filter(i => i.specifier === 'magic-regexp')
36+
if (!statements.length) return
37+
38+
const contextMap: Context = { ...magicRegExp }
39+
const wrapperNames = []
40+
let namespace: string
41+
42+
for (const i of statements.flatMap(i => parseStaticImport(i))) {
43+
if (i.namespacedImport) {
44+
namespace = i.namespacedImport
45+
contextMap[i.namespacedImport] = magicRegExp
46+
}
47+
if (i.namedImports) {
48+
for (const key in i.namedImports) {
49+
contextMap[i.namedImports[key]] = magicRegExp[key]
50+
}
51+
if (i.namedImports.createRegExp) {
52+
wrapperNames.push(i.namedImports.createRegExp)
53+
}
54+
}
55+
}
56+
57+
const context = createContext(contextMap)
3458

3559
const s = new MagicString(code)
3660

3761
walk(this.parse(code), {
3862
enter(node: SimpleCallExpression) {
3963
if (node.type !== 'CallExpression') return
40-
if ((node.callee as any).name !== 'createRegExp') return
64+
if (
65+
// Normal call
66+
!wrapperNames.includes((node.callee as any).name) &&
67+
// Namespaced call
68+
(node.callee.type !== 'MemberExpression' ||
69+
node.callee.object.type !== 'Identifier' ||
70+
node.callee.object.name !== namespace ||
71+
node.callee.property.type !== 'Identifier' ||
72+
node.callee.property.name !== 'createRegExp')
73+
) {
74+
return
75+
}
4176

4277
const { start, end } = node as any as { start: number; end: number }
4378

test/transform.test.ts

Lines changed: 32 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,32 +5,60 @@ import { MagicRegExpTransformPlugin } from '../src/transform'
55

66
describe('transformer', () => {
77
it('preserves context for dynamic regexps', () => {
8-
expect(transform(`console.log(createRegExp(anyOf(keys)))`)).not.toBeDefined()
8+
expect(
9+
transform([
10+
"import { createRegExp } from 'magic-regexp'",
11+
`console.log(createRegExp(anyOf(keys)))`,
12+
])
13+
).not.toBeDefined()
914
})
1015

1116
it('statically replaces regexps where possible', () => {
1217
const code = transform([
18+
"import { createRegExp, exactly, anyOf } from 'magic-regexp'",
19+
'//', // this lets us tree-shake the import for use in our test-suite
1320
"const re1 = createRegExp(exactly('bar').notBefore('foo'))",
1421
"const re2 = createRegExp(anyOf(exactly('bar'), 'foo'))",
1522
"const re3 = createRegExp('/foo/bar')",
1623
// This line will be double-escaped in the snapshot
1724
"re3.test('/foo/bar')",
1825
])
1926
expect(code).toMatchInlineSnapshot(`
20-
"const re1 = /bar(?!foo)/
27+
"import { createRegExp, exactly, anyOf } from 'magic-regexp'
28+
//
29+
const re1 = /bar(?!foo)/
2130
const re2 = /(bar|foo)/
2231
const re3 = /\\\\/foo\\\\/bar/
2332
re3.test('/foo/bar')"
2433
`)
2534
// ... but we test it here.
26-
expect(eval(code)).toMatchInlineSnapshot('true')
35+
expect(eval(code.split('//').pop())).toMatchInlineSnapshot('true')
36+
})
37+
38+
it('respects how users import library', () => {
39+
const code = transform([
40+
"import { createRegExp as cRE } from 'magic-regexp'",
41+
'import { exactly as ext, createRegExp } from "magic-regexp"',
42+
'import * as magicRE from "magic-regexp"',
43+
"const re1 = cRE(ext('bar').notBefore('foo'))",
44+
"const re2 = magicRE.createRegExp(magicRE.anyOf('bar', 'foo'))",
45+
"const re3 = createRegExp('test/value')",
46+
])
47+
expect(code).toMatchInlineSnapshot(`
48+
"import { createRegExp as cRE } from 'magic-regexp'
49+
import { exactly as ext, createRegExp } from \\"magic-regexp\\"
50+
import * as magicRE from \\"magic-regexp\\"
51+
const re1 = /bar(?!foo)/
52+
const re2 = /(bar|foo)/
53+
const re3 = /test\\\\/value/"
54+
`)
2755
})
2856
})
2957

3058
const transform = (code: string | string[]) => {
3159
const plugin = MagicRegExpTransformPlugin.vite()
3260
return plugin.transform.call(
33-
{ parse: (code: string) => parse(code, { ecmaVersion: 2022 }) },
61+
{ parse: (code: string) => parse(code, { ecmaVersion: 2022, sourceType: 'module' }) },
3462
Array.isArray(code) ? code.join('\n') : code,
3563
'some-id.js'
3664
)?.code

0 commit comments

Comments
 (0)