SaylorTwift's picture
SaylorTwift HF Staff
Add files using upload-large-folder tool
488d96b verified
Raw History Blame Contribute Delete
26.1 kB
/**
* The worker's module transform: one acorn parse turns an ES module into a
* CommonJS body **and** routes every suspension point through the ambient-store
* protocol.
*
* Both jobs live in one pass because they are two edits over one syntax tree;
* running a lexer first and a parser second meant two scanners, two sets of
* blind spots, and a second pass reading the first pass's output. Editing is
* interval-based — the original text is sliced and spliced, never reprinted —
* so **line numbers survive**: a stack frame in a transformed module points at
* the same line as the built artifact it came from.
*
* The image packer is this transform's only caller: it lowers every JavaScript
* entry it packs and records `LOWERING_VERSION` in the image manifest, so the
* worker wraps those bodies without carrying a compiler of its own.
* @module @deepseek-ai/dsh-experimental-webworker-runtime/src/compile/transform
*/
import { parse } from 'acorn'
const HELPER_SOURCE: Record<string, string> = {
def: 'const __dsh$def=(t,k,get)=>Object.defineProperty(t,k,{enumerable:true,configurable:true,get});',
default: 'const __dsh$default=(m)=>(m&&m.__esModule?m.default:m);',
ns: 'const __dsh$ns=(m)=>(m&&m.__esModule?m:Object.assign({},m,{default:m}));',
exportAll: 'const __dsh$exportAll=(t,m)=>{for(const k of Object.keys(m))if(k!=="default"&&!(k in t))__dsh$def(t,k,()=>m[k]);};',
dynImport: 'const __dsh$dynImport=(s)=>Promise.resolve().then(()=>__dsh$ns(require(s)));',
}
const HELPER_DEPENDENCIES: Record<string, readonly string[]> = {
exportAll: ['def'],
dynImport: ['ns'],
}
/** Runtime identifier the suspension protocol reaches. */
const ALS = '__als'
interface Node {
readonly type: string
readonly start: number
readonly end: number
readonly [key: string]: unknown
}
/** @returns Number of line breaks in a slice. */
function countNewlines(text: string): number {
let count = 0
for (let index = text.indexOf('\n'); index >= 0; index = text.indexOf('\n', index + 1)) count += 1
return count
}
interface Edit {
readonly start: number
readonly end: number
/** Rendered lazily so edits inside a replaced range still apply. */
readonly render: (inner: (from: number, to: number) => string) => string
}
/** One binding to publish on `exports`. */
interface Binding {
readonly exported: string
readonly local: string
}
class Transformer {
private readonly edits: Edit[] = []
private readonly source: string
private readonly helpers = new Set<string>()
private readonly bindings: Binding[] = []
private modules = 0
private temporaries = 0
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;
// commenting it out in place keeps every offset and the line count intact.
this.source = source.startsWith('#!') ? `//${source.slice(2)}` : source
}
private fail(detail: string, index: number): never {
const line = this.source.slice(0, index).split('\n').length
throw new Error(`webworker transform: ${detail} (${this.path}:${line})`)
}
private helper(name: string): string {
for (const dependency of HELPER_DEPENDENCIES[name] ?? []) this.helper(dependency)
this.helpers.add(name)
return `__dsh$${name}`
}
private moduleTemp(): string {
this.modules += 1
return `__dsh$m${this.modules}`
}
private alsTemp(): string {
this.temporaries += 1
return `__als$${this.temporaries}`
}
/**
* Replace a range, keeping the module's line count.
*
* The padding is the newlines the original range held **minus** the ones the
* replacement re-emits: a rewrite that splices the original body back in
* (a desugared loop) already carries that body's newlines, and padding by the
* whole range again would push every later line down.
*/
private edit(start: number, end: number, build: (inner: (from: number, to: number) => string) => string): void {
const original = countNewlines(this.source.slice(start, end))
this.edits.push({
start,
end,
render: (inner) => {
const text = build(inner)
return text + '\n'.repeat(Math.max(0, original - countNewlines(text)))
},
})
}
private replace(start: number, end: number, text: string): void {
this.edit(start, end, () => text)
}
private insert(at: number, text: string): void {
this.edits.push({ start: at, end: at, render: () => text })
}
private structural(start: number, end: number, render: Edit['render']): void {
this.edit(start, end, render)
}
private literal(node: Node): string {
const value = node.value
if (typeof value !== 'string') this.fail('a module specifier must be a string literal', node.start)
this.moduleRequests.add(value)
return JSON.stringify(value)
}
/** @returns Static module requests the body makes, in first-appearance order. */
requests(): readonly string[] {
return [...this.moduleRequests]
}
/** @returns Literal `import.meta.resolve()` requests, in first-appearance order. */
metaRequests(): readonly string[] {
return [...this.metaResolveRequests]
}
// --- module syntax --------------------------------------------------------
private importDeclaration(node: Node): void {
this.moduleSyntax = true
if (Array.isArray(node.attributes) && node.attributes.length > 0) {
this.fail('import attributes are not supported', node.start)
}
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};`)
return
}
const held = this.moduleTemp()
const lines = [`const ${held}=${request};`]
for (const specifier of specifiers) {
const local = (specifier.local as Node).name as string
if (specifier.type === 'ImportDefaultSpecifier') {
lines.push(`const ${local}=${this.helper('default')}(${held});`)
continue
}
if (specifier.type === 'ImportNamespaceSpecifier') {
lines.push(`const ${local}=${this.helper('ns')}(${held});`)
continue
}
const imported = specifier.imported as Node
const name = imported.type === 'Identifier' ? imported.name as string : imported.value as string
lines.push(`const ${local}=${held}[${JSON.stringify(name)}];`)
}
this.replace(node.start, node.end, lines.join(''))
}
private exportNamed(node: Node): void {
this.moduleSyntax = true
const declaration = node.declaration as Node | null
const source = node.source as Node | null
const specifiers = node.specifiers as Node[]
if (declaration !== null) {
// `export const x = 1` keeps its declaration; only the keyword goes.
this.replace(node.start, declaration.start, '')
for (const { exported, local } of declaredBindings(declaration, detail => this.fail(detail, declaration.start))) {
this.bindings.push({ exported, local })
}
return
}
if (source !== null) {
const held = this.moduleTemp()
const define = this.helper('def')
const lines = [`const ${held}=require(${this.literal(source)});`]
for (const specifier of specifiers) {
const local = nameOf(specifier.local as Node)
const exported = nameOf(specifier.exported as Node)
lines.push(`${define}(exports,${JSON.stringify(exported)},()=>${held}[${JSON.stringify(local)}]);`)
}
this.replace(node.start, node.end, lines.join(''))
return
}
// A bare `export {}` is a module marker with nothing to publish.
for (const specifier of specifiers) {
this.bindings.push({ exported: nameOf(specifier.exported as Node), local: nameOf(specifier.local as Node) })
}
this.replace(node.start, node.end, '')
}
private exportDefault(node: Node): void {
this.moduleSyntax = true
const declaration = node.declaration as Node
this.replace(node.start, declaration.start, 'exports.default = ')
}
private exportAll(node: Node): void {
this.moduleSyntax = true
const request = `require(${this.literal(node.source as Node)})`
const exported = node.exported as Node | null
if (exported === null) {
this.replace(node.start, node.end, `${this.helper('exportAll')}(exports,${request});`)
return
}
const held = this.moduleTemp()
const define = this.helper('def')
this.replace(
node.start,
node.end,
`const ${held}=${this.helper('ns')}(${request});${define}(exports,${JSON.stringify(nameOf(exported))},()=>${held});`,
)
}
// --- suspension points ----------------------------------------------------
private awaitExpression(node: Node): void {
const keywordEnd = node.start + 'await'.length
if (this.source.slice(node.start, keywordEnd) !== 'await') this.fail('unexpected await layout', node.start)
this.replace(node.start, keywordEnd, `${ALS}.resume(await ${ALS}.pause(`)
this.insert(node.end, '))')
}
/**
* `for await (L of R) B` becomes an explicit loop over the same protocol.
* `iterator.return` runs only on abrupt completion, as the language says, and
* is awaited so teardown still orders before the loop exits.
*/
private forAwait(node: Node): void {
const left = node.left as Node
const right = node.right as Node
const body = node.body as Node
const iterator = this.alsTemp()
const step = this.alsTemp()
const exhausted = this.alsTemp()
const binding = (inner: (from: number, to: number) => string): string => {
if (left.type !== 'VariableDeclaration') return `(${inner(left.start, left.end)})=${step}.value;`
const declarations = left.declarations as Node[]
const pattern = declarations[0]?.id as Node | undefined
if (declarations.length !== 1 || pattern === undefined) {
this.fail('for-await must declare exactly one binding', left.start)
}
return `${String(left.kind)} ${inner(pattern.start, pattern.end)}=${step}.value;`
}
this.structural(node.start, node.end, inner => [
`{const ${iterator}=${ALS}.iterator(${inner(right.start, right.end)});`,
`let ${step};let ${exhausted}=false;`,
`try{for(;;){${step}=${ALS}.resume(await ${ALS}.pause(${iterator}.next()));`,
`if(${step}.done){${exhausted}=true;break}`,
`{${binding(inner)}${body.type === 'BlockStatement' ? inner(body.start, body.end) : `{${inner(body.start, body.end)}}`}}}}`,
`finally{if(!${exhausted})${ALS}.resume(await ${ALS}.pause(${ALS}.close(${iterator})))}}`,
].join(''))
}
/**
* `yield` resumes with whatever the consumer sent, so the snapshot is taken
* before suspending and restored when the call completes. `yield*` delegates,
* which has no expression form here: it is desugared as a statement, and a
* consumer's `throw()` is not forwarded into the inner iterator (`next` and
* `return` are).
*/
private yieldExpression(node: Node, statement: Node | undefined): void {
if (node.delegate !== true) {
this.insert(node.start, `${ALS}.afterYield(${ALS}.snapshot(),`)
this.insert(node.end, ')')
return
}
const argument = node.argument as Node | null
if (argument === null) this.fail('yield* without an operand', node.start)
if (statement === undefined) this.fail('yield* is only supported as a statement', node.start)
if ((statement.expression as Node) !== node) {
// Anything around the delegation (`x = yield* g()`, `f(yield* g())`)
// would be silently dropped by the statement-wide rewrite below; the
// all-or-nothing lowering contract demands a loud refusal instead.
this.fail('yield* is only supported as the whole statement expression', node.start)
}
const iterator = this.alsTemp()
const step = this.alsTemp()
const sent = this.alsTemp()
const exhausted = this.alsTemp()
this.structural(statement.start, statement.end, inner => [
`{const ${iterator}=${ALS}.iterator(${inner(argument.start, argument.end)});`,
`let ${sent};let ${exhausted}=false;`,
`try{for(;;){const ${step}=${ALS}.resume(await ${ALS}.pause(${iterator}.next(${sent})));`,
`if(${step}.done){${exhausted}=true;break}`,
`${sent}=${ALS}.afterYield(${ALS}.snapshot(),yield ${step}.value)}}`,
`finally{if(!${exhausted})${ALS}.resume(await ${ALS}.pause(${ALS}.close(${iterator})))}}`,
].join(''))
}
// --- traversal ------------------------------------------------------------
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)
return
}
const record = node as Node
if (typeof record.type !== 'string') return
let next = context
switch (record.type) {
case 'ImportDeclaration': this.importDeclaration(record); break
case 'ExportNamedDeclaration': this.exportNamed(record); break
case 'ExportDefaultDeclaration': this.exportDefault(record); break
case 'ExportAllDeclaration': this.exportAll(record); break
case 'ImportExpression': {
this.moduleSyntax = true
if (!this.source.startsWith('import', record.start)) this.fail('unexpected dynamic import layout', record.start)
this.replace(record.start, record.start + 'import'.length, this.helper('dynImport'))
// A computed dynamic import stays out of the request list; resolution
// then happens (and fails loud) at runtime, never silently at pack time.
const argument = record.source as Node | undefined
if (argument !== undefined && typeof argument.value === 'string') this.moduleRequests.add(argument.value)
break
}
case 'CallExpression': {
// 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 (this.isRequireCall(callee, context.moduleScope) && callArguments.length === 1
&& typeof callArguments[0]?.value === 'string') {
this.moduleRequests.add(callArguments[0].value)
}
// `import.meta.resolve('lit')` is the third static request face: the
// loader answers it from the image, so the pack sweep must keep the
// target. A computed argument stays out, same as dynamic import —
// resolution then fails loud at runtime, never silently at pack time.
if (callee.type === 'MemberExpression') {
const object = callee.object as Node
const property = callee.property as Node
if (object.type === 'MetaProperty' && (object.meta as Node).name === 'import'
&& property.type === 'Identifier' && property.name === 'resolve'
&& typeof callArguments[0]?.value === 'string') {
this.metaResolveRequests.add(callArguments[0].value)
}
}
break
}
case 'MetaProperty': {
// `new.target` is a MetaProperty too, and it must survive untouched:
// the abstract-seam guards in the roster read it (`new.target === X`).
const meta = record.meta as Node
if (meta.name === 'import') {
this.moduleSyntax = true
this.replace(record.start, record.end, '__dsh$meta')
}
break
}
case 'AwaitExpression':
if (context.functionDepth === 0) {
this.fail('top-level await cannot run as CommonJS in the worker', record.start)
}
this.awaitExpression(record)
break
case 'ForOfStatement':
if (record.await === true) {
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
if (body.type === 'ForOfStatement' && body.await === true) {
this.fail('a labeled for-await loop is not supported', record.start)
}
break
}
case 'YieldExpression':
if (context.asyncGenerator) this.yieldExpression(record, context.statement)
break
case 'FunctionDeclaration':
case 'FunctionExpression':
case 'ArrowFunctionExpression':
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 }
for (const [key, value] of Object.entries(record)) {
if (key === 'type' || key === 'start' || key === 'end') continue
this.visit(value, next)
}
}
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
// surface here rather than as a silent tax on every load.
if (this.source.includes(`${ALS}.pause(`) || this.source.includes('__als$')) {
this.fail('the module is already lowered; check the image manifest wiring', 0)
}
let program: Node
try {
program = parse(this.source, {
ecmaVersion: 'latest',
sourceType: 'module',
allowAwaitOutsideFunction: true,
}) as unknown as Node
} catch (reason) {
this.fail(`parse failed: ${(reason as Error).message}`, 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[] = []
if (this.moduleSyntax) prologue.push('"use strict";Object.defineProperty(exports,"__esModule",{value:true});')
if (this.bindings.length > 0) this.helper('def')
for (const [name, source] of Object.entries(HELPER_SOURCE)) {
if (this.helpers.has(name)) prologue.push(source)
}
for (const { exported, local } of this.bindings) {
prologue.push(`__dsh$def(exports,${JSON.stringify(exported)},()=>${local});`)
}
const sorted = [...this.edits].sort((left, right) => left.start - right.start || left.end - right.end)
const render = (from: number, to: number): string => {
let cursor = from
let out = ''
for (const edit of sorted) {
if (edit.start < cursor || edit.end > to) continue
out += this.source.slice(cursor, edit.start) + edit.render(render)
cursor = edit.end
}
return out + this.source.slice(cursor, to)
}
const code = prologue.join('') + render(0, this.source.length)
// Proof that the emitted body is CommonJS a wrapper can compile: any leftover
// module syntax, or any mis-spliced interval, fails here rather than at load.
try {
parse(code, { ecmaVersion: 'latest', sourceType: 'script', allowAwaitOutsideFunction: false })
} catch (reason) {
this.fail(`the transform produced code that does not parse: ${(reason as Error).message}`, 0)
}
return code
}
}
/** @returns The name a specifier or identifier node carries. */
function nameOf(node: Node): string {
return node.type === 'Identifier' ? node.name as string : String(node.value)
}
/** Every binding an exported declaration introduces, including patterns. */
function declaredBindings(declaration: Node, fail: (detail: string) => never): Binding[] {
if (declaration.type === 'FunctionDeclaration' || declaration.type === 'ClassDeclaration') {
const id = declaration.id as Node | null
if (id === null) fail('an exported declaration must be named')
const name = id.name as string
return [{ exported: name, local: name }]
}
if (declaration.type !== 'VariableDeclaration') fail(`unsupported exported declaration ${declaration.type}`)
const bindings: Binding[] = []
const collect = (pattern: Node): void => {
switch (pattern.type) {
case 'Identifier':
bindings.push({ exported: pattern.name as string, local: pattern.name as string })
return
case 'ObjectPattern':
for (const property of pattern.properties as Node[]) {
collect((property.type === 'RestElement' ? property.argument : property.value) as Node)
}
return
case 'ArrayPattern':
for (const element of pattern.elements as Array<Node | null>) if (element !== null) collect(element)
return
case 'AssignmentPattern':
collect(pattern.left as Node)
return
case 'RestElement':
collect(pattern.argument as Node)
return
default:
fail(`unsupported binding pattern ${pattern.type}`)
}
}
for (const declarator of declaration.declarations as Node[]) collect(declarator.id as Node)
return bindings
}
interface TransformedModule {
readonly code: string
readonly moduleRequests: readonly string[]
readonly metaResolveRequests: readonly string[]
}
const cache = new Map<string, TransformedModule>()
/**
* Transform one module into a body for the worker wrapper.
*
* Results are cached by source text, so a module reached through two paths, or
* a repeated build, parses once.
* @param source - Module source, ESM or CommonJS.
* @param path - Path used in diagnostics.
* @returns The lowered body and the module requests found in it.
*/
function transformDetailed(source: string, path: string): TransformedModule {
const cached = cache.get(source)
if (cached !== undefined) return cached
const transformer = new Transformer(source, path)
const transformed = { code: transformer.run(), moduleRequests: transformer.requests(), metaResolveRequests: transformer.metaRequests() }
cache.set(source, transformed)
return transformed
}
/** One module the collector considered. */
export interface LoweredModule {
/** Transformed body, or the input unchanged when nothing needed lowering. */
readonly code: string
/** False means the entry may be packed as it is. */
readonly lowered: boolean
/**
* Static module requests the body makes: import and re-export sources,
* 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[]
/**
* Literal `import.meta.resolve()` requests. These are URL mappings, not
* loads: the pack sweep keeps a resolvable target and tolerates a missing
* one, and the loader answers or throws at the call site.
*/
readonly metaResolveRequests: readonly string[]
}
/**
* Lower one module at image-pack time.
*
* The collector calls this for every JavaScript entry it packs and records
* `LOWERING_VERSION` in the image manifest; the loader then wraps those entries
* without parsing them. `lowered: false` reports that the transform would have
* returned the input verbatim (already CommonJS, no suspension point), so the
* entry may be packed as it is.
*
* Throwing is the intended failure mode: a module this transform cannot express
* must fail the build rather than ship an image that breaks at load.
* @param options - Virtual path inside the image and the module source.
* @returns The code to pack and whether it changed.
*/
export function lowerModuleSource(options: { readonly filename: string; readonly source: string }): LoweredModule {
const { code, moduleRequests, metaResolveRequests } = transformDetailed(options.source, options.filename)
return { code, lowered: code !== options.source, moduleRequests, metaResolveRequests }
}