kitfly 0.1.2 → 0.2.1
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 +46 -0
- package/README.md +63 -16
- package/VERSION +1 -1
- package/dist/_raw/content/deployment/preflight.md +134 -0
- package/dist/_raw/content/deployment/recipes/aws-s3.md +128 -0
- package/dist/_raw/content/deployment/recipes/cloudflare-pages.md +73 -0
- package/dist/_raw/content/deployment/recipes/cloudflare-r2.md +156 -0
- package/dist/_raw/content/deployment/recipes/fly-io.md +57 -0
- package/dist/_raw/content/deployment/recipes/github-pages.md +112 -0
- package/dist/_raw/content/deployment/recipes/netlify.md +99 -0
- package/dist/_raw/content/deployment/recipes/vercel.md +88 -0
- package/dist/_raw/content/deployment/secrets-and-env-vars.md +75 -0
- package/dist/_raw/content/deployment.md +128 -0
- package/dist/_raw/content/guide/approaches.md +182 -0
- package/dist/_raw/content/guide/features.md +121 -0
- package/dist/_raw/content/guide/getting-started.md +112 -0
- package/dist/_raw/content/guide/kitfly-overview.md +209 -0
- package/dist/_raw/content/reference/configuration.md +259 -0
- package/dist/_raw/content/reference/design-catalog.md +167 -0
- package/dist/_raw/content/reference/environment-variables.md +66 -0
- package/dist/_raw/content/reference/glossary.md +92 -0
- package/dist/_raw/content/reference/key-concepts.md +118 -0
- package/dist/_raw/content/reference/plugins.md +220 -0
- package/dist/_raw/content/reference/slides-authoring-guidelines.md +129 -0
- package/dist/_raw/content/reference/structure.md +166 -0
- package/dist/_raw/content/reference.md +20 -0
- package/dist/_raw/content/templates/crucible.md +192 -0
- package/dist/_raw/content/templates/handbook.md +83 -0
- package/dist/_raw/content/templates/minimal.md +138 -0
- package/dist/_raw/content/templates/overview.md +187 -0
- package/dist/_raw/content/templates/pipeline.md +151 -0
- package/dist/_raw/content/templates/productbook.md +187 -0
- package/dist/_raw/content/templates/runbook.md +193 -0
- package/dist/_raw/content/templates/servicebook.md +163 -0
- package/dist/_raw/docs/decisions/ADR-0001-minimalist-site-code.md +118 -0
- package/dist/_raw/docs/decisions/ADR-0002-ai-accessibility.md +153 -0
- package/dist/_raw/docs/decisions/ADR-0003-single-file-bundle.md +93 -0
- package/dist/_raw/docs/decisions/ADR-0004-bun-runtime.md +98 -0
- package/dist/_raw/docs/decisions/ADR-0005-plugin-contract-and-distribution.md +110 -0
- package/dist/_raw/docs/decisions/DDR-0001-viewport-locked-layout.md +111 -0
- package/dist/_raw/docs/decisions/DDR-0002-theme-system.md +131 -0
- package/dist/_raw/docs/decisions/DDR-0003-bounded-logo-slot.md +106 -0
- package/dist/_raw/docs/decisions/DDR-0004-slides-rendering-model.md +113 -0
- package/dist/_raw/docs/decisions/DDR-0005-deterministic-layout-boundary.md +107 -0
- package/dist/_raw/docs/userguide/cli/build.md +85 -0
- package/dist/_raw/docs/userguide/cli/bundle.md +81 -0
- package/dist/_raw/docs/userguide/cli/dev.md +92 -0
- package/dist/_raw/docs/userguide/cli/init.md +116 -0
- package/dist/_raw/docs/userguide/cli/servers.md +69 -0
- package/dist/_raw/docs/userguide/cli/stop.md +76 -0
- package/dist/_raw/docs/userguide/cli/update.md +78 -0
- package/dist/_raw/docs/userguide/cli/version.md +65 -0
- package/dist/_raw/docs/userguide/cli.md +34 -0
- package/dist/_raw/docs/userguide/sharing.md +94 -0
- package/dist/_raw/schemas/plugin-schemas-notes.md +71 -0
- package/dist/_raw/schemas.md +42 -0
- package/dist/assets/brand/kitfly-favicon-32.png +0 -0
- package/dist/assets/brand/kitfly-icon-64.png +0 -0
- package/dist/assets/brand/kitfly-logo-128.png +0 -0
- package/dist/assets/brand/kitfly-logo-512.png +0 -0
- package/dist/assets/brand/kitfly-logo.svg +12132 -0
- package/dist/assets/brand/kitfly-neon-128.png +0 -0
- package/dist/assets/brand/kitfly-neon-192.png +0 -0
- package/dist/assets/brand/kitfly-neon-256.png +0 -0
- package/dist/assets/brand/kitfly-neon.png +0 -0
- package/dist/assets/brand/palette.md +75 -0
- package/dist/content/deployment/index.html +11 -0
- package/dist/content/deployment/preflight.html +418 -0
- package/dist/content/deployment/recipes/aws-s3.html +421 -0
- package/dist/content/deployment/recipes/cloudflare-pages.html +372 -0
- package/dist/content/deployment/recipes/cloudflare-r2.html +443 -0
- package/dist/content/deployment/recipes/fly-io.html +356 -0
- package/dist/content/deployment/recipes/github-pages.html +414 -0
- package/dist/content/deployment/recipes/index.html +11 -0
- package/dist/content/deployment/recipes/netlify.html +394 -0
- package/dist/content/deployment/recipes/vercel.html +382 -0
- package/dist/content/deployment/secrets-and-env-vars.html +380 -0
- package/dist/content/deployment.html +426 -0
- package/dist/content/guide/approaches.html +501 -0
- package/dist/content/guide/features.html +436 -0
- package/dist/content/guide/getting-started.html +403 -0
- package/dist/content/guide/index.html +11 -0
- package/dist/content/guide/kitfly-overview.html +544 -0
- package/dist/content/index.html +11 -0
- package/dist/content/reference/configuration.html +580 -0
- package/dist/content/reference/design-catalog.html +449 -0
- package/dist/content/reference/environment-variables.html +367 -0
- package/dist/content/reference/glossary.html +368 -0
- package/dist/content/reference/index.html +11 -0
- package/dist/content/reference/key-concepts.html +399 -0
- package/dist/content/reference/plugins.html +491 -0
- package/dist/content/reference/slides-authoring-guidelines.html +418 -0
- package/dist/content/reference/structure.html +463 -0
- package/dist/content/reference.html +335 -0
- package/dist/content/templates/crucible.html +546 -0
- package/dist/content/templates/handbook.html +405 -0
- package/dist/content/templates/index.html +11 -0
- package/dist/content/templates/minimal.html +447 -0
- package/dist/content/templates/overview.html +558 -0
- package/dist/content/templates/pipeline.html +494 -0
- package/dist/content/templates/productbook.html +540 -0
- package/dist/content/templates/runbook.html +543 -0
- package/dist/content/templates/servicebook.html +523 -0
- package/dist/content-index.json +549 -0
- package/dist/docs/decisions/ADR-0001-minimalist-site-code.html +491 -0
- package/dist/docs/decisions/ADR-0002-ai-accessibility.html +434 -0
- package/dist/docs/decisions/ADR-0003-single-file-bundle.html +412 -0
- package/dist/docs/decisions/ADR-0004-bun-runtime.html +409 -0
- package/dist/docs/decisions/ADR-0005-plugin-contract-and-distribution.html +402 -0
- package/dist/docs/decisions/DDR-0001-viewport-locked-layout.html +459 -0
- package/dist/docs/decisions/DDR-0002-theme-system.html +452 -0
- package/dist/docs/decisions/DDR-0003-bounded-logo-slot.html +423 -0
- package/dist/docs/decisions/DDR-0004-slides-rendering-model.html +399 -0
- package/dist/docs/decisions/DDR-0005-deterministic-layout-boundary.html +422 -0
- package/dist/docs/decisions/index.html +11 -0
- package/dist/docs/userguide/cli/build.html +408 -0
- package/dist/docs/userguide/cli/bundle.html +419 -0
- package/dist/docs/userguide/cli/dev.html +428 -0
- package/dist/docs/userguide/cli/index.html +11 -0
- package/dist/docs/userguide/cli/init.html +436 -0
- package/dist/docs/userguide/cli/servers.html +393 -0
- package/dist/docs/userguide/cli/stop.html +408 -0
- package/dist/docs/userguide/cli/update.html +406 -0
- package/dist/docs/userguide/cli/version.html +406 -0
- package/dist/docs/userguide/cli.html +386 -0
- package/dist/docs/userguide/index.html +11 -0
- package/dist/docs/userguide/sharing.html +465 -0
- package/dist/index.html +387 -0
- package/dist/llms.txt +18 -0
- package/dist/provenance.json +7 -0
- package/dist/schemas/index.html +11 -0
- package/dist/schemas/plugin-registry.schema.html +327 -0
- package/dist/schemas/plugin-schemas-notes.html +364 -0
- package/dist/schemas/plugin.schema.html +327 -0
- package/dist/schemas/plugins.schema.html +327 -0
- package/dist/schemas/v0/common.schema.html +386 -0
- package/dist/schemas/v0/index.html +11 -0
- package/dist/schemas/v0/plugin-registry.schema.html +547 -0
- package/dist/schemas/v0/plugin.schema.html +497 -0
- package/dist/schemas/v0/plugins.schema.html +406 -0
- package/dist/schemas/v0/site.schema.html +541 -0
- package/dist/schemas/v0/theme.schema.html +615 -0
- package/dist/schemas.html +351 -0
- package/dist/styles.css +1262 -0
- package/package.json +4 -2
- package/plugins-dist/callouts.css +32 -0
- package/plugins-dist/callouts.js +46 -0
- package/plugins-dist/slides-visuals.css +390 -0
- package/plugins-dist/slides-visuals.js +689 -0
- package/registry/plugins.yaml +35 -0
- package/schemas/README.md +10 -0
- package/schemas/plugin-registry.schema.json +5 -0
- package/schemas/plugin-schemas-notes.md +71 -0
- package/schemas/plugin.schema.json +5 -0
- package/schemas/plugins.schema.json +5 -0
- package/schemas/v0/common.schema.json +64 -0
- package/schemas/v0/plugin-registry.schema.json +225 -0
- package/schemas/v0/plugin.schema.json +175 -0
- package/schemas/v0/plugins.schema.json +84 -0
- package/schemas/v0/site.schema.json +56 -9
- package/schemas/v0/theme.schema.json +105 -22
- package/scripts/build.ts +158 -3
- package/scripts/bundle.ts +261 -95
- package/scripts/dev.ts +301 -11
- package/src/__tests__/build.test.ts +220 -1
- package/src/__tests__/bundle.test.ts +31 -0
- package/src/__tests__/cli.test.ts +14 -3
- package/src/__tests__/dev-plugin-errors.test.ts +20 -0
- package/src/__tests__/fixtures/fences/slides-visuals/invalid/bad-list-indent.md +5 -0
- package/src/__tests__/fixtures/fences/slides-visuals/invalid/blank-line.md +5 -0
- package/src/__tests__/fixtures/fences/slides-visuals/invalid/compare-object-items.md +9 -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/indented-fence.md +4 -0
- package/src/__tests__/fixtures/fences/slides-visuals/invalid/staircase-empty-steps.md +3 -0
- package/src/__tests__/fixtures/fences/slides-visuals/invalid/stat-grid-missing-fields.md +5 -0
- package/src/__tests__/fixtures/fences/slides-visuals/invalid/timeline-horizontal-no-events.md +2 -0
- package/src/__tests__/fixtures/fences/slides-visuals/invalid/unknown-type.md +3 -0
- package/src/__tests__/fixtures/fences/slides-visuals/valid/compare.md +10 -0
- package/src/__tests__/fixtures/fences/slides-visuals/valid/comparison-table.md +14 -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/funnel.md +7 -0
- package/src/__tests__/fixtures/fences/slides-visuals/valid/kpi.md +5 -0
- package/src/__tests__/fixtures/fences/slides-visuals/valid/layer-cake.md +6 -0
- package/src/__tests__/fixtures/fences/slides-visuals/valid/pyramid.md +6 -0
- package/src/__tests__/fixtures/fences/slides-visuals/valid/quadrant-grid.md +8 -0
- package/src/__tests__/fixtures/fences/slides-visuals/valid/scorecard.md +13 -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/stat-grid.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 +35 -0
- package/src/__tests__/plugin-loader.test.ts +221 -0
- package/src/__tests__/shared.test.ts +451 -0
- package/src/__tests__/slides-visuals-fence-contract.test.ts +28 -0
- package/src/__tests__/slides-visuals-runtime-regressions.bun.test.ts +147 -0
- package/src/__tests__/styles.test.ts +35 -0
- package/src/cli.ts +9 -4
- package/src/plugin-loader.ts +245 -0
- package/src/shared.ts +650 -7
- package/src/site/styles.css +331 -0
- package/src/site/template.html +66 -5
- package/src/templates/deck.ts +186 -0
- package/src/templates/driver.ts +11 -1
- package/src/templates/minimal.ts +1 -0
|
@@ -648,6 +648,12 @@ describe("buildBundleSidebarHeader", () => {
|
|
|
648
648
|
expect(source).toContain(`\${themeCSS}`);
|
|
649
649
|
});
|
|
650
650
|
|
|
651
|
+
it("bundle script keeps docs-mode smooth anchor scrolling", async () => {
|
|
652
|
+
const source = await readFile(`${process.cwd()}/scripts/bundle.ts`, "utf-8");
|
|
653
|
+
expect(source).toContain("if (!shell) {");
|
|
654
|
+
expect(source).toContain("scrollIntoView({ behavior: 'smooth', block: 'start' });");
|
|
655
|
+
});
|
|
656
|
+
|
|
651
657
|
it("shows version label with v prefix when version is provided", () => {
|
|
652
658
|
const config: SiteConfig = {
|
|
653
659
|
docroot: ".",
|
|
@@ -684,6 +690,31 @@ describe("buildBundleSidebarHeader", () => {
|
|
|
684
690
|
expect(html).toContain('alt="My Company"');
|
|
685
691
|
});
|
|
686
692
|
|
|
693
|
+
it("includes initial fallback metadata and onerror handler", () => {
|
|
694
|
+
const config: SiteConfig = {
|
|
695
|
+
docroot: ".",
|
|
696
|
+
title: "Test",
|
|
697
|
+
brand: { name: "Acme", url: "/" },
|
|
698
|
+
sections: [],
|
|
699
|
+
};
|
|
700
|
+
|
|
701
|
+
const html = buildBundleSidebarHeader(config, "1.0", "logo.png");
|
|
702
|
+
expect(html).toContain('data-initial="A"');
|
|
703
|
+
expect(html).toContain("classList.add('logo-fallback')");
|
|
704
|
+
});
|
|
705
|
+
|
|
706
|
+
it("escapes initial fallback character in data attribute", () => {
|
|
707
|
+
const config: SiteConfig = {
|
|
708
|
+
docroot: ".",
|
|
709
|
+
title: "Test",
|
|
710
|
+
brand: { name: '"quoted', url: "/" },
|
|
711
|
+
sections: [],
|
|
712
|
+
};
|
|
713
|
+
|
|
714
|
+
const html = buildBundleSidebarHeader(config, "1.0", "logo.png");
|
|
715
|
+
expect(html).toContain('data-initial="""');
|
|
716
|
+
});
|
|
717
|
+
|
|
687
718
|
it("links brand to brand URL", () => {
|
|
688
719
|
const config: SiteConfig = {
|
|
689
720
|
docroot: ".",
|
|
@@ -54,7 +54,7 @@ kitfly v${version} - Turn your writing into a website
|
|
|
54
54
|
Usage:
|
|
55
55
|
kitfly dev [folder] Start dev server with hot reload
|
|
56
56
|
kitfly build [folder] Build static site to dist/
|
|
57
|
-
kitfly bundle [folder] Build single-file HTML bundle
|
|
57
|
+
kitfly bundle [folder] Build single-file HTML bundle to bundles/
|
|
58
58
|
kitfly init [name] Create new project from template
|
|
59
59
|
kitfly servers List running dev servers
|
|
60
60
|
kitfly stop <port|all> Stop dev server(s)
|
|
@@ -68,11 +68,15 @@ Dev options:
|
|
|
68
68
|
--json Output JSON (implies --daemon)
|
|
69
69
|
--no-open Don't open browser
|
|
70
70
|
|
|
71
|
-
Build
|
|
71
|
+
Build options:
|
|
72
72
|
--out <dir> Output directory [env: KITFLY_BUILD_OUT] (default: dist)
|
|
73
|
-
--name <file> Bundle filename (default: bundle.html)
|
|
74
73
|
--no-raw Don't include raw markdown
|
|
75
74
|
|
|
75
|
+
Bundle options:
|
|
76
|
+
--out <dir> Output directory [env: KITFLY_BUNDLE_OUT] (default: bundles)
|
|
77
|
+
--name <file> Bundle filename (default: bundle.html)
|
|
78
|
+
--no-raw Don't include raw markdown [env: KITFLY_BUNDLE_RAW]
|
|
79
|
+
|
|
76
80
|
Stop options:
|
|
77
81
|
--force Skip graceful shutdown, kill immediately
|
|
78
82
|
|
|
@@ -84,6 +88,7 @@ Examples:
|
|
|
84
88
|
kitfly stop 4000
|
|
85
89
|
kitfly stop all
|
|
86
90
|
kitfly build ./docs --out ./public
|
|
91
|
+
kitfly bundle ./docs --out ./bundles --name docs.html
|
|
87
92
|
kitfly init my-handbook
|
|
88
93
|
|
|
89
94
|
Documentation: https://kitfly.app
|
|
@@ -647,6 +652,12 @@ describe("command argument defaults", () => {
|
|
|
647
652
|
const name = (flags.name as string) || "bundle.html";
|
|
648
653
|
expect(name).toBe("bundle.html");
|
|
649
654
|
});
|
|
655
|
+
|
|
656
|
+
it("bundle defaults out to bundles", () => {
|
|
657
|
+
const { flags } = routeCommand(["bundle"]);
|
|
658
|
+
const out = (flags.out as string) || "bundles";
|
|
659
|
+
expect(out).toBe("bundles");
|
|
660
|
+
});
|
|
650
661
|
});
|
|
651
662
|
|
|
652
663
|
describe("daemon mode detection", () => {
|
|
@@ -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
|
+
});
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
:::comparison-table
|
|
2
|
+
headers:
|
|
3
|
+
- Feature
|
|
4
|
+
- Us
|
|
5
|
+
- Competitor A
|
|
6
|
+
- Competitor B
|
|
7
|
+
rows:
|
|
8
|
+
- ["Real-time sync", "Yes", "Yes", "No"]
|
|
9
|
+
- ["Offline mode", "Yes", "No", "Yes"]
|
|
10
|
+
- ["Plugin system", "Yes (v0.2.0)", "No", "Limited"]
|
|
11
|
+
- ["Self-hosted option", "Yes", "No", "No"]
|
|
12
|
+
- ["AI generation", "Yes", "Beta", "No"]
|
|
13
|
+
- ["Price (team/mo)", "$49", "$79", "$39"]
|
|
14
|
+
:::
|
|
@@ -51,6 +51,7 @@ describe("template registry", () => {
|
|
|
51
51
|
const ids = templates.map((t) => t.id);
|
|
52
52
|
|
|
53
53
|
expect(ids).toContain("minimal");
|
|
54
|
+
expect(ids).toContain("deck");
|
|
54
55
|
expect(ids).toContain("handbook");
|
|
55
56
|
expect(templates.length).toBeGreaterThanOrEqual(2);
|
|
56
57
|
});
|
|
@@ -257,6 +258,40 @@ describe("handbook template", () => {
|
|
|
257
258
|
});
|
|
258
259
|
});
|
|
259
260
|
|
|
261
|
+
// ---------------------------------------------------------------------------
|
|
262
|
+
// Deck Template (slides mode)
|
|
263
|
+
// ---------------------------------------------------------------------------
|
|
264
|
+
|
|
265
|
+
describe("deck template", () => {
|
|
266
|
+
const projectName = "test-deck";
|
|
267
|
+
|
|
268
|
+
it("creates slides-focused starter files", async () => {
|
|
269
|
+
await runTemplate({
|
|
270
|
+
name: projectName,
|
|
271
|
+
template: "deck",
|
|
272
|
+
git: false,
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
expect(generatedExists(projectName, "site.yaml")).toBe(true);
|
|
276
|
+
expect(generatedExists(projectName, "content/slides/briefing.md")).toBe(true);
|
|
277
|
+
expect(generatedExists(projectName, "CUSTOMIZING.md")).toBe(true);
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
it("configures site.yaml for slides mode", async () => {
|
|
281
|
+
await runTemplate({
|
|
282
|
+
name: projectName,
|
|
283
|
+
template: "deck",
|
|
284
|
+
git: false,
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
const siteYaml = await readGenerated(projectName, "site.yaml");
|
|
288
|
+
expect(siteYaml).toContain("mode: slides");
|
|
289
|
+
expect(siteYaml).toContain('aspect: "16/9"');
|
|
290
|
+
expect(siteYaml).toContain('name: "Slides"');
|
|
291
|
+
expect(siteYaml).toContain('path: "content/slides"');
|
|
292
|
+
});
|
|
293
|
+
});
|
|
294
|
+
|
|
260
295
|
// ---------------------------------------------------------------------------
|
|
261
296
|
// Custom Branding
|
|
262
297
|
// ---------------------------------------------------------------------------
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { mkdir, mkdtemp, readFile, writeFile } from "node:fs/promises";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { dirname, join } from "node:path";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
import { describe, expect, it } from "vitest";
|
|
7
|
+
import {
|
|
8
|
+
loadPluginInjections,
|
|
9
|
+
loadPluginRegistry,
|
|
10
|
+
PluginConfigError,
|
|
11
|
+
PluginIntegrityError,
|
|
12
|
+
} from "../plugin-loader.ts";
|
|
13
|
+
|
|
14
|
+
const REPO_ROOT = join(dirname(fileURLToPath(import.meta.url)), "..", "..");
|
|
15
|
+
|
|
16
|
+
function sha256Hex(text: string): string {
|
|
17
|
+
return createHash("sha256").update(new TextEncoder().encode(text)).digest("hex");
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
describe("plugin loader", () => {
|
|
21
|
+
it("inlines local assets with checksum verification", async () => {
|
|
22
|
+
const root = await mkdtemp(join(tmpdir(), "kitfly-plugins-"));
|
|
23
|
+
|
|
24
|
+
await mkdir(join(root, "registry"), { recursive: true });
|
|
25
|
+
await mkdir(join(root, "plugins-dist"), { recursive: true });
|
|
26
|
+
|
|
27
|
+
const js = "console.log('hello plugin');";
|
|
28
|
+
const css = ".hello{color:red;}";
|
|
29
|
+
await writeFile(join(root, "plugins-dist", "hello.js"), js, "utf-8");
|
|
30
|
+
await writeFile(join(root, "plugins-dist", "hello.css"), css, "utf-8");
|
|
31
|
+
|
|
32
|
+
const registryYaml = `version: 1
|
|
33
|
+
updated: "2026-02-12"
|
|
34
|
+
baseUrl: ""
|
|
35
|
+
plugins:
|
|
36
|
+
hello:
|
|
37
|
+
name: "Hello"
|
|
38
|
+
description: "Test plugin"
|
|
39
|
+
version: "1.0.0"
|
|
40
|
+
contract: "1"
|
|
41
|
+
kitfly: ">=0.2.0 <1.0.0"
|
|
42
|
+
license: MIT
|
|
43
|
+
verified: true
|
|
44
|
+
assets:
|
|
45
|
+
js: "plugins-dist/hello.js"
|
|
46
|
+
css: "plugins-dist/hello.css"
|
|
47
|
+
assetSha256:
|
|
48
|
+
js: "sha256:${sha256Hex(js)}"
|
|
49
|
+
css: "sha256:${sha256Hex(css)}"
|
|
50
|
+
`;
|
|
51
|
+
|
|
52
|
+
await writeFile(join(root, "registry", "plugins.yaml"), registryYaml, "utf-8");
|
|
53
|
+
|
|
54
|
+
const configYaml = `plugins:
|
|
55
|
+
- hello@1.0.0
|
|
56
|
+
`;
|
|
57
|
+
await writeFile(join(root, "kitfly.plugins.yaml"), configYaml, "utf-8");
|
|
58
|
+
|
|
59
|
+
const injections = await loadPluginInjections({ root });
|
|
60
|
+
expect(injections.head).toContain(css);
|
|
61
|
+
expect(injections.bodyEnd).toContain(js);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it("respects plugin mode allowlist (modes)", async () => {
|
|
65
|
+
const root = await mkdtemp(join(tmpdir(), "kitfly-plugins-"));
|
|
66
|
+
|
|
67
|
+
await mkdir(join(root, "registry"), { recursive: true });
|
|
68
|
+
await mkdir(join(root, "plugins-dist"), { recursive: true });
|
|
69
|
+
|
|
70
|
+
const js = "console.log('hello plugin');";
|
|
71
|
+
await writeFile(join(root, "plugins-dist", "hello.js"), js, "utf-8");
|
|
72
|
+
|
|
73
|
+
const registryYaml = `version: 1
|
|
74
|
+
updated: "2026-02-12"
|
|
75
|
+
baseUrl: ""
|
|
76
|
+
plugins:
|
|
77
|
+
hello:
|
|
78
|
+
name: "Hello"
|
|
79
|
+
description: "Test plugin"
|
|
80
|
+
version: "1.0.0"
|
|
81
|
+
contract: "1"
|
|
82
|
+
kitfly: ">=0.2.0 <1.0.0"
|
|
83
|
+
license: MIT
|
|
84
|
+
verified: true
|
|
85
|
+
modes: ["slides"]
|
|
86
|
+
assets:
|
|
87
|
+
js: "plugins-dist/hello.js"
|
|
88
|
+
assetSha256:
|
|
89
|
+
js: "sha256:${sha256Hex(js)}"
|
|
90
|
+
`;
|
|
91
|
+
|
|
92
|
+
await writeFile(join(root, "registry", "plugins.yaml"), registryYaml, "utf-8");
|
|
93
|
+
await writeFile(join(root, "kitfly.plugins.yaml"), "plugins:\n - hello@1.0.0\n", "utf-8");
|
|
94
|
+
|
|
95
|
+
const docs = await loadPluginInjections({ root, mode: "docs" });
|
|
96
|
+
expect(docs.head).toBe("");
|
|
97
|
+
expect(docs.bodyEnd).toBe("");
|
|
98
|
+
|
|
99
|
+
const slides = await loadPluginInjections({ root, mode: "slides" });
|
|
100
|
+
expect(slides.bodyEnd).toContain(js);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it("treats empty modes as blocked", async () => {
|
|
104
|
+
const root = await mkdtemp(join(tmpdir(), "kitfly-plugins-"));
|
|
105
|
+
|
|
106
|
+
await mkdir(join(root, "registry"), { recursive: true });
|
|
107
|
+
await mkdir(join(root, "plugins-dist"), { recursive: true });
|
|
108
|
+
|
|
109
|
+
const js = "console.log('hello plugin');";
|
|
110
|
+
await writeFile(join(root, "plugins-dist", "hello.js"), js, "utf-8");
|
|
111
|
+
|
|
112
|
+
const registryYaml = `version: 1
|
|
113
|
+
updated: "2026-02-12"
|
|
114
|
+
baseUrl: ""
|
|
115
|
+
plugins:
|
|
116
|
+
hello:
|
|
117
|
+
name: "Hello"
|
|
118
|
+
description: "Test plugin"
|
|
119
|
+
version: "1.0.0"
|
|
120
|
+
contract: "1"
|
|
121
|
+
kitfly: ">=0.2.0 <1.0.0"
|
|
122
|
+
license: MIT
|
|
123
|
+
verified: true
|
|
124
|
+
modes: []
|
|
125
|
+
assets:
|
|
126
|
+
js: "plugins-dist/hello.js"
|
|
127
|
+
assetSha256:
|
|
128
|
+
js: "sha256:${sha256Hex(js)}"
|
|
129
|
+
`;
|
|
130
|
+
|
|
131
|
+
await writeFile(join(root, "registry", "plugins.yaml"), registryYaml, "utf-8");
|
|
132
|
+
await writeFile(join(root, "kitfly.plugins.yaml"), "plugins:\n - hello@1.0.0\n", "utf-8");
|
|
133
|
+
|
|
134
|
+
const injections = await loadPluginInjections({ root, mode: "slides" });
|
|
135
|
+
expect(injections.head).toBe("");
|
|
136
|
+
expect(injections.bodyEnd).toBe("");
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it("throws on checksum mismatch", async () => {
|
|
140
|
+
const root = await mkdtemp(join(tmpdir(), "kitfly-plugins-"));
|
|
141
|
+
|
|
142
|
+
await mkdir(join(root, "registry"), { recursive: true });
|
|
143
|
+
await mkdir(join(root, "plugins-dist"), { recursive: true });
|
|
144
|
+
|
|
145
|
+
const js = "console.log('hello plugin');";
|
|
146
|
+
await writeFile(join(root, "plugins-dist", "hello.js"), js, "utf-8");
|
|
147
|
+
|
|
148
|
+
const registryYaml = `version: 1
|
|
149
|
+
updated: "2026-02-12"
|
|
150
|
+
baseUrl: ""
|
|
151
|
+
plugins:
|
|
152
|
+
hello:
|
|
153
|
+
name: "Hello"
|
|
154
|
+
description: "Test plugin"
|
|
155
|
+
version: "1.0.0"
|
|
156
|
+
contract: "1"
|
|
157
|
+
kitfly: ">=0.2.0 <1.0.0"
|
|
158
|
+
license: MIT
|
|
159
|
+
verified: true
|
|
160
|
+
assets:
|
|
161
|
+
js: "plugins-dist/hello.js"
|
|
162
|
+
assetSha256:
|
|
163
|
+
js: "sha256:${"0".repeat(64)}"
|
|
164
|
+
`;
|
|
165
|
+
|
|
166
|
+
await writeFile(join(root, "registry", "plugins.yaml"), registryYaml, "utf-8");
|
|
167
|
+
await writeFile(join(root, "kitfly.plugins.yaml"), "plugins:\n - hello@1.0.0\n", "utf-8");
|
|
168
|
+
|
|
169
|
+
await expect(loadPluginInjections({ root })).rejects.toBeInstanceOf(PluginIntegrityError);
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
it("rejects invalid canonical refs (prevents attribute injection)", async () => {
|
|
173
|
+
const root = await mkdtemp(join(tmpdir(), "kitfly-plugins-"));
|
|
174
|
+
await mkdir(join(root, "registry"), { recursive: true });
|
|
175
|
+
|
|
176
|
+
await writeFile(
|
|
177
|
+
join(root, "registry", "plugins.yaml"),
|
|
178
|
+
`version: 1
|
|
179
|
+
updated: "2026-02-12"
|
|
180
|
+
baseUrl: ""
|
|
181
|
+
plugins: {}
|
|
182
|
+
`,
|
|
183
|
+
"utf-8",
|
|
184
|
+
);
|
|
185
|
+
|
|
186
|
+
await writeFile(
|
|
187
|
+
join(root, "kitfly.plugins.yaml"),
|
|
188
|
+
`plugins:
|
|
189
|
+
- bad"@1.0.0
|
|
190
|
+
`,
|
|
191
|
+
"utf-8",
|
|
192
|
+
);
|
|
193
|
+
|
|
194
|
+
await expect(loadPluginInjections({ root })).rejects.toBeInstanceOf(PluginConfigError);
|
|
195
|
+
});
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
describe("registry consistency", () => {
|
|
199
|
+
it("all plugin dist checksums match registry/plugins.yaml", async () => {
|
|
200
|
+
const registryPath = join(REPO_ROOT, "registry", "plugins.yaml");
|
|
201
|
+
const registry = await loadPluginRegistry(registryPath);
|
|
202
|
+
|
|
203
|
+
for (const [id, plugin] of Object.entries(registry.plugins)) {
|
|
204
|
+
const { assets } = plugin;
|
|
205
|
+
for (const kind of ["js", "css"] as const) {
|
|
206
|
+
const relPath = assets[kind];
|
|
207
|
+
if (!relPath) continue;
|
|
208
|
+
|
|
209
|
+
const expectedRaw = assets.assetSha256[kind];
|
|
210
|
+
if (!expectedRaw) throw new Error(`${id}: missing assetSha256.${kind}`);
|
|
211
|
+
|
|
212
|
+
const expectedHex = expectedRaw.replace(/^sha256:/, "").toLowerCase();
|
|
213
|
+
const filePath = join(REPO_ROOT, relPath);
|
|
214
|
+
const content = await readFile(filePath);
|
|
215
|
+
const actualHex = createHash("sha256").update(content).digest("hex");
|
|
216
|
+
|
|
217
|
+
expect(actualHex, `${id} ${kind} (${relPath})`).toBe(expectedHex);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
});
|
|
221
|
+
});
|