@homepages/template-kit 0.8.0 → 0.8.1-dev-20260718224903

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 CHANGED
@@ -1,5 +1,66 @@
1
1
  # @homepages/template-kit
2
2
 
3
+ ## 0.8.1-dev-20260718224903
4
+
5
+ ### Patch Changes
6
+
7
+ - f3f6d70: `template-kit check` no longer fails on a tsconfig `paths` alias (e.g.
8
+ `"@/platform/*": ["../platform/*"]`) or on a section whose contract files
9
+ share a module large enough for esbuild to code-split. Previously the
10
+ loader's css-stub plugin decided "external vs. bundle" from the specifier's
11
+ own text, so a `paths`-resolved alias got re-emitted as a literal bare
12
+ import Node couldn't resolve; separately, the loader's build applied
13
+ `publicPath` to code-split `chunk-*.js` cross-imports the same way it does
14
+ to real asset files, producing an import path Node couldn't resolve either.
15
+ Both are now decided from the actual resolved file, and `publicPath` is no
16
+ longer set on the loader's build.
17
+ - 1c2c2db: `template-kit dev`'s preview server no longer 500s on `/runtime/base.css`.
18
+ The route resolved the file at a path the build never emits to
19
+ (`dist/css/base.css`; the build copies it flat to `dist/base.css`), so every
20
+ section rendered unstyled — Tailwind-derived colors and layout were silently
21
+ missing everywhere, only arbitrary-value element geometry survived. The
22
+ route now checks the built (flat) path first and falls back to the source
23
+ (`src/css/`) layout, so it resolves correctly both from an installed kit and
24
+ when running the CLI unbuilt via `tsx` during kit development.
25
+ - 989d23e: `template-kit dev` gains an optional `--diagnostics-cmd` flag: point it at any
26
+ command that prints `{"violations": [...]}` JSON for a `--template`/`--section`
27
+ pair, and the standalone section preview
28
+ (`/sections/<template>/<section>/<fixture>`) shows a dismissible banner when it
29
+ reports violations. Unset by default — no behavior change for a workspace that
30
+ doesn't configure it.
31
+ - e1d5792: `template-kit dev`'s preview server now compiles a template's theme-color
32
+ Tailwind utilities (`bg-primary`, `text-ink`, `bg-background`, etc.) instead
33
+ of silently dropping them. `assembleTemplateCss` used to concatenate the
34
+ compiled theme (`compileThemeToCss`, the `@theme inline` alias layer plus
35
+ its `:root` custom props) onto Tailwind's CLI output _after_ the CLI had
36
+ already run, so the theme's color tokens were never part of the build graph
37
+ Tailwind scanned — `@theme` outside that graph is inert, and silently so, so
38
+ any utility built on a template color never got generated. No error, no
39
+ warning: every section rendered with correct layout but default
40
+ black-on-white text and backgrounds. The theme partial is now written to the
41
+ dev cache and `@import`ed into the Tailwind entry ahead of the section
42
+ `@source` scans, mirroring how the registry's own publish pipeline
43
+ (`ci/build-css.ts`) has always assembled it. `fontFaces` is still prepended
44
+ to the final bundle separately, since a nested `@import url(...)` doesn't
45
+ hoist out of an imported partial.
46
+ - 1c4c7dd: Island hydration is now nesting-safe: `hydrateIslands` skips any island marker
47
+ nested inside another marker, so an island can render another island without the
48
+ child being hydrated twice (once by its parent root, once independently). Enables
49
+ interactive islands (accordion, card-slider, carousel) to wrap existing leaf
50
+ islands (expandable-text) on published pages.
51
+ - ff1d6de: `template-kit dev`'s canvas mirror now shows the Islands inspector panel for a
52
+ section-level selection, not just a slot selection. A `fill-parent`/`fill-self`
53
+ island with no owning slot of its own only ever gets its shield dropped by a
54
+ section-level click, so the panel previously could show it as `shielded` but
55
+ never `interactive`. Existing slot-selection behavior (edit panel + Islands
56
+ list) is unchanged.
57
+ - 41d0e88: `template-kit dev` now honors a workspace's tsconfig `paths` when resolving
58
+ imports, the same way `check` already does via esbuild's native tsconfig
59
+ lookup. Previously the dev server's Vite instance had no `paths` awareness at
60
+ all, so a workspace mapping (e.g. `"@/lib/*": ["../lib/*"]`) that made `check`
61
+ pass left `dev` 500ing with `Cannot find module`. A workspace with no `paths`
62
+ mapping is unaffected.
63
+
3
64
  ## 0.8.0
