Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@ const dynamicModule = await import(/* webpackChunkName: "path-to-module" */ './p

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

> **Rspack:** This loader is compatible with Rspack as well. Use the same configuration shape in your `rspack.config.js`.

## Options

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

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'`.
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'`.

### `match`
**type**
Expand Down
6 changes: 6 additions & 0 deletions __tests__/loader.rspack.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import rspack from '@rspack/core'

globalThis.__MCL_BUNDLER__ = rspack
globalThis.__MCL_BUNDLER_NAME__ = 'rspack'

await import('./loader.spec.js')
6 changes: 4 additions & 2 deletions __tests__/loader.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,10 @@ const { dirname, resolve, basename, relative } = path
const filename = fileURLToPath(import.meta.url)
const directory = dirname(filename)
const loaderPath = resolve(directory, '../src/index.js')
const bundler = globalThis.__MCL_BUNDLER__ ?? webpack
const bundlerName = globalThis.__MCL_BUNDLER_NAME__ ?? 'webpack'
const build = (entry, config = { loader: loaderPath }) => {
const compiler = webpack({
const compiler = bundler({
mode: 'none',
context: directory,
entry: `./${entry}`,
Expand Down Expand Up @@ -51,7 +53,7 @@ const build = (entry, config = { loader: loaderPath }) => {
})
}

describe('loader', () => {
describe(`loader (${bundlerName})`, () => {
const entry = '__fixtures__/basic.js'

it('adds webpackChunkName magic comments', async () => {
Expand Down
235 changes: 233 additions & 2 deletions __tests__/parser.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,15 @@
import { parse } from '../src/parser.js'
import { jest } from '@jest/globals'

const loadParser = async parseSyncMock => {
jest.resetModules()
if (parseSyncMock) {
jest.unstable_mockModule('oxc-parser', () => ({ parseSync: parseSyncMock }))
}
return import('../src/parser.js')
}

describe('parse', () => {
it('parses 2023 ecmascript and jsx while tracking block comments', () => {
it('parses 2023 ecmascript and jsx while tracking block comments', async () => {
Copy link

Copilot AI Feb 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The test name says it "parses 2023 ecmascript", but src/parser.js no longer pins an ECMA version (it relies on oxc-parser defaults). Consider updating the test description to avoid implying a fixed ECMA version, or explicitly configuring the parser to the intended target.

Suggested change
it('parses 2023 ecmascript and jsx while tracking block comments', async () => {
it('parses modern ecmascript and jsx while tracking block comments', async () => {

Copilot uses AI. Check for mistakes.
const src = `
// inline comment
const Component = () => {
Expand All @@ -23,8 +31,231 @@ describe('parse', () => {
)
}
`
const { parse } = await loadParser()
const { astComments } = parse(src)

expect(astComments).toEqual([{ start: 175, end: 188, text: ' comment ' }])
})

it('throws on parser errors', async () => {
const parseSync = jest.fn().mockReturnValue({
program: { type: 'Program', body: [] },
comments: [],
errors: [{ message: 'boom' }]
})
const { parse } = await loadParser(parseSync)

expect(() => parse('bad')).toThrow('[oxc-parser] boom')
})

it('uses a generic message when error text is missing', async () => {
const parseSync = jest.fn().mockReturnValue({
program: { type: 'Program', body: [] },
comments: [],
errors: [{ message: 123 }]
})
const { parse } = await loadParser(parseSync)

expect(() => parse('bad')).toThrow('[oxc-parser] Parse error')
})

it('handles non-node programs', async () => {
const parseSync = jest.fn().mockReturnValue({
program: null,
comments: [],
errors: []
})
const { parse } = await loadParser(parseSync)
const result = parse('')

expect(result.importExpressionNodes).toEqual([])
})

it('normalizes span-based import expressions', async () => {
const parseSync = jest.fn().mockReturnValue({
program: {
type: 'Program',
body: [
{
type: 'ImportExpression',
span: { start: 1, end: 9 },
source: {
type: 'StringLiteral',
span: { start: 4, end: 8 }
}
}
]
},
comments: [],
errors: []
})
const { parse } = await loadParser(parseSync)
const result = parse('import("./x")')

expect(result.importExpressionNodes).toEqual([
expect.objectContaining({
start: 1,
end: 9,
source: expect.objectContaining({ start: 4, end: 8 })
})
])
})

it('handles non-object sources and missing spans', async () => {
const parseSync = jest.fn().mockReturnValue({
program: {
type: 'Program',
body: [
{
type: 'ImportExpression',
span: { start: 1, end: 9 },
source: 123
}
]
},
comments: [],
errors: []
})
const { parse } = await loadParser(parseSync)
const result = parse('import("./x")')

expect(result.importExpressionNodes).toEqual([])
})

it('adds start/end from spans when missing', async () => {
const parseSync = jest.fn().mockReturnValue({
program: {
type: 'Program',
body: [
{
type: 'ImportExpression',
span: { start: 10, end: 30 },
source: {
type: 'StringLiteral',
span: { start: 18, end: 28 }
}
}
]
},
comments: [],
errors: []
})
const { parse } = await loadParser(parseSync)
const result = parse('import("./x")')

expect(result.importExpressionNodes[0]).toEqual(
expect.objectContaining({
start: 10,
end: 30,
source: expect.objectContaining({ start: 18, end: 28 })
})
)
})

it('skips import expressions without source spans', async () => {
const parseSync = jest.fn().mockReturnValue({
program: {
type: 'Program',
body: [
{
type: 'ImportExpression',
span: { start: 1, end: 9 },
source: null
}
]
},
comments: [],
errors: []
})
const { parse } = await loadParser(parseSync)
const result = parse('import("./x")')

expect(result.importExpressionNodes).toEqual([])
})

it('filters non-block comments', async () => {
const parseSync = jest.fn().mockReturnValue({
program: { type: 'Program', body: [] },
comments: [{ type: 'Line', start: 1, end: 3, value: 'line' }],
errors: []
})
const { parse } = await loadParser(parseSync)
const result = parse('// comment')

expect(result.astComments).toEqual([])
})

it('reads block comment fields from kind/span/text', async () => {
const parseSync = jest.fn().mockReturnValue({
program: { type: 'Program', body: [] },
comments: [
{
kind: 'Block',
span: { start: 5, end: 9 },
text: ' block '
}
],
errors: []
})
const { parse } = await loadParser(parseSync)
const result = parse('/* block */')

expect(result.astComments).toEqual([{ start: 5, end: 9, text: ' block ' }])
})

it('falls back to comment content when text is missing', async () => {
const parseSync = jest.fn().mockReturnValue({
program: { type: 'Program', body: [] },
comments: [
{
type: 'Block',
start: 2,
end: 6,
content: ' content '
}
],
errors: []
})
const { parse } = await loadParser(parseSync)
const result = parse('/* content */')

expect(result.astComments).toEqual([{ start: 2, end: 6, text: ' content ' }])
})

it('uses comment value when present', async () => {
const parseSync = jest.fn().mockReturnValue({
program: { type: 'Program', body: [] },
comments: [
{
type: 'Block',
start: 3,
end: 7,
value: ' value '
}
],
errors: []
})
const { parse } = await loadParser(parseSync)
const result = parse('/* value */')

expect(result.astComments).toEqual([{ start: 3, end: 7, text: ' value ' }])
})

it('defaults to empty comment text when fields are missing', async () => {
const parseSync = jest.fn().mockReturnValue({
program: { type: 'Program', body: [] },
comments: [
{
type: 'Block',
start: 1,
end: 2
}
],
errors: []
})
const { parse } = await loadParser(parseSync)
const result = parse('/* */')

expect(result.astComments).toEqual([{ start: 1, end: 2, text: '' }])
})
})
13 changes: 2 additions & 11 deletions jest.config.spec.js
Original file line number Diff line number Diff line change
@@ -1,18 +1,9 @@
export default {
collectCoverage: true,
collectCoverageFrom: ['**/src/**/*.js', '!**/node_modules/**'],
coverageProvider: 'babel',
coverageProvider: 'v8',
coverageReporters: ['json', 'lcov', 'clover', 'text', 'text-summary'],
modulePathIgnorePatterns: ['dist'],
/**
* Use alternative runner to circumvent segmentation fault when
* webpack's node.js API uses dynamic imports while running
* jest's v8 vm context code.
*
* @see https://github.com/nodejs/node/issues/35889
* @see https://github.com/nodejs/node/issues/25424
*/
runner: 'jest-light-runner',
testMatch: ['**/__tests__/**/*.spec.js'],
testMatch: ['**/__tests__/**/*.spec.js', '**/__tests__/parser.js'],
Copy link

Copilot AI Feb 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

jest.config.spec.js now matches __tests__/parser.js, but that file is already included in the unit test suite (jest.config.js matches all non-*.spec.js tests). Since npm test runs both test:unit and test:spec, this causes the parser test suite to execute twice. Consider either renaming it to parser.spec.js (and excluding it from unit tests) or removing it from the spec testMatch.

Suggested change
testMatch: ['**/__tests__/**/*.spec.js', '**/__tests__/parser.js'],
testMatch: ['**/__tests__/**/*.spec.js'],

Copilot uses AI. Check for mistakes.
transform: {}
}
Loading
Loading