fix(webworker): retain createRequire dependencies

This commit is contained in:
imccyu
2026-08-26 15:09:48 +08:00
parent b588cdc478
commit d54c2795cc
3 changed files with 82 additions and 6 deletions
@@ -37,7 +37,9 @@ const repoRoot = fileURLToPath(new URL('../../../../', import.meta.url))
const SUBJECT = '@deepseek-ai/dsh-timeout'
const LANDLOCK = '@deepseek-ai/node-addon-landlock-run'
const PLUGIN_INVENTORY = '@deepseek-ai/dsh-plugin-package-inventory-deepseek'
const TERMINAL_BASH = '@deepseek-ai/dsh-terminal-bash'
const WEB_SERVER = '@deepseek-ai/dsh-host-webserver'
const XTERM_HEADLESS = '@xterm/headless'
const workspaces = indexWorkspacePackages(repoRoot)
@@ -114,6 +116,15 @@ const packedWebServer = (): ReturnType<typeof packVfsImage> => webServerMemo ??=
entries: [],
})
let terminalBashMemo: ReturnType<typeof packVfsImage> | undefined
const packedTerminalBash = (): ReturnType<typeof packVfsImage> => terminalBashMemo ??= packVfsImage({
config: `- id: subject\n name: '${TERMINAL_BASH}'\n`,
profile: 'terminal-bash-dependency-check',
workspaces,
resolveFrom: repoRoot,
entries: [],
})
/** The image's archive, inflated once: mounting reads the tar, not the gzip member. */
let archiveMemo: Uint8Array | undefined
const archive = async (): Promise<Uint8Array> =>
@@ -218,6 +229,27 @@ const archive = async (): Promise<Uint8Array> =>
expect(typeof webserver.WebServer).toBe('function')
})
it('loads a third-party dependency requested through createRequire', async () => {
const result = packedTerminalBash()
expect(result.missing).toEqual([])
expect(result.packages.get(XTERM_HEADLESS)).toBeGreaterThan(0)
expect(Object.hasOwn(result.files, `node_modules/${XTERM_HEADLESS}/lib-headless/xterm-headless.js`)).toBe(true)
const vfs = loadVfsImage(await inflateImage(result.image, 'the packed terminal backend'), DEFAULT_ROOT)
const loader = new WorkerModuleLoader({
vfs,
root: DEFAULT_ROOT,
staticModules: createNodeBuiltins(),
staticModulePrefixes: REPLACED_PREFIXES,
})
setActiveVfs(vfs)
setActiveModuleLoader(loader)
const terminal = loader.requireFrom(`${DEFAULT_ROOT}/workspace`)(TERMINAL_BASH) as {
BashTerminalBackend?: unknown
}
expect(typeof terminal.BashTerminalBackend).toBe('function')
})
it('runs the unchanged Landlock entry package over the Worker platform executable', async () => {
const result = packedLandlock()
expect(workspaces.has(LANDLOCK)).toBe(true)
@@ -70,6 +70,8 @@ class Transformer {
private moduleSyntax = false
private readonly moduleRequests = new Set<string>()
private readonly metaResolveRequests = new Set<string>()
private readonly createRequireBindings = new Set<string>()
private readonly requireBindings = new Set<string>(['require'])
constructor(source: string, private readonly path: string) {
// A `#!` line is only legal at offset zero, and the prologue takes that spot;
@@ -154,7 +156,14 @@ 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
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) {
this.replace(node.start, node.end, `${request};`)
@@ -337,12 +346,21 @@ 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 their literal
// `require()` calls are module requests all the same.
// CommonJS bodies pass through untransformed, but literal calls through
// the wrapper's `require` or Node's `createRequire` are module requests
// all the same.
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) && callArguments.length === 1
&& typeof callArguments[0]?.value === 'string') {
this.moduleRequests.add(callArguments[0].value)
}
@@ -410,6 +428,17 @@ 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))
}
private isRequireCall(callee: Node): boolean {
return (callee.type === 'Identifier' && this.requireBindings.has(nameOf(callee)))
|| this.isCreateRequireCall(callee)
}
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
@@ -539,8 +568,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 literal calls through `require` or an imported
* `createRequire`. Computed requests are absent — they resolve (and fail loud)
* at runtime only.
*/
readonly moduleRequests: readonly string[]
/**
@@ -144,6 +144,20 @@ check(
check('lowered mirrors code !== source', cjsAwait.lowered, cjsAwait.code !== 'module.exports = async () => { await 1 }\n')
}
{
const direct = lowerModuleSource({
filename: 'node_modules/p/direct.js',
source: "import { createRequire } from 'node:module'\ncreateRequire(import.meta.url)('@xterm/headless')\n",
})
check('literal createRequire call is a module request', direct.moduleRequests, ['node:module', '@xterm/headless'])
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",
})
check('literal call through a createRequire binding is a module request', assigned.moduleRequests, ['node:module', 'p'])
}
// ---------------------------------------------------------------------------
// 3. Import forms.
// ---------------------------------------------------------------------------