@homepages/template-kit 0.3.0 → 0.4.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 +71 -0
- package/README.md +39 -4
- package/dist/asset-modules.d.ts +81 -0
- package/dist/cli/check/config.js +5 -1
- package/dist/cli/check/css.js +27 -13
- package/dist/cli/check/loader.js +4 -0
- package/dist/cli/check/section-assets.js +58 -0
- package/dist/cli/check/stages/size.js +124 -8
- package/dist/cli/check/stages/tree.js +26 -27
- package/dist/cli/dev/build-css.js +91 -0
- package/dist/cli/dev/catalog.js +35 -0
- package/dist/cli/dev/compose-template.js +38 -0
- package/dist/cli/dev/content-override.js +58 -0
- package/dist/cli/dev/discover.js +35 -0
- package/dist/cli/dev/fill-state.js +75 -0
- package/dist/cli/dev/index.js +32 -0
- package/dist/cli/dev/inspect.js +38 -0
- package/dist/cli/dev/island-bootstrap.js +38 -0
- package/dist/cli/dev/island-map.js +35 -0
- package/dist/cli/dev/island-transform.js +31 -0
- package/dist/cli/dev/manifest-instances.js +31 -0
- package/dist/cli/dev/render-section.js +24 -0
- package/dist/cli/dev/screenshot-target.js +24 -0
- package/dist/cli/dev/section-page.js +54 -0
- package/dist/cli/dev/server.js +480 -0
- package/dist/cli/dev/slot-schema.js +12 -0
- package/dist/cli/dev/structure-summary.js +118 -0
- package/dist/cli/dev/tailwind.js +32 -0
- package/dist/cli/dev/vite-server.js +33 -0
- package/dist/cli/dev/workspace.js +63 -0
- package/dist/cli/link/index.js +74 -0
- package/dist/cli/link/overlay.js +59 -0
- package/dist/cli/link/watch.js +25 -0
- package/dist/cli/new/emit.js +48 -0
- package/dist/cli/new/index.js +69 -0
- package/dist/cli/new/scaffold/section/_Renderer.tsx +20 -0
- package/dist/cli/new/scaffold/section/_fill-spec.ts +18 -0
- package/dist/cli/new/scaffold/section/_fixtures.ts +13 -0
- package/dist/cli/new/scaffold/section/_schema.ts +24 -0
- package/dist/cli/new/scaffold/template/_manifest.json +9 -0
- package/dist/cli/new/scaffold/template/sections/hero/ExpandableText.tsx +45 -0
- package/dist/cli/new/scaffold/template/sections/hero/Renderer.tsx +40 -0
- package/dist/cli/new/scaffold/template/sections/hero/components/Feature.tsx +14 -0
- package/dist/cli/new/scaffold/template/sections/hero/fill-spec.ts +29 -0
- package/dist/cli/new/scaffold/template/sections/hero/fixtures.ts +41 -0
- package/dist/cli/new/scaffold/template/sections/hero/schema.ts +60 -0
- package/dist/cli/new/scaffold/template/theme.css +24 -0
- package/dist/cli/new/scaffold/template/theme.ts +27 -0
- package/dist/cli/new/scaffold-assets.js +20 -0
- package/dist/cli/pack/collect.js +44 -0
- package/dist/cli/pack/guards.js +57 -0
- package/dist/cli/pack/index.js +66 -0
- package/dist/cli/pack/manifest.js +15 -0
- package/dist/cli/pack/zip.js +86 -0
- package/dist/cli.js +80 -14
- package/dist/dev-client/assets/index-DfEfrp6P.css +1 -0
- package/dist/dev-client/assets/index-bpMP0b0W.js +50 -0
- package/dist/dev-client/index.html +13 -0
- package/dist/eslint/is-client-file.d.ts +4 -0
- package/dist/index.d.ts +1 -0
- package/dist/islands/detect.d.ts +16 -0
- package/dist/islands/detect.js +22 -0
- package/dist/islands/discover.d.ts +5 -0
- package/dist/islands/discover.js +19 -0
- package/dist/islands/esbuild-plugin.d.ts +13 -0
- package/dist/islands/esbuild-plugin.js +39 -0
- package/dist/islands/wrap.d.ts +11 -0
- package/dist/islands/wrap.js +48 -0
- package/dist/node_modules/@jridgewell/sourcemap-codec/dist/sourcemap-codec.js +77 -0
- package/dist/node_modules/magic-string/dist/magic-string.es.js +939 -0
- package/dist/package.js +1 -1
- package/dist/ssr.d.ts +31 -0
- package/dist/ssr.js +46 -0
- package/docs/INDEX.md +9 -1
- package/docs/assets.md +128 -0
- package/docs/check.md +6 -3
- package/docs/dev.md +187 -0
- package/docs/islands.md +5 -2
- package/docs/llms.txt +41 -3
- package/docs/new.md +27 -0
- package/docs/overview.md +57 -0
- package/docs/pack.md +46 -0
- package/docs/primitives.md +5 -6
- package/docs/quickstart.md +102 -0
- package/docs/recipes/INDEX.md +27 -0
- package/docs/recipes/bind-property-fact.md +58 -0
- package/docs/recipes/collection-slot.md +100 -0
- package/docs/recipes/fill-spec-decision.md +66 -0
- package/docs/recipes/fixture-states.md +68 -0
- package/docs/recipes/image-slot-crop.md +98 -0
- package/docs/recipes/interactive-island.md +100 -0
- package/docs/recipes/organize-section-folder.md +72 -0
- package/docs/recipes/prepare-submission.md +55 -0
- package/docs/recipes/second-template.md +49 -0
- package/docs/recipes/select-slot.md +59 -0
- package/docs/recipes/static-asset.md +103 -0
- package/docs/recipes/third-party-package.md +90 -0
- package/docs/rules/INDEX.md +4 -2
- package/docs/rules/bundle-binary-asset.md +33 -52
- package/docs/rules/bundle-incomplete.md +2 -2
- package/docs/rules/css-reason.md +3 -3
- package/docs/rules/no-bare-css-import.md +8 -8
- package/docs/rules/no-css-import-from-render-path.md +7 -8
- package/docs/rules/size-assets.md +88 -0
- package/docs/rules/size-island-bundle.md +123 -0
- package/docs/rules/size-section-css.md +19 -14
- package/docs/schema-system.md +28 -28
- package/docs/theme-and-css.md +2 -2
- package/docs/vocabulary.md +98 -0
- package/package.json +11 -3
- package/tsconfig.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,76 @@
|
|
|
1
1
|
# @homepages/template-kit
|
|
2
2
|
|
|
3
|
+
## 0.4.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 94d6458: `template-kit dev`: a full-template published page at `/template/<t>`, scope-aware
|
|
8
|
+
`/api/screenshot` (section or whole template, with `width`), and a new `/api/inspect`
|
|
9
|
+
endpoint returning per-slot fill-state (filled / empty / missing + a render
|
|
10
|
+
cross-check), resolved props, and the null-corpus. Retires `/api/structure-summary`
|
|
11
|
+
(its outline is now `/api/inspect?format=text`).
|
|
12
|
+
- 47dbbc6: Section-folder static assets are now first-class. `check`'s `bundle-binary-asset` rule
|
|
13
|
+
flags only an _unreferenced_ binary in a section folder; an asset imported in a component
|
|
14
|
+
(`import crest from "./assets/crest.svg"` → `<Image src={crest} …>`) or `url()`-referenced
|
|
15
|
+
from a section stylesheet is allowed. A `declare module "*.svg"` (and the other supported
|
|
16
|
+
asset extensions) type shim ships so component imports of assets typecheck.
|
|
17
|
+
- 9a3e0e1: Adds a read-only canvas mirror to `template-kit dev`, at `/canvas/<template>`. It frames
|
|
18
|
+
the **whole template** — every section instance, stacked in one scroll container, exactly
|
|
19
|
+
as the deliverable renders — in an iframe and paints a hover/select overlay on top: click
|
|
20
|
+
an editable slot to select it (a gold ring), hover to preview it (a blue ring);
|
|
21
|
+
non-editable regions select their enclosing section instead. Selection is scoped per
|
|
22
|
+
section instance, so the same slot name in two sections resolves independently. The
|
|
23
|
+
overlay is purely visual — it never alters the rendered markup. A mode toggle switches to
|
|
24
|
+
a clean published render of the full template with no overlay, and the current view —
|
|
25
|
+
template, mode, and the selected instance/slot — is captured in the URL, so a canvas link
|
|
26
|
+
reloads and shares exactly what you see.
|
|
27
|
+
|
|
28
|
+
The `dev` server gains `GET /api/template-schema?template=<t>`, returning each section
|
|
29
|
+
instance's editability map (per slot: its type and whether it is user-editable) for the
|
|
30
|
+
whole template, so a tool can tell which slots are selectable without re-parsing the
|
|
31
|
+
section schemas. (The per-section `/api/render` response also still carries its own
|
|
32
|
+
`slotSchema` map.)
|
|
33
|
+
|
|
34
|
+
- 7f68570: Canvas mirror: exercise slots interactively. Selecting an editable slot opens a
|
|
35
|
+
side panel with a JSON editor for its value; applying a change re-renders that
|
|
36
|
+
section instance live. A per-instance fixture picker drives different content —
|
|
37
|
+
including the empty/missing states from the section's fixture corpus — without
|
|
38
|
+
editing fixture files.
|
|
39
|
+
- 5a8dcd4: Adds the `dev` command: `template-kit dev [template] [--port <n>]`. A local preview
|
|
40
|
+
server for a workspace's templates — it renders every section against its fixtures
|
|
41
|
+
server-side, with the template's theme and compiled Tailwind utilities, exactly the way
|
|
42
|
+
the platform does. A browser UI gives you a picker (template → section → fixture) and a
|
|
43
|
+
grid contact sheet of every fixture at each breakpoint, including empty and null states;
|
|
44
|
+
each section also has a standalone preview at `/sections/<template>/<section>/<fixture>`.
|
|
45
|
+
Editing a section reloads open previews; editing a theme hot-swaps its stylesheet.
|
|
46
|
+
|
|
47
|
+
Endpoints render a section without a browser, for an agent verifying its work from a
|
|
48
|
+
terminal: `/api/inspect` (a low-context per-slot fill-state view as JSON, with
|
|
49
|
+
`?format=text` for the rendered outline) and `/api/screenshot` (a full-page PNG). Like
|
|
50
|
+
`check`, `dev` runs your
|
|
51
|
+
toolchain — Vite and Tailwind resolved from your workspace, not shipped by the kit:
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
npm i -D vite @vitejs/plugin-react tailwindcss @tailwindcss/cli
|
|
55
|
+
npm i -D @playwright/test # optional, for /api/screenshot
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
- 13c3132: Add `template-kit pack <template>` — the per-template submission zip (template
|
|
59
|
+
source + workspace lockfile + a stamped `pack-manifest.json`), which refuses on a
|
|
60
|
+
failing `check`, an active link overlay, a missing lockfile, or local-protocol
|
|
61
|
+
deps. Add the internal `link`/`unlink`/`status` overlay for developing the kit
|
|
62
|
+
against a real consumer without publishing.
|
|
63
|
+
- bcf6fbb: `@homepages/template-kit/ssr` now also exports `wrapClientModuleSource` (the `"use
|
|
64
|
+
client"` → `IslandMarker` codemod), `findSectionIslands` (recursive `"use client"`
|
|
65
|
+
component discovery under a section directory), and `islandTransformEsbuildPlugin`
|
|
66
|
+
(an esbuild `onLoad` plugin that applies the same codemod across a build graph, keyed
|
|
67
|
+
`islandKey(sectionKey, <file-basename>)`). All three are the same primitives
|
|
68
|
+
`template-kit dev`'s Vite plugin already used internally, now exposed so a build-time
|
|
69
|
+
consumer — the publish renderer bundle's esbuild pass, or a native-import loader hook —
|
|
70
|
+
can wrap island modules identically without duplicating the codemod. `esbuild` stays a
|
|
71
|
+
devDependency of the kit; `islandTransformEsbuildPlugin` imports its `Plugin` type only.
|
|
72
|
+
- b76f822: Add the node-only `@homepages/template-kit/ssr` subpath: the shared section SSR helper (`loadSectionBundle`, `renderSectionHtml`) that renders a section + fixture to the exact HTML the publisher emits, plus island detection (`isClientComponentFile`, `hasClientDirective`) and key derivation (`islandKey`) for a section's `"use client"` components.
|
|
73
|
+
|
|
3
74
|
## 0.3.0
|
|
4
75
|
|
|
5
76
|
### Minor Changes
|
package/README.md
CHANGED
|
@@ -6,10 +6,26 @@ contract primitives, the theme system, and the `template-kit` CLI.
|
|
|
6
6
|
**Status: 0.0.x.** The **schema system** (the section authoring contract and the closed
|
|
7
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 `check` and `
|
|
10
|
-
[Checking a workspace](#checking-a-workspace)
|
|
11
|
-
[
|
|
12
|
-
|
|
9
|
+
CLI's `check`, `theme`, `dev`, and `pack` commands are implemented — see
|
|
10
|
+
[Checking a workspace](#checking-a-workspace), [Generating a theme](#generating-a-theme),
|
|
11
|
+
and [Previewing a workspace](#previewing-a-workspace) below — and `new` / `new-section`
|
|
12
|
+
scaffold a template or a section into a workspace (see [docs/new.md](docs/new.md)).
|
|
13
|
+
|
|
14
|
+
## Quick start
|
|
15
|
+
|
|
16
|
+
The fastest way to start is the workspace scaffolder — it creates a workspace with a
|
|
17
|
+
working starter template and the full toolchain already wired:
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
npm create homepages-workspace <name>
|
|
21
|
+
cd <name>
|
|
22
|
+
npm install
|
|
23
|
+
npm run dev # preview the starter; the island hydrates
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Add more templates with `npx template-kit new <template>` and sections with
|
|
27
|
+
`npx template-kit new-section <template>/<section>` — see [docs/new.md](docs/new.md).
|
|
28
|
+
Prefer to wire a workspace by hand? See **Install** below.
|
|
13
29
|
|
|
14
30
|
## Install
|
|
15
31
|
|
|
@@ -119,6 +135,25 @@ Tailwind entry imports. The output is generated: don't hand-edit it, and re-run
|
|
|
119
135
|
command whenever `theme.ts` changes. Full contract:
|
|
120
136
|
**[docs/theme-and-css.md](docs/theme-and-css.md#generating-themecss)**.
|
|
121
137
|
|
|
138
|
+
## Previewing a workspace
|
|
139
|
+
|
|
140
|
+
```bash
|
|
141
|
+
template-kit dev # serve every template in the workspace
|
|
142
|
+
template-kit dev <template> # preselect one template in the picker
|
|
143
|
+
template-kit dev --port 4000 # choose the port (default 5180)
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
`dev` starts a local server that renders every section against its fixtures the same way
|
|
147
|
+
the platform does — with your template's theme and compiled Tailwind — and reloads as you
|
|
148
|
+
edit. A browser UI gives you a picker and a grid contact sheet across breakpoints; each
|
|
149
|
+
section also has a standalone page, and so does the whole template at
|
|
150
|
+
`/template/<template>`. Three endpoints let an agent verify a render from a terminal:
|
|
151
|
+
`/api/inspect` (per-slot fill-state as JSON), `/api/render` (the full structured render
|
|
152
|
+
payload), and `/api/screenshot` (a full-page PNG, section or whole template). Like
|
|
153
|
+
`check`, it runs **your** toolchain — Vite and Tailwind resolved from your workspace,
|
|
154
|
+
installed with `npm i -D vite @vitejs/plugin-react tailwindcss @tailwindcss/cli` — not
|
|
155
|
+
shipped by the kit. Full contract: **[docs/dev.md](docs/dev.md)**.
|
|
156
|
+
|
|
122
157
|
## Checking a workspace
|
|
123
158
|
|
|
124
159
|
```bash
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
//#region src/asset-modules.d.ts
|
|
2
|
+
declare module "*.svg" {
|
|
3
|
+
const src: string;
|
|
4
|
+
export default src;
|
|
5
|
+
}
|
|
6
|
+
declare module "*.png" {
|
|
7
|
+
const src: string;
|
|
8
|
+
export default src;
|
|
9
|
+
}
|
|
10
|
+
declare module "*.jpg" {
|
|
11
|
+
const src: string;
|
|
12
|
+
export default src;
|
|
13
|
+
}
|
|
14
|
+
declare module "*.jpeg" {
|
|
15
|
+
const src: string;
|
|
16
|
+
export default src;
|
|
17
|
+
}
|
|
18
|
+
declare module "*.gif" {
|
|
19
|
+
const src: string;
|
|
20
|
+
export default src;
|
|
21
|
+
}
|
|
22
|
+
declare module "*.webp" {
|
|
23
|
+
const src: string;
|
|
24
|
+
export default src;
|
|
25
|
+
}
|
|
26
|
+
declare module "*.avif" {
|
|
27
|
+
const src: string;
|
|
28
|
+
export default src;
|
|
29
|
+
}
|
|
30
|
+
declare module "*.ico" {
|
|
31
|
+
const src: string;
|
|
32
|
+
export default src;
|
|
33
|
+
}
|
|
34
|
+
declare module "*.woff" {
|
|
35
|
+
const src: string;
|
|
36
|
+
export default src;
|
|
37
|
+
}
|
|
38
|
+
declare module "*.woff2" {
|
|
39
|
+
const src: string;
|
|
40
|
+
export default src;
|
|
41
|
+
}
|
|
42
|
+
declare module "*.ttf" {
|
|
43
|
+
const src: string;
|
|
44
|
+
export default src;
|
|
45
|
+
}
|
|
46
|
+
declare module "*.otf" {
|
|
47
|
+
const src: string;
|
|
48
|
+
export default src;
|
|
49
|
+
}
|
|
50
|
+
declare module "*.eot" {
|
|
51
|
+
const src: string;
|
|
52
|
+
export default src;
|
|
53
|
+
}
|
|
54
|
+
declare module "*.mp4" {
|
|
55
|
+
const src: string;
|
|
56
|
+
export default src;
|
|
57
|
+
}
|
|
58
|
+
declare module "*.webm" {
|
|
59
|
+
const src: string;
|
|
60
|
+
export default src;
|
|
61
|
+
}
|
|
62
|
+
declare module "*.mov" {
|
|
63
|
+
const src: string;
|
|
64
|
+
export default src;
|
|
65
|
+
}
|
|
66
|
+
declare module "*.mp3" {
|
|
67
|
+
const src: string;
|
|
68
|
+
export default src;
|
|
69
|
+
}
|
|
70
|
+
declare module "*.wav" {
|
|
71
|
+
const src: string;
|
|
72
|
+
export default src;
|
|
73
|
+
}
|
|
74
|
+
declare module "*.pdf" {
|
|
75
|
+
const src: string;
|
|
76
|
+
export default src;
|
|
77
|
+
}
|
|
78
|
+
declare module "*.zip" {
|
|
79
|
+
const src: string;
|
|
80
|
+
export default src;
|
|
81
|
+
}
|
package/dist/cli/check/config.js
CHANGED
|
@@ -4,7 +4,11 @@ const CHECK_CONFIG = {
|
|
|
4
4
|
/** Gzipped bytes of a section's renderer bundle, React excluded. */
|
|
5
5
|
rendererBundleGzip: 50 * 1024,
|
|
6
6
|
/** Gzipped bytes of a section's stylesheet, including CSS pulled in from packages. */
|
|
7
|
-
sectionCssGzip: 50 * 1024
|
|
7
|
+
sectionCssGzip: 50 * 1024,
|
|
8
|
+
/** Gzipped bytes of a section's largest island (browser) bundle, React excluded. */
|
|
9
|
+
islandBundleGzip: 50 * 1024,
|
|
10
|
+
/** Raw bytes of a section's static assets (svg/images/fonts). RAW, not gzip — images/fonts are already compressed. */
|
|
11
|
+
assets: 250 * 1024
|
|
8
12
|
},
|
|
9
13
|
audit: {
|
|
10
14
|
/** A vulnerability at or above this severity fails the check. */
|
package/dist/cli/check/css.js
CHANGED
|
@@ -28,18 +28,30 @@ async function sectionCssFiles(dir) {
|
|
|
28
28
|
const rest = found.filter((f) => f !== own).sort();
|
|
29
29
|
return (existsSync(own) ? [own] : []).concat(rest);
|
|
30
30
|
}
|
|
31
|
-
const
|
|
32
|
-
".svg": "
|
|
33
|
-
".png": "
|
|
34
|
-
".jpg": "
|
|
35
|
-
".jpeg": "
|
|
36
|
-
".gif": "
|
|
37
|
-
".webp": "
|
|
38
|
-
".
|
|
39
|
-
".
|
|
40
|
-
".
|
|
41
|
-
".
|
|
31
|
+
const ASSET_LOADERS = {
|
|
32
|
+
".svg": "file",
|
|
33
|
+
".png": "file",
|
|
34
|
+
".jpg": "file",
|
|
35
|
+
".jpeg": "file",
|
|
36
|
+
".gif": "file",
|
|
37
|
+
".webp": "file",
|
|
38
|
+
".avif": "file",
|
|
39
|
+
".ico": "file",
|
|
40
|
+
".woff": "file",
|
|
41
|
+
".woff2": "file",
|
|
42
|
+
".ttf": "file",
|
|
43
|
+
".otf": "file",
|
|
44
|
+
".eot": "file",
|
|
45
|
+
".mp4": "file",
|
|
46
|
+
".webm": "file",
|
|
47
|
+
".mov": "file",
|
|
48
|
+
".mp3": "file",
|
|
49
|
+
".wav": "file",
|
|
50
|
+
".pdf": "file",
|
|
51
|
+
".zip": "file"
|
|
42
52
|
};
|
|
53
|
+
const ASSET_PUBLIC_PATH = "/section-assets";
|
|
54
|
+
const ASSET_NAMES = "[name]-[hash]";
|
|
43
55
|
/**
|
|
44
56
|
* A section's third-party package CSS — discovered by the bundler, not
|
|
45
57
|
* declared. An author writes `import "swiper/css"` in Renderer.tsx exactly
|
|
@@ -75,7 +87,9 @@ async function compileSectionPackageCss(workspaceRoot, sectionDir) {
|
|
|
75
87
|
"react-dom",
|
|
76
88
|
"react/jsx-runtime"
|
|
77
89
|
],
|
|
78
|
-
loader: { ...
|
|
90
|
+
loader: { ...ASSET_LOADERS },
|
|
91
|
+
publicPath: ASSET_PUBLIC_PATH,
|
|
92
|
+
assetNames: ASSET_NAMES,
|
|
79
93
|
absWorkingDir: workspaceRoot,
|
|
80
94
|
logLevel: "silent"
|
|
81
95
|
})).outputFiles?.find((f) => f.path.endsWith(".css"));
|
|
@@ -114,4 +128,4 @@ function findImportLine(css) {
|
|
|
114
128
|
}
|
|
115
129
|
|
|
116
130
|
//#endregion
|
|
117
|
-
export {
|
|
131
|
+
export { ASSET_LOADERS, ASSET_NAMES, ASSET_PUBLIC_PATH, compileSectionPackageCss, findImportLine, sectionCssFiles };
|
package/dist/cli/check/loader.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { loadEsbuild } from "./resolve-tool.js";
|
|
2
|
+
import { ASSET_LOADERS, ASSET_NAMES, ASSET_PUBLIC_PATH } from "./css.js";
|
|
2
3
|
import { join, relative } from "node:path";
|
|
3
4
|
import { createHash } from "node:crypto";
|
|
4
5
|
import { mkdir, rm, stat } from "node:fs/promises";
|
|
@@ -121,6 +122,9 @@ async function buildAndLoad(section, outdir, workspaceRoot) {
|
|
|
121
122
|
jsx: "automatic",
|
|
122
123
|
logLevel: "silent",
|
|
123
124
|
plugins: [cssStub],
|
|
125
|
+
loader: { ...ASSET_LOADERS },
|
|
126
|
+
publicPath: ASSET_PUBLIC_PATH,
|
|
127
|
+
assetNames: ASSET_NAMES,
|
|
124
128
|
absWorkingDir: workspaceRoot
|
|
125
129
|
});
|
|
126
130
|
const load = async (source, emitted) => {
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { loadEsbuild } from "./resolve-tool.js";
|
|
2
|
+
import { ASSET_LOADERS, ASSET_NAMES, ASSET_PUBLIC_PATH, sectionCssFiles } from "./css.js";
|
|
3
|
+
import { isUnder } from "./paths.js";
|
|
4
|
+
import { extname, join, resolve } from "node:path";
|
|
5
|
+
|
|
6
|
+
//#region src/cli/check/section-assets.ts
|
|
7
|
+
const ASSET_EXTS = new Set(Object.keys(ASSET_LOADERS));
|
|
8
|
+
async function referencedSectionAssets(workspaceRoot, section) {
|
|
9
|
+
const { build } = await loadEsbuild(workspaceRoot);
|
|
10
|
+
const referenced = /* @__PURE__ */ new Set();
|
|
11
|
+
const collect = (inputs) => {
|
|
12
|
+
for (const key of Object.keys(inputs ?? {})) {
|
|
13
|
+
const abs = resolve(workspaceRoot, key);
|
|
14
|
+
if (ASSET_EXTS.has(extname(abs).toLowerCase()) && isUnder(section.dir, abs)) referenced.add(abs);
|
|
15
|
+
}
|
|
16
|
+
};
|
|
17
|
+
collect((await build({
|
|
18
|
+
entryPoints: [join(section.dir, "Renderer.tsx")],
|
|
19
|
+
bundle: true,
|
|
20
|
+
write: false,
|
|
21
|
+
metafile: true,
|
|
22
|
+
format: "esm",
|
|
23
|
+
platform: "browser",
|
|
24
|
+
jsx: "automatic",
|
|
25
|
+
outdir: "dist",
|
|
26
|
+
external: [
|
|
27
|
+
"react",
|
|
28
|
+
"react-dom",
|
|
29
|
+
"react-dom/server",
|
|
30
|
+
"react/jsx-runtime",
|
|
31
|
+
"react/jsx-dev-runtime"
|
|
32
|
+
],
|
|
33
|
+
loader: { ...ASSET_LOADERS },
|
|
34
|
+
publicPath: ASSET_PUBLIC_PATH,
|
|
35
|
+
assetNames: ASSET_NAMES,
|
|
36
|
+
absWorkingDir: workspaceRoot,
|
|
37
|
+
logLevel: "silent"
|
|
38
|
+
})).metafile?.inputs);
|
|
39
|
+
for (const cssFile of await sectionCssFiles(section.dir)) collect((await build({
|
|
40
|
+
entryPoints: [cssFile],
|
|
41
|
+
bundle: true,
|
|
42
|
+
write: false,
|
|
43
|
+
metafile: true,
|
|
44
|
+
outdir: "dist",
|
|
45
|
+
loader: {
|
|
46
|
+
".css": "css",
|
|
47
|
+
...ASSET_LOADERS
|
|
48
|
+
},
|
|
49
|
+
publicPath: ASSET_PUBLIC_PATH,
|
|
50
|
+
assetNames: ASSET_NAMES,
|
|
51
|
+
absWorkingDir: workspaceRoot,
|
|
52
|
+
logLevel: "silent"
|
|
53
|
+
})).metafile?.inputs);
|
|
54
|
+
return referenced;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
//#endregion
|
|
58
|
+
export { referencedSectionAssets };
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { loadEsbuild } from "../resolve-tool.js";
|
|
2
|
+
import { ASSET_LOADERS, ASSET_NAMES, ASSET_PUBLIC_PATH, compileSectionPackageCss, sectionCssFiles } from "../css.js";
|
|
2
3
|
import { CHECK_CONFIG } from "../config.js";
|
|
3
|
-
import {
|
|
4
|
+
import { findSectionIslands } from "../../../islands/discover.js";
|
|
4
5
|
import { existsSync } from "node:fs";
|
|
5
|
-
import { join, relative } from "node:path";
|
|
6
|
+
import { basename, join, relative } from "node:path";
|
|
6
7
|
import { readFile } from "node:fs/promises";
|
|
7
8
|
import { gzipSync } from "node:zlib";
|
|
8
9
|
|
|
@@ -104,10 +105,10 @@ async function measureRendererBundle(workspaceRoot, section) {
|
|
|
104
105
|
/**
|
|
105
106
|
* Gzipped bytes of a section's stylesheet: its hand-written CSS
|
|
106
107
|
* (`sectionCssFiles`, published order) concatenated with the package CSS its
|
|
107
|
-
* code pulls in (`compileSectionPackageCss`).
|
|
108
|
-
*
|
|
109
|
-
*
|
|
110
|
-
*
|
|
108
|
+
* code pulls in (`compileSectionPackageCss`). A package's `url()`-referenced
|
|
109
|
+
* fonts and icons no longer inline into this text — they resolve to
|
|
110
|
+
* `/section-assets/` references (see css.ts), and their bytes are measured
|
|
111
|
+
* separately by `measureAssetBytes`, the `size-assets` dimension below.
|
|
111
112
|
*/
|
|
112
113
|
async function measureSectionCss(workspaceRoot, section) {
|
|
113
114
|
const ownFiles = await sectionCssFiles(section.dir);
|
|
@@ -115,6 +116,101 @@ async function measureSectionCss(workspaceRoot, section) {
|
|
|
115
116
|
const packageCss = await compileSectionPackageCss(workspaceRoot, section.dir);
|
|
116
117
|
return gzipSync(Buffer.from(`${own}\n${packageCss}`)).byteLength;
|
|
117
118
|
}
|
|
119
|
+
/**
|
|
120
|
+
* Raw bytes of a section's static assets — the fourth size dimension. Fonts and
|
|
121
|
+
* images are already compressed, so this is RAW bytes, not gzip. Two esbuild
|
|
122
|
+
* passes with the file loader capture every asset the published section emits:
|
|
123
|
+
* 1. Renderer.tsx (bundled) — assets imported in JS AND assets url()-referenced
|
|
124
|
+
* by the package CSS the renderer pulls in.
|
|
125
|
+
* 2. each hand-written stylesheet — its @font-face / url() assets (the author's
|
|
126
|
+
* own styles.css is read raw by the CSS budget, never bundled, so its assets
|
|
127
|
+
* surface only here).
|
|
128
|
+
* Deduped by emitted (content-hashed) filename, so an asset referenced twice or
|
|
129
|
+
* from both a stylesheet and JS is counted once.
|
|
130
|
+
*/
|
|
131
|
+
async function measureAssetBytes(workspaceRoot, section) {
|
|
132
|
+
const { build } = await loadEsbuild(workspaceRoot);
|
|
133
|
+
const byName = /* @__PURE__ */ new Map();
|
|
134
|
+
const collect = (outputFiles) => {
|
|
135
|
+
for (const f of outputFiles) {
|
|
136
|
+
if (/\.(?:js|css|map)$/i.test(f.path)) continue;
|
|
137
|
+
byName.set(basename(f.path), f.contents.byteLength);
|
|
138
|
+
}
|
|
139
|
+
};
|
|
140
|
+
collect((await build({
|
|
141
|
+
entryPoints: [join(section.dir, "Renderer.tsx")],
|
|
142
|
+
bundle: true,
|
|
143
|
+
write: false,
|
|
144
|
+
format: "esm",
|
|
145
|
+
platform: "browser",
|
|
146
|
+
jsx: "automatic",
|
|
147
|
+
outdir: "dist",
|
|
148
|
+
external: [
|
|
149
|
+
"react",
|
|
150
|
+
"react-dom",
|
|
151
|
+
"react-dom/server",
|
|
152
|
+
"react/jsx-runtime",
|
|
153
|
+
"react/jsx-dev-runtime"
|
|
154
|
+
],
|
|
155
|
+
loader: { ...ASSET_LOADERS },
|
|
156
|
+
publicPath: "/section-assets",
|
|
157
|
+
assetNames: "[name]-[hash]",
|
|
158
|
+
absWorkingDir: workspaceRoot,
|
|
159
|
+
logLevel: "silent"
|
|
160
|
+
})).outputFiles ?? []);
|
|
161
|
+
for (const cssFile of await sectionCssFiles(section.dir)) collect((await build({
|
|
162
|
+
entryPoints: [cssFile],
|
|
163
|
+
bundle: true,
|
|
164
|
+
write: false,
|
|
165
|
+
outdir: "dist",
|
|
166
|
+
loader: {
|
|
167
|
+
".css": "css",
|
|
168
|
+
...ASSET_LOADERS
|
|
169
|
+
},
|
|
170
|
+
publicPath: "/section-assets",
|
|
171
|
+
assetNames: "[name]-[hash]",
|
|
172
|
+
absWorkingDir: workspaceRoot,
|
|
173
|
+
logLevel: "silent"
|
|
174
|
+
})).outputFiles ?? []);
|
|
175
|
+
return [...byName.values()].reduce((sum, n) => sum + n, 0);
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* Gzipped bytes of a section's largest island (browser) bundle. Each "use client"
|
|
179
|
+
* component is bundled as its own browser entry, React external (the runtime chunk
|
|
180
|
+
* provides it), everything else bundled — the shape ci/static-assets.ts uploads.
|
|
181
|
+
* Returns 0 for a section with no islands. Uses `sizeStub` (React-only external),
|
|
182
|
+
* NOT the loader's cssStub — see the file header.
|
|
183
|
+
*/
|
|
184
|
+
async function measureIslandBundle(workspaceRoot, section) {
|
|
185
|
+
const islands = await findSectionIslands(section.dir);
|
|
186
|
+
if (islands.length === 0) return 0;
|
|
187
|
+
const { build } = await loadEsbuild(workspaceRoot);
|
|
188
|
+
let max = 0;
|
|
189
|
+
for (const entry of islands) {
|
|
190
|
+
const js = (await build({
|
|
191
|
+
entryPoints: [entry],
|
|
192
|
+
bundle: true,
|
|
193
|
+
minify: true,
|
|
194
|
+
format: "esm",
|
|
195
|
+
platform: "browser",
|
|
196
|
+
jsx: "automatic",
|
|
197
|
+
write: false,
|
|
198
|
+
outfile: "island.js",
|
|
199
|
+
external: [
|
|
200
|
+
"react",
|
|
201
|
+
"react-dom",
|
|
202
|
+
"react-dom/client",
|
|
203
|
+
"react/jsx-runtime",
|
|
204
|
+
"react/jsx-dev-runtime"
|
|
205
|
+
],
|
|
206
|
+
plugins: [sizeStub],
|
|
207
|
+
absWorkingDir: workspaceRoot,
|
|
208
|
+
logLevel: "silent"
|
|
209
|
+
})).outputFiles.find((f) => f.path.endsWith(".js"));
|
|
210
|
+
if (js) max = Math.max(max, gzipSync(js.contents).byteLength);
|
|
211
|
+
}
|
|
212
|
+
return max;
|
|
213
|
+
}
|
|
118
214
|
function kb(bytes) {
|
|
119
215
|
const value = bytes / 1024;
|
|
120
216
|
return `${Number.isInteger(value) ? value.toString() : value.toFixed(1)} KB`;
|
|
@@ -128,9 +224,13 @@ async function sizeStage(workspaceRoot, template) {
|
|
|
128
224
|
for (const section of template.sections) {
|
|
129
225
|
let rendererBytes;
|
|
130
226
|
let cssBytes;
|
|
227
|
+
let islandBytes;
|
|
228
|
+
let assetBytes;
|
|
131
229
|
try {
|
|
132
230
|
rendererBytes = await measureRendererBundle(workspaceRoot, section);
|
|
133
231
|
cssBytes = await measureSectionCss(workspaceRoot, section);
|
|
232
|
+
islandBytes = await measureIslandBundle(workspaceRoot, section);
|
|
233
|
+
assetBytes = await measureAssetBytes(workspaceRoot, section);
|
|
134
234
|
} catch {
|
|
135
235
|
continue;
|
|
136
236
|
}
|
|
@@ -148,11 +248,27 @@ async function sizeStage(workspaceRoot, template) {
|
|
|
148
248
|
section: section.name,
|
|
149
249
|
file: cssDiagnosticFile(workspaceRoot, section),
|
|
150
250
|
message: `section stylesheet is ${kb(cssBytes)} gzip — over the ${kb(CHECK_CONFIG.size.sectionCssGzip)} budget`,
|
|
151
|
-
fix: "Trim the stylesheet.
|
|
251
|
+
fix: "Trim the stylesheet. Pulling in a package's full 'everything' CSS bundle when you use a few of its modules is the usual cause — import only the modules you use. (A url()-referenced font or icon set is measured separately, under size-assets.)"
|
|
252
|
+
});
|
|
253
|
+
if (islandBytes > CHECK_CONFIG.size.islandBundleGzip) diagnostics.push({
|
|
254
|
+
ruleId: "template-kit/size-island-bundle",
|
|
255
|
+
template: template.key,
|
|
256
|
+
section: section.name,
|
|
257
|
+
file: `templates/${template.key}/sections/${section.name}`,
|
|
258
|
+
message: `an island bundle is ${kb(islandBytes)} gzip — over the ${kb(CHECK_CONFIG.size.islandBundleGzip)} budget`,
|
|
259
|
+
fix: "Trim what the client component pulls in. A whole UI/icon library imported for one widget is the usual cause — import only what you use, or split the island."
|
|
260
|
+
});
|
|
261
|
+
if (assetBytes > CHECK_CONFIG.size.assets) diagnostics.push({
|
|
262
|
+
ruleId: "template-kit/size-assets",
|
|
263
|
+
template: template.key,
|
|
264
|
+
section: section.name,
|
|
265
|
+
file: `templates/${template.key}/sections/${section.name}`,
|
|
266
|
+
message: `static assets are ${kb(assetBytes)} — over the ${kb(CHECK_CONFIG.size.assets)} budget`,
|
|
267
|
+
fix: "Shrink or subset the asset (a full font family or an unoptimized image is the usual cause), or drop assets the section does not use."
|
|
152
268
|
});
|
|
153
269
|
}
|
|
154
270
|
return diagnostics;
|
|
155
271
|
}
|
|
156
272
|
|
|
157
273
|
//#endregion
|
|
158
|
-
export { measureRendererBundle, measureSectionCss, sizeStage };
|
|
274
|
+
export { measureAssetBytes, measureIslandBundle, measureRendererBundle, measureSectionCss, sizeStage };
|
|
@@ -1,32 +1,12 @@
|
|
|
1
|
+
import { ASSET_LOADERS, findImportLine, sectionCssFiles } from "../css.js";
|
|
1
2
|
import { CONTRACT_FILES, loadSection } from "../loader.js";
|
|
2
3
|
import { flattenLayout } from "../../../schema/rows.js";
|
|
3
|
-
import {
|
|
4
|
+
import { referencedSectionAssets } from "../section-assets.js";
|
|
4
5
|
import { extname, join, relative } from "node:path";
|
|
5
6
|
import { readFile, readdir } from "node:fs/promises";
|
|
6
7
|
|
|
7
8
|
//#region src/cli/check/stages/tree.ts
|
|
8
|
-
const ASSET_EXTENSIONS =
|
|
9
|
-
".svg",
|
|
10
|
-
".png",
|
|
11
|
-
".jpg",
|
|
12
|
-
".jpeg",
|
|
13
|
-
".gif",
|
|
14
|
-
".webp",
|
|
15
|
-
".avif",
|
|
16
|
-
".ico",
|
|
17
|
-
".woff",
|
|
18
|
-
".woff2",
|
|
19
|
-
".ttf",
|
|
20
|
-
".otf",
|
|
21
|
-
".eot",
|
|
22
|
-
".mp4",
|
|
23
|
-
".webm",
|
|
24
|
-
".mov",
|
|
25
|
-
".mp3",
|
|
26
|
-
".wav",
|
|
27
|
-
".pdf",
|
|
28
|
-
".zip"
|
|
29
|
-
]);
|
|
9
|
+
const ASSET_EXTENSIONS = new Set(Object.keys(ASSET_LOADERS));
|
|
30
10
|
async function isBinaryFile(file) {
|
|
31
11
|
return (await readFile(file)).subarray(0, 4096).includes(0);
|
|
32
12
|
}
|
|
@@ -75,17 +55,35 @@ async function checkBundleAnatomy(workspaceRoot, template, section) {
|
|
|
75
55
|
fix: "Every section needs Renderer.tsx, schema.ts, fill-spec.ts and fixtures.ts. Add the missing file."
|
|
76
56
|
});
|
|
77
57
|
}
|
|
78
|
-
|
|
58
|
+
return out;
|
|
59
|
+
}
|
|
60
|
+
/** A section-folder file that is an asset by extension or by a NUL-byte sniff. */
|
|
61
|
+
async function sectionAssetFiles(dir) {
|
|
62
|
+
const out = [];
|
|
63
|
+
for (const abs of await bundleFiles(dir)) if (ASSET_EXTENSIONS.has(extname(abs).toLowerCase()) || await isBinaryFile(abs)) out.push(abs);
|
|
64
|
+
return out;
|
|
65
|
+
}
|
|
66
|
+
async function checkOrphanAssets(workspaceRoot, template, section) {
|
|
67
|
+
const candidates = await sectionAssetFiles(section.dir);
|
|
68
|
+
if (candidates.length === 0) return [];
|
|
69
|
+
let referenced;
|
|
70
|
+
try {
|
|
71
|
+
referenced = await referencedSectionAssets(workspaceRoot, section);
|
|
72
|
+
} catch {
|
|
73
|
+
return [];
|
|
74
|
+
}
|
|
75
|
+
const out = [];
|
|
76
|
+
for (const abs of candidates) {
|
|
77
|
+
if (referenced.has(abs)) continue;
|
|
79
78
|
const rel = relative(workspaceRoot, abs);
|
|
80
|
-
if (!ASSET_EXTENSIONS.has(extname(abs).toLowerCase()) && !await isBinaryFile(abs)) continue;
|
|
81
79
|
out.push({
|
|
82
80
|
ruleId: "template-kit/bundle-binary-asset",
|
|
83
81
|
template: template.key,
|
|
84
82
|
section: section.name,
|
|
85
83
|
file: rel,
|
|
86
84
|
line: 1,
|
|
87
|
-
message: `Static asset \`${rel}\`
|
|
88
|
-
fix: "
|
|
85
|
+
message: `Static asset \`${rel}\` sits in the section folder but nothing references it.`,
|
|
86
|
+
fix: "Reference it (import it in Renderer.tsx, or url() it from the section's CSS), or delete it. An unreferenced binary bloats the section's content hash and is never served."
|
|
89
87
|
});
|
|
90
88
|
}
|
|
91
89
|
return out;
|
|
@@ -186,6 +184,7 @@ async function treeStage(workspaceRoot, template) {
|
|
|
186
184
|
const diagnostics = [];
|
|
187
185
|
for (const section of template.sections) {
|
|
188
186
|
diagnostics.push(...await checkBundleAnatomy(workspaceRoot, template, section));
|
|
187
|
+
diagnostics.push(...await checkOrphanAssets(workspaceRoot, template, section));
|
|
189
188
|
let loaded;
|
|
190
189
|
try {
|
|
191
190
|
loaded = await loadSection(workspaceRoot, section);
|