diff --git a/packages/ptc-runtime/ptc-runtime-node/README.i18n.yaml b/packages/ptc-runtime/ptc-runtime-node/README.i18n.yaml index 35a00cf82c..72452511df 100644 --- a/packages/ptc-runtime/ptc-runtime-node/README.i18n.yaml +++ b/packages/ptc-runtime/ptc-runtime-node/README.i18n.yaml @@ -39,8 +39,8 @@ en: 53e24b6ba2614d4c zh: 0fe4d5ace9d2ef75 /deepseek-ai-dsh-ptc-runtime-node/understand-the-implementation/source-and-built-bootstraps: - en: 3e30860547f88777 - zh: d7ba84b1f88c3fb2 + en: 735eb081e90834a6 + zh: a657d9ef17be9871 /deepseek-ai-dsh-ptc-runtime-node/understand-the-implementation/source-map: en: a797a875fbfb0598 zh: 0cc0b19f8241f6bc @@ -54,8 +54,8 @@ en: 40294c1c31b74824 zh: d91e7b78c0619c7e /deepseek-ai-dsh-ptc-runtime-node/known-limitations-and-deferred-work: - en: f4c6a8a3e0b1220f - zh: 03fb9482097eabe7 + en: 5a2f3b56db7cb92a + zh: c3eb766cbefc5918 /deepseek-ai-dsh-ptc-runtime-node/known-limitations-and-deferred-work/dev-note: en: 6373aba4e08f7205 zh: 2a8c33d1f2839df4 diff --git a/packages/ptc-runtime/ptc-runtime-node/README.md b/packages/ptc-runtime/ptc-runtime-node/README.md index dd7eac6aa8..05c87e9b2c 100644 --- a/packages/ptc-runtime/ptc-runtime-node/README.md +++ b/packages/ptc-runtime/ptc-runtime-node/README.md @@ -127,9 +127,9 @@ Length-framed JSON travels separately from stdout/stderr. Every evaluation carri ### Source and built bootstraps -Source execution loads an erasable-only bootstrap closure without relying on sibling built exports. Built script execution uses `process.js` for every mode; `persistent-process.js` delegates to the same loop with a REPL default for hosts that omit program syntax. Persistent mode requires a `node-script` launch. A remote script deployment supplies the matching `launch.bootstrapPath`. Embedded carriers launch their private worker through `launch.executable`. `resolveLaunch()` selects the local default during construction; an explicit remote invocation never depends on whether the client itself is packaged. A host path is never assumed to name the same remote file. +Source execution loads an erasable-only bootstrap closure without relying on sibling built exports; its evaluator modules and installed Acorn dependencies must be available in the execution world. Built script execution uses `process.js` for every mode; `persistent-process.js` delegates to the same loop with a REPL default for hosts that omit program syntax. Persistent mode requires a `node-script` launch. A remote script deployment supplies the matching `launch.bootstrapPath`. Embedded carriers launch their private worker through `launch.executable`. `resolveLaunch()` selects the local default during construction; an explicit remote invocation never depends on whether the client itself is packaged. A host path is never assumed to name the same remote file. -Scripts accepted by the native compiler execute unchanged. When compilation requires top-level await support, Acorn parses an async wrapper around the source. The transform follows Node's REPL visitors, declaration hoisting and character-position edits, including its boxed final-expression result; the evaluator unwraps that result and awaits its value. A local AST walker keeps Acorn as the only added dependency. Dynamic `import()` expressions remain native and use the compiling Script's `importModuleDynamically` option. Both private process bundles include Acorn; source execution imports the package directly. +Source containing `await` is checked for top-level await before native compilation, which can otherwise treat `await` as an identifier. Acorn parses an async wrapper, and acorn-walk traverses it using Node's REPL visitors, declaration hoisting and character-position edits. Computed class-method keys are traversed while method bodies remain separate evaluations. The transform boxes the final expression; the evaluator unwraps and awaits its value. Source that needs no lowering uses native compilation; submissions without `await` retain native syntax diagnostics. Dynamic `import()` expressions remain native and use the compiling Script's `importModuleDynamically` option. Both private process bundles include Acorn and acorn-walk; source execution imports the installed packages. ### Source map @@ -188,6 +188,8 @@ These limits qualify the execution guarantees and retained output. - **Cleanup inherits subprocess observability** — escaped descendants on a fallback platform may remain outside the managed range; see the subprocess provider's stated limits. - **Default execution is one-shot** — retained state requires `persistent-in-memory` mode or Script mode with `scriptPersistence: true`; no mode offers a yield/wait API. - **Await lowering relaxes declaration semantics** — in transformed programs, top-level `let`, `const`, and `class` bindings are mutable and initialized to `undefined` before execution; the native temporal dead zone does not apply. Function declarations populate retained globals when execution reaches them, and a `var` declaration without an initializer resets its binding to `undefined`. Direct-eval declarations stay within that evaluation. Top-level `using` declarations are rejected; put disposable bindings inside a block. Runtime stack locations refer to the transformed source. Scripts that compile without lowering keep native semantics. +- **Failed native lexical initialization can reserve a name** — if a top-level `let`, `const`, or `class` initializer throws before initializing its binding, that name remains uninitialized in the retained Script context. Reading, assigning or redeclaring it fails; use a new name or release and recreate the environment. +- **Script values can cross realms** — binding results, binding rejection classes and imported modules can use constructors from outside the Script context. `instanceof Array` or `instanceof Error` can be false for those values; use `Array.isArray` and the declared binding error class. - **In-memory environments retain one process per live owner** — idle owners keep their process and heap until explicit release, owner disposal or failure. Fan-out can retain many processes; idle expiration remains deferred. - **Output caps reject rather than retain every byte** — spill can preserve only the bounded result delivered by this provider. - **Bindings are bounded at transport admission** — control limits do not bound the memory a host binding allocates while producing its result. diff --git a/packages/ptc-runtime/ptc-runtime-node/README.zh.md b/packages/ptc-runtime/ptc-runtime-node/README.zh.md index d598bce776..442da2fd6b 100644 --- a/packages/ptc-runtime/ptc-runtime-node/README.zh.md +++ b/packages/ptc-runtime/ptc-runtime-node/README.zh.md @@ -127,9 +127,9 @@ Host 擦除可擦除类型;需要启动进程时,在配置的执行世界中 ### 源代码与构建后 bootstrap -源代码执行加载仅含可擦除语法的 bootstrap 依赖,不依赖同级包的构建后导出。构建后脚本在所有模式下都使用 `process.js`;`persistent-process.js` 委托同一个循环,供未指定程序语法的宿主默认使用 REPL。保留模式要求 `node-script` 启动方式。远端脚本部署提供对应的 `launch.bootstrapPath`。内嵌载体通过 `launch.executable` 启动私有 worker。`resolveLaunch()` 在构造时选择本地默认值;显式的远端调用不依赖客户端自身是否打包。不会假设 Host 路径对应同一个远程文件。 +源代码执行加载仅含可擦除语法的 bootstrap 依赖,不依赖同级包的构建后导出;执行世界中必须提供求值器模块和已安装的 Acorn 依赖。构建后脚本在所有模式下都使用 `process.js`;`persistent-process.js` 委托同一个循环,供未指定程序语法的宿主默认使用 REPL。保留模式要求 `node-script` 启动方式。远端脚本部署提供对应的 `launch.bootstrapPath`。内嵌载体通过 `launch.executable` 启动私有 worker。`resolveLaunch()` 在构造时选择本地默认值;显式的远端调用不依赖客户端自身是否打包。不会假设 Host 路径对应同一个远程文件。 -原生编译器接受的脚本直接执行。当编译需要顶层 await 支持时,Acorn 解析包住源码的异步函数。转换沿用 Node REPL 的 visitor、声明提升和按字符位置修改源码的方式,包括对末尾表达式结果的包装;求值器解包后等待该值。局部 AST 遍历器使新增依赖仍只有 Acorn。动态 `import()` 表达式保持原生形式,使用编译该 Script 时的 `importModuleDynamically` 选项。两个私有进程 bundle 都包含 Acorn;源码执行直接导入该包。 +包含 `await` 的源码会在原生编译前检查顶层 await,否则原生编译可能将 `await` 当作标识符。Acorn 解析包住源码的异步函数,acorn-walk 沿用 Node REPL 的 visitor、声明提升和按字符位置修改源码的方式进行遍历。类方法的计算属性名参与遍历,方法体仍独立求值。转换包装末尾表达式,求值器解包并等待其值。无需转换的源码使用原生编译;不含 `await` 的提交保留原生语法诊断。动态 `import()` 表达式保持原生形式,使用编译该 Script 时的 `importModuleDynamically` 选项。两个私有进程 bundle 都包含 Acorn 和 acorn-walk;源码执行导入已安装的包。 ### 源码索引 @@ -188,6 +188,8 @@ Host 擦除可擦除类型;需要启动进程时,在配置的执行世界中 - **清理继承子进程的可观测范围**——使用 fallback 的平台上,逃逸的后代可能仍在受管范围之外;参见子进程提供方声明的限制。 - **默认执行是一次性的**:保留状态需要设置 `persistent-in-memory` 模式,或在 Script 模式下启用 `scriptPersistence: true`;所有模式都不提供 yield/wait API。 - **await 转换放宽声明语义**——转换后程序的顶层 `let`、`const` 和 `class` 绑定可修改,且在执行前初始化为 `undefined`,不再具有原生暂时性死区。执行到函数声明时才会为其设置保留的全局变量;没有初始化器的 `var` 声明会将其绑定重置为 `undefined`。直接 eval 中的声明仅在本次求值内有效。不接受顶层 `using` 声明;可释放绑定需放在块内。运行时堆栈位置指向转换后的源码。无需转换即可编译的脚本保持原生语义。 +- **原生词法初始化失败可能占用名称**——顶层 `let`、`const` 或 `class` 的初始化器在完成绑定初始化前抛错时,该名称会在保留的 Script context 中持续处于未初始化状态。读取、赋值和重新声明都会失败;应使用新名称,或释放并重新创建环境。 +- **Script 值可能跨 realm**——绑定结果、绑定拒绝类和导入模块可能使用 Script context 之外的构造器。这些值的 `instanceof Array` 或 `instanceof Error` 可能为 false;应使用 `Array.isArray` 和声明的绑定错误类。 - **内存保留环境为每个存活所有者保留一个进程**——空闲所有者保留进程及堆内存,直到显式释放、所有者销毁或执行失败。并行创建多个所有者会保留多个进程;空闲过期机制留待后续处理。 - **输出上限拒绝超量内容,而不保留每个字节**——溢出只能保存本提供方交付的有界结果。 - **绑定在传输接纳时受限**——控制限制不约束 Host 绑定生成结果期间分配的内存。 diff --git a/packages/ptc-runtime/ptc-runtime-node/package.json b/packages/ptc-runtime/ptc-runtime-node/package.json index 9dfdf5a78c..fdf83495dd 100644 --- a/packages/ptc-runtime/ptc-runtime-node/package.json +++ b/packages/ptc-runtime/ptc-runtime-node/package.json @@ -1,6 +1,6 @@ { "name": "@deepseek-ai/dsh-ptc-runtime-node", - "description": "Node PTC execution with per-call sandboxed processes or in-memory REPL state", + "description": "Node PTC execution with async function, REPL and VM Script evaluators and optional retained state", "version": "0.2.1-rc.2", "publishConfig": { "access": "public" @@ -46,7 +46,8 @@ "dependencies": { "@deepseek-ai/dsh-util-values": "workspace:*", "@deepseek-ai/schemastery": "workspace:~", - "acorn": "^8.18.0" + "acorn": "^8.18.0", + "acorn-walk": "^8.3.5" }, "devDependencies": { "@deepseek-ai/cordis": "workspace:~", diff --git a/packages/ptc-runtime/ptc-runtime-node/src/evaluators/vm-script-await.ts b/packages/ptc-runtime/ptc-runtime-node/src/evaluators/vm-script-await.ts index de32cd17b9..8c73b9972d 100644 --- a/packages/ptc-runtime/ptc-runtime-node/src/evaluators/vm-script-await.ts +++ b/packages/ptc-runtime/ptc-runtime-node/src/evaluators/vm-script-await.ts @@ -2,7 +2,7 @@ * Node REPL top-level-await transformation adapted for complete Script submissions. * Source: Node.js v26.6.0, lib/internal/repl/await.js: * https://github.com/nodejs/node/blob/v26.6.0/lib/internal/repl/await.js - * Uses public Acorn and a local walker; incomplete input is a SyntaxError. + * Uses public Acorn and acorn-walk; incomplete input is a SyntaxError. */ /*! @license * Copyright Node.js contributors. All rights reserved. @@ -27,11 +27,7 @@ */ import { Parser } from 'acorn' import type { AnyNode, BlockStatement, Pattern, Position, Program, Statement, VariableDeclarator } from 'acorn' - -type Walk = (node: AnyNode, state: TransformState) => void -type Visitor = ( - node: Extract, state: TransformState, c: Walk, -) => void +import * as walk from 'acorn-walk' interface TransformState { body: BlockStatement @@ -49,7 +45,8 @@ function isTopLevelDeclaration(state: TransformState): boolean { } const noop = (): void => {} -const visitorsWithoutAncestors: { [K in AnyNode['type']]?: Visitor } = { +const base = walk.base as Required> +const visitorsWithoutAncestors: walk.RecursiveVisitors = { ClassDeclaration(node, state, c) { if (node.id === null) throw new SyntaxError('Script declarations must be named') if (isTopLevelDeclaration(state)) { @@ -57,13 +54,13 @@ const visitorsWithoutAncestors: { [K in AnyNode['type']]?: Visitor } = { state.hoistedDeclarationStatements.push(`let ${node.id.name}; `) } - visitChildren(node, state, c) + base.ClassDeclaration(node, state, c) }, ForOfStatement(node, state, c) { if (node.await) { state.containsAwait = true } - visitChildren(node, state, c) + base.ForOfStatement(node, state, c) }, FunctionDeclaration(node, state) { if (node.id === null) throw new SyntaxError('Script declarations must be named') @@ -72,15 +69,18 @@ const visitorsWithoutAncestors: { [K in AnyNode['type']]?: Visitor } = { }, FunctionExpression: noop, ArrowFunctionExpression: noop, - MethodDefinition: noop, + MethodDefinition(node, state, c) { + // Computed keys execute while the class is defined; method bodies do not. + if (node.computed) c(node.key, state) + }, StaticBlock: noop, AwaitExpression(node, state, c) { state.containsAwait = true - visitChildren(node, state, c) + base.AwaitExpression(node, state, c) }, ReturnStatement(node, state, c) { state.containsReturn = true - visitChildren(node, state, c) + base.ReturnStatement(node, state, c) }, VariableDeclaration(node, state, c) { const variableKind = node.kind @@ -155,33 +155,24 @@ const visitorsWithoutAncestors: { [K in AnyNode['type']]?: Visitor } = { }) } - visitChildren(node, state, c) + base.VariableDeclaration(node, state, c) }, } -/** Acorn's child nodes provide the recursive traversal without an acorn-walk dependency. */ -function visitChildren(node: AnyNode, state: TransformState, c: Walk): void { - const visit = (value: unknown): void => { - if (value !== null && typeof value === 'object' && 'type' in value) c(value as AnyNode, state) - } - for (const value of Object.values(node as AnyNode & Record)) { - if (Array.isArray(value)) value.forEach(visit) - else visit(value) - } -} - -function recursive(node: AnyNode, state: TransformState): void { - // Indexing by node.type selects the visitor accepting this exact Acorn node type. - const callback = (visitorsWithoutAncestors[node.type] ?? visitChildren) as Visitor - const isNew = node !== state.ancestors[state.ancestors.length - 1] - if (isNew) { - state.ancestors.push(node) - } - callback(node, state, recursive) - if (isNew) { - state.ancestors.pop() - } -} +type Visitor = (node: AnyNode, state: TransformState, c: walk.WalkerCallback) => void +const visitors: walk.RecursiveVisitors = Object.fromEntries( + Object.entries({ ...base, ...visitorsWithoutAncestors }).map(([type, visitor]) => { + // acorn-walk selects the callback by the matching node or aggregate type. + const callback = visitor as Visitor + const withAncestors: Visitor = (node, state, c) => { + const isNew = node !== state.ancestors[state.ancestors.length - 1] + if (isNew) state.ancestors.push(node) + callback(node, state, c) + if (isNew) state.ancestors.pop() + } + return [type, withAncestors] + }), +) /** Keep the native-Node bootstrap independent of workspace packages' built exports. */ function assertNever(value: never): never { @@ -205,7 +196,6 @@ export function processTopLevelAwait(src: string): string | null { if (!(error instanceof SyntaxError)) throw error // Acorn syntax errors include offsets and positions; batches cannot request more REPL input. const e = error as SyntaxError & { pos: number; loc: Position } - if (e.message.startsWith('Unterminated ')) throw e // If the parse error is before the first "await", then use the execution // error. Otherwise we must emit this parse error, making it look like a // proper syntax error. @@ -253,7 +243,7 @@ export function processTopLevelAwait(src: string): string | null { containsReturn: false, } - recursive(body, state) + walk.recursive(body, state, visitors) // Do not transform if // 1. False alarm: there isn't actually an await expression. diff --git a/packages/ptc-runtime/ptc-runtime-node/src/evaluators/vm-script.ts b/packages/ptc-runtime/ptc-runtime-node/src/evaluators/vm-script.ts index b07a3863c6..094cd01a82 100644 --- a/packages/ptc-runtime/ptc-runtime-node/src/evaluators/vm-script.ts +++ b/packages/ptc-runtime/ptc-runtime-node/src/evaluators/vm-script.ts @@ -67,16 +67,18 @@ async function evaluateScript(code: string, context: Context): Promise } } -/** Native scripts bypass conversion; only compilation failures enter the await transformer. */ +/** Detect top-level await before native compilation can interpret await as an identifier. */ function compileScript(code: string, options: ScriptOptions): { script: Script; transformed: boolean } { - try { - return { script: new Script(code, options), transformed: false } - } catch (error: unknown) { - if (!(error instanceof SyntaxError)) throw error + if (code.includes('await')) { // Complete submissions need no REPL continuation; the newline terminates trailing comments. const source = code.startsWith('#!') ? `//${code.slice(2)}` : code - const transformed = processTopLevelAwait(`${source}\n`) - if (transformed === null) throw error - return { script: new Script(transformed, options), transformed: true } + let transformed: string | null + try { transformed = processTopLevelAwait(`${source}\n`) } catch (error: unknown) { + // Parser diagnostics describe the program; transformer stack frames do not. + if (error instanceof SyntaxError) error.stack = `${error.name}: ${error.message}` + throw error + } + if (transformed !== null) return { script: new Script(transformed, options), transformed: true } } + return { script: new Script(code, options), transformed: false } } diff --git a/packages/ptc-runtime/ptc-runtime-node/src/execution.ts b/packages/ptc-runtime/ptc-runtime-node/src/execution.ts index 969c54f823..cf5c46964c 100644 --- a/packages/ptc-runtime/ptc-runtime-node/src/execution.ts +++ b/packages/ptc-runtime/ptc-runtime-node/src/execution.ts @@ -166,11 +166,13 @@ function resolveMode(config: ResolvedConfig): ExecutionMode { const lifetime = config.scriptPersistence ? 'Variables, functions, and imported modules remain in memory for this Agent across calls.' : 'Each call runs in a fresh Node process.' - const recovery = config.scriptPersistence ? ' ' + RETAINED_STATE_INSTRUCTIONS : '' + const recovery = config.scriptPersistence + ? ' ' + RETAINED_STATE_INSTRUCTIONS + ' If a native top-level let, const, or class initializer throws, its name may remain permanently uninitialized; use a new name or restart the environment.' + : '' return { format: 'script', retainsState: config.scriptPersistence, - executionInstructions: 'The program contains TypeScript script statements; only erasable syntax is accepted, with no enum or namespaces. Type annotations are advisory. Top-level declarations and await are supported; top-level return is not supported. Emit output with console.log(...) or a final lossless JSON expression. The final expression is awaited if it is a Promise. Only printed values and that final expression become program output. ' + lifetime + ' Use await import(...) for modules. Relative paths use the supplied working directory. Direct filesystem access follows this execution\'s sandbox policy; process.env starts empty.' + recovery, + executionInstructions: 'The program contains TypeScript script statements; only erasable syntax is accepted, with no enum or namespaces. Type annotations are advisory. Top-level declarations and await are supported; top-level return is not supported. Emit output with console.log(...) or a final lossless JSON expression. The final expression is awaited if it is a Promise. Only printed values and that final expression become program output. ' + lifetime + ' Use await import(...) for modules. Relative paths use the supplied working directory. Direct filesystem access follows this execution\'s sandbox policy; process.env starts empty. Await asynchronous work: an uncaught callback exception or unhandled rejection terminates the process and clears its variables. Values from bindings and imports can belong to another realm; use Array.isArray for arrays and the supplied binding error classes for failures, because instanceof Array or Error may be false.' + recovery, stripTypes: stripTypeScriptTypes, } } diff --git a/packages/ptc-runtime/ptc-runtime-node/src/json-wire.ts b/packages/ptc-runtime/ptc-runtime-node/src/json-wire.ts index 2a15d68383..6fa4731f4d 100644 --- a/packages/ptc-runtime/ptc-runtime-node/src/json-wire.ts +++ b/packages/ptc-runtime/ptc-runtime-node/src/json-wire.ts @@ -1,5 +1,5 @@ /** - * Lossless-JSON snapshots for the dependency-free source bootstrap closure. + * Lossless-JSON snapshots without workspace runtime imports in the source bootstrap. * @module @deepseek-ai/dsh-ptc-runtime-node/json-wire */ diff --git a/packages/ptc-runtime/ptc-runtime-node/tsdown.config.ts b/packages/ptc-runtime/ptc-runtime-node/tsdown.config.ts index 66772032f6..366ea73a54 100644 --- a/packages/ptc-runtime/ptc-runtime-node/tsdown.config.ts +++ b/packages/ptc-runtime/ptc-runtime-node/tsdown.config.ts @@ -3,6 +3,6 @@ import { defineConfig } from 'tsdown' /** The Node provider and its private process bootstraps are published bundles. */ export default defineConfig([ { entry: ['lib/types/index.js'], outDir: 'lib', format: ['esm'], platform: 'node', target: 'es2024', fixedExtension: false, dts: false, clean: false }, - { entry: { process: 'lib/types/process-entry.js' }, outDir: 'lib', format: ['esm'], platform: 'node', target: 'es2024', fixedExtension: false, dts: false, clean: false, noExternal: ['@deepseek-ai/dsh-subprocess/control', 'acorn'] }, - { entry: { 'persistent-process': 'lib/types/persistent-process-entry.js' }, outDir: 'lib', format: ['esm'], platform: 'node', target: 'es2024', fixedExtension: false, dts: false, clean: false, noExternal: ['@deepseek-ai/dsh-subprocess/control', 'acorn'] }, + { entry: { process: 'lib/types/process-entry.js' }, outDir: 'lib', format: ['esm'], platform: 'node', target: 'es2024', fixedExtension: false, dts: false, clean: false, noExternal: ['@deepseek-ai/dsh-subprocess/control', 'acorn', 'acorn-walk'] }, + { entry: { 'persistent-process': 'lib/types/persistent-process-entry.js' }, outDir: 'lib', format: ['esm'], platform: 'node', target: 'es2024', fixedExtension: false, dts: false, clean: false, noExternal: ['@deepseek-ai/dsh-subprocess/control', 'acorn', 'acorn-walk'] }, ]) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6c18f37f32..1dcf1cc2d5 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -12117,6 +12117,9 @@ importers: acorn: specifier: ^8.18.0 version: 8.18.0 + acorn-walk: + specifier: ^8.3.5 + version: 8.3.5 devDependencies: '@deepseek-ai/cordis': specifier: workspace:~ @@ -19933,6 +19936,10 @@ packages: peerDependencies: acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 + acorn-walk@8.3.5: + resolution: {integrity: sha512-HEHNfbars9v4pgpW6SO1KSPkfoS0xVOM/9UzkJltjlsHZmJasxg8aXkuZa7SMf8vKGIBhpUsPluQSqhJFCqebw==} + engines: {node: '>=0.4.0'} + acorn@8.17.0: resolution: {integrity: sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg==} engines: {node: '>=0.4.0'} @@ -27374,6 +27381,10 @@ snapshots: dependencies: acorn: 8.17.0 + acorn-walk@8.3.5: + dependencies: + acorn: 8.18.0 + acorn@8.17.0: {} acorn@8.18.0: {}