@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 +61 -0
- package/dist/cli/check/loader.js +9 -6
- package/dist/cli/dev/build-css.js +11 -5
- package/dist/cli/dev/compose-template.js +3 -2
- package/dist/cli/dev/diagnostics.js +68 -0
- package/dist/cli/dev/index.js +12 -2
- package/dist/cli/dev/island-canvas-bootstrap.js +172 -0
- package/dist/cli/dev/section-page.js +38 -1
- package/dist/cli/dev/server.js +26 -2
- package/dist/cli/dev/tsconfig-paths-plugin.js +90 -0
- package/dist/cli/dev/vite-server.js +7 -1
- package/dist/dev-client/assets/index-3Gn34X_P.js +50 -0
- package/dist/dev-client/assets/index-DxQeiHfu.css +1 -0
- package/dist/dev-client/index.html +2 -2
- package/dist/island-runtime.js +1 -1
- package/dist/package.js +1 -1
- package/guide/dev.md +70 -27
- package/guide/islands.md +23 -4
- package/package.json +1 -1
- package/dist/dev-client/assets/index-DfEfrp6P.css +0 -1
- package/dist/dev-client/assets/index-bpMP0b0W.js +0 -50
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
|
package/dist/cli/check/loader.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { loadEsbuild } from "./resolve-tool.js";
|
|
2
|
-
import { ASSET_LOADERS, ASSET_NAMES
|
|
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("/"))
|
|
65
|
-
|
|
66
|
-
|
|
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(
|
|
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
|
-
|
|
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 };
|
package/dist/cli/dev/index.js
CHANGED
|
@@ -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
|
|
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}§ion=${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 };
|
package/dist/cli/dev/server.js
CHANGED
|
@@ -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
|
|
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 = [];
|