@homepages/template-kit 0.2.0 → 0.3.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.
Files changed (74) hide show
  1. package/CHANGELOG.md +151 -0
  2. package/README.md +61 -17
  3. package/dist/base.css +9 -0
  4. package/dist/cli/check/config.js +37 -0
  5. package/dist/cli/check/css.js +117 -0
  6. package/dist/cli/check/diagnostics.js +32 -0
  7. package/dist/cli/check/index.js +87 -0
  8. package/dist/cli/check/loader.js +142 -0
  9. package/dist/cli/check/paths.js +15 -0
  10. package/dist/cli/check/relativize.js +18 -0
  11. package/dist/cli/check/render-invariants.js +24 -0
  12. package/dist/cli/check/resolve-tool.js +38 -0
  13. package/dist/cli/check/stages/deps.js +337 -0
  14. package/dist/cli/check/stages/lint.js +46 -0
  15. package/dist/cli/check/stages/render.js +101 -0
  16. package/dist/cli/check/stages/size.js +158 -0
  17. package/dist/cli/check/stages/tree.js +207 -0
  18. package/dist/cli/check/stages/typecheck.js +46 -0
  19. package/dist/cli/check/stages/validate/catalog-exhaustiveness.js +18 -0
  20. package/dist/cli/check/stages/validate/facts.js +18 -0
  21. package/dist/cli/check/stages/validate/formatter-contract.js +14 -0
  22. package/dist/cli/check/stages/validate/group-contract.js +21 -0
  23. package/dist/cli/check/stages/validate/index.js +66 -0
  24. package/dist/cli/check/stages/validate/list-scalar-contract.js +22 -0
  25. package/dist/cli/check/stages/validate/nav-contract.js +93 -0
  26. package/dist/cli/check/stages/validate/nested-slot-contract.js +12 -0
  27. package/dist/cli/check/stages/validate/object-list-contract.js +31 -0
  28. package/dist/cli/check/stages/validate/orchestrator.js +241 -0
  29. package/dist/cli/check/stages/validate/produced-by-contract.js +18 -0
  30. package/dist/cli/check/stages/validate/row-contract.js +31 -0
  31. package/dist/cli/check/stages/validate/select-contract.js +13 -0
  32. package/dist/cli/check/stages/validate/source-contract.js +21 -0
  33. package/dist/cli/check/stages/validate/variant-contract.js +15 -0
  34. package/dist/cli/check/workspace.js +86 -0
  35. package/dist/cli/theme/generate.js +43 -0
  36. package/dist/cli/theme/index.js +33 -0
  37. package/dist/cli/theme/load-theme.js +69 -0
  38. package/dist/cli.js +22 -4
  39. package/dist/contracts/fill-treatments.js +47 -0
  40. package/dist/design-system/theme.d.ts +30 -300
  41. package/dist/design-system/theme.js +112 -90
  42. package/dist/eslint/rules/no-hex.js +78 -6
  43. package/dist/eslint.js +1 -1
  44. package/dist/index.d.ts +2 -2
  45. package/dist/index.js +2 -2
  46. package/dist/package.js +1 -1
  47. package/dist/primitives/Image.js +1 -1
  48. package/dist/schema/fill-spec.d.ts +80 -590
  49. package/dist/schema/fixture-schema.d.ts +5 -81
  50. package/dist/schema/manifest.d.ts +39 -475
  51. package/dist/schema/resolve-section-ref.js +10 -0
  52. package/dist/schema/rows.js +10 -0
  53. package/dist/schema/section-nav.js +9 -0
  54. package/dist/schema/section-schema.d.ts +51 -437
  55. package/dist/schema/slot-types.d.ts +12 -16
  56. package/dist/styles.css +31 -88
  57. package/docs/INDEX.md +4 -3
  58. package/docs/check.md +121 -0
  59. package/docs/eslint.md +18 -8
  60. package/docs/llms.txt +21 -9
  61. package/docs/primitives.md +5 -0
  62. package/docs/rules/INDEX.md +11 -4
  63. package/docs/rules/bundle-binary-asset.md +102 -0
  64. package/docs/rules/fixtures-invalid.md +96 -0
  65. package/docs/rules/license-denied.md +11 -3
  66. package/docs/rules/manifest-invalid.md +99 -0
  67. package/docs/rules/no-hex.md +76 -10
  68. package/docs/rules/no-templates.md +87 -0
  69. package/docs/rules/parse-error.md +76 -0
  70. package/docs/rules/schema-invalid.md +96 -0
  71. package/docs/rules/size-section-css.md +2 -2
  72. package/docs/theme-and-css.md +155 -90
  73. package/package.json +4 -9
  74. package/tsconfig.json +1 -1
