@gusnips/vite 0.8.35 → 0.8.36

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. package/README.md +20 -20
  2. package/dist/catalog.d.ts +5 -6
  3. package/dist/catalog.d.ts.map +1 -1
  4. package/dist/catalog.js +5 -6
  5. package/dist/catalog.js.map +1 -1
  6. package/dist/contrast.js +8 -8
  7. package/dist/contrast.js.map +1 -1
  8. package/dist/head.d.ts +35 -36
  9. package/dist/head.d.ts.map +1 -1
  10. package/dist/head.js +20 -20
  11. package/dist/head.js.map +1 -1
  12. package/dist/i18n.d.ts +8 -7
  13. package/dist/i18n.d.ts.map +1 -1
  14. package/dist/i18n.js +5 -4
  15. package/dist/i18n.js.map +1 -1
  16. package/dist/icons.d.ts +6 -6
  17. package/dist/icons.d.ts.map +1 -1
  18. package/dist/icons.js +2 -2
  19. package/dist/icons.js.map +1 -1
  20. package/dist/index.d.ts +7 -7
  21. package/dist/index.js +7 -7
  22. package/dist/node.d.ts +24 -25
  23. package/dist/node.d.ts.map +1 -1
  24. package/dist/node.js +26 -26
  25. package/dist/node.js.map +1 -1
  26. package/dist/og.d.ts +7 -7
  27. package/dist/og.d.ts.map +1 -1
  28. package/dist/og.js +2 -2
  29. package/dist/og.js.map +1 -1
  30. package/dist/pre-paint.d.ts +3 -3
  31. package/dist/preset.d.ts +9 -9
  32. package/dist/preset.js +3 -3
  33. package/dist/render.d.ts +9 -9
  34. package/dist/render.js +4 -4
  35. package/dist/render.js.map +1 -1
  36. package/dist/sitemap.d.ts +26 -27
  37. package/dist/sitemap.d.ts.map +1 -1
  38. package/dist/sitemap.js +18 -18
  39. package/dist/sitemap.js.map +1 -1
  40. package/dist/theme.d.ts +1 -2
  41. package/dist/theme.d.ts.map +1 -1
  42. package/dist/theme.js +1 -2
  43. package/dist/theme.js.map +1 -1
  44. package/package.json +2 -2
  45. package/src/catalog.ts +5 -6
  46. package/src/contrast.ts +8 -8
  47. package/src/head.ts +36 -37
  48. package/src/i18n.ts +8 -7
  49. package/src/icons.ts +7 -7
  50. package/src/index.ts +7 -7
  51. package/src/node.ts +28 -29
  52. package/src/og.ts +8 -8
  53. package/src/pre-paint.ts +3 -3
  54. package/src/preset.ts +9 -9
  55. package/src/render.ts +10 -10
  56. package/src/sitemap.ts +27 -28
  57. package/src/theme.ts +1 -2
package/src/head.ts CHANGED
@@ -15,7 +15,7 @@
15
15
  import { PRERENDERED_ROUTE_ATTR } from "@gusnips/react/contract";
16
16
  import { escapeAttr, escapeRegex } from "./escape.ts";
17
17
 
18
- /** The empty root a template must carry — `bakeHead` fills it, and refuses a filled one. */
18
+ /** The empty root a template must carry. `bakeHead` fills it and refuses a filled one. */
19
19
  export const EMPTY_ROOT = '<div id="root"></div>';
20
20
 
