kitfly 0.2.0 → 0.2.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +68 -0
- package/README.md +25 -10
- package/VERSION +1 -1
- package/dist/_raw/content/guide/branding.md +146 -0
- package/dist/_raw/content/guide/data-driven-content.md +204 -0
- package/dist/_raw/content/reference/configuration.md +145 -7
- package/dist/_raw/content/reference/environment-variables.md +26 -1
- package/dist/_raw/content/reference/glossary.md +25 -1
- package/dist/_raw/content/reference/key-concepts.md +30 -2
- package/dist/_raw/content/reference/plugins.md +14 -0
- package/dist/_raw/content/reference/slides-authoring-guidelines.md +129 -0
- package/dist/_raw/content/reference.md +1 -0
- package/dist/_raw/docs/decisions/ADR-0006-data-driven-content.md +350 -0
- package/dist/content/deployment/preflight.html +10 -6
- package/dist/content/deployment/recipes/aws-s3.html +10 -6
- package/dist/content/deployment/recipes/cloudflare-pages.html +10 -6
- package/dist/content/deployment/recipes/cloudflare-r2.html +10 -6
- package/dist/content/deployment/recipes/fly-io.html +10 -6
- package/dist/content/deployment/recipes/github-pages.html +10 -6
- package/dist/content/deployment/recipes/netlify.html +10 -6
- package/dist/content/deployment/recipes/vercel.html +10 -6
- package/dist/content/deployment/secrets-and-env-vars.html +10 -6
- package/dist/content/deployment.html +10 -6
- package/dist/content/guide/approaches.html +10 -6
- package/dist/content/guide/branding.html +510 -0
- package/dist/content/guide/data-driven-content.html +543 -0
- package/dist/content/guide/features.html +10 -6
- package/dist/content/guide/getting-started.html +10 -6
- package/dist/content/guide/kitfly-overview.html +10 -6
- package/dist/content/reference/configuration.html +135 -9
- package/dist/content/reference/design-catalog.html +10 -6
- package/dist/content/reference/environment-variables.html +50 -8
- package/dist/content/reference/glossary.html +24 -8
- package/dist/content/reference/key-concepts.html +33 -9
- package/dist/content/reference/plugins.html +22 -7
- package/dist/content/reference/slides-authoring-guidelines.html +422 -0
- package/dist/content/reference/structure.html +10 -6
- package/dist/content/reference.html +11 -6
- package/dist/content/templates/crucible.html +10 -6
- package/dist/content/templates/handbook.html +10 -6
- package/dist/content/templates/minimal.html +10 -6
- package/dist/content/templates/overview.html +10 -6
- package/dist/content/templates/pipeline.html +10 -6
- package/dist/content/templates/productbook.html +10 -6
- package/dist/content/templates/runbook.html +10 -6
- package/dist/content/templates/servicebook.html +10 -6
- package/dist/content-index.json +38 -2
- package/dist/docs/decisions/ADR-0001-minimalist-site-code.html +10 -6
- package/dist/docs/decisions/ADR-0002-ai-accessibility.html +10 -6
- package/dist/docs/decisions/ADR-0003-single-file-bundle.html +10 -6
- package/dist/docs/decisions/ADR-0004-bun-runtime.html +10 -6
- package/dist/docs/decisions/ADR-0005-plugin-contract-and-distribution.html +10 -6
- package/dist/docs/decisions/ADR-0006-data-driven-content.html +752 -0
- package/dist/docs/decisions/DDR-0001-viewport-locked-layout.html +10 -6
- package/dist/docs/decisions/DDR-0002-theme-system.html +10 -6
- package/dist/docs/decisions/DDR-0003-bounded-logo-slot.html +10 -6
- package/dist/docs/decisions/DDR-0004-slides-rendering-model.html +10 -6
- package/dist/docs/decisions/DDR-0005-deterministic-layout-boundary.html +10 -6
- package/dist/docs/userguide/cli/build.html +10 -6
- package/dist/docs/userguide/cli/bundle.html +10 -6
- package/dist/docs/userguide/cli/dev.html +10 -6
- package/dist/docs/userguide/cli/init.html +10 -6
- package/dist/docs/userguide/cli/servers.html +10 -6
- package/dist/docs/userguide/cli/stop.html +10 -6
- package/dist/docs/userguide/cli/update.html +10 -6
- package/dist/docs/userguide/cli/version.html +10 -6
- package/dist/docs/userguide/cli.html +10 -6
- package/dist/docs/userguide/sharing.html +10 -6
- package/dist/index.html +10 -6
- package/dist/llms.txt +3 -3
- package/dist/provenance.json +4 -4
- package/dist/schemas/plugin-registry.schema.html +10 -6
- package/dist/schemas/plugin-schemas-notes.html +10 -6
- package/dist/schemas/plugin.schema.html +10 -6
- package/dist/schemas/plugins.schema.html +10 -6
- package/dist/schemas/v0/common.schema.html +14 -10
- package/dist/schemas/v0/plugin-registry.schema.html +13 -9
- package/dist/schemas/v0/plugin.schema.html +13 -9
- package/dist/schemas/v0/plugins.schema.html +13 -9
- package/dist/schemas/v0/site.schema.html +67 -7
- package/dist/schemas/v0/theme.schema.html +21 -17
- package/dist/schemas.html +10 -6
- package/dist/styles.css +39 -4
- package/package.json +1 -1
- package/plugins-dist/latex-runtime.js +140 -0
- package/plugins-dist/latex.js +178 -0
- package/plugins-dist/slides-charts-lite-runtime.js +179 -0
- package/plugins-dist/slides-charts-lite.js +198 -0
- package/plugins-dist/slides-visuals.css +166 -0
- package/plugins-dist/slides-visuals.js +124 -33
- package/registry/plugins.yaml +30 -5
- package/schemas/v0/site.schema.json +56 -0
- package/scripts/build.ts +195 -70
- package/scripts/bundle.ts +122 -11
- package/scripts/dev.ts +345 -178
- package/src/__tests__/brief.test.ts +151 -0
- package/src/__tests__/build.test.ts +234 -4
- package/src/__tests__/bundle.test.ts +134 -0
- package/src/__tests__/dev-plugin-errors.test.ts +20 -0
- package/src/__tests__/fixtures/fences/slides-visuals/invalid/flow-branching-no-source.md +5 -0
- package/src/__tests__/fixtures/fences/slides-visuals/invalid/flow-converging-no-target.md +6 -0
- package/src/__tests__/fixtures/fences/slides-visuals/invalid/staircase-empty-steps.md +3 -0
- package/src/__tests__/fixtures/fences/slides-visuals/invalid/timeline-horizontal-no-events.md +2 -0
- package/src/__tests__/fixtures/fences/slides-visuals/valid/flow-branching-no-split.md +7 -0
- package/src/__tests__/fixtures/fences/slides-visuals/valid/flow-branching.md +8 -0
- package/src/__tests__/fixtures/fences/slides-visuals/valid/flow-converging-no-merge.md +7 -0
- package/src/__tests__/fixtures/fences/slides-visuals/valid/flow-converging.md +8 -0
- package/src/__tests__/fixtures/fences/slides-visuals/valid/staircase-down.md +7 -0
- package/src/__tests__/fixtures/fences/slides-visuals/valid/staircase.md +8 -0
- package/src/__tests__/fixtures/fences/slides-visuals/valid/timeline-horizontal.md +9 -0
- package/src/__tests__/fixtures/fences/slides-visuals/valid/timeline-vertical.md +10 -0
- package/src/__tests__/init.test.ts +51 -2
- package/src/__tests__/latex-runtime.bun.test.ts +35 -0
- package/src/__tests__/shared.test.ts +621 -1
- package/src/__tests__/slides-charts-lite-runtime.bun.test.ts +45 -0
- package/src/__tests__/slides-visuals-runtime-regressions.bun.test.ts +33 -0
- package/src/cli.ts +11 -4
- package/src/commands/init.ts +1 -1
- package/src/shared.ts +761 -18
- package/src/site/styles.css +39 -4
- package/src/site/template.html +5 -2
- package/src/templates/brief.ts +486 -0
- package/src/templates/deck.ts +59 -0
- package/src/templates/driver.ts +46 -13
- package/src/templates/handbook.ts +32 -0
- package/src/templates/runbook.ts +32 -0
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for the Brief template definition
|
|
3
|
+
*
|
|
4
|
+
* Covers: src/templates/brief.ts
|
|
5
|
+
* Strategy: import the template directly, verify structure and generated content
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { describe, expect, it } from "vitest";
|
|
9
|
+
import { brief } from "../templates/brief.ts";
|
|
10
|
+
import type { BrandingConfig, TemplateContext, TemplateFile } from "../templates/schema.ts";
|
|
11
|
+
|
|
12
|
+
function makeCtx(overrides?: Partial<TemplateContext>): TemplateContext {
|
|
13
|
+
const branding: BrandingConfig = {
|
|
14
|
+
siteName: "Test Brief",
|
|
15
|
+
brandName: "Acme Corp",
|
|
16
|
+
brandUrl: "https://acme.example.com",
|
|
17
|
+
primaryColor: "#2563eb",
|
|
18
|
+
footerText: "Footer text",
|
|
19
|
+
};
|
|
20
|
+
return {
|
|
21
|
+
name: "test-brief",
|
|
22
|
+
branding,
|
|
23
|
+
template: brief,
|
|
24
|
+
year: 2026,
|
|
25
|
+
...overrides,
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function resolveContent(file: TemplateFile, ctx: TemplateContext): string {
|
|
30
|
+
return typeof file.content === "function" ? file.content(ctx) : file.content;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function findFile(path: string): TemplateFile {
|
|
34
|
+
const f = brief.files.find((entry) => entry.path === path);
|
|
35
|
+
if (!f) throw new Error(`Template file not found: ${path}`);
|
|
36
|
+
return f;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
describe("brief template definition", () => {
|
|
40
|
+
it("has correct identity metadata", () => {
|
|
41
|
+
expect(brief.id).toBe("brief");
|
|
42
|
+
expect(brief.name).toBe("Brief");
|
|
43
|
+
expect(brief.version).toBe(1);
|
|
44
|
+
expect(brief.extends).toBe("minimal");
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it("defines exactly four sections", () => {
|
|
48
|
+
expect(brief.sections).toHaveLength(4);
|
|
49
|
+
expect(brief.sections.map((s) => s.name)).toEqual([
|
|
50
|
+
"Product",
|
|
51
|
+
"Use Cases",
|
|
52
|
+
"Getting Started",
|
|
53
|
+
"Reference",
|
|
54
|
+
]);
|
|
55
|
+
expect(brief.sections.map((s) => s.path)).toEqual([
|
|
56
|
+
"content/product",
|
|
57
|
+
"content/use-cases",
|
|
58
|
+
"content/getting-started",
|
|
59
|
+
"content/reference",
|
|
60
|
+
]);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it("defines unique file paths", () => {
|
|
64
|
+
const paths = brief.files.map((f) => f.path);
|
|
65
|
+
expect(new Set(paths).size).toBe(paths.length);
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
describe("brief site.yaml", () => {
|
|
70
|
+
const ctx = makeCtx();
|
|
71
|
+
const file = findFile("site.yaml");
|
|
72
|
+
|
|
73
|
+
it("includes branding and all four section paths", () => {
|
|
74
|
+
const content = resolveContent(file, ctx);
|
|
75
|
+
expect(content).toContain('title: "Test Brief"');
|
|
76
|
+
expect(content).toContain('name: "Acme Corp"');
|
|
77
|
+
expect(content).toContain('url: "https://acme.example.com"');
|
|
78
|
+
expect(content).toContain('"content/product"');
|
|
79
|
+
expect(content).toContain('"content/use-cases"');
|
|
80
|
+
expect(content).toContain('"content/getting-started"');
|
|
81
|
+
expect(content).toContain('"content/reference"');
|
|
82
|
+
expect(content).toContain('home: "index.md"');
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
describe("brief starter files", () => {
|
|
87
|
+
const ctx = makeCtx();
|
|
88
|
+
const starterFiles = [
|
|
89
|
+
"index.md",
|
|
90
|
+
"content/product/overview.md",
|
|
91
|
+
"content/product/capabilities.md",
|
|
92
|
+
"content/use-cases/overview.md",
|
|
93
|
+
"content/use-cases/example-use-case.md",
|
|
94
|
+
"content/getting-started/overview.md",
|
|
95
|
+
"content/getting-started/requirements.md",
|
|
96
|
+
"content/reference/architecture.md",
|
|
97
|
+
"content/reference/faq.md",
|
|
98
|
+
"content/reference/contacts.md",
|
|
99
|
+
];
|
|
100
|
+
|
|
101
|
+
it("ships all 10 starter files", () => {
|
|
102
|
+
expect(starterFiles).toHaveLength(10);
|
|
103
|
+
for (const path of starterFiles) {
|
|
104
|
+
expect(brief.files.some((f) => f.path === path)).toBe(true);
|
|
105
|
+
}
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it("includes frontmatter in markdown starter files", () => {
|
|
109
|
+
for (const path of starterFiles) {
|
|
110
|
+
const content = resolveContent(findFile(path), ctx);
|
|
111
|
+
expect(content).toMatch(/^---\n/);
|
|
112
|
+
}
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it("includes customize markers in starter files", () => {
|
|
116
|
+
for (const path of starterFiles) {
|
|
117
|
+
const content = resolveContent(findFile(path), ctx);
|
|
118
|
+
expect(content).toContain("<!-- ← CUSTOMIZE");
|
|
119
|
+
}
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it("uses professional framing in index and use cases", () => {
|
|
123
|
+
const index = resolveContent(findFile("index.md"), ctx);
|
|
124
|
+
expect(index).toContain("At a Glance");
|
|
125
|
+
expect(index).toContain("[Use Cases](/content/use-cases/overview)");
|
|
126
|
+
|
|
127
|
+
const useCases = resolveContent(findFile("content/use-cases/example-use-case.md"), ctx);
|
|
128
|
+
expect(useCases).toContain("## Scenario");
|
|
129
|
+
expect(useCases).toContain("## Outcome");
|
|
130
|
+
expect(useCases).toContain("| Metric | Before | After |");
|
|
131
|
+
});
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
describe("brief CUSTOMIZING.md", () => {
|
|
135
|
+
const ctx = makeCtx();
|
|
136
|
+
const file = findFile("CUSTOMIZING.md");
|
|
137
|
+
|
|
138
|
+
it("documents template identity and external-audience tone", () => {
|
|
139
|
+
const content = resolveContent(file, ctx);
|
|
140
|
+
expect(content).toContain("template: brief");
|
|
141
|
+
expect(content).toContain("clients, prospects, and partners");
|
|
142
|
+
expect(content).toContain("Informational and professional");
|
|
143
|
+
expect(content).toContain("Not sales copy");
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
it("guides use-case duplication workflow", () => {
|
|
147
|
+
const content = resolveContent(file, ctx);
|
|
148
|
+
expect(content).toContain("Duplicate `content/use-cases/example-use-case.md`");
|
|
149
|
+
expect(content).toContain("problem -> solution -> outcome");
|
|
150
|
+
});
|
|
151
|
+
});
|
|
@@ -35,7 +35,8 @@ async function writeSiteYaml(dir: string, extra: Record<string, unknown> = {}):
|
|
|
35
35
|
const mode = extra.mode ? `mode: ${extra.mode}\n` : "";
|
|
36
36
|
const aspect = extra.aspect ? `aspect: ${extra.aspect}\n` : "";
|
|
37
37
|
const home = extra.home ? `home: ${extra.home}\n` : "";
|
|
38
|
-
const
|
|
38
|
+
const footer = extra.footer ? `footer:\n${extra.footer}\n` : "";
|
|
39
|
+
const yaml = `title: ${title}\n${version}${mode}${aspect}brand:\n${brand}\n${home}${footer}sections:\n${sections}\n`;
|
|
39
40
|
await writeFile(join(dir, "site.yaml"), yaml);
|
|
40
41
|
}
|
|
41
42
|
|
|
@@ -216,6 +217,45 @@ describe("build", () => {
|
|
|
216
217
|
expect(html).toContain("unversioned");
|
|
217
218
|
});
|
|
218
219
|
|
|
220
|
+
it("renders footer logo with static path prefix in built output", async () => {
|
|
221
|
+
const siteDir = await makeTempDir();
|
|
222
|
+
const outDir = "out";
|
|
223
|
+
await writeSiteYaml(siteDir, {
|
|
224
|
+
footer: ' logo: "assets/brand/footer-logo.png"\n logoAlt: "Footer Brand"\n logoHeight: 24',
|
|
225
|
+
});
|
|
226
|
+
await writeMd(siteDir, "docs/page.md", "# Page");
|
|
227
|
+
|
|
228
|
+
await build({ folder: siteDir, out: outDir });
|
|
229
|
+
|
|
230
|
+
const html = await readFile(join(siteDir, outDir, "index.html"), "utf-8");
|
|
231
|
+
expect(html).toContain('class="footer-logo-img"');
|
|
232
|
+
expect(html).toContain('src="./assets/brand/footer-logo.png"');
|
|
233
|
+
expect(html).toContain('alt="Footer Brand"');
|
|
234
|
+
expect(html).toContain("max-height: 24px");
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
it("renders light/dark variants for header and footer logos when configured", async () => {
|
|
238
|
+
const siteDir = await makeTempDir();
|
|
239
|
+
const outDir = "out";
|
|
240
|
+
await writeSiteYaml(siteDir, {
|
|
241
|
+
brand:
|
|
242
|
+
' name: Test\n url: /\n logo: "assets/brand/logo.png"\n logoDark: "assets/brand/logo-dark.png"',
|
|
243
|
+
footer:
|
|
244
|
+
' logo: "assets/brand/footer-logo.png"\n logoDark: "assets/brand/footer-logo-dark.png"',
|
|
245
|
+
});
|
|
246
|
+
await writeMd(siteDir, "docs/page.md", "# Page");
|
|
247
|
+
|
|
248
|
+
await build({ folder: siteDir, out: outDir });
|
|
249
|
+
|
|
250
|
+
const html = await readFile(join(siteDir, outDir, "index.html"), "utf-8");
|
|
251
|
+
expect(html).toContain("logo-img logo-light");
|
|
252
|
+
expect(html).toContain("logo-img logo-dark");
|
|
253
|
+
expect(html).toContain("./assets/brand/logo-dark.png");
|
|
254
|
+
expect(html).toContain("footer-logo-img logo-light");
|
|
255
|
+
expect(html).toContain("footer-logo-img logo-dark");
|
|
256
|
+
expect(html).toContain("./assets/brand/footer-logo-dark.png");
|
|
257
|
+
});
|
|
258
|
+
|
|
219
259
|
it("builds a single-page hash-routed deck when mode is slides", async () => {
|
|
220
260
|
const siteDir = await makeTempDir();
|
|
221
261
|
const outDir = "out";
|
|
@@ -332,6 +372,87 @@ plugins:
|
|
|
332
372
|
expect(html).toContain(js);
|
|
333
373
|
});
|
|
334
374
|
|
|
375
|
+
it("preserves dollar-sign replacement patterns in injected plugin JS", async () => {
|
|
376
|
+
const siteDir = await makeTempDir();
|
|
377
|
+
const outDir = "out";
|
|
378
|
+
await writeSiteYaml(siteDir);
|
|
379
|
+
await writeMd(siteDir, "docs/page.md", "# Test");
|
|
380
|
+
|
|
381
|
+
const js = "const expr='x'; const sample = `$${expr}$`; const marker = '$`';";
|
|
382
|
+
await mkdir(join(siteDir, "plugins-dist"), { recursive: true });
|
|
383
|
+
await writeFile(join(siteDir, "plugins-dist", "literal.js"), js, "utf-8");
|
|
384
|
+
|
|
385
|
+
await mkdir(join(siteDir, "registry"), { recursive: true });
|
|
386
|
+
await writeFile(
|
|
387
|
+
join(siteDir, "registry", "plugins.yaml"),
|
|
388
|
+
`version: 1
|
|
389
|
+
updated: "2026-02-16"
|
|
390
|
+
baseUrl: ""
|
|
391
|
+
plugins:
|
|
392
|
+
literal:
|
|
393
|
+
name: "Literal"
|
|
394
|
+
description: "Replacement pattern regression"
|
|
395
|
+
version: "1.0.0"
|
|
396
|
+
contract: "1"
|
|
397
|
+
kitfly: ">=0.2.0 <1.0.0"
|
|
398
|
+
license: MIT
|
|
399
|
+
verified: true
|
|
400
|
+
assets:
|
|
401
|
+
js: "plugins-dist/literal.js"
|
|
402
|
+
assetSha256:
|
|
403
|
+
js: "sha256:${sha256Hex(js)}"
|
|
404
|
+
`,
|
|
405
|
+
"utf-8",
|
|
406
|
+
);
|
|
407
|
+
|
|
408
|
+
await writeFile(join(siteDir, "kitfly.plugins.yaml"), "plugins:\n - literal@1.0.0\n", "utf-8");
|
|
409
|
+
|
|
410
|
+
await build({ folder: siteDir, out: outDir });
|
|
411
|
+
|
|
412
|
+
const html = await readFile(join(siteDir, outDir, "index.html"), "utf-8");
|
|
413
|
+
expect(html).toContain("const sample = `$${expr}$`;");
|
|
414
|
+
expect(html).toContain("const marker = '$`';");
|
|
415
|
+
expect(html).not.toContain("<!DOCTYPE html><html");
|
|
416
|
+
});
|
|
417
|
+
|
|
418
|
+
it("preserves dollar-sign replacement patterns in markdown content", async () => {
|
|
419
|
+
const siteDir = await makeTempDir();
|
|
420
|
+
const outDir = "out";
|
|
421
|
+
await writeSiteYaml(siteDir);
|
|
422
|
+
await writeMd(
|
|
423
|
+
siteDir,
|
|
424
|
+
"docs/dollars.md",
|
|
425
|
+
"# Dollars\n\nLiteral dollars: $$ and $` and $' and $&\n\nMath style: $$x^2$$",
|
|
426
|
+
);
|
|
427
|
+
|
|
428
|
+
await build({ folder: siteDir, out: outDir });
|
|
429
|
+
|
|
430
|
+
const html = await readFile(join(siteDir, outDir, "index.html"), "utf-8");
|
|
431
|
+
expect(html).toContain("Literal dollars: $$ and $` and $' and $&");
|
|
432
|
+
expect(html).toContain("Math style: $$x^2$$");
|
|
433
|
+
expect(html.match(/<!DOCTYPE html>/g)?.length ?? 0).toBe(1);
|
|
434
|
+
});
|
|
435
|
+
|
|
436
|
+
it("injects latex plugin in docs mode", async () => {
|
|
437
|
+
const siteDir = await makeTempDir();
|
|
438
|
+
const outDir = "out";
|
|
439
|
+
await writeSiteYaml(siteDir);
|
|
440
|
+
await writeMd(
|
|
441
|
+
siteDir,
|
|
442
|
+
"docs/math.md",
|
|
443
|
+
"# Math\n\nInline: $x^2$.\n\n$$\n\\\\int_0^1 x^2 dx\n$$\n\n```math\n\\\\sum_{i=1}^{n} i\n```",
|
|
444
|
+
);
|
|
445
|
+
await writeFile(join(siteDir, "kitfly.plugins.yaml"), "plugins:\n - latex@0.2.2\n", "utf-8");
|
|
446
|
+
|
|
447
|
+
await build({ folder: siteDir, out: outDir });
|
|
448
|
+
|
|
449
|
+
const html = await readFile(join(siteDir, outDir, "index.html"), "utf-8");
|
|
450
|
+
expect(html).toContain('data-kitfly-plugin="latex@0.2.2"');
|
|
451
|
+
expect(html).toContain("const KATEX_JS_URL =");
|
|
452
|
+
expect(html).toContain("cdn.jsdelivr.net/npm/katex@0.16.21/dist/katex.min.js");
|
|
453
|
+
expect(html).toContain("cdn.jsdelivr.net/npm/katex@0.16.21/dist/katex.min.css");
|
|
454
|
+
});
|
|
455
|
+
|
|
335
456
|
it("injects slides-only plugins when mode=slides", async () => {
|
|
336
457
|
const siteDir = await makeTempDir();
|
|
337
458
|
const outDir = "out";
|
|
@@ -365,7 +486,7 @@ plugins:
|
|
|
365
486
|
slides-visuals:
|
|
366
487
|
name: "Slides Visuals"
|
|
367
488
|
description: "Test visuals"
|
|
368
|
-
version: "0.2.
|
|
489
|
+
version: "0.2.1"
|
|
369
490
|
contract: "1"
|
|
370
491
|
kitfly: ">=0.2.0 <1.0.0"
|
|
371
492
|
license: MIT
|
|
@@ -383,15 +504,124 @@ plugins:
|
|
|
383
504
|
|
|
384
505
|
await writeFile(
|
|
385
506
|
join(siteDir, "kitfly.plugins.yaml"),
|
|
386
|
-
"plugins:\n - slides-visuals@0.2.
|
|
507
|
+
"plugins:\n - slides-visuals@0.2.1\n",
|
|
387
508
|
"utf-8",
|
|
388
509
|
);
|
|
389
510
|
|
|
390
511
|
await build({ folder: siteDir, out: outDir });
|
|
391
512
|
|
|
392
513
|
const html = await readFile(join(siteDir, outDir, "index.html"), "utf-8");
|
|
393
|
-
expect(html).toContain('data-kitfly-plugin="slides-visuals@0.2.
|
|
514
|
+
expect(html).toContain('data-kitfly-plugin="slides-visuals@0.2.1"');
|
|
394
515
|
expect(html).toContain(css);
|
|
395
516
|
expect(html).toContain(js);
|
|
396
517
|
});
|
|
518
|
+
|
|
519
|
+
it("injects slides-charts-lite plugin in slides mode", async () => {
|
|
520
|
+
const siteDir = await makeTempDir();
|
|
521
|
+
const outDir = "out";
|
|
522
|
+
await writeSiteYaml(siteDir, { mode: "slides" });
|
|
523
|
+
await writeMd(
|
|
524
|
+
siteDir,
|
|
525
|
+
"docs/deck.md",
|
|
526
|
+
`# Chart
|
|
527
|
+
|
|
528
|
+
\`\`\`chart
|
|
529
|
+
kind: bar
|
|
530
|
+
title: Revenue
|
|
531
|
+
labels: ["Q1", "Q2"]
|
|
532
|
+
data: [10, 12]
|
|
533
|
+
\`\`\`
|
|
534
|
+
`,
|
|
535
|
+
);
|
|
536
|
+
await writeFile(
|
|
537
|
+
join(siteDir, "kitfly.plugins.yaml"),
|
|
538
|
+
"plugins:\n - slides-charts-lite@0.2.2\n",
|
|
539
|
+
"utf-8",
|
|
540
|
+
);
|
|
541
|
+
|
|
542
|
+
await build({ folder: siteDir, out: outDir });
|
|
543
|
+
|
|
544
|
+
const html = await readFile(join(siteDir, outDir, "index.html"), "utf-8");
|
|
545
|
+
expect(html).toContain('data-kitfly-plugin="slides-charts-lite@0.2.2"');
|
|
546
|
+
expect(html).toContain("kitfly-chart-wrapper");
|
|
547
|
+
});
|
|
548
|
+
|
|
549
|
+
it("does not inject slides-charts-lite in docs mode", async () => {
|
|
550
|
+
const siteDir = await makeTempDir();
|
|
551
|
+
const outDir = "out";
|
|
552
|
+
await writeSiteYaml(siteDir, { mode: "docs" });
|
|
553
|
+
await writeMd(siteDir, "docs/page.md", "# Page");
|
|
554
|
+
await writeFile(
|
|
555
|
+
join(siteDir, "kitfly.plugins.yaml"),
|
|
556
|
+
"plugins:\n - slides-charts-lite@0.2.2\n",
|
|
557
|
+
"utf-8",
|
|
558
|
+
);
|
|
559
|
+
|
|
560
|
+
await build({ folder: siteDir, out: outDir });
|
|
561
|
+
|
|
562
|
+
const html = await readFile(join(siteDir, outDir, "index.html"), "utf-8");
|
|
563
|
+
expect(html).not.toContain('data-kitfly-plugin="slides-charts-lite@0.2.2"');
|
|
564
|
+
});
|
|
565
|
+
|
|
566
|
+
it("ignores unknown slides-visuals block types while enforcing known contracts", async () => {
|
|
567
|
+
const siteDir = await makeTempDir();
|
|
568
|
+
const outDir = "out";
|
|
569
|
+
await writeSiteYaml(siteDir, { mode: "slides" });
|
|
570
|
+
await writeMd(
|
|
571
|
+
siteDir,
|
|
572
|
+
"docs/deck.md",
|
|
573
|
+
`# Title
|
|
574
|
+
|
|
575
|
+
:::future-thing
|
|
576
|
+
note: this should pass through
|
|
577
|
+
:::
|
|
578
|
+
|
|
579
|
+
:::kpi
|
|
580
|
+
label: Uptime
|
|
581
|
+
value: 99.95%
|
|
582
|
+
:::
|
|
583
|
+
`,
|
|
584
|
+
);
|
|
585
|
+
|
|
586
|
+
const js = "console.log('slides visuals');";
|
|
587
|
+
const css = ".kitfly-visual{border:1px solid red;}";
|
|
588
|
+
await mkdir(join(siteDir, "plugins-dist"), { recursive: true });
|
|
589
|
+
await writeFile(join(siteDir, "plugins-dist", "slides-visuals.js"), js, "utf-8");
|
|
590
|
+
await writeFile(join(siteDir, "plugins-dist", "slides-visuals.css"), css, "utf-8");
|
|
591
|
+
await mkdir(join(siteDir, "registry"), { recursive: true });
|
|
592
|
+
await writeFile(
|
|
593
|
+
join(siteDir, "registry", "plugins.yaml"),
|
|
594
|
+
`version: 1
|
|
595
|
+
updated: "2026-02-15"
|
|
596
|
+
baseUrl: ""
|
|
597
|
+
plugins:
|
|
598
|
+
slides-visuals:
|
|
599
|
+
name: "Slides Visuals"
|
|
600
|
+
description: "Test visuals"
|
|
601
|
+
version: "0.2.1"
|
|
602
|
+
contract: "1"
|
|
603
|
+
kitfly: ">=0.2.0 <1.0.0"
|
|
604
|
+
license: MIT
|
|
605
|
+
verified: true
|
|
606
|
+
modes: ["slides"]
|
|
607
|
+
assets:
|
|
608
|
+
js: "plugins-dist/slides-visuals.js"
|
|
609
|
+
css: "plugins-dist/slides-visuals.css"
|
|
610
|
+
assetSha256:
|
|
611
|
+
js: "sha256:${sha256Hex(js)}"
|
|
612
|
+
css: "sha256:${sha256Hex(css)}"
|
|
613
|
+
`,
|
|
614
|
+
"utf-8",
|
|
615
|
+
);
|
|
616
|
+
await writeFile(
|
|
617
|
+
join(siteDir, "kitfly.plugins.yaml"),
|
|
618
|
+
"plugins:\n - slides-visuals@0.2.1\n",
|
|
619
|
+
"utf-8",
|
|
620
|
+
);
|
|
621
|
+
|
|
622
|
+
await expect(build({ folder: siteDir, out: outDir })).resolves.toBeUndefined();
|
|
623
|
+
const html = await readFile(join(siteDir, outDir, "index.html"), "utf-8");
|
|
624
|
+
expect(html).toContain('data-kitfly-plugin="slides-visuals@0.2.1"');
|
|
625
|
+
expect(html).toContain("future-thing");
|
|
626
|
+
});
|
|
397
627
|
});
|
|
@@ -5,6 +5,7 @@ import { afterEach, describe, expect, it } from "vitest";
|
|
|
5
5
|
import {
|
|
6
6
|
buildBundleNav,
|
|
7
7
|
buildBundleSidebarHeader,
|
|
8
|
+
bundleSite,
|
|
8
9
|
fileToDataUri,
|
|
9
10
|
imageMime,
|
|
10
11
|
inlineLocalImages,
|
|
@@ -815,3 +816,136 @@ describe("buildBundleSidebarHeader", () => {
|
|
|
815
816
|
expect(html).toContain(`src="${logo}"`);
|
|
816
817
|
});
|
|
817
818
|
});
|
|
819
|
+
|
|
820
|
+
describe("bundleSite plugin integration", () => {
|
|
821
|
+
it("inlines latex plugin script when enabled", async () => {
|
|
822
|
+
const siteDir = await makeTempDir();
|
|
823
|
+
await mkdir(join(siteDir, "docs"), { recursive: true });
|
|
824
|
+
await writeFile(
|
|
825
|
+
join(siteDir, "site.yaml"),
|
|
826
|
+
'title: "Bundle Test"\nbrand:\n name: "Test"\n url: "/"\nsections:\n - name: Docs\n path: docs\n',
|
|
827
|
+
"utf-8",
|
|
828
|
+
);
|
|
829
|
+
await writeFile(join(siteDir, "docs", "index.md"), "# Bundle Math\n\n$E=mc^2$", "utf-8");
|
|
830
|
+
await writeFile(join(siteDir, "kitfly.plugins.yaml"), "plugins:\n - latex@0.2.2\n", "utf-8");
|
|
831
|
+
|
|
832
|
+
await bundleSite({ folder: siteDir, out: "bundles", name: "bundle.html" });
|
|
833
|
+
|
|
834
|
+
const html = await readFile(join(siteDir, "bundles", "bundle.html"), "utf-8");
|
|
835
|
+
expect(html).toContain('data-kitfly-plugin="latex@0.2.2"');
|
|
836
|
+
expect(html).toContain("const KATEX_JS_URL =");
|
|
837
|
+
expect(html).toContain("cdn.jsdelivr.net/npm/katex@0.16.21/dist/katex.min.js");
|
|
838
|
+
expect(html).toContain("cdn.jsdelivr.net/npm/katex@0.16.21/dist/katex.min.css");
|
|
839
|
+
});
|
|
840
|
+
|
|
841
|
+
it("inlines slides-charts-lite plugin in slides mode", async () => {
|
|
842
|
+
const siteDir = await makeTempDir();
|
|
843
|
+
await mkdir(join(siteDir, "slides"), { recursive: true });
|
|
844
|
+
await writeFile(
|
|
845
|
+
join(siteDir, "site.yaml"),
|
|
846
|
+
'title: "Bundle Charts Test"\nmode: "slides"\nbrand:\n name: "Test"\n url: "/"\nsections:\n - name: Slides\n path: slides\n',
|
|
847
|
+
"utf-8",
|
|
848
|
+
);
|
|
849
|
+
await writeFile(
|
|
850
|
+
join(siteDir, "slides", "deck.md"),
|
|
851
|
+
'# Deck\n\n```chart\nkind: line\nlabels: ["W1", "W2"]\ndata: [2, 3]\n```',
|
|
852
|
+
"utf-8",
|
|
853
|
+
);
|
|
854
|
+
await writeFile(
|
|
855
|
+
join(siteDir, "kitfly.plugins.yaml"),
|
|
856
|
+
"plugins:\n - slides-charts-lite@0.2.2\n",
|
|
857
|
+
"utf-8",
|
|
858
|
+
);
|
|
859
|
+
|
|
860
|
+
await bundleSite({ folder: siteDir, out: "bundles", name: "bundle.html" });
|
|
861
|
+
|
|
862
|
+
const html = await readFile(join(siteDir, "bundles", "bundle.html"), "utf-8");
|
|
863
|
+
expect(html).toContain('data-kitfly-plugin="slides-charts-lite@0.2.2"');
|
|
864
|
+
expect(html).toContain("kitfly-chart-wrapper");
|
|
865
|
+
});
|
|
866
|
+
|
|
867
|
+
it("inlines footer logo image when configured", async () => {
|
|
868
|
+
const siteDir = await makeTempDir();
|
|
869
|
+
await mkdir(join(siteDir, "docs"), { recursive: true });
|
|
870
|
+
await mkdir(join(siteDir, "assets", "brand"), { recursive: true });
|
|
871
|
+
await writeFile(
|
|
872
|
+
join(siteDir, "site.yaml"),
|
|
873
|
+
'title: "Bundle Footer Test"\nbrand:\n name: "Test"\n url: "/"\nfooter:\n logo: "assets/brand/footer-logo.png"\n logoAlt: "Footer Brand"\n logoHeight: 24\nsections:\n - name: Docs\n path: docs\n',
|
|
874
|
+
"utf-8",
|
|
875
|
+
);
|
|
876
|
+
await writeFile(join(siteDir, "docs", "index.md"), "# Footer Logo");
|
|
877
|
+
await writeFile(
|
|
878
|
+
join(siteDir, "assets", "brand", "footer-logo.png"),
|
|
879
|
+
Buffer.from(
|
|
880
|
+
"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAusB9Wl9x9kAAAAASUVORK5CYII=",
|
|
881
|
+
"base64",
|
|
882
|
+
),
|
|
883
|
+
);
|
|
884
|
+
|
|
885
|
+
await bundleSite({ folder: siteDir, out: "bundles", name: "bundle.html" });
|
|
886
|
+
|
|
887
|
+
const html = await readFile(join(siteDir, "bundles", "bundle.html"), "utf-8");
|
|
888
|
+
expect(html).toContain('class="footer-logo-img"');
|
|
889
|
+
expect(html).toContain('src="data:image/png;base64,');
|
|
890
|
+
expect(html).not.toContain('src="assets/brand/footer-logo.png"');
|
|
891
|
+
expect(html).toContain('alt="Footer Brand"');
|
|
892
|
+
expect(html).toContain("max-height: 24px");
|
|
893
|
+
});
|
|
894
|
+
|
|
895
|
+
it("inlines footer logo from site-root-relative path outside assets", async () => {
|
|
896
|
+
const siteDir = await makeTempDir();
|
|
897
|
+
await mkdir(join(siteDir, "docs"), { recursive: true });
|
|
898
|
+
await mkdir(join(siteDir, "logos"), { recursive: true });
|
|
899
|
+
await writeFile(
|
|
900
|
+
join(siteDir, "site.yaml"),
|
|
901
|
+
'title: "Bundle Footer Root Path Test"\nbrand:\n name: "Test"\n url: "/"\nfooter:\n logo: "logos/footer.png"\n logoAlt: "Footer Root Logo"\nsections:\n - name: Docs\n path: docs\n',
|
|
902
|
+
"utf-8",
|
|
903
|
+
);
|
|
904
|
+
await writeFile(join(siteDir, "docs", "index.md"), "# Footer Root Path");
|
|
905
|
+
await writeFile(
|
|
906
|
+
join(siteDir, "logos", "footer.png"),
|
|
907
|
+
Buffer.from(
|
|
908
|
+
"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAusB9Wl9x9kAAAAASUVORK5CYII=",
|
|
909
|
+
"base64",
|
|
910
|
+
),
|
|
911
|
+
);
|
|
912
|
+
|
|
913
|
+
await bundleSite({ folder: siteDir, out: "bundles", name: "bundle.html" });
|
|
914
|
+
|
|
915
|
+
const html = await readFile(join(siteDir, "bundles", "bundle.html"), "utf-8");
|
|
916
|
+
expect(html).toContain('class="footer-logo-img"');
|
|
917
|
+
expect(html).toContain('src="data:image/png;base64,');
|
|
918
|
+
expect(html).not.toContain('src="logos/footer.png"');
|
|
919
|
+
expect(html).toContain('alt="Footer Root Logo"');
|
|
920
|
+
});
|
|
921
|
+
|
|
922
|
+
it("inlines light/dark variants for header and footer logos", async () => {
|
|
923
|
+
const siteDir = await makeTempDir();
|
|
924
|
+
await mkdir(join(siteDir, "docs"), { recursive: true });
|
|
925
|
+
await mkdir(join(siteDir, "assets", "brand"), { recursive: true });
|
|
926
|
+
await writeFile(
|
|
927
|
+
join(siteDir, "site.yaml"),
|
|
928
|
+
'title: "Bundle Dark Logos Test"\nbrand:\n name: "Test"\n url: "/"\n logo: "assets/brand/logo.png"\n logoDark: "assets/brand/logo-dark.png"\nfooter:\n logo: "assets/brand/footer-logo.png"\n logoDark: "assets/brand/footer-logo-dark.png"\nsections:\n - name: Docs\n path: docs\n',
|
|
929
|
+
"utf-8",
|
|
930
|
+
);
|
|
931
|
+
await writeFile(join(siteDir, "docs", "index.md"), "# Dark Logos");
|
|
932
|
+
const pixelPng = Buffer.from(
|
|
933
|
+
"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAusB9Wl9x9kAAAAASUVORK5CYII=",
|
|
934
|
+
"base64",
|
|
935
|
+
);
|
|
936
|
+
await writeFile(join(siteDir, "assets", "brand", "logo.png"), pixelPng);
|
|
937
|
+
await writeFile(join(siteDir, "assets", "brand", "logo-dark.png"), pixelPng);
|
|
938
|
+
await writeFile(join(siteDir, "assets", "brand", "footer-logo.png"), pixelPng);
|
|
939
|
+
await writeFile(join(siteDir, "assets", "brand", "footer-logo-dark.png"), pixelPng);
|
|
940
|
+
|
|
941
|
+
await bundleSite({ folder: siteDir, out: "bundles", name: "bundle.html" });
|
|
942
|
+
|
|
943
|
+
const html = await readFile(join(siteDir, "bundles", "bundle.html"), "utf-8");
|
|
944
|
+
expect(html).toContain('class="logo-img logo-light"');
|
|
945
|
+
expect(html).toContain('class="logo-img logo-dark"');
|
|
946
|
+
expect(html).toContain('class="footer-logo-img logo-light"');
|
|
947
|
+
expect(html).toContain('class="footer-logo-img logo-dark"');
|
|
948
|
+
expect(html).not.toContain('src="assets/brand/logo-dark.png"');
|
|
949
|
+
expect(html).not.toContain('src="assets/brand/footer-logo-dark.png"');
|
|
950
|
+
});
|
|
951
|
+
});
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { buildDevPluginErrorHtml } from "../../scripts/dev.ts";
|
|
3
|
+
|
|
4
|
+
describe("dev plugin error page", () => {
|
|
5
|
+
it("shows actionable update hint for plugin version mismatch", () => {
|
|
6
|
+
const html = buildDevPluginErrorHtml("Plugin slides-visuals version mismatch: 0.2.0 != 0.2.1");
|
|
7
|
+
expect(html).toContain("Plugin setup error");
|
|
8
|
+
expect(html).toContain("kitfly.plugins.yaml");
|
|
9
|
+
expect(html).toContain("slides-visuals@0.2.1");
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
it("escapes error text and falls back to generic guidance", () => {
|
|
13
|
+
const html = buildDevPluginErrorHtml('Invalid plugin ref: bad"@1.0.0 <x>');
|
|
14
|
+
expect(html).toContain(
|
|
15
|
+
"Check <code>kitfly.plugins.yaml</code> and <code>registry/plugins.yaml</code>",
|
|
16
|
+
);
|
|
17
|
+
expect(html).toContain("bad"@1.0.0 <x>");
|
|
18
|
+
expect(html).not.toContain('bad"@1.0.0 <x>');
|
|
19
|
+
});
|
|
20
|
+
});
|