mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-10-21 05:50:55 +08:00
- Enforce oxlint and oxfmt for owned JavaScript and TypeScript with independent CI checks. - Preserve compiler import boundaries and protected fixtures while cleaning up lint findings. - Standardize source formatting and validate import, fixture, and embedded-source protections.
470 lines
15 KiB
TypeScript
470 lines
15 KiB
TypeScript
import { spawnSync } from "node:child_process";
|
|
import { mkdirSync, mkdtempSync, realpathSync, rmSync, symlinkSync, writeFileSync } from "node:fs";
|
|
import { createRequire } from "node:module";
|
|
import { tmpdir } from "node:os";
|
|
import { dirname, join, resolve, sep } from "node:path";
|
|
import { fileURLToPath } from "node:url";
|
|
import { expect, test } from "vitest";
|
|
import { analyze, compile } from "@scriptc/compiler";
|
|
import {
|
|
cjsResolvePaths,
|
|
resolveCjsRuntime,
|
|
} from "../../packages/compiler/src/frontend/cjs-resolve.js";
|
|
import { FrontendInputTracker } from "../../packages/compiler/src/frontend/input-tracker.js";
|
|
|
|
const root = fileURLToPath(new URL("../..", import.meta.url));
|
|
const entry = join(root, "tests/fixtures/self-hosting/runtime-resolver.ts");
|
|
|
|
interface Request {
|
|
from: string;
|
|
request: string;
|
|
paths: string[] | null;
|
|
lookup: boolean;
|
|
}
|
|
|
|
function makeCases(directory: string): Request[] {
|
|
const cases: Request[] = [];
|
|
const main = join(directory, "app/main.cjs");
|
|
function file(
|
|
path: string,
|
|
text = 'throw new Error("resolution must never evaluate code");',
|
|
): void {
|
|
mkdirSync(dirname(path), { recursive: true });
|
|
writeFileSync(path, text);
|
|
}
|
|
function pkg(
|
|
name: string,
|
|
config: unknown,
|
|
files: string[] = ["index.js"],
|
|
parent = "app/node_modules",
|
|
): string {
|
|
const path = join(directory, parent, name);
|
|
file(join(path, "package.json"), JSON.stringify(config));
|
|
for (const name of files) file(join(path, name));
|
|
return path;
|
|
}
|
|
function add(
|
|
requests: string[],
|
|
from = main,
|
|
paths: string[] | null = null,
|
|
lookup = false,
|
|
): void {
|
|
for (const request of requests) cases.push({ from, request, paths, lookup });
|
|
}
|
|
file(main);
|
|
file(
|
|
join(directory, "app/package.json"),
|
|
JSON.stringify({
|
|
name: "application",
|
|
exports: { ".": "./main.cjs", "./self": "./same.js", "./missing": "./absent.js" },
|
|
imports: {
|
|
"#same": "./same.js",
|
|
"#conditional": { types: "./wrong.d.ts", require: "./same.js", default: "./wrong.js" },
|
|
"#pattern/*": "./parts/*.js",
|
|
"#/prefix/*": "./parts/*.js",
|
|
"#blocked": null,
|
|
"#dependency": "dual",
|
|
"#dependency-subpath": "legacy/sub",
|
|
"#dependency-main": "legacy",
|
|
"#missing": "missing-package",
|
|
"#missing-file": "./absent.js",
|
|
"#builtin": "fs",
|
|
"#node-prefix": "node:fs",
|
|
"#invalid-name": ".",
|
|
"#outside": "../outside.js",
|
|
"#self": "application/self",
|
|
"#bad-main": "bad-main",
|
|
"#no-entry": "no-entry",
|
|
},
|
|
}),
|
|
);
|
|
for (const name of [
|
|
"same.js",
|
|
"same.json",
|
|
"same.node",
|
|
"data.json",
|
|
"addon.node",
|
|
"explicit.mjs",
|
|
"explicit.cjs",
|
|
"typed.ts",
|
|
"parts/a.js",
|
|
"hash#file.js",
|
|
"space name.js",
|
|
])
|
|
file(join(directory, "app", name));
|
|
file(join(directory, "app/folder/index.json"), "{}");
|
|
file(join(directory, "app/only-mjs/index.mjs"));
|
|
file(join(directory, "app/only-cjs/index.cjs"));
|
|
file(join(directory, "outside.js"));
|
|
pkg(
|
|
"dual",
|
|
{
|
|
exports: {
|
|
".": { types: "./wrong.d.ts", import: "./wrong.mjs", require: "./right.cjs" },
|
|
"./sub": "./feature.js",
|
|
},
|
|
},
|
|
["right.cjs", "feature.js", "wrong.d.ts", "wrong.mjs"],
|
|
);
|
|
pkg("legacy", { main: "start" }, ["start.js", "sub.js", "index.js"]);
|
|
pkg("module-field", { module: "wrong.mjs", main: "index.js" }, ["index.js", "wrong.mjs"]);
|
|
pkg("directory-main", { main: "entry" }, ["entry/index.json"]);
|
|
pkg("nested-main", { main: "entry" }, ["entry/index.js", "entry/wrong.js"]);
|
|
file(join(directory, "app/node_modules/nested-main/entry/package.json"), '{"main":"wrong.js"}');
|
|
pkg("bad-main", { main: "missing" }, []);
|
|
pkg("no-main", {}, ["index.node"]);
|
|
pkg("null-exports", { exports: null, main: "index.js" });
|
|
pkg("self-null", { name: "self-null", exports: null });
|
|
pkg("no-entry", {}, []);
|
|
for (const [index, value] of [null, 123, false, "str", [], {}].entries()) {
|
|
pkg(`imports-${index}`, { imports: value });
|
|
add(["#missing"], join(directory, `app/node_modules/imports-${index}/main.cjs`));
|
|
}
|
|
for (const [index, value] of [true, null, 123, [], "text"].entries()) {
|
|
pkg(`invalid-config-${index}`, value);
|
|
add([`invalid-config-${index}`]);
|
|
}
|
|
file(join(directory, "app/node_modules/bom/package.json"), '\ufeff{"main":"index.js"}');
|
|
file(join(directory, "app/node_modules/bom/index.js"));
|
|
file(join(directory, "app/node_modules/manifest-free/index.js"));
|
|
pkg("@scope/pkg", { exports: { ".": "./index.js", "./sub": "./sub.js" } }, [
|
|
"index.js",
|
|
"sub.js",
|
|
]);
|
|
pkg(
|
|
"patterns",
|
|
{
|
|
exports: {
|
|
"./*": "./general/*.js",
|
|
"./long/*": "./long/*.js",
|
|
"./long/*.js": "./specific/*.js",
|
|
"./exact": "./exact.js",
|
|
},
|
|
},
|
|
["general/a.js", "long/a.js", "specific/a.js", "exact.js"],
|
|
);
|
|
pkg(
|
|
"conditions",
|
|
{
|
|
exports: {
|
|
".": { node: { require: "./node.js" }, default: "./default.js" },
|
|
"./default-first": { default: "./default.js", require: "./node.js" },
|
|
"./null-first": { require: null, default: "./default.js" },
|
|
"./nested-unmatched": { node: { unknown: "./node.js" }, default: "./default.js" },
|
|
"./sync": { "module-sync": "./node.js", default: "./default.js" },
|
|
"./addons": { "node-addons": "./node.js", default: "./default.js" },
|
|
},
|
|
},
|
|
["node.js", "default.js"],
|
|
);
|
|
pkg("arrays", {
|
|
exports: {
|
|
".": ["../invalid", null, "./index.js"],
|
|
"./invalid": ["../bad", "../worse"],
|
|
"./null": ["../bad", null],
|
|
"./missing": ["./missing.js", "./index.js"],
|
|
"./unmatched": [{ unknown: "./missing.js" }, "./index.js"],
|
|
"./empty": [],
|
|
},
|
|
});
|
|
for (const [name, value] of [
|
|
["invalid-target", "outside"],
|
|
["invalid-number", 123],
|
|
["mixed-map", { ".": "./index.js", require: "./index.js" }],
|
|
["numeric-condition", { "0": "./index.js", require: "./index.js" }],
|
|
["encoded-separator", "./a%2fb.js"],
|
|
["traversal-target", "./x/../index.js"],
|
|
["encoded-traversal", "./%2e%2e/outside.js"],
|
|
["node-modules-target", "./node_modules/a.js"],
|
|
["directory-target", "./entry"],
|
|
["extension-target", "./index"],
|
|
["malformed-escape", "./%E0.js"],
|
|
] as const)
|
|
pkg(name, { exports: value }, ["index.js", "entry/index.js"]);
|
|
file(join(directory, "app/node_modules/malformed/package.json"), "{ broken");
|
|
pkg("scoped-errors", {
|
|
name: "scoped-errors",
|
|
exports: { "./num": { "0": "./index.js" }, "./mix": { require: "outside" } },
|
|
});
|
|
const linked = pkg("linked", { exports: "./index.js" }, ["index.js"], "store");
|
|
symlinkSync(
|
|
linked,
|
|
join(directory, "app/node_modules/linked"),
|
|
process.platform === "win32" ? "junction" : "dir",
|
|
);
|
|
pkg("preferred", { main: "index.js" }, ["index.js"], "vendor/node_modules");
|
|
pkg("preferred", { main: "index.js" }, ["index.js"]);
|
|
pkg("nested", {}, ["index.js"], "app/node_modules/outer/node_modules");
|
|
file(join(directory, "app/node_modules/outer/main.cjs"));
|
|
file(join(directory, "app/node_modules/outer/package.json"), '{"name":"outer"}');
|
|
file(join(directory, "standalone/main.cjs"));
|
|
add([
|
|
"fs",
|
|
"node:fs",
|
|
"node:test",
|
|
"node:missing",
|
|
"missing-package",
|
|
"./missing",
|
|
"./same",
|
|
"./same.js",
|
|
"./data",
|
|
"./addon",
|
|
"./explicit.mjs",
|
|
"./explicit",
|
|
"./explicit.cjs",
|
|
"./typed",
|
|
"./typed.ts",
|
|
"./folder",
|
|
"./folder/",
|
|
"./only-mjs",
|
|
"./only-cjs",
|
|
"./hash#file.js",
|
|
"./space name.js",
|
|
"../outside",
|
|
"./same/",
|
|
"",
|
|
".",
|
|
"..",
|
|
]);
|
|
add([join(directory, "app/same.js"), join(directory, "app/folder"), join(directory, "absent")]);
|
|
add([
|
|
"dual",
|
|
"dual/sub",
|
|
"dual/private",
|
|
"legacy",
|
|
"legacy/sub",
|
|
"module-field",
|
|
"directory-main",
|
|
"nested-main",
|
|
"bad-main",
|
|
"no-main",
|
|
"null-exports",
|
|
"manifest-free",
|
|
"@scope/pkg",
|
|
"@scope/pkg/sub",
|
|
"@scope/pkg/private",
|
|
"linked",
|
|
]);
|
|
add([
|
|
"patterns/a",
|
|
"patterns/long/a",
|
|
"patterns/long/a.js",
|
|
"patterns/exact",
|
|
"patterns/missing",
|
|
"patterns/../x",
|
|
"patterns/%2e%2e/x",
|
|
"patterns/node_modules/x",
|
|
]);
|
|
add([
|
|
"conditions",
|
|
"conditions/default-first",
|
|
"conditions/null-first",
|
|
"conditions/nested-unmatched",
|
|
"conditions/sync",
|
|
"conditions/addons",
|
|
]);
|
|
add([
|
|
"arrays",
|
|
"arrays/invalid",
|
|
"arrays/null",
|
|
"arrays/missing",
|
|
"arrays/unmatched",
|
|
"arrays/empty",
|
|
]);
|
|
add([
|
|
"invalid-target",
|
|
"invalid-number",
|
|
"mixed-map",
|
|
"numeric-condition",
|
|
"encoded-separator",
|
|
"traversal-target",
|
|
"encoded-traversal",
|
|
"node-modules-target",
|
|
"directory-target",
|
|
"extension-target",
|
|
"malformed",
|
|
"malformed-escape",
|
|
"bom",
|
|
]);
|
|
add(["application", "application/self", "application/private", "application/missing"]);
|
|
add([
|
|
"#same",
|
|
"#conditional",
|
|
"#pattern/a",
|
|
"#/prefix/a",
|
|
"#blocked",
|
|
"#dependency",
|
|
"#dependency-subpath",
|
|
"#dependency-main",
|
|
"#missing",
|
|
"#missing-file",
|
|
"#builtin",
|
|
"#node-prefix",
|
|
"#invalid-name",
|
|
"#outside",
|
|
"#self",
|
|
"#unknown",
|
|
"#",
|
|
"#pattern/",
|
|
]);
|
|
add(
|
|
["scoped-errors/num", "scoped-errors/mix"],
|
|
join(directory, "app/node_modules/scoped-errors/inside.cjs"),
|
|
);
|
|
add(["self-null"], join(directory, "app/node_modules/self-null/main.cjs"));
|
|
add(["#bad-main", "#no-entry"]);
|
|
add(["nested", "dual", "#same"], join(directory, "app/node_modules/outer/main.cjs"));
|
|
add(["preferred"], main, [join(directory, "vendor"), join(directory, "app")]);
|
|
add(["preferred"], main, [join(directory, "app"), join(directory, "vendor")]);
|
|
add(["preferred", "./same.js", "fs", join(directory, "app/same.js")], main, []);
|
|
add(["./same.js"], main, [join(directory, "app")]);
|
|
add(["#same", "application/self"], main, []);
|
|
add(["./same.js", "./absent"], join(directory, "app") + sep);
|
|
add(["./same.js"], join(directory, "app") + sep, null, true);
|
|
add(["#unknown", "#"], join(directory, "standalone/main.cjs"));
|
|
add(
|
|
[
|
|
"fs",
|
|
"node:test",
|
|
"./same",
|
|
"../other",
|
|
"...",
|
|
"..name",
|
|
".name",
|
|
"bare",
|
|
"@scope/pkg",
|
|
"/absolute",
|
|
"#same",
|
|
],
|
|
main,
|
|
null,
|
|
true,
|
|
);
|
|
add(
|
|
["nested", "../child", "#same"],
|
|
join(directory, "app/node_modules/outer/main.cjs"),
|
|
null,
|
|
true,
|
|
);
|
|
if (process.platform === "win32") {
|
|
for (const from of [
|
|
"C:\\main.cjs",
|
|
"C:\\work\\node_modules\\package\\main.cjs",
|
|
"\\\\server\\share\\work\\main.cjs",
|
|
"\\\\server\\share\\main.cjs",
|
|
]) {
|
|
add(["bare", "./file", "..\\file"], from, null, true);
|
|
}
|
|
}
|
|
return cases;
|
|
}
|
|
|
|
function oracle(request: Request): unknown {
|
|
const require = createRequire(request.from);
|
|
if (request.lookup) return require.resolve.paths(request.request);
|
|
try {
|
|
return {
|
|
ok: true,
|
|
value:
|
|
request.paths === null
|
|
? require.resolve(request.request)
|
|
: require.resolve(request.request, { paths: request.paths }),
|
|
};
|
|
} catch (error) {
|
|
const e = error as NodeJS.ErrnoException;
|
|
return { ok: false, error: { name: e.name, code: e.code ?? "", message: e.message } };
|
|
}
|
|
}
|
|
|
|
function workspace(): string {
|
|
return mkdtempSync(
|
|
join(process.platform === "win32" ? tmpdir() : "/tmp", "scriptc-runtime-resolver-"),
|
|
);
|
|
}
|
|
|
|
test("filesystem runtime resolver agrees with pinned Node across package and path rules", () => {
|
|
const directory = workspace();
|
|
try {
|
|
for (const request of makeCases(directory)) {
|
|
const actual = request.lookup
|
|
? cjsResolvePaths(request.from, request.request)
|
|
: resolveCjsRuntime(request.from, request.request, request.paths ?? undefined);
|
|
expect.soft(actual, JSON.stringify(request)).toEqual(oracle(request));
|
|
}
|
|
} finally {
|
|
rmSync(directory, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
test("runtime lookup tracks failed candidates as well as selected files", () => {
|
|
const directory = workspace();
|
|
try {
|
|
makeCases(directory);
|
|
const tracker = new FrontendInputTracker();
|
|
tracker.run(() => resolveCjsRuntime(join(directory, "app/main.cjs"), "./data"));
|
|
const probes = tracker.snapshot().probes;
|
|
expect(probes).toContainEqual({
|
|
op: "kind",
|
|
path: join(directory, "app/data.js"),
|
|
kind: "missing",
|
|
});
|
|
expect(probes).toContainEqual({
|
|
op: "realpath",
|
|
path: join(directory, "app/data.json"),
|
|
target: realpathSync(join(directory, "app/data.json")),
|
|
});
|
|
} finally {
|
|
rmSync(directory, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
for (const backend of ["llvm"] as const) {
|
|
test(`native runtime resolver: ${backend} matches pinned Node without evaluating modules`, async () => {
|
|
const directory = workspace();
|
|
try {
|
|
const requests = makeCases(directory);
|
|
const input = join(directory, "requests.json");
|
|
writeFileSync(input, JSON.stringify(requests));
|
|
const { coverage } = analyze(entry, { dynamic: false });
|
|
expect(coverage.preflightFailed).toBe(false);
|
|
expect(coverage.stats.statementsFailed).toBe(0);
|
|
expect(coverage.stats.statementsIsland).toBe(0);
|
|
expect(coverage.stats.functionsSkipped).toBe(0);
|
|
const built = await compile(entry, {
|
|
outDir: directory,
|
|
outPath: join(directory, process.platform === "win32" ? "resolver.exe" : "resolver"),
|
|
backend,
|
|
dynamic: false,
|
|
sanitize: process.env["SCRIPTC_SAN"] === "1",
|
|
});
|
|
if (!built.ok)
|
|
throw new Error(built.diagnostics.map((d) => `${d.code}: ${d.message}`).join("\n"));
|
|
const run = spawnSync(built.binaryPath, [input], {
|
|
cwd: root,
|
|
timeout: 30_000,
|
|
maxBuffer: 4 * 1024 * 1024,
|
|
});
|
|
expect(run.error).toBeUndefined();
|
|
expect(run.signal).toBeNull();
|
|
expect(run.status, run.stderr.toString()).toBe(0);
|
|
expect(run.stderr.toString()).toBe("");
|
|
const lines = run.stdout.toString().trimEnd().split("\n");
|
|
expect(lines).toHaveLength(requests.length);
|
|
for (const [index, request] of requests.entries()) {
|
|
const expected = oracle(request);
|
|
// The native host uses its own executable prefix for the last legacy
|
|
// global directory; all other search entries must match Node exactly.
|
|
if (request.lookup && Array.isArray(expected) && expected.length > 1) {
|
|
const executable = realpathSync(built.binaryPath);
|
|
const prefix =
|
|
process.platform === "win32" ? dirname(executable) : dirname(dirname(executable));
|
|
expected[expected.length - 1] = resolve(prefix, "lib", "node");
|
|
}
|
|
expect(JSON.parse(lines[index]!), JSON.stringify(request)).toEqual(expected);
|
|
}
|
|
} finally {
|
|
rmSync(directory, { recursive: true, force: true });
|
|
}
|
|
});
|
|
}
|