173 lines
5.3 KiB
TypeScript
173 lines
5.3 KiB
TypeScript
import assert from "node:assert/strict";
|
|
import { join } from "node:path";
|
|
import { test } from "vitest";
|
|
|
|
import {
|
|
isUnderNodeModules,
|
|
resolveSubprocessModule,
|
|
} from "../../web/ts-subprocess-flags.ts";
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// isUnderNodeModules — exported utility
|
|
// ---------------------------------------------------------------------------
|
|
|
|
test("isUnderNodeModules returns false for paths outside node_modules", () => {
|
|
assert.equal(isUnderNodeModules("/home/user/projects/sf"), false);
|
|
});
|
|
|
|
test("isUnderNodeModules returns true for Unix paths under node_modules/", () => {
|
|
assert.equal(isUnderNodeModules("/usr/lib/node_modules/sf-run"), true);
|
|
});
|
|
|
|
test("isUnderNodeModules returns true for Windows paths under node_modules/", () => {
|
|
assert.equal(
|
|
isUnderNodeModules("C:\\Users\\dev\\AppData\\node_modules\\sf-run"),
|
|
true,
|
|
);
|
|
});
|
|
|
|
test("isUnderNodeModules returns false for substring match without trailing slash", () => {
|
|
assert.equal(
|
|
isUnderNodeModules("/home/user/my_node_modules_backup/sf"),
|
|
false,
|
|
);
|
|
});
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// resolveSubprocessModule — resolves .ts → dist .js under node_modules
|
|
// ---------------------------------------------------------------------------
|
|
|
|
test("resolveSubprocessModule returns source .ts path when NOT under node_modules", () => {
|
|
const packageRoot = "/home/user/projects/sf";
|
|
const result = resolveSubprocessModule(
|
|
packageRoot,
|
|
"resources/extensions/sf/workspace-index.ts",
|
|
// existsSync not needed — should return src path without checking dist
|
|
);
|
|
|
|
assert.deepEqual(result, {
|
|
modulePath: join(
|
|
packageRoot,
|
|
"src",
|
|
"resources/extensions/sf/workspace-index.ts",
|
|
),
|
|
useCompiledJs: false,
|
|
});
|
|
});
|
|
|
|
test("resolveSubprocessModule returns compiled .js path when under node_modules and dist file exists", () => {
|
|
const packageRoot = "/usr/lib/node_modules/sf-run";
|
|
const distPath = join(
|
|
packageRoot,
|
|
"dist",
|
|
"resources/extensions/sf/workspace-index.js",
|
|
);
|
|
const result = resolveSubprocessModule(
|
|
packageRoot,
|
|
"resources/extensions/sf/workspace-index.ts",
|
|
(p: string) => p === distPath,
|
|
);
|
|
|
|
assert.deepEqual(result, {
|
|
modulePath: distPath,
|
|
useCompiledJs: true,
|
|
});
|
|
});
|
|
|
|
test("resolveSubprocessModule falls back to source .ts when under node_modules but dist file missing", () => {
|
|
const packageRoot = "/usr/lib/node_modules/sf-run";
|
|
const result = resolveSubprocessModule(
|
|
packageRoot,
|
|
"resources/extensions/sf/workspace-index.ts",
|
|
() => false, // dist file does not exist
|
|
);
|
|
|
|
assert.deepEqual(result, {
|
|
modulePath: join(
|
|
packageRoot,
|
|
"src",
|
|
"resources/extensions/sf/workspace-index.ts",
|
|
),
|
|
useCompiledJs: false,
|
|
});
|
|
});
|
|
|
|
test("resolveSubprocessModule handles Windows paths under node_modules", () => {
|
|
const packageRoot = "C:\\Users\\dev\\AppData\\node_modules\\sf-run";
|
|
const distPath = join(packageRoot, "dist", "resources/extensions/sf/auto.js");
|
|
const result = resolveSubprocessModule(
|
|
packageRoot,
|
|
"resources/extensions/sf/auto.ts",
|
|
(p: string) => p === distPath,
|
|
);
|
|
|
|
assert.deepEqual(result, {
|
|
modulePath: distPath,
|
|
useCompiledJs: true,
|
|
});
|
|
});
|
|
|
|
test("resolveSubprocessModule strips .ts extension when building dist .js path", () => {
|
|
const packageRoot = "/usr/lib/node_modules/sf-run";
|
|
let checkedPath = "";
|
|
resolveSubprocessModule(
|
|
packageRoot,
|
|
"resources/extensions/sf/doctor.ts",
|
|
(p: string) => {
|
|
checkedPath = p;
|
|
return true;
|
|
},
|
|
);
|
|
|
|
assert.equal(
|
|
checkedPath,
|
|
join(packageRoot, "dist", "resources/extensions/sf/doctor.js"),
|
|
"should check for .js file in dist/, not .ts",
|
|
);
|
|
});
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Integration: bridge-service subprocess resolution pattern
|
|
// ---------------------------------------------------------------------------
|
|
|
|
test("bridge-service workspace-index subprocess uses compiled JS when under node_modules (source audit)", async () => {
|
|
// Verify bridge-service.ts calls resolveSubprocessModule for workspace-index
|
|
const { readFileSync } = await import("node:fs");
|
|
const bridgeSource = readFileSync(
|
|
join(process.cwd(), "src", "web", "bridge-service.ts"),
|
|
"utf-8",
|
|
);
|
|
|
|
assert.match(
|
|
bridgeSource,
|
|
/resolveSubprocessModule/,
|
|
"bridge-service.ts must use resolveSubprocessModule to resolve workspace-index path — " +
|
|
"hardcoded .ts paths fail with ERR_UNSUPPORTED_NODE_MODULES_TYPE_STRIPPING on Node v26 (see #2279)",
|
|
);
|
|
});
|
|
|
|
test("all web service files use resolveSubprocessModule instead of hardcoded .ts paths (source audit)", async () => {
|
|
const { readFileSync, readdirSync } = await import("node:fs");
|
|
|
|
const serviceFiles = readdirSync(join(process.cwd(), "src", "web")).filter(
|
|
(f: string) => f.endsWith("-service.ts"),
|
|
);
|
|
|
|
for (const file of serviceFiles) {
|
|
const source = readFileSync(
|
|
join(process.cwd(), "src", "web", file),
|
|
"utf-8",
|
|
);
|
|
|
|
// If the service file imports resolveTypeStrippingFlag it spawns subprocesses
|
|
// and must also use resolveSubprocessModule
|
|
if (source.includes("resolveTypeStrippingFlag")) {
|
|
assert.match(
|
|
source,
|
|
/resolveSubprocessModule/,
|
|
`${file} uses resolveTypeStrippingFlag but does not use resolveSubprocessModule — ` +
|
|
"subprocess .ts paths will fail under node_modules/ on Node v26 (#2279)",
|
|
);
|
|
}
|
|
}
|
|
});
|