package/CHANGELOG.md CHANGED
@@ -1,5 +1,156 @@
1
1
  # @homepages/template-kit
2
2
 
3
+ ## 0.3.0
4
+
5
+ ### Minor Changes
6
+
7
+ - bdb4dec: `zod` is now the kit's only dependency of any kind, besides its React peers. ESLint,
8
+ `@typescript-eslint/parser` and `esbuild` are no longer declared by the kit at all — a
9
+ linter and a bundler are development tools, and as dependencies they landed in the
10
+ **production** install of every consumer, including apps that only render a published
11
+ template and deploy with `npm ci --omit=dev`. For a consuming service that lints, the
12
+ production dependency graph drops by roughly 125 packages.
13
+
14
+ **Action required if you use the ESLint preset or the `check` command** — install the
15
+ toolchain they run, which they now resolve from your workspace rather than shipping
16
+ their own copy of:
17
+
18
+ ```bash
19
+ npm i -D eslint @typescript-eslint/parser # for @homepages/template-kit/eslint
20
+ npm i -D typescript esbuild # for template-kit check (with the two above)
21
+ ```
22
+
23
+ Nothing changes for code that only imports the kit's primitives, schema system or theme
24
+ tokens. The preset's rules and parser wiring are unchanged; `check`'s stages and rule ids
25
+ are unchanged — and it now reports a missing tool by name, with the install line, instead
26
+ of failing obscurely. As a side benefit, `check` runs against _your_ pinned TypeScript,
27
+ ESLint and esbuild.
28
+
29
+ - 49177ab: Adds the `check` command: `template-kit check [template] [--all] [--json]`. Runs a
30
+ template's authoring contract end to end — type errors, the lint preset, the schema and
31
+ manifest contracts, section-folder structure (contract files present, editor slot
32
+ markers, hand-CSS hygiene), a render-regression pass (determinism, broken images, and
33
+ other objective defects), and per-section bundle-size budgets — plus workspace-wide
34
+ dependency gates (lockfile health, a single copy of React, dependency licenses,
35
+ vulnerability severity) evaluated once per run. Every failure prints a stable rule id, a
36
+ file and line, and a one-line fix. Exits non-zero on any failure; pass `--json` for a
37
+ script-consumable report.
38
+ - 2a30f0b: `template-kit theme` generates your template's `theme.css` from its `theme.ts`.
39
+
40
+ `@theme` is a Tailwind directive — it only does anything when a real `.css` file carrying
41
+ it flows through your Tailwind entry. So the alias layer `compileThemeToCss` produces from
42
+ your `theme.ts` has to reach that entry as a file. Rather than call the compiler from your
43
+ own build script (and risk `theme.css` silently drifting from `theme.ts`), run:
44
+
45
+ ```
46
+ template-kit theme # the template in the current folder
47
+ template-kit theme <name> # a named template
48
+ template-kit theme --all # every template in the workspace
49
+ ```
50
+
51
+ It reads `templates/<key>/theme.ts` (which must `export const theme: TokenTheme`), validates
52
+ it, and writes `templates/<key>/theme.css` with a "generated — do not edit" header. Import
53
+ that file into your Tailwind entry after `@import "tailwindcss"` and the kit's `styles.css`:
54
+
55
+ ```css
56
+ @import "tailwindcss";
57
+ @import "@homepages/template-kit/styles.css";
58
+ @import "./theme.css"; /* generated by `template-kit theme` */
59
+ ```
60
+
61
+ Re-run the command whenever you change `theme.ts`; a token you add becomes a working Tailwind
62
+ utility with nothing else to edit. A missing `theme.ts`, a missing `theme` export, or a theme
63
+ that fails validation each fail with an actionable message.
64
+
65
+ - e15de58: Your template now owns its design tokens end to end — and names them itself.
66
+
67
+ `TokenTheme` no longer imposes a vocabulary. Instead of satisfying a fixed set of
68
+ semantic roles (`ink`, `surfaceAlt`, `primaryStrong`, …), you declare whatever tokens
69
+ your design actually has, under kebab-case names you choose, and `compileThemeToCss`
70
+ generates the Tailwind `@theme inline` alias layer from them. Declare `lagoon` and
71
+ `bg-lagoon` exists. Nothing is required, and Tailwind's own defaults (`text-2xl`,
72
+ `rounded-lg`, `red-500`) still work underneath, so a sparse theme is a working theme.
73
+
74
+ This also closes the hole that made the old "open palette" a fiction: a brand swatch
75
+ only became a usable utility once it was added to the kit's shared `styles.css`, so it
76
+ was never really yours. There is no shared file to edit any more.
77
+
78
+ **Upgrading — three changes:**
79
+
80
+ 1. **Rewrite `theme.ts`.** `colors` and `palette` merge into one open record, and keys
81
+ become kebab-case (`inkSoft` → `ink-soft`). `fontFaces` moves out of `fonts` to the
82
+ top level. Emission is verbatim: the key `ink-soft` becomes `--tr-color-ink-soft` and
83
+ the `text-ink-soft` utility.
84
+
85
+ One token yields exactly one utility, so two utility names mean two tokens. If you
86
+ relied on an alias that pointed two names at one value, declare both.
87
+
88
+ 2. **Add a `document` block.** The `body` defaults can no longer be assumed — there is no
89
+ `ink` to guess at — so name the tokens that dress the page:
90
+
91
+ ```ts
92
+ document: { font: "sans", color: "ink", background: "page", fontSize: "base" }
93
+ ```
94
+
95
+ Each reference is validated against your declared tokens: a typo fails at build
96
+ rather than rendering an unthemed page in silence.
97
+
98
+ 3. **Import your compiled theme into your Tailwind entry.** `@theme` is a Tailwind
99
+ directive and is inert outside the build graph, so the theme partial must be
100
+ `@import`ed by your entry, after the kit's `styles.css`:
101
+
102
+ ```css
103
+ @import "tailwindcss";
104
+ @import "@homepages/template-kit/styles.css";
105
+ @import "./theme.css"; /* compileThemeToCss output */
106
+ ```
107
+
108
+ `styles.css` and `base.css` keep their export paths — no import changes. `styles.css`
109
+ now carries only the breakpoint ladder, the `--spacing` base unit, and the motion
110
+ defaults.
111
+
112
+ The `Image` primitive no longer paints its frame with `bg-surface-alt` (a token your
113
+ theme may not declare). The fill lives on `.tr-image-frame` in `base.css`; retint it by
114
+ setting `--tr-image-frame-bg`.
115
+
116
+ - 57eddee: Migrate the schema system from Zod 3 to Zod 4.
117
+
118
+ `zod` is the kit's one runtime dependency, and it now tracks Zod 4 (`"zod": "^4"`).
119
+ Because the types you author section schemas against are Zod types, bumping Zod a
120
+ major is a breaking change for authors: your workspace resolves Zod 4 in its
121
+ lockfile, and the schema/slot types you see in editor tooltips are now Zod 4 types.
122
+
123
+ **Action required** — if your workspace declares `zod`, move it to `^4` so a single
124
+ Zod copy resolves across your workspace and the kit:
125
+
126
+ ```bash
127
+ npm i zod@^4
128
+ ```
129
+
130
+ Nothing else changes. The authoring API is untouched — the slot-type catalog, the
131
+ source/transform vocabulary, and `SectionProps<typeof schema>` inference are all
132
+ identical, and every section still materializes to a byte-identical serialized
133
+ schema. This is a dependency migration, invisible to an author apart from the Zod
134
+ version in the lockfile.
135
+
136
+ ### Patch Changes
137
+
138
+ - 6675c4c: `no-hex` no longer reports a `color-mix()` whose colour arguments are all theme-derived.
139
+ A mix over `var(--…)` references, `transparent`, or `currentColor` carries no palette of
140
+ its own — it composes a state colour (hover, active, pressed) out of the theme the
141
+ section was handed — so banning it contradicted the rule's own reason. A hard-coded
142
+ argument still reports: `color-mix(in srgb, var(--tr-color-primary) 80%, #fff)` and
143
+ `color-mix(…, white)` are violations, as is a `var()` fallback that bakes a colour
144
+ (`var(--x, #fff)`). The carve-out is `color-mix()` only — `rgb()`, `oklch()`, and the
145
+ rest take raw channel values and stay banned outright, so the "a section carries zero
146
+ palette" guarantee is unchanged.
147
+ - 1749cea: `no-hex` now ships as a **warning**, not an error. Centralized theme colours are
148
+ encouraged, not gated: a hard-coded colour still squiggles in the editor and the rule
149
+ still explains how to derive the colour from theme tokens, but it no longer fails
150
+ `template-kit check` locally or at ingest (both gate on errors only). Every other preset
151
+ rule stays an error — this is the one advisory rule. The `color-mix()` over theme-derived
152
+ arguments carve-out is unchanged; it just governs a warning now instead of an error.
153
+
3
154
  ## 0.2.0
