CI / verify (pull_request) Successful in 1m0s
CI / infrastructure-smoke (pull_request) Successful in 36s
CI / migration-smoke (pull_request) Successful in 1m53s
CI / image-build (pull_request) Successful in 23s
CI / deploy-smoke (same-origin) (pull_request) Successful in 50s
CI / deploy-smoke (split) (pull_request) Successful in 1m30s
589 lines
22 KiB
TypeScript
589 lines
22 KiB
TypeScript
import { mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
|
|
import { tmpdir } from "node:os";
|
|
import path from "node:path";
|
|
import { afterEach, describe, expect, it } from "vitest";
|
|
import { analyzeModuleUi, buildModuleUi, ModuleUiBuildError } from "../src/index.js";
|
|
|
|
const temporaryDirectories: string[] = [];
|
|
|
|
afterEach(async () => {
|
|
await Promise.all(
|
|
temporaryDirectories
|
|
.splice(0)
|
|
.map((directory) => rm(directory, { recursive: true, force: true })),
|
|
);
|
|
});
|
|
|
|
interface ModuleFixtureOptions {
|
|
/** Omit to leave `entrypoints.ui` undeclared, as a backend-only module does. */
|
|
readonly ui?: string;
|
|
readonly schema?: string;
|
|
readonly migrations?: string;
|
|
readonly jobs?: string;
|
|
readonly server?: string;
|
|
/** The complete inert `entrypoints.display` value, including its permission policy. */
|
|
readonly display?: unknown;
|
|
readonly manifest?: string;
|
|
readonly files?: Record<string, string>;
|
|
}
|
|
|
|
async function createModule(options: ModuleFixtureOptions = {}): Promise<string> {
|
|
const directory = await mkdtemp(path.join(tmpdir(), "ekklesia-ui-build-"));
|
|
temporaryDirectories.push(directory);
|
|
|
|
const manifest =
|
|
options.manifest ??
|
|
JSON.stringify({
|
|
manifestVersion: 8,
|
|
id: "example",
|
|
name: "Example",
|
|
version: "0.1.0",
|
|
compatibility: { platform: "^0.1.0", moduleSdk: "^0.1.0" },
|
|
entrypoints: {
|
|
server: options.server ?? "./dist/server/index.js",
|
|
...(options.schema === undefined ? {} : { schema: options.schema }),
|
|
...(options.migrations === undefined ? {} : { migrations: options.migrations }),
|
|
...(options.jobs === undefined ? {} : { jobs: options.jobs }),
|
|
...(options.ui === undefined ? {} : { ui: options.ui }),
|
|
...(options.display === undefined ? {} : { display: options.display }),
|
|
},
|
|
});
|
|
await writeFile(path.join(directory, "ekklesia.module.json"), manifest);
|
|
|
|
for (const [relativePath, contents] of Object.entries(options.files ?? {})) {
|
|
const target = path.join(directory, relativePath);
|
|
await mkdir(path.dirname(target), { recursive: true });
|
|
await writeFile(target, contents);
|
|
}
|
|
return directory;
|
|
}
|
|
|
|
/** A UI entrypoint with no React, so the build tool's own tests exercise the pipeline mechanics
|
|
* without depending on a UI framework. The real React path is proven end to end by the module
|
|
* template's own test suite. */
|
|
const plainUiSource = `
|
|
const definition = {
|
|
id: "example",
|
|
version: "0.1.0",
|
|
mount(container) {
|
|
container.textContent = "example";
|
|
return () => {
|
|
container.textContent = "";
|
|
};
|
|
},
|
|
};
|
|
export default definition;
|
|
`;
|
|
|
|
describe("module UI build", () => {
|
|
it("bundles the declared entrypoint into the exact path the manifest names", async () => {
|
|
const directory = await createModule({
|
|
ui: "./dist/ui/index.js",
|
|
files: { "src/ui/index.tsx": plainUiSource },
|
|
});
|
|
|
|
const result = await buildModuleUi({ moduleDirectory: directory });
|
|
const artifactResult = result.artifacts[0];
|
|
|
|
expect(result.artifacts).toHaveLength(1);
|
|
expect(artifactResult).toMatchObject({
|
|
kind: "ui",
|
|
outputFile: path.join(directory, "dist", "ui", "index.js"),
|
|
});
|
|
const artifact = await readFile(artifactResult?.outputFile ?? "", "utf8");
|
|
expect(artifact).toContain("export {");
|
|
expect(artifact).toContain("as default");
|
|
});
|
|
|
|
it("honours an alternative declared artifact path", async () => {
|
|
const directory = await createModule({
|
|
ui: "./build/browser/ui.mjs",
|
|
files: { "src/ui/index.tsx": plainUiSource },
|
|
});
|
|
|
|
const result = await buildModuleUi({ moduleDirectory: directory });
|
|
|
|
expect(result.artifacts[0]?.outputFile).toBe(
|
|
path.join(directory, "build", "browser", "ui.mjs"),
|
|
);
|
|
});
|
|
|
|
it("emits a source map alongside the artifact", async () => {
|
|
const directory = await createModule({
|
|
ui: "./dist/ui/index.js",
|
|
files: { "src/ui/index.tsx": plainUiSource },
|
|
});
|
|
|
|
const result = await buildModuleUi({ moduleDirectory: directory });
|
|
|
|
await expect(readFile(`${result.artifacts[0]?.outputFile}.map`, "utf8")).resolves.toContain(
|
|
"sources",
|
|
);
|
|
});
|
|
|
|
it("returns an explicit empty result for a backend-only module", async () => {
|
|
const directory = await createModule({ files: { "src/ui/index.tsx": plainUiSource } });
|
|
|
|
await expect(buildModuleUi({ moduleDirectory: directory })).resolves.toEqual({ artifacts: [] });
|
|
});
|
|
|
|
it("fails when the declared UI has no source entrypoint", async () => {
|
|
const directory = await createModule({ ui: "./dist/ui/index.js" });
|
|
|
|
await expect(buildModuleUi({ moduleDirectory: directory })).rejects.toMatchObject({
|
|
code: "MODULE_UI_SOURCE_MISSING",
|
|
kind: "ui",
|
|
});
|
|
});
|
|
|
|
it("builds a display-only module from the fixed display source", async () => {
|
|
const directory = await createModule({
|
|
display: {
|
|
artifact: "./dist/display/index.js",
|
|
permission: "example.display.view",
|
|
},
|
|
files: { "src/display/index.tsx": plainUiSource },
|
|
});
|
|
|
|
const result = await buildModuleUi({ moduleDirectory: directory });
|
|
|
|
expect(result.artifacts).toEqual([
|
|
{
|
|
kind: "display",
|
|
outputFile: path.join(directory, "dist/display/index.js"),
|
|
sourceFile: path.join(directory, "src/display/index.tsx"),
|
|
},
|
|
]);
|
|
await expect(readFile(result.artifacts[0]?.outputFile ?? "", "utf8")).resolves.toContain(
|
|
"as default",
|
|
);
|
|
});
|
|
|
|
it("builds primary and display artifacts in deterministic role order", async () => {
|
|
const directory = await createModule({
|
|
ui: "./dist/ui/index.js",
|
|
display: {
|
|
artifact: "./dist/display/index.mjs",
|
|
permission: "example.display.view",
|
|
},
|
|
files: {
|
|
"src/ui/index.tsx": plainUiSource.replaceAll("example", "primary"),
|
|
"src/display/index.tsx": plainUiSource.replaceAll("example", "display"),
|
|
},
|
|
});
|
|
|
|
const result = await buildModuleUi({ moduleDirectory: directory });
|
|
|
|
expect(result.artifacts.map((artifact) => artifact.kind)).toEqual(["ui", "display"]);
|
|
await expect(readFile(path.join(directory, "dist/ui/index.js"), "utf8")).resolves.toContain(
|
|
'"primary"',
|
|
);
|
|
await expect(
|
|
readFile(path.join(directory, "dist/display/index.mjs"), "utf8"),
|
|
).resolves.toContain('"display"');
|
|
});
|
|
|
|
it("preflights every declared source before writing either artifact", async () => {
|
|
const directory = await createModule({
|
|
ui: "./dist/ui/index.js",
|
|
display: {
|
|
artifact: "./dist/display/index.js",
|
|
permission: "example.display.view",
|
|
},
|
|
files: { "src/ui/index.tsx": plainUiSource },
|
|
});
|
|
|
|
await expect(buildModuleUi({ moduleDirectory: directory })).rejects.toMatchObject({
|
|
code: "MODULE_UI_SOURCE_MISSING",
|
|
kind: "display",
|
|
});
|
|
await expect(readFile(path.join(directory, "dist/ui/index.js"))).rejects.toThrow();
|
|
});
|
|
|
|
it("writes neither role when the second declared bundle fails to compile", async () => {
|
|
const directory = await createModule({
|
|
ui: "./dist/ui/index.js",
|
|
display: {
|
|
artifact: "./dist/display/index.js",
|
|
permission: "example.display.view",
|
|
},
|
|
files: {
|
|
"src/ui/index.tsx": plainUiSource,
|
|
"src/display/index.tsx": "export default { broken:",
|
|
},
|
|
});
|
|
|
|
await expect(buildModuleUi({ moduleDirectory: directory })).rejects.toMatchObject({
|
|
code: "MODULE_UI_BUILD_FAILED",
|
|
kind: "display",
|
|
});
|
|
await expect(readFile(path.join(directory, "dist/ui/index.js"))).rejects.toThrow();
|
|
await expect(readFile(path.join(directory, "dist/display/index.js"))).rejects.toThrow();
|
|
});
|
|
|
|
it("rejects colliding primary and display output paths before building", async () => {
|
|
const directory = await createModule({
|
|
ui: "./dist/ui/index.js",
|
|
display: {
|
|
artifact: "./dist/ui/index.js",
|
|
permission: "example.display.view",
|
|
},
|
|
files: {
|
|
"src/ui/index.tsx": plainUiSource,
|
|
"src/display/index.tsx": plainUiSource,
|
|
},
|
|
});
|
|
|
|
await expect(buildModuleUi({ moduleDirectory: directory })).rejects.toMatchObject({
|
|
code: "MODULE_UI_ENTRYPOINT_COLLISION",
|
|
kind: "display",
|
|
});
|
|
});
|
|
|
|
for (const entrypointName of ["server", "schema", "migrations", "jobs"] as const) {
|
|
it(`rejects a display output that would overwrite the declared ${entrypointName} artifact before building`, async () => {
|
|
const sharedPath = "./dist/display/index.js";
|
|
const directory = await createModule({
|
|
[entrypointName]: sharedPath,
|
|
display: {
|
|
artifact: sharedPath,
|
|
permission: "example.display.view",
|
|
},
|
|
files: { "src/display/index.tsx": plainUiSource },
|
|
});
|
|
|
|
await expect(buildModuleUi({ moduleDirectory: directory })).rejects.toMatchObject({
|
|
code: "MODULE_UI_ENTRYPOINT_COLLISION",
|
|
kind: "display",
|
|
});
|
|
await expect(readFile(path.join(directory, "dist/display/index.js"))).rejects.toThrow();
|
|
});
|
|
}
|
|
|
|
it("fails when the manifest cannot be read", async () => {
|
|
const directory = await createModule({ manifest: "{ not json" });
|
|
|
|
await expect(buildModuleUi({ moduleDirectory: directory })).rejects.toMatchObject({
|
|
code: "MODULE_UI_MANIFEST_UNREADABLE",
|
|
});
|
|
});
|
|
|
|
it.each([
|
|
["escapes the package", "../outside/ui.js"],
|
|
["is absolute", "/tmp/ui.js"],
|
|
["traverses upward", "./dist/../../ui.js"],
|
|
["is source TypeScript", "./src/ui/index.tsx"],
|
|
["carries a query", "./dist/ui/index.js?raw"],
|
|
["uses backslashes", ".\\dist\\ui\\index.js"],
|
|
])("refuses to write an artifact whose declared path %s", async (_label, ui) => {
|
|
const directory = await createModule({ ui, files: { "src/ui/index.tsx": plainUiSource } });
|
|
|
|
await expect(buildModuleUi({ moduleDirectory: directory })).rejects.toMatchObject({
|
|
code: "MODULE_UI_ENTRYPOINT_UNSAFE",
|
|
kind: "ui",
|
|
});
|
|
});
|
|
|
|
it("applies the same safe compiled-output rules to display artifacts", async () => {
|
|
const directory = await createModule({
|
|
display: {
|
|
artifact: "./src/display/index.tsx",
|
|
permission: "example.display.view",
|
|
},
|
|
files: { "src/display/index.tsx": plainUiSource },
|
|
});
|
|
|
|
await expect(buildModuleUi({ moduleDirectory: directory })).rejects.toMatchObject({
|
|
code: "MODULE_UI_ENTRYPOINT_UNSAFE",
|
|
kind: "display",
|
|
});
|
|
});
|
|
|
|
it("does not reinterpret a bare display path as the version 6 display declaration", async () => {
|
|
const directory = await createModule({
|
|
display: "./dist/display/index.js",
|
|
files: { "src/display/index.tsx": plainUiSource },
|
|
});
|
|
|
|
await expect(buildModuleUi({ moduleDirectory: directory })).rejects.toMatchObject({
|
|
code: "MODULE_UI_MANIFEST_UNREADABLE",
|
|
kind: "display",
|
|
});
|
|
await expect(readFile(path.join(directory, "dist/display/index.js"))).rejects.toThrow();
|
|
});
|
|
|
|
it("surfaces a build failure with esbuild's diagnostics attached", async () => {
|
|
const directory = await createModule({
|
|
ui: "./dist/ui/index.js",
|
|
files: { "src/ui/index.tsx": "export default { id: 'example', " },
|
|
});
|
|
|
|
try {
|
|
await buildModuleUi({ moduleDirectory: directory });
|
|
expect.unreachable("expected the build to fail");
|
|
} catch (error) {
|
|
expect(error).toBeInstanceOf(ModuleUiBuildError);
|
|
expect((error as ModuleUiBuildError).code).toBe("MODULE_UI_BUILD_FAILED");
|
|
// The sanitized message carries no path; the diagnostics a developer needs are attached.
|
|
expect((error as ModuleUiBuildError).message).not.toContain(directory);
|
|
expect((error as { diagnostics?: unknown[] }).diagnostics?.length).toBeGreaterThan(0);
|
|
}
|
|
});
|
|
|
|
it("fails on an unresolvable import instead of emitting a partial bundle", async () => {
|
|
const directory = await createModule({
|
|
ui: "./dist/ui/index.js",
|
|
files: { "src/ui/index.tsx": 'import "totally-not-installed";\nexport default {};' },
|
|
});
|
|
|
|
await expect(buildModuleUi({ moduleDirectory: directory })).rejects.toMatchObject({
|
|
code: "MODULE_UI_BUILD_FAILED",
|
|
});
|
|
});
|
|
|
|
it.each([
|
|
"theme.scss",
|
|
"layout.less",
|
|
"tokens.styl",
|
|
])("rejects a %s import: only plain CSS has a compilation contract", async (stylesheet) => {
|
|
const directory = await createModule({
|
|
ui: "./dist/ui/index.js",
|
|
files: {
|
|
"src/ui/index.tsx": `import "./${stylesheet}";\nexport default {};`,
|
|
[`src/ui/${stylesheet}`]: ".example { color: red; }",
|
|
},
|
|
});
|
|
|
|
try {
|
|
await buildModuleUi({ moduleDirectory: directory });
|
|
expect.unreachable("expected the stylesheet import to be rejected");
|
|
} catch (error) {
|
|
expect((error as ModuleUiBuildError).code).toBe("MODULE_UI_STYLESHEET_INVALID");
|
|
const diagnostics = (error as { diagnostics?: { text?: string }[] }).diagnostics ?? [];
|
|
expect(diagnostics.some((entry) => entry.text?.includes("plain .css only"))).toBe(true);
|
|
}
|
|
});
|
|
|
|
it("compiles a plain stylesheet into the artifact instead of emitting a sidecar", async () => {
|
|
const directory = await createModule({
|
|
ui: "./dist/ui/index.js",
|
|
files: {
|
|
"src/ui/index.tsx": `
|
|
import styles from "./styles.css";
|
|
export default { id: "example", version: "0.1.0", styles, mount: () => undefined };`,
|
|
"src/ui/styles.css": ".example { color: var(--ekklesia-text-primary); }",
|
|
},
|
|
});
|
|
|
|
const result = await buildModuleUi({ moduleDirectory: directory });
|
|
const artifact = await readFile(result.artifacts[0]?.outputFile ?? "", "utf8");
|
|
|
|
// The whole stylesheet travels inside the one declared artifact.
|
|
expect(artifact).toContain(".example");
|
|
expect(artifact).toContain("--ekklesia-text-primary");
|
|
// And nothing was emitted beside it. A sidecar is the exact failure this design prevents: a
|
|
// file nothing declares, no host fetches, and every local test silently passes without.
|
|
await expect(
|
|
readFile(path.join(directory, "dist", "ui", "index.css"), "utf8"),
|
|
).rejects.toThrow();
|
|
});
|
|
|
|
it("resolves a local @import so the compiled stylesheet is self-contained", async () => {
|
|
const directory = await createModule({
|
|
ui: "./dist/ui/index.js",
|
|
files: {
|
|
"src/ui/index.tsx": `
|
|
import styles from "./styles.css";
|
|
export default { id: "example", version: "0.1.0", styles, mount: () => undefined };`,
|
|
"src/ui/styles.css": '@import "./base.css";\n.example { color: red; }',
|
|
"src/ui/base.css": ".base { display: grid; }",
|
|
},
|
|
});
|
|
|
|
const result = await buildModuleUi({ moduleDirectory: directory });
|
|
const artifact = await readFile(result.artifacts[0]?.outputFile ?? "", "utf8");
|
|
|
|
expect(artifact).toContain(".base");
|
|
expect(artifact).toContain(".example");
|
|
});
|
|
|
|
it("rejects a stylesheet that keeps an unresolved @import, so the artifact stays self-contained", async () => {
|
|
const directory = await createModule({
|
|
ui: "./dist/ui/index.js",
|
|
files: {
|
|
"src/ui/index.tsx": `
|
|
import styles from "./styles.css";
|
|
export default { id: "example", version: "0.1.0", styles, mount: () => undefined };`,
|
|
"src/ui/styles.css": '@import url("https://fonts.example.invalid/family.css");',
|
|
},
|
|
});
|
|
|
|
await expect(buildModuleUi({ moduleDirectory: directory })).rejects.toMatchObject({
|
|
code: "MODULE_UI_STYLESHEET_INVALID",
|
|
});
|
|
});
|
|
|
|
it("rejects a stylesheet that declares a global Ekklesia theme variable", async () => {
|
|
const directory = await createModule({
|
|
ui: "./dist/ui/index.js",
|
|
files: {
|
|
"src/ui/index.tsx": `
|
|
import styles from "./styles.css";
|
|
export default { id: "example", version: "0.1.0", styles, mount: () => undefined };`,
|
|
// Referencing a token is the contract; *declaring* one would invent a global theme variable
|
|
// outside the registry a future theme editor enumerates (ADR 0039).
|
|
"src/ui/styles.css": ":host { --ekklesia-text-primary: hotpink; }",
|
|
},
|
|
});
|
|
|
|
try {
|
|
await buildModuleUi({ moduleDirectory: directory });
|
|
expect.unreachable("expected the stylesheet to be rejected");
|
|
} catch (error) {
|
|
expect((error as ModuleUiBuildError).code).toBe("MODULE_UI_STYLESHEET_INVALID");
|
|
const diagnostics = (error as { diagnostics?: { text?: string }[] }).diagnostics ?? [];
|
|
expect(diagnostics.some((entry) => entry.text?.includes("never declare one"))).toBe(true);
|
|
}
|
|
});
|
|
|
|
it("accepts a private alias derived from a registered token", async () => {
|
|
const directory = await createModule({
|
|
ui: "./dist/ui/index.js",
|
|
files: {
|
|
"src/ui/index.tsx": `
|
|
import styles from "./styles.css";
|
|
export default { id: "example", version: "0.1.0", styles, mount: () => undefined };`,
|
|
"src/ui/styles.css":
|
|
":host { --example-gap: var(--ekklesia-spacing-sm); }\n.example { gap: var(--example-gap); }",
|
|
},
|
|
});
|
|
|
|
const artifact = await readFile(
|
|
(await buildModuleUi({ moduleDirectory: directory })).artifacts[0]?.outputFile ?? "",
|
|
"utf8",
|
|
);
|
|
expect(artifact).toContain("--example-gap");
|
|
});
|
|
|
|
it("compiles a display artifact's stylesheet the same way", async () => {
|
|
const directory = await createModule({
|
|
display: {
|
|
artifact: "./dist/display/index.js",
|
|
permission: "example.display.view",
|
|
},
|
|
files: {
|
|
"src/display/index.tsx": `
|
|
import styles from "./styles.css";
|
|
export default { id: "example", version: "0.1.0", styles, mount: () => undefined };`,
|
|
"src/display/styles.css": ".display { position: fixed; }",
|
|
},
|
|
});
|
|
|
|
const result = await buildModuleUi({ moduleDirectory: directory });
|
|
const artifact = await readFile(result.artifacts[0]?.outputFile ?? "", "utf8");
|
|
|
|
expect(artifact).toContain(".display");
|
|
await expect(readFile(path.join(directory, "dist/display/index.css"))).rejects.toThrow();
|
|
});
|
|
|
|
it("substitutes NODE_ENV so the artifact never touches a Node global in a browser", async () => {
|
|
const directory = await createModule({
|
|
ui: "./dist/ui/index.js",
|
|
files: {
|
|
"src/ui/index.tsx":
|
|
'const mode = process.env.NODE_ENV;\nexport default { id: "example", version: "0.1.0", mount: () => { void mode; } };',
|
|
},
|
|
});
|
|
|
|
const result = await buildModuleUi({ moduleDirectory: directory });
|
|
const artifact = await readFile(result.artifacts[0]?.outputFile ?? "", "utf8");
|
|
|
|
expect(artifact).toContain('"production"');
|
|
expect(artifact).not.toContain("process.env.NODE_ENV");
|
|
});
|
|
});
|
|
|
|
describe("module UI build analysis", () => {
|
|
const styledSource = `import styles from "./styles.css";
|
|
${plainUiSource.replace("const definition = {", "const definition = {\n styles,")}`;
|
|
|
|
it("reports every source file behind each artifact, stylesheets included, and writes nothing", async () => {
|
|
const directory = await createModule({
|
|
ui: "./dist/ui/index.js",
|
|
files: {
|
|
"src/ui/index.tsx": styledSource,
|
|
"src/ui/styles.css":
|
|
'@import "./base.css";\n.example { color: var(--ekklesia-color-text); }\n',
|
|
"src/ui/base.css": ".example-base { margin: 0; }\n",
|
|
},
|
|
});
|
|
await buildModuleUi({ moduleDirectory: directory });
|
|
const built = await readFile(path.join(directory, "dist/ui/index.js"));
|
|
|
|
const analysis = await analyzeModuleUi({ moduleDirectory: directory });
|
|
|
|
expect(analysis.artifacts).toHaveLength(1);
|
|
const [artifact] = analysis.artifacts;
|
|
expect(artifact?.kind).toBe("ui");
|
|
expect(artifact?.outputFile).toBe(path.join(directory, "dist/ui/index.js"));
|
|
expect(artifact?.builtArtifactCurrent).toBe(true);
|
|
expect(artifact?.inputs).toEqual(
|
|
expect.arrayContaining([
|
|
path.join(directory, "src/ui/index.tsx"),
|
|
path.join(directory, "src/ui/styles.css"),
|
|
path.join(directory, "src/ui/base.css"),
|
|
]),
|
|
);
|
|
expect([...(artifact?.inputs ?? [])]).toEqual([...new Set(artifact?.inputs)].sort());
|
|
// Nothing was written: the built artifact is byte-for-byte what it was.
|
|
expect(await readFile(path.join(directory, "dist/ui/index.js"))).toEqual(built);
|
|
});
|
|
|
|
it("compiles stylesheets relative to the module, whatever directory the build runs from", async () => {
|
|
const directory = await createModule({
|
|
ui: "./dist/ui/index.js",
|
|
files: {
|
|
"src/ui/index.tsx": styledSource,
|
|
"src/ui/styles.css": '@import "./base.css";\n.example { margin: 0; }\n',
|
|
"src/ui/base.css": ".example-base { margin: 0; }\n",
|
|
},
|
|
});
|
|
// This test runs from the package directory, never from the module's own: the artifact must
|
|
// still name its stylesheets relative to the module, or its bytes would depend on the caller.
|
|
expect(process.cwd()).not.toBe(directory);
|
|
await buildModuleUi({ moduleDirectory: directory });
|
|
const artifact = await readFile(path.join(directory, "dist/ui/index.js"), "utf8");
|
|
expect(artifact).toContain("/* src/ui/styles.css */");
|
|
expect(artifact).toContain("/* src/ui/base.css */");
|
|
expect(artifact).not.toContain(path.basename(directory));
|
|
});
|
|
|
|
it("says when a built artifact no longer matches its sources", async () => {
|
|
const directory = await createModule({
|
|
ui: "./dist/ui/index.js",
|
|
files: { "src/ui/index.tsx": plainUiSource },
|
|
});
|
|
await buildModuleUi({ moduleDirectory: directory });
|
|
await writeFile(path.join(directory, "dist/ui/index.js"), "export default {};\n");
|
|
|
|
const analysis = await analyzeModuleUi({ moduleDirectory: directory });
|
|
expect(analysis.artifacts[0]?.builtArtifactCurrent).toBe(false);
|
|
});
|
|
|
|
it("says when a declared artifact was never built", async () => {
|
|
const directory = await createModule({
|
|
ui: "./dist/ui/index.js",
|
|
files: { "src/ui/index.tsx": plainUiSource },
|
|
});
|
|
const analysis = await analyzeModuleUi({ moduleDirectory: directory });
|
|
expect(analysis.artifacts[0]?.builtArtifactCurrent).toBe(false);
|
|
});
|
|
|
|
it("returns an empty analysis for a backend-only module", async () => {
|
|
const directory = await createModule();
|
|
await expect(analyzeModuleUi({ moduleDirectory: directory })).resolves.toEqual({
|
|
artifacts: [],
|
|
});
|
|
});
|
|
});
|