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
|
@@ -12,3 +12,38 @@ describe("sidebar folder indicator CSS", () => {
|
|
|
12
12
|
expect(css).toContain("transform: rotate(90deg)");
|
|
13
13
|
});
|
|
14
14
|
});
|
|
15
|
+
|
|
16
|
+
describe("slide layout primitives CSS", () => {
|
|
17
|
+
it("includes core block-flow/grid/stack primitives", async () => {
|
|
18
|
+
const css = await readFile(join(process.cwd(), "src/site/styles.css"), "utf-8");
|
|
19
|
+
expect(css).toContain(".block-flow");
|
|
20
|
+
expect(css).toContain(".block-grid");
|
|
21
|
+
expect(css).toContain(".block-stack");
|
|
22
|
+
expect(css).toContain(".block-label");
|
|
23
|
+
expect(css).toContain(".block-flow:not(.vertical) .block:not(:last-child)::after");
|
|
24
|
+
expect(css).toContain(".block-flow.vertical .block:not(:last-child)::after");
|
|
25
|
+
expect(css).toContain(".block-grid.cols-3");
|
|
26
|
+
expect(css).toContain(".block-grid.cols-4");
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it("includes shape modifiers for block visuals", async () => {
|
|
30
|
+
const css = await readFile(join(process.cwd(), "src/site/styles.css"), "utf-8");
|
|
31
|
+
expect(css).toContain(".block.circle");
|
|
32
|
+
expect(css).toContain(".block.pill");
|
|
33
|
+
expect(css).toContain(".block.diamond");
|
|
34
|
+
expect(css).toContain(".block.chevron");
|
|
35
|
+
expect(css).toContain(".block.hexagon");
|
|
36
|
+
expect(css).toContain(".block.triangle");
|
|
37
|
+
expect(css).toContain(".block.block-arrow");
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it("keeps non-active layout-class slides hidden", async () => {
|
|
41
|
+
const css = await readFile(join(process.cwd(), "src/site/styles.css"), "utf-8");
|
|
42
|
+
expect(css).toContain(".slide {");
|
|
43
|
+
expect(css).toContain("display: none;");
|
|
44
|
+
expect(css).toContain(".slide.active.centered");
|
|
45
|
+
expect(css).toContain(".slide.active.two-column");
|
|
46
|
+
expect(css).not.toContain(".slide.centered {\n min-height: 100%;\n display: flex;");
|
|
47
|
+
expect(css).not.toContain(".slide.two-column {\n display: grid;");
|
|
48
|
+
});
|
|
49
|
+
});
|
package/src/cli.ts
CHANGED
|
@@ -70,7 +70,7 @@ kitfly v${VERSION} - Turn your writing into a website
|
|
|
70
70
|
Usage:
|
|
71
71
|
kitfly dev [folder] Start dev server with hot reload
|
|
72
72
|
kitfly build [folder] Build static site to dist/
|
|
73
|
-
kitfly bundle [folder] Build single-file HTML bundle
|
|
73
|
+
kitfly bundle [folder] Build single-file HTML bundle to bundles/
|
|
74
74
|
kitfly init [name] Create new project from template
|
|
75
75
|
kitfly update [version] Update standalone site code
|
|
76
76
|
kitfly servers List running dev servers
|
|
@@ -86,11 +86,15 @@ Dev options:
|
|
|
86
86
|
--json Output JSON (implies --daemon)
|
|
87
87
|
--no-open Don't open browser
|
|
88
88
|
|
|
89
|
-
Build
|
|
89
|
+
Build options:
|
|
90
90
|
--out <dir> Output directory [env: KITFLY_BUILD_OUT] (default: dist)
|
|
91
|
-
--name <file> Bundle filename (default: bundle.html)
|
|
92
91
|
--no-raw Don't include raw markdown
|
|
93
92
|
|
|
93
|
+
Bundle options:
|
|
94
|
+
--out <dir> Output directory [env: KITFLY_BUNDLE_OUT] (default: bundles)
|
|
95
|
+
--name <file> Bundle filename (default: bundle.html)
|
|
96
|
+
--no-raw Don't include raw markdown [env: KITFLY_BUNDLE_RAW]
|
|
97
|
+
|
|
94
98
|
Stop options:
|
|
95
99
|
--force Skip graceful shutdown, kill immediately
|
|
96
100
|
|
|
@@ -117,6 +121,7 @@ Examples:
|
|
|
117
121
|
kitfly logs 3340 --follow
|
|
118
122
|
kitfly logs --clean
|
|
119
123
|
kitfly build ./docs --out ./public
|
|
124
|
+
kitfly bundle ./docs --out ./bundles --name docs.html
|
|
120
125
|
kitfly init my-handbook
|
|
121
126
|
kitfly update --check
|
|
122
127
|
|
|
@@ -359,7 +364,7 @@ async function main() {
|
|
|
359
364
|
|
|
360
365
|
case "bundle": {
|
|
361
366
|
const folder = positional[0] || ".";
|
|
362
|
-
const out = (flags.out as string) || "
|
|
367
|
+
const out = (flags.out as string) || "bundles";
|
|
363
368
|
const name = (flags.name as string) || "bundle.html";
|
|
364
369
|
const raw = flags.raw !== false; // --no-raw disables raw markdown
|
|
365
370
|
const { bundleSite } = await import("../scripts/bundle.ts");
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
3
|
+
import { basename, dirname, join } from "node:path";
|
|
4
|
+
import { ENGINE_ROOT } from "./engine.ts";
|
|
5
|
+
import { exists, parseYaml, validatePath } from "./shared.ts";
|
|
6
|
+
|
|
7
|
+
export class PluginNetworkError extends Error {}
|
|
8
|
+
export class PluginIntegrityError extends Error {}
|
|
9
|
+
export class PluginPolicyError extends Error {}
|
|
10
|
+
export class PluginConfigError extends Error {}
|
|
11
|
+
|
|
12
|
+
export type PluginInjections = {
|
|
13
|
+
head: string;
|
|
14
|
+
bodyEnd: string;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export type KitflyMode = "docs" | "slides";
|
|
18
|
+
|
|
19
|
+
type RegistryAssetChecksums = {
|
|
20
|
+
js?: string;
|
|
21
|
+
css?: string;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
type RegistryAssets = {
|
|
25
|
+
js?: string;
|
|
26
|
+
css?: string;
|
|
27
|
+
assetSha256: RegistryAssetChecksums;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
type RegistryPlugin = {
|
|
31
|
+
name: string;
|
|
32
|
+
description: string;
|
|
33
|
+
version: string;
|
|
34
|
+
contract: string;
|
|
35
|
+
kitfly: string;
|
|
36
|
+
license: string;
|
|
37
|
+
verified: boolean;
|
|
38
|
+
modes?: KitflyMode[];
|
|
39
|
+
assets: RegistryAssets;
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
type PluginRegistry = {
|
|
43
|
+
version: number;
|
|
44
|
+
updated: string;
|
|
45
|
+
baseUrl: string;
|
|
46
|
+
plugins: Record<string, RegistryPlugin>;
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
type CanonicalPluginRef = {
|
|
50
|
+
id: string;
|
|
51
|
+
version: string;
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
const PLUGIN_ID_RE = /^[a-z][a-z0-9-]*$/;
|
|
55
|
+
const SEMVER_RE =
|
|
56
|
+
/^(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)(?:-[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$/;
|
|
57
|
+
|
|
58
|
+
function parseCanonicalRef(spec: string): CanonicalPluginRef | null {
|
|
59
|
+
const at = spec.lastIndexOf("@");
|
|
60
|
+
if (at <= 0) return null;
|
|
61
|
+
const id = spec.slice(0, at).trim();
|
|
62
|
+
const version = spec.slice(at + 1).trim();
|
|
63
|
+
if (!id || !version) return null;
|
|
64
|
+
if (!PLUGIN_ID_RE.test(id) || id.length > 40) return null;
|
|
65
|
+
if (!SEMVER_RE.test(version)) return null;
|
|
66
|
+
return { id, version };
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function sha256Hex(data: Uint8Array): string {
|
|
70
|
+
return createHash("sha256").update(data).digest("hex");
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function requiredSha256Hex(expected: string, context: string): string {
|
|
74
|
+
if (!expected.startsWith("sha256:")) {
|
|
75
|
+
throw new PluginConfigError(`${context}: expected sha256:<hex>, got ${expected}`);
|
|
76
|
+
}
|
|
77
|
+
const hex = expected.slice("sha256:".length);
|
|
78
|
+
if (!/^[0-9a-f]{64}$/i.test(hex)) {
|
|
79
|
+
throw new PluginConfigError(`${context}: invalid sha256 hex`);
|
|
80
|
+
}
|
|
81
|
+
return hex.toLowerCase();
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function templateBaseUrl(url: string, baseUrl: string): string {
|
|
85
|
+
const token = "${" + "baseUrl}";
|
|
86
|
+
return url.replaceAll(token, baseUrl);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
async function fetchTextOrThrow(url: string): Promise<string> {
|
|
90
|
+
if (!/^https?:\/\//.test(url)) {
|
|
91
|
+
throw new PluginConfigError(`Unsupported URL scheme: ${url}`);
|
|
92
|
+
}
|
|
93
|
+
let res: Response;
|
|
94
|
+
try {
|
|
95
|
+
res = await fetch(url);
|
|
96
|
+
} catch (e) {
|
|
97
|
+
throw new PluginNetworkError(`Failed to fetch ${url}: ${String(e)}`);
|
|
98
|
+
}
|
|
99
|
+
if (!res.ok)
|
|
100
|
+
throw new PluginNetworkError(`Failed to fetch ${url}: ${res.status} ${res.statusText}`);
|
|
101
|
+
return res.text();
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
async function readLocalTextOrThrow(root: string, relOrAbs: string): Promise<string> {
|
|
105
|
+
const rel = relOrAbs.replace(/^\//, "");
|
|
106
|
+
const fsPath = validatePath(root, ".", rel, true);
|
|
107
|
+
if (!fsPath) throw new PluginConfigError(`Invalid local plugin asset path: ${relOrAbs}`);
|
|
108
|
+
return readFile(fsPath, "utf-8");
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
async function fetchWithSha256Cache(
|
|
112
|
+
cacheFile: string,
|
|
113
|
+
url: string,
|
|
114
|
+
expectedSha256Hex: string,
|
|
115
|
+
assetRoot: string,
|
|
116
|
+
): Promise<string> {
|
|
117
|
+
if (!/^https?:\/\//.test(url)) {
|
|
118
|
+
const text = await readLocalTextOrThrow(assetRoot, url);
|
|
119
|
+
const actual = sha256Hex(new TextEncoder().encode(text));
|
|
120
|
+
if (actual !== expectedSha256Hex) {
|
|
121
|
+
throw new PluginIntegrityError(
|
|
122
|
+
`CHECKSUM MISMATCH for ${url}\n expected: ${expectedSha256Hex}\n got: ${actual}`,
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
return text;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if (await exists(cacheFile)) {
|
|
129
|
+
const cached = await readFile(cacheFile);
|
|
130
|
+
if (sha256Hex(cached) === expectedSha256Hex) return cached.toString("utf-8");
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const text = await fetchTextOrThrow(url);
|
|
134
|
+
const bytes = new TextEncoder().encode(text);
|
|
135
|
+
const actual = sha256Hex(bytes);
|
|
136
|
+
if (actual !== expectedSha256Hex) {
|
|
137
|
+
throw new PluginIntegrityError(
|
|
138
|
+
`CHECKSUM MISMATCH for ${url}\n expected: ${expectedSha256Hex}\n got: ${actual}`,
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
await mkdir(dirname(cacheFile), { recursive: true });
|
|
143
|
+
await writeFile(cacheFile, text, "utf-8");
|
|
144
|
+
return text;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export async function loadPluginRegistry(registryPath: string): Promise<PluginRegistry> {
|
|
148
|
+
const raw = await readFile(registryPath, "utf-8");
|
|
149
|
+
const parsed = parseYaml(raw) as unknown as PluginRegistry;
|
|
150
|
+
if (!parsed || typeof parsed !== "object") throw new PluginConfigError("Invalid plugin registry");
|
|
151
|
+
if (typeof parsed.baseUrl !== "string")
|
|
152
|
+
throw new PluginConfigError("Invalid plugin registry shape");
|
|
153
|
+
if (!parsed.plugins || typeof parsed.plugins !== "object") {
|
|
154
|
+
throw new PluginConfigError("Invalid plugin registry shape");
|
|
155
|
+
}
|
|
156
|
+
return parsed;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export async function loadPluginInjections(opts: {
|
|
160
|
+
root: string;
|
|
161
|
+
mode?: KitflyMode;
|
|
162
|
+
registryPath?: string;
|
|
163
|
+
configPath?: string;
|
|
164
|
+
cacheDir?: string;
|
|
165
|
+
allowUntrusted?: boolean;
|
|
166
|
+
}): Promise<PluginInjections> {
|
|
167
|
+
const configPath = opts.configPath ?? join(opts.root, "kitfly.plugins.yaml");
|
|
168
|
+
const cacheDir = opts.cacheDir ?? join(opts.root, "node_modules", ".kitfly-plugins");
|
|
169
|
+
const allowUntrusted = opts.allowUntrusted ?? false;
|
|
170
|
+
const mode: KitflyMode = opts.mode ?? "docs";
|
|
171
|
+
|
|
172
|
+
if (!(await exists(configPath))) return { head: "", bodyEnd: "" };
|
|
173
|
+
|
|
174
|
+
const engineRegistryPath = join(ENGINE_ROOT, "registry", "plugins.yaml");
|
|
175
|
+
const siteRegistryPath = join(opts.root, "registry", "plugins.yaml");
|
|
176
|
+
const hasSiteRegistry = await exists(siteRegistryPath);
|
|
177
|
+
const registryPath =
|
|
178
|
+
opts.registryPath ?? (hasSiteRegistry ? siteRegistryPath : engineRegistryPath);
|
|
179
|
+
const assetRoot = hasSiteRegistry ? opts.root : ENGINE_ROOT;
|
|
180
|
+
|
|
181
|
+
if (!(await exists(registryPath)))
|
|
182
|
+
throw new PluginConfigError(`Missing registry: ${registryPath}`);
|
|
183
|
+
|
|
184
|
+
const registry = await loadPluginRegistry(registryPath);
|
|
185
|
+
const config = parseYaml(await readFile(configPath, "utf-8")) as Record<string, unknown>;
|
|
186
|
+
const plugins = Array.isArray(config.plugins) ? config.plugins : [];
|
|
187
|
+
|
|
188
|
+
let head = "";
|
|
189
|
+
let bodyEnd = "";
|
|
190
|
+
|
|
191
|
+
for (const entry of plugins) {
|
|
192
|
+
if (typeof entry !== "string") {
|
|
193
|
+
if (allowUntrusted) {
|
|
194
|
+
throw new PluginPolicyError("Third-party plugin objects are not supported yet");
|
|
195
|
+
}
|
|
196
|
+
throw new PluginPolicyError("Third-party plugins require --allow-untrusted");
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const ref = parseCanonicalRef(entry);
|
|
200
|
+
if (!ref) throw new PluginConfigError(`Invalid plugin ref: ${entry}`);
|
|
201
|
+
const reg = registry.plugins?.[ref.id];
|
|
202
|
+
if (!reg) throw new PluginConfigError(`Plugin not in registry: ${ref.id}`);
|
|
203
|
+
if (reg.version !== ref.version) {
|
|
204
|
+
throw new PluginConfigError(
|
|
205
|
+
`Plugin ${ref.id} version mismatch: ${ref.version} != ${reg.version}`,
|
|
206
|
+
);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
if (Array.isArray(reg.modes)) {
|
|
210
|
+
if (reg.modes.length === 0) continue;
|
|
211
|
+
if (!reg.modes.includes(mode)) continue;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const assets = reg.assets;
|
|
215
|
+
const pluginCacheDir = join(cacheDir, `${ref.id}@${ref.version}`);
|
|
216
|
+
|
|
217
|
+
if (assets.css) {
|
|
218
|
+
const url = templateBaseUrl(assets.css, registry.baseUrl);
|
|
219
|
+
const expected = requiredSha256Hex(
|
|
220
|
+
assets.assetSha256.css ?? "",
|
|
221
|
+
`${ref.id}: assetSha256.css`,
|
|
222
|
+
);
|
|
223
|
+
const css = await fetchWithSha256Cache(
|
|
224
|
+
join(pluginCacheDir, basename(url)),
|
|
225
|
+
url,
|
|
226
|
+
expected,
|
|
227
|
+
assetRoot,
|
|
228
|
+
);
|
|
229
|
+
head += `\n<style data-kitfly-plugin="${ref.id}@${ref.version}">\n${css}\n</style>\n`;
|
|
230
|
+
}
|
|
231
|
+
if (assets.js) {
|
|
232
|
+
const url = templateBaseUrl(assets.js, registry.baseUrl);
|
|
233
|
+
const expected = requiredSha256Hex(assets.assetSha256.js ?? "", `${ref.id}: assetSha256.js`);
|
|
234
|
+
const js = await fetchWithSha256Cache(
|
|
235
|
+
join(pluginCacheDir, basename(url)),
|
|
236
|
+
url,
|
|
237
|
+
expected,
|
|
238
|
+
assetRoot,
|
|
239
|
+
);
|
|
240
|
+
bodyEnd += `\n<script data-kitfly-plugin="${ref.id}@${ref.version}">\n${js}\n</script>\n`;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
return { head, bodyEnd };
|
|
245
|
+
}
|