@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,337 +0,0 @@
|
|
|
1
|
-
import { CHECK_CONFIG } from "../config.js";
|
|
2
|
-
import { z } from "zod";
|
|
3
|
-
import { promisify } from "node:util";
|
|
4
|
-
import { readFile, readdir, realpath, stat } from "node:fs/promises";
|
|
5
|
-
import { join } from "node:path";
|
|
6
|
-
import { execFile } from "node:child_process";
|
|
7
|
-
|
|
8
|
-
//#region src/cli/check/stages/deps.ts
|
|
9
|
-
const execFileAsync = promisify(execFile);
|
|
10
|
-
/** Real `execFile`-backed runner. Never throws on a non-zero exit — `npm audit`
|
|
11
|
-
* and `npm ls` both use exit codes to report *findings*, not tool failure, so
|
|
12
|
-
* the caller must always get the payload to parse. */
|
|
13
|
-
const defaultRun = async (cmd, args, cwd) => {
|
|
14
|
-
try {
|
|
15
|
-
const { stdout } = await execFileAsync(cmd, args, {
|
|
16
|
-
cwd,
|
|
17
|
-
maxBuffer: 64 * 1024 * 1024
|
|
18
|
-
});
|
|
19
|
-
return {
|
|
20
|
-
stdout,
|
|
21
|
-
code: 0
|
|
22
|
-
};
|
|
23
|
-
} catch (err) {
|
|
24
|
-
const e = err;
|
|
25
|
-
return {
|
|
26
|
-
stdout: typeof e.stdout === "string" ? e.stdout : "",
|
|
27
|
-
code: typeof e.code === "number" ? e.code : 1
|
|
28
|
-
};
|
|
29
|
-
}
|
|
30
|
-
};
|
|
31
|
-
async function isFile(p) {
|
|
32
|
-
try {
|
|
33
|
-
return (await stat(p)).isFile();
|
|
34
|
-
} catch {
|
|
35
|
-
return false;
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
async function isDir(p) {
|
|
39
|
-
try {
|
|
40
|
-
return (await stat(p)).isDirectory();
|
|
41
|
-
} catch {
|
|
42
|
-
return false;
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
/** `fs.realpath`, or `undefined` if the path is missing or unresolvable
|
|
46
|
-
* (e.g. a broken symlink or a genuine symlink loop, which trips `ELOOP`). */
|
|
47
|
-
async function realPathOrUndefined(p) {
|
|
48
|
-
try {
|
|
49
|
-
return await realpath(p);
|
|
50
|
-
} catch {
|
|
51
|
-
return;
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
async function readJsonIfPresent(path) {
|
|
55
|
-
if (!await isFile(path)) return void 0;
|
|
56
|
-
const text = await readFile(path, "utf8");
|
|
57
|
-
return JSON.parse(text);
|
|
58
|
-
}
|
|
59
|
-
const LOCKFILE_MISSING_FIX = "Run npm install and commit the package-lock.json it writes. A submission without one cannot be reproduced.";
|
|
60
|
-
function lockfileMissingGate(hasLockfile) {
|
|
61
|
-
if (hasLockfile) return [];
|
|
62
|
-
return [{
|
|
63
|
-
ruleId: "template-kit/lockfile-missing",
|
|
64
|
-
template: "",
|
|
65
|
-
message: "no package-lock.json at the workspace root.",
|
|
66
|
-
fix: LOCKFILE_MISSING_FIX
|
|
67
|
-
}];
|
|
68
|
-
}
|
|
69
|
-
const LOCKFILE_STALE_FIX = "The lockfile does not match package.json. Run npm install and commit the updated lockfile.";
|
|
70
|
-
const npmLsNodeSchema = z.lazy(() => z.object({
|
|
71
|
-
invalid: z.boolean().optional(),
|
|
72
|
-
dependencies: z.record(z.string(), npmLsNodeSchema).optional()
|
|
73
|
-
}).passthrough());
|
|
74
|
-
const npmLsPayloadSchema = z.object({
|
|
75
|
-
problems: z.array(z.unknown()).optional(),
|
|
76
|
-
dependencies: z.record(z.string(), npmLsNodeSchema).optional()
|
|
77
|
-
}).passthrough();
|
|
78
|
-
function anyInvalid(nodes) {
|
|
79
|
-
if (!nodes) return false;
|
|
80
|
-
for (const node of Object.values(nodes)) {
|
|
81
|
-
if (node.invalid === true) return true;
|
|
82
|
-
if (anyInvalid(node.dependencies)) return true;
|
|
83
|
-
}
|
|
84
|
-
return false;
|
|
85
|
-
}
|
|
86
|
-
async function lockfileStaleGate(run, workspaceRoot) {
|
|
87
|
-
const { stdout, code } = await run("npm", [
|
|
88
|
-
"ls",
|
|
89
|
-
"--json",
|
|
90
|
-
"--package-lock-only"
|
|
91
|
-
], workspaceRoot);
|
|
92
|
-
const parsed = npmLsPayloadSchema.safeParse(safeJsonParse(stdout));
|
|
93
|
-
if (!(code !== 0 || !parsed.success || (parsed.data.problems?.length ?? 0) > 0 || anyInvalid(parsed.data.dependencies))) return [];
|
|
94
|
-
return [{
|
|
95
|
-
ruleId: "template-kit/lockfile-stale",
|
|
96
|
-
template: "",
|
|
97
|
-
message: "the lockfile does not satisfy package.json — npm ci would fail.",
|
|
98
|
-
fix: LOCKFILE_STALE_FIX
|
|
99
|
-
}];
|
|
100
|
-
}
|
|
101
|
-
function safeJsonParse(text) {
|
|
102
|
-
try {
|
|
103
|
-
return JSON.parse(text);
|
|
104
|
-
} catch {
|
|
105
|
-
return;
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
const SINGLE_REACT_FIX = "Two copies of React break hydration. React is a peer of everything here — deduplicate with npm dedupe, or align the dependency that pins its own copy.";
|
|
109
|
-
const packageManifestSchema = z.object({ version: z.string().optional() }).passthrough();
|
|
110
|
-
async function readVersion(pkgDir) {
|
|
111
|
-
try {
|
|
112
|
-
const raw = await readJsonIfPresent(join(pkgDir, "package.json"));
|
|
113
|
-
const parsed = packageManifestSchema.safeParse(raw);
|
|
114
|
-
return parsed.success && parsed.data.version !== void 0 ? parsed.data.version : "unknown version";
|
|
115
|
-
} catch {
|
|
116
|
-
return "unknown version";
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
/** True when `entry` is worth descending into: a real directory, or a
|
|
120
|
-
* symlink whose target is one. `Dirent.isDirectory()` reports the dirent's
|
|
121
|
-
* OWN type, not what it resolves to — a symlink to a directory (exactly how
|
|
122
|
-
* `npm link` and npm/yarn workspaces lay out linked packages) reports
|
|
123
|
-
* `false`, so trusting it blindly makes the walk blind to every linked
|
|
124
|
-
* package's own `node_modules`. `isDir` re-`stat`s, which follows the link. */
|
|
125
|
-
async function isTraversableDir(dir, entry) {
|
|
126
|
-
if (entry.isDirectory()) return true;
|
|
127
|
-
if (!entry.isSymbolicLink()) return false;
|
|
128
|
-
return isDir(join(dir, entry.name));
|
|
129
|
-
}
|
|
130
|
-
/**
|
|
131
|
-
* Walks `node_modules` for a nested `react` or `react-dom` — one whose own
|
|
132
|
-
* `node_modules` (not the workspace's top-level one) holds a copy. `owner` is
|
|
133
|
-
* the name of the package whose private `node_modules` directory holds the
|
|
134
|
-
* offending copy, so the message can point straight at what to dedupe.
|
|
135
|
-
*
|
|
136
|
-
* `topLevelReal` maps "react"/"react-dom" to the REAL (symlink-resolved)
|
|
137
|
-
* path of the workspace's own top-level copy, if any — see
|
|
138
|
-
* `visitReactCandidate` for why identity is checked against it rather than
|
|
139
|
-
* against walk depth. `visited` is the set of real directory paths already
|
|
140
|
-
* walked: a defense against a symlink cycle (a linked package whose own
|
|
141
|
-
* `node_modules` loops back to an ancestor already on the path) turning this
|
|
142
|
-
* into infinite recursion.
|
|
143
|
-
*/
|
|
144
|
-
async function walkForNestedReact(dir, atTopLevel, owner, out, topLevelReal, visited) {
|
|
145
|
-
const real = await realPathOrUndefined(dir);
|
|
146
|
-
if (real === void 0 || visited.has(real)) return;
|
|
147
|
-
visited.add(real);
|
|
148
|
-
let entries;
|
|
149
|
-
try {
|
|
150
|
-
entries = await readdir(dir, { withFileTypes: true });
|
|
151
|
-
} catch {
|
|
152
|
-
return;
|
|
153
|
-
}
|
|
154
|
-
for (const entry of entries) {
|
|
155
|
-
if (!await isTraversableDir(dir, entry)) continue;
|
|
156
|
-
if (entry.name.startsWith("@")) {
|
|
157
|
-
const scopeDir = join(dir, entry.name);
|
|
158
|
-
let scoped;
|
|
159
|
-
try {
|
|
160
|
-
scoped = await readdir(scopeDir, { withFileTypes: true });
|
|
161
|
-
} catch {
|
|
162
|
-
continue;
|
|
163
|
-
}
|
|
164
|
-
for (const inner of scoped) {
|
|
165
|
-
if (!await isTraversableDir(scopeDir, inner)) continue;
|
|
166
|
-
await visitReactCandidate(join(scopeDir, inner.name), `${entry.name}/${inner.name}`, atTopLevel, owner, out, topLevelReal, visited);
|
|
167
|
-
}
|
|
168
|
-
continue;
|
|
169
|
-
}
|
|
170
|
-
await visitReactCandidate(join(dir, entry.name), entry.name, atTopLevel, owner, out, topLevelReal, visited);
|
|
171
|
-
}
|
|
172
|
-
}
|
|
173
|
-
async function visitReactCandidate(pkgDir, name, atTopLevel, owner, out, topLevelReal, visited) {
|
|
174
|
-
if (!atTopLevel && (name === "react" || name === "react-dom")) {
|
|
175
|
-
if ((await realPathOrUndefined(pkgDir) ?? pkgDir) !== topLevelReal.get(name)) out.push({
|
|
176
|
-
name,
|
|
177
|
-
version: await readVersion(pkgDir),
|
|
178
|
-
owner
|
|
179
|
-
});
|
|
180
|
-
}
|
|
181
|
-
const nested = join(pkgDir, "node_modules");
|
|
182
|
-
if (await isDir(nested)) await walkForNestedReact(nested, false, name, out, topLevelReal, visited);
|
|
183
|
-
}
|
|
184
|
-
async function singleReactGate(workspaceRoot) {
|
|
185
|
-
const nodeModules = join(workspaceRoot, "node_modules");
|
|
186
|
-
const offenders = [];
|
|
187
|
-
const topLevelReal = /* @__PURE__ */ new Map();
|
|
188
|
-
for (const name of ["react", "react-dom"]) {
|
|
189
|
-
const real = await realPathOrUndefined(join(nodeModules, name));
|
|
190
|
-
if (real !== void 0) topLevelReal.set(name, real);
|
|
191
|
-
}
|
|
192
|
-
await walkForNestedReact(nodeModules, true, "", offenders, topLevelReal, /* @__PURE__ */ new Set());
|
|
193
|
-
return offenders.map((offender) => ({
|
|
194
|
-
ruleId: "template-kit/single-react",
|
|
195
|
-
template: "",
|
|
196
|
-
message: `a nested copy of ${offender.name}@${offender.version} is bundled inside ${offender.owner}'s own node_modules.`,
|
|
197
|
-
fix: SINGLE_REACT_FIX
|
|
198
|
-
}));
|
|
199
|
-
}
|
|
200
|
-
const AUDIT_SEVERITY_FIX = "Run npm audit fix, or upgrade the package the advisory names.";
|
|
201
|
-
const npmAuditPayloadSchema = z.object({ metadata: z.object({ vulnerabilities: z.record(z.string(), z.number()).optional() }).passthrough().optional() }).passthrough();
|
|
202
|
-
async function auditSeverityGate(run, workspaceRoot) {
|
|
203
|
-
const { stdout } = await run("npm", ["audit", "--json"], workspaceRoot);
|
|
204
|
-
const parsed = npmAuditPayloadSchema.safeParse(safeJsonParse(stdout));
|
|
205
|
-
const vulnerabilities = parsed.success ? parsed.data.metadata?.vulnerabilities : void 0;
|
|
206
|
-
if (vulnerabilities === void 0) return [{
|
|
207
|
-
ruleId: "template-kit/audit-severity",
|
|
208
|
-
template: "",
|
|
209
|
-
message: "npm audit did not return a parseable report — the audit could not run.",
|
|
210
|
-
fix: "Run npm audit --json in the workspace and fix whatever is preventing it (usually missing network access), then re-run check."
|
|
211
|
-
}];
|
|
212
|
-
const offendingSeverities = CHECK_CONFIG.audit.failOn.filter((severity) => (vulnerabilities[severity] ?? 0) > 0);
|
|
213
|
-
if (offendingSeverities.length === 0) return [];
|
|
214
|
-
return [{
|
|
215
|
-
ruleId: "template-kit/audit-severity",
|
|
216
|
-
template: "",
|
|
217
|
-
message: `npm audit found ${offendingSeverities.map((severity) => `${String(vulnerabilities[severity] ?? 0)} ${severity}`).join(", ")} severity advisories.`,
|
|
218
|
-
fix: AUDIT_SEVERITY_FIX
|
|
219
|
-
}];
|
|
220
|
-
}
|
|
221
|
-
const LICENSE_DENIED_FIX = "Only permissively licensed packages can ship in a template. Replace this one, or ask us to review it.";
|
|
222
|
-
const lockPackageEntrySchema = z.object({
|
|
223
|
-
version: z.string().optional(),
|
|
224
|
-
license: z.string().optional(),
|
|
225
|
-
dependencies: z.record(z.string(), z.string()).optional(),
|
|
226
|
-
peerDependencies: z.record(z.string(), z.string()).optional(),
|
|
227
|
-
peerDependenciesMeta: z.record(z.string(), z.object({ optional: z.boolean().optional() }).passthrough()).optional()
|
|
228
|
-
}).passthrough();
|
|
229
|
-
const lockFileSchema = z.object({ packages: z.record(z.string(), lockPackageEntrySchema) }).passthrough();
|
|
230
|
-
/** `"node_modules/@scope/name/node_modules/other"` -> `["@scope/name", "other"]`; `""` -> `[]`. */
|
|
231
|
-
function parseChain(key) {
|
|
232
|
-
if (key === "") return [];
|
|
233
|
-
return key.split("/node_modules/").map((segment, i) => i === 0 ? segment.replace(/^node_modules\//, "") : segment);
|
|
234
|
-
}
|
|
235
|
-
function buildKey(chain) {
|
|
236
|
-
return chain.length === 0 ? "" : `node_modules/${chain.join("/node_modules/")}`;
|
|
237
|
-
}
|
|
238
|
-
/** Node's own nearest-ancestor `node_modules` resolution, applied to lockfile keys instead of disk. */
|
|
239
|
-
function resolveDependencyKey(packages, fromKey, name) {
|
|
240
|
-
const chain = parseChain(fromKey);
|
|
241
|
-
for (let i = chain.length; i >= 0; i--) {
|
|
242
|
-
const candidate = buildKey([...chain.slice(0, i), name]);
|
|
243
|
-
if (Object.hasOwn(packages, candidate)) return candidate;
|
|
244
|
-
}
|
|
245
|
-
}
|
|
246
|
-
/**
|
|
247
|
-
* The set of lockfile package keys in the production install tree (what an
|
|
248
|
-
* `npm ci --omit=dev` leaves on disk): the root's `dependencies` (never its
|
|
249
|
-
* `devDependencies`), walked transitively through each package's own
|
|
250
|
-
* `dependencies` and its `peerDependencies`. A `devDependencies` edge is a
|
|
251
|
-
* dead end. An optional peer is walked only when it resolves to an installed
|
|
252
|
-
* entry — one the workspace has anyway is kept by `npm ci --omit=dev`; one it
|
|
253
|
-
* lacks never installs and resolves to nothing.
|
|
254
|
-
*/
|
|
255
|
-
function shippedPackageKeys(packages) {
|
|
256
|
-
const root = packages[""];
|
|
257
|
-
const visited = /* @__PURE__ */ new Set();
|
|
258
|
-
const queue = [];
|
|
259
|
-
const seed = (fromKey, deps) => {
|
|
260
|
-
if (!deps) return;
|
|
261
|
-
for (const name of Object.keys(deps)) {
|
|
262
|
-
const key = resolveDependencyKey(packages, fromKey, name);
|
|
263
|
-
if (key !== void 0) queue.push(key);
|
|
264
|
-
}
|
|
265
|
-
};
|
|
266
|
-
seed("", root?.dependencies);
|
|
267
|
-
for (let key = queue.pop(); key !== void 0; key = queue.pop()) {
|
|
268
|
-
if (visited.has(key)) continue;
|
|
269
|
-
visited.add(key);
|
|
270
|
-
const entry = packages[key];
|
|
271
|
-
if (!entry) continue;
|
|
272
|
-
seed(key, entry.dependencies);
|
|
273
|
-
if (entry.peerDependencies) for (const name of Object.keys(entry.peerDependencies)) {
|
|
274
|
-
const peerKey = resolveDependencyKey(packages, key, name);
|
|
275
|
-
if (peerKey !== void 0) queue.push(peerKey);
|
|
276
|
-
}
|
|
277
|
-
}
|
|
278
|
-
return visited;
|
|
279
|
-
}
|
|
280
|
-
function packageNameFromKey(key) {
|
|
281
|
-
const chain = parseChain(key);
|
|
282
|
-
return chain[chain.length - 1] ?? key;
|
|
283
|
-
}
|
|
284
|
-
async function licenseOf(entry, workspaceRoot, key) {
|
|
285
|
-
if (entry.license !== void 0 && entry.license !== "") return entry.license;
|
|
286
|
-
const raw = await readJsonIfPresent(join(workspaceRoot, key, "package.json")).catch(() => void 0);
|
|
287
|
-
const parsed = packageManifestLicenseSchema.safeParse(raw);
|
|
288
|
-
return parsed.success ? parsed.data.license : void 0;
|
|
289
|
-
}
|
|
290
|
-
const packageManifestLicenseSchema = z.object({ license: z.string().optional() }).passthrough();
|
|
291
|
-
async function licenseDeniedGate(lockfileText, workspaceRoot) {
|
|
292
|
-
const parsed = lockFileSchema.safeParse(safeJsonParse(lockfileText));
|
|
293
|
-
if (!parsed.success) return [];
|
|
294
|
-
const packages = parsed.data.packages;
|
|
295
|
-
const shipped = shippedPackageKeys(packages);
|
|
296
|
-
const out = [];
|
|
297
|
-
for (const key of shipped) {
|
|
298
|
-
const name = packageNameFromKey(key);
|
|
299
|
-
if (CHECK_CONFIG.licenseExemptions.includes(name)) continue;
|
|
300
|
-
const entry = packages[key];
|
|
301
|
-
if (!entry) continue;
|
|
302
|
-
const license = await licenseOf(entry, workspaceRoot, key);
|
|
303
|
-
const version = entry.version ?? "unknown version";
|
|
304
|
-
if (license === void 0) {
|
|
305
|
-
out.push({
|
|
306
|
-
ruleId: "template-kit/license-denied",
|
|
307
|
-
template: "",
|
|
308
|
-
message: `${name}@${version} has no license field.`,
|
|
309
|
-
fix: LICENSE_DENIED_FIX
|
|
310
|
-
});
|
|
311
|
-
continue;
|
|
312
|
-
}
|
|
313
|
-
if (!CHECK_CONFIG.licenseAllowlist.includes(license)) out.push({
|
|
314
|
-
ruleId: "template-kit/license-denied",
|
|
315
|
-
template: "",
|
|
316
|
-
message: `${name}@${version} is licensed ${license}, which is not on the allowlist.`,
|
|
317
|
-
fix: LICENSE_DENIED_FIX
|
|
318
|
-
});
|
|
319
|
-
}
|
|
320
|
-
return out;
|
|
321
|
-
}
|
|
322
|
-
async function depsStage(workspaceRoot, run = defaultRun) {
|
|
323
|
-
const lockfilePath = join(workspaceRoot, "package-lock.json");
|
|
324
|
-
const hasLockfile = await isFile(lockfilePath);
|
|
325
|
-
const diagnostics = [...lockfileMissingGate(hasLockfile)];
|
|
326
|
-
if (hasLockfile) {
|
|
327
|
-
diagnostics.push(...await lockfileStaleGate(run, workspaceRoot));
|
|
328
|
-
const lockfileText = await readFile(lockfilePath, "utf8");
|
|
329
|
-
diagnostics.push(...await licenseDeniedGate(lockfileText, workspaceRoot));
|
|
330
|
-
}
|
|
331
|
-
diagnostics.push(...await singleReactGate(workspaceRoot));
|
|
332
|
-
diagnostics.push(...await auditSeverityGate(run, workspaceRoot));
|
|
333
|
-
return diagnostics;
|
|
334
|
-
}
|
|
335
|
-
|
|
336
|
-
//#endregion
|
|
337
|
-
export { defaultRun, depsStage };
|
|
@@ -1,46 +0,0 @@
|
|
|
1
|
-
import { resolveFromWorkspace } from "../resolve-tool.js";
|
|
2
|
-
import { sectionOf } from "../paths.js";
|
|
3
|
-
import { relativizePaths } from "../relativize.js";
|
|
4
|
-
import { join, relative } from "node:path";
|
|
5
|
-
|
|
6
|
-
//#region src/cli/check/stages/lint.ts
|
|
7
|
-
const PARSE_ERROR_RULE_ID = "template-kit/parse-error";
|
|
8
|
-
const FIX_HINTS = {
|
|
9
|
-
"template-kit/no-nondeterminism": "Remove the clock or randomness read from server-rendered code, or move this logic into an island (a file marked use client).",
|
|
10
|
-
"template-kit/no-client-runtime-in-server": "Move this into an island (mark the file use client) or drop the browser-only API — a Renderer must be a pure function of its props.",
|
|
11
|
-
"template-kit/serializable-island-props": "Pass only JSON-serializable values to an island: no functions, Date/Map/Set instances, bigints, NaN, or Infinity.",
|
|
12
|
-
"template-kit/no-inline-style": "Replace the style prop with a Tailwind class or a design-system token. Inline style bypasses the cascade and responsive prefixes.",
|
|
13
|
-
"template-kit/no-raw-element": "Use the contract-bearing primitive instead of the raw element: Section for section, Image for img.",
|
|
14
|
-
"template-kit/image-bare-needs-reason": "Add a bare: comment above the Image explaining why the frame is skipped, or remove the bare prop.",
|
|
15
|
-
"template-kit/slot-marker-literal": "Give the marker's id a string literal, not an expression, so the slot-marker completeness check can read it statically.",
|
|
16
|
-
"template-kit/no-hex": "Use a design-system color token instead of a hex literal or CSS color function.",
|
|
17
|
-
"template-kit/no-css-import-from-render-path": "Delete the relative CSS import. The file's own CSS is already collected by path; use a bare package specifier for a third-party stylesheet.",
|
|
18
|
-
"template-kit/props-from-schema": "Derive Props from SectionProps<typeof schema> instead of hand-writing the type.",
|
|
19
|
-
"template-kit/no-client-directive-in-contract": "Remove the use client directive from this contract file. schema.ts, fill-spec.ts, fixtures.ts and Renderer.tsx are read outside the browser."
|
|
20
|
-
};
|
|
21
|
-
async function lintStage(workspaceRoot, template) {
|
|
22
|
-
const { ESLint } = await resolveFromWorkspace(workspaceRoot, "eslint");
|
|
23
|
-
const results = await new ESLint({
|
|
24
|
-
cwd: workspaceRoot,
|
|
25
|
-
errorOnUnmatchedPattern: false
|
|
26
|
-
}).lintFiles([join(template.dir, "**/*.{ts,tsx}")]);
|
|
27
|
-
const out = [];
|
|
28
|
-
for (const result of results) for (const message of result.messages) {
|
|
29
|
-
if (message.severity !== 2) continue;
|
|
30
|
-
const ruleId = message.ruleId ?? PARSE_ERROR_RULE_ID;
|
|
31
|
-
const text = relativizePaths(message.message, workspaceRoot);
|
|
32
|
-
out.push({
|
|
33
|
-
ruleId,
|
|
34
|
-
template: template.key,
|
|
35
|
-
section: sectionOf(template, result.filePath),
|
|
36
|
-
file: relative(workspaceRoot, result.filePath),
|
|
37
|
-
line: message.line,
|
|
38
|
-
message: text,
|
|
39
|
-
fix: FIX_HINTS[ruleId] ?? text
|
|
40
|
-
});
|
|
41
|
-
}
|
|
42
|
-
return out;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
//#endregion
|
|
46
|
-
export { lintStage };
|
|
@@ -1,98 +0,0 @@
|
|
|
1
|
-
import { FixtureModuleShape, buildInvariantFixtures } from "../../../schema/fixture-schema.js";
|
|
2
|
-
import { loadSection } from "../loader.js";
|
|
3
|
-
import { resolveFixtureSetMedia } from "../../../media/resolve-fixture-media.js";
|
|
4
|
-
import { flattenLayout } from "../../../schema/rows.js";
|
|
5
|
-
import { renderSectionHtml } from "../../../ssr.js";
|
|
6
|
-
import { checkRenderInvariants } from "../render-invariants.js";
|
|
7
|
-
|
|
8
|
-
//#region src/cli/check/stages/render.ts
|
|
9
|
-
const SLOT_ID_RE = /data-slot-id="([^"]*)"/g;
|
|
10
|
-
function renderSlotOrder(html) {
|
|
11
|
-
const seen = /* @__PURE__ */ new Set();
|
|
12
|
-
const order = [];
|
|
13
|
-
for (const match of html.matchAll(SLOT_ID_RE)) {
|
|
14
|
-
const id = match[1];
|
|
15
|
-
if (seen.has(id)) continue;
|
|
16
|
-
seen.add(id);
|
|
17
|
-
order.push(id);
|
|
18
|
-
}
|
|
19
|
-
return order;
|
|
20
|
-
}
|
|
21
|
-
function checkSidebarOrder(sectionKey, schema, html) {
|
|
22
|
-
const out = [];
|
|
23
|
-
const groupOf = /* @__PURE__ */ new Map();
|
|
24
|
-
for (const group of schema.meta?.groups ?? []) for (const member of flattenLayout(group.members)) groupOf.set(member, group.id);
|
|
25
|
-
const cardOf = (slotId) => groupOf.get(slotId) ?? slotId;
|
|
26
|
-
const slotOrder = renderSlotOrder(html);
|
|
27
|
-
const renderCardOrder = [];
|
|
28
|
-
for (const slotId of slotOrder) {
|
|
29
|
-
const card = cardOf(slotId);
|
|
30
|
-
if (!renderCardOrder.includes(card)) renderCardOrder.push(card);
|
|
31
|
-
}
|
|
32
|
-
const presentCards = new Set(renderCardOrder);
|
|
33
|
-
const schemaCardOrder = [];
|
|
34
|
-
for (const slotId of Object.keys(schema.slots)) {
|
|
35
|
-
const card = cardOf(slotId);
|
|
36
|
-
if (!presentCards.has(card)) continue;
|
|
37
|
-
if (!schemaCardOrder.includes(card)) schemaCardOrder.push(card);
|
|
38
|
-
}
|
|
39
|
-
if (JSON.stringify(schemaCardOrder) !== JSON.stringify(renderCardOrder)) out.push({
|
|
40
|
-
section: sectionKey,
|
|
41
|
-
rule: "sidebar-order",
|
|
42
|
-
message: `Sidebar card order (schema slot declaration order) does not match page render order.\n schema: [${schemaCardOrder.join(", ")}]\n render: [${renderCardOrder.join(", ")}]`
|
|
43
|
-
});
|
|
44
|
-
return out;
|
|
45
|
-
}
|
|
46
|
-
async function renderStage(workspaceRoot, template) {
|
|
47
|
-
const diagnostics = [];
|
|
48
|
-
for (const section of template.sections) {
|
|
49
|
-
const file = `templates/${template.key}/sections/${section.name}/Renderer.tsx`;
|
|
50
|
-
let loaded;
|
|
51
|
-
let fixtures;
|
|
52
|
-
try {
|
|
53
|
-
loaded = await loadSection(workspaceRoot, section);
|
|
54
|
-
fixtures = resolveFixtureSetMedia(buildInvariantFixtures({
|
|
55
|
-
schema: loaded.schema,
|
|
56
|
-
module: FixtureModuleShape.parse(loaded.fixtures),
|
|
57
|
-
selfAnchor: section.name
|
|
58
|
-
}));
|
|
59
|
-
} catch {
|
|
60
|
-
continue;
|
|
61
|
-
}
|
|
62
|
-
for (const fixture of fixtures) {
|
|
63
|
-
const props = fixture.props;
|
|
64
|
-
const first = renderSectionHtml(loaded.Renderer, props);
|
|
65
|
-
if (first !== renderSectionHtml(loaded.Renderer, props)) {
|
|
66
|
-
diagnostics.push({
|
|
67
|
-
ruleId: "template-kit/determinism-drift",
|
|
68
|
-
template: template.key,
|
|
69
|
-
section: section.name,
|
|
70
|
-
file,
|
|
71
|
-
message: `fixture "${fixture.id}" rendered differently twice from the same props`,
|
|
72
|
-
fix: "Something in the render path is nondeterministic — a clock, a random, or an unstable iteration order. Server-rendered code must be a pure function of its props."
|
|
73
|
-
});
|
|
74
|
-
continue;
|
|
75
|
-
}
|
|
76
|
-
for (const violation of checkRenderInvariants(first)) diagnostics.push({
|
|
77
|
-
ruleId: "template-kit/render-invariant",
|
|
78
|
-
template: template.key,
|
|
79
|
-
section: section.name,
|
|
80
|
-
file,
|
|
81
|
-
message: `fixture "${fixture.id}": ${violation.detail}`,
|
|
82
|
-
fix: violation.rule === "broken-image" ? "Render a placeholder, or nothing, when an image slot is empty — never an <img> with no src." : "A value reached the page without a formatter. Format it, or render nothing when it is absent."
|
|
83
|
-
});
|
|
84
|
-
if (fixture.id === "typical") for (const violation of checkSidebarOrder(section.name, loaded.schema, first)) diagnostics.push({
|
|
85
|
-
ruleId: "template-kit/sidebar-order",
|
|
86
|
-
template: template.key,
|
|
87
|
-
section: section.name,
|
|
88
|
-
file: `templates/${template.key}/sections/${section.name}/schema.ts`,
|
|
89
|
-
message: violation.message,
|
|
90
|
-
fix: "The editor lists slots in schema declaration order. Reorder the schema, or the markup, so the two agree."
|
|
91
|
-
});
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
return diagnostics;
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
//#endregion
|
|
98
|
-
export { checkSidebarOrder, renderStage };
|