@homepages/template-kit 0.0.1 → 0.2.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 +251 -0
- package/README.md +77 -11
- package/dist/base.css +64 -0
- package/dist/browser.d.ts +26 -0
- package/dist/browser.js +55 -0
- package/dist/cli.js +1 -1
- package/dist/contracts/image-descriptor.d.ts +17 -0
- package/dist/contracts/markers.d.ts +20 -0
- package/dist/contracts/markers.js +21 -0
- package/dist/contracts/slot-catalog.d.ts +237 -0
- package/dist/contracts/slot-catalog.js +247 -0
- package/dist/contracts/transforms.d.ts +108 -0
- package/dist/contracts/transforms.js +117 -0
- package/dist/design-system/breakpoints.d.ts +5 -0
- package/dist/design-system/breakpoints.js +14 -0
- package/dist/design-system/theme.d.ts +308 -0
- package/dist/design-system/theme.js +102 -0
- package/dist/eslint/is-client-file.js +79 -0
- package/dist/eslint/rules/image-bare-needs-reason.js +43 -0
- package/dist/eslint/rules/no-client-directive-in-contract.js +31 -0
- package/dist/eslint/rules/no-client-runtime-in-server.js +103 -0
- package/dist/eslint/rules/no-css-import-from-render-path.js +29 -0
- package/dist/eslint/rules/no-hex.js +116 -0
- package/dist/eslint/rules/no-inline-style.js +27 -0
- package/dist/eslint/rules/no-nondeterminism.js +48 -0
- package/dist/eslint/rules/no-raw-element.js +37 -0
- package/dist/eslint/rules/props-from-schema.js +37 -0
- package/dist/eslint/rules/serializable-island-props.js +112 -0
- package/dist/eslint/rules/slot-marker-literal.js +38 -0
- package/dist/eslint.d.ts +4 -8
- package/dist/eslint.js +75 -11
- package/dist/fixtures/sample-property.d.ts +67 -0
- package/dist/fixtures/sample-property.js +707 -0
- package/dist/fixtures.d.ts +2 -0
- package/dist/fixtures.js +3 -0
- package/dist/index.d.ts +28 -12
- package/dist/index.js +34 -2
- package/dist/island-runtime.d.ts +30 -0
- package/dist/island-runtime.js +73 -0
- package/dist/islands/contract.d.ts +56 -0
- package/dist/islands/contract.js +71 -0
- package/dist/islands/marker.d.ts +16 -0
- package/dist/islands/marker.js +18 -0
- package/dist/package.js +5 -0
- package/dist/primitives/Image.d.ts +49 -0
- package/dist/primitives/Image.js +95 -0
- package/dist/primitives/Section.d.ts +22 -0
- package/dist/primitives/Section.js +26 -0
- package/dist/primitives/Slot.d.ts +50 -0
- package/dist/primitives/Slot.js +39 -0
- package/dist/primitives/SlotGroup.d.ts +38 -0
- package/dist/primitives/SlotGroup.js +24 -0
- package/dist/primitives/SlotItem.d.ts +43 -0
- package/dist/primitives/SlotItem.js +38 -0
- package/dist/primitives/index.d.ts +7 -0
- package/dist/primitives/picture-sources.d.ts +22 -0
- package/dist/primitives/picture-sources.js +52 -0
- package/dist/rules/registry.js +16 -0
- package/dist/schema/fill-spec.d.ts +692 -0
- package/dist/schema/fill-spec.js +98 -0
- package/dist/schema/fixture-schema.d.ts +143 -0
- package/dist/schema/fixture-schema.js +113 -0
- package/dist/schema/index.d.ts +16 -0
- package/dist/schema/manifest.d.ts +527 -0
- package/dist/schema/manifest.js +73 -0
- package/dist/schema/property-facts.d.ts +44 -0
- package/dist/schema/resolve-variants.d.ts +11 -0
- package/dist/schema/resolve-variants.js +15 -0
- package/dist/schema/runtime-values.d.ts +45 -0
- package/dist/schema/section-nav.d.ts +7 -0
- package/dist/schema/section-props.d.ts +69 -0
- package/dist/schema/section-schema.d.ts +533 -0
- package/dist/schema/section-schema.js +72 -0
- package/dist/schema/slot-types.d.ts +117 -0
- package/dist/schema/slot-types.js +181 -0
- package/dist/schema/source.d.ts +13 -0
- package/dist/schema/source.js +12 -0
- package/dist/schema/synthesize-nullable.d.ts +18 -0
- package/dist/schema/synthesize-nullable.js +47 -0
- package/dist/styles.css +131 -9
- package/docs/INDEX.md +7 -4
- package/docs/eslint.md +55 -0
- package/docs/islands.md +150 -0
- package/docs/llms.txt +33 -3
- package/docs/primitives.md +214 -0
- package/docs/rules/INDEX.md +58 -0
- package/docs/rules/audit-severity.md +91 -0
- package/docs/rules/bundle-incomplete.md +117 -0
- package/docs/rules/css-reason.md +94 -0
- package/docs/rules/determinism-drift.md +112 -0
- package/docs/rules/image-bare-needs-reason.md +117 -0
- package/docs/rules/license-denied.md +106 -0
- package/docs/rules/lockfile-missing.md +68 -0
- package/docs/rules/lockfile-stale.md +88 -0
- package/docs/rules/missing-slot-marker.md +140 -0
- package/docs/rules/no-bare-css-import.md +128 -0
- package/docs/rules/no-client-directive-in-contract.md +111 -0
- package/docs/rules/no-client-runtime-in-server.md +143 -0
- package/docs/rules/no-css-import-from-render-path.md +118 -0
- package/docs/rules/no-hex.md +97 -0
- package/docs/rules/no-inline-style.md +121 -0
- package/docs/rules/no-nondeterminism.md +100 -0
- package/docs/rules/no-raw-element.md +100 -0
- package/docs/rules/props-from-schema.md +119 -0
- package/docs/rules/render-invariant.md +165 -0
- package/docs/rules/serializable-island-props.md +146 -0
- package/docs/rules/server-vs-client.md +105 -0
- package/docs/rules/sidebar-order.md +109 -0
- package/docs/rules/single-react.md +97 -0
- package/docs/rules/size-renderer-bundle.md +109 -0
- package/docs/rules/size-section-css.md +95 -0
- package/docs/rules/slot-group-marker.md +127 -0
- package/docs/rules/slot-marker-literal.md +148 -0
- package/docs/rules/typecheck.md +103 -0
- package/docs/rules/unlayered-fence.md +113 -0
- package/docs/schema-system.md +240 -0
- package/docs/theme-and-css.md +225 -0
- package/package.json +30 -6
- package/tsconfig.base.json +16 -0
- package/tsconfig.json +2 -13
- package/dist/src-dZr5N30y.js +0 -35
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
---
|
|
2
|
+
purpose: The template-kit/bundle-incomplete authoring rule — what it enforces, why, and how to fix it.
|
|
3
|
+
status: living
|
|
4
|
+
related: [INDEX.md, ../schema-system.md]
|
|
5
|
+
updated: 2026-07-14
|
|
6
|
+
---
|
|
7
|
+
# template-kit/bundle-incomplete
|
|
8
|
+
|
|
9
|
+
> A section folder is **yours**. Its only structural requirement is that four contract
|
|
10
|
+
> files exist at its root.
|
|
11
|
+
|
|
12
|
+
## Rule
|
|
13
|
+
|
|
14
|
+
A section folder must contain these four, **as files, at the folder root**:
|
|
15
|
+
|
|
16
|
+
| File | Declares |
|
|
17
|
+
|---|---|
|
|
18
|
+
| `schema.ts` | the slots and options the section has |
|
|
19
|
+
| `fill-spec.ts` | how AI fills those slots |
|
|
20
|
+
| `fixtures.ts` | sample content for local preview |
|
|
21
|
+
| `Renderer.tsx` | the markup |
|
|
22
|
+
|
|
23
|
+
Each missing file is one violation, so a folder holding only a `Renderer.tsx` reports
|
|
24
|
+
three times.
|
|
25
|
+
|
|
26
|
+
Two near-misses do **not** satisfy it:
|
|
27
|
+
|
|
28
|
+
- a **directory** named `schema.ts/` — the check wants a file;
|
|
29
|
+
- a file at `components/schema.ts` — the four are looked for at the folder root, not
|
|
30
|
+
searched for down the tree.
|
|
31
|
+
|
|
32
|
+
Everything else in the folder is unconstrained. `components/`, `hooks/`, `utils/`, any
|
|
33
|
+
nesting, any number of support modules, islands, stylesheets, enhancers: the check has no
|
|
34
|
+
opinion about them, and neither does the rest of the kit.
|
|
35
|
+
|
|
36
|
+
## Reason
|
|
37
|
+
|
|
38
|
+
The four files are the platform's entire interface to a section. The publisher reads
|
|
39
|
+
`schema.ts` to know what the section contains, the fill pipeline reads `fill-spec.ts` to
|
|
40
|
+
fill it, preview reads `fixtures.ts`, and `Renderer.tsx` is the markup. A folder missing
|
|
41
|
+
one of them cannot be loaded as a section.
|
|
42
|
+
|
|
43
|
+
That freedom has one non-obvious consequence, worth knowing before it surprises you:
|
|
44
|
+
**every file in the folder is part of the section's identity.** The content hash covers
|
|
45
|
+
the whole folder, not just the four — so editing a helper, a util, or even a comment
|
|
46
|
+
produces a new section version on the next publish, exactly as editing `Renderer.tsx`
|
|
47
|
+
would.
|
|
48
|
+
|
|
49
|
+
## Fix
|
|
50
|
+
|
|
51
|
+
Create the missing file at the folder root. A section that fills nothing still needs a
|
|
52
|
+
`fill-spec.ts`; a section with no designed states still needs a `fixtures.ts`. Declaring
|
|
53
|
+
"nothing to fill" is a declaration.
|
|
54
|
+
|
|
55
|
+
### Before
|
|
56
|
+
|
|
57
|
+
```text
|
|
58
|
+
sections/hero/
|
|
59
|
+
Renderer.tsx
|
|
60
|
+
schema.ts
|
|
61
|
+
components/
|
|
62
|
+
Headline.tsx
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Two violations: `fill-spec.ts` and `fixtures.ts` are missing. `components/` is not one of
|
|
66
|
+
them — it was never a problem.
|
|
67
|
+
|
|
68
|
+
### After
|
|
69
|
+
|
|
70
|
+
```text
|
|
71
|
+
sections/hero/
|
|
72
|
+
Renderer.tsx
|
|
73
|
+
schema.ts
|
|
74
|
+
fill-spec.ts ← added
|
|
75
|
+
fixtures.ts ← added
|
|
76
|
+
components/
|
|
77
|
+
Headline.tsx
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
```ts
|
|
81
|
+
// sections/hero/fill-spec.ts
|
|
82
|
+
import type { FillSpecShape } from "@homepages/template-kit";
|
|
83
|
+
|
|
84
|
+
export const fillSpec = {
|
|
85
|
+
reads: [],
|
|
86
|
+
decisions: [
|
|
87
|
+
{
|
|
88
|
+
id: "headline_text",
|
|
89
|
+
type: "text-block",
|
|
90
|
+
produces: ["headline"],
|
|
91
|
+
length_cap: 60,
|
|
92
|
+
structure: "single_sentence",
|
|
93
|
+
voice_prompt: "Confident premium-listing voice. Title Case.",
|
|
94
|
+
},
|
|
95
|
+
],
|
|
96
|
+
} as const satisfies FillSpecShape;
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
```ts
|
|
100
|
+
// sections/hero/fixtures.ts
|
|
101
|
+
import type { FixtureModule } from "@homepages/template-kit";
|
|
102
|
+
|
|
103
|
+
const fixtures: FixtureModule = {
|
|
104
|
+
base: {
|
|
105
|
+
slots: { headline: "A mansard Victorian in the heart of Jamaica Plain" },
|
|
106
|
+
},
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
export default fixtures;
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
## See also
|
|
113
|
+
|
|
114
|
+
- [The schema system](../schema-system.md) — what each of the four files declares, and
|
|
115
|
+
the types they satisfy.
|
|
116
|
+
- [`missing-slot-marker`](missing-slot-marker.md) — the check that runs once the four
|
|
117
|
+
exist: every editable slot in `schema.ts` has a marker in the JSX.
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
---
|
|
2
|
+
purpose: The template-kit/css-reason authoring rule — what it enforces, why, and how to fix it.
|
|
3
|
+
status: living
|
|
4
|
+
related: [INDEX.md, ../theme-and-css.md]
|
|
5
|
+
updated: 2026-07-14
|
|
6
|
+
---
|
|
7
|
+
# template-kit/css-reason
|
|
8
|
+
|
|
9
|
+
> Hand-written CSS is the escape hatch from Tailwind, so every stylesheet must **open**
|
|
10
|
+
> with a comment saying what it escapes for.
|
|
11
|
+
|
|
12
|
+
## Rule
|
|
13
|
+
|
|
14
|
+
Every hand-written `.css` file — a section's `styles.css`, any other `.css` in a section
|
|
15
|
+
folder, and the template's own `styles.css` — must **begin** with a block comment
|
|
16
|
+
containing the token `css-reason:`.
|
|
17
|
+
|
|
18
|
+
Three ways to get this wrong:
|
|
19
|
+
|
|
20
|
+
- **The comment is not first.** The token must be in the file's *leading* comment.
|
|
21
|
+
Mentioning `css-reason:` further down, after a rule, does not pass.
|
|
22
|
+
- **The comment is a `//` line.** `//` does not open a comment in CSS. It is not a comment
|
|
23
|
+
at all — it is a parse error waiting to happen, and it does not satisfy this check.
|
|
24
|
+
- **The file is in a subdirectory and you assumed it was exempt.** CSS is collected **by
|
|
25
|
+
path, not by import**: the build walks the whole section folder. A stylesheet at
|
|
26
|
+
`sections/map/styles/markers.css` is gated — and published — exactly like `styles.css`.
|
|
27
|
+
|
|
28
|
+
The token is all that is checked, but the sentence after it is the point. Write it for the
|
|
29
|
+
next author.
|
|
30
|
+
|
|
31
|
+
## Reason
|
|
32
|
+
|
|
33
|
+
Tailwind is the default, and it is the default for a reason: a utility class is scoped to
|
|
34
|
+
the element it sits on, participates in the cascade layers the platform already orders,
|
|
35
|
+
and cannot leak into another section on the same page. Hand-written CSS gives all of that
|
|
36
|
+
up.
|
|
37
|
+
|
|
38
|
+
There is exactly one thing utilities cannot do: style **DOM that Tailwind's scanner never
|
|
39
|
+
sees**. Markup an enhancer builds at runtime has no class in your source for the scanner
|
|
40
|
+
to find, and a third-party widget's own DOM was never yours to put classes on. Those are
|
|
41
|
+
the cases hand-CSS exists for.
|
|
42
|
+
|
|
43
|
+
We cannot cheaply prove that a given rule is *inexpressible* as a utility — so the check
|
|
44
|
+
enforces the half of the bar that is checkable: **you have to say which un-scannable DOM
|
|
45
|
+
you are styling.** A stylesheet that cannot name one is almost always a stylesheet whose
|
|
46
|
+
rules should have been utility classes, and a reasonless file leaves the next author
|
|
47
|
+
unable to tell the difference.
|
|
48
|
+
|
|
49
|
+
## Fix
|
|
50
|
+
|
|
51
|
+
Make the first thing in the file a block comment that names the DOM Tailwind cannot see.
|
|
52
|
+
If you find you cannot name one, that is the answer: delete the CSS and use utility
|
|
53
|
+
classes on the markup.
|
|
54
|
+
|
|
55
|
+
### Before
|
|
56
|
+
|
|
57
|
+
```css
|
|
58
|
+
/* sections/neighborhood-map/styles.css */
|
|
59
|
+
.tr-map-marker {
|
|
60
|
+
display: grid;
|
|
61
|
+
place-items: center;
|
|
62
|
+
border-radius: var(--tr-radius-full);
|
|
63
|
+
background: var(--tr-color-primary);
|
|
64
|
+
}
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
### After
|
|
68
|
+
|
|
69
|
+
```css
|
|
70
|
+
/* css-reason: styles the marker DOM the neighborhood-map enhancer builds at runtime —
|
|
71
|
+
the nodes are created by the map library after hydration, so Tailwind's scanner never
|
|
72
|
+
sees a class to compile. */
|
|
73
|
+
.tr-map-marker {
|
|
74
|
+
display: grid;
|
|
75
|
+
place-items: center;
|
|
76
|
+
border-radius: var(--tr-radius-full);
|
|
77
|
+
background: var(--tr-color-primary);
|
|
78
|
+
}
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
And the case where the fix is not a comment at all — this rule *is* expressible as
|
|
82
|
+
utilities, so the stylesheet goes away and the classes move onto the element:
|
|
83
|
+
|
|
84
|
+
```tsx
|
|
85
|
+
<div className="grid place-items-center rounded-full bg-primary" />
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
## See also
|
|
89
|
+
|
|
90
|
+
- [Theme and CSS](../theme-and-css.md) — the token namespace utilities compile from, and
|
|
91
|
+
the layer order hand-CSS lands in.
|
|
92
|
+
- [`no-bare-css-import`](no-bare-css-import.md) — the other constraint on a hand-written
|
|
93
|
+
stylesheet: no `@import`, ever.
|
|
94
|
+
- [`unlayered-fence`](unlayered-fence.md) — the escape hatch *within* the escape hatch.
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
---
|
|
2
|
+
purpose: The template-kit/determinism-drift authoring rule — what it enforces, why, and how to fix it.
|
|
3
|
+
status: living
|
|
4
|
+
related: [INDEX.md, server-vs-client.md]
|
|
5
|
+
updated: 2026-07-14
|
|
6
|
+
---
|
|
7
|
+
# template-kit/determinism-drift
|
|
8
|
+
|
|
9
|
+
> The same fixture, rendered twice, must produce **byte-identical** HTML. This is the
|
|
10
|
+
> runtime back-stop to `no-nondeterminism` — it catches what a linter cannot see.
|
|
11
|
+
|
|
12
|
+
## Rule
|
|
13
|
+
|
|
14
|
+
Every fixture in the section's invariant corpus is server-rendered **twice**, and the two
|
|
15
|
+
outputs are compared byte for byte. Any difference is a violation.
|
|
16
|
+
|
|
17
|
+
The corpus is broader than the fixtures you wrote:
|
|
18
|
+
|
|
19
|
+
- your **`base`** fixture;
|
|
20
|
+
- each of your **`states`** fixtures;
|
|
21
|
+
- one **synthesized** fixture per **optional** slot, with that slot set to its empty value
|
|
22
|
+
(`null` for a scalar or an image, `[]` for a list).
|
|
23
|
+
|
|
24
|
+
You do not declare that last set. Nullability coverage is derived from the schema — from
|
|
25
|
+
`(type, required)` — so a nullable branch cannot be forgotten by forgetting to write a
|
|
26
|
+
fixture for it.
|
|
27
|
+
|
|
28
|
+
## Reason
|
|
29
|
+
|
|
30
|
+
[`no-nondeterminism`](no-nondeterminism.md) bans the clock and randomness **in your code**.
|
|
31
|
+
A linter reads one file at a time and can only report on what it can see, so it stops at
|
|
32
|
+
your source. This check runs the render.
|
|
33
|
+
|
|
34
|
+
That gap is where the real bugs live, and they arrive **through a package**. A helper that
|
|
35
|
+
mints a fresh id per call. A library that iterates a hash and hands back a different order
|
|
36
|
+
each time. Nothing in your Renderer looks nondeterministic — you never wrote `Math.random()`
|
|
37
|
+
— but the output moves.
|
|
38
|
+
|
|
39
|
+
It matters because the publisher and the editor **both** render the section and must agree.
|
|
40
|
+
Byte-identical output is what makes a section's HTML content-addressable: the same content
|
|
41
|
+
must hash to the same page. A section whose HTML changes on every render re-publishes
|
|
42
|
+
forever and never converges.
|
|
43
|
+
|
|
44
|
+
## Fix
|
|
45
|
+
|
|
46
|
+
If the nondeterminism is your own: compute the value at **fill time** and pass it in as a
|
|
47
|
+
slot value, or move the markup into a `"use client"` island — an island renders in the
|
|
48
|
+
browser after hydration, and its output is nobody's content hash.
|
|
49
|
+
|
|
50
|
+
If the nondeterminism came from a package: replace it, or give it a deterministic input.
|
|
51
|
+
An id generator gets a stable id derived from props. A collection gets sorted before you
|
|
52
|
+
map it.
|
|
53
|
+
|
|
54
|
+
### Before
|
|
55
|
+
|
|
56
|
+
```tsx
|
|
57
|
+
// sections/facts/Renderer.tsx
|
|
58
|
+
import { Section, Slot } from "@homepages/template-kit";
|
|
59
|
+
import { nanoid } from "nanoid";
|
|
60
|
+
|
|
61
|
+
import type { Props } from "./schema.js";
|
|
62
|
+
|
|
63
|
+
export function Renderer({ slots, nav }: Props) {
|
|
64
|
+
const descriptionId = nanoid(); // ← a new id on every render; the HTML differs each time
|
|
65
|
+
|
|
66
|
+
return (
|
|
67
|
+
<Section id={nav.selfAnchor}>
|
|
68
|
+
<Slot id="headline" as="h1" textLeaf>
|
|
69
|
+
<span aria-describedby={descriptionId}>{slots.headline}</span>
|
|
70
|
+
</Slot>
|
|
71
|
+
<p id={descriptionId}>{slots.summary}</p>
|
|
72
|
+
</Section>
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
Nothing here reads the clock or a random source, so no lint rule fires. The section still
|
|
78
|
+
renders differently every single time.
|
|
79
|
+
|
|
80
|
+
### After
|
|
81
|
+
|
|
82
|
+
The id is derived from something the section was **given**, so it is the same on every
|
|
83
|
+
render:
|
|
84
|
+
|
|
85
|
+
```tsx
|
|
86
|
+
// sections/facts/Renderer.tsx
|
|
87
|
+
import { Section, Slot } from "@homepages/template-kit";
|
|
88
|
+
|
|
89
|
+
import type { Props } from "./schema.js";
|
|
90
|
+
|
|
91
|
+
export function Renderer({ slots, nav }: Props) {
|
|
92
|
+
const descriptionId = `${nav.selfAnchor}-summary`;
|
|
93
|
+
|
|
94
|
+
return (
|
|
95
|
+
<Section id={nav.selfAnchor}>
|
|
96
|
+
<Slot id="headline" as="h1" textLeaf>
|
|
97
|
+
<span aria-describedby={descriptionId}>{slots.headline}</span>
|
|
98
|
+
</Slot>
|
|
99
|
+
<p id={descriptionId}>{slots.summary}</p>
|
|
100
|
+
</Section>
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
## See also
|
|
106
|
+
|
|
107
|
+
- [`no-nondeterminism`](no-nondeterminism.md) — the same ban, enforced in your editor, on the
|
|
108
|
+
code a linter can see.
|
|
109
|
+
- [Server vs client](server-vs-client.md) — why a server-rendered file must be a pure
|
|
110
|
+
function of its props, and why an island is not.
|
|
111
|
+
- [`render-invariant`](render-invariant.md) — the other check that runs over this same
|
|
112
|
+
fixture corpus.
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
---
|
|
2
|
+
purpose: The template-kit/image-bare-needs-reason authoring rule — what it enforces, why, and how to fix it.
|
|
3
|
+
status: living
|
|
4
|
+
related: [INDEX.md, ../primitives.md]
|
|
5
|
+
updated: 2026-07-14
|
|
6
|
+
---
|
|
7
|
+
# template-kit/image-bare-needs-reason
|
|
8
|
+
|
|
9
|
+
> `<Image bare>` needs a `// bare: <reason>` comment above it. **A reason cannot be spent
|
|
10
|
+
> twice** — one comment documents exactly one element.
|
|
11
|
+
|
|
12
|
+
## Rule
|
|
13
|
+
|
|
14
|
+
Every `<Image bare …>` must have a qualifying reason comment. A comment qualifies when
|
|
15
|
+
all three hold:
|
|
16
|
+
|
|
17
|
+
1. **It contains `bare:`** — e.g. `// bare: the logo is chrome; the parent already sizes it.`
|
|
18
|
+
2. **It sits strictly above the element, within 5 lines.** Above, not beside and not below.
|
|
19
|
+
3. **It has not already been spent** on an earlier `<Image bare>`.
|
|
20
|
+
|
|
21
|
+
Both comment forms count — a `//` line comment and a `{/* … */}` JSX comment. The rule
|
|
22
|
+
reads the raw comment stream rather than hopping up the AST, so proximity survives
|
|
23
|
+
arbitrary wrapping: a container `<div>`, a `.map()` callback, a parenthesized `return`, an
|
|
24
|
+
assignment to a `const`. If it *looks* directly above the element, it counts.
|
|
25
|
+
|
|
26
|
+
**The surprising part: a reason cannot be spent twice.** One comment above a row of
|
|
27
|
+
sibling bare logos documents only the **first** of them; the second still reports. Each
|
|
28
|
+
bare image is its own decision, so each gets its own sentence. (A comment written *between*
|
|
29
|
+
two bare images is fine for the second — it starts after the first element ends.)
|
|
30
|
+
|
|
31
|
+
## Reason
|
|
32
|
+
|
|
33
|
+
Framed is `<Image>`'s default, and the frame is load-bearing: a wrapper `div` that holds
|
|
34
|
+
the aspect-ratio box, carries the slot marker, and renders a neutral fallback rect when
|
|
35
|
+
`src` is empty.
|
|
36
|
+
|
|
37
|
+
`bare` drops all of it — one `<img>`, no wrapper. So a bare image with no source is
|
|
38
|
+
**nothing at all**: the layout collapses, and whatever sat below it jumps up the page. A
|
|
39
|
+
missing image is a supported state on this platform (a thin listing simply has no photo),
|
|
40
|
+
so "the source is always there" is an assumption, not a fact.
|
|
41
|
+
|
|
42
|
+
`bare` is still the right call sometimes — a logo whose parent already establishes the
|
|
43
|
+
box, a slide inside its own aspect-ratio parent. The rule does not ban it; it makes you
|
|
44
|
+
**say why the parent guarantees the box**, so the next author (or the next agent) can
|
|
45
|
+
tell whether the frame is safe to restore.
|
|
46
|
+
|
|
47
|
+
## Fix
|
|
48
|
+
|
|
49
|
+
Put a `// bare: <reason>` comment immediately above the element. A reason is legitimate in
|
|
50
|
+
either of two ways: it can name what **guarantees the layout box** without `<Image>`'s
|
|
51
|
+
frame (a parent that already establishes the size, a slide inside its own aspect-ratio
|
|
52
|
+
container) — or it can name something about the **image's own content** that makes the
|
|
53
|
+
frame the wrong choice, such as a transparent PNG that framing would letterbox. If
|
|
54
|
+
neither holds, drop `bare` and let `<Image>` frame it.
|
|
55
|
+
|
|
56
|
+
### Before
|
|
57
|
+
|
|
58
|
+
```tsx
|
|
59
|
+
import { Image, Section } from "@homepages/template-kit";
|
|
60
|
+
|
|
61
|
+
import type { Props } from "./schema.js";
|
|
62
|
+
|
|
63
|
+
export function Renderer({ slots }: Props) {
|
|
64
|
+
return (
|
|
65
|
+
<Section>
|
|
66
|
+
<div className="h-12 w-32">
|
|
67
|
+
<Image bare src={slots.brand_logo.url} alt={slots.brand_logo.alt} className="object-contain" />
|
|
68
|
+
</div>
|
|
69
|
+
</Section>
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
### After
|
|
75
|
+
|
|
76
|
+
```tsx
|
|
77
|
+
import { Image, Section } from "@homepages/template-kit";
|
|
78
|
+
|
|
79
|
+
import type { Props } from "./schema.js";
|
|
80
|
+
|
|
81
|
+
export function Renderer({ slots }: Props) {
|
|
82
|
+
return (
|
|
83
|
+
<Section>
|
|
84
|
+
<div className="h-12 w-32">
|
|
85
|
+
{/* bare: the logo is chrome; the parent already sizes it. */}
|
|
86
|
+
<Image bare src={slots.brand_logo.url} alt={slots.brand_logo.alt} className="object-contain" />
|
|
87
|
+
</div>
|
|
88
|
+
</Section>
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
Two bare images need two comments — the first one's reason is spent:
|
|
94
|
+
|
|
95
|
+
```tsx
|
|
96
|
+
<div className="flex gap-4">
|
|
97
|
+
{/* bare: the primary logo is chrome; the row sizes it. */}
|
|
98
|
+
<Image bare src={slots.logo_a.url} alt={slots.logo_a.alt} className="h-8 object-contain" />
|
|
99
|
+
{/* bare: the partner logo is chrome; the row sizes it. */}
|
|
100
|
+
<Image bare src={slots.logo_b.url} alt={slots.logo_b.alt} className="h-8 object-contain" />
|
|
101
|
+
</div>
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
A content-based reason is written the same way — only the sentence changes, because it's
|
|
105
|
+
justifying a different thing (the image, not the layout around it):
|
|
106
|
+
|
|
107
|
+
```tsx
|
|
108
|
+
{/* bare: a transparent PNG badge; Image's frame would letterbox its transparent
|
|
109
|
+
padding against the section background. */}
|
|
110
|
+
<Image bare src={slots.certification_badge.url} alt={slots.certification_badge.alt} className="h-10" />
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
## See also
|
|
114
|
+
|
|
115
|
+
- [Contract primitives: `Image`](../primitives.md#image--the-defensive-image-primitive) —
|
|
116
|
+
framed vs bare, and what the frame does for you.
|
|
117
|
+
- [`no-raw-element`](no-raw-element.md) — why `<img>` goes through `<Image>` at all.
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
---
|
|
2
|
+
purpose: The template-kit/license-denied authoring rule — what it enforces, why, and how to fix it.
|
|
3
|
+
status: living
|
|
4
|
+
related: [INDEX.md, audit-severity.md]
|
|
5
|
+
updated: 2026-07-14
|
|
6
|
+
---
|
|
7
|
+
# template-kit/license-denied
|
|
8
|
+
|
|
9
|
+
> Every package in your **production** tree must carry a license from a fixed allowlist.
|
|
10
|
+
> There is no per-package override.
|
|
11
|
+
|
|
12
|
+
## Rule
|
|
13
|
+
|
|
14
|
+
Each package in the production dependency tree must declare a license on this list:
|
|
15
|
+
|
|
16
|
+
| Allowed |
|
|
17
|
+
|---|
|
|
18
|
+
| `MIT` |
|
|
19
|
+
| `ISC` |
|
|
20
|
+
| `Apache-2.0` |
|
|
21
|
+
| `BSD-2-Clause` |
|
|
22
|
+
| `BSD-3-Clause` |
|
|
23
|
+
| `0BSD` |
|
|
24
|
+
| `Unlicense` |
|
|
25
|
+
| `CC0-1.0` |
|
|
26
|
+
|
|
27
|
+
Two cases people expect to slide through, and neither does:
|
|
28
|
+
|
|
29
|
+
- **`SEE LICENSE IN <file>` is a denial**, not a pass. The check cannot read the file, so
|
|
30
|
+
it cannot clear the terms.
|
|
31
|
+
- **A missing `license` field is a denial**, not a pass. An unlicensed package is not a
|
|
32
|
+
permissive one — by default, no rights are granted at all.
|
|
33
|
+
|
|
34
|
+
**Scope: the production tree only.** `devDependencies` are not evaluated — your bundler,
|
|
35
|
+
your test runner, and your formatter do not ship to anyone.
|
|
36
|
+
|
|
37
|
+
**There is no per-package override.** No allowlist entry you can add, no waiver comment. By
|
|
38
|
+
design.
|
|
39
|
+
|
|
40
|
+
## Reason
|
|
41
|
+
|
|
42
|
+
Your template's production dependencies are compiled into a page we host, and serve, on
|
|
43
|
+
behalf of a paying customer. Their license terms become terms we are shipping under. A
|
|
44
|
+
license we cannot clear in advance is a license we cannot ship — so the check is a
|
|
45
|
+
gate rather than a warning, and the list is the set we have already cleared.
|
|
46
|
+
|
|
47
|
+
## Fix
|
|
48
|
+
|
|
49
|
+
**Replace the package** with an equivalent whose license is on the list. That is the only
|
|
50
|
+
fix, and for the overwhelming majority of what a section needs there is an MIT or Apache-2.0
|
|
51
|
+
alternative.
|
|
52
|
+
|
|
53
|
+
One legitimate adjacent fix: if the package is genuinely **build-time only** — it never
|
|
54
|
+
reaches the published bundle — then it belongs in `devDependencies` anyway, and
|
|
55
|
+
`devDependencies` are not evaluated. Be honest about this. If your published page imports
|
|
56
|
+
it, it is a production dependency no matter which stanza it is listed under, and moving it
|
|
57
|
+
is a bug in your `package.json` rather than a fix.
|
|
58
|
+
|
|
59
|
+
### Before
|
|
60
|
+
|
|
61
|
+
```jsonc
|
|
62
|
+
// package.json
|
|
63
|
+
{
|
|
64
|
+
"dependencies": {
|
|
65
|
+
"react": "^19.0.0",
|
|
66
|
+
"react-dom": "^19.0.0",
|
|
67
|
+
"fancy-charts": "^3.2.0" // "license": "GPL-3.0-only" → denied
|
|
68
|
+
},
|
|
69
|
+
"devDependencies": {
|
|
70
|
+
"typescript": "^5.7.2"
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
### After
|
|
76
|
+
|
|
77
|
+
```jsonc
|
|
78
|
+
// package.json
|
|
79
|
+
{
|
|
80
|
+
"dependencies": {
|
|
81
|
+
"react": "^19.0.0",
|
|
82
|
+
"react-dom": "^19.0.0",
|
|
83
|
+
"lightweight-charts": "^4.2.0" // "license": "Apache-2.0" → allowed
|
|
84
|
+
},
|
|
85
|
+
"devDependencies": {
|
|
86
|
+
"typescript": "^5.7.2"
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
```bash
|
|
92
|
+
npm uninstall fancy-charts
|
|
93
|
+
npm install lightweight-charts
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
Confirm what a package declares before you commit to it:
|
|
97
|
+
|
|
98
|
+
```bash
|
|
99
|
+
npm view <package> license
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
## See also
|
|
103
|
+
|
|
104
|
+
- [`audit-severity`](audit-severity.md) — the other gate on the packages you ship.
|
|
105
|
+
- [`size-renderer-bundle`](size-renderer-bundle.md) — the third: what those packages cost the
|
|
106
|
+
page in bytes.
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
---
|
|
2
|
+
purpose: The template-kit/lockfile-missing authoring rule — what it enforces, why, and how to fix it.
|
|
3
|
+
status: living
|
|
4
|
+
related: [INDEX.md, lockfile-stale.md]
|
|
5
|
+
updated: 2026-07-14
|
|
6
|
+
---
|
|
7
|
+
# template-kit/lockfile-missing
|
|
8
|
+
|
|
9
|
+
> Your workspace must have a committed `package-lock.json`. Without one, your submission
|
|
10
|
+
> cannot be installed at all.
|
|
11
|
+
|
|
12
|
+
## Rule
|
|
13
|
+
|
|
14
|
+
There must be a `package-lock.json` at the **workspace root**.
|
|
15
|
+
|
|
16
|
+
This is checked once per run, not once per template. A workspace has one `package.json` and
|
|
17
|
+
one lockfile, however many templates and sections live inside it — so this violation is
|
|
18
|
+
reported once and fixed once.
|
|
19
|
+
|
|
20
|
+
A `yarn.lock` or a `pnpm-lock.yaml` does not satisfy it. The install is `npm ci`, and
|
|
21
|
+
`npm ci` reads `package-lock.json`.
|
|
22
|
+
|
|
23
|
+
## Reason
|
|
24
|
+
|
|
25
|
+
The platform installs your submission with **`npm ci`** — the reproducible install, which
|
|
26
|
+
takes the tree the lockfile pins and does not resolve anything itself. It requires a
|
|
27
|
+
lockfile, and refuses to run without one.
|
|
28
|
+
|
|
29
|
+
That is the whole point of the requirement: the tree that is built, bundled, audited, and
|
|
30
|
+
published is byte-for-byte the tree you tested against. No lockfile, no reproducible tree,
|
|
31
|
+
no build — and "works on my machine" would otherwise be the only thing standing behind a
|
|
32
|
+
page a customer is paying for.
|
|
33
|
+
|
|
34
|
+
## Fix
|
|
35
|
+
|
|
36
|
+
Run `npm install` and **commit the lockfile**. If it is being ignored, stop ignoring it —
|
|
37
|
+
that is the single most common cause of this violation, and the ignore rule is almost always
|
|
38
|
+
inherited from a template `.gitignore` that was written for an application, not a package.
|
|
39
|
+
|
|
40
|
+
### Before
|
|
41
|
+
|
|
42
|
+
```gitignore
|
|
43
|
+
# .gitignore
|
|
44
|
+
node_modules/
|
|
45
|
+
dist/
|
|
46
|
+
package-lock.json ← the lockfile never gets committed
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
`npm ci` on the checked-out submission: `The 'npm ci' command can only install with an
|
|
50
|
+
existing package-lock.json`.
|
|
51
|
+
|
|
52
|
+
### After
|
|
53
|
+
|
|
54
|
+
```gitignore
|
|
55
|
+
# .gitignore
|
|
56
|
+
node_modules/
|
|
57
|
+
dist/
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
npm install # regenerates package-lock.json from package.json
|
|
62
|
+
git add package-lock.json
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## See also
|
|
66
|
+
|
|
67
|
+
- [`lockfile-stale`](lockfile-stale.md) — the lockfile exists but no longer reproduces the
|
|
68
|
+
tree `package.json` asks for.
|