@norskvideo/ctl-dev-kit 0.1.54 → 0.1.56
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/build/Dockerfile.bundle +5 -0
- package/build/build-bundle-image.sh +8 -1
- package/conventions/check-drift.ts +5 -1
- package/conventions/docs.md +16 -3
- package/conventions/sync-drift.ts +7 -1
- package/doc-guide/build-manual.d.ts +34 -2
- package/doc-guide/build-manual.js +44 -6
- package/doc-guide/bundle.d.ts +37 -0
- package/doc-guide/bundle.js +97 -0
- package/package.json +2 -1
package/build/Dockerfile.bundle
CHANGED
|
@@ -50,6 +50,11 @@ COPY dashboards /usr/src/dashboards
|
|
|
50
50
|
COPY components/lib /usr/src/components/lib
|
|
51
51
|
# Optional product-template assets (funke's slate/black PNGs). Empty otherwise.
|
|
52
52
|
COPY assets /usr/src/assets
|
|
53
|
+
# The docs bundle (docs/generated/bundle/ on the host), served by the product
|
|
54
|
+
# at /docs. Beside backend/ and frontend/ so server.ts reaches it the same way
|
|
55
|
+
# it reaches frontend/dist (../../docs from the bundle dir). Empty when the
|
|
56
|
+
# product built none.
|
|
57
|
+
COPY docs ./docs
|
|
53
58
|
ENV PORT=${PRODUCT_PORT}
|
|
54
59
|
EXPOSE ${PRODUCT_PORT}
|
|
55
60
|
CMD ["/usr/local/bin/bun", "run", "backend/dist/index.js"]
|
|
@@ -23,6 +23,7 @@
|
|
|
23
23
|
# components/lib the components workspace's build output
|
|
24
24
|
# native/ native addon(s) (probe's signer) -> beside bundle
|
|
25
25
|
# assets/ product-template assets (funke's PNGs)
|
|
26
|
+
# docs/generated/bundle/ the docs bundle (doc-guide buildBundle) -> docs/
|
|
26
27
|
# scripts/runtime-images.ts -> emits the norsk-ctl.runtime-images label
|
|
27
28
|
#
|
|
28
29
|
# Only backend/dist is required. A product with no frontend/dashboard/components
|
|
@@ -51,7 +52,7 @@ trap 'rm -rf "${stage}"' EXIT
|
|
|
51
52
|
|
|
52
53
|
echo "==> staging artifacts"
|
|
53
54
|
mkdir -p "${stage}/backend" "${stage}/frontend/dist" "${stage}/components/lib" \
|
|
54
|
-
"${stage}/dashboards" "${stage}/native" "${stage}/assets"
|
|
55
|
+
"${stage}/dashboards" "${stage}/native" "${stage}/assets" "${stage}/docs"
|
|
55
56
|
# The backend bundle is the one non-negotiable payload — every bundle product has
|
|
56
57
|
# one, so a missing dist is a broken build, not an absent workspace.
|
|
57
58
|
cp -R "${PRODUCT_DIR}/backend/dist" "${stage}/backend/dist"
|
|
@@ -87,6 +88,12 @@ if [ -d "${PRODUCT_DIR}/assets" ]; then
|
|
|
87
88
|
cp -R "${PRODUCT_DIR}/assets/." "${stage}/assets/"
|
|
88
89
|
fi
|
|
89
90
|
|
|
91
|
+
# The docs bundle (optional): ONLY the built bundle, never the hand-written
|
|
92
|
+
# docs/ tree beside it (planning notes, customer folders).
|
|
93
|
+
if [ -d "${PRODUCT_DIR}/docs/generated/bundle" ]; then
|
|
94
|
+
cp -R "${PRODUCT_DIR}/docs/generated/bundle/." "${stage}/docs/"
|
|
95
|
+
fi
|
|
96
|
+
|
|
90
97
|
# Runtime images the default templates launch, stamped on as a label so a
|
|
91
98
|
# golden-image bake can warm them with `docker inspect` + `docker pull` alone —
|
|
92
99
|
# no daemon, no license. Derived from the rendered compose (scripts/runtime-images.ts),
|
|
@@ -264,7 +264,11 @@ export function demoProblems(repoRoot: string, canonicalShim: string): string[]
|
|
|
264
264
|
const problems: string[] = [];
|
|
265
265
|
const hasSpec = existsSync(join(repoRoot, DEMO_SPEC));
|
|
266
266
|
const shimPath = join(repoRoot, DEMO_SHIM);
|
|
267
|
-
if (existsSync(shimPath)) {
|
|
267
|
+
if (existsSync(shimPath) && statSync(shimPath).isDirectory()) {
|
|
268
|
+
problems.push(
|
|
269
|
+
`${DEMO_SHIM} is a directory; the convention puts the executable shim at that path — move the directory (scripts/demo-tools, say) and re-run sync-drift to write the shim.`,
|
|
270
|
+
);
|
|
271
|
+
} else if (existsSync(shimPath)) {
|
|
268
272
|
const actual = readFileSync(shimPath, "utf8");
|
|
269
273
|
if (actual !== canonicalShim) {
|
|
270
274
|
problems.push(
|
package/conventions/docs.md
CHANGED
|
@@ -95,9 +95,22 @@ they do not copy it:
|
|
|
95
95
|
docsRoot, … })`, the Playwright config for the fixture/frontend tiers, carrying
|
|
96
96
|
the CI defaults (bind+probe `127.0.0.1`, 120s webServer timeout, piped output).
|
|
97
97
|
- `@norskvideo/ctl-dev-kit/doc-guide/build-manual` — `buildManual(spec, {
|
|
98
|
-
docsRoot })`, the generic assembler. The product supplies ONLY the
|
|
99
|
-
(`ManualSpec`: brand, overview, functionality[], examples[]
|
|
100
|
-
|
|
98
|
+
docsRoot, provenance })`, the generic assembler. The product supplies ONLY the
|
|
99
|
+
page content (`ManualSpec`: brand, overview, functionality[], examples[], and
|
|
100
|
+
`links` — the way out: the product's page on norsk.video, a contact); layout,
|
|
101
|
+
CSS and routing live in the assembler so every product's manual looks the
|
|
102
|
+
same. Every export ends in a provenance footer (build time, source sha, CI
|
|
103
|
+
run id — `provenanceFromEnv()` reads them from a GitHub Actions run).
|
|
104
|
+
- `@norskvideo/ctl-dev-kit/doc-guide/bundle` — `buildBundle(spec, { docsRoot,
|
|
105
|
+
outDir, provenance })`, the docs bundle: `bundle.json` + `pages/<id>.md` +
|
|
106
|
+
`captures/` + the derived `manual.html`/`manual-linked.html`, in one
|
|
107
|
+
directory. Write it to `docs/generated/bundle/`: the image build stages that
|
|
108
|
+
directory as the image's `docs/` and the backend serves it at `/docs` with
|
|
109
|
+
the SDK's `serveDocs(app, docsBundleDir(import.meta.dir))` — one line in
|
|
110
|
+
`server.ts`, a no-op when there is no bundle. Declare the nav entry in the
|
|
111
|
+
manifest: `ui.sidebarEntries: [{ label: "Documentation", route: "/docs/" }]`;
|
|
112
|
+
ctl links it under `/products/<name>/`. An older ctl simply does not link it
|
|
113
|
+
(additive: nothing about launch changes).
|
|
101
114
|
- `@norskvideo/ctl-dev-kit/doc-guide/{instance-proxy,live-browser}` — engine-tier
|
|
102
115
|
support (serve the baked dashboard under its advertised instance prefix; launch
|
|
103
116
|
the nix chromium for a live capture).
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
// cannot mechanically converge (e.g. a workflow with no product key to preserve)
|
|
19
19
|
// is left for check-drift to flag, and a convention that genuinely breaks the
|
|
20
20
|
// product surfaces as a red lint/typecheck/test — not as a silent bad copy.
|
|
21
|
-
import { chmodSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
21
|
+
import { chmodSync, existsSync, mkdirSync, readFileSync, statSync, writeFileSync } from "node:fs";
|
|
22
22
|
import { dirname, join } from "node:path";
|
|
23
23
|
import {
|
|
24
24
|
BEGIN,
|
|
@@ -203,6 +203,12 @@ function syncDemoShim(repoRoot: string, canonical: string, r: SyncReport): void
|
|
|
203
203
|
const rel = "scripts/demo";
|
|
204
204
|
const path = join(repoRoot, rel);
|
|
205
205
|
if (!existsSync(join(repoRoot, "tests/demo.spec.ts")) && !existsSync(path)) return;
|
|
206
|
+
// A product's own tooling may already live at scripts/demo/ (playout did):
|
|
207
|
+
// never write over it; the gate names it and says where the shim goes.
|
|
208
|
+
if (existsSync(path) && statSync(path).isDirectory()) {
|
|
209
|
+
r.skipped.push(`${rel} (a directory; move it, the shim goes here)`);
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
206
212
|
mkdirSync(dirname(path), { recursive: true });
|
|
207
213
|
writeIfChanged(path, canonical, rel, r.written);
|
|
208
214
|
chmodSync(path, 0o755);
|
|
@@ -34,6 +34,13 @@ export interface ManualOverview {
|
|
|
34
34
|
/** The small print under the right column heading. */
|
|
35
35
|
byExampleIntro: string;
|
|
36
36
|
}
|
|
37
|
+
/** A way out of the manual: the product's marketing page, the platform docs,
|
|
38
|
+
* a contact. Rendered in the footer of every export. Hash links are the only
|
|
39
|
+
* navigation otherwise, so a manual without these is a cul-de-sac. */
|
|
40
|
+
export interface ManualLink {
|
|
41
|
+
label: string;
|
|
42
|
+
href: string;
|
|
43
|
+
}
|
|
37
44
|
export interface ManualSpec {
|
|
38
45
|
/** Sidebar wordmark + document brand. */
|
|
39
46
|
brand: string;
|
|
@@ -42,7 +49,27 @@ export interface ManualSpec {
|
|
|
42
49
|
overview: ManualOverview;
|
|
43
50
|
functionality: ManualPage[];
|
|
44
51
|
examples: ManualPage[];
|
|
52
|
+
/** Outbound links for the footer. Optional, but every published manual should
|
|
53
|
+
* carry at least one. */
|
|
54
|
+
links?: ManualLink[];
|
|
55
|
+
}
|
|
56
|
+
/** Where a build came from, stamped into the footer so two copies of "the
|
|
57
|
+
* manual" can be told apart without hashing them. `builtAt` defaults to now;
|
|
58
|
+
* the rest is whatever the build knows (CI knows all of it, see
|
|
59
|
+
* {@link provenanceFromEnv}). */
|
|
60
|
+
export interface Provenance {
|
|
61
|
+
/** Source commit the captures and copy were built from. */
|
|
62
|
+
sha?: string;
|
|
63
|
+
/** The CI run that built it, when there was one. */
|
|
64
|
+
runId?: string;
|
|
65
|
+
/** ISO-8601. */
|
|
66
|
+
builtAt?: string;
|
|
67
|
+
/** The product version, when the build knows it. */
|
|
68
|
+
version?: string;
|
|
45
69
|
}
|
|
70
|
+
/** Provenance as a GitHub Actions run sees it (GITHUB_SHA / GITHUB_RUN_ID), or
|
|
71
|
+
* as much of it as a local build has. */
|
|
72
|
+
export declare function provenanceFromEnv(env?: NodeJS.ProcessEnv): Provenance;
|
|
46
73
|
export interface BuildManualResult {
|
|
47
74
|
/** Artifact-ready fragment (no doctype/head/body). */
|
|
48
75
|
indexHtml: string;
|
|
@@ -55,6 +82,9 @@ export interface BuildManualResult {
|
|
|
55
82
|
standaloneAssetHtml?: string;
|
|
56
83
|
/** `<slug>/<file>` of every referenced capture not found on disk. */
|
|
57
84
|
missing: string[];
|
|
85
|
+
/** `<slug>/<file>` -> the sidecar href it was written to. Only when `assets`
|
|
86
|
+
* is set; what a bundle's markdown pages reference captures by. */
|
|
87
|
+
sidecarHrefs?: Record<string, string>;
|
|
58
88
|
}
|
|
59
89
|
/** Where to write de-inlined captures for the standalone manual. `dir` is the
|
|
60
90
|
* filesystem directory; `href` (default "images") is the relative URL prefix
|
|
@@ -63,7 +93,9 @@ export interface ManualAssets {
|
|
|
63
93
|
dir: string;
|
|
64
94
|
href?: string;
|
|
65
95
|
}
|
|
66
|
-
export
|
|
96
|
+
export interface BuildManualOptions {
|
|
67
97
|
docsRoot: string;
|
|
68
98
|
assets?: ManualAssets;
|
|
69
|
-
|
|
99
|
+
provenance?: Provenance;
|
|
100
|
+
}
|
|
101
|
+
export declare function buildManual(spec: ManualSpec, opts: BuildManualOptions): BuildManualResult;
|
|
@@ -26,6 +26,16 @@
|
|
|
26
26
|
import { spawnSync } from "node:child_process";
|
|
27
27
|
import { existsSync, mkdirSync, writeFileSync } from "node:fs";
|
|
28
28
|
import { join } from "node:path";
|
|
29
|
+
/** Provenance as a GitHub Actions run sees it (GITHUB_SHA / GITHUB_RUN_ID), or
|
|
30
|
+
* as much of it as a local build has. */
|
|
31
|
+
export function provenanceFromEnv(env = process.env) {
|
|
32
|
+
const p = { builtAt: new Date().toISOString() };
|
|
33
|
+
if (env.GITHUB_SHA)
|
|
34
|
+
p.sha = env.GITHUB_SHA;
|
|
35
|
+
if (env.GITHUB_RUN_ID)
|
|
36
|
+
p.runId = env.GITHUB_RUN_ID;
|
|
37
|
+
return p;
|
|
38
|
+
}
|
|
29
39
|
const esc = (s) => s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
30
40
|
/** Resize a captured PNG to <=1100px wide JPEG. A missing capture becomes a
|
|
31
41
|
* labelled placeholder so the manual still builds, and is recorded in `missing`. */
|
|
@@ -63,8 +73,22 @@ function fileUri(a, file, assets) {
|
|
|
63
73
|
writeFileSync(join(assets.dir, name), a.kind === "svg" ? Buffer.from(a.text) : a.bytes);
|
|
64
74
|
return `${assets.href ?? "images"}/${name}`;
|
|
65
75
|
}
|
|
76
|
+
/** The footer every export carries: provenance first, then the way out. */
|
|
77
|
+
function footerHtml(spec, provenance) {
|
|
78
|
+
const facts = [];
|
|
79
|
+
if (provenance.version)
|
|
80
|
+
facts.push(`version ${esc(provenance.version)}`);
|
|
81
|
+
facts.push(`built ${esc(provenance.builtAt ?? new Date().toISOString())}`);
|
|
82
|
+
if (provenance.sha)
|
|
83
|
+
facts.push(`source <span class="mono">${esc(provenance.sha)}</span>`);
|
|
84
|
+
if (provenance.runId)
|
|
85
|
+
facts.push(`run <span class="mono">${esc(provenance.runId)}</span>`);
|
|
86
|
+
const links = (spec.links ?? []).map((l) => `<a href="${esc(l.href)}">${esc(l.label)}</a>`).join("");
|
|
87
|
+
return `<footer class="prov"><div class="facts">${esc(spec.brand)} · ${facts.join(" · ")}</div>${links ? `<div class="out">${links}</div>` : ""}</footer>`;
|
|
88
|
+
}
|
|
66
89
|
export function buildManual(spec, opts) {
|
|
67
90
|
const { docsRoot, assets } = opts;
|
|
91
|
+
const provenance = { builtAt: new Date().toISOString(), ...opts.provenance };
|
|
68
92
|
const missing = [];
|
|
69
93
|
// Each shot is loaded (resized) at most once, then rendered inline for the
|
|
70
94
|
// fragment and — when `assets` is set — as a sidecar file for the standalone.
|
|
@@ -79,7 +103,12 @@ export function buildManual(spec, opts) {
|
|
|
79
103
|
return a;
|
|
80
104
|
};
|
|
81
105
|
const inline = (slug, file) => inlineUri(load(slug, file));
|
|
82
|
-
const
|
|
106
|
+
const sidecarHrefs = {};
|
|
107
|
+
const external = (slug, file) => {
|
|
108
|
+
const href = fileUri(load(slug, file), file, assets);
|
|
109
|
+
sidecarHrefs[`${slug}/${file}`] = href;
|
|
110
|
+
return href;
|
|
111
|
+
};
|
|
83
112
|
const FN = spec.functionality;
|
|
84
113
|
const EX = spec.examples;
|
|
85
114
|
const ALL = [...FN, ...EX];
|
|
@@ -124,8 +153,7 @@ export function buildManual(spec, opts) {
|
|
|
124
153
|
<a href="#overview" data-nav class="ovlink">Overview</a>
|
|
125
154
|
<div class="ngroup"><div class="ghead">Functionality</div>${FN.map((p) => `<a href="#${p.id}" data-nav>${esc(p.nav)}</a>`).join("")}</div>
|
|
126
155
|
<div class="ngroup"><div class="ghead">Examples <span class="ct">${EX.length}</span></div>${EX.map((p) => `<a href="#${p.id}" data-nav>${esc(p.nav)}</a>`).join("")}</div>`;
|
|
127
|
-
const html = `<
|
|
128
|
-
<style>
|
|
156
|
+
const html = `<style>
|
|
129
157
|
:root{--bg:#0a0d12;--bg2:#0c1118;--surf:#131923;--ink:#eaeff5;--dim:#939dab;--faint:#5f6a79;
|
|
130
158
|
--line:#212a36;--accent:#54d6cf;--measure:64ch}
|
|
131
159
|
@media (prefers-color-scheme:light){:root{--bg:#f5f7f9;--bg2:#eef1f4;--surf:#fff;--ink:#0f1620;
|
|
@@ -185,6 +213,10 @@ main{min-width:0;padding:0 clamp(20px,5vw,72px)}
|
|
|
185
213
|
.linklist{display:flex;flex-direction:column;gap:2px}
|
|
186
214
|
.linklist a{text-decoration:none;color:var(--dim);font-size:14px;padding:5px 0}
|
|
187
215
|
.linklist a:hover{color:var(--accent)}
|
|
216
|
+
.prov{margin:0 0 48px;padding-top:18px;border-top:1px solid var(--line);font-size:12px;color:var(--faint);display:flex;flex-wrap:wrap;gap:8px 24px;justify-content:space-between}
|
|
217
|
+
.prov .out{display:flex;flex-wrap:wrap;gap:14px}
|
|
218
|
+
.prov a{color:var(--dim);text-decoration:none}
|
|
219
|
+
.prov a:hover{color:var(--accent)}
|
|
188
220
|
@media (max-width:880px){.wrap{grid-template-columns:1fr}
|
|
189
221
|
aside{position:static;height:auto;border-right:none;border-bottom:1px solid var(--line)}
|
|
190
222
|
.twocol{grid-template-columns:1fr}}
|
|
@@ -195,6 +227,7 @@ main{min-width:0;padding:0 clamp(20px,5vw,72px)}
|
|
|
195
227
|
<main>
|
|
196
228
|
${overview}
|
|
197
229
|
${ALL.map(pageHtml).join("\n")}
|
|
230
|
+
${footerHtml(spec, provenance)}
|
|
198
231
|
</main>
|
|
199
232
|
</div>
|
|
200
233
|
|
|
@@ -223,12 +256,17 @@ ${ALL.map(pageHtml).join("\n")}
|
|
|
223
256
|
${body}
|
|
224
257
|
</body>
|
|
225
258
|
</html>`;
|
|
259
|
+
// The Artifact tool supplies the head, so the fragment carries its own
|
|
260
|
+
// <title>; the standalone wraps the BODY alone, so the title appears once.
|
|
261
|
+
const fragment = (body) => `<title>${esc(spec.title)}</title>\n${body}`;
|
|
226
262
|
// Fragment and standalone are both inlined (Artifact CSP; publishable on its
|
|
227
263
|
// own). Sidecars are an EXTRA output, never a substitution.
|
|
228
|
-
const
|
|
229
|
-
const
|
|
264
|
+
const body = render(inline);
|
|
265
|
+
const indexHtml = fragment(body);
|
|
266
|
+
const standaloneHtml = document(body);
|
|
230
267
|
if (!assets)
|
|
231
268
|
return { indexHtml, standaloneHtml, missing };
|
|
232
269
|
mkdirSync(assets.dir, { recursive: true });
|
|
233
|
-
|
|
270
|
+
const standaloneAssetHtml = document(render(external));
|
|
271
|
+
return { indexHtml, standaloneHtml, standaloneAssetHtml, missing, sidecarHrefs };
|
|
234
272
|
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { type BuildManualResult, type ManualLink, type ManualSpec, type Provenance } from "./build-manual.js";
|
|
2
|
+
export declare const BUNDLE_SCHEMA_VERSION = 1;
|
|
3
|
+
export interface BundlePageEntry {
|
|
4
|
+
id: string;
|
|
5
|
+
nav: string;
|
|
6
|
+
title: string;
|
|
7
|
+
kind: "overview" | "fn" | "ex";
|
|
8
|
+
/** Relative to the bundle directory. */
|
|
9
|
+
file: string;
|
|
10
|
+
}
|
|
11
|
+
export interface BundleManifest {
|
|
12
|
+
schemaVersion: number;
|
|
13
|
+
brand: string;
|
|
14
|
+
title: string;
|
|
15
|
+
provenance: Provenance;
|
|
16
|
+
links: ManualLink[];
|
|
17
|
+
pages: BundlePageEntry[];
|
|
18
|
+
/** Derived single-page exports, relative to the bundle directory. */
|
|
19
|
+
exports: {
|
|
20
|
+
manual: string;
|
|
21
|
+
manualLinked: string;
|
|
22
|
+
};
|
|
23
|
+
/** `<slug>/<file>` of every referenced capture that was not on disk — a
|
|
24
|
+
* bundle with gaps is still a bundle, but a consumer can see them. */
|
|
25
|
+
missing: string[];
|
|
26
|
+
}
|
|
27
|
+
export interface BuildBundleOptions {
|
|
28
|
+
docsRoot: string;
|
|
29
|
+
/** Where the bundle is written; created if absent. */
|
|
30
|
+
outDir: string;
|
|
31
|
+
provenance?: Provenance;
|
|
32
|
+
}
|
|
33
|
+
export interface BuildBundleResult extends BuildManualResult {
|
|
34
|
+
manifest: BundleManifest;
|
|
35
|
+
outDir: string;
|
|
36
|
+
}
|
|
37
|
+
export declare function buildBundle(spec: ManualSpec, opts: BuildBundleOptions): BuildBundleResult;
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
// The docs bundle: what a product's docs build produces, and the unit every
|
|
2
|
+
// downstream consumer reads (ADR-0012). A directory holding
|
|
3
|
+
//
|
|
4
|
+
// bundle.json this manifest — identity, provenance, the page list
|
|
5
|
+
// pages/<id>.md one markdown page per manual page, captures by path
|
|
6
|
+
// captures/<file> every referenced capture, resized (or a labelled gap)
|
|
7
|
+
// manual.html derived: the self-contained single-page export
|
|
8
|
+
// manual-linked.html derived: the same page referencing captures/
|
|
9
|
+
//
|
|
10
|
+
// The product image bakes this directory in and serves it at /docs; the public
|
|
11
|
+
// plane ingests pages/*.md. `ManualSpec` stays the authoring input — the
|
|
12
|
+
// bundle is that spec promoted to a contract, with the captures alongside.
|
|
13
|
+
import { mkdirSync, writeFileSync } from "node:fs";
|
|
14
|
+
import { join } from "node:path";
|
|
15
|
+
import { buildManual, } from "./build-manual.js";
|
|
16
|
+
export const BUNDLE_SCHEMA_VERSION = 1;
|
|
17
|
+
const yamlString = (s) => JSON.stringify(s);
|
|
18
|
+
export function buildBundle(spec, opts) {
|
|
19
|
+
const { docsRoot, outDir } = opts;
|
|
20
|
+
const provenance = { builtAt: new Date().toISOString(), ...opts.provenance };
|
|
21
|
+
mkdirSync(join(outDir, "pages"), { recursive: true });
|
|
22
|
+
const manual = buildManual(spec, {
|
|
23
|
+
docsRoot,
|
|
24
|
+
provenance,
|
|
25
|
+
assets: { dir: join(outDir, "captures"), href: "captures" },
|
|
26
|
+
});
|
|
27
|
+
const hrefs = manual.sidecarHrefs ?? {};
|
|
28
|
+
// Pages live one level down, so a capture href is reached via `../`.
|
|
29
|
+
const captureRef = (slug, file) => `../${hrefs[`${slug}/${file}`]}`;
|
|
30
|
+
const all = [...spec.functionality, ...spec.examples];
|
|
31
|
+
const byId = new Map(all.map((p) => [p.id, p]));
|
|
32
|
+
const pageLink = (id) => {
|
|
33
|
+
const t = byId.get(id);
|
|
34
|
+
return t ? `[${t.nav}](${id}.md)` : "";
|
|
35
|
+
};
|
|
36
|
+
const pageMarkdown = (p) => {
|
|
37
|
+
const head = [
|
|
38
|
+
"---",
|
|
39
|
+
`title: ${yamlString(p.title)}`,
|
|
40
|
+
`nav: ${yamlString(p.nav)}`,
|
|
41
|
+
`kind: ${p.kind}`,
|
|
42
|
+
...(p.tag ? [`tag: ${yamlString(p.tag)}`] : []),
|
|
43
|
+
"---",
|
|
44
|
+
];
|
|
45
|
+
const steps = p.steps.map((s, i) => `### ${String(i + 1).padStart(2, "0")} ${s.head}\n\n${s.desc}\n\n})`);
|
|
46
|
+
const links = p.links.map(pageLink).filter(Boolean);
|
|
47
|
+
const related = links.length ? [`${p.kind === "fn" ? "Seen in" : "Uses"}: ${links.join(", ")}`] : [];
|
|
48
|
+
return [...head, "", p.intro, "", ...steps.flatMap((s) => [s, ""]), ...related].join("\n").trimEnd().concat("\n");
|
|
49
|
+
};
|
|
50
|
+
const ov = spec.overview;
|
|
51
|
+
const overviewMarkdown = [
|
|
52
|
+
"---",
|
|
53
|
+
`title: ${yamlString(ov.headline)}`,
|
|
54
|
+
`nav: "Overview"`,
|
|
55
|
+
"kind: overview",
|
|
56
|
+
"---",
|
|
57
|
+
"",
|
|
58
|
+
ov.lead,
|
|
59
|
+
"",
|
|
60
|
+
`})`,
|
|
61
|
+
"",
|
|
62
|
+
`_${ov.hero.caption}_`,
|
|
63
|
+
"",
|
|
64
|
+
`## ${ov.byFunctionHeading ?? "By function"}`,
|
|
65
|
+
"",
|
|
66
|
+
...spec.functionality.map((p) => `- ${pageLink(p.id)}`),
|
|
67
|
+
"",
|
|
68
|
+
`## ${ov.byExampleHeading ?? "By example"}`,
|
|
69
|
+
"",
|
|
70
|
+
ov.byExampleIntro,
|
|
71
|
+
"",
|
|
72
|
+
...spec.examples.map((p) => `- ${pageLink(p.id)}`),
|
|
73
|
+
"",
|
|
74
|
+
].join("\n");
|
|
75
|
+
const pages = [
|
|
76
|
+
{ id: "overview", nav: "Overview", title: ov.headline, kind: "overview", file: "pages/overview.md" },
|
|
77
|
+
...all.map((p) => ({ id: p.id, nav: p.nav, title: p.title, kind: p.kind, file: `pages/${p.id}.md` })),
|
|
78
|
+
];
|
|
79
|
+
writeFileSync(join(outDir, "pages/overview.md"), overviewMarkdown);
|
|
80
|
+
for (const p of all)
|
|
81
|
+
writeFileSync(join(outDir, `pages/${p.id}.md`), pageMarkdown(p));
|
|
82
|
+
const exports = { manual: "manual.html", manualLinked: "manual-linked.html" };
|
|
83
|
+
writeFileSync(join(outDir, exports.manual), manual.standaloneHtml);
|
|
84
|
+
writeFileSync(join(outDir, exports.manualLinked), manual.standaloneAssetHtml ?? manual.standaloneHtml);
|
|
85
|
+
const manifest = {
|
|
86
|
+
schemaVersion: BUNDLE_SCHEMA_VERSION,
|
|
87
|
+
brand: spec.brand,
|
|
88
|
+
title: spec.title,
|
|
89
|
+
provenance,
|
|
90
|
+
links: spec.links ?? [],
|
|
91
|
+
pages,
|
|
92
|
+
exports,
|
|
93
|
+
missing: manual.missing,
|
|
94
|
+
};
|
|
95
|
+
writeFileSync(join(outDir, "bundle.json"), `${JSON.stringify(manifest, null, 2)}\n`);
|
|
96
|
+
return { ...manual, manifest, outDir };
|
|
97
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@norskvideo/ctl-dev-kit",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.56",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"exports": {
|
|
6
6
|
"./package.json": "./package.json",
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
"./docs/path-existence": "./docs/path-existence.ts",
|
|
11
11
|
"./doc-guide": "./doc-guide/doc-guide.js",
|
|
12
12
|
"./doc-guide/build-manual": "./doc-guide/build-manual.js",
|
|
13
|
+
"./doc-guide/bundle": "./doc-guide/bundle.js",
|
|
13
14
|
"./doc-guide/guides-config": "./doc-guide/guides-config.js",
|
|
14
15
|
"./doc-guide/instance-proxy": "./doc-guide/instance-proxy.js",
|
|
15
16
|
"./doc-guide/live-browser": "./doc-guide/live-browser.js"
|