4
155
 
5
156
  ### Minor Changes
package/README.md CHANGED
@@ -1,21 +1,32 @@
1
1
  # @homepages/template-kit
2
2
 
3
3
  The authoring surface for HomePages marketing-section templates: schema system,
4
- contract primitives, theme tokens, and the `template-kit` CLI.
4
+ contract primitives, the theme system, and the `template-kit` CLI.
5
5
 
6
6
  **Status: 0.0.x.** The **schema system** (the section authoring contract and the closed
7
- vocabulary it validates against), the **contract primitives**, and the **theme tokens**
7
+ vocabulary it validates against), the **contract primitives**, and the **theme system**
8
8
  are complete — a section is authorable end to end against the kit. The `template-kit`
9
- CLI's commands are still being extracted.
9
+ CLI's `check` and `theme` commands are implemented see
10
+ [Checking a workspace](#checking-a-workspace) and
11
+ [Generating a theme](#generating-a-theme) below; its other commands arrive with the
12
+ surfaces they drive.
10
13
 
11
14
  ## Install
12
15
 
13
16
  ```bash
14
17
  npm install @homepages/template-kit react react-dom
18
+ npm install -D typescript eslint @typescript-eslint/parser esbuild # the authoring toolchain
15
19
  ```
16
20
 
17
21
  React 19 is a **peer dependency** — the kit never bundles its own copy.
18
22
 
23
+ The kit has exactly **one runtime dependency** (`zod`). The authoring toolchain the
24
+ [`check` command](docs/check.md) and the [ESLint preset](docs/eslint.md) run on is
25
+ yours: both resolve TypeScript, ESLint, the parser and esbuild from your workspace, so
26
+ your pinned versions are the ones that judge your code. The kit does not depend on them
27
+ at all — which is why a linter never lands in the production install of an app that
28
+ merely renders a published template.
29
+
19
30
  ## Authoring a section
20
31
 
21
32
  A section is four files: `schema.ts` (slots + options), `fill-spec.ts` (how AI
@@ -64,7 +75,7 @@ Full contract: **[docs/primitives.md](docs/primitives.md)**.
64
75
  | `@homepages/template-kit` | Authoring API — schema system, primitives, marker contract, theme types, and the golden fixtures |
65
76
  | `@homepages/template-kit/island-runtime` | Browser loader that hydrates a page's islands |
66
77
  | `@homepages/template-kit/browser` | Browser helpers for published pages (site config, signed ingest) |
67
- | `@homepages/template-kit/styles.css` | Tailwind v4 theme tokens, the breakpoint ladder, kit source registration |
78
+ | `@homepages/template-kit/styles.css` | The global Tailwind preamble: the breakpoint ladder, the spacing base unit, the motion defaults, kit source registration |
68
79
  | `@homepages/template-kit/base.css` | Cascade-layer order, the three reset gaps Tailwind's preflight leaves, `.tr-section`, and the motion tokens |
69
80
  | `@homepages/template-kit/eslint` | ESLint flat-config preset |
70
81
  | `@homepages/template-kit/tsconfig` | tsconfig preset (`"extends"`) |
@@ -72,18 +83,21 @@ Full contract: **[docs/primitives.md](docs/primitives.md)**.
72
83
  The authoring API has **no subpath imports** — one specifier for everything you
73
84
  write a section against. The two CSS entries are the deliberate exception.
74
85
 
75
- `styles.css` does **not** import Tailwind. Import it *after* Tailwind in your entry:
86
+ `styles.css` does **not** import Tailwind. Import it *after* Tailwind in your entry, then
87
+ your generated theme after that:
76
88
 
77
89
  ```css
78
90
  @import "tailwindcss";
79
91
  @import "@homepages/template-kit/styles.css";
92
+ @import "./theme.css"; /* generated by `template-kit theme` from theme.ts */
80
93
  ```
81
94
 
82
- Two `@import "tailwindcss"` in one graph double-emits preflight. `base.css` is not part
83
- of that entry it loads as its own stylesheet on the page. `base.css` ships no reset
84
- of its own it depends on the preflight your entry's `@import "tailwindcss"` brings.
85
- Both must be on the page. Full contract:
86
- **[docs/theme-and-css.md](docs/theme-and-css.md)**.
95
+ Two `@import "tailwindcss"` in one graph double-emits preflight. Your generated theme
96
+ belongs in this entry and nowhere else: it carries an `@theme` block, a Tailwind directive
97
+ that is inert outside the build graph. `base.css` is not part of that entry it loads as
98
+ its own stylesheet on the page. `base.css` ships no reset of its own — it depends on the
99
+ preflight your entry's `@import "tailwindcss"` brings. Both must be on the page. Full
100
+ contract: **[docs/theme-and-css.md](docs/theme-and-css.md)**.
87
101
 
88
102
  ## Rules
89
103
 
@@ -93,6 +107,33 @@ The lint preset and the `template-kit check` CLI share one id namespace,
93
107
  Start at **[docs/rules/INDEX.md](docs/rules/INDEX.md)**; the full doc set is routed
94
108
  from **[docs/INDEX.md](docs/INDEX.md)**.
95
109
 
110
+ ## Generating a theme
111
+
112
+ ```bash
113
+ template-kit theme # infer the template from cwd, or the workspace's one template
114
+ template-kit theme --all # generate every template's theme.css
115
+ ```
116
+
117
+ `theme` compiles a template's `theme.ts` into `templates/<key>/theme.css` — the file your
118
+ Tailwind entry imports. The output is generated: don't hand-edit it, and re-run the
119
+ command whenever `theme.ts` changes. Full contract:
120
+ **[docs/theme-and-css.md](docs/theme-and-css.md#generating-themecss)**.
121
+
122
+ ## Checking a workspace
123
+
124
+ ```bash
125
+ template-kit check # infer the template from cwd, or the workspace's one template
126
+ template-kit check --all # check every template
127
+ template-kit check --json # machine-readable report
128
+ ```
129
+
130
+ `check` typechecks, lints, and validates a template's schema/fill-spec/fixtures/manifest
131
+ contracts, runs a determinism render probe, and enforces per-section byte budgets —
132
+ plus workspace-wide dependency gates (lockfile health, a single React, license and
133
+ vulnerability checks) evaluated once per run. It exits non-zero on any failure and is
134
+ the same gate the platform re-runs when you submit a template. Full contract:
135
+ **[docs/check.md](docs/check.md)**.
136
+
96
137
  ## Development
97
138
 
98
139
  ```bash
@@ -101,11 +142,13 @@ npm run check # typecheck + lint + build + island-size gate + test + publint
101
142
  ```
102
143
 
103
144
  `npm run verify:consumer` is the boundary gate: it packs the kit, installs the
104
- tarball into a scratch project, and asserts that types resolve; that a real section
145
+ tarball into scratch projects, and asserts that a service which pins the kit and
146
+ deploys with `npm ci --omit=dev` installs no ESLint or esbuild package at all, with
147
+ `dependencies` carrying nothing but `zod`; that types resolve; that a real section
105
148
  triple typechecks against the installed package, with a misspelled property fact or
106
- slot key still failing to compile; that Tailwind's cross-package `@source` scan
107
- reaches the shipped `dist`; and that only one React ends up in the tree. Run it
108
- before every release.
149
+ slot key still failing to compile; that the ESLint preset lints using the consumer's
150
+ own parser; that Tailwind's cross-package `@source` scan reaches the shipped `dist`;
151
+ and that only one React ends up in the tree. Run it before every release.
109
152
 
110
153
  The island loader (`@homepages/template-kit/island-runtime`) has its own gate,
111
154
  `npm run size:islands`: it must stay under 3072 B gzip and never import
@@ -131,9 +174,10 @@ token anywhere, and adding one would silently disable OIDC.
131
174
 
132
175
  3. **Merge the Version Packages PR.** That merge publishes to npm.
133
176
 
134
- Semver contract: the platform vocabulary (slot types, sources, transforms,
135
- tokens) is closed and versioned. A breaking change to it is a **major**; a new
136
- optional capability is a **minor**.
177
+ Semver contract: the platform vocabulary (slot types, sources, transforms) is
178
+ closed and versioned. A breaking change to it is a **major**; a new optional
179
+ capability is a **minor**. Design tokens are *not* part of that vocabulary —
180
+ your template names its own.
137
181
 
138
182
  ### Bumping the version by hand
139
183
 
package/dist/base.css CHANGED
@@ -9,6 +9,7 @@
9
9
  * - The cascade-layer order declaration above — the one place it is stated
10
10
  * - The three reset gaps Tailwind's preflight leaves (below)
11
11
  * - The section box model (.tr-section, emitted by the <Section> primitive)
12
+ * - The <Image> frame's placeholder fill (.tr-image-frame)
12
13
  * - Universal tokens: motion
13
14
  *
14
15
  * REQUIRES A TAILWIND ENTRY ON THE PAGE. Your entry's `@import "tailwindcss"` brings
@@ -54,6 +55,14 @@
54
55
  margin: 0;
55
56
  }
56
57
 
58
+ /* The <Image> frame's placeholder fill, showing while an image is absent or loading.
59
+ * It is a plain class rather than a `bg-*` utility on the primitive because a template
60
+ * names its own tokens — there is no colour token this primitive may assume exists.
61
+ * Retint it by setting --tr-image-frame-bg in your theme's layout tokens. */
62
+ .tr-image-frame {
63
+ background-color: var(--tr-image-frame-bg, oklch(0.97 0 0));
64
+ }
65
+
57
66
  /* ----- Universal tokens ------------------------------------------------- */
58
67
 
59
68
  :root {
@@ -0,0 +1,37 @@
1
+ //#region src/cli/check/config.ts
2
+ const CHECK_CONFIG = {
3
+ size: {
4
+ /** Gzipped bytes of a section's renderer bundle, React excluded. */
5
+ rendererBundleGzip: 50 * 1024,
6
+ /** Gzipped bytes of a section's stylesheet, including CSS pulled in from packages. */
7
+ sectionCssGzip: 50 * 1024
8
+ },
9
+ audit: {
10
+ /** A vulnerability at or above this severity fails the check. */
11
+ failOn: ["high", "critical"] },
12
+ /**
13
+ * A package whose license is not on this list fails the check. A missing
14
+ * license field, or a bespoke "SEE LICENSE IN …", is a denial — not a pass.
15
+ */
16
+ licenseAllowlist: [
17
+ "MIT",
18
+ "ISC",
19
+ "Apache-2.0",
20
+ "BSD-2-Clause",
21
+ "BSD-3-Clause",
22
+ "0BSD",
23
+ "Unlicense",
24
+ "CC0-1.0"
25
+ ],
26
+ /**
27
+ * The license gate walks the *shipped* dependency graph of every third-party
28
+ * package a template pulls in — this is the first-party exception to that.
29
+ * The kit itself is `UNLICENSED` by design (it is not published for reuse
30
+ * outside this platform) and every workspace depends on it, so without this
31
+ * exemption the gate would deny the kit on a stock, correct workspace.
32
+ */
33
+ licenseExemptions: ["@homepages/template-kit"]
34
+ };
35
+
36
+ //#endregion
37
+ export { CHECK_CONFIG };
@@ -0,0 +1,117 @@
1
+ import { loadEsbuild } from "./resolve-tool.js";
2
+ import { existsSync } from "node:fs";
3
+ import { join } from "node:path";
4
+ import { readdir } from "node:fs/promises";
5
+
6
+ //#region src/cli/check/css.ts
7
+ /**
8
+ * Every `.css` file anywhere in a section folder (the folder is free-form —
9
+ * an author may nest CSS under components/, enhancers/, wherever), in
10
+ * published order: `styles.css` first (the conventional root-level home),
11
+ * then every other file sorted by path. `snapshots/` and `node_modules/` are
12
+ * skipped — generated or not authored, never hand-CSS.
13
+ */
14
+ async function sectionCssFiles(dir) {
15
+ if (!existsSync(dir)) return [];
16
+ const found = [];
17
+ const walk = async (d) => {
18
+ for (const entry of await readdir(d, { withFileTypes: true })) {
19
+ const full = join(d, entry.name);
20
+ if (entry.isDirectory()) {
21
+ if (entry.name === "snapshots" || entry.name === "node_modules") continue;
22
+ await walk(full);
23
+ } else if (entry.isFile() && entry.name.endsWith(".css")) found.push(full);
24
+ }
25
+ };
26
+ await walk(dir);
27
+ const own = join(dir, "styles.css");
28
+ const rest = found.filter((f) => f !== own).sort();
29
+ return (existsSync(own) ? [own] : []).concat(rest);
30
+ }
31
+ const CSS_ASSET_LOADERS = {
32
+ ".svg": "dataurl",
33
+ ".png": "dataurl",
34
+ ".jpg": "dataurl",
35
+ ".jpeg": "dataurl",
36
+ ".gif": "dataurl",
37
+ ".webp": "dataurl",
38
+ ".woff": "dataurl",
39
+ ".woff2": "dataurl",
40
+ ".ttf": "dataurl",
41
+ ".eot": "dataurl"
42
+ };
43
+ /**
44
+ * A section's third-party package CSS — discovered by the bundler, not
45
+ * declared. An author writes `import "swiper/css"` in Renderer.tsx exactly
46
+ * as in React; this runs a CSS-only esbuild pass rooted at Renderer.tsx and
47
+ * returns the emitted stylesheet (empty string if there is none).
48
+ *
49
+ * A relative CSS import in the render path is a lint error
50
+ * (`no-css-import-from-render-path`, already enforced by ESLint) — this pass
51
+ * does not re-implement that rejection, it only has to not crash on one.
52
+ *
53
+ * `platform: "neutral"` defaults `mainFields` to `[]`, which cannot resolve a
54
+ * package that ships only a CJS `main` (no `exports` map) — `mainFields`
55
+ * is set explicitly so such a package still resolves to its ESM `module`
56
+ * build where one exists.
57
+ */
58
+ async function compileSectionPackageCss(workspaceRoot, sectionDir) {
59
+ const renderer = join(sectionDir, "Renderer.tsx");
60
+ if (!existsSync(renderer)) return "";
61
+ const { build } = await loadEsbuild(workspaceRoot);
62
+ const css = (await build({
63
+ entryPoints: [renderer],
64
+ bundle: true,
65
+ write: false,
66
+ minify: true,
67
+ outdir: join(workspaceRoot, "node_modules", ".template-kit", "check-css"),
68
+ format: "esm",
69
+ target: "es2022",
70
+ platform: "neutral",
71
+ mainFields: ["module", "main"],
72
+ jsx: "automatic",
73
+ external: [
74
+ "react",
75
+ "react-dom",
76
+ "react/jsx-runtime"
77
+ ],
78
+ loader: { ...CSS_ASSET_LOADERS },
79
+ absWorkingDir: workspaceRoot,
80
+ logLevel: "silent"
81
+ })).outputFiles?.find((f) => f.path.endsWith(".css"));
82
+ return css ? css.text : "";
83
+ }
84
+ /**
85
+ * The 1-based line of the first top-level `@import`, or `undefined` if there
86
+ * is none. Comment-aware: an `@import` mentioned inside a CSS block comment
87
+ * is prose (e.g. a header warning authors not to write one), not a rule — a
88
+ * naive line-regex would false-fire on it.
89
+ */
90
+ function findImportLine(css) {
91
+ const lines = css.split("\n");
92
+ let inComment = false;
93
+ for (let i = 0; i < lines.length; i++) {
94
+ let line = lines[i];
95
+ if (inComment) {
96
+ const close = line.indexOf("*/");
97
+ if (close === -1) continue;
98
+ line = line.slice(close + 2);
99
+ inComment = false;
100
+ }
101
+ for (;;) {
102
+ const open = line.indexOf("/*");
103
+ if (open === -1) break;
104
+ const close = line.indexOf("*/", open + 2);
105
+ if (close === -1) {
106
+ line = line.slice(0, open);
107
+ inComment = true;
108
+ break;
109
+ }
110
+ line = line.slice(0, open) + line.slice(close + 2);
111
+ }
112
+ if (/^\s*@import\b/.test(line)) return i + 1;
113
+ }
114
+ }
115
+
116
+ //#endregion
117
+ export { CSS_ASSET_LOADERS, compileSectionPackageCss, findImportLine, sectionCssFiles };
@@ -0,0 +1,32 @@
1
+ //#region src/cli/check/diagnostics.ts
2
+ function location(d) {
3
+ if (!d.file) return "";
4
+ return d.line === void 0 ? d.file : `${d.file}:${d.line}`;
5
+ }
6
+ function formatOne(d) {
7
+ const where = location(d);
8
+ return `${where ? ` ${where}\n` : ""} [${d.ruleId}] ${d.message}\n fix: ${d.fix}\n`;
9
+ }
10
+ function formatHuman(report) {
11
+ const parts = [];
12
+ for (const [key, t] of Object.entries(report.templates)) {
13
+ if (t.ok) {
14
+ parts.push(`✓ ${key}\n`);
15
+ continue;
16
+ }
17
+ parts.push(`✗ ${key} — ${String(t.diagnostics.length)} problem(s)\n`);
18
+ for (const d of t.diagnostics) parts.push(formatOne(d));
19
+ }
20
+ if (!report.workspace.ok) {
21
+ parts.push(`✗ workspace — ${String(report.workspace.diagnostics.length)} problem(s)\n`);
22
+ for (const d of report.workspace.diagnostics) parts.push(formatOne(d));
23
+ }
24
+ if (report.ok) parts.push("check passed\n");
25
+ return parts.join("");
26
+ }
27
+ function toJson(report) {
28
+ return JSON.stringify(report, null, 2);
29
+ }
30
+
31
+ //#endregion
32
+ export { formatHuman, toJson };
@@ -0,0 +1,87 @@
1
+ import { KIT_VERSION } from "../../index.js";
2
+ import { formatHuman, toJson } from "./diagnostics.js";
3
+ import { loadEsbuild } from "./resolve-tool.js";
4
+ import { resetLoadCache } from "./loader.js";
5
+ import { depsStage } from "./stages/deps.js";
6
+ import { lintStage } from "./stages/lint.js";
7
+ import { renderStage } from "./stages/render.js";
8
+ import { sizeStage } from "./stages/size.js";
9
+ import { treeStage } from "./stages/tree.js";
10
+ import { typecheckStage } from "./stages/typecheck.js";
11
+ import { validateStage } from "./stages/validate/index.js";
12
+ import { findWorkspaceRoot, selectTemplates, templateFromCwd } from "./workspace.js";
13
+ import { parseArgs } from "node:util";
14
+
15
+ //#region src/cli/check/index.ts
16
+ const STAGES = [
17
+ typecheckStage,
18
+ lintStage,
19
+ validateStage,
20
+ treeStage,
21
+ renderStage,
22
+ sizeStage
23
+ ];
24
+ /**
25
+ * A run that gated nothing must never report success. `every()` over an empty set
26
+ * is vacuously true, so a selection of zero templates printed "check passed" and
27
+ * exited 0 having run not one gate — the worst failure this command has, since
28
+ * ingest re-runs it and would accept the submission on that same silence.
29
+ */
30
+ function emptySelectionDiagnostics(selected, all) {
31
+ if (selected.length > 0) return [];
32
+ return [{
33
+ ruleId: "template-kit/no-templates",
34
+ template: "",
35
+ file: "templates",
36
+ message: all ? "check --all found no template to check: templates/ holds no template folder." : "This workspace has no template to check: templates/ holds no template folder.",
37
+ fix: "Add a template: templates/<key>/ with a manifest.json declaring its section composition and a sections/ folder holding the sections it composes."
38
+ }];
39
+ }
40
+ async function checkTemplate(root, template) {
41
+ const diagnostics = [];
42
+ for (const stage of STAGES) diagnostics.push(...await stage(root, template));
43
+ return {
44
+ ok: diagnostics.length === 0,
45
+ diagnostics
46
+ };
47
+ }
48
+ async function runCheck(argv, cwd) {
49
+ resetLoadCache();
50
+ const { values, positionals } = parseArgs({
51
+ args: argv,
52
+ options: {
53
+ all: {
54
+ type: "boolean",
55
+ default: false
56
+ },
57
+ json: {
58
+ type: "boolean",
59
+ default: false
60
+ }
61
+ },
62
+ allowPositionals: true
63
+ });
64
+ const root = await findWorkspaceRoot(cwd);
65
+ const selected = await selectTemplates(root, positionals[0] ?? (values.all ? void 0 : templateFromCwd(root, cwd)), values.all, "check");
66
+ if (selected.length > 0) await loadEsbuild(root);
67
+ const templates = {};
68
+ for (const template of selected) templates[template.key] = await checkTemplate(root, template);
69
+ const workspaceDiagnostics = [...await depsStage(root), ...emptySelectionDiagnostics(selected, values.all)];
70
+ const workspace = {
71
+ ok: workspaceDiagnostics.length === 0,
72
+ diagnostics: workspaceDiagnostics
73
+ };
74
+ const report = {
75
+ ok: workspace.ok && Object.values(templates).every((t) => t.ok),
76
+ kitVersion: KIT_VERSION,
77
+ templates,
78
+ workspace
79
+ };
80
+ return {
81
+ code: report.ok ? 0 : 1,
82
+ output: values.json ? toJson(report) : formatHuman(report)
83
+ };
84
+ }
85
+
86
+ //#endregion
87
+ export { runCheck };