@iterant/site-runtime 3.8.1 → 3.8.2

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.8.1._
53
+ _Generated from package.json by scripts/generate-kit-table.mjs. Runtime 3.8.2._
54
54
 
55
55
  **Toolchain** (this package owns the version; do NOT declare these):
56
56
 
@@ -749,6 +749,29 @@ The preset also pre-bundles `astro/logger/json`, which the supervisor's
749
749
  reloaded the program and split React in two inside workerd, and every island
750
750
  render then failed on `useRef`.
751
751
 
752
+ ### The worker's half of a content refresh (3.8.2)
753
+
754
+ Astro syncs a changed page entry on the node side and then invalidates the data
755
+ store module in the ssr module graph. Under the cloudflare adapter that graph
756
+ entry is the only lever that reaches a render: the modules are evaluated inside
757
+ workerd, and Astro cannot clear a runner it does not own. When the entry is
758
+ missing at the moment the store is written (after a program reload, measured in
759
+ the sandbox image), the node side logs `Reloaded data from home.json`, the store
760
+ on disk is fresh, and `/` keeps serving the entry the worker evaluated last,
761
+ until any SSR module changes. A first build showed two sections as "Coming up"
762
+ placeholders for 95 seconds after their real rows had been written and
763
+ committed.
764
+
765
+ Since 3.8.2 the signals integration watches `.astro/data-store.json`, the file
766
+ Astro itself writes on every sync, and hands it to every environment Astro
767
+ cannot reach in process: the content virtual modules are invalidated in that
768
+ environment's graph and the environment is sent a full reload, so the worker
769
+ imports its entrypoints again against the new store. It prints
770
+ `{"iterant":"dev-server","event":"content-reloaded","restart":...,"environments":["ssr"]}`.
771
+ The reload is the same one an `.astro` edit causes and costs the same; a `.tsx`
772
+ edit still hot-reloads, and the boot server is armed the same way as a restarted
773
+ one, because the hole belongs to the adapter and not to the restart.
774
+
752
775
  ## Verify: the gate
753
776
 
