Files
openclaw/scripts/test-built-bundled-runtime-deps.mjs
2026-04-27 08:50:56 +01:00

251 lines
8.0 KiB
JavaScript

import assert from "node:assert/strict";
import { spawnSync } from "node:child_process";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { pathToFileURL } from "node:url";
import {
collectBuiltBundledPluginStagedRuntimeDependencyErrors,
collectBundledPluginRootRuntimeMirrorErrors,
collectBundledPluginRuntimeDependencySpecs,
collectRootDistBundledRuntimeMirrors,
} from "./lib/bundled-plugin-root-runtime-mirrors.mjs";
import { parsePackageRootArg } from "./lib/package-root-args.mjs";
const { packageRoot } = parsePackageRootArg(
process.argv.slice(2),
"OPENCLAW_BUNDLED_RUNTIME_DEPS_ROOT",
);
const rootPackageJsonPath = path.join(packageRoot, "package.json");
const builtPluginsDir = path.join(packageRoot, "dist", "extensions");
assert.ok(fs.existsSync(rootPackageJsonPath), `package.json missing from ${packageRoot}`);
assert.ok(fs.existsSync(builtPluginsDir), `built bundled plugins missing from ${builtPluginsDir}`);
const rootPackageJson = JSON.parse(fs.readFileSync(rootPackageJsonPath, "utf8"));
const bundledRuntimeDependencySpecs = collectBundledPluginRuntimeDependencySpecs(
path.join(packageRoot, "extensions"),
);
const requiredRootMirrors = collectRootDistBundledRuntimeMirrors({
bundledRuntimeDependencySpecs,
distDir: path.join(packageRoot, "dist"),
});
const errors = [
...collectBundledPluginRootRuntimeMirrorErrors({
bundledRuntimeDependencySpecs,
requiredRootMirrors,
rootPackageJson,
}),
...collectBuiltBundledPluginStagedRuntimeDependencyErrors({
bundledPluginsDir: builtPluginsDir,
}),
];
assert.deepEqual(errors, [], errors.join("\n"));
function packageNodeModulesPath(nodeModulesDir, packageName) {
return path.join(nodeModulesDir, ...packageName.split("/"));
}
function stageBrowserRuntimeDependencyStub(stageNodeModulesDir, packageName) {
const packageDir = packageNodeModulesPath(stageNodeModulesDir, packageName);
fs.mkdirSync(packageDir, { recursive: true });
fs.writeFileSync(
path.join(packageDir, "package.json"),
`${JSON.stringify(
{
name: packageName,
version: "0.0.0",
main: "./index.cjs",
},
null,
2,
)}\n`,
"utf8",
);
if (packageName === "playwright-core") {
fs.writeFileSync(
path.join(packageDir, "index.cjs"),
[
"module.exports = {",
" chromium: { marker: 'stub-chromium' },",
" devices: { 'Stub Device': { marker: 'stub-device' } },",
"};",
"",
].join("\n"),
"utf8",
);
return;
}
if (packageName === "typebox") {
fs.writeFileSync(
path.join(packageDir, "index.cjs"),
[
"const createSchema = (kind, value = {}) => ({ kind, ...value });",
"const Type = new Proxy(function Type() {}, {",
" get(_target, prop) {",
" if (prop === Symbol.toStringTag) {",
" return 'Type';",
" }",
" return (...args) => createSchema(String(prop), { args });",
" },",
"});",
"module.exports = { Type };",
"",
].join("\n"),
"utf8",
);
return;
}
fs.writeFileSync(path.join(packageDir, "index.cjs"), "module.exports = {};\n", "utf8");
}
function findBuiltBrowserEntryPath(distDir) {
const candidates = fs
.readdirSync(distDir, { withFileTypes: true })
.filter((entry) => entry.isFile() && /^pw-ai-(?!state-).*\.js$/u.test(entry.name))
.map((entry) => path.join(distDir, entry.name))
.toSorted((left, right) => left.localeCompare(right));
if (candidates.length === 0) {
throw new assert.AssertionError({
message: `missing built pw-ai entry under ${distDir}`,
});
}
return candidates[0];
}
function createBuiltBrowserImportSmokeFixture(packageRoot) {
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-built-browser-smoke-"));
const tempDistDir = path.join(tempRoot, "dist");
const tempNodeModulesDir = path.join(tempRoot, "node_modules");
const stageNodeModulesDir = path.join(
tempRoot,
".openclaw",
"plugin-runtime-deps",
"browser",
"node_modules",
);
fs.cpSync(path.join(packageRoot, "dist"), tempDistDir, {
recursive: true,
dereference: true,
});
fs.copyFileSync(path.join(packageRoot, "package.json"), path.join(tempRoot, "package.json"));
fs.cpSync(path.join(packageRoot, "node_modules"), tempNodeModulesDir, {
recursive: true,
dereference: true,
});
fs.rmSync(path.join(tempNodeModulesDir, "playwright-core"), {
force: true,
recursive: true,
});
assert.ok(!fs.existsSync(path.join(tempNodeModulesDir, "playwright-core")));
fs.mkdirSync(stageNodeModulesDir, { recursive: true });
assert.deepEqual(fs.readdirSync(stageNodeModulesDir), []);
const browserPackageJson = JSON.parse(
fs.readFileSync(path.join(tempDistDir, "extensions", "browser", "package.json"), "utf8"),
);
const browserRuntimeDeps = new Map(
[
...Object.entries(browserPackageJson.dependencies ?? {}),
...Object.entries(browserPackageJson.optionalDependencies ?? {}),
].filter((entry) => typeof entry[1] === "string" && entry[1].length > 0),
);
const missingBrowserRuntimeDeps = [...browserRuntimeDeps.keys()]
.filter((packageName) => {
const rootSentinel = path.join(tempNodeModulesDir, ...packageName.split("/"), "package.json");
const stagedSentinel = path.join(
stageNodeModulesDir,
...packageName.split("/"),
"package.json",
);
return !fs.existsSync(rootSentinel) && !fs.existsSync(stagedSentinel);
})
.toSorted((left, right) => left.localeCompare(right));
for (const packageName of missingBrowserRuntimeDeps) {
stageBrowserRuntimeDependencyStub(stageNodeModulesDir, packageName);
}
return {
entryPath: findBuiltBrowserEntryPath(tempDistDir),
stageNodeModulesDir,
tempRoot,
};
}
function runNodeEval(params) {
return spawnSync(process.execPath, ["--input-type=module", "--eval", params.source], {
cwd: params.cwd,
encoding: "utf8",
env: params.env,
});
}
function runBuiltBrowserImportSmoke(packageRoot) {
const fixture = createBuiltBrowserImportSmokeFixture(packageRoot);
try {
assert.ok(fs.existsSync(fixture.entryPath), `missing built pw-ai entry: ${fixture.entryPath}`);
assert.ok(
!fs.existsSync(path.join(fixture.tempRoot, "node_modules", "playwright-core")),
"package-root playwright-core should be absent in the smoke fixture",
);
assert.ok(
fs.existsSync(path.join(fixture.stageNodeModulesDir, "playwright-core", "package.json")),
"staged playwright-core should be present in the smoke fixture",
);
const rootEsmResult = runNodeEval({
cwd: fixture.tempRoot,
env: { ...process.env, NODE_PATH: fixture.stageNodeModulesDir },
source:
"await import('playwright-core')" +
".then(() => { process.exitCode = 1; })" +
".catch((error) => { if (error?.code !== 'ERR_MODULE_NOT_FOUND') throw error; });",
});
assert.equal(
rootEsmResult.status,
0,
[
"[build-smoke] native ESM unexpectedly resolved staged playwright-core",
rootEsmResult.stdout.trim(),
rootEsmResult.stderr.trim(),
]
.filter(Boolean)
.join("\n"),
);
const builtImportResult = runNodeEval({
cwd: fixture.tempRoot,
env: { ...process.env, NODE_PATH: fixture.stageNodeModulesDir },
source: `await import(${JSON.stringify(pathToFileURL(fixture.entryPath).href)});`,
});
assert.equal(
builtImportResult.status,
0,
[
"[build-smoke] built browser pw-ai import failed",
`status=${String(builtImportResult.status)}`,
`signal=${String(builtImportResult.signal)}`,
builtImportResult.stdout.trim(),
builtImportResult.stderr.trim(),
]
.filter(Boolean)
.join("\n"),
);
} finally {
fs.rmSync(fixture.tempRoot, { recursive: true, force: true });
}
}
runBuiltBrowserImportSmoke(packageRoot);
process.stdout.write(
`[build-smoke] bundled runtime dependency smoke passed packageRoot=${packageRoot}\n`,
);