Files
scriptc/tests/harness/portless-e2e.test.ts
Jimmy Miller 46541750e0 feat: support compiling and running Portless
What we did:

- Add the compiler and runtime support required to build the real Portless CLI as a fully static native binary and run its proxy, alias, framework, parallel-registration, and process-cleanup workflows without Node.
- Support Portless's dual HTTP/1.1 and HTTP/2 secure-server graph, including ALPN dispatch, shared request/connect listeners, HTTP/2 CONNECT responses, enableConnectProtocol settings, req.stream access, session errors, and the required ownership and lifecycle handling.
- Lower Portless's indexed absence probes, strict indexed comparisons, optional chains, server options, and container patterns. Preserve Node-style self-reexecution and correctly frame forwarded chunked responses.
- Add focused corpus and server differentials plus an isolated-copy acceptance harness that builds and exercises the pinned Portless checkout in plain and sanitized modes without modifying the external checkout.
- Validate the static binary against Portless's official e2e suite: all 13 files and 16 tests pass.

What we decided was out of scope:

- Broader HTTP/2 client policy, lifecycle compatibility, protocol validation, listener overloads, and unrelated HTTP/2 completeness.
- General runtime-optional soundness beyond the Portless paths. Making that mechanism generally sound requires function-scoped tracking so reused local IDs cannot leak optional state between functions, assignment-aware tracking so writing a definitely present value clears hidden undefined state, and correct preservation across generators, modules, and finally blocks. That larger cross-cutting change remains a dedicated follow-up.
- Removing the self-reexecution heuristic's inherent ambiguity. A direct invocation whose first user argument canonically names the executable is currently treated as Node's repeated script slot and collapsed. An explicit native reexec marker can address that in future work.
2026-08-13 15:47:42 -04:00

150 lines
6.1 KiB
TypeScript