754
777
  `bun run verify` maps to `site-runtime verify` and is the gate. It is silent on
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@iterant/site-runtime",
3
- "version": "3.8.1",
3
+ "version": "3.8.2",
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": {
@@ -164,6 +164,10 @@ export function createContentSchemas({
164
164
  // brand asset; alt is the accessible/og:image:alt text.
165
165
  ogImage: z
166
166
  .object({
167
+ // Every other image leaf in an entry is tagged type: "image",
168
+ // and the model reaches for the same tag here; the tag is
169
+ // accepted so a share image never refuses a whole page write.
170
+ type: z.literal("image").optional(),
167
171
  src: z.string().url(),
168
172
  assetId: z.string().optional(),
169
173
  alt: z.string().optional(),
@@ -66,6 +66,33 @@ import { beginRestart, endRestart } from "./dev-restart-state.mjs";
66
66
  // and it is why narrowing watcher #1 with `unwatch` was NOT done: it would buy
67
67
  // back six watched paths and cost the self-heal.
68
68
  //
69
+ // THE WORKER'S HALF OF A CONTENT REFRESH. A synced store still has to reach the
70
+ // environment that renders. Astro does that in
71
+ // `content/vite-plugin-content-virtual-mod.js` (`invalidateDataStore`): it
72
+ // invalidates the data store module in the ssr module graph and, for an
73
+ // environment whose modules THIS process evaluates, drops the runner's copy as
74
+ // well. `astro dev` with the cloudflare adapter has neither shape: the modules
75
+ // live inside workerd, the plugin's environment is a plain DevEnvironment, and
76
+ // the graph entry is the only lever Astro has left. A prune or a program reload
77
+ // can leave that graph without the entry at the moment the store is written,
78
+ // and then nothing reaches the render at all.
79
+ //
80
+ // Measured in the sandbox image on 2026-09-11, with the graph lever removed to
81
+ // hold the state still: the node side logs `Reloaded data from home.json`, the
82
+ // synced store on disk carries the new rows, `astro:content-changed` goes out
83
+ // over the worker's channel, and / keeps serving the entry the worker evaluated
84
+ // at the last program reload for as long as nothing else touches an SSR module.
85
+ // A customer's first build showed two sections as "Coming up:" placeholders for
86
+ // 95 s after their real rows had been written and committed.
87
+ //
88
+ // So the store write drives a reload of every environment Astro cannot reach in
89
+ // process. The graph entries go first, so the re-import reads the new store
90
+ // rather than a cached transform, and the full reload makes the worker clear its
91
+ // evaluated modules and import its entrypoints again. This watches the same file
92
+ // Astro's own plugin watches, so the store is on disk by the time the event
93
+ // arrives, and it is armed on the BOOT server too: the hole belongs to the
94
+ // adapter, not to the restart.
95
+ //
69
96
  // STDOUT SIGNALS. One single-line JSON object per event, every one keyed
70
97
  // `iterant` so the preview supervisor (which already matches Astro's `ready in`
71
98
  // line) can filter them out of the dev log. Every `at`/timestamp field in this
@@ -78,6 +105,7 @@ import { beginRestart, endRestart } from "./dev-restart-state.mjs";
78
105
  // {"iterant":"dev-server","event":"ready","restart":true}
79
106
  // {"iterant":"dev-server","event":"content-synced","restart":true,"paths":[...]}
80
107
  // {"iterant":"dev-server","event":"content-sync-failed","restart":true,"paths":[...],"error":"..."}
108
+ // {"iterant":"dev-server","event":"content-reloaded","restart":true,"environments":["ssr"]}
81
109
  //
82
110
  // `paths` are root-relative (`src/content/pages/home.json`), which is what a
83
111
  // writer wrote and the only key shape that survives a relocated tree. An empty
@@ -121,6 +149,10 @@ import { beginRestart, endRestart } from "./dev-restart-state.mjs";
121
149
  // `globalDataStore`), which an integration must not import.
122
150
 
123
151
  const REFRESH_DEBOUNCE_MS = 100;
152
+ /** Astro's dev-time content store, under the project's own `.astro` dir. */
153
+ const DATA_STORE_SEGMENTS = [".astro", "data-store.json"];
154
+ /** The modules an SSR render reads a page entry through, by resolved id. */
155
+ const CONTENT_VIRTUAL_IDS = ["\0astro:data-layer-content", "\0astro:content"];
124
156
  const STATE_PATH = "/__iterant/dev-state";
125
157
  const REFRESH_PATH = "/__iterant/refresh";
126
158
  const STATE_HEADER = "x-iterant-dev-state";
@@ -241,6 +273,23 @@ export default function devServerSignals({ pagesDir, chromeDir }) {
241
273
  scheduleRefresh();
242
274
  }
243
275
 
276
+ // Whichever side synced it, a written store has to reach the render.
277
+ // Armed on both kinds of server: the environment that cannot be
278
+ // invalidated is the adapter's, and a boot server has the same one.
279
+ const dataStore = resolve(root, ...DATA_STORE_SEGMENTS);
280
+ server.watcher.on("all", (event, path) => {
281
+ if (event !== "add" && event !== "change") return;
282
+ if (path !== dataStore) return;
283
+ const reloaded = reloadRenderEnvironments(server);
284
+ if (reloaded.length > 0) {
285
+ signal({
286
+ event: "content-reloaded",
287
+ restart,
288
+ environments: reloaded,
289
+ });
290
+ }
291
+ });
292
+
244
293
  // An obsolete generation must not fire after its server is gone: Vite's
245
294
  // in-place restart calls close() on the old server (which closes this
246
295
  // httpServer) before the new one listens, and a timer still pending
@@ -317,6 +366,56 @@ export default function devServerSignals({ pagesDir, chromeDir }) {
317
366
  };
318
367
  }
319
368
 
369
+ /**
370
+ * Hand the freshly written store to every environment Astro's own invalidation
371
+ * leaves behind: not the client, which Astro reloads itself, and not one this
372
+ * process evaluates, whose runner Astro already clears. What is left in this
373
+ * stack is the single workerd environment the cloudflare plugin creates, and it
374
+ * is the one that renders.
375
+ *
376
+ * The `runner` property is the test rather than vite's `isRunnableDevEnvironment`
377
+ * (which is `instanceof RunnableDevEnvironment`): that handle is what Astro's
378
+ * own content invalidation reaches for, and this file is loaded as source out of
379
+ * a brand's node_modules, where an import of vite to ask the question would be
380
+ * the only one any integration here has.
381
+ *
382
+ * @typedef {Parameters<
383
+ * NonNullable<import("astro").AstroIntegration["hooks"]["astro:server:setup"]>
384
+ * >[0]["server"]} DevServer
385
+ */
386
+
387
+ /**
388
+ * @param {DevServer} server
389
+ * @returns {string[]} The environments reloaded, for the signal line.
390
+ */
391
+ function reloadRenderEnvironments(server) {
392
+ /** @type {string[]} */
393
+ const reloaded = [];
394
+ for (const environment of Object.values(server.environments)) {
395
+ if (environment.name === "client" || "runner" in environment) continue;
396
+ const timestamp = Date.now();
397
+ for (const id of CONTENT_VIRTUAL_IDS) {
398
+ const module = environment.moduleGraph.getModuleById(id);
399
+ // The graph entry first: a reload without it re-imports the cached
400
+ // transform, which is the store as it stood when that transform ran.
401
+ if (module) {
402
+ environment.moduleGraph.invalidateModule(
403
+ module,
404
+ undefined,
405
+ timestamp,
406
+ true,
407
+ );
408
+ }
409
+ }
410
+ // No `triggeredBy`: the runner then clears every evaluated module and
411
+ // imports each entrypoint again, rather than walking one file's importers
412
+ // to entrypoints the store module has no edge to.
413
+ environment.hot.send({ type: "full-reload", path: "*" });
414
+ reloaded.push(environment.name);
415
+ }
416
+ return reloaded;
417
+ }
418
+
320
419
  /**
321
420
  * One signal line on stdout. Written directly rather than through Astro's
322
421
  * logger, which wraps and colorizes: a supervisor parses these, so they have to