fix(webworker): scope createRequire dependency discovery

This commit is contained in:
imccyu
2026-08-26 15:09:49 +08:00
parent 437ab3cefe
commit b72f5879c8
2 changed files with 70 additions and 31 deletions
@@ -71,7 +71,6 @@ class Transformer {
private readonly moduleRequests = new Set<string>() private readonly moduleRequests = new Set<string>()
private readonly metaResolveRequests = new Set<string>() private readonly metaResolveRequests = new Set<string>()
private readonly createRequireBindings = new Set<string>() private readonly createRequireBindings = new Set<string>()
private readonly requireBindings = new Set<string>(['require'])
constructor(source: string, private readonly path: string) { constructor(source: string, private readonly path: string) {
// A `#!` line is only legal at offset zero, and the prologue takes that spot; // A `#!` line is only legal at offset zero, and the prologue takes that spot;
@@ -157,12 +156,6 @@ class Transformer {
this.fail('import attributes are not supported', node.start) this.fail('import attributes are not supported', node.start)
} }
const source = node.source as Node const source = node.source as Node
if (source.value === 'node:module' || source.value === 'module') {
for (const specifier of node.specifiers as Node[]) {
if (specifier.type !== 'ImportSpecifier' || nameOf(specifier.imported as Node) !== 'createRequire') continue
this.createRequireBindings.add(nameOf(specifier.local as Node))
}
}
const request = `require(${this.literal(source)})` const request = `require(${this.literal(source)})`
const specifiers = node.specifiers as Node[] const specifiers = node.specifiers as Node[]
if (specifiers.length === 0) { if (specifiers.length === 0) {
@@ -322,7 +315,12 @@ class Transformer {
// --- traversal ------------------------------------------------------------ // --- traversal ------------------------------------------------------------
private visit(node: unknown, context: { asyncGenerator: boolean; functionDepth: number; statement?: Node }): void { private visit(node: unknown, context: {
asyncGenerator: boolean
functionDepth: number
moduleScope: boolean
statement?: Node
}): void {
if (node === null || typeof node !== 'object') return if (node === null || typeof node !== 'object') return
if (Array.isArray(node)) { if (Array.isArray(node)) {
for (const child of node) this.visit(child, context) for (const child of node) this.visit(child, context)
@@ -346,21 +344,13 @@ class Transformer {
if (argument !== undefined && typeof argument.value === 'string') this.moduleRequests.add(argument.value) if (argument !== undefined && typeof argument.value === 'string') this.moduleRequests.add(argument.value)
break break
} }
case 'VariableDeclarator': {
const id = record.id as Node
const init = record.init as Node | null
if (id.type === 'Identifier' && init !== null && this.isCreateRequireCall(init)) {
this.requireBindings.add(nameOf(id))
}
break
}
case 'CallExpression': { case 'CallExpression': {
// CommonJS bodies pass through untransformed, but literal calls through // CommonJS bodies pass through untransformed, but literal calls through
// the wrapper's `require` or Node's `createRequire` are module requests // the wrapper's `require` remain module requests. The ESM case accepts
// all the same. // only a direct module-scope createRequire call with the importer URL.
const callee = record.callee as Node const callee = record.callee as Node
const callArguments = record.arguments as Node[] const callArguments = record.arguments as Node[]
if (this.isRequireCall(callee) && callArguments.length === 1 if (this.isRequireCall(callee, context.moduleScope) && callArguments.length === 1
&& typeof callArguments[0]?.value === 'string') { && typeof callArguments[0]?.value === 'string') {
this.moduleRequests.add(callArguments[0].value) this.moduleRequests.add(callArguments[0].value)
} }
@@ -400,6 +390,7 @@ class Transformer {
if (context.functionDepth === 0) this.fail('a top-level for-await loop cannot run as CommonJS', record.start) if (context.functionDepth === 0) this.fail('a top-level for-await loop cannot run as CommonJS', record.start)
this.forAwait(record) this.forAwait(record)
} }
next = { ...next, moduleScope: false }
break break
case 'LabeledStatement': { case 'LabeledStatement': {
const body = record.body as Node const body = record.body as Node
@@ -417,8 +408,17 @@ class Transformer {
next = { next = {
asyncGenerator: record.async === true && record.generator === true, asyncGenerator: record.async === true && record.generator === true,
functionDepth: context.functionDepth + 1, functionDepth: context.functionDepth + 1,
moduleScope: false,
} }
break break
case 'BlockStatement':
case 'CatchClause':
case 'ClassBody':
case 'ForStatement':
case 'ForInStatement':
case 'SwitchStatement':
next = { ...next, moduleScope: false }
break
default: break default: break
} }
if (record.type === 'ExpressionStatement') next = { ...next, statement: record } if (record.type === 'ExpressionStatement') next = { ...next, statement: record }
@@ -431,12 +431,35 @@ class Transformer {
private isCreateRequireCall(node: Node): boolean { private isCreateRequireCall(node: Node): boolean {
if (node.type !== 'CallExpression') return false if (node.type !== 'CallExpression') return false
const callee = node.callee as Node const callee = node.callee as Node
return callee.type === 'Identifier' && this.createRequireBindings.has(nameOf(callee)) const args = node.arguments as Node[]
if (callee.type !== 'Identifier' || !this.createRequireBindings.has(nameOf(callee)) || args.length !== 1) {
return false
}
const base = args[0] as Node
if (base.type !== 'MemberExpression' || base.computed === true) return false
const object = base.object as Node
const property = base.property as Node
return object.type === 'MetaProperty'
&& (object.meta as Node).name === 'import'
&& property.type === 'Identifier'
&& property.name === 'url'
} }
private isRequireCall(callee: Node): boolean { private isRequireCall(callee: Node, moduleScope: boolean): boolean {
return (callee.type === 'Identifier' && this.requireBindings.has(nameOf(callee))) return (callee.type === 'Identifier' && callee.name === 'require')
|| this.isCreateRequireCall(callee) || (moduleScope && this.isCreateRequireCall(callee))
}
private indexCreateRequireImports(program: Node): void {
for (const statement of program.body as Node[]) {
if (statement.type !== 'ImportDeclaration') continue
const source = statement.source as Node
if (source.value !== 'node:module' && source.value !== 'module') continue
for (const specifier of statement.specifiers as Node[]) {
if (specifier.type !== 'ImportSpecifier' || nameOf(specifier.imported as Node) !== 'createRequire') continue
this.createRequireBindings.add(nameOf(specifier.local as Node))
}
}
} }
run(): string { run(): string {
@@ -456,7 +479,8 @@ class Transformer {
} catch (reason) { } catch (reason) {
this.fail(`parse failed: ${(reason as Error).message}`, 0) this.fail(`parse failed: ${(reason as Error).message}`, 0)
} }
this.visit(program, { asyncGenerator: false, functionDepth: 0 }) this.indexCreateRequireImports(program)
this.visit(program, { asyncGenerator: false, functionDepth: 0, moduleScope: true })
if (this.edits.length === 0 && !this.moduleSyntax) return this.source if (this.edits.length === 0 && !this.moduleSyntax) return this.source
const prologue: string[] = [] const prologue: string[] = []
@@ -568,9 +592,9 @@ export interface LoweredModule {
readonly lowered: boolean readonly lowered: boolean
/** /**
* Static module requests the body makes: import and re-export sources, * Static module requests the body makes: import and re-export sources,
* literal dynamic imports, and literal calls through `require` or an imported * literal dynamic imports and calls through `require`, plus module-scope
* `createRequire`. Computed requests are absent — they resolve (and fail loud) * direct literal calls through an imported `createRequire(import.meta.url)`.
* at runtime only. * Computed and rebased requests resolve (and fail loud) at runtime only.
*/ */
readonly moduleRequests: readonly string[] readonly moduleRequests: readonly string[]
/** /**
@@ -151,11 +151,26 @@ check(
}) })
check('literal createRequire call is a module request', direct.moduleRequests, ['node:module', 'external-package']) check('literal createRequire call is a module request', direct.moduleRequests, ['node:module', 'external-package'])
const assigned = lowerModuleSource({ const aliased = lowerModuleSource({
filename: 'node_modules/p/assigned.js', filename: 'node_modules/p/aliased.js',
source: "import { createRequire as makeRequire } from 'node:module'\nconst localRequire = makeRequire(import.meta.url)\nlocalRequire('p')\n", source: "makeRequire(import.meta.url)('aliased-package')\nimport { createRequire as makeRequire } from 'node:module'\n",
}) })
check('literal call through a createRequire binding is a module request', assigned.moduleRequests, ['node:module', 'p']) check('aliased createRequire import is indexed before traversal', aliased.moduleRequests, ['aliased-package', 'node:module'])
const runtimeOnly = lowerModuleSource({
filename: 'node_modules/p/runtime-only.js',
source: [
"import { createRequire } from 'node:module'",
'const localRequire = createRequire(import.meta.url)',
"localRequire('stored')",
"createRequire(new URL('./other.js', import.meta.url))('rebased')",
"{ const createRequire = () => () => undefined; createRequire(import.meta.url)('block-shadowed') }",
"function load(createRequire) { createRequire(import.meta.url)('parameter-shadowed') }",
"for (const createRequire of []) createRequire(import.meta.url)('for-of-shadowed')",
"switch (0) { case 0: const createRequire = () => () => undefined; createRequire(import.meta.url)('switch-shadowed') }",
].join('\n'),
})
check('stored, rebased, and shadowed createRequire calls stay runtime-only', runtimeOnly.moduleRequests, ['node:module'])
} }
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------