@iterant/site-runtime 3.6.0 → 3.6.1

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.
@@ -50,7 +50,7 @@ runtime and says so.
50
50
 
51
51
  <!-- generated: available libraries -->
52
52
 
53
- _Generated from package.json by scripts/generate-kit-table.mjs. Runtime 3.6.0._
53
+ _Generated from package.json by scripts/generate-kit-table.mjs. Runtime 3.6.1._
54
54
 
55
55
  **Toolchain** (this package owns the version; do NOT declare these):
56
56
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@iterant/site-runtime",
3
- "version": "3.6.0",
3
+ "version": "3.6.1",
4
4
  "type": "module",
5
5
  "description": "The platform layer every Iterant brand site runs on: content grammar, collection schemas, SEO head and JSON-LD, layout core, Astro config preset, dev integrations and the verify gates.",
6
6
  "scripts": {
@@ -19,12 +19,29 @@
19
19
  * the shape the catch-all's pickPageExport (src/lib/bespoke-pages.ts)
20
20
  * resolves.
21
21
  *
22
- * For every NON-DRAFT bespoke base entry, this asserts (1) against the built
23
- * manifest and (2) by importing the built client entry itself. Draft bespoke
24
- * pages are exempt: drafts never publish, their siblings are never advertised
25
- * (hreflang/sitemap), and the stock `not-found` page is a draft that
26
- * deliberately lives outside the `<base>/page.tsx` convention (404.astro
27
- * renders it). Publishing a bespoke page puts it in scope on the next verify.
22
+ * For every NON-DRAFT bespoke base entry THAT HAS a locale sibling file
23
+ * (`<base>.<locale>.json`, draft or not: a draft sibling still renders in a
24
+ * preview), this asserts (1) against the built manifest and (2) by importing
25
+ * the built client entry itself. Draft bespoke pages are exempt: drafts never
26
+ * publish, their siblings are never advertised (hreflang/sitemap), and the
27
+ * stock `not-found` page is a draft that deliberately lives outside the
28
+ * `<base>/page.tsx` convention (404.astro renders it). Publishing a bespoke
29
+ * page puts it in scope on the next verify.
30
+ *
31
+ * Scoping to bases-with-siblings (3.6.1) loses nothing: the hazard cannot
32
+ * materialize without a sibling, a sibling only comes into existence through
33
+ * a save, and every save runs this gate, so a base picks up the full check
34
+ * the moment its first sibling lands. Before that, a pre-convention bespoke
35
+ * page (every pre-3.6 blog) must not block a same-major runtime bump over a
36
+ * feature the repo does not use.
37
+ *
38
+ * A base whose sibling route is SHADOWED is also out of scope (3.6.1): when
39
+ * `src/pages/[locale]/<base>.astro` exists (`index.astro` for the `home`
40
+ * base), Astro's route priority sends `/es` to that explicit route, never to
41
+ * the catch-all, so the catch-all hydration contract this gate asserts does
42
+ * not serve the sibling at all. Replica-era repos localize exactly this way.
43
+ * The shadowing route's own hydration is `astro build`'s concern, like every
44
+ * other explicit route.
28
45
  *
29
46
  * It also pins the Astro runtime contract the catch-all relies on: the island
30
47
  * renderer must keep accepting `client:component-path` /
@@ -37,7 +54,7 @@
37
54
  * weaken this script.
38
55
  */
39
56
 
40
- import { readdir, readFile } from "node:fs/promises";
57
+ import { readdir, readFile, stat } from "node:fs/promises";
41
58
  import { join } from "node:path";
42
59
  import { pathToFileURL } from "node:url";
43
60
  import { exit } from "node:process";
@@ -51,41 +68,34 @@ const CONVENTIONS =
51
68
  /** @type {string[]} */
52
69
  const problems = [];
53
70
 
54
- // ---- SF-pin: the Astro runtime directive contract --------------------------
55
-
56
- const hydrationJs = join(
57
- process.cwd(),
58
- "node_modules/astro/dist/runtime/server/hydration.js",
59
- );
60
- const hydrationSrc = await readFile(hydrationJs, "utf8").catch(() => "");
61
- for (const directive of ["client:component-path", "client:component-export"]) {
62
- if (!hydrationSrc.includes(`case "${directive}"`)) {
63
- problems.push(
64
- `astro runtime contract changed: ${hydrationJs} no longer handles ` +
65
- `"${directive}" as a prop. Bespoke locale siblings hydrate through ` +
66
- `exactly that escape hatch (src/pages/[...slug].astro) — re-verify ` +
67
- `sibling hydration against this Astro version before shipping.`,
68
- );
69
- }
70
- }
71
-
72
- // ---- SF1+SF2: every non-draft bespoke base ---------------------------------
71
+ // ---- SF1+SF2: every non-draft bespoke base with a sibling ------------------
73
72
 
74
- /** Non-draft bespoke base entry ids (`home`), never locale siblings. */
75
- async function bespokeBases() {
73
+ /**
74
+ * Non-draft bespoke base entry ids (`home`), never locale siblings, plus the
75
+ * set of base ids that have at least one sibling file. Sibling presence
76
+ * counts by filename alone (`home.es.json` -> `home`): a sibling's own draft
77
+ * flag or parse failure does not excuse its base, because the sibling still
78
+ * renders in a preview.
79
+ */
80
+ async function bespokeInventory() {
76
81
  const pagesDir = join(process.cwd(), "src/content/pages");
77
82
  /** @type {string[]} */
78
83
  const bases = [];
84
+ /** @type {Set<string>} */
85
+ const siblingBases = new Set();
79
86
  /** @type {string[]} */
80
87
  let names = [];
81
88
  try {
82
89
  names = await readdir(pagesDir);
83
90
  } catch {
84
- return bases; // no pages dir astro check owns that failure
91
+ return { bases, siblingBases }; // no pages dir: astro check owns that failure
85
92
  }
86
93
  for (const name of names.filter((n) => n.endsWith(".json")).sort()) {
87
94
  const id = name.replace(/\.json$/, "");
88
- if (id.includes(".")) continue; // locale sibling — guarded via its base
95
+ if (id.includes(".")) {
96
+ siblingBases.add(id.slice(0, id.indexOf(".")));
97
+ continue; // locale sibling: guarded via its base
98
+ }
89
99
  /** @type {{ mode?: string; draft?: boolean }} */
90
100
  let entry;
91
101
  try {
@@ -95,7 +105,21 @@ async function bespokeBases() {
95
105
  }
96
106
  if (entry.mode === "bespoke" && entry.draft !== true) bases.push(id);
97
107
  }
98
- return bases;
108
+ return { bases, siblingBases };
109
+ }
110
+
111
+ /**
112
+ * Whether an explicit locale route shadows this base's siblings. Astro sends
113
+ * `/es` (and `/es/<base>`) to `src/pages/[locale]/index.astro` (respectively
114
+ * `[locale]/<base>.astro`) before any rest-param catch-all, so when that file
115
+ * exists the sibling never renders through the catch-all this gate asserts.
116
+ */
117
+ async function siblingRouteShadowed(base) {
118
+ const file = base === "home" ? "index.astro" : `${base}.astro`;
119
+ return stat(join(process.cwd(), "src/pages/[locale]", file)).then(
120
+ (s) => s.isFile(),
121
+ () => false,
122
+ );
99
123
  }
100
124
 
101
125
  /**
@@ -133,8 +157,37 @@ async function builtEntryModules() {
133
157
  return null;
134
158
  }
135
159
 
136
- const bases = await bespokeBases();
137
- if (bases.length > 0 && problems.length === 0) {
160
+ const inventory = await bespokeInventory();
161
+ /** @type {string[]} */
162
+ const bases = [];
163
+ for (const base of inventory.bases) {
164
+ if (!inventory.siblingBases.has(base)) continue;
165
+ if (await siblingRouteShadowed(base)) continue;
166
+ bases.push(base);
167
+ }
168
+ // Nothing can hydrate through the escape hatch: the first sibling arrives via
169
+ // a save, and every save re-runs this gate, so exiting here defers nothing.
170
+ if (bases.length === 0) exit(0);
171
+
172
+ // ---- SF-pin: the Astro runtime directive contract --------------------------
173
+
174
+ const hydrationJs = join(
175
+ process.cwd(),
176
+ "node_modules/astro/dist/runtime/server/hydration.js",
177
+ );
178
+ const hydrationSrc = await readFile(hydrationJs, "utf8").catch(() => "");
179
+ for (const directive of ["client:component-path", "client:component-export"]) {
180
+ if (!hydrationSrc.includes(`case "${directive}"`)) {
181
+ problems.push(
182
+ `astro runtime contract changed: ${hydrationJs} no longer handles ` +
183
+ `"${directive}" as a prop. Bespoke locale siblings hydrate through ` +
184
+ `exactly that escape hatch (src/pages/[...slug].astro): re-verify ` +
185
+ `sibling hydration against this Astro version before shipping.`,
186
+ );
187
+ }
188
+ }
189
+
190
+ if (problems.length === 0) {
138
191
  const entryModules = await builtEntryModules();
139
192
  if (!entryModules) {
140
193
  problems.push(