4
65
 
5
66
  ### Minor Changes
@@ -1,5 +1,5 @@
1
1
  import { loadEsbuild } from "./resolve-tool.js";
2
- import { ASSET_LOADERS, ASSET_NAMES, ASSET_PUBLIC_PATH } from "./css.js";
2
+ import { ASSET_LOADERS, ASSET_NAMES } from "./css.js";
3
3
  import { join, relative } from "node:path";
4
4
  import { mkdir, rm, stat } from "node:fs/promises";
5
5
  import { createHash } from "node:crypto";
@@ -45,6 +45,7 @@ var MissingSectionExport = class extends Error {
45
45
  }
46
46
  };
47
47
  const RESOLVING = Symbol("css-stub:resolving");
48
+ const isUnderNodeModules = (path) => path.split(/[/\\]/).includes("node_modules");
48
49
  const cssStub = {
49
50
  name: "css-stub",
50
51
  setup(b) {
@@ -61,10 +62,13 @@ const cssStub = {
61
62
  path: resolved.path,
62
63
  namespace: "css-stub"
63
64
  };
64
- if (!args.path.startsWith(".") && !args.path.startsWith("/")) return {
65
- path: args.path,
66
- external: true
67
- };
65
+ if (!args.path.startsWith(".") && !args.path.startsWith("/")) {
66
+ if (resolved.errors.length > 0 || isUnderNodeModules(resolved.path)) return {
67
+ path: args.path,
68
+ external: true
69
+ };
70
+ return null;
71
+ }
68
72
  return null;
69
73
  });
