|
|
@@ -70,6 +70,7 @@ class Transformer {
|
|
|
private moduleSyntax = false
|
|
|
private readonly moduleRequests = new Set<string>()
|
|
|
private readonly metaResolveRequests = new Set<string>()
|
|
|
+ private readonly createRequireBindings = new Set<string>()
|
|
|
|
|
|
constructor(source: string, private readonly path: string) {
|
|
|
// A `#!` line is only legal at offset zero, and the prologue takes that spot;
|
|
|
@@ -154,7 +155,8 @@ class Transformer {
|
|
|
if (Array.isArray(node.attributes) && node.attributes.length > 0) {
|
|
|
this.fail('import attributes are not supported', node.start)
|
|
|
}
|
|
|
- const request = `require(${this.literal(node.source as Node)})`
|
|
|
+ const source = node.source as Node
|
|
|
+ const request = `require(${this.literal(source)})`
|
|
|
const specifiers = node.specifiers as Node[]
|
|
|
if (specifiers.length === 0) {
|
|
|
this.replace(node.start, node.end, `${request};`)
|
|
|
@@ -313,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)
|
|
|
@@ -338,11 +345,12 @@ class Transformer {
|
|
|
break
|
|
|
}
|
|
|
case 'CallExpression': {
|
|
|
- // CommonJS bodies pass through untransformed, but their literal
|
|
|
- // `require()` calls are module requests all the same.
|
|
|
+ // CommonJS bodies pass through untransformed, but literal calls through
|
|
|
+ // 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 (callee.type === 'Identifier' && callee.name === 'require' && callArguments.length === 1
|
|
|
+ if (this.isRequireCall(callee, context.moduleScope) && callArguments.length === 1
|
|
|
&& typeof callArguments[0]?.value === 'string') {
|
|
|
this.moduleRequests.add(callArguments[0].value)
|
|
|
}
|
|
|
@@ -382,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
|
|
|
@@ -399,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 }
|
|
|
@@ -410,6 +428,40 @@ class Transformer {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+ private isCreateRequireCall(node: Node): boolean {
|
|
|
+ if (node.type !== 'CallExpression') return false
|
|
|
+ const callee = node.callee as Node
|
|
|
+ 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, 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 {
|
|
|
// Transforming a lowered body again would nest the protocol inside itself:
|
|
|
// it still runs, only slower and unreadable, so a mis-wired manifest must
|
|
|
@@ -427,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[] = []
|
|
|
@@ -539,8 +592,9 @@ export interface LoweredModule {
|
|
|
readonly lowered: boolean
|
|
|
/**
|
|
|
* Static module requests the body makes: import and re-export sources,
|
|
|
- * literal dynamic imports, and literal `require()` calls. 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[]
|
|
|
/**
|