@norskvideo/ctl-dev-kit 0.1.36 → 0.1.38
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/doc-guide/build-manual.d.ts +8 -0
- package/doc-guide/build-manual.js +86 -37
- package/local-dev/README.md +4 -4
- package/local-dev/dev-link.sh +1 -1
- package/package.json +1 -1
|
@@ -51,6 +51,14 @@ export interface BuildManualResult {
|
|
|
51
51
|
/** `<slug>/<file>` of every referenced capture not found on disk. */
|
|
52
52
|
missing: string[];
|
|
53
53
|
}
|
|
54
|
+
/** Where to write de-inlined captures for the standalone manual. `dir` is the
|
|
55
|
+
* filesystem directory; `href` (default "images") is the relative URL prefix
|
|
56
|
+
* the manual references them by — so `dir` sits at `<manual>/<href>`. */
|
|
57
|
+
export interface ManualAssets {
|
|
58
|
+
dir: string;
|
|
59
|
+
href?: string;
|
|
60
|
+
}
|
|
54
61
|
export declare function buildManual(spec: ManualSpec, opts: {
|
|
55
62
|
docsRoot: string;
|
|
63
|
+
assets?: ManualAssets;
|
|
56
64
|
}): BuildManualResult;
|
|
@@ -4,28 +4,37 @@
|
|
|
4
4
|
//
|
|
5
5
|
// Every screenshot is a real capture a doc-guide wrote under
|
|
6
6
|
// <docsRoot>/<slug>/<slug>-00N.png. This assembler reads those PNGs, resizes each
|
|
7
|
-
// with ImageMagick,
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
7
|
+
// with ImageMagick, and returns both the Artifact FRAGMENT (index.html — the
|
|
8
|
+
// Artifact tool supplies the doctype/head/body) and a complete STANDALONE document
|
|
9
|
+
// (manual.html — for hosting the manual directly as a page). A referenced shot that
|
|
10
|
+
// hasn't been generated yet renders as a labelled gap rather than crashing the
|
|
11
|
+
// build; every gap is reported in `missing`.
|
|
12
|
+
//
|
|
13
|
+
// The FRAGMENT always inlines each image as a data URI — Artifacts serve under a
|
|
14
|
+
// CSP that blocks external image hosts, so a self-contained fragment is the only
|
|
15
|
+
// thing that renders there. The STANDALONE inlines too by default, but pass
|
|
16
|
+
// `opts.assets` to write each capture as a sidecar file and reference it by a
|
|
17
|
+
// relative href instead — the shape marketing hosts and zips, where individual
|
|
18
|
+
// images must be liftable rather than buried in base64.
|
|
12
19
|
//
|
|
13
20
|
// Product-specific content — the page arrays, the overview copy, the brand — is
|
|
14
21
|
// supplied by the caller in `ManualSpec`; everything structural (layout, CSS,
|
|
15
22
|
// routing) lives here so every product's manual looks and behaves the same.
|
|
16
23
|
import { spawnSync } from "node:child_process";
|
|
17
|
-
import { existsSync } from "node:fs";
|
|
24
|
+
import { existsSync, mkdirSync, writeFileSync } from "node:fs";
|
|
18
25
|
import { join } from "node:path";
|
|
19
26
|
const esc = (s) => s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
20
|
-
/** Resize a captured PNG to <=1100px wide JPEG
|
|
21
|
-
*
|
|
22
|
-
function
|
|
27
|
+
/** Resize a captured PNG to <=1100px wide JPEG. A missing capture becomes a
|
|
28
|
+
* labelled placeholder so the manual still builds, and is recorded in `missing`. */
|
|
29
|
+
function loadAsset(docsRoot, slug, file, missing) {
|
|
23
30
|
const path = join(docsRoot, slug, file);
|
|
24
31
|
if (!existsSync(path)) {
|
|
25
32
|
missing.push(`${slug}/${file}`);
|
|
26
33
|
const label = esc(`missing: ${slug}/${file}`);
|
|
27
|
-
|
|
28
|
-
|
|
34
|
+
return {
|
|
35
|
+
kind: "svg",
|
|
36
|
+
text: `<svg xmlns="http://www.w3.org/2000/svg" width="880" height="200"><rect width="100%" height="100%" fill="#1a2230"/><text x="50%" y="50%" fill="#6b7787" font-family="monospace" font-size="15" text-anchor="middle" dominant-baseline="middle">${label}</text></svg>`,
|
|
37
|
+
};
|
|
29
38
|
}
|
|
30
39
|
const r = spawnSync("magick", [path, "-resize", "1100x>", "-quality", "82", "jpg:-"], {
|
|
31
40
|
maxBuffer: 64 * 1024 * 1024,
|
|
@@ -33,43 +42,73 @@ function makeUri(docsRoot, slug, file, missing) {
|
|
|
33
42
|
if (r.status !== 0 || !r.stdout?.length) {
|
|
34
43
|
throw new Error(`magick failed for ${path}: ${r.stderr?.toString() ?? "no output"}`);
|
|
35
44
|
}
|
|
36
|
-
return
|
|
45
|
+
return { kind: "jpeg", bytes: r.stdout };
|
|
46
|
+
}
|
|
47
|
+
/** Inline the asset as a data URI (fragment path — always self-contained). */
|
|
48
|
+
function inlineUri(a) {
|
|
49
|
+
return a.kind === "svg"
|
|
50
|
+
? `data:image/svg+xml;base64,${Buffer.from(a.text).toString("base64")}`
|
|
51
|
+
: `data:image/jpeg;base64,${a.bytes.toString("base64")}`;
|
|
52
|
+
}
|
|
53
|
+
/** Write the asset as a sidecar file under `assets.dir` and return its relative
|
|
54
|
+
* href (standalone path). The capture filename convention `<slug>-00N.png` is
|
|
55
|
+
* already unique, so the basename (extension swapped for the encoded format) is
|
|
56
|
+
* a safe sidecar name. */
|
|
57
|
+
function fileUri(a, file, assets) {
|
|
58
|
+
const ext = a.kind === "svg" ? "svg" : "jpg";
|
|
59
|
+
const name = `${file.replace(/\.[^.]+$/, "")}.${ext}`;
|
|
60
|
+
writeFileSync(join(assets.dir, name), a.kind === "svg" ? Buffer.from(a.text) : a.bytes);
|
|
61
|
+
return `${assets.href ?? "images"}/${name}`;
|
|
37
62
|
}
|
|
38
63
|
export function buildManual(spec, opts) {
|
|
39
|
-
const { docsRoot } = opts;
|
|
64
|
+
const { docsRoot, assets } = opts;
|
|
40
65
|
const missing = [];
|
|
41
|
-
|
|
66
|
+
// Each shot is loaded (resized) at most once, then rendered inline for the
|
|
67
|
+
// fragment and — when `assets` is set — as a sidecar file for the standalone.
|
|
68
|
+
const cache = new Map();
|
|
69
|
+
const load = (slug, file) => {
|
|
70
|
+
const key = `${slug}/${file}`;
|
|
71
|
+
let a = cache.get(key);
|
|
72
|
+
if (!a) {
|
|
73
|
+
a = loadAsset(docsRoot, slug, file, missing);
|
|
74
|
+
cache.set(key, a);
|
|
75
|
+
}
|
|
76
|
+
return a;
|
|
77
|
+
};
|
|
78
|
+
const inline = (slug, file) => inlineUri(load(slug, file));
|
|
79
|
+
const external = (slug, file) => fileUri(load(slug, file), file, assets);
|
|
42
80
|
const FN = spec.functionality;
|
|
43
81
|
const EX = spec.examples;
|
|
44
82
|
const ALL = [...FN, ...EX];
|
|
45
83
|
const byId = new Map(ALL.map((p) => [p.id, p]));
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
84
|
+
const render = (uri) => {
|
|
85
|
+
function pageHtml(p) {
|
|
86
|
+
const steps = p.steps
|
|
87
|
+
.map((s, i) => `<li class="step">
|
|
49
88
|
<div class="stinfo"><span class="stnum">${String(i + 1).padStart(2, "0")}</span><div><h3>${esc(s.head)}</h3><p>${esc(s.desc)}</p></div></div>
|
|
50
89
|
<div class="shot"><img loading="lazy" src="${uri(s.slug, s.file)}" alt="${esc(s.head)}"></div>
|
|
51
90
|
</li>`)
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
91
|
+
.join("\n");
|
|
92
|
+
const links = p.links.length
|
|
93
|
+
? `<div class="xlinks"><span class="xlab">${p.kind === "fn" ? "Seen in" : "Uses"}</span>${p.links
|
|
94
|
+
.map((id) => {
|
|
95
|
+
const t = byId.get(id);
|
|
96
|
+
return t ? `<a href="#${id}" class="chip">${esc(t.nav)}</a>` : "";
|
|
97
|
+
})
|
|
98
|
+
.join("")}</div>`
|
|
99
|
+
: "";
|
|
100
|
+
const thin = p.thin
|
|
101
|
+
? `<p class="thin">More of this walkthrough is coming — this deployment shape has one screen captured so far.</p>`
|
|
102
|
+
: "";
|
|
103
|
+
return `<article class="page" id="${p.id}">
|
|
65
104
|
<div class="phead"><span class="kind ${p.kind}">${p.kind === "fn" ? "Functionality" : "Example deployment"}</span><h1>${esc(p.title)}</h1>${p.tag ? `<div class="ptag mono">${esc(p.tag)}</div>` : ""}</div>
|
|
66
105
|
<p class="lead">${esc(p.intro)}</p>
|
|
67
106
|
<ol class="steps">${steps}</ol>
|
|
68
107
|
${thin}${links}
|
|
69
108
|
</article>`;
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
109
|
+
}
|
|
110
|
+
const ov = spec.overview;
|
|
111
|
+
const overview = `<article class="page" id="overview">
|
|
73
112
|
<div class="phead"><span class="kind ov">Overview</span><h1>${esc(ov.headline)}</h1></div>
|
|
74
113
|
<p class="lead">${esc(ov.lead)}</p>
|
|
75
114
|
<div class="heroshot"><div class="shot"><img src="${uri(ov.hero.slug, ov.hero.file)}" alt="${esc(ov.hero.alt)}"></div><p class="hcap">${esc(ov.hero.caption)}</p></div>
|
|
@@ -78,11 +117,11 @@ export function buildManual(spec, opts) {
|
|
|
78
117
|
<div><h2>${esc(ov.byExampleHeading ?? "By example")}</h2><p class="mini">${esc(ov.byExampleIntro)}</p><div class="linklist">${EX.map((p) => `<a href="#${p.id}">${esc(p.nav)}</a>`).join("")}</div></div>
|
|
79
118
|
</div>
|
|
80
119
|
</article>`;
|
|
81
|
-
|
|
120
|
+
const nav = `
|
|
82
121
|
<a href="#overview" data-nav class="ovlink">Overview</a>
|
|
83
122
|
<div class="ngroup"><div class="ghead">Functionality</div>${FN.map((p) => `<a href="#${p.id}" data-nav>${esc(p.nav)}</a>`).join("")}</div>
|
|
84
123
|
<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>`;
|
|
85
|
-
|
|
124
|
+
const html = `<title>${esc(spec.title)}</title>
|
|
86
125
|
<style>
|
|
87
126
|
:root{--bg:#0a0d12;--bg2:#0c1118;--surf:#131923;--ink:#eaeff5;--dim:#939dab;--faint:#5f6a79;
|
|
88
127
|
--line:#212a36;--accent:#54d6cf;--measure:64ch}
|
|
@@ -168,6 +207,16 @@ ${ALL.map(pageHtml).join("\n")}
|
|
|
168
207
|
window.addEventListener('hashchange',show);show();
|
|
169
208
|
})();
|
|
170
209
|
</script>`;
|
|
210
|
+
return html;
|
|
211
|
+
};
|
|
212
|
+
// Fragment: always inlined (Artifact CSP). Standalone: de-inlined sidecar files
|
|
213
|
+
// when `assets` is set, otherwise the same inlined body wrapped in a document.
|
|
214
|
+
const indexHtml = render(inline);
|
|
215
|
+
let standaloneBody = indexHtml;
|
|
216
|
+
if (assets) {
|
|
217
|
+
mkdirSync(assets.dir, { recursive: true });
|
|
218
|
+
standaloneBody = render(external);
|
|
219
|
+
}
|
|
171
220
|
const standaloneHtml = `<!doctype html>
|
|
172
221
|
<html lang="en">
|
|
173
222
|
<head>
|
|
@@ -176,8 +225,8 @@ ${ALL.map(pageHtml).join("\n")}
|
|
|
176
225
|
<title>${esc(spec.title)}</title>
|
|
177
226
|
</head>
|
|
178
227
|
<body>
|
|
179
|
-
${
|
|
228
|
+
${standaloneBody}
|
|
180
229
|
</body>
|
|
181
230
|
</html>`;
|
|
182
|
-
return { indexHtml
|
|
231
|
+
return { indexHtml, standaloneHtml, missing };
|
|
183
232
|
}
|
package/local-dev/README.md
CHANGED
|
@@ -57,12 +57,12 @@ HARDWARE=none NORSK_LICENSE_FILE=... bun run iterate # no GPU reservat
|
|
|
57
57
|
To develop a product against **un-published** ctl code or a **locally-built**
|
|
58
58
|
media server, there are three independent override layers. Each is opt-in and
|
|
59
59
|
leaves the default (registry/pinned) path untouched when unset. They compose:
|
|
60
|
-
changing the daemon
|
|
60
|
+
changing the daemon _and_ a ctl library it exposes means layers 1 + 3 together.
|
|
61
61
|
|
|
62
62
|
## 1. The npm layer — local `@norskvideo/ctl-*` checkouts
|
|
63
63
|
|
|
64
64
|
The product depends on `@norskvideo/ctl-{sdk,foundation,product-template-schema,
|
|
65
|
-
|
|
65
|
+
test-harness,commands,dev-kit}` at a caret range. To point one (or all)
|
|
66
66
|
at a local checkout of the ctl monorepo instead of the registry, use
|
|
67
67
|
`dev-link.sh`:
|
|
68
68
|
|
|
@@ -119,9 +119,9 @@ bun run --cwd ../norsk-ctl cli product add --dev-url http://localhost:<backendPo
|
|
|
119
119
|
bun run --cwd ../norsk-ctl cli instance launch-template demo --template <t> --hardware none
|
|
120
120
|
```
|
|
121
121
|
|
|
122
|
-
No linking is needed for this — you are running the daemon
|
|
122
|
+
No linking is needed for this — you are running the daemon _code_ directly. Pair
|
|
123
123
|
with layer 1 (`dev-link.sh`) only if you also changed a `@norskvideo/ctl-*`
|
|
124
|
-
library the product
|
|
124
|
+
library the product _compiles_ against.
|
|
125
125
|
|
|
126
126
|
### Integration tests against source ctl
|
|
127
127
|
|
package/local-dev/dev-link.sh
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
# Nothing is committed — the links live only in this repo's node_modules.
|
|
9
9
|
set -euo pipefail
|
|
10
10
|
|
|
11
|
-
PACKAGES=(sdk foundation product-template-schema
|
|
11
|
+
PACKAGES=(sdk foundation product-template-schema test-harness commands dev-kit)
|
|
12
12
|
product_root="$(cd "$(dirname "${BASH_SOURCE[0]}")" && git -C . rev-parse --show-toplevel 2>/dev/null || pwd)"
|
|
13
13
|
|
|
14
14
|
if [ "${1:-}" = "--unlink" ]; then
|