Skip to content

Commit 9876859

Browse files
committed
refactor: switch to oxc-parser.
1 parent a71b9ad commit 9876859

File tree

8 files changed

+1006
-177
lines changed

8 files changed

+1006
-177
lines changed

README.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,8 @@ const dynamicModule = await import(/* webpackChunkName: "path-to-module" */ './p
4141

4242
The `webpackChunkName` comment is added by default when registering the loader. See the supported [options](#options) to learn about configuring other magic comments.
4343

44+
> **Rspack:** This loader is compatible with Rspack as well. Use the same configuration shape in your `rspack.config.js`.
45+
4446
## Options
4547

4648
* [`verbose`](#verbose)
@@ -65,7 +67,7 @@ Prints console statements of the module filepath and updated `import()` during t
6567
```
6668
**default** `'parser'`
6769

68-
Sets how the loader finds dynamic import expressions in your source code, either using an [ECMAScript parser](https://github.com/acornjs/acorn), or a regular expression. Your mileage may vary when using `'regexp'`.
70+
Sets how the loader finds dynamic import expressions in your source code, either using an [ECMAScript parser](https://github.com/oxc-project/oxc/tree/main/crates/oxc_parser) (oxc-parser), or a regular expression. Your mileage may vary when using `'regexp'`.
6971

7072
### `match`
7173
**type**

__tests__/loader.rspack.spec.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import rspack from '@rspack/core'
2+
3+
globalThis.__MCL_BUNDLER__ = rspack
4+
globalThis.__MCL_BUNDLER_NAME__ = 'rspack'
5+
6+
await import('./loader.spec.js')

__tests__/loader.spec.js

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,10 @@ const { dirname, resolve, basename, relative } = path
88
const filename = fileURLToPath(import.meta.url)
99
const directory = dirname(filename)
1010
const loaderPath = resolve(directory, '../src/index.js')
11+
const bundler = globalThis.__MCL_BUNDLER__ ?? webpack
12+
const bundlerName = globalThis.__MCL_BUNDLER_NAME__ ?? 'webpack'
1113
const build = (entry, config = { loader: loaderPath }) => {
12-
const compiler = webpack({
14+
const compiler = bundler({
1315
mode: 'none',
1416
context: directory,
1517
entry: `./${entry}`,
@@ -51,7 +53,7 @@ const build = (entry, config = { loader: loaderPath }) => {
5153
})
5254
}
5355

54-
describe('loader', () => {
56+
describe(`loader (${bundlerName})`, () => {
5557
const entry = '__fixtures__/basic.js'
5658

5759
it('adds webpackChunkName magic comments', async () => {

__tests__/parser.js

Lines changed: 233 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,15 @@
1-
import { parse } from '../src/parser.js'
1+
import { jest } from '@jest/globals'
2+
3+
const loadParser = async parseSyncMock => {
4+
jest.resetModules()
5+
if (parseSyncMock) {
6+
jest.unstable_mockModule('oxc-parser', () => ({ parseSync: parseSyncMock }))
7+
}
8+
return import('../src/parser.js')
9+
}
210

311
describe('parse', () => {
4-
it('parses 2023 ecmascript and jsx while tracking block comments', () => {
12+
it('parses 2023 ecmascript and jsx while tracking block comments', async () => {
513
const src = `
614
// inline comment
715
const Component = () => {
@@ -23,8 +31,231 @@ describe('parse', () => {
2331
)
2432
}
2533
`
34+
const { parse } = await loadParser()
2635
const { astComments } = parse(src)
2736

2837
expect(astComments).toEqual([{ start: 175, end: 188, text: ' comment ' }])
2938
})
39+
40+
it('throws on parser errors', async () => {
41+
const parseSync = jest.fn().mockReturnValue({
42+
program: { type: 'Program', body: [] },
43+
comments: [],
44+
errors: [{ message: 'boom' }]
45+
})
46+
const { parse } = await loadParser(parseSync)
47+
48+
expect(() => parse('bad')).toThrow('[oxc-parser] boom')
49+
})
50+
51+
it('uses a generic message when error text is missing', async () => {
52+
const parseSync = jest.fn().mockReturnValue({
53+
program: { type: 'Program', body: [] },
54+
comments: [],
55+
errors: [{ message: 123 }]
56+
})
57+
const { parse } = await loadParser(parseSync)
58+
59+
expect(() => parse('bad')).toThrow('[oxc-parser] Parse error')
60+
})
61+
62+
it('handles non-node programs', async () => {
63+
const parseSync = jest.fn().mockReturnValue({
64+
program: null,
65+
comments: [],
66+
errors: []
67+
})
68+
const { parse } = await loadParser(parseSync)
69+
const result = parse('')
70+
71+
expect(result.importExpressionNodes).toEqual([])
72+
})
73+
74+
it('normalizes span-based import expressions', async () => {
75+
const parseSync = jest.fn().mockReturnValue({
76+
program: {
77+
type: 'Program',
78+
body: [
79+
{
80+
type: 'ImportExpression',
81+
span: { start: 1, end: 9 },
82+
source: {
83+
type: 'StringLiteral',
84+
span: { start: 4, end: 8 }
85+
}
86+
}
87+
]
88+
},
89+
comments: [],
90+
errors: []
91+
})
92+
const { parse } = await loadParser(parseSync)
93+
const result = parse('import("./x")')
94+
95+
expect(result.importExpressionNodes).toEqual([
96+
expect.objectContaining({
97+
start: 1,
98+
end: 9,
99+
source: expect.objectContaining({ start: 4, end: 8 })
100+
})
101+
])
102+
})
103+
104+
it('handles non-object sources and missing spans', async () => {
105+
const parseSync = jest.fn().mockReturnValue({
106+
program: {
107+
type: 'Program',
108+
body: [
109+
{
110+
type: 'ImportExpression',
111+
span: { start: 1, end: 9 },
112+
source: 123
113+
}
114+
]
115+
},
116+
comments: [],
117+
errors: []
118+
})
119+
const { parse } = await loadParser(parseSync)
120+
const result = parse('import("./x")')
121+
122+
expect(result.importExpressionNodes).toEqual([])
123+
})
124+
125+
it('adds start/end from spans when missing', async () => {
126+
const parseSync = jest.fn().mockReturnValue({
127+
program: {
128+
type: 'Program',
129+
body: [
130+
{
131+
type: 'ImportExpression',
132+
span: { start: 10, end: 30 },
133+
source: {
134+
type: 'StringLiteral',
135+
span: { start: 18, end: 28 }
136+
}
137+
}
138+
]
139+
},
140+
comments: [],
141+
errors: []
142+
})
143+
const { parse } = await loadParser(parseSync)
144+
const result = parse('import("./x")')
145+
146+
expect(result.importExpressionNodes[0]).toEqual(
147+
expect.objectContaining({
148+
start: 10,
149+
end: 30,
150+
source: expect.objectContaining({ start: 18, end: 28 })
151+
})
152+
)
153+
})
154+
155+
it('skips import expressions without source spans', async () => {
156+
const parseSync = jest.fn().mockReturnValue({
157+
program: {
158+
type: 'Program',
159+
body: [
160+
{
161+
type: 'ImportExpression',
162+
span: { start: 1, end: 9 },
163+
source: null
164+
}
165+
]
166+
},
167+
comments: [],
168+
errors: []
169+
})
170+
const { parse } = await loadParser(parseSync)
171+
const result = parse('import("./x")')
172+
173+
expect(result.importExpressionNodes).toEqual([])
174+
})
175+
176+
it('filters non-block comments', async () => {
177+
const parseSync = jest.fn().mockReturnValue({
178+
program: { type: 'Program', body: [] },
179+
comments: [{ type: 'Line', start: 1, end: 3, value: 'line' }],
180+
errors: []
181+
})
182+
const { parse } = await loadParser(parseSync)
183+
const result = parse('// comment')
184+
185+
expect(result.astComments).toEqual([])
186+
})
187+
188+
it('reads block comment fields from kind/span/text', async () => {
189+
const parseSync = jest.fn().mockReturnValue({
190+
program: { type: 'Program', body: [] },
191+
comments: [
192+
{
193+
kind: 'Block',
194+
span: { start: 5, end: 9 },
195+
text: ' block '
196+
}
197+
],
198+
errors: []
199+
})
200+
const { parse } = await loadParser(parseSync)
201+
const result = parse('/* block */')
202+
203+
expect(result.astComments).toEqual([{ start: 5, end: 9, text: ' block ' }])
204+
})
205+
206+
it('falls back to comment content when text is missing', async () => {
207+
const parseSync = jest.fn().mockReturnValue({
208+
program: { type: 'Program', body: [] },
209+
comments: [
210+
{
211+
type: 'Block',
212+
start: 2,
213+
end: 6,
214+
content: ' content '
215+
}
216+
],
217+
errors: []
218+
})
219+
const { parse } = await loadParser(parseSync)
220+
const result = parse('/* content */')
221+
222+
expect(result.astComments).toEqual([{ start: 2, end: 6, text: ' content ' }])
223+
})
224+
225+
it('uses comment value when present', async () => {
226+
const parseSync = jest.fn().mockReturnValue({
227+
program: { type: 'Program', body: [] },
228+
comments: [
229+
{
230+
type: 'Block',
231+
start: 3,
232+
end: 7,
233+
value: ' value '
234+
}
235+
],
236+
errors: []
237+
})
238+
const { parse } = await loadParser(parseSync)
239+
const result = parse('/* value */')
240+
241+
expect(result.astComments).toEqual([{ start: 3, end: 7, text: ' value ' }])
242+
})
243+
244+
it('defaults to empty comment text when fields are missing', async () => {
245+
const parseSync = jest.fn().mockReturnValue({
246+
program: { type: 'Program', body: [] },
247+
comments: [
248+
{
249+
type: 'Block',
250+
start: 1,
251+
end: 2
252+
}
253+
],
254+
errors: []
255+
})
256+
const { parse } = await loadParser(parseSync)
257+
const result = parse('/* */')
258+
259+
expect(result.astComments).toEqual([{ start: 1, end: 2, text: '' }])
260+
})
30261
})

jest.config.spec.js

Lines changed: 2 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,9 @@
11
export default {
22
collectCoverage: true,
33
collectCoverageFrom: ['**/src/**/*.js', '!**/node_modules/**'],
4-
coverageProvider: 'babel',
4+
coverageProvider: 'v8',
55
coverageReporters: ['json', 'lcov', 'clover', 'text', 'text-summary'],
66
modulePathIgnorePatterns: ['dist'],
7-
/**
8-
* Use alternative runner to circumvent segmentation fault when
9-
* webpack's node.js API uses dynamic imports while running
10-
* jest's v8 vm context code.
11-
*
12-
* @see https://github.com/nodejs/node/issues/35889
13-
* @see https://github.com/nodejs/node/issues/25424
14-
*/
15-
runner: 'jest-light-runner',
16-
testMatch: ['**/__tests__/**/*.spec.js'],
7+
testMatch: ['**/__tests__/**/*.spec.js', '**/__tests__/parser.js'],
178
transform: {}
189
}

0 commit comments

Comments
 (0)