@iterant/site-runtime 3.8.1 → 3.9.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/docs/runtime-contract.md
CHANGED
|
@@ -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.
|
|
53
|
+
_Generated from package.json by scripts/generate-kit-table.mjs. Runtime 3.9.0._
|
|
54
54
|
|
|
55
55
|
**Toolchain** (this package owns the version; do NOT declare these):
|
|
56
56
|
|
|
@@ -617,13 +617,27 @@ The layout preloads the upright latin face of each named family and nothing
|
|
|
617
617
|
else. Every face stays in the `@font-face` block, so the browser still fetches a
|
|
618
618
|
slanted or extended-latin one the moment a glyph needs it.
|
|
619
619
|
|
|
620
|
-
**`src/site-config.ts` is watched.** Astro restarts its dev server
|
|
621
|
-
config file, `package.json` and the tsconfig sources, and on nothing
|
|
622
|
-
family added to `SITE_CONFIG.fonts` would be rendered by the layout
|
|
623
|
-
had never resolved it. The preset adds the file to
|
|
624
|
-
(`site-config-watch`), which makes the change restart the
|
|
625
|
-
font declarations. A repo that keeps its site config
|
|
626
|
-
`iterantStarter({ siteConfigPath })`.
|
|
620
|
+
**`src/site-config.ts` is watched in list mode.** Astro restarts its dev server
|
|
621
|
+
on its own config file, `package.json` and the tsconfig sources, and on nothing
|
|
622
|
+
else, so a family added to `SITE_CONFIG.fonts` would be rendered by the layout
|
|
623
|
+
while Astro had never resolved it. The preset adds the file to
|
|
624
|
+
`settings.watchFiles` (`site-config-watch`), which makes the change restart the
|
|
625
|
+
server and reach the font declarations. A repo that keeps its site config
|
|
626
|
+
elsewhere passes `iterantStarter({ siteConfigPath })`.
|
|
627
|
+
|
|
628
|
+
**Under `fonts: "catalog"` the preset registers no restart watch on it; Vite
|
|
629
|
+
still watches it as a source module.** Every
|
|
630
|
+
family is declared at boot, so a family written into `SITE_CONFIG.fonts` after
|
|
631
|
+
that is one Astro already resolved, and the layout picks it up on the first
|
|
632
|
+
render after the module's hot update has been processed. The site config is then outside the Astro
|
|
633
|
+
config's import graph too, since the config no longer reads it, so no write of
|
|
634
|
+
it restarts the server. That is the mode the platform's own starters run: the
|
|
635
|
+
direction pick writes the file on every first build, and the restart it used to
|
|
636
|
+
cause cost 11.7s of preview outage, ending in a re-attach the customer saw as
|
|
637
|
+
the waking card (measured 2026-09-12). The boot pays 6.3s of font resolution
|
|
638
|
+
instead of 1.2s on a cold tree, and nothing on a tree whose `.astro/fonts` cache
|
|
639
|
+
came with it and whose metadata is under seven days old (Astro refreshes the
|
|
640
|
+
metadata after that, through the cache's own files).
|
|
627
641
|
|
|
628
642
|
### The browser floor
|
|
629
643
|
|
|
@@ -749,6 +763,29 @@ The preset also pre-bundles `astro/logger/json`, which the supervisor's
|
|
|
749
763
|
reloaded the program and split React in two inside workerd, and every island
|
|
750
764
|
render then failed on `useRef`.
|
|
751
765
|
|
|
766
|
+
### The worker's half of a content refresh (3.8.2)
|
|
767
|
+
|
|
768
|
+
Astro syncs a changed page entry on the node side and then invalidates the data
|
|
769
|
+
store module in the ssr module graph. Under the cloudflare adapter that graph
|
|
770
|
+
entry is the only lever that reaches a render: the modules are evaluated inside
|
|
771
|
+
workerd, and Astro cannot clear a runner it does not own. When the entry is
|
|
772
|
+
missing at the moment the store is written (after a program reload, measured in
|
|
773
|
+
the sandbox image), the node side logs `Reloaded data from home.json`, the store
|
|
774
|
+
on disk is fresh, and `/` keeps serving the entry the worker evaluated last,
|
|
775
|
+
until any SSR module changes. A first build showed two sections as "Coming up"
|
|
776
|
+
placeholders for 95 seconds after their real rows had been written and
|
|
777
|
+
committed.
|
|
778
|
+
|
|
779
|
+
Since 3.8.2 the signals integration watches `.astro/data-store.json`, the file
|
|
780
|
+
Astro itself writes on every sync, and hands it to every environment Astro
|
|
781
|
+
cannot reach in process: the content virtual modules are invalidated in that
|
|
782
|
+
environment's graph and the environment is sent a full reload, so the worker
|
|
783
|
+
imports its entrypoints again against the new store. It prints
|
|
784
|
+
`{"iterant":"dev-server","event":"content-reloaded","restart":...,"environments":["ssr"]}`.
|
|
785
|
+
The reload is the same one an `.astro` edit causes and costs the same; a `.tsx`
|
|
786
|
+
edit still hot-reloads, and the boot server is armed the same way as a restarted
|
|
787
|
+
one, because the hole belongs to the adapter and not to the restart.
|
|
788
|
+
|
|
752
789
|
## Verify: the gate
|
|
753
790
|
|
|
754
791
|
`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.
|
|
3
|
+
"version": "3.9.0",
|
|
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": {
|
package/src/config/preset.ts
CHANGED
|
@@ -89,6 +89,11 @@ const FONT_SUBSETS: NonEmpty<string> = ["latin", "latin-ext"];
|
|
|
89
89
|
* render a face Astro never resolved. `site-config-watch` makes that restart
|
|
90
90
|
* happen, and these eleven make it unnecessary for the common pick.
|
|
91
91
|
*
|
|
92
|
+
* `"catalog"` takes the other road: with every family declared, nothing the
|
|
93
|
+
* platform writes into SITE_CONFIG.fonts can name a face Astro did not resolve,
|
|
94
|
+
* so the site config leaves the dev server's restart list entirely and a
|
|
95
|
+
* direction pick reaches the page as a hot update instead of a reboot.
|
|
96
|
+
*
|
|
92
97
|
* A name the catalog does not carry is skipped rather than declared: Astro's
|
|
93
98
|
* `<Font>` throws on a CSS variable no family registered, and the layout reads
|
|
94
99
|
* the same catalog to decide what to render.
|
|
@@ -163,9 +168,12 @@ export interface IterantStarterOptions {
|
|
|
163
168
|
*/
|
|
164
169
|
fonts?: readonly string[] | "catalog";
|
|
165
170
|
/**
|
|
166
|
-
* The repo's site config module, relative to the project root.
|
|
167
|
-
* change to `SITE_CONFIG.fonts` restarts the dev server
|
|
168
|
-
* declarations; the preset does not import the file, it
|
|
171
|
+
* The repo's site config module, relative to the project root. In list mode
|
|
172
|
+
* it is watched, so a change to `SITE_CONFIG.fonts` restarts the dev server
|
|
173
|
+
* and reaches the font declarations; the preset does not import the file, it
|
|
174
|
+
* only names it. Under `fonts: "catalog"` the preset registers no restart
|
|
175
|
+
* watch on it (Vite still watches it as a module), because every
|
|
176
|
+
* family is already declared.
|
|
169
177
|
*/
|
|
170
178
|
siteConfigPath?: string;
|
|
171
179
|
/**
|
|
@@ -226,8 +234,16 @@ export function iterantStarter({
|
|
|
226
234
|
newFileReload(),
|
|
227
235
|
// Dev-only: restart signals, the content re-arm, and the dev-state probe.
|
|
228
236
|
devServerSignals({ pagesDir, chromeDir }),
|
|
229
|
-
// Restart the dev server when the site config the fonts come from changes
|
|
230
|
-
|
|
237
|
+
// Restart the dev server when the site config the fonts come from changes,
|
|
238
|
+
// in list mode alone. The watch exists for one reason: the preset turns
|
|
239
|
+
// SITE_CONFIG.fonts into font declarations once, at server start, so a
|
|
240
|
+
// family added to that list after boot would render a face Astro never
|
|
241
|
+
// resolved. `"catalog"` declares every family up front, which answers the
|
|
242
|
+
// same question without a restart, and the restart is not free: every first
|
|
243
|
+
// build writes src/site-config.ts on the direction pick, and that cost 11.7s
|
|
244
|
+
// of preview outage, with a `preview.upstream-error` and a re-attach the
|
|
245
|
+
// customer sees as the waking card (measured 2026-09-12, run 10).
|
|
246
|
+
...(fonts === "catalog" ? [] : [siteConfigWatch(siteConfigPath)]),
|
|
231
247
|
];
|
|
232
248
|
|
|
233
249
|
const fontFamilies = catalogFontFamilies(fonts);
|
package/src/content/schema.ts
CHANGED
|
@@ -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
|