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
package/scripts/dev.ts
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
* Options:
|
|
7
7
|
* -p, --port <number> Port to serve on [env: KITFLY_DEV_PORT] [default: 3333]
|
|
8
8
|
* -H, --host <string> Host to bind to [env: KITFLY_DEV_HOST] [default: localhost]
|
|
9
|
+
* --profile <name> Active content profile [env: KITFLY_PROFILE]
|
|
9
10
|
* -o, --open Open browser on start [env: KITFLY_DEV_OPEN] [default: true]
|
|
10
11
|
* --no-open Don't open browser
|
|
11
12
|
* --help Show help message
|
|
@@ -18,14 +19,23 @@ import { readdir, readFile, stat } from "node:fs/promises";
|
|
|
18
19
|
import { basename, extname, join, resolve } from "node:path";
|
|
19
20
|
import { marked, Renderer } from "marked";
|
|
20
21
|
import { ENGINE_ASSETS_DIR, ENGINE_SITE_DIR } from "../src/engine.ts";
|
|
21
|
-
import {
|
|
22
|
+
import {
|
|
23
|
+
loadPluginInjections,
|
|
24
|
+
PluginConfigError,
|
|
25
|
+
PluginIntegrityError,
|
|
26
|
+
PluginNetworkError,
|
|
27
|
+
PluginPolicyError,
|
|
28
|
+
} from "../src/plugin-loader.ts";
|
|
22
29
|
import {
|
|
23
30
|
buildBreadcrumbsSimple,
|
|
24
31
|
buildFooter,
|
|
32
|
+
buildLogoImgHtml,
|
|
25
33
|
buildNavSimple,
|
|
26
34
|
buildPageMeta,
|
|
27
|
-
|
|
35
|
+
buildSlideNavHierarchical,
|
|
28
36
|
buildToc,
|
|
37
|
+
// Types
|
|
38
|
+
type ContentFile,
|
|
29
39
|
// Network utilities
|
|
30
40
|
checkPortOrExit,
|
|
31
41
|
// Navigation/template building
|
|
@@ -37,21 +47,26 @@ import {
|
|
|
37
47
|
envString,
|
|
38
48
|
// Formatting
|
|
39
49
|
escapeHtml,
|
|
50
|
+
filterByProfile,
|
|
51
|
+
filterUnknownSlidesVisualsTypeDiagnostics,
|
|
40
52
|
// Provenance
|
|
41
53
|
generateProvenance,
|
|
54
|
+
loadDataBindings,
|
|
42
55
|
// YAML/Config parsing
|
|
43
56
|
loadSiteConfig,
|
|
57
|
+
mergeFrontmatterWithBody,
|
|
44
58
|
type Provenance,
|
|
59
|
+
pagePathForData,
|
|
45
60
|
// Markdown utilities
|
|
46
61
|
parseFrontmatter,
|
|
47
62
|
parseYaml,
|
|
63
|
+
resolveBindings,
|
|
48
64
|
resolveStylesPath,
|
|
49
65
|
resolveTemplatePath,
|
|
50
66
|
rewriteRelativeAssetUrls,
|
|
51
|
-
|
|
67
|
+
runPrebuildHooks,
|
|
52
68
|
type SiteConfig,
|
|
53
69
|
slugify,
|
|
54
|
-
toUrlPath,
|
|
55
70
|
validatePath,
|
|
56
71
|
validateSlidesVisualsFences,
|
|
57
72
|
} from "../src/shared.ts";
|
|
@@ -66,6 +81,7 @@ let HOST = DEFAULT_HOST;
|
|
|
66
81
|
let ROOT = process.cwd();
|
|
67
82
|
let OPEN_BROWSER = true;
|
|
68
83
|
let LOG_FORMAT = ""; // "structured" when invoked by CLI daemon
|
|
84
|
+
let ACTIVE_PROFILE: string | undefined;
|
|
69
85
|
|
|
70
86
|
// Structured logger for daemon mode — set during main() init.
|
|
71
87
|
// When null, all output goes through console.log (standalone mode).
|
|
@@ -75,6 +91,86 @@ let daemonLog: {
|
|
|
75
91
|
error: (msg: string) => void;
|
|
76
92
|
} | null = null;
|
|
77
93
|
|
|
94
|
+
async function applyDataBindingsToMarkdown(
|
|
95
|
+
rawMarkdown: string,
|
|
96
|
+
filePath: string,
|
|
97
|
+
config: SiteConfig,
|
|
98
|
+
): Promise<{ frontmatter: Record<string, unknown>; body: string }> {
|
|
99
|
+
const parsed = parseFrontmatter(rawMarkdown);
|
|
100
|
+
const dataRef = typeof parsed.frontmatter.data === "string" ? parsed.frontmatter.data.trim() : "";
|
|
101
|
+
if (!dataRef) return parsed;
|
|
102
|
+
|
|
103
|
+
const pagePath = pagePathForData(ROOT, config.docroot, filePath);
|
|
104
|
+
const bindings = await loadDataBindings(dataRef, pagePath, ROOT, config.docroot, config.dataroot);
|
|
105
|
+
return {
|
|
106
|
+
frontmatter: parsed.frontmatter,
|
|
107
|
+
body: resolveBindings(parsed.body, bindings, pagePath),
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
async function applyDataBindingsForSlides(
|
|
112
|
+
rawMarkdown: string,
|
|
113
|
+
filePath: string,
|
|
114
|
+
config: SiteConfig,
|
|
115
|
+
): Promise<string> {
|
|
116
|
+
const resolved = await applyDataBindingsToMarkdown(rawMarkdown, filePath, config);
|
|
117
|
+
return mergeFrontmatterWithBody(rawMarkdown, resolved.body);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function isPluginLoaderError(error: unknown): error is Error {
|
|
121
|
+
return (
|
|
122
|
+
error instanceof PluginConfigError ||
|
|
123
|
+
error instanceof PluginIntegrityError ||
|
|
124
|
+
error instanceof PluginPolicyError ||
|
|
125
|
+
error instanceof PluginNetworkError
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function pluginVersionMismatchHint(message: string): string {
|
|
130
|
+
const m = message.match(/^Plugin ([a-z0-9-]+) version mismatch: ([^ ]+) != ([^ ]+)$/i);
|
|
131
|
+
if (!m) return "";
|
|
132
|
+
const pluginId = m[1];
|
|
133
|
+
const expected = m[3];
|
|
134
|
+
return `Update <code>kitfly.plugins.yaml</code> to <code>${pluginId}@${expected}</code>, then refresh.`;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export function buildDevPluginErrorHtml(message: string): string {
|
|
138
|
+
const hint = pluginVersionMismatchHint(message);
|
|
139
|
+
const safeMessage = escapeHtml(message);
|
|
140
|
+
const hintBlock = hint
|
|
141
|
+
? `<p>${hint}</p>`
|
|
142
|
+
: "<p>Check <code>kitfly.plugins.yaml</code> and <code>registry/plugins.yaml</code>, then refresh.</p>";
|
|
143
|
+
return `<!doctype html>
|
|
144
|
+
<html lang="en">
|
|
145
|
+
<head>
|
|
146
|
+
<meta charset="utf-8">
|
|
147
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
148
|
+
<title>Plugin Configuration Error</title>
|
|
149
|
+
<style>
|
|
150
|
+
body { font-family: ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, sans-serif; margin: 0; background: #0b1020; color: #e8ecf3; }
|
|
151
|
+
main { max-width: 820px; margin: 8vh auto; padding: 1.25rem; }
|
|
152
|
+
.card { background: #131a2e; border: 1px solid #2a3557; border-radius: 12px; padding: 1rem 1.1rem; }
|
|
153
|
+
h1 { margin: 0 0 0.75rem; font-size: 1.25rem; }
|
|
154
|
+
p, li { line-height: 1.5; }
|
|
155
|
+
code { background: #0e1528; padding: 0.08rem 0.3rem; border-radius: 6px; border: 1px solid #2a3557; }
|
|
156
|
+
pre { margin: 0.8rem 0 0; padding: 0.75rem; background: #0e1528; border: 1px solid #2a3557; border-radius: 8px; overflow: auto; }
|
|
157
|
+
.muted { color: #b5bfd2; font-size: 0.92rem; }
|
|
158
|
+
</style>
|
|
159
|
+
</head>
|
|
160
|
+
<body>
|
|
161
|
+
<main>
|
|
162
|
+
<div class="card">
|
|
163
|
+
<h1>Plugin setup error</h1>
|
|
164
|
+
<p>Kitfly could not load one or more plugins for dev preview.</p>
|
|
165
|
+
${hintBlock}
|
|
166
|
+
<pre><code>${safeMessage}</code></pre>
|
|
167
|
+
<p class="muted">After updating config, refresh this page. No dev server restart required.</p>
|
|
168
|
+
</div>
|
|
169
|
+
</main>
|
|
170
|
+
</body>
|
|
171
|
+
</html>`;
|
|
172
|
+
}
|
|
173
|
+
|
|
78
174
|
/** Log info — uses structured logger in daemon mode, console.log otherwise */
|
|
79
175
|
function logInfo(msg: string): void {
|
|
80
176
|
if (daemonLog) daemonLog.info(msg);
|
|
@@ -97,6 +193,7 @@ interface ParsedArgs {
|
|
|
97
193
|
open?: boolean;
|
|
98
194
|
folder?: string;
|
|
99
195
|
logFormat?: string;
|
|
196
|
+
profile?: string;
|
|
100
197
|
}
|
|
101
198
|
|
|
102
199
|
function parseArgs(argv: string[]): ParsedArgs {
|
|
@@ -114,6 +211,9 @@ function parseArgs(argv: string[]): ParsedArgs {
|
|
|
114
211
|
} else if (arg === "--log-format") {
|
|
115
212
|
result.logFormat = next;
|
|
116
213
|
i++;
|
|
214
|
+
} else if (arg === "--profile" && next && !next.startsWith("-")) {
|
|
215
|
+
result.profile = next;
|
|
216
|
+
i++;
|
|
117
217
|
} else if (arg === "--open" || arg === "-o") {
|
|
118
218
|
result.open = true;
|
|
119
219
|
} else if (arg === "--no-open") {
|
|
@@ -131,6 +231,7 @@ function getConfig(): {
|
|
|
131
231
|
open: boolean;
|
|
132
232
|
folder?: string;
|
|
133
233
|
logFormat?: string;
|
|
234
|
+
profile?: string;
|
|
134
235
|
} {
|
|
135
236
|
const args = parseArgs(process.argv.slice(2));
|
|
136
237
|
return {
|
|
@@ -139,9 +240,15 @@ function getConfig(): {
|
|
|
139
240
|
open: args.open ?? envBool("KITFLY_DEV_OPEN", true),
|
|
140
241
|
folder: args.folder,
|
|
141
242
|
logFormat: args.logFormat,
|
|
243
|
+
profile: args.profile ?? process.env.KITFLY_PROFILE,
|
|
142
244
|
};
|
|
143
245
|
}
|
|
144
246
|
|
|
247
|
+
async function getFilteredFiles(config: SiteConfig): Promise<ContentFile[]> {
|
|
248
|
+
const files = await collectFiles(ROOT, config);
|
|
249
|
+
return filterByProfile(files, ACTIVE_PROFILE, config.profiles);
|
|
250
|
+
}
|
|
251
|
+
|
|
145
252
|
function getContentType(filePath: string): string {
|
|
146
253
|
const ext = extname(filePath).toLowerCase();
|
|
147
254
|
switch (ext) {
|
|
@@ -288,7 +395,7 @@ async function renderPage(
|
|
|
288
395
|
}
|
|
289
396
|
htmlContent = `<h1>${title}</h1>\n<pre><code class="language-json">${escapeHtml(prettyJson)}</code></pre>`;
|
|
290
397
|
} else {
|
|
291
|
-
const { frontmatter, body } =
|
|
398
|
+
const { frontmatter, body } = await applyDataBindingsToMarkdown(content, filePath, config);
|
|
292
399
|
if (frontmatter.title) {
|
|
293
400
|
title = frontmatter.title as string;
|
|
294
401
|
}
|
|
@@ -296,16 +403,16 @@ async function renderPage(
|
|
|
296
403
|
htmlContent = marked.parse(body) as string;
|
|
297
404
|
}
|
|
298
405
|
|
|
299
|
-
const files = await
|
|
406
|
+
const files = await getFilteredFiles(config);
|
|
300
407
|
const currentUrlPath = urlPath.slice(1).replace(/\.html$/, "");
|
|
408
|
+
const pathPrefix = "/";
|
|
301
409
|
const nav = buildNavSimple(files, config, currentUrlPath);
|
|
302
|
-
const footer = buildFooter(provenance, config);
|
|
410
|
+
const footer = buildFooter(provenance, config, pathPrefix);
|
|
303
411
|
const breadcrumbs = buildBreadcrumbsSimple(urlPath, files, config);
|
|
304
412
|
const toc = buildToc(htmlContent);
|
|
305
413
|
const brandTarget = config.brand.external ? ' target="_blank" rel="noopener"' : "";
|
|
306
414
|
const themeCSS = generateThemeCSS(theme);
|
|
307
415
|
const prismUrls = getPrismUrls(theme);
|
|
308
|
-
const pathPrefix = "/";
|
|
309
416
|
const plugins = await getPluginInjectionsCached(config.mode === "slides" ? "slides" : "docs");
|
|
310
417
|
|
|
311
418
|
const hotReloadScript = `
|
|
@@ -317,33 +424,51 @@ async function renderPage(
|
|
|
317
424
|
|
|
318
425
|
const logoClass = config.brand.logoType === "wordmark" ? "logo-wordmark" : "logo-icon";
|
|
319
426
|
const brandInitial = escapeHtml(config.brand.name.trim().charAt(0).toUpperCase() || "K");
|
|
427
|
+
const mobileLogoHtml = buildLogoImgHtml({
|
|
428
|
+
logo: config.brand.logo || "assets/brand/logo.png",
|
|
429
|
+
logoDark: config.brand.logoDark,
|
|
430
|
+
alt: config.brand.name,
|
|
431
|
+
className: `logo-img ${logoClass}`,
|
|
432
|
+
pathPrefix,
|
|
433
|
+
onerrorFallback: true,
|
|
434
|
+
});
|
|
435
|
+
const sidebarLogoHtml = buildLogoImgHtml({
|
|
436
|
+
logo: config.brand.logo || "assets/brand/logo.png",
|
|
437
|
+
logoDark: config.brand.logoDark,
|
|
438
|
+
alt: config.brand.name,
|
|
439
|
+
className: "logo-img",
|
|
440
|
+
pathPrefix,
|
|
441
|
+
onerrorFallback: true,
|
|
442
|
+
});
|
|
320
443
|
|
|
321
444
|
return template
|
|
322
445
|
.replace("{{BODY_CLASS}}", "mode-docs")
|
|
323
|
-
.replace(/\{\{PATH_PREFIX\}\}/g, pathPrefix)
|
|
324
|
-
.replace(/\{\{BRAND_URL\}\}/g, config.brand.url)
|
|
325
|
-
.replace(/\{\{BRAND_TARGET\}\}/g, brandTarget)
|
|
326
|
-
.replace(/\{\{BRAND_NAME\}\}/g, config.brand.name)
|
|
327
|
-
.replace(/\{\{BRAND_INITIAL\}\}/g, brandInitial)
|
|
328
|
-
.replace(
|
|
329
|
-
.replace(
|
|
330
|
-
.replace(/\{\{
|
|
331
|
-
.replace(/\{\{
|
|
332
|
-
.replace(
|
|
333
|
-
.replace(
|
|
334
|
-
.replace("{{
|
|
335
|
-
.replace("{{
|
|
336
|
-
.replace("{{
|
|
337
|
-
.replace("{{
|
|
338
|
-
.replace("{{
|
|
339
|
-
.replace("{{
|
|
340
|
-
.replace("{{
|
|
341
|
-
.replace("{{
|
|
342
|
-
.replace("{{
|
|
343
|
-
.replace("{{
|
|
344
|
-
.replace("{{
|
|
345
|
-
.replace("{{
|
|
346
|
-
.replace("{{
|
|
446
|
+
.replace(/\{\{PATH_PREFIX\}\}/g, () => pathPrefix)
|
|
447
|
+
.replace(/\{\{BRAND_URL\}\}/g, () => config.brand.url)
|
|
448
|
+
.replace(/\{\{BRAND_TARGET\}\}/g, () => brandTarget)
|
|
449
|
+
.replace(/\{\{BRAND_NAME\}\}/g, () => config.brand.name)
|
|
450
|
+
.replace(/\{\{BRAND_INITIAL\}\}/g, () => brandInitial)
|
|
451
|
+
.replace("{{MOBILE_BRAND_LOGO_IMG}}", () => mobileLogoHtml)
|
|
452
|
+
.replace("{{SIDEBAR_BRAND_LOGO_IMG}}", () => sidebarLogoHtml)
|
|
453
|
+
.replace(/\{\{BRAND_LOGO\}\}/g, () => config.brand.logo || "assets/brand/logo.png")
|
|
454
|
+
.replace(/\{\{BRAND_FAVICON\}\}/g, () => config.brand.favicon || "assets/brand/favicon.png")
|
|
455
|
+
.replace(/\{\{BRAND_LOGO_CLASS\}\}/g, () => logoClass)
|
|
456
|
+
.replace(/\{\{SITE_TITLE\}\}/g, () => config.title)
|
|
457
|
+
.replace("{{TITLE}}", () => title)
|
|
458
|
+
.replace("{{VERSION}}", () => uiVersion)
|
|
459
|
+
.replace("{{BRANCH}}", () => provenance.gitBranch)
|
|
460
|
+
.replace("{{BREADCRUMBS}}", () => breadcrumbs)
|
|
461
|
+
.replace("{{PAGE_META}}", () => pageMeta)
|
|
462
|
+
.replace("{{NAV}}", () => nav)
|
|
463
|
+
.replace("{{CONTENT}}", () => htmlContent)
|
|
464
|
+
.replace("{{TOC}}", () => toc)
|
|
465
|
+
.replace("{{FOOTER}}", () => footer)
|
|
466
|
+
.replace("{{THEME_CSS}}", () => themeCSS)
|
|
467
|
+
.replace("{{PLUGIN_HEAD}}", () => plugins.head)
|
|
468
|
+
.replace("{{PLUGIN_BODY_END}}", () => plugins.bodyEnd)
|
|
469
|
+
.replace("{{PRISM_LIGHT_URL}}", () => prismUrls.light)
|
|
470
|
+
.replace("{{PRISM_DARK_URL}}", () => prismUrls.dark)
|
|
471
|
+
.replace("{{HOT_RELOAD_SCRIPT}}", () => hotReloadScript);
|
|
347
472
|
}
|
|
348
473
|
|
|
349
474
|
async function renderSlidesPage(
|
|
@@ -353,8 +478,10 @@ async function renderSlidesPage(
|
|
|
353
478
|
): Promise<string> {
|
|
354
479
|
const uiVersion = provenance.version ? `v${provenance.version}` : "unversioned";
|
|
355
480
|
const template = await readFile(await resolveTemplatePath(ROOT), "utf-8");
|
|
356
|
-
const files = await
|
|
357
|
-
const slides = await collectSlides(files
|
|
481
|
+
const files = await getFilteredFiles(config);
|
|
482
|
+
const slides = await collectSlides(files, {
|
|
483
|
+
markdownTransform: (raw, file) => applyDataBindingsForSlides(raw, file.path, config),
|
|
484
|
+
});
|
|
358
485
|
|
|
359
486
|
if (slides.length === 0) {
|
|
360
487
|
return renderGettingStarted(provenance, config, theme);
|
|
@@ -367,7 +494,9 @@ async function renderSlidesPage(
|
|
|
367
494
|
let inner = "";
|
|
368
495
|
if (slide.kind === "markdown") {
|
|
369
496
|
if (validateFences) {
|
|
370
|
-
const diagnostics =
|
|
497
|
+
const diagnostics = filterUnknownSlidesVisualsTypeDiagnostics(
|
|
498
|
+
validateSlidesVisualsFences(slide.body),
|
|
499
|
+
);
|
|
371
500
|
if (diagnostics.length) {
|
|
372
501
|
const msg = diagnostics
|
|
373
502
|
.slice(0, 12)
|
|
@@ -413,8 +542,8 @@ async function renderSlidesPage(
|
|
|
413
542
|
</div>
|
|
414
543
|
</div>`;
|
|
415
544
|
|
|
416
|
-
const nav =
|
|
417
|
-
const footer = buildFooter(provenance, config);
|
|
545
|
+
const nav = buildSlideNavHierarchical(slides, config, "slide-1");
|
|
546
|
+
const footer = buildFooter(provenance, config, pathPrefix);
|
|
418
547
|
const brandTarget = config.brand.external ? ' target="_blank" rel="noopener"' : "";
|
|
419
548
|
const themeCSS = generateThemeCSS(theme);
|
|
420
549
|
const prismUrls = getPrismUrls(theme);
|
|
@@ -427,33 +556,51 @@ async function renderSlidesPage(
|
|
|
427
556
|
const plugins = await getPluginInjectionsCached("slides");
|
|
428
557
|
const logoClass = config.brand.logoType === "wordmark" ? "logo-wordmark" : "logo-icon";
|
|
429
558
|
const brandInitial = escapeHtml(config.brand.name.trim().charAt(0).toUpperCase() || "K");
|
|
559
|
+
const mobileLogoHtml = buildLogoImgHtml({
|
|
560
|
+
logo: config.brand.logo || "assets/brand/logo.png",
|
|
561
|
+
logoDark: config.brand.logoDark,
|
|
562
|
+
alt: config.brand.name,
|
|
563
|
+
className: `logo-img ${logoClass}`,
|
|
564
|
+
pathPrefix,
|
|
565
|
+
onerrorFallback: true,
|
|
566
|
+
});
|
|
567
|
+
const sidebarLogoHtml = buildLogoImgHtml({
|
|
568
|
+
logo: config.brand.logo || "assets/brand/logo.png",
|
|
569
|
+
logoDark: config.brand.logoDark,
|
|
570
|
+
alt: config.brand.name,
|
|
571
|
+
className: "logo-img",
|
|
572
|
+
pathPrefix,
|
|
573
|
+
onerrorFallback: true,
|
|
574
|
+
});
|
|
430
575
|
|
|
431
576
|
return template
|
|
432
577
|
.replace("{{BODY_CLASS}}", "mode-slides")
|
|
433
|
-
.replace(/\{\{PATH_PREFIX\}\}/g, pathPrefix)
|
|
434
|
-
.replace(/\{\{BRAND_URL\}\}/g, config.brand.url)
|
|
435
|
-
.replace(/\{\{BRAND_TARGET\}\}/g, brandTarget)
|
|
436
|
-
.replace(/\{\{BRAND_NAME\}\}/g, config.brand.name)
|
|
437
|
-
.replace(/\{\{BRAND_INITIAL\}\}/g, brandInitial)
|
|
438
|
-
.replace(
|
|
439
|
-
.replace(
|
|
440
|
-
.replace(/\{\{
|
|
441
|
-
.replace(/\{\{
|
|
442
|
-
.replace(
|
|
443
|
-
.replace(
|
|
444
|
-
.replace("{{
|
|
578
|
+
.replace(/\{\{PATH_PREFIX\}\}/g, () => pathPrefix)
|
|
579
|
+
.replace(/\{\{BRAND_URL\}\}/g, () => config.brand.url)
|
|
580
|
+
.replace(/\{\{BRAND_TARGET\}\}/g, () => brandTarget)
|
|
581
|
+
.replace(/\{\{BRAND_NAME\}\}/g, () => config.brand.name)
|
|
582
|
+
.replace(/\{\{BRAND_INITIAL\}\}/g, () => brandInitial)
|
|
583
|
+
.replace("{{MOBILE_BRAND_LOGO_IMG}}", () => mobileLogoHtml)
|
|
584
|
+
.replace("{{SIDEBAR_BRAND_LOGO_IMG}}", () => sidebarLogoHtml)
|
|
585
|
+
.replace(/\{\{BRAND_LOGO\}\}/g, () => config.brand.logo || "assets/brand/logo.png")
|
|
586
|
+
.replace(/\{\{BRAND_FAVICON\}\}/g, () => config.brand.favicon || "assets/brand/favicon.png")
|
|
587
|
+
.replace(/\{\{BRAND_LOGO_CLASS\}\}/g, () => logoClass)
|
|
588
|
+
.replace(/\{\{SITE_TITLE\}\}/g, () => config.title)
|
|
589
|
+
.replace("{{TITLE}}", () => config.title)
|
|
590
|
+
.replace("{{VERSION}}", () => uiVersion)
|
|
591
|
+
.replace("{{BRANCH}}", () => provenance.gitBranch)
|
|
445
592
|
.replace("{{BREADCRUMBS}}", "")
|
|
446
593
|
.replace("{{PAGE_META}}", "")
|
|
447
|
-
.replace("{{NAV}}", nav)
|
|
448
|
-
.replace("{{CONTENT}}", htmlContent)
|
|
594
|
+
.replace("{{NAV}}", () => nav)
|
|
595
|
+
.replace("{{CONTENT}}", () => htmlContent)
|
|
449
596
|
.replace("{{TOC}}", "")
|
|
450
|
-
.replace("{{FOOTER}}", footer)
|
|
451
|
-
.replace("{{THEME_CSS}}", themeCSS)
|
|
452
|
-
.replace("{{PLUGIN_HEAD}}", plugins.head)
|
|
453
|
-
.replace("{{PLUGIN_BODY_END}}", plugins.bodyEnd)
|
|
454
|
-
.replace("{{PRISM_LIGHT_URL}}", prismUrls.light)
|
|
455
|
-
.replace("{{PRISM_DARK_URL}}", prismUrls.dark)
|
|
456
|
-
.replace("{{HOT_RELOAD_SCRIPT}}", hotReloadScript);
|
|
597
|
+
.replace("{{FOOTER}}", () => footer)
|
|
598
|
+
.replace("{{THEME_CSS}}", () => themeCSS)
|
|
599
|
+
.replace("{{PLUGIN_HEAD}}", () => plugins.head)
|
|
600
|
+
.replace("{{PLUGIN_BODY_END}}", () => plugins.bodyEnd)
|
|
601
|
+
.replace("{{PRISM_LIGHT_URL}}", () => prismUrls.light)
|
|
602
|
+
.replace("{{PRISM_DARK_URL}}", () => prismUrls.dark)
|
|
603
|
+
.replace("{{HOT_RELOAD_SCRIPT}}", () => hotReloadScript);
|
|
457
604
|
}
|
|
458
605
|
|
|
459
606
|
// Render Getting Started page when no config
|
|
@@ -502,33 +649,51 @@ sections:
|
|
|
502
649
|
|
|
503
650
|
const logoClass = config.brand.logoType === "wordmark" ? "logo-wordmark" : "logo-icon";
|
|
504
651
|
const brandInitial = escapeHtml(config.brand.name.trim().charAt(0).toUpperCase() || "K");
|
|
652
|
+
const mobileLogoHtml = buildLogoImgHtml({
|
|
653
|
+
logo: config.brand.logo || "assets/brand/logo.png",
|
|
654
|
+
logoDark: config.brand.logoDark,
|
|
655
|
+
alt: config.brand.name,
|
|
656
|
+
className: `logo-img ${logoClass}`,
|
|
657
|
+
pathPrefix,
|
|
658
|
+
onerrorFallback: true,
|
|
659
|
+
});
|
|
660
|
+
const sidebarLogoHtml = buildLogoImgHtml({
|
|
661
|
+
logo: config.brand.logo || "assets/brand/logo.png",
|
|
662
|
+
logoDark: config.brand.logoDark,
|
|
663
|
+
alt: config.brand.name,
|
|
664
|
+
className: "logo-img",
|
|
665
|
+
pathPrefix,
|
|
666
|
+
onerrorFallback: true,
|
|
667
|
+
});
|
|
505
668
|
|
|
506
669
|
return template
|
|
507
670
|
.replace("{{BODY_CLASS}}", "mode-docs")
|
|
508
|
-
.replace(/\{\{PATH_PREFIX\}\}/g, pathPrefix)
|
|
509
|
-
.replace(/\{\{BRAND_URL\}\}/g, config.brand.url)
|
|
510
|
-
.replace(/\{\{BRAND_TARGET\}\}/g, brandTarget)
|
|
511
|
-
.replace(/\{\{BRAND_NAME\}\}/g, config.brand.name)
|
|
512
|
-
.replace(/\{\{BRAND_INITIAL\}\}/g, brandInitial)
|
|
513
|
-
.replace(
|
|
514
|
-
.replace(
|
|
515
|
-
.replace(/\{\{
|
|
516
|
-
.replace(/\{\{
|
|
671
|
+
.replace(/\{\{PATH_PREFIX\}\}/g, () => pathPrefix)
|
|
672
|
+
.replace(/\{\{BRAND_URL\}\}/g, () => config.brand.url)
|
|
673
|
+
.replace(/\{\{BRAND_TARGET\}\}/g, () => brandTarget)
|
|
674
|
+
.replace(/\{\{BRAND_NAME\}\}/g, () => config.brand.name)
|
|
675
|
+
.replace(/\{\{BRAND_INITIAL\}\}/g, () => brandInitial)
|
|
676
|
+
.replace("{{MOBILE_BRAND_LOGO_IMG}}", () => mobileLogoHtml)
|
|
677
|
+
.replace("{{SIDEBAR_BRAND_LOGO_IMG}}", () => sidebarLogoHtml)
|
|
678
|
+
.replace(/\{\{BRAND_LOGO\}\}/g, () => config.brand.logo || "assets/brand/logo.png")
|
|
679
|
+
.replace(/\{\{BRAND_FAVICON\}\}/g, () => config.brand.favicon || "assets/brand/favicon.png")
|
|
680
|
+
.replace(/\{\{BRAND_LOGO_CLASS\}\}/g, () => logoClass)
|
|
681
|
+
.replace(/\{\{SITE_TITLE\}\}/g, () => config.title)
|
|
517
682
|
.replace("{{TITLE}}", "Getting Started")
|
|
518
|
-
.replace("{{VERSION}}", uiVersion)
|
|
519
|
-
.replace("{{BRANCH}}", provenance.gitBranch)
|
|
683
|
+
.replace("{{VERSION}}", () => uiVersion)
|
|
684
|
+
.replace("{{BRANCH}}", () => provenance.gitBranch)
|
|
520
685
|
.replace("{{BREADCRUMBS}}", "")
|
|
521
686
|
.replace("{{PAGE_META}}", "")
|
|
522
687
|
.replace("{{NAV}}", "<ul></ul>")
|
|
523
|
-
.replace("{{CONTENT}}", htmlContent)
|
|
688
|
+
.replace("{{CONTENT}}", () => htmlContent)
|
|
524
689
|
.replace("{{TOC}}", "")
|
|
525
|
-
.replace("{{FOOTER}}", buildFooter(provenance, config))
|
|
526
|
-
.replace("{{THEME_CSS}}", themeCSS)
|
|
527
|
-
.replace("{{PLUGIN_HEAD}}", plugins.head)
|
|
528
|
-
.replace("{{PLUGIN_BODY_END}}", plugins.bodyEnd)
|
|
529
|
-
.replace("{{PRISM_LIGHT_URL}}", prismUrls.light)
|
|
530
|
-
.replace("{{PRISM_DARK_URL}}", prismUrls.dark)
|
|
531
|
-
.replace("{{HOT_RELOAD_SCRIPT}}", hotReloadScript);
|
|
690
|
+
.replace("{{FOOTER}}", () => buildFooter(provenance, config, pathPrefix))
|
|
691
|
+
.replace("{{THEME_CSS}}", () => themeCSS)
|
|
692
|
+
.replace("{{PLUGIN_HEAD}}", () => plugins.head)
|
|
693
|
+
.replace("{{PLUGIN_BODY_END}}", () => plugins.bodyEnd)
|
|
694
|
+
.replace("{{PRISM_LIGHT_URL}}", () => prismUrls.light)
|
|
695
|
+
.replace("{{PRISM_DARK_URL}}", () => prismUrls.dark)
|
|
696
|
+
.replace("{{HOT_RELOAD_SCRIPT}}", () => hotReloadScript);
|
|
532
697
|
}
|
|
533
698
|
|
|
534
699
|
async function tryServeFile(filePath: string): Promise<Response | null> {
|
|
@@ -563,9 +728,11 @@ async function tryServeContentAsset(
|
|
|
563
728
|
}
|
|
564
729
|
|
|
565
730
|
// Find file for a URL path
|
|
566
|
-
async function findFile(
|
|
567
|
-
|
|
568
|
-
|
|
731
|
+
async function findFile(
|
|
732
|
+
urlPath: string,
|
|
733
|
+
config: SiteConfig,
|
|
734
|
+
files: ContentFile[],
|
|
735
|
+
): Promise<string | null> {
|
|
569
736
|
// Remove leading slash and .html extension (for compatibility with built links)
|
|
570
737
|
const path = urlPath.slice(1).replace(/\.html$/, "") || "";
|
|
571
738
|
|
|
@@ -573,84 +740,20 @@ async function findFile(urlPath: string, config: SiteConfig): Promise<string | n
|
|
|
573
740
|
if (!path) {
|
|
574
741
|
if (config.home) {
|
|
575
742
|
const homePath = validatePath(ROOT, config.docroot, config.home, true);
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
await stat(homePath);
|
|
579
|
-
return homePath;
|
|
580
|
-
} catch {
|
|
581
|
-
// Home file not found, fall through
|
|
582
|
-
}
|
|
583
|
-
}
|
|
743
|
+
const homeFile = homePath ? files.find((file) => file.path === homePath) : undefined;
|
|
744
|
+
if (homeFile) return homeFile.path;
|
|
584
745
|
}
|
|
585
746
|
// Fallback to first file
|
|
586
|
-
const files = await collectFiles(ROOT, config);
|
|
587
747
|
return files.length > 0 ? files[0].path : null;
|
|
588
748
|
}
|
|
589
749
|
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
for (const file of section.files) {
|
|
598
|
-
const name = file.replace(/\.(md|yaml|json)$/, "").toLowerCase();
|
|
599
|
-
if (name === path) {
|
|
600
|
-
const filePath = join(sectionPath, file);
|
|
601
|
-
try {
|
|
602
|
-
await stat(filePath);
|
|
603
|
-
return filePath;
|
|
604
|
-
} catch {
|
|
605
|
-
// Continue
|
|
606
|
-
}
|
|
607
|
-
}
|
|
608
|
-
}
|
|
609
|
-
} else {
|
|
610
|
-
// Check directory for matching file (supports nested paths)
|
|
611
|
-
const urlBase = toUrlPath(ROOT, sectionPath);
|
|
612
|
-
if (path.startsWith(`${urlBase}/`) || path === urlBase) {
|
|
613
|
-
const relPath = path === urlBase ? "" : path.slice(urlBase.length + 1);
|
|
614
|
-
// Guard against path traversal
|
|
615
|
-
if (relPath.includes("..")) continue;
|
|
616
|
-
const extensions = [".md", ".yaml", ".json"];
|
|
617
|
-
|
|
618
|
-
if (relPath === "") {
|
|
619
|
-
// Section root URL — try index file
|
|
620
|
-
for (const ext of extensions) {
|
|
621
|
-
const filePath = join(sectionPath, `index${ext}`);
|
|
622
|
-
try {
|
|
623
|
-
await stat(filePath);
|
|
624
|
-
return filePath;
|
|
625
|
-
} catch {
|
|
626
|
-
// Continue
|
|
627
|
-
}
|
|
628
|
-
}
|
|
629
|
-
} else {
|
|
630
|
-
// Try direct file match at nested path
|
|
631
|
-
for (const ext of extensions) {
|
|
632
|
-
const filePath = join(sectionPath, relPath + ext);
|
|
633
|
-
try {
|
|
634
|
-
await stat(filePath);
|
|
635
|
-
return filePath;
|
|
636
|
-
} catch {
|
|
637
|
-
// Continue
|
|
638
|
-
}
|
|
639
|
-
}
|
|
640
|
-
// Try as directory with index file
|
|
641
|
-
for (const ext of extensions) {
|
|
642
|
-
const filePath = join(sectionPath, relPath, `index${ext}`);
|
|
643
|
-
try {
|
|
644
|
-
await stat(filePath);
|
|
645
|
-
return filePath;
|
|
646
|
-
} catch {
|
|
647
|
-
// Continue
|
|
648
|
-
}
|
|
649
|
-
}
|
|
650
|
-
}
|
|
651
|
-
}
|
|
652
|
-
}
|
|
653
|
-
}
|
|
750
|
+
const directMatch = files.find((file) => file.urlPath === path);
|
|
751
|
+
if (directMatch) return directMatch.path;
|
|
752
|
+
|
|
753
|
+
const sectionIndex = files.find(
|
|
754
|
+
(file) => file.urlPath === `${path}/index` || file.urlPath === path,
|
|
755
|
+
);
|
|
756
|
+
if (sectionIndex) return sectionIndex.path;
|
|
654
757
|
|
|
655
758
|
return null;
|
|
656
759
|
}
|
|
@@ -687,17 +790,35 @@ function startWatcher(config: SiteConfig) {
|
|
|
687
790
|
for (const dir of watchDirs) {
|
|
688
791
|
try {
|
|
689
792
|
watch(dir, { recursive: true }, (_event, filename) => {
|
|
690
|
-
if (
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
793
|
+
if (!filename) return;
|
|
794
|
+
void (async () => {
|
|
795
|
+
try {
|
|
796
|
+
let hooksRan = 0;
|
|
797
|
+
if (config.prebuild?.length) {
|
|
798
|
+
hooksRan = await runPrebuildHooks(
|
|
799
|
+
config.prebuild,
|
|
800
|
+
ROOT,
|
|
801
|
+
"dev",
|
|
802
|
+
ACTIVE_PROFILE,
|
|
803
|
+
config.dataroot || "data",
|
|
804
|
+
filename,
|
|
805
|
+
);
|
|
806
|
+
}
|
|
807
|
+
const shouldReload =
|
|
808
|
+
hooksRan > 0 ||
|
|
809
|
+
filename.endsWith(".md") ||
|
|
810
|
+
filename.endsWith(".yaml") ||
|
|
811
|
+
filename.endsWith(".json") ||
|
|
812
|
+
filename.endsWith(".html") ||
|
|
813
|
+
filename.endsWith(".css");
|
|
814
|
+
if (shouldReload) {
|
|
815
|
+
logInfo(`File changed: ${filename}`);
|
|
816
|
+
notifyReload();
|
|
817
|
+
}
|
|
818
|
+
} catch (error) {
|
|
819
|
+
logWarn(error instanceof Error ? error.message : String(error));
|
|
820
|
+
}
|
|
821
|
+
})();
|
|
701
822
|
});
|
|
702
823
|
} catch {
|
|
703
824
|
// Directory doesn't exist, skip
|
|
@@ -721,6 +842,10 @@ async function main() {
|
|
|
721
842
|
// Load configuration
|
|
722
843
|
const config = await loadSiteConfig(ROOT);
|
|
723
844
|
logInfo(`Loaded config: "${config.title}" (${config.sections.length} sections)`);
|
|
845
|
+
if (config.prebuild?.length) {
|
|
846
|
+
await runPrebuildHooks(config.prebuild, ROOT, "dev", ACTIVE_PROFILE, config.dataroot || "data");
|
|
847
|
+
logInfo(`Ran prebuild hooks (${config.prebuild.length})`);
|
|
848
|
+
}
|
|
724
849
|
|
|
725
850
|
// Apply server config from site.yaml if CLI didn't override
|
|
726
851
|
if (config.server?.port && PORT === DEFAULT_PORT) {
|
|
@@ -795,7 +920,7 @@ async function main() {
|
|
|
795
920
|
if (assetResponse) return assetResponse;
|
|
796
921
|
|
|
797
922
|
// Check for content
|
|
798
|
-
const files = await
|
|
923
|
+
const files = await getFilteredFiles(config);
|
|
799
924
|
if (files.length === 0) {
|
|
800
925
|
// No content - render Getting Started page
|
|
801
926
|
const html = await renderGettingStarted(provenance, config, theme);
|
|
@@ -813,7 +938,7 @@ async function main() {
|
|
|
813
938
|
}
|
|
814
939
|
|
|
815
940
|
// Find and render markdown/yaml file
|
|
816
|
-
const filePath = await findFile(url.pathname, config);
|
|
941
|
+
const filePath = await findFile(url.pathname, config, files);
|
|
817
942
|
if (filePath) {
|
|
818
943
|
// If this is an index/readme file and the URL lacks a trailing slash,
|
|
819
944
|
// redirect so relative links resolve correctly (BUG-003)
|
|
@@ -854,19 +979,44 @@ async function main() {
|
|
|
854
979
|
return new Response("Not found", { status: 404 });
|
|
855
980
|
}
|
|
856
981
|
|
|
857
|
-
// Wrap with request logging
|
|
858
|
-
const fetch =
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
982
|
+
// Wrap with request logging + friendly plugin errors.
|
|
983
|
+
const fetch = async (req: Request) => {
|
|
984
|
+
const start = performance.now();
|
|
985
|
+
const url = new URL(req.url);
|
|
986
|
+
try {
|
|
987
|
+
const response = await handleRequest(req);
|
|
988
|
+
if (daemonLog && url.pathname !== "/__reload") {
|
|
862
989
|
const duration = (performance.now() - start).toFixed(0);
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
990
|
+
daemonLog.info(`${req.method} ${url.pathname} ${response.status} ${duration}ms`);
|
|
991
|
+
}
|
|
992
|
+
return response;
|
|
993
|
+
} catch (error) {
|
|
994
|
+
const duration = (performance.now() - start).toFixed(0);
|
|
995
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
996
|
+
if (isPluginLoaderError(error)) {
|
|
997
|
+
if (daemonLog && url.pathname !== "/__reload") {
|
|
998
|
+
daemonLog.warn(
|
|
999
|
+
`${req.method} ${url.pathname} 500 ${duration}ms plugin error: ${message}`,
|
|
1000
|
+
);
|
|
1001
|
+
} else if (!daemonLog) {
|
|
1002
|
+
logWarn(`Plugin error: ${message}`);
|
|
866
1003
|
}
|
|
867
|
-
return
|
|
1004
|
+
return new Response(buildDevPluginErrorHtml(message), {
|
|
1005
|
+
status: 500,
|
|
1006
|
+
headers: { "Content-Type": "text/html; charset=utf-8" },
|
|
1007
|
+
});
|
|
1008
|
+
}
|
|
1009
|
+
if (daemonLog && url.pathname !== "/__reload") {
|
|
1010
|
+
daemonLog.error(`${req.method} ${url.pathname} 500 ${duration}ms ${message}`);
|
|
1011
|
+
} else if (!daemonLog) {
|
|
1012
|
+
console.error(error);
|
|
868
1013
|
}
|
|
869
|
-
|
|
1014
|
+
return new Response("Internal server error", {
|
|
1015
|
+
status: 500,
|
|
1016
|
+
headers: { "Content-Type": "text/plain; charset=utf-8" },
|
|
1017
|
+
});
|
|
1018
|
+
}
|
|
1019
|
+
};
|
|
870
1020
|
|
|
871
1021
|
// Create server
|
|
872
1022
|
Bun.serve({
|
|
@@ -910,9 +1060,22 @@ async function main() {
|
|
|
910
1060
|
}
|
|
911
1061
|
}
|
|
912
1062
|
|
|
913
|
-
// Open browser (
|
|
1063
|
+
// Open browser (cross-platform)
|
|
914
1064
|
if (OPEN_BROWSER) {
|
|
915
|
-
|
|
1065
|
+
try {
|
|
1066
|
+
if (process.platform === "win32") {
|
|
1067
|
+
// cmd.exe built-in: start
|
|
1068
|
+
// Empty title argument avoids treating URL as window title.
|
|
1069
|
+
Bun.spawn(["cmd", "/c", "start", "", serverUrl]);
|
|
1070
|
+
} else if (process.platform === "darwin") {
|
|
1071
|
+
Bun.spawn(["open", serverUrl]);
|
|
1072
|
+
} else {
|
|
1073
|
+
// Most Linux distros
|
|
1074
|
+
Bun.spawn(["xdg-open", serverUrl]);
|
|
1075
|
+
}
|
|
1076
|
+
} catch {
|
|
1077
|
+
// Non-fatal: server is already running; user can open manually.
|
|
1078
|
+
}
|
|
916
1079
|
}
|
|
917
1080
|
}
|
|
918
1081
|
|
|
@@ -923,6 +1086,7 @@ export interface DevOptions {
|
|
|
923
1086
|
host?: string;
|
|
924
1087
|
open?: boolean;
|
|
925
1088
|
logFormat?: string;
|
|
1089
|
+
profile?: string;
|
|
926
1090
|
}
|
|
927
1091
|
|
|
928
1092
|
export async function dev(options: DevOptions = {}) {
|
|
@@ -941,6 +1105,7 @@ export async function dev(options: DevOptions = {}) {
|
|
|
941
1105
|
if (options.logFormat) {
|
|
942
1106
|
LOG_FORMAT = options.logFormat;
|
|
943
1107
|
}
|
|
1108
|
+
ACTIVE_PROFILE = options.profile;
|
|
944
1109
|
await main();
|
|
945
1110
|
}
|
|
946
1111
|
|
|
@@ -954,6 +1119,7 @@ Usage: bun run dev [folder] [options]
|
|
|
954
1119
|
Options:
|
|
955
1120
|
-p, --port <number> Port to serve on [env: KITFLY_DEV_PORT] [default: ${DEFAULT_PORT}]
|
|
956
1121
|
-H, --host <string> Host to bind to [env: KITFLY_DEV_HOST] [default: ${DEFAULT_HOST}]
|
|
1122
|
+
--profile <name> Active content profile [env: KITFLY_PROFILE]
|
|
957
1123
|
-o, --open Open browser on start [env: KITFLY_DEV_OPEN] [default: true]
|
|
958
1124
|
--no-open Don't open browser
|
|
959
1125
|
--help Show this help message
|
|
@@ -975,5 +1141,6 @@ Examples:
|
|
|
975
1141
|
host: cfg.host,
|
|
976
1142
|
open: cfg.open,
|
|
977
1143
|
logFormat: cfg.logFormat,
|
|
1144
|
+
profile: cfg.profile,
|
|
978
1145
|
}).catch(console.error);
|
|
979
1146
|
}
|