Skip to content

Commit

Permalink
feat: support dynamic imports with comments.
Browse files Browse the repository at this point in the history
  • Loading branch information
morganney committed Jan 28, 2024
1 parent a13e3f6 commit daf1c48
Show file tree
Hide file tree
Showing 13 changed files with 228 additions and 37 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ jobs:
- name: Test
run: npm test
- name: Report Coverage
uses: codecov/[email protected].4
uses: codecov/[email protected].5
with:
token: ${{ secrets.CODECOV_TOKEN }}
- name: Lint
Expand Down
1 change: 1 addition & 0 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,3 +33,4 @@ jobs:
uses: JS-DevTools/[email protected]
with:
token: ${{ secrets.NPM_AUTH_TOKEN }}
tag: ${{ contains(github.ref, '-') && 'next' || 'latest' }}
13 changes: 13 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ The `webpackChunkName` comment is added by default when registering the loader.
* [`verbose`](#verbose)
* [`mode`](#mode)
* [`match`](#match)
* [`comments`](#comments)
* `[magicCommentName: string]: MagicCommentValue` see `magic-comments` [options](https://github.com/morganney/magic-comments#options) for details

### `verbose`
Expand Down Expand Up @@ -75,6 +76,18 @@ Sets how the loader finds dynamic import expressions in your source code, either

Sets how globs are matched, either the module file path, or the `import()` specifier.

### `comments`
**type**
```ts
'ignore' | 'prepend' | 'append' | 'replace'
| (cmts: Array<{ start: number; end: number; text: string }>, magicComment: string) => string
```
**default** `'ignore'`

_Note, this option only considers block comments that precede the dynamic imports specifier, and any comments coming after are ignored and left intact._

Sets how dynamic imports with block comments are handled. If `ignore` is used, then it will be skipped and no magic comments from your configuration will be applied. If `replace` is used, then all found comments will be replaced with the magic comments. `append` and `prepend` add the magic comments before, or after the found comments, respectively. If a function is used it will be passed the found comments, and the magic comment string that is to be applied. The return value has the same effect as `replace`.

## Examples

Below are examples for some of the supported magic comments. Consult the [loader specification](https://github.com/morganney/magic-comments-loader/blob/main/__tests__/loader.spec.js) for a comprehensive usage example.
Expand Down
7 changes: 7 additions & 0 deletions __tests__/__fixtures__/commented.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import(
/* webpackChunkNames: "test-chunk" */
/* something else */
/* webpackFetchPriority: "high" */
'./folder/module.js'
/* after the specifier */
)
6 changes: 4 additions & 2 deletions __tests__/formatter.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@ describe('format', () => {
match: 'module',
source: src,
filepath: 'src/module.js',
comments: [{ start: openLen, end: openLen + commentLen, commentText: ' comment ' }],
comments: 'ignore',
astComments: [{ start: openLen, end: openLen + commentLen, text: ' comment ' }],
magicCommentOptions: { webpackChunkName: true },
importExpressionNodes: [
{
Expand Down Expand Up @@ -38,7 +39,8 @@ describe('format', () => {
match: 'module',
source: src,
filepath: 'src/module.js',
comments: [],
comments: 'ignore',
astComments: [],
magicCommentOptions: { webpackMode: () => 'invalid' },
importExpressionNodes: [
{
Expand Down
120 changes: 120 additions & 0 deletions __tests__/loader.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -1080,4 +1080,124 @@ describe('loader', () => {

expect(output).toEqual(expect.stringContaining("import('./folder/module.js')"))
})

it('updates imports with comments based on configuration', async () => {
const entry = '__fixtures__/commented.js'
let stats = await build(entry, {
use: {
loader: loaderPath,
options: {
comments: 'replace',
webpackChunkName: true
}
}
})
let output = stats.toJson({ source: true }).modules[0].source

expect(output).toMatch(/webpackChunkName: "folder-module"/)
expect(output).not.toMatch(/something else/)

stats = await build(entry, {
use: {
loader: loaderPath,
options: {
comments: 'prepend',
webpackExports: () => ['a']
}
}
})
output = stats.toJson({ source: true }).modules[0].source

expect(output).toMatch(/webpackExports: \["a"\]/)
expect(output).toMatch('webpackChunkNames: "test-chunk"')
expect(output.indexOf('webpackExports') < output.indexOf('webpackChunkName')).toBe(
true
)

stats = await build(entry, {
use: {
loader: loaderPath,
options: {
comments: 'append',
webpackExports: () => ['b']
}
}
})
output = stats.toJson({ source: true }).modules[0].source

expect(output).toMatch(/webpackExports: \["b"\]/)
expect(output).toMatch('webpackFetchPriority: "high"')
expect(
output.indexOf('webpackExports') > output.indexOf('webpackFetchPriority')
).toBe(true)

let firstCmt = ''
let magicCmt = ''

stats = await build(entry, {
use: {
loader: loaderPath,
options: {
comments: (cmts, magicComment) => {
firstCmt = cmts[0]
magicCmt = magicComment
return `${magicComment} /* ${cmts[0].text} */`
},
webpackExports: () => ['c']
}
}
})
output = stats.toJson({ source: true }).modules[0].source

expect(output).toMatch(/webpackExports: \["c"\]/)
expect(output).toMatch(firstCmt.text)
expect(output.indexOf(firstCmt.text) > output.indexOf(magicCmt)).toBe(true)

stats = await build(entry, {
use: {
loader: loaderPath,
options: {
comments: () => {
return 123
},
webpackExports: () => ['c']
}
}
})
output = stats.toJson({ source: true }).modules[0].source

// Return values other than strings result in no changes
expect(output).not.toMatch(/webpackExports: \["c"\]/)
expect(output).toMatch(firstCmt.text)

stats = await build(entry, {
use: {
loader: loaderPath,
options: {
comments: 'prepend',
webpackExports: {
options: {
exports: () => ['c']
},
overrides: [
{
files: '**/*.js',
options: {
exports: () => ['d']
}
}
]
}
}
}
})
output = stats.toJson({ source: true }).modules[0].source

// The `comments` option Should work with overrides too.
expect(output).toMatch(/webpackExports: \["d"\]/)
expect(output).toMatch('webpackChunkNames: "test-chunk"')
expect(output.indexOf('webpackExports') < output.indexOf('webpackChunkName')).toBe(
true
)
})
})
4 changes: 2 additions & 2 deletions __tests__/parser.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,8 @@ describe('parse', () => {
)
}
`
const { comments } = parse(src)
const { astComments } = parse(src)

expect(comments).toEqual([{ start: 175, end: 188, commentText: ' comment ' }])
expect(astComments).toEqual([{ start: 175, end: 188, text: ' comment ' }])
})
})
12 changes: 6 additions & 6 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "magic-comments-loader",
"version": "2.0.5",
"version": "2.1.0",
"description": "Add webpack magic comments to your dynamic imports at build time.",
"main": "dist",
"type": "module",
Expand Down Expand Up @@ -68,7 +68,7 @@
"magic-comments": "^2.1.12",
"magic-string": "^0.30.0",
"micromatch": "^4.0.4",
"schema-utils": "^4.1.0"
"schema-utils": "^4.2.0"
},
"prettier": {
"printWidth": 90,
Expand Down
64 changes: 45 additions & 19 deletions src/formatter.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,27 +4,25 @@ import MagicString from 'magic-string'
const format = ({
match,
source,
filepath,
comments,
filepath,
astComments,
magicCommentOptions,
importExpressionNodes
}) => {
const magicImports = []
const cmts = [...comments]
const src = new MagicString(source)
const hasComment = node => {
const idx = cmts.findIndex(cmt => cmt.start > node.start && cmt.end < node.end)
const wasFound = idx > -1

if (wasFound) {
cmts.splice(idx, 1)
}

return wasFound
const getComments = node => {
// This ignores comments that come after the imports specifier.
return astComments.filter(
cmt => cmt.start > node.start && cmt.end < node.end && cmt.start < node.source.end
)
}

for (const node of importExpressionNodes) {
if (!hasComment(node)) {
const cmts = getComments(node)

if (!cmts.length || comments !== 'ignore') {
const specifier = source.substring(node.source.start, node.source.end)
const magicComment = getMagicComment({
match,
Expand All @@ -34,13 +32,41 @@ const format = ({
})

if (magicComment) {
magicImports.push(
src
.snip(node.start, node.end)
.toString()
.replace(specifier, `${magicComment} ${specifier}`)
)
src.appendLeft(node.source.start, `${magicComment} `)
const clone = src.snip(node.start, node.end)

if (!cmts.length) {
magicImports.push(
clone.toString().replace(specifier, `${magicComment} ${specifier}`)
)
src.appendLeft(node.source.start, `${magicComment} `)
} else {
/**
* Get the minimum start and maximum end.
* Assumption is that comment nodes are sorted
* in ascending order of `node.start`.
*/
const minStart = cmts[0].start
const maxEnd = cmts[cmts.length - 1].end

if (comments === 'replace') {
magicImports.push(clone.overwrite(minStart, maxEnd, magicComment).toString())
src.overwrite(minStart, maxEnd, magicComment)
} else if (comments === 'append') {
magicImports.push(clone.appendRight(maxEnd, ` ${magicComment}`).toString())
src.appendRight(maxEnd, ` ${magicComment}`)
} else if (comments === 'prepend') {
magicImports.push(clone.prependLeft(minStart, `${magicComment} `).toString())
src.prependLeft(minStart, `${magicComment} `)
} else {
// Has to be a function or the schema validator is broken
const replacement = comments(cmts, magicComment)

if (typeof replacement === 'string') {
magicImports.push(clone.overwrite(minStart, maxEnd, replacement).toString())
src.overwrite(minStart, maxEnd, replacement)
}
}
}
}
}
}
Expand Down
9 changes: 8 additions & 1 deletion src/loader.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,13 @@ const loader = function (source) {
name: 'magic-comments-loader'
})

const { mode = 'parser', match = 'module', verbose = false, ...rest } = options
const {
mode = 'parser',
match = 'module',
comments = 'ignore',
verbose = false,
...rest
} = options
const magicCommentOptions = Object.keys(rest).length ? rest : { webpackChunkName: true }
const filepath = this.resourcePath

Expand All @@ -26,6 +32,7 @@ const loader = function (source) {
...parse(source),
match,
filepath,
comments,
magicCommentOptions
})

Expand Down
8 changes: 4 additions & 4 deletions src/parser.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ extend(base)

const jsxParser = Parser.extend(jsx())
const parse = source => {
const comments = []
const astComments = []
const importExpressionNodes = []
const ast = jsxParser.parse(source, {
locations: false,
Expand All @@ -25,9 +25,9 @@ const parse = source => {
allowAwaitOutsideFunction: true,
allowReturnOutsideFunction: true,
allowImportExportEverywhere: true,
onComment: (isBlock, commentText, start, end) => {
onComment: (isBlock, text, start, end) => {
if (isBlock) {
comments.push({ start, end, commentText })
astComments.push({ start, end, text })
}
}
})
Expand All @@ -38,7 +38,7 @@ const parse = source => {
}
})

return { ast, comments, importExpressionNodes, source }
return { ast, astComments, importExpressionNodes, source }
}

export { parse }
Loading

0 comments on commit daf1c48

Please sign in to comment.