mirror of
https://github.com/openclaw/openclaw.git
synced 2026-10-03 01:29:56 +00:00
fix(update): record the published Node bootstrap contract (#156873)
Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
This commit is contained in:
parent
2a5cda2715
commit
0580bd9045
3 changed files with 178 additions and 30 deletions
|
|
@ -12,7 +12,7 @@ import {
|
|||
parseModule,
|
||||
type UpdateCompatibilityOrigin,
|
||||
} from "./update-compat-module-graph.mts";
|
||||
import { isUpdateSourceScriptImport } from "./update-compat-source-imports.mts";
|
||||
import { isUpdatePackageAssetImport } from "./update-compat-source-imports.mts";
|
||||
|
||||
export { isUpdateCompatibilityChunk } from "./update-compat-contract.mjs";
|
||||
export const UPDATE_COMPATIBILITY_INVENTORY_FILE = "update-compat-inventory.json";
|
||||
|
|
@ -198,7 +198,7 @@ export function recordUpdateCompatibilityRelease(params: {
|
|||
if (owner && POST_SWAP_OWNER.test(owner) && owner !== "src/cli/update-cli/wizard.ts") {
|
||||
const specifier = node.arguments[0];
|
||||
if (!specifier || !ts.isStringLiteralLike(specifier)) {
|
||||
if (isUpdateSourceScriptImport(owner, node)) {
|
||||
if (isUpdatePackageAssetImport(owner, node)) {
|
||||
return;
|
||||
}
|
||||
throw new Error(`Nonliteral post-swap import in ${file}: ${node.getText()}`);
|
||||
|
|
|
|||
|
|
@ -1,11 +1,88 @@
|
|||
import type ts from "typescript";
|
||||
import { getTypeScript } from "./ts-guard-utils.mts";
|
||||
|
||||
/** Source-update completion loads the installed checkout's scripts, not retained dist chunks. */
|
||||
export function isUpdateSourceScriptImport(owner: string, node: ts.CallExpression): boolean {
|
||||
if (owner !== "src/cli/update-cli/update-command-runtime.ts") {
|
||||
function precedingConstInitializer(node: ts.Node, name: string): ts.Expression | undefined {
|
||||
const ts = getTypeScript();
|
||||
let statement = node;
|
||||
while (statement.parent && !ts.isBlock(statement.parent)) {
|
||||
statement = statement.parent;
|
||||
}
|
||||
if (!statement.parent || !ts.isBlock(statement.parent)) {
|
||||
return undefined;
|
||||
}
|
||||
for (const preceding of statement.parent.statements) {
|
||||
if (preceding === statement) {
|
||||
break;
|
||||
}
|
||||
if (!ts.isVariableStatement(preceding)) {
|
||||
continue;
|
||||
}
|
||||
for (const declaration of preceding.declarationList.declarations) {
|
||||
if (ts.isIdentifier(declaration.name) && declaration.name.text === name) {
|
||||
return preceding.declarationList.flags & ts.NodeFlags.Const
|
||||
? declaration.initializer
|
||||
: undefined;
|
||||
}
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function isOwnPackageRoot(node: ts.Node, name: string): boolean {
|
||||
const ts = getTypeScript();
|
||||
const awaited = node.parent;
|
||||
const declaration = awaited?.parent;
|
||||
const declarations = declaration?.parent;
|
||||
const statement = declarations?.parent;
|
||||
// The shipped bootstrap is a direct const initializer. Crossing loops or
|
||||
// functions could mistake an outer driverRoot for a shadowing lexical binding.
|
||||
if (
|
||||
!awaited ||
|
||||
!ts.isAwaitExpression(awaited) ||
|
||||
!declaration ||
|
||||
!ts.isVariableDeclaration(declaration) ||
|
||||
declaration.initializer !== awaited ||
|
||||
!declarations ||
|
||||
!ts.isVariableDeclarationList(declarations) ||
|
||||
!(declarations.flags & ts.NodeFlags.Const) ||
|
||||
declarations.declarations.length !== 1 ||
|
||||
!statement ||
|
||||
!ts.isVariableStatement(statement) ||
|
||||
!ts.isBlock(statement.parent)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
const initializer = precedingConstInitializer(node, name);
|
||||
if (
|
||||
!initializer ||
|
||||
!ts.isCallExpression(initializer) ||
|
||||
initializer.questionDotToken ||
|
||||
initializer.expression.getText() !== "resolveOpenClawPackageRootSync" ||
|
||||
initializer.arguments.length !== 1
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
const options = initializer.arguments[0];
|
||||
const property =
|
||||
options && ts.isObjectLiteralExpression(options) && options.properties.length === 1
|
||||
? options.properties[0]
|
||||
: undefined;
|
||||
return Boolean(
|
||||
property &&
|
||||
ts.isPropertyAssignment(property) &&
|
||||
ts.isIdentifier(property.name) &&
|
||||
property.name.text === "moduleUrl" &&
|
||||
ts.isPropertyAccessExpression(property.initializer) &&
|
||||
!property.initializer.questionDotToken &&
|
||||
property.initializer.name.text === "url" &&
|
||||
ts.isMetaProperty(property.initializer.expression) &&
|
||||
property.initializer.expression.keywordToken === ts.SyntaxKind.ImportKeyword &&
|
||||
property.initializer.expression.name.text === "meta",
|
||||
);
|
||||
}
|
||||
|
||||
/** Recognize shipped package assets outside the hashed dist compatibility graph. */
|
||||
export function isUpdatePackageAssetImport(owner: string, node: ts.CallExpression): boolean {
|
||||
const ts = getTypeScript();
|
||||
const specifier = node.arguments[0];
|
||||
if (!specifier || !ts.isPropertyAccessExpression(specifier) || specifier.name.text !== "href") {
|
||||
|
|
@ -21,37 +98,35 @@ export function isUpdateSourceScriptImport(owner: string, node: ts.CallExpressio
|
|||
}
|
||||
let target = url.arguments[0];
|
||||
if (target && ts.isIdentifier(target)) {
|
||||
const name = target.text;
|
||||
let statement: ts.Node = node;
|
||||
while (statement.parent && !ts.isBlock(statement.parent)) {
|
||||
statement = statement.parent;
|
||||
}
|
||||
if (!statement.parent || !ts.isBlock(statement.parent)) {
|
||||
return false;
|
||||
}
|
||||
target = undefined;
|
||||
for (const preceding of statement.parent.statements) {
|
||||
if (preceding === statement) {
|
||||
break;
|
||||
}
|
||||
if (
|
||||
!ts.isVariableStatement(preceding) ||
|
||||
!(preceding.declarationList.flags & ts.NodeFlags.Const)
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
for (const declaration of preceding.declarationList.declarations) {
|
||||
if (ts.isIdentifier(declaration.name) && declaration.name.text === name) {
|
||||
target = declaration.initializer;
|
||||
}
|
||||
}
|
||||
}
|
||||
target = precedingConstInitializer(node, target.text);
|
||||
}
|
||||
if (!target || !ts.isCallExpression(target) || target.expression.getText() !== "path.join") {
|
||||
return false;
|
||||
}
|
||||
const [root, ...segments] = target.arguments;
|
||||
if (owner === "src/cli/update-cli/update-command-node-runtime-resolution.ts") {
|
||||
const entry = segments[0];
|
||||
// The published Node bootstrap resolves installer assets from its own package root.
|
||||
// It is not a dist bridge; unknown roots or paths must still fail recording.
|
||||
return Boolean(
|
||||
node.arguments.length === 1 &&
|
||||
!specifier.questionDotToken &&
|
||||
!url.questionDotToken &&
|
||||
!target.questionDotToken &&
|
||||
url.arguments[0] &&
|
||||
ts.isCallExpression(url.arguments[0]) &&
|
||||
root &&
|
||||
ts.isIdentifier(root) &&
|
||||
root.text === "driverRoot" &&
|
||||
segments.length === 1 &&
|
||||
entry &&
|
||||
ts.isStringLiteral(entry) &&
|
||||
entry.text === "node-runtime-recovery.mjs" &&
|
||||
isOwnPackageRoot(node, root.text),
|
||||
);
|
||||
}
|
||||
if (
|
||||
owner !== "src/cli/update-cli/update-command-runtime.ts" ||
|
||||
!root ||
|
||||
!ts.isIdentifier(root) ||
|
||||
root.text !== "root" ||
|
||||
|
|
|
|||
|
|
@ -1341,6 +1341,79 @@ describe("previous release update compatibility", () => {
|
|||
},
|
||||
);
|
||||
|
||||
it.each([
|
||||
{ name: "published bootstrap" },
|
||||
{ name: "different owner", owner: "src/cli/update-cli/update-command-runtime.ts" },
|
||||
{
|
||||
name: "mutable root",
|
||||
binding: "let driverRoot = resolveOpenClawPackageRootSync({ moduleUrl: import.meta.url });",
|
||||
},
|
||||
{ name: "unknown root", binding: "" },
|
||||
{ name: "target root", binding: "const driverRoot = root;" },
|
||||
{
|
||||
name: "cwd root",
|
||||
binding: "const driverRoot = resolveOpenClawPackageRootSync({ cwd: process.cwd() });",
|
||||
},
|
||||
{
|
||||
name: "different module",
|
||||
binding: "const driverRoot = resolveOpenClawPackageRootSync({ moduleUrl: targetUrl });",
|
||||
},
|
||||
{
|
||||
name: "extra root options",
|
||||
binding:
|
||||
"const driverRoot = resolveOpenClawPackageRootSync({ moduleUrl: import.meta.url, cwd: root });",
|
||||
},
|
||||
{ name: "extra path", target: 'path.join(driverRoot, "node-runtime-recovery.mjs", "extra")' },
|
||||
{ name: "dist path", target: 'path.join(driverRoot, "dist", "node-runtime-recovery.mjs")' },
|
||||
{ name: "traversal", target: 'path.join(driverRoot, "../node-runtime-recovery.mjs")' },
|
||||
{ name: "dynamic path", target: "path.join(driverRoot, entry)" },
|
||||
{
|
||||
name: "different URL form",
|
||||
url: 'new URL("node-runtime-recovery.mjs", import.meta.url).href',
|
||||
},
|
||||
{ name: "import options", suffix: ", { with: options }" },
|
||||
{ name: "for-of shadow", prefix: "for (const driverRoot of roots) ", declaration: "" },
|
||||
{
|
||||
name: "for initializer shadow",
|
||||
prefix: "for (const driverRoot = root; driverRoot;) ",
|
||||
declaration: "",
|
||||
},
|
||||
{ name: "arrow shadow", prefix: "const load = async (driverRoot) => ", declaration: "" },
|
||||
])(
|
||||
"records dist edges while qualifying the package bootstrap ($name)",
|
||||
({
|
||||
name,
|
||||
owner = "src/cli/update-cli/update-command-node-runtime-resolution.ts",
|
||||
binding = "const driverRoot = resolveOpenClawPackageRootSync({ moduleUrl: import.meta.url });",
|
||||
target = 'path.join(driverRoot, "node-runtime-recovery.mjs")',
|
||||
url = `pathToFileURL(${target}).href`,
|
||||
suffix = "",
|
||||
prefix = "",
|
||||
declaration = "const { findUsableNodeRuntime } = ",
|
||||
}) => {
|
||||
const expression = `await (async () => {
|
||||
${binding}
|
||||
if (!driverRoot) return;
|
||||
${prefix}${declaration}await import(${url}${suffix});
|
||||
return (await import("./surface-abcdefgh.js")).x;
|
||||
})()`;
|
||||
const record = () =>
|
||||
recordImportedFixture(
|
||||
expression,
|
||||
{ "surface-abcdefgh.js": "//#region src/infra/value.ts\nexport const x = 1;\n" },
|
||||
undefined,
|
||||
owner,
|
||||
);
|
||||
if (name !== "published bootstrap") {
|
||||
expect(record).toThrow("Nonliteral post-swap import");
|
||||
return;
|
||||
}
|
||||
expect(record().inventory.releases[0]?.chunks.map((chunk) => chunk.path)).toEqual([
|
||||
"surface-abcdefgh.js",
|
||||
]);
|
||||
},
|
||||
);
|
||||
|
||||
it.each(
|
||||
previousReleaseInventory.releases
|
||||
.filter(({ version }) => ["2026.9.1", "2026.9.2", "2026.9.3", "2026.9.4"].includes(version))
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue