diff --git a/packages/experimental/webworker-runtime/src/compile/transform.ts b/packages/experimental/webworker-runtime/src/compile/transform.ts index 6da482af11..9e3b652e65 100644 --- a/packages/experimental/webworker-runtime/src/compile/transform.ts +++ b/packages/experimental/webworker-runtime/src/compile/transform.ts @@ -71,7 +71,6 @@ class Transformer { private readonly moduleRequests = new Set() private readonly metaResolveRequests = new Set() private readonly createRequireBindings = new Set() - private readonly requireBindings = new Set(['require']) constructor(source: string, private readonly path: string) { // 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) } 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 specifiers = node.specifiers as Node[] if (specifiers.length === 0) { @@ -322,7 +315,12 @@ class Transformer { // --- 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 (Array.isArray(node)) { 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) 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': { // CommonJS bodies pass through untransformed, but literal calls through - // the wrapper's `require` or Node's `createRequire` are module requests - // all the same. + // the wrapper's `require` remain module requests. The ESM case accepts + // only a direct module-scope createRequire call with the importer URL. const callee = record.callee 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') { 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) this.forAwait(record) } + next = { ...next, moduleScope: false } break case 'LabeledStatement': { const body = record.body as Node @@ -417,8 +408,17 @@ class Transformer { next = { asyncGenerator: record.async === true && record.generator === true, functionDepth: context.functionDepth + 1, + moduleScope: false, } break + case 'BlockStatement': + case 'CatchClause': + case 'ClassBody': + case 'ForStatement': + case 'ForInStatement': + case 'SwitchStatement': + next = { ...next, moduleScope: false } + break default: break } if (record.type === 'ExpressionStatement') next = { ...next, statement: record } @@ -431,12 +431,35 @@ class Transformer { private isCreateRequireCall(node: Node): boolean { if (node.type !== 'CallExpression') return false 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 { - return (callee.type === 'Identifier' && this.requireBindings.has(nameOf(callee))) - || this.isCreateRequireCall(callee) + private isRequireCall(callee: Node, moduleScope: boolean): boolean { + return (callee.type === 'Identifier' && callee.name === 'require') + || (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 { @@ -456,7 +479,8 @@ class Transformer { } catch (reason) { 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 const prologue: string[] = [] @@ -568,9 +592,9 @@ export interface LoweredModule { readonly lowered: boolean /** * Static module requests the body makes: import and re-export sources, - * literal dynamic imports, and literal calls through `require` or an imported - * `createRequire`. Computed requests are absent — they resolve (and fail loud) - * at runtime only. + * literal dynamic imports and calls through `require`, plus module-scope + * direct literal calls through an imported `createRequire(import.meta.url)`. + * Computed and rebased requests resolve (and fail loud) at runtime only. */ readonly moduleRequests: readonly string[] /** diff --git a/packages/experimental/webworker-runtime/tests/compile/transform.spec.ts b/packages/experimental/webworker-runtime/tests/compile/transform.spec.ts index ce41cbaef2..be5747ba53 100644 --- a/packages/experimental/webworker-runtime/tests/compile/transform.spec.ts +++ b/packages/experimental/webworker-runtime/tests/compile/transform.spec.ts @@ -151,11 +151,26 @@ check( }) check('literal createRequire call is a module request', direct.moduleRequests, ['node:module', 'external-package']) - const assigned = lowerModuleSource({ - filename: 'node_modules/p/assigned.js', - source: "import { createRequire as makeRequire } from 'node:module'\nconst localRequire = makeRequire(import.meta.url)\nlocalRequire('p')\n", + const aliased = lowerModuleSource({ + filename: 'node_modules/p/aliased.js', + 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']) } // ---------------------------------------------------------------------------