mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-12 21:53:00 -06:00
fix(scripts): anchor hydrated tsx at checkout root (#121149)
Resolve pnpm's configured modules directory from the checkout root before TypeScript boot, preserving both environment spellings and canonical checkout/common-worktree fallbacks. Use Node package self-resolution from the hydrated tsx package so nested wrappers cannot change relative path semantics.
This commit is contained in:
committed by
GitHub
parent
f9eae0c276
commit
cfe6ebcd1e
@@ -58,7 +58,7 @@ const WINDOWS_SQLITE_SCOPE_RE = /^src\/(?:state\/|.*sqlite.*\.ts$)/;
|
||||
const WINDOWS_FILE_URL_SCOPE_RE =
|
||||
/^(?:src\/media\/(?:local-media-path(?:\.windows\.test)?|local-roots(?:\.test)?|web-media(?:\.file-url\.windows\.test)?)|src\/channels\/inbound-event\/media(?:\.test)?|src\/gateway\/managed-image-attachments(?:\.test)?|extensions\/msteams\/src\/(?:media-helpers|messenger)(?:\.test)?)\.ts$/;
|
||||
const WINDOWS_SCOPE_RE =
|
||||
/^(extensions\/mxc\/|src\/agents\/(?:bash-tools\.exec-script-(?:preflight|target)|bash-tools\.exec\.script-preflight\.test)\.ts$|src\/config\/sessions\/(?:session-accessor\.sqlite-archive(?:\.worker(?:\.test)?)?|store\.session-lifecycle-mutation\.test)\.ts$|src\/process\/|src\/infra\/(?:(?:exec-allowlist-pattern|fs-safe-remove)(?:\.test)?|ssh-client(?:\.windows\.test)?|update-managed-service-handoff(?:-(?:command|lifecycle)\.test)?|windows-install-roots)\.ts$|src\/shared\/(?:import-specifier|runtime-import)(?:\.test)?\.ts$|src\/test-utils\/openclaw-test-state(?:\.test)?\.ts$|scripts\/(?:android-(?:app-i18n|pin-version)\.ts|ci-run-timings\.mjs|e2e\/lib\/package-compat\.mjs|generate-bundled-channel-config-metadata\.ts|install\.ps1|openclaw-cross-os-release-checks\.ts|plan-release-workflow-matrix\.mjs|run-additional-boundary-checks\.mts|verify-docker-attestations\.mjs|github\/run-openclaw-cross-os-release-checks\.sh|(?:npm-runner|pnpm-runner|ui|vitest-process-group)\.(?:mjs|mts|js)|lib\/(?:direct-run\.(?:mjs|mts)|format-generated-module\.mts|cross-os-release-checks\/[^/]+\.ts))$|test\/scripts\/(?:direct-run-entrypoints|format-generated-module|install-ps1|npm-runner|openclaw-cross-os-release-workflow|pnpm-runner|ui|vitest-process-group)\.test\.ts$|package\.json$|pnpm-lock\.yaml$|pnpm-workspace\.yaml$|\.github\/workflows\/(?:ci|openclaw-cross-os-release-checks-reusable)\.yml$|\.github\/actions\/setup-node-env\/action\.yml$|\.github\/actions\/setup-pnpm-store-cache\/action\.yml$)/;
|
||||
/^(extensions\/mxc\/|src\/agents\/(?:bash-tools\.exec-script-(?:preflight|target)|bash-tools\.exec\.script-preflight\.test)\.ts$|src\/config\/sessions\/(?:session-accessor\.sqlite-archive(?:\.worker(?:\.test)?)?|store\.session-lifecycle-mutation\.test)\.ts$|src\/process\/|src\/infra\/(?:(?:exec-allowlist-pattern|fs-safe-remove)(?:\.test)?|ssh-client(?:\.windows\.test)?|update-managed-service-handoff(?:-(?:command|lifecycle)\.test)?|windows-install-roots)\.ts$|src\/shared\/(?:import-specifier|runtime-import)(?:\.test)?\.ts$|src\/test-utils\/openclaw-test-state(?:\.test)?\.ts$|scripts\/(?:android-(?:app-i18n|pin-version)\.ts|ci-run-timings\.mjs|e2e\/lib\/package-compat\.mjs|generate-bundled-channel-config-metadata\.ts|install\.ps1|openclaw-cross-os-release-checks\.ts|plan-release-workflow-matrix\.mjs|run-additional-boundary-checks\.mts|verify-docker-attestations\.mjs|github\/run-openclaw-cross-os-release-checks\.sh|(?:npm-runner|pnpm-runner|ui|vitest-process-group)\.(?:mjs|mts|js)|lib\/(?:direct-run\.(?:mjs|mts)|format-generated-module\.mts|tsx-cli-shim\.mjs|cross-os-release-checks\/[^/]+\.ts))$|test\/scripts\/(?:direct-run-entrypoints|format-generated-module|install-ps1|npm-runner|openclaw-cross-os-release-workflow|pnpm-runner|ui|vitest-process-group)\.test\.ts$|package\.json$|pnpm-lock\.yaml$|pnpm-workspace\.yaml$|\.github\/workflows\/(?:ci|openclaw-cross-os-release-checks-reusable)\.yml$|\.github\/actions\/setup-node-env\/action\.yml$|\.github\/actions\/setup-pnpm-store-cache\/action\.yml$)/;
|
||||
const WINDOWS_TEST_SCOPE_RE =
|
||||
/^(extensions\/mxc\/test\/(?:mxc-backend|sandbox-policy-loader)\.test\.ts$|src\/agents\/bash-tools\.exec\.script-preflight\.test\.ts$|src\/config\/sessions\/(?:session-accessor\.sqlite-archive\.worker|store\.session-lifecycle-mutation)\.test\.ts$|src\/process\/(?:exec\.windows|windows-command)\.test\.ts$|src\/infra\/(?:exec-allowlist-pattern|fs-safe-remove|ssh-client\.windows|update-managed-service-handoff-(?:command|lifecycle)|windows-install-roots)\.test\.ts$|src\/shared\/runtime-import\.test\.ts$|src\/state\/openclaw-database-paths\.windows\.test\.ts$|src\/test-utils\/openclaw-test-state\.test\.ts$|test\/scripts\/(?:direct-run-entrypoints|format-generated-module|npm-runner|openclaw-cross-os-release-workflow|pnpm-runner|ui|vitest-process-group)\.test\.ts$)/;
|
||||
const WINDOWS_SECRETREF_SCOPE_RE =
|
||||
|
||||
@@ -7,10 +7,11 @@ import { fileURLToPath, pathToFileURL } from "node:url";
|
||||
|
||||
const FORWARDED_SIGNALS = ["SIGINT", "SIGTERM", "SIGHUP"];
|
||||
const FORCE_KILL_DELAY_MS = 5_000;
|
||||
const SHIM_CHECKOUT_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..", "..");
|
||||
|
||||
function resolvePrimaryRoot(repoRoot) {
|
||||
function resolvePrimaryRoot(checkoutRoot) {
|
||||
const result = spawnSync("git", ["rev-parse", "--git-common-dir"], {
|
||||
cwd: repoRoot,
|
||||
cwd: checkoutRoot,
|
||||
encoding: "utf8",
|
||||
stdio: ["ignore", "pipe", "ignore"],
|
||||
});
|
||||
@@ -21,13 +22,22 @@ function resolvePrimaryRoot(repoRoot) {
|
||||
if (!commonDir) {
|
||||
return null;
|
||||
}
|
||||
const resolved = path.resolve(repoRoot, commonDir);
|
||||
const resolved = path.resolve(checkoutRoot, commonDir);
|
||||
return path.basename(resolved) === ".git" ? path.dirname(resolved) : null;
|
||||
}
|
||||
|
||||
function resolveTsxImport(repoRoot) {
|
||||
function resolveTsxImport(checkoutRoot) {
|
||||
const modulesDir =
|
||||
process.env.PNPM_CONFIG_MODULES_DIR?.trim() || process.env.npm_config_modules_dir?.trim();
|
||||
const hydratedTsxRoot = modulesDir
|
||||
? path.join(path.resolve(checkoutRoot, modulesDir), "tsx")
|
||||
: null;
|
||||
let resolutionError;
|
||||
for (const candidateRoot of [repoRoot, resolvePrimaryRoot(repoRoot)].filter(Boolean)) {
|
||||
for (const candidateRoot of [
|
||||
hydratedTsxRoot,
|
||||
checkoutRoot,
|
||||
resolvePrimaryRoot(checkoutRoot),
|
||||
].filter(Boolean)) {
|
||||
try {
|
||||
const require = createRequire(path.join(candidateRoot, "package.json"));
|
||||
return pathToFileURL(require.resolve("tsx")).href;
|
||||
@@ -100,8 +110,7 @@ async function runTsxCliShimInner(moduleUrl, options) {
|
||||
try {
|
||||
const implementationUrl = new URL(options.implementation, moduleUrl);
|
||||
const implementationPath = fileURLToPath(implementationUrl);
|
||||
const repoRoot = path.resolve(path.dirname(implementationPath), "..");
|
||||
const tsxImport = resolveTsxImport(repoRoot);
|
||||
const tsxImport = resolveTsxImport(SHIM_CHECKOUT_ROOT);
|
||||
const nodeExecutable = process.versions.bun ? "node" : process.execPath;
|
||||
child = spawn(
|
||||
nodeExecutable,
|
||||
|
||||
@@ -1,5 +1,14 @@
|
||||
import { spawnSync } from "node:child_process";
|
||||
import { readFileSync } from "node:fs";
|
||||
import {
|
||||
copyFileSync,
|
||||
mkdirSync,
|
||||
mkdtempSync,
|
||||
readFileSync,
|
||||
realpathSync,
|
||||
rmSync,
|
||||
writeFileSync,
|
||||
} from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import path from "node:path";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { detectChangedScope } from "../../scripts/ci-changed-scope.mjs";
|
||||
@@ -71,6 +80,78 @@ function runEntrypoint(entrypoint: (typeof EXECUTABLE_ENTRYPOINTS)[number]) {
|
||||
});
|
||||
}
|
||||
|
||||
const TSX_SHIM_WRAPPERS = [
|
||||
"scripts/run-vitest.mjs",
|
||||
"scripts/lib/plugin-npm-package-manifest.mjs",
|
||||
"scripts/e2e/kitchen-sink-rpc-walk.mjs",
|
||||
"scripts/perf/summarize-cpuprofile.mjs",
|
||||
] as const;
|
||||
|
||||
type ModulesEnv = Partial<Record<"PNPM_CONFIG_MODULES_DIR" | "npm_config_modules_dir", string>>;
|
||||
|
||||
function writeTsxFixture(modulesDir: string, marker: string) {
|
||||
const packageDir = path.join(modulesDir, "tsx");
|
||||
mkdirSync(packageDir, { recursive: true });
|
||||
writeFileSync(
|
||||
path.join(packageDir, "package.json"),
|
||||
JSON.stringify({ name: "tsx", type: "module", exports: "./loader.mjs" }),
|
||||
);
|
||||
writeFileSync(
|
||||
path.join(packageDir, "loader.mjs"),
|
||||
`process.env.OPENCLAW_TSX_FIXTURE_LOADER = ${JSON.stringify(marker)};\n`,
|
||||
);
|
||||
}
|
||||
|
||||
function runShimFixture(
|
||||
wrapper: (typeof TSX_SHIM_WRAPPERS)[number],
|
||||
configureModules: (paths: {
|
||||
checkoutRoot: string;
|
||||
fixtureRoot: string;
|
||||
}) => ModulesEnv = () => ({}),
|
||||
) {
|
||||
const fixtureRoot = realpathSync(mkdtempSync(path.join(tmpdir(), "openclaw-tsx-cli-shim-")));
|
||||
const checkoutRoot = path.join(fixtureRoot, "checkout");
|
||||
const wrapperPath = path.join(checkoutRoot, wrapper);
|
||||
const implementationPath = wrapperPath.replace(/\.mjs$/u, ".mts");
|
||||
try {
|
||||
mkdirSync(path.dirname(wrapperPath), { recursive: true });
|
||||
mkdirSync(path.join(checkoutRoot, "scripts", "lib"), { recursive: true });
|
||||
copyFileSync(wrapper, wrapperPath);
|
||||
copyFileSync(
|
||||
"scripts/lib/tsx-cli-shim.mjs",
|
||||
path.join(checkoutRoot, "scripts", "lib", "tsx-cli-shim.mjs"),
|
||||
);
|
||||
writeFileSync(path.join(checkoutRoot, "pnpm-lock.yaml"), "lockfileVersion: '9.0'\n");
|
||||
writeFileSync(
|
||||
implementationPath,
|
||||
"process.stdout.write(JSON.stringify({ loader: process.env.OPENCLAW_TSX_FIXTURE_LOADER, args: process.argv.slice(2) }));\n",
|
||||
);
|
||||
writeTsxFixture(path.join(checkoutRoot, "node_modules"), "checkout");
|
||||
const modulesEnv = configureModules({ checkoutRoot, fixtureRoot });
|
||||
|
||||
const env = { ...process.env };
|
||||
delete env.NODE_OPTIONS;
|
||||
delete env.NODE_PATH;
|
||||
delete env.PNPM_CONFIG_MODULES_DIR;
|
||||
delete env.npm_config_modules_dir;
|
||||
Object.assign(env, modulesEnv);
|
||||
return spawnSync(process.execPath, [wrapperPath, "--hydrated-proof"], {
|
||||
cwd: fixtureRoot,
|
||||
encoding: "utf8",
|
||||
env,
|
||||
timeout: 10_000,
|
||||
});
|
||||
} finally {
|
||||
rmSync(fixtureRoot, { recursive: true, force: true });
|
||||
}
|
||||
}
|
||||
|
||||
function expectShimLoader(result: ReturnType<typeof runShimFixture>, loader: string) {
|
||||
expect(result.error).toBeUndefined();
|
||||
expect(result.status, result.stderr).toBe(0);
|
||||
expect(JSON.parse(result.stdout)).toEqual({ loader, args: ["--hydrated-proof"] });
|
||||
}
|
||||
|
||||
describe("script direct-run entrypoints", () => {
|
||||
it.each(EXECUTABLE_ENTRYPOINTS)("runs $script through its guarded CLI", (entrypoint) => {
|
||||
const result = runEntrypoint(entrypoint);
|
||||
@@ -81,6 +162,40 @@ describe("script direct-run entrypoints", () => {
|
||||
expect(output).toContain(entrypoint.output);
|
||||
});
|
||||
|
||||
it.each([
|
||||
{ envKey: "PNPM_CONFIG_MODULES_DIR", mode: "absolute", wrapper: TSX_SHIM_WRAPPERS[0] },
|
||||
{ envKey: "npm_config_modules_dir", mode: "relative", wrapper: TSX_SHIM_WRAPPERS[1] },
|
||||
{ envKey: "PNPM_CONFIG_MODULES_DIR", mode: "relative", wrapper: TSX_SHIM_WRAPPERS[2] },
|
||||
{ envKey: "npm_config_modules_dir", mode: "absolute", wrapper: TSX_SHIM_WRAPPERS[3] },
|
||||
] as const)("boots $wrapper from a $mode $envKey", ({ envKey, mode, wrapper }) => {
|
||||
const result = runShimFixture(wrapper, ({ checkoutRoot, fixtureRoot }) => {
|
||||
const modulesDir = path.join(fixtureRoot, "hydrated-modules");
|
||||
writeTsxFixture(modulesDir, "hydrated");
|
||||
const configuredDir =
|
||||
mode === "absolute" ? modulesDir : path.relative(checkoutRoot, modulesDir);
|
||||
return { [envKey]: configuredDir };
|
||||
});
|
||||
expectShimLoader(result, "hydrated");
|
||||
});
|
||||
|
||||
it("prefers PNPM_CONFIG_MODULES_DIR over npm_config_modules_dir", () => {
|
||||
const result = runShimFixture(TSX_SHIM_WRAPPERS[2], ({ fixtureRoot }) => {
|
||||
const preferredDir = path.join(fixtureRoot, "preferred-modules");
|
||||
const fallbackDir = path.join(fixtureRoot, "fallback-modules");
|
||||
writeTsxFixture(preferredDir, "preferred");
|
||||
writeTsxFixture(fallbackDir, "lowercase");
|
||||
return {
|
||||
PNPM_CONFIG_MODULES_DIR: preferredDir,
|
||||
npm_config_modules_dir: fallbackDir,
|
||||
};
|
||||
});
|
||||
expectShimLoader(result, "preferred");
|
||||
});
|
||||
|
||||
it("falls back to checkout dependencies without an external modules directory", () => {
|
||||
expectShimLoader(runShimFixture(TSX_SHIM_WRAPPERS[3]), "checkout");
|
||||
});
|
||||
|
||||
it("matches Windows drive paths case-insensitively", () => {
|
||||
expect(
|
||||
isDirectRunPath(
|
||||
@@ -102,6 +217,7 @@ describe("script direct-run entrypoints", () => {
|
||||
it.each([
|
||||
...DIRECT_RUN_SCRIPTS,
|
||||
"scripts/lib/direct-run.mjs",
|
||||
"scripts/lib/tsx-cli-shim.mjs",
|
||||
"test/scripts/direct-run-entrypoints.test.ts",
|
||||
])("routes %s through Windows CI", (changedPath) => {
|
||||
expect(detectChangedScope([changedPath]).runWindows).toBe(true);
|
||||
|
||||
Reference in New Issue
Block a user