@homepages/template-kit 0.9.0-dev-20260719224319 → 0.9.0
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 +29 -59
- package/README.md +14 -4
- package/dist/contracts/fill-treatments.d.ts +23 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.js +2 -2
- package/dist/islands/discover.js +1 -1
- package/dist/islands/esbuild-plugin.js +1 -1
- package/dist/islands/wrap.d.ts +9 -1
- package/dist/media/resolve-fixture-media.d.ts +22 -0
- package/dist/package.js +1 -1
- package/dist/schema/resolve-section-ref.d.ts +4 -0
- package/dist/schema/rows.d.ts +9 -0
- package/dist/schema/section-nav.d.ts +5 -1
- package/dist/schema/slot-types.d.ts +1 -1
- package/dist/ssr.d.ts +7 -2
- package/dist/ssr.js +6 -2
- package/guide/INDEX.md +5 -0
- package/guide/check.md +1 -0
- package/guide/dev.md +1 -0
- package/guide/llms.txt +11 -3
- package/guide/upgrading.md +12 -8
- package/package.json +4 -16
- package/dist/canvas-core/chrome-css.d.ts +0 -24
- package/dist/canvas-core/chrome-css.js +0 -403
- package/dist/canvas-core/chrome.d.ts +0 -49
- package/dist/canvas-core/chrome.js +0 -147
- package/dist/canvas-core/frame.d.ts +0 -20
- package/dist/canvas-core/frame.js +0 -23
- package/dist/canvas-core/hidden-tabs.d.ts +0 -16
- package/dist/canvas-core/hidden-tabs.js +0 -84
- package/dist/canvas-core/resolve-target.js +0 -55
- package/dist/canvas-core/scroll.d.ts +0 -10
- package/dist/canvas-core/scroll.js +0 -15
- package/dist/canvas-core/section-dom.d.ts +0 -6
- package/dist/canvas-core/section-dom.js +0 -43
- package/dist/canvas-core/srcdoc.d.ts +0 -8
- package/dist/canvas-core/srcdoc.js +0 -77
- package/dist/canvas-core/types.d.ts +0 -76
- package/dist/canvas-core.d.ts +0 -9
- package/dist/canvas-core.js +0 -9
- package/dist/cli/check/config.js +0 -41
- package/dist/cli/check/css.js +0 -131
- package/dist/cli/check/diagnostics.js +0 -37
- package/dist/cli/check/index.js +0 -94
- package/dist/cli/check/loader.js +0 -149
- package/dist/cli/check/paths.js +0 -15
- package/dist/cli/check/relativize.js +0 -18
- package/dist/cli/check/render-invariants.js +0 -24
- package/dist/cli/check/resolve-tool.js +0 -38
- package/dist/cli/check/section-assets.js +0 -58
- package/dist/cli/check/stages/deps.js +0 -337
- package/dist/cli/check/stages/lint.js +0 -46
- package/dist/cli/check/stages/render.js +0 -98
- package/dist/cli/check/stages/size.js +0 -274
- package/dist/cli/check/stages/tree.js +0 -206
- package/dist/cli/check/stages/typecheck.js +0 -46
- package/dist/cli/check/stages/validate/catalog-exhaustiveness.js +0 -18
- package/dist/cli/check/stages/validate/facts.js +0 -19
- package/dist/cli/check/stages/validate/formatter-contract.js +0 -14
- package/dist/cli/check/stages/validate/group-contract.js +0 -21
- package/dist/cli/check/stages/validate/index.js +0 -66
- package/dist/cli/check/stages/validate/list-scalar-contract.js +0 -22
- package/dist/cli/check/stages/validate/nav-contract.js +0 -93
- package/dist/cli/check/stages/validate/nested-slot-contract.js +0 -12
- package/dist/cli/check/stages/validate/object-list-contract.js +0 -31
- package/dist/cli/check/stages/validate/orchestrator.js +0 -241
- package/dist/cli/check/stages/validate/produced-by-contract.js +0 -18
- package/dist/cli/check/stages/validate/row-contract.js +0 -31
- package/dist/cli/check/stages/validate/select-contract.js +0 -13
- package/dist/cli/check/stages/validate/source-contract.js +0 -21
- package/dist/cli/check/stages/validate/variant-contract.js +0 -15
- package/dist/cli/check/workspace.js +0 -99
- package/dist/cli/dev/build-css.js +0 -97
- package/dist/cli/dev/catalog.js +0 -35
- package/dist/cli/dev/compose-template.js +0 -39
- package/dist/cli/dev/content-override.js +0 -58
- package/dist/cli/dev/diagnostics.js +0 -68
- package/dist/cli/dev/discover.js +0 -35
- package/dist/cli/dev/fill-state.js +0 -77
- package/dist/cli/dev/index.js +0 -42
- package/dist/cli/dev/inspect.js +0 -38
- package/dist/cli/dev/island-bootstrap.js +0 -38
- package/dist/cli/dev/island-canvas-bootstrap.js +0 -204
- package/dist/cli/dev/island-map.js +0 -35
- package/dist/cli/dev/island-transform.js +0 -31
- package/dist/cli/dev/manifest-instances.js +0 -31
- package/dist/cli/dev/render-section.js +0 -24
- package/dist/cli/dev/screenshot-target.js +0 -24
- package/dist/cli/dev/section-page.js +0 -91
- package/dist/cli/dev/server.js +0 -511
- package/dist/cli/dev/slot-schema.js +0 -12
- package/dist/cli/dev/structure-summary.js +0 -119
- package/dist/cli/dev/tailwind.js +0 -32
- package/dist/cli/dev/tsconfig-paths-plugin.js +0 -90
- package/dist/cli/dev/vite-server.js +0 -39
- package/dist/cli/dev/workspace.js +0 -63
- package/dist/cli/link/index.js +0 -74
- package/dist/cli/link/overlay.js +0 -59
- package/dist/cli/link/watch.js +0 -25
- package/dist/cli/new/emit.js +0 -48
- package/dist/cli/new/index.js +0 -69
- package/dist/cli/new/scaffold/section/_Renderer.tsx +0 -20
- package/dist/cli/new/scaffold/section/_fill-spec.ts +0 -18
- package/dist/cli/new/scaffold/section/_fixtures.ts +0 -13
- package/dist/cli/new/scaffold/section/_schema.ts +0 -24
- package/dist/cli/new/scaffold/template/_manifest.json +0 -9
- package/dist/cli/new/scaffold/template/sections/hero/ExpandableText.tsx +0 -45
- package/dist/cli/new/scaffold/template/sections/hero/Renderer.tsx +0 -40
- package/dist/cli/new/scaffold/template/sections/hero/components/Feature.tsx +0 -14
- package/dist/cli/new/scaffold/template/sections/hero/fill-spec.ts +0 -29
- package/dist/cli/new/scaffold/template/sections/hero/fixtures.ts +0 -41
- package/dist/cli/new/scaffold/template/sections/hero/schema.ts +0 -60
- package/dist/cli/new/scaffold/template/theme.css +0 -24
- package/dist/cli/new/scaffold/template/theme.ts +0 -27
- package/dist/cli/new/scaffold-assets.js +0 -20
- package/dist/cli/pack/collect.js +0 -44
- package/dist/cli/pack/guards.js +0 -57
- package/dist/cli/pack/index.js +0 -66
- package/dist/cli/pack/manifest.js +0 -15
- package/dist/cli/pack/zip.js +0 -86
- package/dist/cli/theme/generate.js +0 -43
- package/dist/cli/theme/index.js +0 -33
- package/dist/cli/theme/load-theme.js +0 -69
- package/dist/cli.d.ts +0 -1
- package/dist/cli.js +0 -138
- package/dist/dev-client/assets/index-CJa9G1Na.css +0 -1
- package/dist/dev-client/assets/index-C_1atZs4.js +0 -450
- package/dist/dev-client/index.html +0 -13
- package/dist/tokens.css +0 -21
|
@@ -1,274 +0,0 @@
|
|
|
1
|
-
import { loadEsbuild } from "../resolve-tool.js";
|
|
2
|
-
import { ASSET_LOADERS, ASSET_NAMES, ASSET_PUBLIC_PATH, compileSectionPackageCss, sectionCssFiles } from "../css.js";
|
|
3
|
-
import { CHECK_CONFIG } from "../config.js";
|
|
4
|
-
import { findSectionIslands } from "../../../islands/discover.js";
|
|
5
|
-
import { readFile } from "node:fs/promises";
|
|
6
|
-
import { basename, join, relative } from "node:path";
|
|
7
|
-
import { existsSync } from "node:fs";
|
|
8
|
-
import { gzipSync } from "node:zlib";
|
|
9
|
-
|
|
10
|
-
//#region src/cli/check/stages/size.ts
|
|
11
|
-
const RESOLVING = Symbol("size-stub:resolving");
|
|
12
|
-
/**
|
|
13
|
-
* A CSS-only stub, local to this file and NOT `loader.ts`'s `cssStub`: that
|
|
14
|
-
* plugin marks *every* bare specifier external (right for evaluating a
|
|
15
|
-
* section — it must resolve the workspace's real React and real kit
|
|
16
|
-
* primitives, not a second copy), which is exactly wrong for measurement,
|
|
17
|
-
* where an npm package's bundled weight is the entire point of the gate.
|
|
18
|
-
* Only CSS is stubbed here; everything else is left to the `build()` call's
|
|
19
|
-
* own `external` option (see `measureRendererBundle`) plus esbuild's default
|
|
20
|
-
* bundling.
|
|
21
|
-
*
|
|
22
|
-
* Detects a CSS import by resolving the specifier and inspecting the
|
|
23
|
-
* *resolved path's* extension — not by pattern-matching the specifier text.
|
|
24
|
-
* A CSS-only package need not say so in its specifier (`@fontsource/inter`
|
|
25
|
-
* has no `.css` suffix and no `css` path segment; `swiper/css` has no `.css`
|
|
26
|
-
* suffix either), so only real resolution tells the two apart. `pluginData`
|
|
27
|
-
* is the standard recursion guard for `build.resolve()` re-entering this same
|
|
28
|
-
* hook (esbuild docs' own example for this pattern) — mirrors `loader.ts`'s
|
|
29
|
-
* `cssStub`, which learned this the hard way.
|
|
30
|
-
*/
|
|
31
|
-
const sizeStub = {
|
|
32
|
-
name: "size-stub",
|
|
33
|
-
setup(b) {
|
|
34
|
-
b.onResolve({ filter: /.*/ }, async (args) => {
|
|
35
|
-
if (args.pluginData === RESOLVING) return null;
|
|
36
|
-
const resolved = await b.resolve(args.path, {
|
|
37
|
-
importer: args.importer,
|
|
38
|
-
namespace: args.namespace,
|
|
39
|
-
resolveDir: args.resolveDir,
|
|
40
|
-
kind: args.kind,
|
|
41
|
-
pluginData: RESOLVING
|
|
42
|
-
});
|
|
43
|
-
if (resolved.errors.length === 0 && resolved.path !== "" && resolved.path.endsWith(".css")) return {
|
|
44
|
-
path: resolved.path,
|
|
45
|
-
namespace: "size-stub"
|
|
46
|
-
};
|
|
47
|
-
return null;
|
|
48
|
-
});
|
|
49
|
-
b.onLoad({
|
|
50
|
-
filter: /.*/,
|
|
51
|
-
namespace: "size-stub"
|
|
52
|
-
}, () => ({
|
|
53
|
-
contents: "",
|
|
54
|
-
loader: "js"
|
|
55
|
-
}));
|
|
56
|
-
}
|
|
57
|
-
};
|
|
58
|
-
/**
|
|
59
|
-
* Gzipped bytes of a section's renderer bundle, its own code *and every npm
|
|
60
|
-
* package it imports* — the published bundle's actual shape. React alone is
|
|
61
|
-
* excluded: it is a peer the platform supplies at render time, never bundled
|
|
62
|
-
* into a section. Everything else, including `@homepages/template-kit`
|
|
63
|
-
* itself (its primitives are ordinary React components that ship inside the
|
|
64
|
-
* section bundle, not a peer), is bundled and counted — that is the entire
|
|
65
|
-
* point of this budget. esbuilds `Renderer.tsx` alone (not the other three
|
|
66
|
-
* contract files — schema, fill-spec and fixtures never reach the browser).
|
|
67
|
-
*
|
|
68
|
-
* `external` is deliberately the five React entry points a section can
|
|
69
|
-
* actually reach, not `packages: "external"`: the latter would externalize
|
|
70
|
-
* every bare specifier — including the icon library or schema library this
|
|
71
|
-
* gate exists to catch — reproducing the exact bug this function fixes. Do
|
|
72
|
-
* not "simplify" this back to `packages: "external"`.
|
|
73
|
-
*
|
|
74
|
-
* `platform: "browser"`, not `"neutral"`: `neutral` defaults `mainFields` to
|
|
75
|
-
* `[]`, which cannot resolve a CommonJS-only package's `main` field — and
|
|
76
|
-
* real sections import such packages (an icon library, for instance). With
|
|
77
|
-
* `neutral` the measurement simply fails for those sections; `browser`'s
|
|
78
|
-
* default `mainFields` resolves them.
|
|
79
|
-
*/
|
|
80
|
-
async function measureRendererBundle(workspaceRoot, section) {
|
|
81
|
-
const { build } = await loadEsbuild(workspaceRoot);
|
|
82
|
-
const js = (await build({
|
|
83
|
-
entryPoints: [join(section.dir, "Renderer.tsx")],
|
|
84
|
-
bundle: true,
|
|
85
|
-
minify: true,
|
|
86
|
-
format: "esm",
|
|
87
|
-
platform: "browser",
|
|
88
|
-
jsx: "automatic",
|
|
89
|
-
write: false,
|
|
90
|
-
outfile: "renderer.js",
|
|
91
|
-
external: [
|
|
92
|
-
"react",
|
|
93
|
-
"react-dom",
|
|
94
|
-
"react-dom/server",
|
|
95
|
-
"react/jsx-runtime",
|
|
96
|
-
"react/jsx-dev-runtime"
|
|
97
|
-
],
|
|
98
|
-
plugins: [sizeStub],
|
|
99
|
-
absWorkingDir: workspaceRoot,
|
|
100
|
-
logLevel: "silent"
|
|
101
|
-
})).outputFiles.find((f) => f.path.endsWith(".js"));
|
|
102
|
-
if (!js) throw new Error(`esbuild produced no JS output for ${section.dir}`);
|
|
103
|
-
return gzipSync(js.contents).byteLength;
|
|
104
|
-
}
|
|
105
|
-
/**
|
|
106
|
-
* Gzipped bytes of a section's stylesheet: its hand-written CSS
|
|
107
|
-
* (`sectionCssFiles`, published order) concatenated with the package CSS its
|
|
108
|
-
* code pulls in (`compileSectionPackageCss`). A package's `url()`-referenced
|
|
109
|
-
* fonts and icons no longer inline into this text — they resolve to
|
|
110
|
-
* `/section-assets/` references (see css.ts), and their bytes are measured
|
|
111
|
-
* separately by `measureAssetBytes`, the `size-assets` dimension below.
|
|
112
|
-
*/
|
|
113
|
-
async function measureSectionCss(workspaceRoot, section) {
|
|
114
|
-
const ownFiles = await sectionCssFiles(section.dir);
|
|
115
|
-
const own = (await Promise.all(ownFiles.map((f) => readFile(f, "utf8")))).join("\n");
|
|
116
|
-
const packageCss = await compileSectionPackageCss(workspaceRoot, section.dir);
|
|
117
|
-
return gzipSync(Buffer.from(`${own}\n${packageCss}`)).byteLength;
|
|
118
|
-
}
|
|
119
|
-
/**
|
|
120
|
-
* Raw bytes of a section's static assets — the fourth size dimension. Fonts and
|
|
121
|
-
* images are already compressed, so this is RAW bytes, not gzip. Two esbuild
|
|
122
|
-
* passes with the file loader capture every asset the published section emits:
|
|
123
|
-
* 1. Renderer.tsx (bundled) — assets imported in JS AND assets url()-referenced
|
|
124
|
-
* by the package CSS the renderer pulls in.
|
|
125
|
-
* 2. each hand-written stylesheet — its @font-face / url() assets (the author's
|
|
126
|
-
* own styles.css is read raw by the CSS budget, never bundled, so its assets
|
|
127
|
-
* surface only here).
|
|
128
|
-
* Deduped by emitted (content-hashed) filename, so an asset referenced twice or
|
|
129
|
-
* from both a stylesheet and JS is counted once.
|
|
130
|
-
*/
|
|
131
|
-
async function measureAssetBytes(workspaceRoot, section) {
|
|
132
|
-
const { build } = await loadEsbuild(workspaceRoot);
|
|
133
|
-
const byName = /* @__PURE__ */ new Map();
|
|
134
|
-
const collect = (outputFiles) => {
|
|
135
|
-
for (const f of outputFiles) {
|
|
136
|
-
if (/\.(?:js|css|map)$/i.test(f.path)) continue;
|
|
137
|
-
byName.set(basename(f.path), f.contents.byteLength);
|
|
138
|
-
}
|
|
139
|
-
};
|
|
140
|
-
collect((await build({
|
|
141
|
-
entryPoints: [join(section.dir, "Renderer.tsx")],
|
|
142
|
-
bundle: true,
|
|
143
|
-
write: false,
|
|
144
|
-
format: "esm",
|
|
145
|
-
platform: "browser",
|
|
146
|
-
jsx: "automatic",
|
|
147
|
-
outdir: "dist",
|
|
148
|
-
external: [
|
|
149
|
-
"react",
|
|
150
|
-
"react-dom",
|
|
151
|
-
"react-dom/server",
|
|
152
|
-
"react/jsx-runtime",
|
|
153
|
-
"react/jsx-dev-runtime"
|
|
154
|
-
],
|
|
155
|
-
loader: { ...ASSET_LOADERS },
|
|
156
|
-
publicPath: "/section-assets",
|
|
157
|
-
assetNames: "[name]-[hash]",
|
|
158
|
-
absWorkingDir: workspaceRoot,
|
|
159
|
-
logLevel: "silent"
|
|
160
|
-
})).outputFiles ?? []);
|
|
161
|
-
for (const cssFile of await sectionCssFiles(section.dir)) collect((await build({
|
|
162
|
-
entryPoints: [cssFile],
|
|
163
|
-
bundle: true,
|
|
164
|
-
write: false,
|
|
165
|
-
outdir: "dist",
|
|
166
|
-
loader: {
|
|
167
|
-
".css": "css",
|
|
168
|
-
...ASSET_LOADERS
|
|
169
|
-
},
|
|
170
|
-
publicPath: "/section-assets",
|
|
171
|
-
assetNames: "[name]-[hash]",
|
|
172
|
-
absWorkingDir: workspaceRoot,
|
|
173
|
-
logLevel: "silent"
|
|
174
|
-
})).outputFiles ?? []);
|
|
175
|
-
return [...byName.values()].reduce((sum, n) => sum + n, 0);
|
|
176
|
-
}
|
|
177
|
-
/**
|
|
178
|
-
* Gzipped bytes of a section's largest island (browser) bundle. Each "use client"
|
|
179
|
-
* component is bundled as its own browser entry, React external (the runtime chunk
|
|
180
|
-
* provides it), everything else bundled — the shape ci/static-assets.ts uploads.
|
|
181
|
-
* Returns 0 for a section with no islands. Uses `sizeStub` (React-only external),
|
|
182
|
-
* NOT the loader's cssStub — see the file header.
|
|
183
|
-
*/
|
|
184
|
-
async function measureIslandBundle(workspaceRoot, section) {
|
|
185
|
-
const islands = await findSectionIslands(section.dir);
|
|
186
|
-
if (islands.length === 0) return 0;
|
|
187
|
-
const { build } = await loadEsbuild(workspaceRoot);
|
|
188
|
-
let max = 0;
|
|
189
|
-
for (const entry of islands) {
|
|
190
|
-
const js = (await build({
|
|
191
|
-
entryPoints: [entry],
|
|
192
|
-
bundle: true,
|
|
193
|
-
minify: true,
|
|
194
|
-
format: "esm",
|
|
195
|
-
platform: "browser",
|
|
196
|
-
jsx: "automatic",
|
|
197
|
-
write: false,
|
|
198
|
-
outfile: "island.js",
|
|
199
|
-
external: [
|
|
200
|
-
"react",
|
|
201
|
-
"react-dom",
|
|
202
|
-
"react-dom/client",
|
|
203
|
-
"react/jsx-runtime",
|
|
204
|
-
"react/jsx-dev-runtime"
|
|
205
|
-
],
|
|
206
|
-
plugins: [sizeStub],
|
|
207
|
-
absWorkingDir: workspaceRoot,
|
|
208
|
-
logLevel: "silent"
|
|
209
|
-
})).outputFiles.find((f) => f.path.endsWith(".js"));
|
|
210
|
-
if (js) max = Math.max(max, gzipSync(js.contents).byteLength);
|
|
211
|
-
}
|
|
212
|
-
return max;
|
|
213
|
-
}
|
|
214
|
-
function kb(bytes) {
|
|
215
|
-
const value = bytes / 1024;
|
|
216
|
-
return `${Number.isInteger(value) ? value.toString() : value.toFixed(1)} KB`;
|
|
217
|
-
}
|
|
218
|
-
function cssDiagnosticFile(workspaceRoot, section) {
|
|
219
|
-
const stylesCss = join(section.dir, "styles.css");
|
|
220
|
-
return relative(workspaceRoot, existsSync(stylesCss) ? stylesCss : section.dir);
|
|
221
|
-
}
|
|
222
|
-
async function sizeStage(workspaceRoot, template) {
|
|
223
|
-
const diagnostics = [];
|
|
224
|
-
for (const section of template.sections) {
|
|
225
|
-
let rendererBytes;
|
|
226
|
-
let cssBytes;
|
|
227
|
-
let islandBytes;
|
|
228
|
-
let assetBytes;
|
|
229
|
-
try {
|
|
230
|
-
rendererBytes = await measureRendererBundle(workspaceRoot, section);
|
|
231
|
-
cssBytes = await measureSectionCss(workspaceRoot, section);
|
|
232
|
-
islandBytes = await measureIslandBundle(workspaceRoot, section);
|
|
233
|
-
assetBytes = await measureAssetBytes(workspaceRoot, section);
|
|
234
|
-
} catch {
|
|
235
|
-
continue;
|
|
236
|
-
}
|
|
237
|
-
if (rendererBytes > CHECK_CONFIG.size.rendererBundleGzip) diagnostics.push({
|
|
238
|
-
ruleId: "template-kit/size-renderer-bundle",
|
|
239
|
-
template: template.key,
|
|
240
|
-
section: section.name,
|
|
241
|
-
file: `templates/${template.key}/sections/${section.name}/Renderer.tsx`,
|
|
242
|
-
message: `renderer bundle is ${kb(rendererBytes)} gzip — over the ${kb(CHECK_CONFIG.size.rendererBundleGzip)} budget`,
|
|
243
|
-
fix: "Trim what the renderer pulls in. A whole icon library imported for two icons is the usual cause — import only the icons you use, or inline them as SVG."
|
|
244
|
-
});
|
|
245
|
-
if (cssBytes > CHECK_CONFIG.size.sectionCssGzip) diagnostics.push({
|
|
246
|
-
ruleId: "template-kit/size-section-css",
|
|
247
|
-
template: template.key,
|
|
248
|
-
section: section.name,
|
|
249
|
-
file: cssDiagnosticFile(workspaceRoot, section),
|
|
250
|
-
message: `section stylesheet is ${kb(cssBytes)} gzip — over the ${kb(CHECK_CONFIG.size.sectionCssGzip)} budget`,
|
|
251
|
-
fix: "Trim the stylesheet. Pulling in a package's full 'everything' CSS bundle when you use a few of its modules is the usual cause — import only the modules you use. (A url()-referenced font or icon set is measured separately, under size-assets.)"
|
|
252
|
-
});
|
|
253
|
-
if (islandBytes > CHECK_CONFIG.size.islandBundleGzip) diagnostics.push({
|
|
254
|
-
ruleId: "template-kit/size-island-bundle",
|
|
255
|
-
template: template.key,
|
|
256
|
-
section: section.name,
|
|
257
|
-
file: `templates/${template.key}/sections/${section.name}`,
|
|
258
|
-
message: `an island bundle is ${kb(islandBytes)} gzip — over the ${kb(CHECK_CONFIG.size.islandBundleGzip)} budget`,
|
|
259
|
-
fix: "Trim what the client component pulls in. A whole UI/icon library imported for one widget is the usual cause — import only what you use, or split the island."
|
|
260
|
-
});
|
|
261
|
-
if (assetBytes > CHECK_CONFIG.size.assets) diagnostics.push({
|
|
262
|
-
ruleId: "template-kit/size-assets",
|
|
263
|
-
template: template.key,
|
|
264
|
-
section: section.name,
|
|
265
|
-
file: `templates/${template.key}/sections/${section.name}`,
|
|
266
|
-
message: `static assets are ${kb(assetBytes)} — over the ${kb(CHECK_CONFIG.size.assets)} budget`,
|
|
267
|
-
fix: "Shrink or subset the asset (a full font family or an unoptimized image is the usual cause), or drop assets the section does not use."
|
|
268
|
-
});
|
|
269
|
-
}
|
|
270
|
-
return diagnostics;
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
//#endregion
|
|
274
|
-
export { measureAssetBytes, measureIslandBundle, measureRendererBundle, measureSectionCss, sizeStage };
|
|
@@ -1,206 +0,0 @@
|
|
|
1
|
-
import { ASSET_LOADERS, findImportLine, sectionCssFiles } from "../css.js";
|
|
2
|
-
import { CONTRACT_FILES, loadSection } from "../loader.js";
|
|
3
|
-
import { flattenLayout } from "../../../schema/rows.js";
|
|
4
|
-
import { referencedSectionAssets } from "../section-assets.js";
|
|
5
|
-
import { readFile, readdir } from "node:fs/promises";
|
|
6
|
-
import { extname, join, relative } from "node:path";
|
|
7
|
-
|
|
8
|
-
//#region src/cli/check/stages/tree.ts
|
|
9
|
-
const ASSET_EXTENSIONS = new Set(Object.keys(ASSET_LOADERS));
|
|
10
|
-
async function isBinaryFile(file) {
|
|
11
|
-
return (await readFile(file)).subarray(0, 4096).includes(0);
|
|
12
|
-
}
|
|
13
|
-
/** Every authored file in the bundle, recursive. `snapshots/` and `node_modules/` are skipped — generated or foreign, never authored. */
|
|
14
|
-
async function bundleFiles(dir) {
|
|
15
|
-
const out = [];
|
|
16
|
-
for (const entry of await readdir(dir, { withFileTypes: true })) {
|
|
17
|
-
const full = join(dir, entry.name);
|
|
18
|
-
if (entry.isDirectory()) {
|
|
19
|
-
if (entry.name === "snapshots" || entry.name === "node_modules") continue;
|
|
20
|
-
out.push(...await bundleFiles(full));
|
|
21
|
-
} else if (entry.isFile()) out.push(full);
|
|
22
|
-
}
|
|
23
|
-
return out.sort();
|
|
24
|
-
}
|
|
25
|
-
/**
|
|
26
|
-
* The bundle's authored JSX surface: every `.tsx` except tests. An author may
|
|
27
|
-
* put markup anywhere in the folder (components/, hooks/, …), so the marker
|
|
28
|
-
* rules scan all of these, not just Renderer.tsx. Tests are excluded
|
|
29
|
-
* deliberately — a test's expected-markup assertion must not satisfy a slot
|
|
30
|
-
* marker, and fixtures legitimately write raw markup.
|
|
31
|
-
*/
|
|
32
|
-
async function bundleJsxFiles(dir) {
|
|
33
|
-
return (await bundleFiles(dir)).filter((f) => f.endsWith(".tsx") && !/\.test\.tsx$/.test(f));
|
|
34
|
-
}
|
|
35
|
-
function escapeRegExp(s) {
|
|
36
|
-
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
37
|
-
}
|
|
38
|
-
const CSS_REASON = /css-reason:/;
|
|
39
|
-
function opensWithCssReason(text) {
|
|
40
|
-
const lead = text.match(/^\s*\/\*([\s\S]*?)\*\//);
|
|
41
|
-
return lead != null && CSS_REASON.test(lead[1] ?? "");
|
|
42
|
-
}
|
|
43
|
-
async function checkBundleAnatomy(workspaceRoot, template, section) {
|
|
44
|
-
const out = [];
|
|
45
|
-
const entries = await readdir(section.dir, { withFileTypes: true });
|
|
46
|
-
for (const f of CONTRACT_FILES) {
|
|
47
|
-
if (entries.some((entry) => entry.isFile() && entry.name === f)) continue;
|
|
48
|
-
out.push({
|
|
49
|
-
ruleId: "template-kit/bundle-incomplete",
|
|
50
|
-
template: template.key,
|
|
51
|
-
section: section.name,
|
|
52
|
-
file: relative(workspaceRoot, join(section.dir, f)),
|
|
53
|
-
line: 1,
|
|
54
|
-
message: `Section "${section.name}" is missing the required contract file \`${f}\`.`,
|
|
55
|
-
fix: "Every section needs Renderer.tsx, schema.ts, fill-spec.ts and fixtures.ts. Add the missing file."
|
|
56
|
-
});
|
|
57
|
-
}
|
|
58
|
-
return out;
|
|
59
|
-
}
|
|
60
|
-
/** A section-folder file that is an asset by extension or by a NUL-byte sniff. */
|
|
61
|
-
async function sectionAssetFiles(dir) {
|
|
62
|
-
const out = [];
|
|
63
|
-
for (const abs of await bundleFiles(dir)) if (ASSET_EXTENSIONS.has(extname(abs).toLowerCase()) || await isBinaryFile(abs)) out.push(abs);
|
|
64
|
-
return out;
|
|
65
|
-
}
|
|
66
|
-
async function checkOrphanAssets(workspaceRoot, template, section) {
|
|
67
|
-
const candidates = await sectionAssetFiles(section.dir);
|
|
68
|
-
if (candidates.length === 0) return [];
|
|
69
|
-
let referenced;
|
|
70
|
-
try {
|
|
71
|
-
referenced = await referencedSectionAssets(workspaceRoot, section);
|
|
72
|
-
} catch {
|
|
73
|
-
return [];
|
|
74
|
-
}
|
|
75
|
-
const out = [];
|
|
76
|
-
for (const abs of candidates) {
|
|
77
|
-
if (referenced.has(abs)) continue;
|
|
78
|
-
const rel = relative(workspaceRoot, abs);
|
|
79
|
-
out.push({
|
|
80
|
-
ruleId: "template-kit/bundle-binary-asset",
|
|
81
|
-
template: template.key,
|
|
82
|
-
section: section.name,
|
|
83
|
-
file: rel,
|
|
84
|
-
line: 1,
|
|
85
|
-
message: `Static asset \`${rel}\` sits in the section folder but nothing references it.`,
|
|
86
|
-
fix: "Reference it (import it in Renderer.tsx, or url() it from the section's CSS), or delete it. An unreferenced binary bloats the section's content hash and is never served."
|
|
87
|
-
});
|
|
88
|
-
}
|
|
89
|
-
return out;
|
|
90
|
-
}
|
|
91
|
-
function asSchema(loaded) {
|
|
92
|
-
return typeof loaded.schema === "object" && loaded.schema !== null ? loaded.schema : {};
|
|
93
|
-
}
|
|
94
|
-
function checkSlotMarkers(workspaceRoot, template, section, loaded, jsxFiles) {
|
|
95
|
-
const slots = asSchema(loaded).slots ?? {};
|
|
96
|
-
const out = [];
|
|
97
|
-
const rendererFile = relative(workspaceRoot, join(section.dir, "Renderer.tsx"));
|
|
98
|
-
for (const [slot, spec] of Object.entries(slots)) {
|
|
99
|
-
if (spec.editable_by_user === false) continue;
|
|
100
|
-
const re = new RegExp(`(<Slot\\s+(?:[^>]*\\s+)?id|data-slot-id|slotId)\\s*=\\s*["']${escapeRegExp(slot)}["']`);
|
|
101
|
-
if (jsxFiles.some((f) => re.test(f.text))) continue;
|
|
102
|
-
out.push({
|
|
103
|
-
ruleId: "template-kit/missing-slot-marker",
|
|
104
|
-
template: template.key,
|
|
105
|
-
section: section.name,
|
|
106
|
-
file: rendererFile,
|
|
107
|
-
message: `Slot "${slot}" (editable_by_user: true) declared in schema.ts has no matching \`data-slot-id="${slot}"\` or \`slotId="${slot}"\` marker anywhere in the bundle.`,
|
|
108
|
-
fix: `Mark the slot in Renderer.tsx: <Slot id="${slot}">. The id must be a string literal.`
|
|
109
|
-
});
|
|
110
|
-
}
|
|
111
|
-
return out;
|
|
112
|
-
}
|
|
113
|
-
function checkSlotGroupMarkers(workspaceRoot, template, section, loaded, jsxFiles) {
|
|
114
|
-
const groups = asSchema(loaded).meta?.groups ?? [];
|
|
115
|
-
const out = [];
|
|
116
|
-
const rendererFile = relative(workspaceRoot, join(section.dir, "Renderer.tsx"));
|
|
117
|
-
for (const group of groups) {
|
|
118
|
-
if (flattenLayout(group.members).length < 2) continue;
|
|
119
|
-
const re = new RegExp(`<SlotGroup\\s+(?:[^>]*\\s+)?id\\s*=\\s*["']${escapeRegExp(group.id)}["']`);
|
|
120
|
-
if (jsxFiles.some((f) => re.test(f.text))) continue;
|
|
121
|
-
out.push({
|
|
122
|
-
ruleId: "template-kit/slot-group-marker",
|
|
123
|
-
template: template.key,
|
|
124
|
-
section: section.name,
|
|
125
|
-
file: rendererFile,
|
|
126
|
-
message: `Group "${group.id}" in schema.ts has no matching <SlotGroup id="${group.id}"> anywhere in the bundle.`,
|
|
127
|
-
fix: `Wrap the group's slots in <SlotGroup id="${group.id}"> so the editor can present them together.`
|
|
128
|
-
});
|
|
129
|
-
}
|
|
130
|
-
return out;
|
|
131
|
-
}
|
|
132
|
-
async function checkSectionCss(workspaceRoot, template, section) {
|
|
133
|
-
const out = [];
|
|
134
|
-
for (const cssFile of await sectionCssFiles(section.dir)) {
|
|
135
|
-
const rel = relative(workspaceRoot, cssFile);
|
|
136
|
-
const text = await readFile(cssFile, "utf8");
|
|
137
|
-
if (!opensWithCssReason(text)) out.push({
|
|
138
|
-
ruleId: "template-kit/css-reason",
|
|
139
|
-
template: template.key,
|
|
140
|
-
section: section.name,
|
|
141
|
-
file: rel,
|
|
142
|
-
line: 1,
|
|
143
|
-
message: `${rel} must open with a \`css-reason:\` comment justifying the hand-CSS.`,
|
|
144
|
-
fix: "Open the file with a comment saying why it exists: /* css-reason: … */"
|
|
145
|
-
});
|
|
146
|
-
for (const m of text.matchAll(/\/\*\s*unlayered:\s*([^*]*)\*\//g)) {
|
|
147
|
-
if ((m[1] ?? "").trim() !== "") continue;
|
|
148
|
-
out.push({
|
|
149
|
-
ruleId: "template-kit/unlayered-fence",
|
|
150
|
-
template: template.key,
|
|
151
|
-
section: section.name,
|
|
152
|
-
file: rel,
|
|
153
|
-
line: text.slice(0, m.index).split("\n").length,
|
|
154
|
-
message: `${rel}: an \`/* unlayered: */\` fence must state why it escapes the cascade layers.`,
|
|
155
|
-
fix: "Give the fence a reason (/* unlayered: … */) and close it with /* endunlayered */."
|
|
156
|
-
});
|
|
157
|
-
}
|
|
158
|
-
const opens = (text.match(/\/\*\s*unlayered:\s*[^*]*\*\//g) ?? []).length;
|
|
159
|
-
const closes = (text.match(/\/\*\s*endunlayered\s*\*\//g) ?? []).length;
|
|
160
|
-
if (opens !== closes) out.push({
|
|
161
|
-
ruleId: "template-kit/unlayered-fence",
|
|
162
|
-
template: template.key,
|
|
163
|
-
section: section.name,
|
|
164
|
-
file: rel,
|
|
165
|
-
line: 1,
|
|
166
|
-
message: `${rel}: each \`/* unlayered: <reason> */\` needs a matching \`/* endunlayered */\` (found ${String(opens)} open, ${String(closes)} close).`,
|
|
167
|
-
fix: "Give the fence a reason (/* unlayered: … */) and close it with /* endunlayered */."
|
|
168
|
-
});
|
|
169
|
-
const importLine = findImportLine(text);
|
|
170
|
-
if (importLine !== void 0) out.push({
|
|
171
|
-
ruleId: "template-kit/no-bare-css-import",
|
|
172
|
-
template: template.key,
|
|
173
|
-
section: section.name,
|
|
174
|
-
file: rel,
|
|
175
|
-
line: importLine,
|
|
176
|
-
message: `${rel}:${String(importLine)} — @import is not allowed in hand-CSS; the browser drops it silently and the rules never reach the page.`,
|
|
177
|
-
fix: "Import package CSS from the code that needs it: import \"swiper/css\" in Renderer.tsx, and the build routes it into this layer for you."
|
|
178
|
-
});
|
|
179
|
-
}
|
|
180
|
-
return out;
|
|
181
|
-
}
|
|
182
|
-
/** The tree-level rules for one template: every static, cross-file section-folder invariant ESLint cannot express. */
|
|
183
|
-
async function treeStage(workspaceRoot, template) {
|
|
184
|
-
const diagnostics = [];
|
|
185
|
-
for (const section of template.sections) {
|
|
186
|
-
diagnostics.push(...await checkBundleAnatomy(workspaceRoot, template, section));
|
|
187
|
-
diagnostics.push(...await checkOrphanAssets(workspaceRoot, template, section));
|
|
188
|
-
let loaded;
|
|
189
|
-
try {
|
|
190
|
-
loaded = await loadSection(workspaceRoot, section);
|
|
191
|
-
} catch {
|
|
192
|
-
loaded = void 0;
|
|
193
|
-
}
|
|
194
|
-
const jsxPaths = await bundleJsxFiles(section.dir);
|
|
195
|
-
if (loaded !== void 0 && jsxPaths.length > 0) {
|
|
196
|
-
const jsxFiles = await Promise.all(jsxPaths.map(async (abs) => ({ text: await readFile(abs, "utf8") })));
|
|
197
|
-
diagnostics.push(...checkSlotMarkers(workspaceRoot, template, section, loaded, jsxFiles));
|
|
198
|
-
diagnostics.push(...checkSlotGroupMarkers(workspaceRoot, template, section, loaded, jsxFiles));
|
|
199
|
-
}
|
|
200
|
-
diagnostics.push(...await checkSectionCss(workspaceRoot, template, section));
|
|
201
|
-
}
|
|
202
|
-
return diagnostics;
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
//#endregion
|
|
206
|
-
export { treeStage };
|
|
@@ -1,46 +0,0 @@
|
|
|
1
|
-
import { resolveFromWorkspace } from "../resolve-tool.js";
|
|
2
|
-
import { isUnder, sectionOf } from "../paths.js";
|
|
3
|
-
import { relativizePaths } from "../relativize.js";
|
|
4
|
-
import { join, relative } from "node:path";
|
|
5
|
-
|
|
6
|
-
//#region src/cli/check/stages/typecheck.ts
|
|
7
|
-
const CONFIG_FIX_HINT = "Fix tsconfig.json. It must extend the kit preset: \"extends\": \"@homepages/template-kit/tsconfig\".";
|
|
8
|
-
async function typecheckStage(workspaceRoot, template) {
|
|
9
|
-
const ts = await resolveFromWorkspace(workspaceRoot, "typescript");
|
|
10
|
-
const configPath = join(workspaceRoot, "tsconfig.json");
|
|
11
|
-
const configFile = ts.readConfigFile(configPath, (file) => ts.sys.readFile(file));
|
|
12
|
-
if (configFile.error) throw new Error(`could not read ${configPath}: ${ts.flattenDiagnosticMessageText(configFile.error.messageText, " ")}`);
|
|
13
|
-
const parsed = ts.parseJsonConfigFileContent(configFile.config, ts.sys, workspaceRoot);
|
|
14
|
-
const out = [];
|
|
15
|
-
for (const configError of parsed.errors) out.push({
|
|
16
|
-
ruleId: "template-kit/typecheck",
|
|
17
|
-
template: template.key,
|
|
18
|
-
file: relative(workspaceRoot, configPath),
|
|
19
|
-
message: relativizePaths(ts.flattenDiagnosticMessageText(configError.messageText, " "), workspaceRoot),
|
|
20
|
-
fix: CONFIG_FIX_HINT
|
|
21
|
-
});
|
|
22
|
-
const program = ts.createProgram({
|
|
23
|
-
rootNames: parsed.fileNames,
|
|
24
|
-
options: parsed.options,
|
|
25
|
-
configFileParsingDiagnostics: parsed.errors
|
|
26
|
-
});
|
|
27
|
-
const tsDiagnostics = [...program.getSyntacticDiagnostics(), ...program.getSemanticDiagnostics()];
|
|
28
|
-
for (const diagnostic of tsDiagnostics) {
|
|
29
|
-
if (!diagnostic.file) continue;
|
|
30
|
-
const fileName = diagnostic.file.fileName;
|
|
31
|
-
if (!isUnder(template.dir, fileName)) continue;
|
|
32
|
-
out.push({
|
|
33
|
-
ruleId: "template-kit/typecheck",
|
|
34
|
-
template: template.key,
|
|
35
|
-
section: sectionOf(template, fileName),
|
|
36
|
-
file: relative(workspaceRoot, fileName),
|
|
37
|
-
line: diagnostic.start === void 0 ? void 0 : ts.getLineAndCharacterOfPosition(diagnostic.file, diagnostic.start).line + 1,
|
|
38
|
-
message: relativizePaths(ts.flattenDiagnosticMessageText(diagnostic.messageText, " "), workspaceRoot),
|
|
39
|
-
fix: "Fix the type error. Section props are derived from schema.ts — never hand-written."
|
|
40
|
-
});
|
|
41
|
-
}
|
|
42
|
-
return out;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
//#endregion
|
|
46
|
-
export { typecheckStage };
|
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
import { SLOT_TYPES } from "../../../../contracts/slot-catalog.js";
|
|
2
|
-
import { SlotDef } from "../../../../schema/slot-types.js";
|
|
3
|
-
|
|
4
|
-
//#region src/cli/check/stages/validate/catalog-exhaustiveness.ts
|
|
5
|
-
function assertCatalogExhaustiveness() {
|
|
6
|
-
const unionTypes = /* @__PURE__ */ new Set();
|
|
7
|
-
const union = SlotDef._def.getter?.() ?? SlotDef;
|
|
8
|
-
for (const option of union.options) {
|
|
9
|
-
const typeSchema = option.shape.type;
|
|
10
|
-
unionTypes.add(typeSchema.value);
|
|
11
|
-
}
|
|
12
|
-
const catalog = new Set(SLOT_TYPES);
|
|
13
|
-
for (const t of catalog) if (!unionTypes.has(t)) throw new Error(`slot-catalog SLOT_TYPES has "${t}" but section-schema SlotDef has no matching Zod variant.`);
|
|
14
|
-
for (const t of unionTypes) if (!catalog.has(t)) throw new Error(`section-schema SlotDef has a "${t}" variant not in slot-catalog SLOT_TYPES.`);
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
//#endregion
|
|
18
|
-
export { assertCatalogExhaustiveness };
|
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
//#region src/cli/check/stages/validate/facts.ts
|
|
2
|
-
const FACTS_ARRAY_FIELDS = /* @__PURE__ */ new Set([
|
|
3
|
-
"units",
|
|
4
|
-
"photos",
|
|
5
|
-
"videos",
|
|
6
|
-
"floor_plans",
|
|
7
|
-
"pois",
|
|
8
|
-
"contacts"
|
|
9
|
-
]);
|
|
10
|
-
const FACTS_READABLE = /* @__PURE__ */ new Set([
|
|
11
|
-
"extras",
|
|
12
|
-
"tours",
|
|
13
|
-
"floor_plans",
|
|
14
|
-
"photos",
|
|
15
|
-
"pois"
|
|
16
|
-
]);
|
|
17
|
-
|
|
18
|
-
//#endregion
|
|
19
|
-
export { FACTS_ARRAY_FIELDS, FACTS_READABLE };
|
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
import { FORMATTER_NAMES } from "../../../../contracts/slot-catalog.js";
|
|
2
|
-
|
|
3
|
-
//#region src/cli/check/stages/validate/formatter-contract.ts
|
|
4
|
-
function assertFormatterContract(schema) {
|
|
5
|
-
for (const [name, slot] of Object.entries(schema.slots)) {
|
|
6
|
-
const fmt = slot.format;
|
|
7
|
-
if (fmt == null) continue;
|
|
8
|
-
const fn = typeof fmt === "string" ? fmt : fmt.fn;
|
|
9
|
-
if (typeof fn !== "string" || !FORMATTER_NAMES.includes(fn)) throw new Error(`${schema.key}.${name}: unknown formatter "${String(fn)}" (allowed: ${FORMATTER_NAMES.join(", ")})`);
|
|
10
|
-
}
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
//#endregion
|
|
14
|
-
export { assertFormatterContract };
|
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
import { flattenLayout } from "../../../../schema/rows.js";
|
|
2
|
-
|
|
3
|
-
//#region src/cli/check/stages/validate/group-contract.ts
|
|
4
|
-
function assertGroupContract(schema, sectionKey) {
|
|
5
|
-
const groups = schema.groups;
|
|
6
|
-
if (!groups || groups.length === 0) return;
|
|
7
|
-
const seenIds = /* @__PURE__ */ new Set();
|
|
8
|
-
const seenSlots = /* @__PURE__ */ new Set();
|
|
9
|
-
for (const group of groups) {
|
|
10
|
-
if (seenIds.has(group.id)) throw new Error(`Section "${sectionKey}": duplicate group id "${group.id}".`);
|
|
11
|
-
seenIds.add(group.id);
|
|
12
|
-
for (const member of flattenLayout(group.members)) {
|
|
13
|
-
if (!(member in schema.slots)) throw new Error(`Section "${sectionKey}": group "${group.id}" names unknown slot "${member}".`);
|
|
14
|
-
if (seenSlots.has(member)) throw new Error(`Section "${sectionKey}": slot "${member}" appears in more than one group.`);
|
|
15
|
-
seenSlots.add(member);
|
|
16
|
-
}
|
|
17
|
-
}
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
//#endregion
|
|
21
|
-
export { assertGroupContract };
|
|
@@ -1,66 +0,0 @@
|
|
|
1
|
-
import { MissingContractFile, MissingSectionExport, loadSection } from "../../loader.js";
|
|
2
|
-
import { relativizePaths } from "../../relativize.js";
|
|
3
|
-
import { validateFixtureModule, validateOneSection, validateTemplateManifest } from "./orchestrator.js";
|
|
4
|
-
import { join } from "node:path";
|
|
5
|
-
import { existsSync } from "node:fs";
|
|
6
|
-
|
|
7
|
-
//#region src/cli/check/stages/validate/index.ts
|
|
8
|
-
async function validateStage(workspaceRoot, template) {
|
|
9
|
-
const diagnostics = [];
|
|
10
|
-
for (const section of template.sections) {
|
|
11
|
-
const sectionFile = (name) => `templates/${template.key}/sections/${section.name}/${name}`;
|
|
12
|
-
let loaded;
|
|
13
|
-
try {
|
|
14
|
-
loaded = await loadSection(workspaceRoot, section);
|
|
15
|
-
} catch (cause) {
|
|
16
|
-
if (cause instanceof MissingContractFile) continue;
|
|
17
|
-
diagnostics.push({
|
|
18
|
-
ruleId: "template-kit/schema-invalid",
|
|
19
|
-
template: template.key,
|
|
20
|
-
section: section.name,
|
|
21
|
-
file: cause instanceof MissingSectionExport ? cause.file : sectionFile("schema.ts"),
|
|
22
|
-
message: relativizePaths(cause instanceof Error ? cause.message : String(cause), workspaceRoot),
|
|
23
|
-
fix: "The section's contract files could not be loaded or parsed. Fix the error above."
|
|
24
|
-
});
|
|
25
|
-
continue;
|
|
26
|
-
}
|
|
27
|
-
for (const message of validateOneSection(section.name, loaded)) diagnostics.push({
|
|
28
|
-
ruleId: "template-kit/schema-invalid",
|
|
29
|
-
template: template.key,
|
|
30
|
-
section: section.name,
|
|
31
|
-
file: sectionFile("schema.ts"),
|
|
32
|
-
message,
|
|
33
|
-
fix: "Fix the schema, fill-spec or fixtures declaration this message names."
|
|
34
|
-
});
|
|
35
|
-
for (const message of validateFixtureModule(section.name, loaded)) diagnostics.push({
|
|
36
|
-
ruleId: "template-kit/fixtures-invalid",
|
|
37
|
-
template: template.key,
|
|
38
|
-
section: section.name,
|
|
39
|
-
file: sectionFile("fixtures.ts"),
|
|
40
|
-
message,
|
|
41
|
-
fix: "Fix the fixtures module: it must default-export { base: { slots: { … } } }, plus the optional option_matrix and states blocks."
|
|
42
|
-
});
|
|
43
|
-
}
|
|
44
|
-
if (!existsSync(join(template.dir, "manifest.json"))) {
|
|
45
|
-
diagnostics.push({
|
|
46
|
-
ruleId: "template-kit/manifest-invalid",
|
|
47
|
-
template: template.key,
|
|
48
|
-
file: `templates/${template.key}/manifest.json`,
|
|
49
|
-
line: 1,
|
|
50
|
-
message: `Template "${template.key}" has no manifest.json.`,
|
|
51
|
-
fix: "Add manifest.json to the template folder: a template needs one to declare its section composition — which sections it is built from, in what order."
|
|
52
|
-
});
|
|
53
|
-
return diagnostics;
|
|
54
|
-
}
|
|
55
|
-
for (const message of await validateTemplateManifest(workspaceRoot, template)) diagnostics.push({
|
|
56
|
-
ruleId: "template-kit/manifest-invalid",
|
|
57
|
-
template: template.key,
|
|
58
|
-
file: `templates/${template.key}/manifest.json`,
|
|
59
|
-
message,
|
|
60
|
-
fix: "Fix the manifest entry this message names."
|
|
61
|
-
});
|
|
62
|
-
return diagnostics;
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
//#endregion
|
|
66
|
-
export { validateStage };
|