70
74
  b.onLoad({
@@ -123,7 +127,6 @@ async function buildAndLoad(section, outdir, workspaceRoot) {
123
127
  logLevel: "silent",
124
128
  plugins: [cssStub],
125
129
  loader: { ...ASSET_LOADERS },
126
- publicPath: ASSET_PUBLIC_PATH,
127
130
  assetNames: ASSET_NAMES,
128
131
  absWorkingDir: workspaceRoot
129
132
  });
@@ -13,7 +13,7 @@ import { mkdir, readFile, writeFile } from "node:fs/promises";
13
13
  * primitives back in. Every path is emitted relative to `entryDir` (where the
14
14
  * entry is written) so it resolves from that location.
15
15
  */
16
- function buildTailwindEntry(entryDir, sectionDirs) {
16
+ function buildTailwindEntry(entryDir, sectionDirs, themePartialPath) {
17
17
  const rel = (target) => {
18
18
  const path = relative(entryDir, target);
19
19
  return path.startsWith(".") ? path : `./${path}`;
@@ -21,16 +21,22 @@ function buildTailwindEntry(entryDir, sectionDirs) {
21
21
  return [
22
22
  `@import "tailwindcss" source(none);`,
23
23
  `@import "@homepages/template-kit/styles.css";`,
24
+ `@import "${rel(themePartialPath)}";`,
24
25
  ...sectionDirs.map((d) => `@source "${rel(d)}/**/*.tsx";`),
25
26
  ""
26
27
  ].join("\n");
27
28
  }
28
- async function compileUtilities(root, sectionDirs) {
29
+ async function compileUtilities(root, theme, sectionDirs) {
29
30
  const cacheDir = join(root, ".tk-dev-cache");
30
31
  await mkdir(cacheDir, { recursive: true });
31
32
  const input = join(cacheDir, "tw-entry.css");
33
+ const themePath = join(cacheDir, "tw-theme.css");
32
34
  const output = join(cacheDir, "tw-out.css");
33
- await writeFile(input, buildTailwindEntry(cacheDir, sectionDirs), "utf8");
35
+ await writeFile(themePath, compileThemeToCss({
36
+ ...theme,
37
+ fontFaces: void 0
38
+ }), "utf8");
39
+ await writeFile(input, buildTailwindEntry(cacheDir, sectionDirs, themePath), "utf8");
34
40
  await runTailwind(root, {
35
41
  input,
36
42
  output
@@ -75,11 +81,11 @@ async function compileSectionHandCss(root, sectionDirs) {
75
81
  /** theme :root/@theme custom props, the compiled Tailwind utilities, then each
76
82
  * section's hand-CSS with its `url()` asset refs rewritten to servable paths. */
77
83
  async function assembleTemplateCss(root, theme, sectionDirs) {
78
- const utilities = await compileUtilities(root, sectionDirs);
84
+ const utilities = await compileUtilities(root, theme, sectionDirs);
79
85
  const sections = await compileSectionHandCss(root, sectionDirs);
80
86
  return {
81
87
  css: [
82
- compileThemeToCss(theme),
88
+ theme.fontFaces,
83
89
  utilities,
84
90
  sections.css
85
91
  ].filter(Boolean).join("\n"),
@@ -12,7 +12,7 @@ function wrapInstance(instanceId, label, html) {
12
12
  return `<div ${ATTR_SECTION_INSTANCE_ID}="${escapeAttr(instanceId)}" data-section-label="${escapeAttr(label)}">${html}</div>`;
13
13
  }
14
14
  async function composeTemplatePage(vite, args) {
15
- const { workspace, template, islandBootstrapUrl, canvas = false } = args;
15
+ const { workspace, template, islandBootstrapUrl, canvasIslandBootstrapUrl, canvas = false } = args;
16
16
  const instances = await resolveManifestInstances(template);
17
17
  const bodies = [];
18
18
  const islands = {};
@@ -29,8 +29,9 @@ async function composeTemplatePage(vite, args) {
29
29
  sectionHtml: bodies.join("\n"),
30
30
  cssRev: "0",
31
31
  template: template.key,
32
+ section: "",
32
33
  islands,
33
- bootstrapUrl: islandBootstrapUrl
34
+ bootstrapUrl: canvas ? canvasIslandBootstrapUrl : islandBootstrapUrl
34
35
  });
35
36
  }
36
37
 
@@ -0,0 +1,68 @@
1
+ import { promisify } from "node:util";
2
+ import { execFile } from "node:child_process";
3
+
4
+ //#region src/cli/dev/diagnostics.ts
5
+ const exec = promisify(execFile);
6
+ const MAX_BUFFER = 1024 * 1024;
7
+ const TIMEOUT_MS = 15e3;
8
+ async function runDiagnostics(cmd, args) {
9
+ if (cmd === null) return { available: false };
10
+ const parts = cmd.trim().split(/\s+/);
11
+ const program = parts[0];
12
+ if (!program) return { available: false };
13
+ const baseArgs = parts.slice(1);
14
+ let stdout;
15
+ try {
16
+ stdout = (await exec(program, [
17
+ ...baseArgs,
18
+ "--template",
19
+ args.template,
20
+ "--section",
21
+ args.section
22
+ ], {
23
+ maxBuffer: MAX_BUFFER,
24
+ timeout: TIMEOUT_MS
25
+ })).stdout;
26
+ } catch (err) {
27
+ return {
28
+ available: true,
29
+ error: err instanceof Error ? err.message : String(err)
30
+ };
31
+ }
32
+ return parseViolations(stdout);
33
+ }
34
+ function parseViolations(stdout) {
35
+ let parsed;
36
+ try {
37
+ parsed = JSON.parse(stdout);
38
+ } catch {
39
+ return {
40
+ available: true,
41
+ error: `diagnostics command did not print valid JSON: ${stdout.slice(0, 200)}`
42
+ };
43
+ }
44
+ const body = parsed;
45
+ if (typeof parsed !== "object" || parsed === null || !Array.isArray(body.violations)) return {
46
+ available: true,
47
+ error: `diagnostics command JSON is missing a "violations" array`
48
+ };
49
+ const violations = [];
50
+ for (const raw of body.violations) {
51
+ if (typeof raw !== "object" || raw === null) continue;
52
+ const v = raw;
53
+ if (typeof v.rule !== "string" || typeof v.message !== "string") continue;
54
+ violations.push({
55
+ rule: v.rule,
56
+ message: v.message,
57
+ ...typeof v.file === "string" ? { file: v.file } : {},
58
+ ...typeof v.line === "number" ? { line: v.line } : {}
59
+ });
60
+ }
61
+ return {
62
+ available: true,
63
+ violations
64
+ };
65
+ }
66
+
67
+ //#endregion
68
+ export { runDiagnostics };
@@ -3,12 +3,16 @@ import { startDevServer } from "./server.js";
3
3
  //#region src/cli/dev/index.ts
4
4
  const DEFAULT_PORT = 5180;
5
5
  async function runDev(argv, cwd) {
6
+ const port = parsePort(argv) ?? DEFAULT_PORT;
7
+ const diagnosticsCmd = parseDiagnosticsCmd(argv);
6
8
  const server = await startDevServer({
7
9
  cwd,
8
- port: parsePort(argv) ?? DEFAULT_PORT
10
+ port,
11
+ diagnosticsCmd
9
12
  });
10
13
  process.stdout.write(`template-kit dev — ${server.workspace.templates.length} template(s) at ${server.url}\n`);
11
14
  for (const template of server.workspace.templates) process.stdout.write(` ${template.key}: ${server.url}/sections/${template.key}/<section>/<fixture>\n`);
15
+ if (diagnosticsCmd) process.stdout.write(` diagnostics: ${diagnosticsCmd}\n`);
12
16
  await new Promise((resolve) => {
13
17
  const stop = () => {
14
18
  server.close().then(resolve);
@@ -27,6 +31,12 @@ function parsePort(argv) {
27
31
  }
28
32
  return null;
29
33
  }
34
+ function parseDiagnosticsCmd(argv) {
35
+ const i = argv.indexOf("--diagnostics-cmd");
36
+ if (i < 0) return null;
37
+ const value = argv[i + 1];
38
+ return typeof value === "string" ? value : null;
39
+ }
30
40
 
31
41
  //#endregion
32
- export { runDev };
42
+ export { parseDiagnosticsCmd, runDev };
@@ -0,0 +1,172 @@
1
+ import { join } from "node:path";
2
+ import { mkdir, writeFile } from "node:fs/promises";
3
+
4
+ //#region src/cli/dev/island-canvas-bootstrap.ts
5
+ const CACHE_DIR = ".tk-dev-cache";
6
+ const BOOTSTRAP_FILE = "island-canvas-bootstrap.js";
7
+ const BOOTSTRAP_SOURCE = `import { hydrateIslands, unmountIslands } from "@homepages/template-kit/island-runtime";
8
+ import { createElement, useLayoutEffect } from "react";
9
+
10
+ const SHIELD_CLASS = "tr-canvas-shield";
11
+ const LIVE_ATTR = "data-tr-live";
12
+
13
+ const modules = import.meta.glob("/templates/**/*.{tsx,jsx}");
14
+ const map = window.__TR_ISLAND_MAP || {};
15
+
16
+ // Per-island hydration-commit signal, keyed by the \`editor\` object THIS load()
17
+ // call clones for that island — never by island key. A section rendering N of
18
+ // one island calls load() N times; keying by key would let the last call's
19
+ // commit gate every handle, un-shielding N-1 of them before their own root has
20
+ // actually committed.
21
+ const commitOf = new WeakMap();
22
+
23
+ // Wrap the loaded default export so it reports its own hydration commit.
24
+ // Renders no DOM of its own (the tree still matches the SSR markup exactly),
25
+ // and useLayoutEffect fires after the commit but before paint, so the canvas
26
+ // never shows an unshielded frame.
27
+ function withCommitSignal(component, signalCommit) {
28
+ return function IslandCommitSignal(props) {
29
+ useLayoutEffect(signalCommit, []);
30
+ return createElement(component, props);
31
+ };
32
+ }
33
+
34
+ async function load(key) {
35
+ const url = map[key];
36
+ const importer = url ? modules[url] : undefined;
37
+ if (!importer) return Promise.reject(new Error("template-kit dev: no island module for " + key));
38
+ const mod = await importer();
39
+
40
+ let signalCommit;
41
+ const committed = new Promise((resolve) => { signalCommit = resolve; });
42
+ const editor = mod.editor ? { ...mod.editor } : undefined;
43
+ if (editor) commitOf.set(editor, { committed, signalCommit });
44
+
45
+ return { ...mod, editor, default: withCommitSignal(mod.default, signalCommit) };
46
+ }
47
+
48
+ function makeShield(doc) {
49
+ const shield = doc.createElement("div");
50
+ shield.className = SHIELD_CLASS;
51
+ shield.setAttribute("aria-hidden", "true");
52
+ return shield;
53
+ }
54
+
55
+ // "fill-self": surface made position:relative, shield appended as its last
56
+ // child (unit cards). "fill-parent": shield is a sibling after the surface
57
+ // (map canvas, slider viewport). Idempotent in both modes.
58
+ function installShield(surface, mode) {
59
+ const doc = surface.ownerDocument;
60
+ if (mode === "fill-self") {
61
+ const pos = doc.defaultView ? doc.defaultView.getComputedStyle(surface).position : "";
62
+ if (!pos || pos === "static") surface.style.position = "relative";
63
+ if (Array.from(surface.children).some((c) => c.classList.contains(SHIELD_CLASS))) return;
64
+ surface.appendChild(makeShield(doc));
65
+ return;
66
+ }
67
+ const next = surface.nextElementSibling;
68
+ if (next && next.classList.contains(SHIELD_CLASS)) return;
69
+ surface.insertAdjacentElement("afterend", makeShield(doc));
70
+ }
71
+
72
+ function shieldHandle(handle) {
73
+ const editor = handle.editor;
74
+ if (!editor || !editor.live) return;
75
+ const surfaces = editor.liveSurface ? editor.liveSurface(handle.element) : [handle.element];
76
+ const mode = editor.shieldMode || "fill-parent";
77
+ for (const surface of surfaces) installShield(surface, mode);
78
+ // Stamped for island-inspect.ts's read-only panel — not read by any hydration logic.
79
+ handle.element.setAttribute("data-tr-island-live", "true");
80
+ handle.element.setAttribute("data-tr-shield-mode", mode);
81
+ }
82
+
83
+ // Every TOP-LEVEL island marker under root, excluding nested ones (a marker
84
+ // inside another marker is hydrated by its parent island's own React root —
85
+ // see hydrateIslands' own filter in island-runtime; this mirrors it so the
86
+ // inspector never double-reports or mis-marks a nested child).
87
+ function topLevelMarkers(root) {
88
+ return Array.from(root.querySelectorAll("[data-tr-island]")).filter(
89
+ (el) => !el.parentElement || !el.parentElement.closest("[data-tr-island]"),
90
+ );
91
+ }
92
+
93
+ let styleInstalled = false;
94
+ function ensureShieldStyle() {
95
+ if (styleInstalled || document.getElementById("tr-canvas-shield-style")) return;
96
+ styleInstalled = true;
97
+ const style = document.createElement("style");
98
+ style.id = "tr-canvas-shield-style";
99
+ style.textContent =
100
+ "." + SHIELD_CLASS + "{position:absolute;inset:0;z-index:1;cursor:pointer;background:transparent;}" +
101
+ "[" + LIVE_ATTR + "] ." + SHIELD_CLASS + "{display:none;}";
102
+ document.head.appendChild(style);
103
+ }
104
+
105
+ // Hydrate every live island under \`root\`, install its shield(s), and mark every
106
+ // top-level island (live or not) with data-tr-island-live for the inspector.
107
+ async function hydrateRoot(root) {
108
+ ensureShieldStyle();
109
+ const before = new Set(topLevelMarkers(root));
110
+
111
+ const handles = await hydrateIslands({
112
+ root,
113
+ load,
114
+ shouldHydrate: (ctx) => (ctx.editor ? ctx.editor.live === true : false),
115
+ applyEditorProps: true,
116
+ onError: (error, ctx) => {
117
+ console.error("template-kit dev: canvas island failed to hydrate:", ctx.key, error);
118
+ // Settle its commit: a failed island never signals one, and the wait
119
+ // below must not stall the rest of this root's shields on it.
120
+ if (ctx.editor) { const c = commitOf.get(ctx.editor); if (c) c.signalCommit(); }
121
+ },
122
+ });
123
+
124
+ // hydrateRoot (React's) flushes on a scheduler task, so hydrateIslands
125
+ // returns before React has committed. Shielding here would mutate a
126
+ // not-yet-hydrated tree: in fill-self mode the shield lands INSIDE a
127
+ // React-owned surface, and React then discards the whole SSR tree and
128
+ // client-renders, wiping shield and inline position both. Waiting on the
129
+ // real commit — not a task-turn guess — is what makes the mutation stick.
130
+ await Promise.all(
131
+ handles
132
+ .map((h) => (h.editor ? commitOf.get(h.editor) : undefined))
133
+ .filter((c) => c !== undefined)
134
+ .map((c) => c.committed),
135
+ );
136
+
137
+ for (const handle of handles) {
138
+ shieldHandle(handle);
139
+ before.delete(handle.element);
140
+ }
141
+ // Whatever's left in \`before\` is a top-level island that did not pass
142
+ // shouldHydrate (editor.live !== true, or no editor at all) — static by its
143
+ // own contract; mark it so for the inspector.
144
+ for (const el of before) el.setAttribute("data-tr-island-live", "false");
145
+
146
+ return handles;
147
+ }
148
+
149
+ function unmountRoot(root) {
150
+ unmountIslands(root);
151
+ root.querySelectorAll("." + SHIELD_CLASS).forEach((s) => s.remove());
152
+ }
153
+
154
+ window.__trCanvasIslands = { hydrateRoot, unmountRoot };
155
+ void hydrateRoot(document.body);
156
+ `;
157
+ /**
158
+ * Write the shared canvas bootstrap into the workspace cache dir and return
159
+ * the URL the canvas page loads it from. Same mechanics as
160
+ * writeIslandBootstrap: lives under the root so Vite serves + transforms it,
161
+ * reached via Vite's `/@fs/` prefix.
162
+ */
163
+ async function writeIslandCanvasBootstrap(root) {
164
+ const dir = join(root, CACHE_DIR);
165
+ await mkdir(dir, { recursive: true });
166
+ const abs = join(dir, BOOTSTRAP_FILE);
167
+ await writeFile(abs, BOOTSTRAP_SOURCE, "utf8");
168
+ return `/@fs/${abs}`;
169
+ }
170
+
171
+ //#endregion
172
+ export { writeIslandCanvasBootstrap };
@@ -1,6 +1,7 @@
1
1
  //#region src/cli/dev/section-page.ts
2
2
  function wrapSectionHtml(opts) {
3
3
  const t = encodeURIComponent(opts.template);
4
+ const s = encodeURIComponent(opts.section);
4
5
  const v = encodeURIComponent(opts.cssRev);
5
6
  return `<!doctype html>
6
7
  <html lang="en">
@@ -14,10 +15,15 @@ function wrapSectionHtml(opts) {
14
15
  </head>
15
16
  <body>
16
17
  <div id="section-host">${opts.sectionHtml}</div>${islandBootstrap(opts.islands, opts.bootstrapUrl)}
17
- <script>${HMR_CLIENT_JS}<\/script>
18
+ <script>${HMR_CLIENT_JS}<\/script>${diagnosticsBanner(opts.section, t, s)}
18
19
  </body>
19
20
  </html>`;
20
21
  }
22
+ function diagnosticsBanner(section, template, encodedSection) {
23
+ if (section === "") return "";
24
+ return `
25
+ <script>${diagnosticsClientJs(template, encodedSection)}<\/script>`;
26
+ }
21
27
  function islandBootstrap(islands, bootstrapUrl) {
22
28
  if (Object.keys(islands).length === 0) return "";
23
29
  const mapJson = JSON.stringify(islands).replace(/</g, "\\u003c");
@@ -49,6 +55,37 @@ const HMR_CLIENT_JS = `
49
55
  } catch (e) { /* dev-only; ignore */ }
50
56
  })();
51
57
  `.trim();
58
+ function diagnosticsClientJs(template, section) {
59
+ return `
60
+ (function () {
61
+ function render(violations) {
62
+ var banner = document.createElement("div");
63
+ banner.setAttribute("data-diagnostics-banner", "");
64
+ banner.style.cssText = "position:sticky;top:0;z-index:9999;background:#fee2e2;color:#7f1d1d;" +
65
+ "font:13px/1.4 monospace;padding:8px 12px;border-bottom:1px solid #fca5a5;";
66
+ var dismiss = document.createElement("button");
67
+ dismiss.textContent = "dismiss";
68
+ dismiss.style.cssText = "float:right;cursor:pointer;";
69
+ dismiss.onclick = function () { banner.remove(); };
70
+ var text = document.createElement("pre");
71
+ text.style.cssText = "margin:0;white-space:pre-wrap;";
72
+ text.textContent = violations.map(function (v) {
73
+ var loc = v.file ? " (" + v.file + (v.line ? ":" + v.line : "") + ")" : "";
74
+ return "[" + v.rule + "] " + v.message + loc;
75
+ }).join("\\n");
76
+ banner.appendChild(dismiss);
77
+ banner.appendChild(text);
78
+ document.body.insertBefore(banner, document.body.firstChild);
79
+ }
80
+ fetch("/api/diagnostics?template=${template}&section=${section}")
81
+ .then(function (r) { return r.json(); })
82
+ .then(function (body) {
83
+ if (body.available && body.violations && body.violations.length > 0) render(body.violations);
84
+ })
85
+ .catch(function () { /* dev-only; ignore */ });
86
+ })();
87
+ `.trim();
88
+ }
52
89
 
53
90
  //#endregion
54
91
  export { wrapSectionHtml };
@@ -11,10 +11,12 @@ import { projectSlotSchema } from "./slot-schema.js";
11
11
  import { renderSection } from "./render-section.js";
12
12
  import { wrapSectionHtml } from "./section-page.js";
13
13
  import { composeTemplatePage } from "./compose-template.js";
14
+ import { runDiagnostics } from "./diagnostics.js";
14
15
  import { collectRenderedSlotIds, computeSlotFills } from "./fill-state.js";
15
16
  import { summarizeHtml } from "./structure-summary.js";
16
17
  import { inspectSection } from "./inspect.js";
17
18
  import { writeIslandBootstrap } from "./island-bootstrap.js";
19
+ import { writeIslandCanvasBootstrap } from "./island-canvas-bootstrap.js";
18
20
  import { resolveScreenshotPath } from "./screenshot-target.js";
19
21
  import { createDevViteServer } from "./vite-server.js";
20
22
  import { resolveWorkspace } from "./workspace.js";
@@ -25,7 +27,9 @@ import { fileURLToPath } from "node:url";
25
27
  import { createServer } from "node:http";
26
28
 
27
29
  //#region src/cli/dev/server.ts
28
- const BASE_CSS_PATH = fileURLToPath(new URL("../../css/base.css", import.meta.url));
30
+ const BASE_CSS_PATH_BUILT = fileURLToPath(new URL("../../base.css", import.meta.url));
31
+ const BASE_CSS_PATH_SOURCE = fileURLToPath(new URL("../../css/base.css", import.meta.url));
32
+ const BASE_CSS_PATH = existsSync(BASE_CSS_PATH_BUILT) ? BASE_CSS_PATH_BUILT : BASE_CSS_PATH_SOURCE;
29
33
  const DEV_CLIENT_DIR = join(findPackageRoot(fileURLToPath(import.meta.url)), "dist", "dev-client");
30
34
  const ASSET_MIME = {
31
35
  ".js": "text/javascript; charset=utf-8",
@@ -64,6 +68,7 @@ async function startDevServer(opts) {
64
68
  const cssCache = /* @__PURE__ */ new Map();
65
69
  const sectionAssets = /* @__PURE__ */ new Map();
66
70
  const islandBootstrapUrl = await writeIslandBootstrap(workspace.root);
71
+ const canvasIslandBootstrapUrl = await writeIslandCanvasBootstrap(workspace.root);
67
72
  vite.watcher.on("change", () => {
68
73
  vite.moduleGraph.invalidateAll();
69
74
  cssCache.clear();
@@ -77,7 +82,9 @@ async function startDevServer(opts) {
77
82
  port: opts.port,
78
83
  cssCache,
79
84
  sectionAssets,
80
- islandBootstrapUrl
85
+ islandBootstrapUrl,
86
+ canvasIslandBootstrapUrl,
87
+ diagnosticsCmd: opts.diagnosticsCmd ?? null
81
88
  };
82
89
  const server = createServer((req, res) => {
83
90
  handle(req, res, ctx).catch((err) => {
@@ -115,6 +122,7 @@ async function handle(req, res, ctx) {
115
122
  if (path.startsWith("/section-assets/")) return sendSectionAsset(ctx, path, res);
116
123
  if (path === "/__hmr") return ctx.hmr.attach(res);
117
124
  if (path === "/api/inspect") return sendInspect(ctx, url, res);
125
+ if (path === "/api/diagnostics") return sendDiagnostics(ctx, url, res);
118
126
  if (path === "/api/screenshot") return sendScreenshot(ctx, url, res);
119
127
  if (path === "/api/render") return sendRenderPayload(ctx, url, req, res);
120
128
  if (path === "/api/template-schema") return sendTemplateSchema(ctx, url, res);
@@ -154,6 +162,7 @@ async function sendTemplatePage(ctx, templateKey, canvas, res) {
154
162
  workspace: ctx.workspace,
155
163
  template,
156
164
  islandBootstrapUrl: ctx.islandBootstrapUrl,
165
+ canvasIslandBootstrapUrl: ctx.canvasIslandBootstrapUrl,
157
166
  canvas
158
167
  }));
159
168
  }
@@ -208,6 +217,7 @@ async function renderSectionPage(ctx, match, overrides) {
208
217
  sectionHtml: html,
209
218
  cssRev: "0",
210
219
  template: match.template,
220
+ section: match.section,
211
221
  islands,
212
222
  bootstrapUrl: ctx.islandBootstrapUrl
213
223
  });
@@ -236,6 +246,20 @@ async function sendInspect(ctx, url, res) {
236
246
  withOutline: url.searchParams.get("format") === "text"
237
247
  }));
238
248
  }
249
+ async function sendDiagnostics(ctx, url, res) {
250
+ const template = url.searchParams.get("template");
251
+ const section = url.searchParams.get("section");
252
+ if (!template || !section) {
253
+ res.statusCode = 400;
254
+ res.setHeader("content-type", "text/plain");
255
+ res.end("diagnostics requires template and section query params");
256
+ return;
257
+ }
258
+ sendJson(res, await runDiagnostics(ctx.diagnosticsCmd, {
259
+ template,
260
+ section
261
+ }));
262
+ }
239
263
  const MAX_POST_OVERRIDE_BYTES = 1024 * 1024;
240
264
  async function readJsonBody(req, maxBytes) {
241
265
  const chunks = [];