Skip to content

Commit a625a4c

Browse files
authored
fix(deps): check dynamic imports in onlyImport (#1019)
1 parent 8d80e30 commit a625a4c

5 files changed

Lines changed: 58 additions & 16 deletions

File tree

docs/options/dependencies.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,7 @@ In this example, the output is only allowed to import `cac`. If any chunk import
107107
- Type declaration output (`.d.ts`) is checked as well.
108108

109109
::: warning
110-
Only ESM output is checked. CJS output (`require` calls) is not detected.
110+
ES imports and dynamic `import()` expressions are checked. CJS `require()` calls are not detected.
111111
:::
112112

113113
### `deps.neverBundle`

docs/zh-CN/options/dependencies.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,7 @@ export default defineConfig({
107107
- 类型声明产物(`.d.ts`)同样会被检查。
108108

109109
::: warning
110-
仅检查 ESM 产物。CJS 产物(`require` 调用)不会被检测
110+
会检查 ES 导入和动态 `import()` 表达式。CJS `require()` 调用不会被检测
111111
:::
112112

113113
### `deps.neverBundle`

skills/tsdown/references/option-dependencies.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -119,7 +119,7 @@ export default defineConfig({
119119
- Relative imports between code-split chunks are always allowed.
120120
- Declaration output (`.d.ts`) is checked too.
121121

122-
**Limitation:** Only ESM output is checked. CJS output (`require` calls) is not detected.
122+
**Limitation:** ES imports and dynamic `import()` expressions are checked. CJS `require()` calls are not detected.
123123

124124
### `deps.skipNodeModulesBundle`
125125

src/features/deps.ts

Lines changed: 39 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { blue, underline, yellow } from 'ansis'
55
import { createDebug } from 'obug'
66
import { RE_DTS, RE_NODE_MODULES } from 'rolldown-plugin-dts/internal'
77
import { and, id, importerId, include } from 'rolldown/filter'
8-
import { parse } from 'rolldown/utils'
8+
import { parse, Visitor, type ESTree } from 'rolldown/utils'
99
import {
1010
matchPattern,
1111
resolveRegex,
@@ -56,7 +56,8 @@ export interface DepsConfig {
5656
* Node built-in modules are always allowed to be imported
5757
* when `platform` is `node`.
5858
*
59-
* Note: Only ESM output is checked. CJS output (`require` calls) is not detected.
59+
* Note: ES imports and dynamic import expressions are checked. CJS
60+
* `require` calls are not detected.
6061
*/
6162
onlyImport?: Arrayable<string | RegExp>
6263
/**
@@ -273,18 +274,9 @@ export function DepsPlugin(
273274
moduleIds.some((id) => chunk.code.includes(id))
274275
) {
275276
const { program } = await parse(chunk.fileName, chunk.code)
276-
for (const stmt of program.body) {
277-
if (
278-
stmt.type !== 'ImportDeclaration' &&
279-
stmt.type !== 'ExportAllDeclaration' &&
280-
stmt.type !== 'ExportNamedDeclaration'
281-
) {
282-
continue
283-
}
284-
const source = stmt.source?.value
285-
277+
for (const source of collectImportSources(program)) {
286278
// relative imports of sibling chunks emitted by code splitting
287-
if (!source || source[0] === '.') continue
279+
if (source[0] === '.') continue
288280
if (platform === 'node' && isBuiltin(source)) continue
289281
if (matchPattern(parsePackageSpecifier(source)[0], onlyImport)) {
290282
continue
@@ -421,6 +413,40 @@ export function DepsPlugin(
421413
}
422414
}
423415

416+
function collectImportSources(program: ESTree.Program): string[] {
417+
const sources: string[] = []
418+
419+
new Visitor({
420+
ImportDeclaration(node) {
421+
sources.push(node.source.value)
422+
},
423+
ExportAllDeclaration(node) {
424+
sources.push(node.source.value)
425+
},
426+
ExportNamedDeclaration(node) {
427+
if (node.source) sources.push(node.source.value)
428+
},
429+
ImportExpression(node) {
430+
const source = getStaticString(node.source)
431+
if (source) sources.push(source)
432+
},
433+
}).visit(program)
434+
435+
return sources
436+
}
437+
438+
function getStaticString(value: ESTree.Expression): string | undefined {
439+
if (value.type === 'Literal' && typeof value.value === 'string') {
440+
return value.value
441+
}
442+
443+
if (value.type !== 'TemplateLiteral') return
444+
if (value.expressions.length || value.quasis.length !== 1) return
445+
446+
const { cooked, raw } = value.quasis[0].value
447+
return cooked ?? raw
448+
}
449+
424450
export function parsePackageSpecifier(
425451
id: string,
426452
): [name: string, subpath: string] {

tests/e2e.test.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -357,6 +357,22 @@ describe('deps', () => {
357357
).rejects.toThrow('but is not included in')
358358
})
359359

360+
test('should throw error for unlisted dynamic imports', async (context) => {
361+
const files = {
362+
'index.ts': `export const load = (): Promise<unknown> => import('cac')`,
363+
}
364+
await expect(() =>
365+
testBuild({
366+
context,
367+
files,
368+
options: {
369+
deps: { onlyImport: [] },
370+
plugins: [pluginMockDepCode],
371+
},
372+
}),
373+
).rejects.toThrow('but is not included in')
374+
})
375+
360376
test('should always allow node builtin modules when platform is node', async (context) => {
361377
const files = {
362378
'index.ts': `import path from 'node:path'

0 commit comments

Comments
 (0)