Files
aphrody-m3-bun/test/compile.test.ts
T

183 lines
6.8 KiB
TypeScript

// SPDX-License-Identifier: Apache-2.0
import { afterAll, describe, expect, test } from "bun:test";
import { existsSync, readFileSync, statSync } from "node:fs";
import { join } from "node:path";
import { compileApp, defaultOutfile, renderEntry } from "../src/compile.ts";
import { loadApp } from "../src/config.ts";
import { formatSmoke, smoke } from "../src/runtime.ts";
import { cleanup, fixtureCopy, freePort } from "./helpers.ts";
afterAll(cleanup);
describe("renderEntry", () => {
const entry = renderEntry({
runtime: "/pkg/src/entry.ts",
api: "/app/src/api.ts",
files: [
["/index.html", "/app/dist/index.html"],
["/chunk-a.js", "/app/dist/chunk-a.js"],
],
name: "demo",
version: "1.0.0",
buildId: "abc",
port: 3000,
csp: false,
});
test("embeds every file with the file loader and hands the table to runApp", () => {
expect(entry).toContain('import { runApp } from "/pkg/src/entry.ts";');
expect(entry).toContain('import api from "/app/src/api.ts";');
expect(entry).toContain('import file0 from "/app/dist/index.html" with { type: "file" };');
expect(entry).toContain('import file1 from "/app/dist/chunk-a.js" with { type: "file" };');
expect(entry).toContain('"/index.html": file0,');
expect(entry).toContain('"/chunk-a.js": file1,');
expect(entry).toContain("csp: false,");
expect(entry).toContain(" api,");
});
test("the entry has no top-level await: bytecode output is CommonJS", () => {
expect(entry).not.toMatch(/^await /m);
expect(entry).toContain("runApp({");
expect(entry).toContain("}).catch((error: unknown) => {");
// Parsing it as a CommonJS-compatible script must not trip on await.
const transpiled = new Bun.Transpiler({ loader: "ts" }).transformSync(entry);
expect(transpiled).not.toMatch(/^await /m);
});
test("without an API or a CSP those lines are absent; strings are JSON-escaped", () => {
const plain = renderEntry({
runtime: "/r.ts",
files: [],
name: 'we"ird',
version: "1",
buildId: "x",
port: 1,
});
expect(plain).not.toContain("import api");
expect(plain).not.toContain("csp:");
expect(plain).toContain('name: "we\\"ird"');
});
});
describe("defaultOutfile", () => {
test("bin/<name>, with the target in the name and .exe for Windows", async () => {
const app = await loadApp(fixtureCopy());
const hostSuffix = process.platform === "win32" ? ".exe" : "";
expect(defaultOutfile(app, undefined)).toBe(join(app.dir, "bin", `hello-m3${hostSuffix}`));
expect(defaultOutfile(app, "linux-x64-musl")).toBe(
join(app.dir, "bin", "hello-m3-linux-x64-musl"),
);
expect(defaultOutfile(app, "windows-x64")).toBe(
join(app.dir, "bin", "hello-m3-windows-x64.exe"),
);
});
});
describe("compileApp: one executable that serves the UI and the API", () => {
test("compiles the fixture, then the executable answers over HTTP", async () => {
const dir = fixtureCopy();
const report = await compileApp({ dir });
expect(report.files).toBe(3);
expect(report.bytecode).toBe(true);
expect(existsSync(report.outfile)).toBe(true);
if (process.platform !== "win32") expect(statSync(report.outfile).mode & 0o111).not.toBe(0);
expect(report.bytes).toBeGreaterThan(10_000_000);
expect(readFileSync(report.entryFile, "utf8")).toContain(
"// Generated by @aphrody/m3-bun compile.",
);
const version = Bun.spawnSync([report.outfile, "--version"], {
windowsHide: true,
stdout: "pipe",
});
expect(version.stdout.toString().trim()).toMatch(/^hello-m3 1\.2\.3 \([0-9a-f]{16}\)$/);
const port = freePort();
const result = await smoke(
report.outfile,
port,
["/", "/healthz", "/api/m3/info", "/api/hello/world"],
{
"/": "text/html",
"/healthz": "application/json",
"/api/m3/info": "application/json",
"/api/hello/world": "application/json",
},
);
expect(result.ok).toBe(true);
expect(result.checks.map((c) => c.status)).toEqual([200, 200, 200, 200]);
expect(["buv-runtime", "bun-spawn"]).toContain(result.via);
expect(formatSmoke(result)).toContain("m3-bun smoke:");
}, 60_000);
test("--healthcheck exits 0 against a running instance and 1 against nothing", async () => {
const dir = fixtureCopy();
const { outfile } = await compileApp({ dir });
const port = freePort();
const server = Bun.spawn([outfile], {
windowsHide: true,
env: { ...process.env, PORT: String(port) },
stdout: "ignore",
stderr: "ignore",
});
try {
for (let i = 0; i < 100; i++) {
try {
// oxlint-disable-next-line no-await-in-loop -- Probe serially until the compiled server is ready, then stop polling.
await fetch(`http://127.0.0.1:${port}/healthz`);
break;
} catch {
// oxlint-disable-next-line no-await-in-loop -- Delay the next readiness probe only after a failed request.
await Bun.sleep(50);
}
}
expect(
Bun.spawnSync([outfile, "--healthcheck"], {
windowsHide: true,
env: { ...process.env, PORT: String(port) },
}).exitCode,
).toBe(0);
} finally {
server.kill();
await server.exited;
}
expect(
Bun.spawnSync([outfile, "--healthcheck"], {
windowsHide: true,
env: { ...process.env, PORT: String(freePort()) },
}).exitCode,
).toBe(1);
}, 60_000);
test("compile.bytecode: false turns bytecode off, and the executable still runs", async () => {
const dir = fixtureCopy();
await Bun.write(
join(dir, "m3.config.ts"),
`import { defineM3Config } from "@aphrody/m3/config";
import { defineM3Bun } from "../../../src/config.ts";
export default defineM3Config({ entry: "src/index.html", theme: { seed: "#6750a4" } });
export const bun = defineM3Bun({ api: "src/api.ts", compile: { bytecode: false } });
`,
);
const plain = await compileApp({ dir });
expect(plain.bytecode).toBe(false);
const withBytecode = await compileApp({ dir: fixtureCopy() });
expect(withBytecode.bytecode).toBe(true);
expect(withBytecode.bytes).toBeGreaterThan(plain.bytes);
expect(
Bun.spawnSync([plain.outfile, "--version"], {
windowsHide: true,
stdout: "pipe",
}).stdout.toString(),
).toContain("hello-m3 1.2.3");
expect(
Bun.spawnSync([withBytecode.outfile, "--version"], {
windowsHide: true,
stdout: "pipe",
}).stdout.toString(),
).toContain("hello-m3 1.2.3");
}, 60_000);
test("compiling without a build and without --no-build output is an error that says what to run", async () => {
const dir = fixtureCopy();
await expect(compileApp({ dir, build: false })).rejects.toThrow(/no build in .*m3-bun build/);
});
});