/* Portless end-to-end: compile the real external checkout and run its proxy
* and alias workflows without Node at runtime. This follows the optional
* external-suite convention used by prettier-e2e and vercel-e2e: the suite
* skips when the checkout is absent and SCRIPTC_PORTLESS_ROOT overrides the
* default location.
*
* SETUP:
* git clone https://github.com/vercel-labs/portless.git ~/Developer/portless-scratch
* git -C ~/Developer/portless-scratch checkout d42c741ac67d20a0b6e1f8f5b4192136de34fa03
* pnpm -C ~/Developer/portless-scratch install --frozen-lockfile
*
* Portless owns any build-time version stamping. Scriptc deliberately does
* not provide bundler-style constant substitution. */
import { spawn } from "node:child_process";
import { existsSync, readFileSync, writeFileSync } from "node:fs";
import { cp, mkdtemp, rm, symlink } from "node:fs/promises";
import { createServer, request } from "node:http";
import { homedir, tmpdir } from "node:os";
import { join } from "node:path";
import { afterAll, describe, expect, test } from "vitest";
import { compile } from "@scriptc/compiler";
const portlessRoot = process.env["SCRIPTC_PORTLESS_ROOT"] ?? join(homedir(), "Developer/portless-scratch");
const portlessPackage = join(portlessRoot, "packages/portless");
const sourceEntry = join(portlessPackage, "src/cli.ts");
const suite = existsSync(sourceEntry) ? describe : describe.skip;
const cleanup: Array<() => void | Promise<void>> = [];
afterAll(async () => {
for (const fn of cleanup.splice(0).reverse()) await fn();
});
function childEnv(overrides: Record<string, string>): NodeJS.ProcessEnv {
const env: NodeJS.ProcessEnv = {};
for (const key of [
"PATH", "HOME", "TMPDIR", "TMP", "TEMP", "LANG", "LC_ALL",
"ASAN_OPTIONS", "LSAN_OPTIONS", "UBSAN_OPTIONS", "DYLD_LIBRARY_PATH", "LD_LIBRARY_PATH",
]) {
if (process.env[key] !== undefined) env[key] = process.env[key];
}
return { ...env, ...overrides };
}
function terminate(child: ReturnType<typeof spawn>): Promise<void> {
if (child.exitCode !== null || child.signalCode !== null) return Promise.resolve();
return new Promise((resolve) => {
child.once("close", () => resolve());
child.kill("SIGTERM");
});
}
function freePort(): Promise<number> {
return new Promise((resolve, reject) => {
const server = createServer();
server.once("error", reject);
server.listen(0, "127.0.0.1", () => {
const address = server.address();
if (address === null || typeof address === "string") return reject(new Error("no TCP address"));
server.close(() => resolve(address.port));
});
});
}
function get(port: number, host: string): Promise<{ status: number; body: string }> {
return new Promise((resolve, reject) => {
const req = request({ hostname: "127.0.0.1", port, headers: { host } }, (res) => {
const chunks: Buffer[] = [];
res.on("data", (chunk: Buffer) => chunks.push(chunk));
res.on("end", () => resolve({ status: res.statusCode ?? 0, body: Buffer.concat(chunks).toString("utf8") }));
});
req.once("error", reject);
req.end();
});
}
async function waitForProxy(port: number): Promise<void> {
for (let i = 0; i < 50; i++) {
try {
await get(port, "missing.localhost");
return;
} catch {
await new Promise((resolve) => setTimeout(resolve, 50));
}
}
throw new Error("native Portless proxy did not start");
}
suite("Portless acceptance", () => {
test("builds and proxies a live alias without Node at runtime", async () => {
const work = await mkdtemp(join(tmpdir(), "scriptc-portless-"));
cleanup.push(() => rm(work, { recursive: true, force: true }));
const pkg = JSON.parse(readFileSync(join(portlessRoot, "packages/portless/package.json"), "utf8")) as { version: string };
const shadowPackage = join(work, "checkout");
await cp(portlessPackage, shadowPackage, {
recursive: true,
filter: (source) => source !== join(portlessPackage, "node_modules"),
});
await symlink(join(portlessPackage, "node_modules"), join(shadowPackage, "node_modules"),
process.platform === "win32" ? "junction" : "dir");
const entry = join(shadowPackage, "src/cli.ts");
const source = readFileSync(entry, "utf8");
const stamped = source.replace(
/^(?:declare const __VERSION__: string;|const __VERSION__ = .*;)$/m,
`const __VERSION__ = ${JSON.stringify(pkg.version)};`,
);
if (stamped === source && !source.includes(`const __VERSION__ = ${JSON.stringify(pkg.version)};`)) {
throw new Error("Portless __VERSION__ declaration was not found");
}
writeFileSync(entry, stamped);
const binary = join(work, "portless");
const built = await compile(entry, {
outDir: work,
outPath: binary,
backend: "c",
sanitize: process.env["SCRIPTC_SAN"] === "1",
});
expect(built.ok).toBe(true);
if (!built.ok) return;
const proxyPort = await freePort();
const backendPort = await freePort();
const stateDir = join(work, "state");
const env = childEnv({ PORTLESS_STATE_DIR: stateDir, PORTLESS_SYNC_HOSTS: "0" });
const proxy = spawn(binary, ["proxy", "start", "--no-tls", "-p", String(proxyPort), "--foreground"], {
cwd: portlessRoot,
env,
stdio: "ignore",
});
cleanup.push(() => terminate(proxy));
await waitForProxy(proxyPort);
const backend = createServer((_req, res) => res.end("native-portless-ok"));
await new Promise<void>((resolve, reject) => {
backend.once("error", reject);
backend.listen(backendPort, "127.0.0.1", resolve);
});
cleanup.push(() => new Promise<void>((resolve) => backend.close(() => resolve())));
const alias = spawn(binary, ["alias", "app", String(backendPort), "-p", String(proxyPort)], {
cwd: portlessRoot,
env,
stdio: "ignore",
});
const aliasCode = await new Promise<number | null>((resolve) => alias.once("close", resolve));
expect(aliasCode).toBe(0);
await new Promise((resolve) => setTimeout(resolve, 200));
await expect(get(proxyPort, "app.localhost")).resolves.toEqual({ status: 200, body: "native-portless-ok" });
}, 300_000);
});