mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-10-22 05:59:31 +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.
226 lines
8.0 KiB
TypeScript
226 lines
8.0 KiB
TypeScript
/* Executable runtime reachability. Section GC removes unused code from the
|
|
* precompiled runtime pack. Hello must discard unreachable families, while
|
|
* each feature fixture retains its runtime anchor and matches Node. */
|
|
import { execFile } from "node:child_process";
|
|
import { createServer } from "node:http";
|
|
import { mkdirSync, statSync, writeFileSync } from "node:fs";
|
|
import { join } from "node:path";
|
|
import { promisify } from "node:util";
|
|
import { expect, test } from "vitest";
|
|
import { compile } from "@scriptc/compiler";
|
|
|
|
const execFileAsync = promisify(execFile);
|
|
const repoRoot = join(import.meta.dirname, "../..");
|
|
const cacheDir = join(repoRoot, "node_modules/.cache/scriptc-tests/runtime-tree-shaking");
|
|
// The executable and symbol contracts use POSIX tools and /bin/echo.
|
|
const nativeToolchainTest = process.platform === "win32" ? test.skip : test;
|
|
|
|
interface Fixture {
|
|
name: string;
|
|
source: string;
|
|
anchor: string;
|
|
}
|
|
|
|
const FIXTURES: Fixture[] = [
|
|
{
|
|
name: "regex",
|
|
source: `console.log(/world/.test("hello world"));\n`,
|
|
anchor: "scr_regex_test",
|
|
},
|
|
{
|
|
name: "child",
|
|
source: `import { spawnSync } from "node:child_process";
|
|
const r = spawnSync("/bin/echo", ["child"], { encoding: "utf8" });
|
|
console.log(r.stdout.trim(), r.status);
|
|
`,
|
|
anchor: "scr_spawn_sync",
|
|
},
|
|
{
|
|
name: "path-posix",
|
|
source: `import { join } from "node:path/posix";
|
|
console.log(join("a", "..", "b"));
|
|
`,
|
|
anchor: "scr_path_join",
|
|
},
|
|
{
|
|
name: "path-win32",
|
|
source: `import { join } from "node:path/win32";
|
|
console.log(join("C:\\\\a", "..", "b"));
|
|
`,
|
|
anchor: "scr_path_win32_join",
|
|
},
|
|
{
|
|
name: "url",
|
|
source: `import { fileURLToPath, pathToFileURL } from "node:url";
|
|
console.log(fileURLToPath(pathToFileURL("/tmp/a b")));
|
|
`,
|
|
// This composition is optimized through the URL bridge, so its direct
|
|
// anchors are the file-path conversion helpers rather than URL parsing.
|
|
anchor: "scr_url_from_path",
|
|
},
|
|
{
|
|
name: "json-parse",
|
|
source: `console.log(JSON.parse('{"ok":true}').ok);
|
|
`,
|
|
anchor: "scr_json_parse",
|
|
},
|
|
{
|
|
name: "date",
|
|
source: `console.log(new Date(0).toISOString());
|
|
`,
|
|
anchor: "scr_date_to_iso",
|
|
},
|
|
{
|
|
name: "stream-consumers-json",
|
|
source: `import { Readable } from "node:stream";
|
|
import { json } from "node:stream/consumers";
|
|
const stream = new Readable({ read() {} });
|
|
stream.push('{"value":7}');
|
|
stream.push(null);
|
|
console.log((await json(stream)).value);
|
|
`,
|
|
anchor: "scr_json_parse",
|
|
},
|
|
];
|
|
|
|
async function build(name: string, source: string) {
|
|
const outDir = join(cacheDir, name);
|
|
const sourcePath = join(outDir, "main.mjs");
|
|
mkdirSync(outDir, { recursive: true });
|
|
writeFileSync(sourcePath, source);
|
|
const result = await compile(sourcePath, {
|
|
outPath: join(outDir, "program"),
|
|
outDir,
|
|
backend: "llvm",
|
|
});
|
|
if (!result.ok) {
|
|
throw new Error(result.diagnostics.map((d) => `${d.code}: ${d.message}`).join("\n"));
|
|
}
|
|
return { sourcePath, binaryPath: result.binaryPath };
|
|
}
|
|
|
|
async function output(command: string, args: string[]): Promise<string> {
|
|
return (await execFileAsync(command, args, { encoding: "utf8" })).stdout;
|
|
}
|
|
|
|
async function symbols(binaryPath: string): Promise<string> {
|
|
// `nm` is supplied by Xcode/binutils on the supported native lanes. Inspect
|
|
// local symbols too: a static process cache, for example, is still retained
|
|
// payload even though it is not part of the executable's external ABI.
|
|
return output("nm", [binaryPath]);
|
|
}
|
|
|
|
async function expectNoReleaseDebugInfo(binaryPath: string): Promise<void> {
|
|
if (process.platform !== "linux") return;
|
|
const sections = await output("readelf", ["--section-headers", "--wide", binaryPath]);
|
|
expect(sections).not.toMatch(/\.(?:z?debug_|stab)/);
|
|
}
|
|
|
|
async function expectNodeParity(
|
|
sourcePath: string,
|
|
binaryPath: string,
|
|
args: string[] = [],
|
|
): Promise<void> {
|
|
const [node, native] = await Promise.all([
|
|
output(process.execPath, [sourcePath, ...args]),
|
|
output(binaryPath, args),
|
|
]);
|
|
expect(native).toBe(node);
|
|
}
|
|
|
|
nativeToolchainTest(
|
|
"static hello strips unreachable runtime families while feature programs retain them",
|
|
async () => {
|
|
const helloSource = `console.log("hello", "world");\n`;
|
|
const hello = await build("hello", helloSource);
|
|
await expectNodeParity(hello.sourcePath, hello.binaryPath);
|
|
await expectNoReleaseDebugInfo(hello.binaryPath);
|
|
const helloSymbols = await symbols(hello.binaryPath);
|
|
for (const family of [
|
|
"scr_path_win32_",
|
|
"scr_exec_",
|
|
"scr_url_",
|
|
"scr_json_parse",
|
|
"scr_date_",
|
|
]) {
|
|
expect(helloSymbols, `hello retains ${family}`).not.toContain(family);
|
|
}
|
|
|
|
for (const fixture of FIXTURES) {
|
|
// /bin/echo is the portable POSIX child fixture. The Windows child
|
|
// surface remains covered by its cross-target corpus contracts.
|
|
if (fixture.name === "child" && process.platform === "win32") continue;
|
|
const result = await build(fixture.name, fixture.source);
|
|
await expectNodeParity(result.sourcePath, result.binaryPath);
|
|
await expectNoReleaseDebugInfo(result.binaryPath);
|
|
const nativeSymbols = await symbols(result.binaryPath);
|
|
expect(nativeSymbols, `${fixture.name} lost ${fixture.anchor}`).toContain(fixture.anchor);
|
|
}
|
|
|
|
// Symbol absence is the primary reachability contract. Keep a deliberately
|
|
// roomy, platform-specific hello-world ceiling too: it catches losing
|
|
// section GC without pinning an exact linker/SDK byte count. The canonical
|
|
// Linux build is about 41KB and current Mach-O builds are about 70KB;
|
|
// these limits leave several native pages of linker-version slack while
|
|
// remaining far below the former roughly-400KB always-linked runtime.
|
|
const helloSizeLimit = process.platform === "linux" ? 64 * 1024 : 96 * 1024;
|
|
expect(statSync(hello.binaryPath).size).toBeLessThan(helloSizeLimit);
|
|
},
|
|
);
|
|
|
|
nativeToolchainTest("fetch response JSON retains the URL and parser runtime", async () => {
|
|
const server = createServer((_request, response) => {
|
|
response.setHeader("content-type", "application/json");
|
|
response.end('{"ok":true}');
|
|
});
|
|
await new Promise<void>((resolve) => server.listen(0, "127.0.0.1", resolve));
|
|
try {
|
|
const address = server.address();
|
|
if (address === null || typeof address === "string")
|
|
throw new Error("test server has no TCP address");
|
|
const fixture: Fixture = {
|
|
name: "fetch-response-json",
|
|
source: `const response = await fetch(process.argv[2]);
|
|
console.log((await response.json()).ok);
|
|
`,
|
|
anchor: "scr_json_parse",
|
|
};
|
|
const result = await build(fixture.name, fixture.source);
|
|
await expectNodeParity(result.sourcePath, result.binaryPath, [
|
|
`http://127.0.0.1:${address.port}`,
|
|
]);
|
|
await expectNoReleaseDebugInfo(result.binaryPath);
|
|
const nativeSymbols = await symbols(result.binaryPath);
|
|
expect(nativeSymbols).toContain("scr_json_parse");
|
|
expect(nativeSymbols).toContain("scr_url_release");
|
|
} finally {
|
|
await new Promise<void>((resolve, reject) =>
|
|
server.close((error) => (error ? reject(error) : resolve())),
|
|
);
|
|
}
|
|
});
|
|
|
|
// Also exercise default backend selection on the Darwin shipping host.
|
|
test.skipIf(process.platform !== "darwin" || process.arch !== "arm64")(
|
|
"macOS helper/runtime-pack links dead-strip static hello too",
|
|
async () => {
|
|
const outDir = join(cacheDir, "hello-runtime-pack");
|
|
const sourcePath = join(outDir, "main.mjs");
|
|
mkdirSync(outDir, { recursive: true });
|
|
writeFileSync(sourcePath, `console.log("hello", "world");\n`);
|
|
const result = await compile(sourcePath, { outPath: join(outDir, "program"), outDir });
|
|
if (!result.ok) throw new Error(result.diagnostics.map((d) => d.message).join("\n"));
|
|
await expectNodeParity(sourcePath, result.binaryPath);
|
|
const nativeSymbols = await symbols(result.binaryPath);
|
|
for (const family of [
|
|
"scr_path_win32_",
|
|
"scr_exec_",
|
|
"scr_url_",
|
|
"scr_json_parse",
|
|
"scr_date_",
|
|
]) {
|
|
expect(nativeSymbols, `runtime pack retained ${family}`).not.toContain(family);
|
|
}
|
|
},
|
|
);
|