Squashed 'third_party/pi/' content from commit d981de122
git-subtree-dir: third_party/pi git-subtree-split: d981de1229ef899957bbe968bc8dcda02a21f477
This commit is contained in:
259
packages/chord/test/bundle.test.ts
Normal file
259
packages/chord/test/bundle.test.ts
Normal file
@@ -0,0 +1,259 @@
|
||||
import { mkdir, mkdtemp, readdir, readFile, rm, writeFile } from "node:fs/promises";
|
||||
import { dirname, join, resolve } from "node:path";
|
||||
import { fileURLToPath, pathToFileURL } from "node:url";
|
||||
import { afterEach, describe, expect, test } from "vitest";
|
||||
import { bundleFacetPackage, bundleFacets } from "../src/bundler.ts";
|
||||
import { createFacetHost, defineFacet, defineService } from "../src/index.ts";
|
||||
import {
|
||||
createFacetBundleArtifactLoader,
|
||||
createFacetBundleLoader,
|
||||
readFacetBundleArtifact,
|
||||
readFacetBundleManifest,
|
||||
} from "../src/node.ts";
|
||||
|
||||
interface GenerationValue {
|
||||
read(): string;
|
||||
}
|
||||
|
||||
const packageDirectory = resolve(dirname(fileURLToPath(import.meta.url)), "..");
|
||||
const temporaryDirectories: string[] = [];
|
||||
const GenerationValue = defineService<GenerationValue>("test.bundle.generation", { local: true });
|
||||
|
||||
afterEach(async () => {
|
||||
await Promise.all(temporaryDirectories.splice(0).map((path) => rm(path, { force: true, recursive: true })));
|
||||
});
|
||||
|
||||
describe("facet bundles", () => {
|
||||
test("builds independent content-addressed entries and loads fresh reloadable generations", async () => {
|
||||
const directory = await mkdtemp(join(packageDirectory, ".bundle-test-"));
|
||||
temporaryDirectories.push(directory);
|
||||
const sourceDirectory = join(directory, "src");
|
||||
const outputDirectory = join(directory, "bundle");
|
||||
await mkdir(sourceDirectory);
|
||||
await writeFile(
|
||||
join(sourceDirectory, "helper.ts"),
|
||||
'export const decorate = (value: string): string => "generation:" + value;\n',
|
||||
);
|
||||
const entryPath = join(sourceDirectory, "entry.ts");
|
||||
const presentationPath = join(sourceDirectory, "presentation.ts");
|
||||
await writeGeneration(entryPath, "A");
|
||||
await writeFile(presentationPath, 'export default { id: "bundle-presentation", setup() {} };\n');
|
||||
const facetEntries = { presentation: presentationPath, worker: entryPath };
|
||||
|
||||
const firstBuild = await bundleFacets({
|
||||
plugin: { id: "test-bundle", version: "1" },
|
||||
entries: facetEntries,
|
||||
outdir: outputDirectory,
|
||||
sourceMap: true,
|
||||
});
|
||||
const firstEntry = firstBuild.manifest.entries.worker!;
|
||||
expect(firstEntry.file).toMatch(/^facet-[a-f0-9]{12}-[A-Z0-9]+\.cjs$/u);
|
||||
expect(firstEntry.sourceMap).toBe(`${firstEntry.file}.map`);
|
||||
expect(firstEntry.externalImports).toEqual(["@earendil-works/chord"]);
|
||||
expect(firstBuild.manifest.entries.presentation!.file).not.toBe(firstEntry.file);
|
||||
expect((await readdir(outputDirectory)).filter((path) => path.endsWith(".cjs"))).toHaveLength(2);
|
||||
const firstSource = await readFile(join(outputDirectory, firstEntry.file), "utf8");
|
||||
expect(firstSource).toContain('require("@earendil-works/chord")');
|
||||
expect(firstSource).toContain("module.exports");
|
||||
expect((await readFile(firstBuild.manifestPath, "utf8")).endsWith("\n")).toBe(true);
|
||||
|
||||
const presentation = await createFacetBundleLoader({
|
||||
manifestPath: firstBuild.manifestPath,
|
||||
entry: "presentation",
|
||||
}).load();
|
||||
expect(presentation.facets.map(({ id }) => id)).toEqual(["bundle-presentation"]);
|
||||
await presentation.dispose();
|
||||
|
||||
const artifact = await readFacetBundleArtifact({
|
||||
manifestPath: firstBuild.manifestPath,
|
||||
entry: "worker",
|
||||
});
|
||||
const materializedDirectory = join(directory, "materialized");
|
||||
const transported = await createFacetBundleArtifactLoader({
|
||||
artifact: structuredClone(artifact),
|
||||
temporaryDirectory: materializedDirectory,
|
||||
}).load();
|
||||
expect(transported.facets.map(({ id }) => id)).toEqual(["bundle-provider"]);
|
||||
expect(await readdir(materializedDirectory)).toHaveLength(1);
|
||||
await transported.dispose();
|
||||
expect(await readdir(materializedDirectory)).toEqual([]);
|
||||
|
||||
const secondBuild = await bundleFacets({
|
||||
plugin: { id: "test-bundle", version: "1" },
|
||||
entries: facetEntries,
|
||||
outdir: outputDirectory,
|
||||
sourceMap: true,
|
||||
});
|
||||
expect(secondBuild.manifest.entries.worker).toEqual(firstEntry);
|
||||
|
||||
const loader = createFacetBundleLoader({ manifestPath: secondBuild.manifestPath, entry: "worker" });
|
||||
const loadedA = await loader.load();
|
||||
const loadedACopy = await loader.load();
|
||||
expect(loadedACopy.facets[0]).not.toBe(loadedA.facets[0]);
|
||||
await loadedACopy.dispose();
|
||||
|
||||
let retained: GenerationValue | undefined;
|
||||
const consumer = defineFacet({
|
||||
id: "bundle-consumer",
|
||||
setup(env) {
|
||||
retained = env.use(GenerationValue);
|
||||
},
|
||||
});
|
||||
const host = await createFacetHost({ facets: [consumer, ...loadedA.facets] });
|
||||
expect(retained!.read()).toBe("generation:A");
|
||||
|
||||
await writeGeneration(entryPath, "B");
|
||||
const thirdBuild = await bundleFacets({
|
||||
plugin: { id: "test-bundle", version: "2" },
|
||||
entries: facetEntries,
|
||||
outdir: outputDirectory,
|
||||
sourceMap: true,
|
||||
});
|
||||
expect(thirdBuild.manifest.entries.worker!.file).not.toBe(firstEntry.file);
|
||||
const loadedB = await loader.load();
|
||||
await host.reload(loadedB.facets);
|
||||
await loadedA.dispose();
|
||||
expect(retained!.read()).toBe("generation:B");
|
||||
|
||||
await host.dispose();
|
||||
await loadedB.dispose();
|
||||
});
|
||||
|
||||
test("loads host externals through the controlled CommonJS require", async () => {
|
||||
const directory = await mkdtemp(join(packageDirectory, ".bundle-external-test-"));
|
||||
temporaryDirectories.push(directory);
|
||||
const externalPath = join(directory, "host.mjs");
|
||||
const entryPath = join(directory, "entry.ts");
|
||||
const outputDirectory = join(directory, "bundle");
|
||||
await writeFile(externalPath, 'export const named = "host-named";\n');
|
||||
await writeFile(
|
||||
entryPath,
|
||||
'import { named } from "@example/host";\n' +
|
||||
'export const loadDynamic = () => import("@example/dynamic");\n' +
|
||||
'if (named !== "host-named") throw new Error("external mismatch");\n' +
|
||||
'export default { id: "external-facet", setup() {} };\n',
|
||||
);
|
||||
const result = await bundleFacets({
|
||||
plugin: { id: "external-bundle" },
|
||||
entries: { worker: entryPath },
|
||||
external: ["@example/host", "@example/dynamic"],
|
||||
outdir: outputDirectory,
|
||||
});
|
||||
const entry = result.manifest.entries.worker!;
|
||||
const source = await readFile(join(outputDirectory, entry.file), "utf8");
|
||||
expect(source).not.toContain("import(");
|
||||
expect(source).toContain('require("@example/dynamic")');
|
||||
const loaded = await createFacetBundleLoader({
|
||||
manifestPath: result.manifestPath,
|
||||
entry: "worker",
|
||||
resolveExternal: (specifier) =>
|
||||
specifier === "@example/host" || specifier === "@example/dynamic" ? pathToFileURL(externalPath) : undefined,
|
||||
}).load();
|
||||
expect(loaded.facets.map(({ id }) => id)).toEqual(["external-facet"]);
|
||||
await loaded.dispose();
|
||||
});
|
||||
|
||||
test("builds plugin packages from conventional and configured facet entries", async () => {
|
||||
const directory = await mkdtemp(join(packageDirectory, ".bundle-package-test-"));
|
||||
temporaryDirectories.push(directory);
|
||||
const sourceDirectory = join(directory, "src");
|
||||
await mkdir(sourceDirectory);
|
||||
await Promise.all([
|
||||
writeFile(
|
||||
join(directory, "package.json"),
|
||||
`${JSON.stringify({
|
||||
name: "@example/conventional-plugin",
|
||||
version: "1.2.3",
|
||||
peerDependencies: { "@example/host": "^1.0.0" },
|
||||
})}\n`,
|
||||
),
|
||||
writeFile(
|
||||
join(sourceDirectory, "session.ts"),
|
||||
'import "@example/host/plugin"; export default { id: "package-session", setup() {} };\n',
|
||||
),
|
||||
writeFile(join(sourceDirectory, "tui.ts"), 'export default { id: "package-tui", setup() {} };\n'),
|
||||
writeFile(join(sourceDirectory, "contract.ts"), "export const ignored = true;\n"),
|
||||
writeFile(join(sourceDirectory, "presentation.ts"), 'export default { id: "configured-tui", setup() {} };\n'),
|
||||
]);
|
||||
|
||||
const conventional = await bundleFacetPackage({
|
||||
packagePath: directory,
|
||||
outdir: join(directory, "build"),
|
||||
defaultFacets: { session: "src/session.ts", tui: "src/tui.ts", browser: "src/browser.ts" },
|
||||
});
|
||||
expect(conventional.packageDirectory).toBe(directory);
|
||||
expect(conventional.manifest.plugin).toEqual({ id: "@example/conventional-plugin", version: "1.2.3" });
|
||||
expect(Object.keys(conventional.manifest.entries)).toEqual(["session", "tui"]);
|
||||
expect(conventional.manifest.entries.session?.externalImports).toEqual(["@example/host/plugin"]);
|
||||
expect(conventional.manifest.entries.tui?.sourceMap).toMatch(/\.cjs\.map$/u);
|
||||
|
||||
await writeFile(
|
||||
join(directory, "package.json"),
|
||||
`${JSON.stringify({
|
||||
name: "@example/conventional-plugin",
|
||||
version: "2.0.0",
|
||||
chord: { facets: { session: false, tui: "src/presentation.ts" }, sourceMap: false },
|
||||
})}\n`,
|
||||
);
|
||||
const configured = await bundleFacetPackage({
|
||||
packagePath: join(directory, "package.json"),
|
||||
outdir: join(directory, "build"),
|
||||
defaultFacets: { session: "src/session.ts", tui: "src/tui.ts" },
|
||||
});
|
||||
expect(Object.keys(configured.manifest.entries)).toEqual(["tui"]);
|
||||
expect(configured.manifest.entries.tui?.sourceMap).toBeUndefined();
|
||||
const loaded = await createFacetBundleLoader({
|
||||
manifestPath: configured.manifestPath,
|
||||
entry: "tui",
|
||||
}).load();
|
||||
expect(loaded.facets.map(({ id }) => id)).toEqual(["configured-tui"]);
|
||||
await loaded.dispose();
|
||||
});
|
||||
|
||||
test("rejects invalid plugin package entry configuration", async () => {
|
||||
const directory = await mkdtemp(join(packageDirectory, ".bundle-package-test-"));
|
||||
temporaryDirectories.push(directory);
|
||||
await writeFile(
|
||||
join(directory, "package.json"),
|
||||
`${JSON.stringify({
|
||||
name: "invalid-plugin",
|
||||
version: "1.0.0",
|
||||
chord: { facets: { tui: "../outside.ts" } },
|
||||
})}\n`,
|
||||
);
|
||||
await expect(bundleFacetPackage({ packagePath: directory, outdir: join(directory, "build") })).rejects.toThrow(
|
||||
"escapes the package directory",
|
||||
);
|
||||
});
|
||||
|
||||
test("rejects corrupt entries and invalid module exports", async () => {
|
||||
const directory = await mkdtemp(join(packageDirectory, ".bundle-test-"));
|
||||
temporaryDirectories.push(directory);
|
||||
const entryPath = join(directory, "entry.ts");
|
||||
const outputDirectory = join(directory, "bundle");
|
||||
await writeFile(entryPath, "export default { id: 'missing-setup' };\n");
|
||||
const result = await bundleFacets({
|
||||
plugin: { id: "invalid-bundle" },
|
||||
entries: { invalid: entryPath },
|
||||
outdir: outputDirectory,
|
||||
});
|
||||
const loader = createFacetBundleLoader({ manifestPath: result.manifestPath, entry: "invalid" });
|
||||
await expect(loader.load()).rejects.toThrow("has no setup function");
|
||||
|
||||
const manifest = await readFacetBundleManifest(result.manifestPath);
|
||||
await writeFile(join(outputDirectory, manifest.entries.invalid!.file), "export default {};\n");
|
||||
await expect(loader.load()).rejects.toThrow("integrity check failed");
|
||||
});
|
||||
});
|
||||
|
||||
async function writeGeneration(path: string, generation: string): Promise<void> {
|
||||
await writeFile(
|
||||
path,
|
||||
`import "@earendil-works/chord";\n` +
|
||||
`import { decorate } from "./helper.ts";\n` +
|
||||
`const Value = { id: "test.bundle.generation", local: true };\n` +
|
||||
`export default { id: "bundle-provider", setup(env) {\n` +
|
||||
` env.provide(Value, { read() { return decorate(${JSON.stringify(generation)}); } });\n` +
|
||||
`}};\n`,
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user