mirror of
https://github.com/aphrody-labs/aphrody-m3-bun.git
synced 2026-10-13 19:09:55 +00:00
183 lines
6.8 KiB
TypeScript
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/);
|
|
});
|
|
});
|