21
21
  /**
@@ -106,7 +106,7 @@ function setMetaIfPresent(html: string, patterns: readonly RegExp[], value: stri
106
106
  return rewriteAttr(html, patterns, value) ?? html;
107
107
  }
108
108
 
109
- /** One `<link rel="alternate" hreflang>` — this page's address in another language. */
109
+ /** One `<link rel="alternate" hreflang>` for this page's address in another language. */
110
110
  export interface Alternate {
111
111
  /** A BCP-47 tag, or `x-default` for the address a crawler should show when it has no reason
112
112
  * to prefer one. */
@@ -122,17 +122,16 @@ export interface HeadTags {
122
122
  /**
123
123
  * The page's own URL, for `<link rel="canonical">` and the share tags.
124
124
  *
125
- * `null` means this page has no canonical URL and must not claim one — the 404 shell, which
126
- * is served for every address that does not exist and would otherwise tell a crawler that
125
+ * `null` means this page has no canonical URL and must not claim one, as with the 404 shell,
126
+ * which is served for every address that does not exist and would otherwise tell a crawler that
127
127
  * all of them are the front page.
128
128
  */
129
129
  canonical: string | null;
130
130
  ogTitle?: string;
131
131
  ogDescription?: string;
132
- /** Absolute URL of the share card. Omit where the template carries no `og:image` tag at all
133
- * — setting one there is a build error, by design. */
132
+ /** Absolute URL of the share card. Omit where the template carries no `og:image` tag. Setting one there is a build error, by design. */
134
133
  image?: string;
135
- /** Markup inserted before `</head>` — structured data, and nothing else so far. Emitted here
134
+ /** Markup inserted before `</head>` for structured data, and nothing else so far. Emitted here
136
135
  * because its only reader is a crawler. */
137
136
  headExtra?: string;
138
137
  /** Keep this page out of every index. Pairs with `canonical: null`. */
@@ -142,8 +141,8 @@ export interface HeadTags {
142
141
  * goes with it.
143
142
  *
144
143
  * A crawler and a screen reader both read `lang`, and neither runs the bundle that would
145
- * otherwise set it — so a Portuguese file whose `<html>` still says `en` is announced in the
146
- * wrong voice and indexed as the wrong language.
144
+ * otherwise set it, so a Portuguese file whose `<html>` still says `en` is announced in the wrong
145
+ * voice and indexed as the wrong language.
147
146
  */
148
147
  lang?: string;
149
148
  /**
@@ -158,14 +157,14 @@ export interface HeadTags {
158
157
  * The rendered page, injected into `<div id="root">`, and the route it is a render OF.
159
158
  *
160
159
  * The head alone was never enough. A shipped `<div id="root"></div>` is a page whose entire
161
- * content is a description tag — it can be listed, and it can never be read, quoted or
162
- * answered from.
160
+ * content is a description tag. It can be listed, and it can never be read, quoted or answered
161
+ * from.
163
162
  *
164
- * The two travel together because markup alone is not enough to hydrate against. A static
165
- * host answers every unpublished address with the nearest `404.html`, and that file has a
166
- * rendered body in it — so a route served from the shell would find a full root and hydrate
167
- * the not-found page into a page that is not it. The marker is what the browser entry
168
- * compares its own route against before deciding to hydrate or to mount fresh.
163
+ * The two travel together because markup alone is not enough to hydrate against. A static host
164
+ * answers every unpublished address with the nearest `404.html`, and that file has a rendered
165
+ * body in it, so a route served from the shell would find a full root and hydrate the not-found
166
+ * page into a page that is not it. The marker is what the browser entry compares its own route
167
+ * against before deciding to hydrate or to mount fresh.
169
168
  */
170
169
  body?: { route: string; html: string };
171
170
  }
@@ -174,18 +173,18 @@ export interface HeadTags {
174
173
  * A BCP-47 tag in Open Graph's spelling: underscore, and a TERRITORY it will not infer.
175
174
  *
176
175
  * `og:locale` wants `language_TERRITORY` and quietly ignores anything else, which is the same
177
- * outcome as omitting it — the `en_US` default. So a Portuguese page with a Portuguese
178
- * `og:title` and no `og:locale` tells every share crawler the card is English. `pt-BR` already
179
- * carries its territory; `en` and `es` do not, so one is chosen here rather than left to a
180
- * crawler. `es_ES` is not a claim that the copy is peninsular Spanish — it is the most widely
181
- * recognised Spanish value, and the tag's job is to be understood, not precise about dialect.
176
+ * outcome as omitting it: the `en_US` default. So a Portuguese page with a Portuguese `og:title`
177
+ * and no `og:locale` tells every share crawler the card is English. `pt-BR` already carries its
178
+ * territory; `en` and `es` do not, so one is chosen here rather than left to a crawler. `es_ES` is
179
+ * not a claim that the copy is peninsular Spanish. It is the most widely recognised Spanish value,
180
+ * and the tag's job is to be understood, not precise about dialect.
182
181
  */
183
182
  export function ogLocale(tag: string): string {
184
183
  const TERRITORY: Record<string, string> = { en: "en_US", es: "es_ES" };
185
184
  return TERRITORY[tag] ?? tag.replace("-", "_");
186
185
  }
187
186
 
188
- /** One question and its answer, as the page renders them — pass the same strings, so the
187
+ /** One question and its answer, as the page renders them. Pass the same strings, so the
189
188
  * structured data cannot drift from the copy. One donor's template carried four questions
190
189
  * where the page renders seven, and nothing compared them until the migration. */
191
190
  export interface FaqEntry {
@@ -196,11 +195,11 @@ export interface FaqEntry {
196
195
  /**
197
196
  * FAQ structured data for `headExtra`, with the script-closing sequence escaped.
198
197
  *
199
- * A `</script>` inside an answer ends the element early and ships the rest of the JSON as
200
- * markup. `<\/` is the same string to a JSON parser and inert to an HTML one, so every `</`
201
- * becomes `<\/` and `<!--` becomes `<\!--`. `JSON.stringify` never emits `<` outside a string
202
- * — the structural characters are all brackets, quotes and digits — so the replace only ever
203
- * touches copy.
198
+ * A `</script>` inside an answer ends the element early and ships the rest of the JSON as markup.
199
+ * `<\/` is the same string to a JSON parser and inert to an HTML one, so every `</` becomes `<\/`
200
+ * and `<!--` becomes `<\!--`. `JSON.stringify` never emits `<` outside a string, since the
201
+ * structural characters are all brackets, quotes and digits. The replacement only ever touches
202
+ * copy.
204
203
  */
205
204
  export function faqJsonLd(faqs: readonly FaqEntry[]): string {
206
205
  const data = {
@@ -219,9 +218,9 @@ export function faqJsonLd(faqs: readonly FaqEntry[]): string {
219
218
  /**
220
219
  * One page's `<head>` and body, written into the built template.
221
220
  *
222
- * Every tag is SET rather than appended, and `setMeta` throws on a tag the template does not
223
- * carry — so the failure mode of editing `index.html` is a red build, never a page quietly
224
- * shipping somebody else's description.
221
+ * Every tag is SET rather than appended, and `setMeta` throws on a tag the template does not carry,
222
+ * so the failure mode of editing `index.html` is a red build, never a page quietly shipping
223
+ * somebody else's description.
225
224
  */
226
225
  export function bakeHead(template: string, tags: HeadTags): string {
227
226
  const shareTitle = tags.ogTitle ?? tags.title;
@@ -345,7 +344,7 @@ export function bakeHead(template: string, tags: HeadTags): string {
345
344
  }
346
345
 
347
346
  export interface RenderedChecks {
348
- /** The BCP-47 tag this file must be marked with — the same `lang` passed to `bakeHead`. */
347
+ /** The BCP-47 tag this file must be marked with. Pass the same `lang` to `bakeHead`. */
349
348
  lang?: string;
350
349
  /** Bytes the page must have gained over the template before it counts as rendered.
351
350
  * 500 is the floor both donor builds ran with. */
@@ -375,11 +374,11 @@ const RENDERER_ERROR =
375
374
  /**
376
375
  * What every written file must be true of before the build is allowed to pass.
377
376
  *
378
- * The regression this exists for is a root that renders to nothing. A router whose location
379
- * does not match its routes yields empty markup with no error and no warning — it looks fine
380
- * in every browser and is invisible to everything that reads a link. Checking the bytes we
381
- * actually wrote is the only thing that catches it, and it needs no browser, so it gates the
382
- * BUILD rather than sitting in a test suite.
377
+ * The regression this exists for is a root that renders to nothing. A router whose location does
378
+ * not match its routes yields empty markup with no error and no warning. It looks fine in every
379
+ * browser and is invisible to everything that reads a link. Checking the bytes we actually wrote is
380
+ * the only thing that catches it, and it needs no browser, so it gates the BUILD rather than
381
+ * sitting in a test suite.
383
382
  *
384
383
  * The loading-screen check is the half a size floor misses. With `lazy()` routes behind one
385
384
  * `<Suspense fallback={<Spinner />}>`, a render that resolves nothing still produces a
@@ -455,5 +454,5 @@ export function assertRendered(
455
454
  // guard missed it from the other side: it failed a file containing a spinner class its splash
456
455
  // does not use. Two checks written for one regression, neither of which could ever fire.
457
456
  if (grew < SPLASH_MAX_GROWTH && head.includes('role="status"'))
458
- fail("rendered the loading screen, not the page — something suspended and never resolved");
457
+ fail("rendered the loading screen, not the page. Something suspended and never resolved");
459
458
  }
package/src/i18n.ts CHANGED
@@ -43,7 +43,7 @@ export interface I18nBundle {
43
43
  /** The app's `fallbackLng`: the language the others are compared to, and code keys must be in. */
44
44
  canonical: string;
45
45
  /**
46
- * One language's catalogs. A throw fails the check with its message — it never skips the bundle,
46
+ * One language's catalogs. A throw fails the check with its message. It never skips the bundle,
47
47
  * because every other rule compares languages and a missing one would pass them all.
48
48
  */
49
49
  load: (locale: string) => Catalogs | Promise<Catalogs>;
@@ -388,8 +388,8 @@ async function files(root: string, patterns: readonly string[]): Promise<string[
388
388
  export interface MergeOptions {
389
389
  /**
390
390
  * Refuse two fragments that share a top-level key. Set it when the app merges fragments at
391
- * runtime with a spread — `{ ...auth.en, ...nav.en }` — where the second `nav` replaces the
392
- * first instead of joining it, and the keys of one fragment vanish with no error anywhere.
391
+ * runtime with a spread, `{ ...auth.en, ...nav.en }`, where the second `nav` replaces the first
392
+ * instead of joining it, and the keys of one fragment vanish with no error anywhere.
393
393
  */
394
394
  ownTopLevel?: boolean;
395
395
  }
@@ -446,10 +446,11 @@ function sortTree(tree: Tree): Tree {
446
446
  }
447
447
 
448
448
  /**
449
- * Fold `dir/*.json` — one file per area, every language side by side, `{ "en": {…}, "pt-BR": {…} }`
450
- * — into one catalog per language. Fragments exist so that people and agents building screens in
451
- * parallel do not all edit the same three files. Deterministic: files in name order, keys sorted,
452
- * so two runs write the same bytes and a derived catalog can be compared with a fresh merge.
449
+ * Fold `dir/*.json` into one catalog per language. Each file holds one area, with languages side by
450
+ * side: `{ "en": {…}, "pt-BR": {…} }`. Fragments exist so that people and agents building screens
451
+ * in parallel do not all edit the same three files. Deterministic: files in name order, keys
452
+ * sorted, so two runs write the same bytes and a derived catalog can be compared with a fresh
453
+ * merge.
453
454
  */
454
455
  export async function mergeFragments(
455
456
  dir: string,
package/src/icons.ts CHANGED
@@ -18,7 +18,7 @@ export type RenderIcon = (svg: string, size: number) => Uint8Array | Promise<Uin
18
18
  export type PackIco = (pngs: readonly Uint8Array[]) => Uint8Array | Promise<Uint8Array>;
19
19
 
20
20
  export interface WriteIconSetOptions {
21
- /** Where the files land — `public/` in every donor, so they are served as static files. */
21
+ /** Where the files land. Every donor uses `public/`, so they are served as static files. */
22
22
  outDir: string;
23
23
  /** The one master artwork. A single path with no variants is what keeps every size in step. */
24
24
  svg: string;
@@ -27,9 +27,9 @@ export interface WriteIconSetOptions {
27
27
  /** The file name for one PNG size. Default `` `icon-${size}.png` ``. */
28
28
  file?: (size: number) => string;
29
29
  /**
30
- * The size written as `apple-touch-icon.png`. Default 180 — what the reader's home screen
31
- * asks for. Reuses the PNG render when it is already in `pngSizes`, so the common case costs
32
- * no extra raster.
30
+ * The size written as `apple-touch-icon.png`. Defaults to 180, the size the reader's home screen
31
+ * asks for. Reuses the PNG render when it is already in `pngSizes`, so the common case costs no
32
+ * extra raster.
33
33
  */
34
34
  appleTouchSize?: number;
35
35
  /** Sizes re-rendered and packed into `favicon.ico`, e.g. `[16, 32, 48]`. */
@@ -44,7 +44,7 @@ export interface WriteIconSetOptions {
44
44
  * Render one icon set and write it, plus the multi-size `favicon.ico`.
45
45
  *
46
46
  * Every size is rendered BEFORE anything is written, so a renderer that throws leaves no
47
- * half-written set behind — the same order `writeOgCards` keeps for the same reason.
47
+ * half-written set behind. This is the same order `writeOgCards` keeps for the same reason.
48
48
  */
49
49
  export async function writeIconSet({
50
50
  outDir,
@@ -100,7 +100,7 @@ export interface WebManifestOptions {
100
100
  display?: string;
101
101
  backgroundColor?: string;
102
102
  themeColor?: string;
103
- /** At least one — a manifest with no icons fails installability with no error anywhere. */
103
+ /** At least one icon is required. A manifest with no icons fails installability with no error anywhere. */
104
104
  icons: readonly ManifestIcon[];
105
105
  }
106
106
 
@@ -121,7 +121,7 @@ export function webManifest({
121
121
  }: WebManifestOptions): string {
122
122
  if (name.trim() === "") throw new Error("manifest: `name` is empty");
123
123
  if (icons.length === 0)
124
- throw new Error("manifest: `icons` is empty — name what writeIconSet wrote");
124
+ throw new Error("manifest: `icons` is empty. Name what writeIconSet wrote");
125
125
  const manifest: Record<string, unknown> = { name };
126
126
  if (shortName !== undefined) manifest["short_name"] = shortName;
127
127
  if (description !== undefined) manifest["description"] = description;
package/src/index.ts CHANGED
@@ -1,10 +1,10 @@
1
1
  /**
2
2
  * The build-time half of a prerendered Vite + React SPA.
3
3
  *
4
- * A Vite SPA ships one `index.html` and draws the rest with JavaScript. Nothing that reads a
5
- * link for a living runs that bundle — not a search crawler, not an LLM, not the thing that
6
- * draws the preview card in a chat app. So the build renders every public route to a real file
7
- * with a real `<head>` and a real body, and this is the part every app doing that shares.
4
+ * A Vite SPA ships one `index.html` and draws the rest with JavaScript. Nothing that reads a link
5
+ * for a living runs that bundle, whether it is a search crawler, an LLM or the thing that draws the
6
+ * preview card in a chat app. So the build renders every public route to a real file with a real
7
+ * `<head>` and a real body, and this is the part every app doing that shares.
8
8
  *
9
9
  * Four repos wrote it independently and each learned something the others had not. What is
10
10
  * here is the merge; every non-obvious rule carries the reason it exists.
@@ -13,9 +13,9 @@
13
13
  * otherwise force on everyone: the vite config preset at `@gusnips/vite/preset` (the React and
14
14
  * Tailwind plugins), `renderTree` at `@gusnips/vite/render`, the theme pre-paint plugin at
15
15
  * `@gusnips/vite/theme` (both React itself), the catalog check at `@gusnips/vite/i18n`, and the
16
- * contrast gate at `@gusnips/vite/contrast` (tailwindcss) — the last two run in CI and never in
17
- * a build. Nothing here imports React, vite or tailwindcss, so a prerender script, an OG
18
- * generator and a repo that only wants a sitemap all install exactly what they use.
16
+ * contrast gate at `@gusnips/vite/contrast` (tailwindcss). The last two run in CI and never in a
17
+ * build. Nothing here imports React, vite or tailwindcss, so a prerender script, an OG generator
18
+ * and a repo that only wants a sitemap all install exactly what they use.
19
19
  */
20
20
  export { resolveKey } from "./catalog.ts";
21
21
  // `headExtra` takes raw markup, so the moment an adopter builds a tag out of its own copy it
package/src/node.ts CHANGED
@@ -35,7 +35,7 @@ export async function loadTemplate(distDir: string): Promise<string> {
35
35
  if (!template.includes(EMPTY_ROOT))
36
36
  throw new Error(
37
37
  `prerender: ${file} does not carry ${EMPTY_ROOT}. Either it is already a rendered page ` +
38
- "— run `vite build` to regenerate the shell this reads — or the app's root element " +
38
+ "(run `vite build` to regenerate the shell this reads), or the app's root element " +
39
39
  "carries attributes, which the baker cannot fill.",
40
40
  );
41
41
  return template;
@@ -58,16 +58,16 @@ function exportsRenderer<Context>(mod: unknown): mod is { renderPage: PageRender
58
58
  * produced it. So the entry is a path on disk, written by `vite build --ssr src/entry-server.tsx`,
59
59
  * and nothing in the source tree references it.
60
60
  *
61
- * **The calling script has to exit itself.** This import is where the renderer enters your
62
- * process, and under bun `react-dom/static.browser` — which invariant 1 requires, so every app on
63
- * this rig loads it — leaves that process alive after its last await. Measured by elimination with
64
- * no app code at all: `react-dom/server` exits, `static.browser` does not, and
65
- * `process.getActiveResourcesInfo()` reports nothing either way, so the handle is invisible to
66
- * everything except the hang. Under node it exits. Nothing here can undo it — a library must not
67
- * call `process.exit` on its host — so a prerender script ends with `process.exit(0)` once every
68
- * file is written, and in CI the alternative is a job that does all its work and then runs to its
69
- * timeout. Five adopters had already written that line against a DIFFERENT holder (an auth client
70
- * whose token-refresh timer starts at module scope); none of them knew the renderer does it too.
61
+ * **The calling script has to exit itself.** This import is where the renderer enters your process,
62
+ * and under bun `react-dom/static.browser`, which invariant 1 requires every app on this rig to
63
+ * load, leaves that process alive after its last await. Measured by elimination with no app code at
64
+ * all: `react-dom/server` exits, `static.browser` does not, and `process.getActiveResourcesInfo()`
65
+ * reports nothing either way, so the handle is invisible to everything except the hang. Under node
66
+ * it exits. Nothing here can undo it, because a library must not call `process.exit` on its host. A
67
+ * prerender script ends with `process.exit(0)` once every file is written, and in CI the
68
+ * alternative is a job that does all its work and then runs to its timeout. Five adopters had
69
+ * already written that line against a DIFFERENT holder (an auth client whose token-refresh timer
70
+ * starts at module scope); none of them knew the renderer does it too.
71
71
  */
72
72
  export async function loadRenderer<Context = unknown>(
73
73
  entryFile: string,
@@ -75,7 +75,7 @@ export async function loadRenderer<Context = unknown>(
75
75
  const mod: unknown = await import(pathToFileURL(entryFile).href);
76
76
  if (!exportsRenderer<Context>(mod))
77
77
  throw new Error(
78
- `prerender: ${entryFile} does not export renderPage — run \`vite build --ssr\` first`,
78
+ `prerender: ${entryFile} does not export renderPage. Run \`vite build --ssr\` first`,
79
79
  );
80
80
  return mod.renderPage;
81
81
  }
@@ -90,14 +90,13 @@ export async function writeDist(distDir: string, file: string, contents: string)
90
90
  /** One laid-out share card: the bytes, and anything that did not fit. */
91
91
  export interface OgCard {
92
92
  png: Uint8Array;
93
- /** Copy the layout could not hold. A single one refuses the whole run — see
94
- * {@link writeOgCards}. */
93
+ /** Copy the layout could not hold. A single one refuses the whole run. See {@link writeOgCards}. */
95
94
  overflow?: readonly OgOverflow[];
96
95
  }
97
96
 
98
97
  export interface WriteOgCardsOptions<Page> {
99
98
  pages: readonly Page[];
100
- /** Where the cards land — `public/og` in both donors, so they are committed beside the
99
+ /** Where the cards land. Both donors use `public/og`, so they are committed beside the
101
100
  * favicons and served as static files. */
102
101
  outDir: string;
103
102
  /** The file name for one page, inside `outDir`. `(page) => \`${pageSlug(page.path)}.png\``. */
@@ -112,10 +111,10 @@ export interface WriteOgCardsOptions<Page> {
112
111
  * Walk a page registry and write one share card per page.
113
112
  *
114
113
  * Every card is laid out BEFORE any is written, and a single line that does not fit refuses the
115
- * whole run. That order is the point: a card that cannot hold its copy is a product decision,
116
- * not something to resolve with an ellipsis — an ellipsis makes every string "fit", so
117
- * overgrown copy has no failing case and ships a card missing the end of the one line the card
118
- * exists to carry. The reader who finds out is someone else's link unfurl.
114
+ * whole run. That order is the point: a card that cannot hold its copy is a product decision, not
115
+ * something to resolve with an ellipsis. An ellipsis makes every string "fit", so overgrown copy
116
+ * has no failing case and ships a card missing the end of the one line the card exists to carry.
117
+ * The reader who finds out is someone else's link unfurl.
119
118
  *
120
119
  * Overflow is collected rather than thrown on the first card, so one run names every bad one:
121
120
  * copy lands per locale in batches, and a build that dies on the first of six sends its
@@ -139,7 +138,7 @@ export async function writeOgCards<Page>({
139
138
 
140
139
  if (overflows.length > 0)
141
140
  throw new Error(
142
- `og: ${String(overflows.length)} card(s) cannot hold their copy —\n\n` +
141
+ `og: ${String(overflows.length)} card(s) cannot hold their copy:\n\n` +
143
142
  overflows.map((o) => ` · ${describeOverflow(o)}`).join("\n\n") +
144
143
  "\n\n Shorten the copy, or change the size ladder deliberately. Nothing was written.",
145
144
  );
@@ -201,11 +200,11 @@ async function isFile(path: string): Promise<boolean> {
201
200
  * Every share card a built page advertises is a file that exists.
202
201
  *
203
202
  * Three repos shipped a page whose `og:image` named a card nothing had ever rendered. All three
204
- * were `404.html`, all three for the same reason: a card generator walks the page registry, and
205
- * a not-found shell is not in the registry, so a head built from the same template inherits a
206
- * card address with no card behind it. Every share of a dead link unfurled broken, in one case
207
- * in three languages, and **nothing in a browser shows it** — the page looks perfect, and the
208
- * only reader who finds out is whoever the link was sent to.
203
+ * were `404.html`, all three for the same reason: a card generator walks the page registry, and a
204
+ * not-found shell is not in the registry, so a head built from the same template inherits a card
205
+ * address with no card behind it. Every share of a dead link unfurled broken, in one case in three
206
+ * languages, and **nothing in a browser shows it**. The page looks perfect, and the only reader who
207
+ * finds out is whoever the link was sent to.
209
208
  *
210
209
  * `bakeHead` cannot catch it: it is handed an `image` and writes it, and a shell carrying the
211
210
  * brand card is correct, so it has no way to tell a right image from a wrong one (invariant 12).
@@ -216,9 +215,9 @@ async function isFile(path: string): Promise<boolean> {
216
215
  * await assertOgImages("dist", "https://example.com");
217
216
  * ```
218
217
  *
219
- * Cards on another host are somebody else's to serve and are skipped — but a build whose pages
220
- * all advertise cards and whose origin matches NONE of them has been given the wrong origin,
221
- * and a check that silently passes is worse than no check, so that is an error too.
218
+ * Cards on another host are somebody else's to serve and are skipped. But a build whose pages all
219
+ * advertise cards and whose origin matches NONE of them has been given the wrong origin, and a
220
+ * check that silently passes is worse than no check, so that is an error too.
222
221
  */
223
222
  export async function assertOgImages(distDir: string, origin: string): Promise<number> {
224
223
  const root = siteOrigin(origin);
@@ -247,7 +246,7 @@ export async function assertOgImages(distDir: string, origin: string): Promise<n
247
246
 
248
247
  if (missing.length > 0)
249
248
  throw new Error(
250
- `og: ${String(missing.length)} page(s) advertise a share card that is not in ${distDir} —\n\n` +
249
+ `og: ${String(missing.length)} page(s) advertise a share card that is not in ${distDir}:\n\n` +
251
250
  missing.map((card) => ` · ${card.page} → ${card.url}`).join("\n") +
252
251
  "\n\n Either render the card, or leave `image` out of the head for that page.",
253
252
  );
package/src/og.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Fitting copy into a share card.
3
3
  *
4
- * The card's ART — the palette, the lockup, the size ladder — is the product, and it stays in
4
+ * The card's ART, including the palette, lockup and size ladder, is the product, and it stays in
5
5
  * the product. What ships here is the part two independent generators got wrong the same way:
6
6
  * deciding what happens when a headline does not fit.
7
7
  *
@@ -19,7 +19,7 @@ export interface OgOverflow {
19
19
  /** The string as given. */
20
20
  text: string;
21
21
  maxLines: number;
22
- /** Characters per line at this size — the budget the text blew. */
22
+ /** Characters per line at this size, the budget the text exceeded. */
23
23
  maxChars: number;
24
24
  size: number;
25
25
  }
@@ -34,11 +34,11 @@ export interface FitOptions {
34
34
  /**
35
35
  * Average glyph advance as a fraction of the font size.
36
36
  *
37
- * Character-budget estimation rather than real metrics: an SVG rasterizer gives no measuring
38
- * API, and card copy is short enough that the estimate never drifts more than a word. Measure
39
- * it off a rendered card and round UP — a budget that is too generous overflows the column,
40
- * while one that is too mean only breaks a line early. Donor values: ~0.5 for a display face
41
- * at headline sizes, ~0.46 for body copy.
37
+ * Character-budget estimation rather than real metrics: an SVG rasterizer gives no measuring API,
38
+ * and card copy is short enough that the estimate never drifts more than a word. Measure it off a
39
+ * rendered card and round UP. A budget that is too generous overflows the column, while one that
40
+ * is too mean only breaks a line early. Donor values: ~0.5 for a display face at headline sizes,
41
+ * ~0.46 for body copy.
42
42
  */
43
43
  advance: number;
44
44
  /** Names this line in an overflow report. `"pricing headline"`, not `"line 1"`. */
@@ -96,7 +96,7 @@ export function fitText(text: string, options: FitOptions): FitResult {
96
96
  /** One overflow, as the line an operator reads in a failed build. */
97
97
  export function describeOverflow(overflow: OgOverflow): string {
98
98
  return (
99
- `${overflow.label} — ${String(overflow.maxLines)} lines of ~${String(overflow.maxChars)} ` +
99
+ `${overflow.label}: ${String(overflow.maxLines)} lines of ~${String(overflow.maxChars)} ` +
100
100
  `chars at ${String(overflow.size)}px, ${String(overflow.text.length)} chars given\n` +
101
101
  ` ${JSON.stringify(overflow.text)}`
102
102
  );
package/src/pre-paint.ts CHANGED
@@ -26,10 +26,10 @@ export interface PrePaintScriptOptions {
26
26
  /** The script's full text. It runs alone, so it must not reference anything outside itself. */
27
27
  source: string;
28
28
  /**
29
- * Where the tag goes. `"head"` (the end of `<head>`) when the script reads elements above it —
29
+ * Where the tag goes. `"head"` (the end of `<head>`) when the script reads elements above it, as
30
30
  * the theme reads `<meta name="theme-color">`. `"head-prepend"` when it only needs to run as
31
- * early as possible: a classic script after a stylesheet waits for that stylesheet, so the
32
- * locale gate goes first and redirects without downloading the page's CSS.
31
+ * early as possible: a classic script after a stylesheet waits for that stylesheet, so the locale
32
+ * gate goes first and redirects without downloading the page's CSS.
33
33
  */
34
34
  position?: "head" | "head-prepend";
35
35
  }
package/src/preset.ts CHANGED
@@ -16,9 +16,9 @@ import type { Plugin, PluginOption, UserConfig } from "vite";
16
16
  * `index.html` cannot import TypeScript, so a brand name or tagline written there is a literal
17
17
  * that drifts from the one the app renders. This injects them from the module that owns them.
18
18
  *
19
- * Vite already replaces `%VITE_FOO%` from the environment, and that is the right tool when the
20
- * value IS environment — an API URL, a build id. It is the wrong one for brand identity, which
21
- * belongs in a typed module the app imports, not in a `.env` nobody reviews.
19
+ * Vite already replaces `%VITE_FOO%` from the environment, and that is the right tool for an
20
+ * environment value, such as an API URL or a build id. It is the wrong one for brand identity,
21
+ * which belongs in a typed module the app imports, not in a `.env` nobody reviews.
22
22
  */
23
23
  export function htmlPlaceholders(values: Readonly<Record<string, string>>): Plugin {
24
24
  return {
@@ -34,7 +34,7 @@ export function htmlPlaceholders(values: Readonly<Record<string, string>>): Plug
34
34
  }
35
35
 
36
36
  export interface WebPresetOptions {
37
- /** The app folder — the one holding `index.html`. In a `vite.config.ts` that is
37
+ /** The app folder holding `index.html`. In a `vite.config.ts` that is
38
38
  * `import.meta.dirname`. `@` resolves to `<root>/src`. */
39
39
  root: string;
40
40
  /** Dev server port. Two apps in one repo must not share one, which is why it has no clever
@@ -48,11 +48,11 @@ export interface WebPresetOptions {
48
48
  /**
49
49
  * The workspace scope to bundle into the SSR build, as in `"@acme"`.
50
50
  *
51
- * INSURANCE, not a fix. Measured: Vite already bundles linked workspace dependencies in an
52
- * SSR build — one donor runs without this declaration and its SSR output has zero bare
53
- * imports. It is here because workspace packages are consumed as TypeScript SOURCE through
54
- * subpath exports, and leaving them external would hand the runtime `.ts` files with
55
- * Vite-only semantics in them (aliases, `?raw`, `define`) if that behaviour ever changed.
51
+ * INSURANCE, not a fix. Measured: Vite already bundles linked workspace dependencies in an SSR
52
+ * build. One donor runs without this declaration and its SSR output has zero bare imports. It is
53
+ * here because workspace packages are consumed as TypeScript SOURCE through subpath exports, and
54
+ * leaving them external would hand the runtime `.ts` files with Vite-only semantics in them
55
+ * (aliases, `?raw`, `define`) if that behaviour ever changed.
56
56
  */
57
57
  ssrScope?: string;
58
58
  /** More plugins, after the preset's own: `[themeScript(THEME)]`. */
package/src/render.ts CHANGED
@@ -1,10 +1,10 @@
1
1
  /**
2
2
  * One page, as the markup that goes inside `<div id="root">`.
3
3
  *
4
- * This is the build-time half of `main.tsx`, and the app's `entry-server.tsx` is expected to
5
- * be four lines around it: the same `<App />` and the same route table the browser runs, with
6
- * a `StaticRouter` in place of the history the build does not have. `main.tsx` is deliberately
7
- * NOT reused — it reads `window.location`, registers listeners and starts analytics at module
4
+ * This is the build-time half of `main.tsx`, and the app's `entry-server.tsx` is expected to be
5
+ * four lines around it: the same `<App />` and the same route table the browser runs, with a
6
+ * `StaticRouter` in place of the history the build does not have. `main.tsx` is deliberately NOT
7
+ * reused because it reads `window.location`, registers listeners and starts analytics at module
8
8
  * scope, none of which mean anything here.
9
9
  *
10
10
  * No `node:` import: this file is bundled into the SSR build by Vite, and it is React's own
@@ -37,9 +37,9 @@ const HOISTED_HEAD = /^(?:<title>[^<]*<\/title>|<meta\b[^>]*\/?>|<link\b[^>]*\/?
37
37
  * Render a tree to markup with `prerender` from `react-dom/static`.
38
38
  *
39
39
  * **Never `renderToString`.** With `lazy()` routes behind a `<Suspense fallback={<Spinner />}>`,
40
- * `renderToString` renders the FALLBACK — it would write a loading screen into every file and
41
- * pass every gate that only asks whether the root has children. `prerender` waits for the tree
42
- * to settle, which is also why this is async and why the whole renderer contract is.
40
+ * `renderToString` renders the FALLBACK. It would write a loading screen into every file and pass
41
+ * every gate that only asks whether the root has children. `prerender` waits for the tree to
42
+ * settle, which is also why this is async and why the whole renderer contract is.
43
43
  *
44
44
  * Two more halves of the same lesson are here too: `onError` is captured and rethrown, so a
45
45
  * render failure fails the build rather than shipping a partial page; and an empty result is
@@ -59,7 +59,7 @@ export async function renderTree(tree: ReactNode): Promise<string> {
59
59
  const markup = html.replace(HOISTED_HEAD, "");
60
60
  if (markup.trim() === "")
61
61
  throw new Error(
62
- "renderTree: the tree rendered to nothing — usually a router whose location or basename " +
62
+ "renderTree: the tree rendered to nothing. Usually a router whose location or basename " +
63
63
  "matches no route, which React reports as an empty string rather than an error",
64
64
  );
65
65
  return markup;
@@ -68,8 +68,8 @@ export async function renderTree(tree: ReactNode): Promise<string> {
68
68
  /**
69
69
  * What an SSR entry exports, and what {@link loadRenderer} looks for.
70
70
  *
71
- * `context` is opaque on purpose. A multi-locale app builds its i18n instance per call — three
72
- * languages render in one process and a shared singleton would have them racing for one `lng` —
71
+ * `context` is opaque on purpose. A multi-locale app builds its i18n instance per call. Three
72
+ * languages render in one process and a shared singleton would have them racing for one `lng`,
73
73
  * while a single-locale app ignores the argument and keeps its singleton.
74
74
  */
75
75
  export type PageRenderer<Context = unknown> = (route: string, context?: Context) => Promise<string>;