@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.
- package/CHANGELOG.md +151 -0
- package/README.md +61 -17
- package/dist/base.css +9 -0
- package/dist/cli/check/config.js +37 -0
- package/dist/cli/check/css.js +117 -0
- package/dist/cli/check/diagnostics.js +32 -0
- package/dist/cli/check/index.js +87 -0
- package/dist/cli/check/loader.js +142 -0
- package/dist/cli/check/paths.js +15 -0
- package/dist/cli/check/relativize.js +18 -0
- package/dist/cli/check/render-invariants.js +24 -0
- package/dist/cli/check/resolve-tool.js +38 -0
- package/dist/cli/check/stages/deps.js +337 -0
- package/dist/cli/check/stages/lint.js +46 -0
- package/dist/cli/check/stages/render.js +101 -0
- package/dist/cli/check/stages/size.js +158 -0
- package/dist/cli/check/stages/tree.js +207 -0
- package/dist/cli/check/stages/typecheck.js +46 -0
- package/dist/cli/check/stages/validate/catalog-exhaustiveness.js +18 -0
- package/dist/cli/check/stages/validate/facts.js +18 -0
- package/dist/cli/check/stages/validate/formatter-contract.js +14 -0
- package/dist/cli/check/stages/validate/group-contract.js +21 -0
- package/dist/cli/check/stages/validate/index.js +66 -0
- package/dist/cli/check/stages/validate/list-scalar-contract.js +22 -0
- package/dist/cli/check/stages/validate/nav-contract.js +93 -0
- package/dist/cli/check/stages/validate/nested-slot-contract.js +12 -0
- package/dist/cli/check/stages/validate/object-list-contract.js +31 -0
- package/dist/cli/check/stages/validate/orchestrator.js +241 -0
- package/dist/cli/check/stages/validate/produced-by-contract.js +18 -0
- package/dist/cli/check/stages/validate/row-contract.js +31 -0
- package/dist/cli/check/stages/validate/select-contract.js +13 -0
- package/dist/cli/check/stages/validate/source-contract.js +21 -0
- package/dist/cli/check/stages/validate/variant-contract.js +15 -0
- package/dist/cli/check/workspace.js +86 -0
- package/dist/cli/theme/generate.js +43 -0
- package/dist/cli/theme/index.js +33 -0
- package/dist/cli/theme/load-theme.js +69 -0
- package/dist/cli.js +22 -4
- package/dist/contracts/fill-treatments.js +47 -0
- package/dist/design-system/theme.d.ts +30 -300
- package/dist/design-system/theme.js +112 -90
- package/dist/eslint/rules/no-hex.js +78 -6
- package/dist/eslint.js +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.js +2 -2
- package/dist/package.js +1 -1
- package/dist/primitives/Image.js +1 -1
- package/dist/schema/fill-spec.d.ts +80 -590
- package/dist/schema/fixture-schema.d.ts +5 -81
- package/dist/schema/manifest.d.ts +39 -475
- package/dist/schema/resolve-section-ref.js +10 -0
- package/dist/schema/rows.js +10 -0
- package/dist/schema/section-nav.js +9 -0
- package/dist/schema/section-schema.d.ts +51 -437
- package/dist/schema/slot-types.d.ts +12 -16
- package/dist/styles.css +31 -88
- package/docs/INDEX.md +4 -3
- package/docs/check.md +121 -0
- package/docs/eslint.md +18 -8
- package/docs/llms.txt +21 -9
- package/docs/primitives.md +5 -0
- package/docs/rules/INDEX.md +11 -4
- package/docs/rules/bundle-binary-asset.md +102 -0
- package/docs/rules/fixtures-invalid.md +96 -0
- package/docs/rules/license-denied.md +11 -3
- package/docs/rules/manifest-invalid.md +99 -0
- package/docs/rules/no-hex.md +76 -10
- package/docs/rules/no-templates.md +87 -0
- package/docs/rules/parse-error.md +76 -0
- package/docs/rules/schema-invalid.md +96 -0
- package/docs/rules/size-section-css.md +2 -2
- package/docs/theme-and-css.md +155 -90
- package/package.json +4 -9
- 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
|
|
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
|
|
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
|
|
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` |
|
|
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.
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
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
|
|
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
|
|
107
|
-
|
|
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
|
-
|
|
136
|
-
|
|
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 };
|