mirror of
https://github.com/badlogic/pi-mono.git
synced 2026-11-05 23:37:25 +00:00
chord/next/http: a Web fetch handler built from the registry Http (routes, wraps, sockets, hosts, relays, CSP sources) and Authenticators, responses as descriptions, a security baseline on by default, Chord's own Node listener as a host-lifetime object, nodeRequest for apps mounting it, links and plugin sockets. chord/next/workers: a forked worker per address, started on demand, stopped when idle, never restarted by itself, plugins built by the server and shipped. Plugins: kinds with platform and host packages, createPluginRuntime, scripts(kind), rollout results per host, imports of other plugins by name. Kernel: RouteHandle (retry, suspend), no route backoff, anonymous host.root, origins only from relays. ws is an optional peer dependency.
293 lines
14 KiB
TypeScript
293 lines
14 KiB
TypeScript
// Tests of building (slice 7c §2): every row of the import table, failures as results, source maps relative to the
|
|
// plugin and digests stable across two checkout paths, inputs hashed as esbuild read them.
|
|
|
|
import { createHash } from "node:crypto";
|
|
import { readFile, realpath } from "node:fs/promises";
|
|
import { join } from "node:path";
|
|
import { describe, expect, it } from "vitest";
|
|
import { BACKGROUND_CONTEXT as context } from "../../../src/context/index.ts";
|
|
import {
|
|
type BuildOptions,
|
|
type BuiltFacet,
|
|
buildFacet,
|
|
discoverPlugins,
|
|
type Plugin,
|
|
} from "../../../src/next-node/plugins/index.ts";
|
|
import { KINDS, tempRoot, writeFiles, writePackage, writePlugin } from "./support.ts";
|
|
|
|
const NODE: BuildOptions = { hostPackages: ["@earendil-works/chord/next"], platform: "node" };
|
|
const BROWSER: BuildOptions = {
|
|
hostPackages: ["@earendil-works/chord/next", "react/jsx-runtime"],
|
|
platform: "browser",
|
|
publicPath: "/assets/",
|
|
};
|
|
|
|
/** The plugins under `root`, and the one named `name`. */
|
|
async function discovered(root: string, name: string): Promise<{ plugins: readonly Plugin[]; plugin: Plugin }> {
|
|
const { plugins } = await discoverPlugins([root], { kinds: KINDS }, context);
|
|
return { plugins, plugin: plugins.find((each) => each.name === name)! };
|
|
}
|
|
|
|
/** `built`'s script, failing the test with its errors when it failed. */
|
|
function script(built: BuiltFacet): Extract<BuiltFacet, { kind: "script" }> {
|
|
if (built.kind === "failed") throw new Error(`build failed: ${built.errors.join("\n")}`);
|
|
return built;
|
|
}
|
|
|
|
/** The errors of a build that must fail. */
|
|
function errors(built: BuiltFacet): readonly string[] {
|
|
if (built.kind !== "failed") throw new Error("the build succeeded");
|
|
return built.errors;
|
|
}
|
|
|
|
/** The sources of `built`'s inline source map. */
|
|
function sourcesOf(built: Extract<BuiltFacet, { kind: "script" }>): string[] {
|
|
const inline = /sourceMappingURL=data:application\/json;base64,(\S+)/.exec(built.source)![1]!;
|
|
return JSON.parse(Buffer.from(inline, "base64").toString()).sources;
|
|
}
|
|
|
|
describe("building a facet: the import table", () => {
|
|
it("inlines the plugin's own files and another plugin's file (copied); keeps host packages external", async () => {
|
|
const root = await tempRoot(["contracts", "notes"]);
|
|
const { plugins, plugin } = await discovered(root, "@test/notes");
|
|
const built = script(await buildFacet(plugin, "worker", { ...NODE, plugins }, context));
|
|
const real = await realpath(root);
|
|
expect(built).toMatchObject({ plugin: "@test/notes", facet: "worker", assets: [] });
|
|
expect(built.source.startsWith("(function (require, module, exports) {")).toBe(true);
|
|
expect(built.source).toContain('require("@earendil-works/chord/next")');
|
|
expect(built.copied).toEqual([join(real, "contracts", "contract.ts")]);
|
|
expect(built.inputs.map(({ file }) => file)).toEqual([
|
|
join(real, "contracts", "contract.ts"),
|
|
join(real, "notes", "label.ts"),
|
|
join(real, "notes", "worker.ts"),
|
|
]);
|
|
// Relative to the plugin (a file whose code minified away maps nothing and is left out).
|
|
expect(sourcesOf(built)).toEqual(expect.arrayContaining(["worker.ts", "../contracts/contract.ts"]));
|
|
for (const source of sourcesOf(built)) expect(source.startsWith("/")).toBe(false);
|
|
// Positions only: no copy of the sources' text in the script (Opus N3).
|
|
const inline = /sourceMappingURL=data:application\/json;base64,(\S+)/.exec(built.source)![1]!;
|
|
expect(JSON.parse(Buffer.from(inline, "base64").toString())).not.toHaveProperty("sourcesContent");
|
|
});
|
|
|
|
it("hashes each input as esbuild read it; the digest is the source's hash", async () => {
|
|
const root = await tempRoot(["contracts", "notes"]);
|
|
const { plugins, plugin } = await discovered(root, "@test/notes");
|
|
const built = script(await buildFacet(plugin, "worker", { ...NODE, plugins }, context));
|
|
for (const { file, hash } of built.inputs) {
|
|
expect(hash).toBe(
|
|
createHash("sha256")
|
|
.update(await readFile(file))
|
|
.digest("hex"),
|
|
);
|
|
}
|
|
expect(built.digest).toBe(createHash("sha256").update(built.source).digest("hex"));
|
|
});
|
|
|
|
it("gives the same script and digest from two checkout paths (relative source maps)", async () => {
|
|
const first = await tempRoot(["contracts", "notes"]);
|
|
const second = await tempRoot(["contracts", "notes"]);
|
|
const builds = await Promise.all(
|
|
[first, second].map(async (root) => {
|
|
const { plugins, plugin } = await discovered(root, "@test/notes");
|
|
return script(await buildFacet(plugin, "worker", { ...NODE, plugins }, context));
|
|
}),
|
|
);
|
|
expect(builds[0]!.digest).toBe(builds[1]!.digest);
|
|
expect(builds[0]!.source).toBe(builds[1]!.source);
|
|
});
|
|
|
|
it("inlines a package of its dependencies; keeps chord.external packages and Node builtins external on Node", async () => {
|
|
const root = await tempRoot();
|
|
const dir = await writePlugin(
|
|
root,
|
|
"deps",
|
|
"@test/deps",
|
|
{
|
|
"worker.ts": [
|
|
'import { pad } from "pad-dep";',
|
|
'import native from "native-dep";',
|
|
'import { join } from "node:path";',
|
|
'import { readFileSync } from "fs";',
|
|
'import { pluginFile } from "@earendil-works/chord/next/plugins";',
|
|
"export default { setup: () => [pad, native, join, readFileSync, pluginFile] };",
|
|
].join("\n"),
|
|
},
|
|
{ dependencies: { "pad-dep": "1.0.0" }, chord: { external: ["native-dep"] } },
|
|
);
|
|
await writePackage(dir, "pad-dep", { "index.js": 'exports.pad = (s) => "pad:" + s;' });
|
|
await writePackage(dir, "native-dep", { "index.js": "module.exports = 1;" });
|
|
const { plugin } = await discovered(root, "@test/deps");
|
|
const built = script(await buildFacet(plugin, "worker", NODE, context));
|
|
expect(built.source).toContain('"pad:"');
|
|
for (const external of ["native-dep", "node:path", "fs", "@earendil-works/chord/next/plugins"]) {
|
|
expect(built.source).toContain(`require("${external}")`);
|
|
}
|
|
// Packages are no inputs: the watcher ignores node_modules.
|
|
expect(built.inputs.map(({ file }) => file)).toEqual([join(await realpath(dir), "worker.ts")]);
|
|
});
|
|
|
|
it("refuses a chord.external package and a Node builtin in a browser build", async () => {
|
|
const root = await tempRoot();
|
|
const dir = await writePlugin(
|
|
root,
|
|
"web",
|
|
"@test/web",
|
|
{
|
|
"web.ts":
|
|
'import native from "native-dep";\nimport { join } from "node:path";\nexport default { setup: () => [native, join] };',
|
|
},
|
|
{ chord: { external: ["native-dep"] } },
|
|
);
|
|
await writePackage(dir, "native-dep", { "index.js": "module.exports = 1;" });
|
|
const { plugin } = await discovered(root, "@test/web");
|
|
expect(errors(await buildFacet(plugin, "web", BROWSER, context))).toEqual([
|
|
'web.ts:1:20: "native-dep": chord.external packages are for Node builds',
|
|
'web.ts:2:22: "node:path": a Node builtin in a browser build',
|
|
]);
|
|
});
|
|
|
|
it("collects a browser build's CSS into one stylesheet and its assets into content-hashed files", async () => {
|
|
const root = await tempRoot();
|
|
await writePlugin(root, "web", "@test/web", {
|
|
"web.tsx": [
|
|
'import "./notes.css";',
|
|
'import iconUrl from "./assets/icon.svg?url";',
|
|
"const Icon = () => <img src={iconUrl} />;",
|
|
"export default { setup: () => Icon };",
|
|
].join("\n"),
|
|
"notes.css": ".notes { color: red; }",
|
|
"assets/icon.svg": "<svg></svg>",
|
|
});
|
|
const { plugin } = await discovered(root, "@test/web");
|
|
const built = script(await buildFacet(plugin, "web", BROWSER, context));
|
|
expect(built.css?.name).toMatch(/^web-[A-Z0-9]+\.css$/);
|
|
expect(built.css?.text).toContain(".notes{color:red}");
|
|
expect(built.assets.map(({ name }) => name)).toEqual([expect.stringMatching(/^icon-[A-Z0-9]+\.svg$/)]);
|
|
expect(new TextDecoder().decode(built.assets[0]!.bytes)).toBe("<svg></svg>");
|
|
expect(built.source).toContain(`"/assets/${built.assets[0]!.name}"`);
|
|
expect(built.source).toContain('require("react/jsx-runtime")');
|
|
});
|
|
|
|
it("refuses CSS and assets in a Node build", async () => {
|
|
const root = await tempRoot();
|
|
await writePlugin(root, "n", "@test/n", {
|
|
"worker.ts": 'import "./a.css";\nimport url from "./icon.svg?url";\nexport default { setup: () => url };',
|
|
"a.css": "a{}",
|
|
"icon.svg": "<svg/>",
|
|
});
|
|
const { plugin } = await discovered(root, "@test/n");
|
|
const found = errors(await buildFacet(plugin, "worker", NODE, context));
|
|
expect(found).toEqual([
|
|
'worker.ts:1:8: "./a.css": stylesheets and assets are for browser builds',
|
|
expect.stringMatching(/^worker\.ts:2:17: "\.\/icon\.svg\?url": assets by URL are for browser builds/),
|
|
]);
|
|
});
|
|
|
|
it("refuses import.meta.url, __dirname, __filename, a dynamic import() and top-level await, pointing to pluginFile", async () => {
|
|
const root = await tempRoot();
|
|
await writePlugin(root, "bad", "@test/bad", {
|
|
"worker.ts": "export const a = import.meta.url;\nexport default { setup: () => [__dirname, __filename] };",
|
|
"server.ts": 'export default { setup: () => import("./other.ts") };',
|
|
"web.ts": "await Promise.resolve();\nexport default { setup: () => {} };",
|
|
"other.ts": "export const x = 1;",
|
|
});
|
|
const { plugin } = await discovered(root, "@test/bad");
|
|
const meta = errors(await buildFacet(plugin, "worker", NODE, context));
|
|
expect(meta.length).toBeGreaterThan(0);
|
|
for (const error of meta)
|
|
expect(error).toMatch(/^worker\.ts:\d+:\d+: import\.meta\.url, __dirname and __filename .*pluginFile/);
|
|
// Without `import.meta` in the file: each free `__dirname` and `__filename` at its own line (example 37).
|
|
await writeFiles(join(root, "bad"), {
|
|
"worker.ts": "// The globals.\nconsole.log(__dirname);\nexport default { setup: () => __filename };",
|
|
});
|
|
expect(errors(await buildFacet(plugin, "worker", NODE, context))).toEqual([
|
|
expect.stringMatching(/^worker\.ts:2:13: import\.meta\.url, __dirname and __filename .*pluginFile/),
|
|
expect.stringMatching(/^worker\.ts:3:\d+: import\.meta\.url, __dirname and __filename .*pluginFile/),
|
|
]);
|
|
expect(errors(await buildFacet(plugin, "server", NODE, context))).toEqual([
|
|
expect.stringMatching(
|
|
/^server\.ts:1:\d+: import\("\.\/other\.ts"\): a plugin script has no dynamic import .*pluginFile/,
|
|
),
|
|
]);
|
|
expect(errors(await buildFacet(plugin, "web", NODE, context))).toEqual([
|
|
"web.ts:1:1: a plugin script has no top-level await (a facet declares synchronously)",
|
|
]);
|
|
});
|
|
|
|
it("refuses a path outside the plugin, a package not in its dependencies, and a package in a copied file", async () => {
|
|
const root = await tempRoot(["contracts"]);
|
|
await writePlugin(root, "out", "@test/out", {
|
|
"worker.ts":
|
|
'import { base } from "../base.ts";\nimport x from "left-out";\nexport default { setup: () => [base, x] };',
|
|
});
|
|
await writeFiles(root, { "base.ts": "export const base = 1;" });
|
|
await writePlugin(root, "copier", "@test/copier", {
|
|
"worker.ts": 'import { y } from "@test/shared/shared";\nexport default { setup: () => y };',
|
|
});
|
|
await writePlugin(root, "shared", "@test/shared", {
|
|
"shared.ts": 'import y from "some-package";\nexport { y };',
|
|
});
|
|
const { plugins } = await discoverPlugins([root], { kinds: KINDS }, context);
|
|
const out = plugins.find(({ name }) => name === "@test/out")!;
|
|
expect(errors(await buildFacet(out, "worker", { ...NODE, plugins }, context))).toEqual([
|
|
'worker.ts:1:22: "../base.ts": outside the plugin (base code is reached through a host package, packages through dependencies)',
|
|
'worker.ts:2:15: "left-out": neither a host package, in the dependencies of plugin "@test/out" nor a discovered plugin "left-out"',
|
|
]);
|
|
const copier = plugins.find(({ name }) => name === "@test/copier")!;
|
|
expect(errors(await buildFacet(copier, "worker", { ...NODE, plugins }, context))).toEqual([
|
|
'../shared/shared.ts:1:15: "some-package": a file copied from plugin "@test/shared" may import only host packages and files of its own plugin',
|
|
]);
|
|
});
|
|
|
|
it("imports another plugin's file by name from any root; refuses relative paths into it, names a missing plugin", async () => {
|
|
const first = await tempRoot();
|
|
const second = await tempRoot();
|
|
await writePlugin(second, "elsewhere", "@test/contracts", {
|
|
"contract.ts": 'export const name = "contract";',
|
|
"other.ts": 'import { name } from "./contract.ts";\nexport const other = name;',
|
|
});
|
|
await writePlugin(first, "user", "@test/user", {
|
|
"worker.ts": [
|
|
'import { name } from "@test/contracts/contract";',
|
|
'import { other } from "@test/contracts/other";',
|
|
"export default { setup: () => [name, other] };",
|
|
].join("\n"),
|
|
});
|
|
await writePlugin(first, "relative", "@test/relative", {
|
|
"worker.ts": 'import { name } from "../user/worker.ts";\nexport default { setup: () => name };',
|
|
});
|
|
await writePlugin(first, "missing", "@test/missing", {
|
|
"worker.ts": 'import { name } from "@test/absent/contract";\nexport default { setup: () => name };',
|
|
});
|
|
const { plugins } = await discoverPlugins([first, second], { kinds: KINDS }, context);
|
|
const build = (name: string) =>
|
|
buildFacet(plugins.find((each) => each.name === name)!, "worker", { ...NODE, plugins }, context);
|
|
const user = await build("@test/user");
|
|
if (user.kind === "failed") throw new Error(user.errors.join("\n"));
|
|
expect(user.copied.map((file) => file.slice(second.length + 1)).sort()).toEqual([
|
|
"elsewhere/contract.ts",
|
|
"elsewhere/other.ts",
|
|
]);
|
|
expect(errors(await build("@test/relative"))).toEqual([
|
|
'worker.ts:1:22: "../user/worker.ts": a relative path into plugin "@test/user" (import its files by name: "@test/user/…")',
|
|
]);
|
|
expect(errors(await build("@test/missing"))).toEqual([
|
|
'worker.ts:1:22: "@test/absent/contract": neither a host package, in the dependencies of plugin "@test/missing" nor a discovered plugin "@test/absent"',
|
|
]);
|
|
});
|
|
|
|
it("returns a syntax error and a missing facet as failed results, never a throw", async () => {
|
|
const root = await tempRoot();
|
|
await writePlugin(root, "syntax", "@test/syntax", { "worker.ts": "export default {" });
|
|
const { plugin } = await discovered(root, "@test/syntax");
|
|
expect(errors(await buildFacet(plugin, "worker", NODE, context))).toEqual([
|
|
expect.stringMatching(/^worker\.ts:1:\d+: Expected /),
|
|
]);
|
|
expect(await buildFacet(plugin, "server", NODE, context)).toEqual({
|
|
kind: "failed",
|
|
plugin: "@test/syntax",
|
|
facet: "server",
|
|
errors: ['plugin "@test/syntax" has no server facet'],
|
|
});
|
|
});
|
|
});
|