kitfly 0.2.1 → 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 +56 -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/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 +10 -6
- package/dist/content/reference/structure.html +10 -6
- package/dist/content/reference.html +10 -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 +29 -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/registry/plugins.yaml +25 -0
- package/schemas/v0/site.schema.json +56 -0
- package/scripts/build.ts +191 -69
- package/scripts/bundle.ts +118 -10
- package/scripts/dev.ts +245 -166
- package/src/__tests__/brief.test.ts +151 -0
- package/src/__tests__/build.test.ts +169 -1
- package/src/__tests__/bundle.test.ts +134 -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 +598 -1
- package/src/__tests__/slides-charts-lite-runtime.bun.test.ts +45 -0
- package/src/cli.ts +11 -4
- package/src/commands/init.ts +1 -1
- package/src/shared.ts +725 -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
|
|
@@ -28,10 +29,13 @@ import {
|
|
|
28
29
|
import {
|
|
29
30
|
buildBreadcrumbsSimple,
|
|
30
31
|
buildFooter,
|
|
32
|
+
buildLogoImgHtml,
|
|
31
33
|
buildNavSimple,
|
|
32
34
|
buildPageMeta,
|
|
33
|
-
|
|
35
|
+
buildSlideNavHierarchical,
|
|
34
36
|
buildToc,
|
|
37
|
+
// Types
|
|
38
|
+
type ContentFile,
|
|
35
39
|
// Network utilities
|
|
36
40
|
checkPortOrExit,
|
|
37
41
|
// Navigation/template building
|
|
@@ -43,22 +47,26 @@ import {
|
|
|
43
47
|
envString,
|
|
44
48
|
// Formatting
|
|
45
49
|
escapeHtml,
|
|
50
|
+
filterByProfile,
|
|
46
51
|
filterUnknownSlidesVisualsTypeDiagnostics,
|
|
47
52
|
// Provenance
|
|
48
53
|
generateProvenance,
|
|
54
|
+
loadDataBindings,
|
|
49
55
|
// YAML/Config parsing
|
|
50
56
|
loadSiteConfig,
|
|
57
|
+
mergeFrontmatterWithBody,
|
|
51
58
|
type Provenance,
|
|
59
|
+
pagePathForData,
|
|
52
60
|
// Markdown utilities
|
|
53
61
|
parseFrontmatter,
|
|
54
62
|
parseYaml,
|
|
63
|
+
resolveBindings,
|
|
55
64
|
resolveStylesPath,
|
|
56
65
|
resolveTemplatePath,
|
|
57
66
|
rewriteRelativeAssetUrls,
|
|
58
|
-
|
|
67
|
+
runPrebuildHooks,
|
|
59
68
|
type SiteConfig,
|
|
60
69
|
slugify,
|
|
61
|
-
toUrlPath,
|
|
62
70
|
validatePath,
|
|
63
71
|
validateSlidesVisualsFences,
|
|
64
72
|
} from "../src/shared.ts";
|
|
@@ -73,6 +81,7 @@ let HOST = DEFAULT_HOST;
|
|
|
73
81
|
let ROOT = process.cwd();
|
|
74
82
|
let OPEN_BROWSER = true;
|
|
75
83
|
let LOG_FORMAT = ""; // "structured" when invoked by CLI daemon
|
|
84
|
+
let ACTIVE_PROFILE: string | undefined;
|
|
76
85
|
|
|
77
86
|
// Structured logger for daemon mode — set during main() init.
|
|
78
87
|
// When null, all output goes through console.log (standalone mode).
|
|
@@ -82,6 +91,32 @@ let daemonLog: {
|
|
|
82
91
|
error: (msg: string) => void;
|
|
83
92
|
} | null = null;
|
|
84
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
|
+
|
|
85
120
|
function isPluginLoaderError(error: unknown): error is Error {
|
|
86
121
|
return (
|
|
87
122
|
error instanceof PluginConfigError ||
|
|
@@ -158,6 +193,7 @@ interface ParsedArgs {
|
|
|
158
193
|
open?: boolean;
|
|
159
194
|
folder?: string;
|
|
160
195
|
logFormat?: string;
|
|
196
|
+
profile?: string;
|
|
161
197
|
}
|
|
162
198
|
|
|
163
199
|
function parseArgs(argv: string[]): ParsedArgs {
|
|
@@ -175,6 +211,9 @@ function parseArgs(argv: string[]): ParsedArgs {
|
|
|
175
211
|
} else if (arg === "--log-format") {
|
|
176
212
|
result.logFormat = next;
|
|
177
213
|
i++;
|
|
214
|
+
} else if (arg === "--profile" && next && !next.startsWith("-")) {
|
|
215
|
+
result.profile = next;
|
|
216
|
+
i++;
|
|
178
217
|
} else if (arg === "--open" || arg === "-o") {
|
|
179
218
|
result.open = true;
|
|
180
219
|
} else if (arg === "--no-open") {
|
|
@@ -192,6 +231,7 @@ function getConfig(): {
|
|
|
192
231
|
open: boolean;
|
|
193
232
|
folder?: string;
|
|
194
233
|
logFormat?: string;
|
|
234
|
+
profile?: string;
|
|
195
235
|
} {
|
|
196
236
|
const args = parseArgs(process.argv.slice(2));
|
|
197
237
|
return {
|
|
@@ -200,9 +240,15 @@ function getConfig(): {
|
|
|
200
240
|
open: args.open ?? envBool("KITFLY_DEV_OPEN", true),
|
|
201
241
|
folder: args.folder,
|
|
202
242
|
logFormat: args.logFormat,
|
|
243
|
+
profile: args.profile ?? process.env.KITFLY_PROFILE,
|
|
203
244
|
};
|
|
204
245
|
}
|
|
205
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
|
+
|
|
206
252
|
function getContentType(filePath: string): string {
|
|
207
253
|
const ext = extname(filePath).toLowerCase();
|
|
208
254
|
switch (ext) {
|
|
@@ -349,7 +395,7 @@ async function renderPage(
|
|
|
349
395
|
}
|
|
350
396
|
htmlContent = `<h1>${title}</h1>\n<pre><code class="language-json">${escapeHtml(prettyJson)}</code></pre>`;
|
|
351
397
|
} else {
|
|
352
|
-
const { frontmatter, body } =
|
|
398
|
+
const { frontmatter, body } = await applyDataBindingsToMarkdown(content, filePath, config);
|
|
353
399
|
if (frontmatter.title) {
|
|
354
400
|
title = frontmatter.title as string;
|
|
355
401
|
}
|
|
@@ -357,16 +403,16 @@ async function renderPage(
|
|
|
357
403
|
htmlContent = marked.parse(body) as string;
|
|
358
404
|
}
|
|
359
405
|
|
|
360
|
-
const files = await
|
|
406
|
+
const files = await getFilteredFiles(config);
|
|
361
407
|
const currentUrlPath = urlPath.slice(1).replace(/\.html$/, "");
|
|
408
|
+
const pathPrefix = "/";
|
|
362
409
|
const nav = buildNavSimple(files, config, currentUrlPath);
|
|
363
|
-
const footer = buildFooter(provenance, config);
|
|
410
|
+
const footer = buildFooter(provenance, config, pathPrefix);
|
|
364
411
|
const breadcrumbs = buildBreadcrumbsSimple(urlPath, files, config);
|
|
365
412
|
const toc = buildToc(htmlContent);
|
|
366
413
|
const brandTarget = config.brand.external ? ' target="_blank" rel="noopener"' : "";
|
|
367
414
|
const themeCSS = generateThemeCSS(theme);
|
|
368
415
|
const prismUrls = getPrismUrls(theme);
|
|
369
|
-
const pathPrefix = "/";
|
|
370
416
|
const plugins = await getPluginInjectionsCached(config.mode === "slides" ? "slides" : "docs");
|
|
371
417
|
|
|
372
418
|
const hotReloadScript = `
|
|
@@ -378,33 +424,51 @@ async function renderPage(
|
|
|
378
424
|
|
|
379
425
|
const logoClass = config.brand.logoType === "wordmark" ? "logo-wordmark" : "logo-icon";
|
|
380
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
|
+
});
|
|
381
443
|
|
|
382
444
|
return template
|
|
383
445
|
.replace("{{BODY_CLASS}}", "mode-docs")
|
|
384
|
-
.replace(/\{\{PATH_PREFIX\}\}/g, pathPrefix)
|
|
385
|
-
.replace(/\{\{BRAND_URL\}\}/g, config.brand.url)
|
|
386
|
-
.replace(/\{\{BRAND_TARGET\}\}/g, brandTarget)
|
|
387
|
-
.replace(/\{\{BRAND_NAME\}\}/g, config.brand.name)
|
|
388
|
-
.replace(/\{\{BRAND_INITIAL\}\}/g, brandInitial)
|
|
389
|
-
.replace(
|
|
390
|
-
.replace(
|
|
391
|
-
.replace(/\{\{
|
|
392
|
-
.replace(/\{\{
|
|
393
|
-
.replace(
|
|
394
|
-
.replace(
|
|
395
|
-
.replace("{{
|
|
396
|
-
.replace("{{
|
|
397
|
-
.replace("{{
|
|
398
|
-
.replace("{{
|
|
399
|
-
.replace("{{
|
|
400
|
-
.replace("{{
|
|
401
|
-
.replace("{{
|
|
402
|
-
.replace("{{
|
|
403
|
-
.replace("{{
|
|
404
|
-
.replace("{{
|
|
405
|
-
.replace("{{
|
|
406
|
-
.replace("{{
|
|
407
|
-
.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);
|
|
408
472
|
}
|
|
409
473
|
|
|
410
474
|
async function renderSlidesPage(
|
|
@@ -414,8 +478,10 @@ async function renderSlidesPage(
|
|
|
414
478
|
): Promise<string> {
|
|
415
479
|
const uiVersion = provenance.version ? `v${provenance.version}` : "unversioned";
|
|
416
480
|
const template = await readFile(await resolveTemplatePath(ROOT), "utf-8");
|
|
417
|
-
const files = await
|
|
418
|
-
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
|
+
});
|
|
419
485
|
|
|
420
486
|
if (slides.length === 0) {
|
|
421
487
|
return renderGettingStarted(provenance, config, theme);
|
|
@@ -476,8 +542,8 @@ async function renderSlidesPage(
|
|
|
476
542
|
</div>
|
|
477
543
|
</div>`;
|
|
478
544
|
|
|
479
|
-
const nav =
|
|
480
|
-
const footer = buildFooter(provenance, config);
|
|
545
|
+
const nav = buildSlideNavHierarchical(slides, config, "slide-1");
|
|
546
|
+
const footer = buildFooter(provenance, config, pathPrefix);
|
|
481
547
|
const brandTarget = config.brand.external ? ' target="_blank" rel="noopener"' : "";
|
|
482
548
|
const themeCSS = generateThemeCSS(theme);
|
|
483
549
|
const prismUrls = getPrismUrls(theme);
|
|
@@ -490,33 +556,51 @@ async function renderSlidesPage(
|
|
|
490
556
|
const plugins = await getPluginInjectionsCached("slides");
|
|
491
557
|
const logoClass = config.brand.logoType === "wordmark" ? "logo-wordmark" : "logo-icon";
|
|
492
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
|
+
});
|
|
493
575
|
|
|
494
576
|
return template
|
|
495
577
|
.replace("{{BODY_CLASS}}", "mode-slides")
|
|
496
|
-
.replace(/\{\{PATH_PREFIX\}\}/g, pathPrefix)
|
|
497
|
-
.replace(/\{\{BRAND_URL\}\}/g, config.brand.url)
|
|
498
|
-
.replace(/\{\{BRAND_TARGET\}\}/g, brandTarget)
|
|
499
|
-
.replace(/\{\{BRAND_NAME\}\}/g, config.brand.name)
|
|
500
|
-
.replace(/\{\{BRAND_INITIAL\}\}/g, brandInitial)
|
|
501
|
-
.replace(
|
|
502
|
-
.replace(
|
|
503
|
-
.replace(/\{\{
|
|
504
|
-
.replace(/\{\{
|
|
505
|
-
.replace(
|
|
506
|
-
.replace(
|
|
507
|
-
.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)
|
|
508
592
|
.replace("{{BREADCRUMBS}}", "")
|
|
509
593
|
.replace("{{PAGE_META}}", "")
|
|
510
|
-
.replace("{{NAV}}", nav)
|
|
511
|
-
.replace("{{CONTENT}}", htmlContent)
|
|
594
|
+
.replace("{{NAV}}", () => nav)
|
|
595
|
+
.replace("{{CONTENT}}", () => htmlContent)
|
|
512
596
|
.replace("{{TOC}}", "")
|
|
513
|
-
.replace("{{FOOTER}}", footer)
|
|
514
|
-
.replace("{{THEME_CSS}}", themeCSS)
|
|
515
|
-
.replace("{{PLUGIN_HEAD}}", plugins.head)
|
|
516
|
-
.replace("{{PLUGIN_BODY_END}}", plugins.bodyEnd)
|
|
517
|
-
.replace("{{PRISM_LIGHT_URL}}", prismUrls.light)
|
|
518
|
-
.replace("{{PRISM_DARK_URL}}", prismUrls.dark)
|
|
519
|
-
.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);
|
|
520
604
|
}
|
|
521
605
|
|
|
522
606
|
// Render Getting Started page when no config
|
|
@@ -565,33 +649,51 @@ sections:
|
|
|
565
649
|
|
|
566
650
|
const logoClass = config.brand.logoType === "wordmark" ? "logo-wordmark" : "logo-icon";
|
|
567
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
|
+
});
|
|
568
668
|
|
|
569
669
|
return template
|
|
570
670
|
.replace("{{BODY_CLASS}}", "mode-docs")
|
|
571
|
-
.replace(/\{\{PATH_PREFIX\}\}/g, pathPrefix)
|
|
572
|
-
.replace(/\{\{BRAND_URL\}\}/g, config.brand.url)
|
|
573
|
-
.replace(/\{\{BRAND_TARGET\}\}/g, brandTarget)
|
|
574
|
-
.replace(/\{\{BRAND_NAME\}\}/g, config.brand.name)
|
|
575
|
-
.replace(/\{\{BRAND_INITIAL\}\}/g, brandInitial)
|
|
576
|
-
.replace(
|
|
577
|
-
.replace(
|
|
578
|
-
.replace(/\{\{
|
|
579
|
-
.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)
|
|
580
682
|
.replace("{{TITLE}}", "Getting Started")
|
|
581
|
-
.replace("{{VERSION}}", uiVersion)
|
|
582
|
-
.replace("{{BRANCH}}", provenance.gitBranch)
|
|
683
|
+
.replace("{{VERSION}}", () => uiVersion)
|
|
684
|
+
.replace("{{BRANCH}}", () => provenance.gitBranch)
|
|
583
685
|
.replace("{{BREADCRUMBS}}", "")
|
|
584
686
|
.replace("{{PAGE_META}}", "")
|
|
585
687
|
.replace("{{NAV}}", "<ul></ul>")
|
|
586
|
-
.replace("{{CONTENT}}", htmlContent)
|
|
688
|
+
.replace("{{CONTENT}}", () => htmlContent)
|
|
587
689
|
.replace("{{TOC}}", "")
|
|
588
|
-
.replace("{{FOOTER}}", buildFooter(provenance, config))
|
|
589
|
-
.replace("{{THEME_CSS}}", themeCSS)
|
|
590
|
-
.replace("{{PLUGIN_HEAD}}", plugins.head)
|
|
591
|
-
.replace("{{PLUGIN_BODY_END}}", plugins.bodyEnd)
|
|
592
|
-
.replace("{{PRISM_LIGHT_URL}}", prismUrls.light)
|
|
593
|
-
.replace("{{PRISM_DARK_URL}}", prismUrls.dark)
|
|
594
|
-
.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);
|
|
595
697
|
}
|
|
596
698
|
|
|
597
699
|
async function tryServeFile(filePath: string): Promise<Response | null> {
|
|
@@ -626,9 +728,11 @@ async function tryServeContentAsset(
|
|
|
626
728
|
}
|
|
627
729
|
|
|
628
730
|
// Find file for a URL path
|
|
629
|
-
async function findFile(
|
|
630
|
-
|
|
631
|
-
|
|
731
|
+
async function findFile(
|
|
732
|
+
urlPath: string,
|
|
733
|
+
config: SiteConfig,
|
|
734
|
+
files: ContentFile[],
|
|
735
|
+
): Promise<string | null> {
|
|
632
736
|
// Remove leading slash and .html extension (for compatibility with built links)
|
|
633
737
|
const path = urlPath.slice(1).replace(/\.html$/, "") || "";
|
|
634
738
|
|
|
@@ -636,84 +740,20 @@ async function findFile(urlPath: string, config: SiteConfig): Promise<string | n
|
|
|
636
740
|
if (!path) {
|
|
637
741
|
if (config.home) {
|
|
638
742
|
const homePath = validatePath(ROOT, config.docroot, config.home, true);
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
await stat(homePath);
|
|
642
|
-
return homePath;
|
|
643
|
-
} catch {
|
|
644
|
-
// Home file not found, fall through
|
|
645
|
-
}
|
|
646
|
-
}
|
|
743
|
+
const homeFile = homePath ? files.find((file) => file.path === homePath) : undefined;
|
|
744
|
+
if (homeFile) return homeFile.path;
|
|
647
745
|
}
|
|
648
746
|
// Fallback to first file
|
|
649
|
-
const files = await collectFiles(ROOT, config);
|
|
650
747
|
return files.length > 0 ? files[0].path : null;
|
|
651
748
|
}
|
|
652
749
|
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
for (const file of section.files) {
|
|
661
|
-
const name = file.replace(/\.(md|yaml|json)$/, "").toLowerCase();
|
|
662
|
-
if (name === path) {
|
|
663
|
-
const filePath = join(sectionPath, file);
|
|
664
|
-
try {
|
|
665
|
-
await stat(filePath);
|
|
666
|
-
return filePath;
|
|
667
|
-
} catch {
|
|
668
|
-
// Continue
|
|
669
|
-
}
|
|
670
|
-
}
|
|
671
|
-
}
|
|
672
|
-
} else {
|
|
673
|
-
// Check directory for matching file (supports nested paths)
|
|
674
|
-
const urlBase = toUrlPath(ROOT, sectionPath);
|
|
675
|
-
if (path.startsWith(`${urlBase}/`) || path === urlBase) {
|
|
676
|
-
const relPath = path === urlBase ? "" : path.slice(urlBase.length + 1);
|
|
677
|
-
// Guard against path traversal
|
|
678
|
-
if (relPath.includes("..")) continue;
|
|
679
|
-
const extensions = [".md", ".yaml", ".json"];
|
|
680
|
-
|
|
681
|
-
if (relPath === "") {
|
|
682
|
-
// Section root URL — try index file
|
|
683
|
-
for (const ext of extensions) {
|
|
684
|
-
const filePath = join(sectionPath, `index${ext}`);
|
|
685
|
-
try {
|
|
686
|
-
await stat(filePath);
|
|
687
|
-
return filePath;
|
|
688
|
-
} catch {
|
|
689
|
-
// Continue
|
|
690
|
-
}
|
|
691
|
-
}
|
|
692
|
-
} else {
|
|
693
|
-
// Try direct file match at nested path
|
|
694
|
-
for (const ext of extensions) {
|
|
695
|
-
const filePath = join(sectionPath, relPath + ext);
|
|
696
|
-
try {
|
|
697
|
-
await stat(filePath);
|
|
698
|
-
return filePath;
|
|
699
|
-
} catch {
|
|
700
|
-
// Continue
|
|
701
|
-
}
|
|
702
|
-
}
|
|
703
|
-
// Try as directory with index file
|
|
704
|
-
for (const ext of extensions) {
|
|
705
|
-
const filePath = join(sectionPath, relPath, `index${ext}`);
|
|
706
|
-
try {
|
|
707
|
-
await stat(filePath);
|
|
708
|
-
return filePath;
|
|
709
|
-
} catch {
|
|
710
|
-
// Continue
|
|
711
|
-
}
|
|
712
|
-
}
|
|
713
|
-
}
|
|
714
|
-
}
|
|
715
|
-
}
|
|
716
|
-
}
|
|
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;
|
|
717
757
|
|
|
718
758
|
return null;
|
|
719
759
|
}
|
|
@@ -750,17 +790,35 @@ function startWatcher(config: SiteConfig) {
|
|
|
750
790
|
for (const dir of watchDirs) {
|
|
751
791
|
try {
|
|
752
792
|
watch(dir, { recursive: true }, (_event, filename) => {
|
|
753
|
-
if (
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
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
|
+
})();
|
|
764
822
|
});
|
|
765
823
|
} catch {
|
|
766
824
|
// Directory doesn't exist, skip
|
|
@@ -784,6 +842,10 @@ async function main() {
|
|
|
784
842
|
// Load configuration
|
|
785
843
|
const config = await loadSiteConfig(ROOT);
|
|
786
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
|
+
}
|
|
787
849
|
|
|
788
850
|
// Apply server config from site.yaml if CLI didn't override
|
|
789
851
|
if (config.server?.port && PORT === DEFAULT_PORT) {
|
|
@@ -858,7 +920,7 @@ async function main() {
|
|
|
858
920
|
if (assetResponse) return assetResponse;
|
|
859
921
|
|
|
860
922
|
// Check for content
|
|
861
|
-
const files = await
|
|
923
|
+
const files = await getFilteredFiles(config);
|
|
862
924
|
if (files.length === 0) {
|
|
863
925
|
// No content - render Getting Started page
|
|
864
926
|
const html = await renderGettingStarted(provenance, config, theme);
|
|
@@ -876,7 +938,7 @@ async function main() {
|
|
|
876
938
|
}
|
|
877
939
|
|
|
878
940
|
// Find and render markdown/yaml file
|
|
879
|
-
const filePath = await findFile(url.pathname, config);
|
|
941
|
+
const filePath = await findFile(url.pathname, config, files);
|
|
880
942
|
if (filePath) {
|
|
881
943
|
// If this is an index/readme file and the URL lacks a trailing slash,
|
|
882
944
|
// redirect so relative links resolve correctly (BUG-003)
|
|
@@ -998,9 +1060,22 @@ async function main() {
|
|
|
998
1060
|
}
|
|
999
1061
|
}
|
|
1000
1062
|
|
|
1001
|
-
// Open browser (
|
|
1063
|
+
// Open browser (cross-platform)
|
|
1002
1064
|
if (OPEN_BROWSER) {
|
|
1003
|
-
|
|
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
|
+
}
|
|
1004
1079
|
}
|
|
1005
1080
|
}
|
|
1006
1081
|
|
|
@@ -1011,6 +1086,7 @@ export interface DevOptions {
|
|
|
1011
1086
|
host?: string;
|
|
1012
1087
|
open?: boolean;
|
|
1013
1088
|
logFormat?: string;
|
|
1089
|
+
profile?: string;
|
|
1014
1090
|
}
|
|
1015
1091
|
|
|
1016
1092
|
export async function dev(options: DevOptions = {}) {
|
|
@@ -1029,6 +1105,7 @@ export async function dev(options: DevOptions = {}) {
|
|
|
1029
1105
|
if (options.logFormat) {
|
|
1030
1106
|
LOG_FORMAT = options.logFormat;
|
|
1031
1107
|
}
|
|
1108
|
+
ACTIVE_PROFILE = options.profile;
|
|
1032
1109
|
await main();
|
|
1033
1110
|
}
|
|
1034
1111
|
|
|
@@ -1042,6 +1119,7 @@ Usage: bun run dev [folder] [options]
|
|
|
1042
1119
|
Options:
|
|
1043
1120
|
-p, --port <number> Port to serve on [env: KITFLY_DEV_PORT] [default: ${DEFAULT_PORT}]
|
|
1044
1121
|
-H, --host <string> Host to bind to [env: KITFLY_DEV_HOST] [default: ${DEFAULT_HOST}]
|
|
1122
|
+
--profile <name> Active content profile [env: KITFLY_PROFILE]
|
|
1045
1123
|
-o, --open Open browser on start [env: KITFLY_DEV_OPEN] [default: true]
|
|
1046
1124
|
--no-open Don't open browser
|
|
1047
1125
|
--help Show this help message
|
|
@@ -1063,5 +1141,6 @@ Examples:
|
|
|
1063
1141
|
host: cfg.host,
|
|
1064
1142
|
open: cfg.open,
|
|
1065
1143
|
logFormat: cfg.logFormat,
|
|
1144
|
+
profile: cfg.profile,
|
|
1066
1145
|
}).catch(console.error);
|
|
1067
1146
|
}
|