toiljs 0.0.60 → 0.0.61

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 (119) hide show
  1. package/.github/workflows/ci.yml +31 -0
  2. package/CHANGELOG.md +5 -0
  3. package/build/cli/.tsbuildinfo +1 -1
  4. package/build/cli/index.js +2 -2
  5. package/build/client/.tsbuildinfo +1 -1
  6. package/build/client/index.d.ts +1 -1
  7. package/build/client/index.js +1 -1
  8. package/build/client/routing/mount.js +12 -1
  9. package/build/client/ssr/markers.d.ts +1 -0
  10. package/build/client/ssr/markers.js +3 -0
  11. package/build/compiler/.tsbuildinfo +1 -1
  12. package/build/compiler/config.d.ts +21 -0
  13. package/build/compiler/config.js +35 -0
  14. package/build/compiler/docs.d.ts +2 -1
  15. package/build/compiler/docs.js +33 -304
  16. package/build/compiler/index.d.ts +13 -0
  17. package/build/compiler/index.js +113 -21
  18. package/build/compiler/template-build.d.ts +21 -1
  19. package/build/compiler/template-build.js +110 -26
  20. package/build/compiler/toil-docs.generated.d.ts +1 -0
  21. package/build/compiler/toil-docs.generated.js +20 -0
  22. package/build/devserver/.tsbuildinfo +1 -1
  23. package/build/devserver/daemon/catalog.d.ts +26 -0
  24. package/build/devserver/daemon/catalog.js +48 -0
  25. package/build/devserver/daemon/cron.d.ts +4 -0
  26. package/build/devserver/daemon/cron.js +50 -0
  27. package/build/devserver/daemon/host.d.ts +37 -0
  28. package/build/devserver/daemon/host.js +94 -0
  29. package/build/devserver/daemon/index.d.ts +34 -0
  30. package/build/devserver/daemon/index.js +241 -0
  31. package/build/devserver/db/catalog.d.ts +2 -1
  32. package/build/devserver/db/catalog.js +44 -44
  33. package/build/devserver/db/database.d.ts +27 -11
  34. package/build/devserver/db/database.js +539 -169
  35. package/build/devserver/db/index.d.ts +1 -1
  36. package/build/devserver/db/index.js +1 -1
  37. package/build/devserver/db/routeKinds.d.ts +8 -0
  38. package/build/devserver/db/routeKinds.js +139 -0
  39. package/build/devserver/db/types.d.ts +64 -1
  40. package/build/devserver/db/types.js +33 -1
  41. package/build/devserver/index.d.ts +10 -0
  42. package/build/devserver/index.js +7 -0
  43. package/build/devserver/mstore/store.d.ts +18 -0
  44. package/build/devserver/mstore/store.js +82 -0
  45. package/build/devserver/runtime/host.d.ts +6 -0
  46. package/build/devserver/runtime/host.js +45 -1
  47. package/build/devserver/runtime/module.d.ts +1 -0
  48. package/build/devserver/runtime/module.js +27 -1
  49. package/build/devserver/server.d.ts +6 -0
  50. package/build/devserver/server.js +59 -0
  51. package/build/devserver/ssr.d.ts +25 -0
  52. package/build/devserver/ssr.js +114 -0
  53. package/build/devserver/wasm/sections.d.ts +2 -0
  54. package/build/devserver/wasm/sections.js +42 -0
  55. package/build/devserver/wasm/surface.d.ts +18 -0
  56. package/build/devserver/wasm/surface.js +41 -0
  57. package/docs/README.md +4 -4
  58. package/docs/auth-todo.md +6 -6
  59. package/docs/caching.md +5 -5
  60. package/docs/cli.md +15 -0
  61. package/docs/client.md +40 -0
  62. package/docs/crypto.md +4 -4
  63. package/docs/data.md +6 -6
  64. package/docs/email.md +28 -28
  65. package/docs/environment.md +10 -10
  66. package/docs/index.md +26 -0
  67. package/docs/ratelimit.md +10 -10
  68. package/docs/routing.md +2 -2
  69. package/docs/server.md +61 -0
  70. package/docs/ssr.md +561 -113
  71. package/docs/styling.md +22 -0
  72. package/docs/time.md +1 -1
  73. package/eslint.config.js +10 -1
  74. package/examples/basic/client/components/Header.tsx +3 -0
  75. package/examples/basic/client/routes/features/actions.tsx +0 -2
  76. package/examples/basic/client/routes/hello.tsx +89 -19
  77. package/examples/basic/client/styles/main.css +48 -0
  78. package/examples/basic/server/SsrHelloRender.ts +97 -0
  79. package/examples/basic/server/main.ts +5 -0
  80. package/examples/basic/server/streams/Echo.ts +49 -0
  81. package/package.json +12 -10
  82. package/scripts/gen-toil-docs.mjs +96 -0
  83. package/src/cli/create.ts +2 -2
  84. package/src/client/index.ts +1 -1
  85. package/src/client/routing/mount.tsx +18 -2
  86. package/src/client/ssr/markers.tsx +22 -0
  87. package/src/compiler/config.ts +88 -2
  88. package/src/compiler/docs.ts +47 -308
  89. package/src/compiler/index.ts +236 -32
  90. package/src/compiler/ssr-codegen.ts +1 -1
  91. package/src/compiler/template-build.ts +247 -46
  92. package/src/compiler/toil-docs.generated.ts +26 -0
  93. package/src/devserver/daemon/catalog.ts +120 -0
  94. package/src/devserver/daemon/cron.ts +87 -0
  95. package/src/devserver/daemon/host.ts +224 -0
  96. package/src/devserver/daemon/index.ts +349 -0
  97. package/src/devserver/db/catalog.ts +61 -53
  98. package/src/devserver/db/database.ts +613 -149
  99. package/src/devserver/db/index.ts +1 -1
  100. package/src/devserver/db/routeKinds.ts +147 -0
  101. package/src/devserver/db/types.ts +65 -2
  102. package/src/devserver/index.ts +12 -0
  103. package/src/devserver/mstore/store.ts +121 -0
  104. package/src/devserver/runtime/host.ts +92 -1
  105. package/src/devserver/runtime/module.ts +35 -1
  106. package/src/devserver/server.ts +101 -0
  107. package/src/devserver/ssr.ts +166 -0
  108. package/src/devserver/wasm/sections.ts +59 -0
  109. package/src/devserver/wasm/surface.ts +88 -0
  110. package/test/daemon-build.test.ts +198 -0
  111. package/test/daemon-catalog.test.ts +265 -0
  112. package/test/daemon-emulation.test.ts +216 -0
  113. package/test/devserver-database.test.ts +396 -5
  114. package/test/email-preview.test.ts +6 -1
  115. package/test/fixtures/daemon-app.ts +56 -0
  116. package/test/global-setup.ts +17 -0
  117. package/test/ssr-render.test.ts +94 -27
  118. package/test/ssr-template.test.tsx +44 -1
  119. package/vitest.config.ts +3 -0
@@ -14,6 +14,15 @@
14
14
  * `Slot` enum + `HASH`. A route (or layout) that throws under static markup
15
15
  * (e.g. it uses router hooks outside the supported subset) is skipped with a
16
16
  * warning and falls back to pure client rendering.
17
+ *
18
+ * The `Slot` enum + `HASH` is also what the SERVER `render` imports, so it must
19
+ * exist BEFORE the server compiles. The build therefore runs the render in two
20
+ * passes: a slots PRE-PASS (`extractServerSlots`, before the server build) emits
21
+ * the server-importable `<server>/_ssr/<name>.slots.ts` so toilscript can compile
22
+ * the `render`; the FINAL pass (`extractTemplates`, after the Vite client build)
23
+ * rewrites it against the real built shell so the `HASH` is authoritative, and
24
+ * reports whether it rotated so the caller can recompile the server once. This is
25
+ * what makes a clean build need ZERO hand-maintained slots.
17
26
  */
18
27
 
19
28
  import fs from 'node:fs';
@@ -146,46 +155,81 @@ export function writeTemplateArtifacts(ssrDir: string, art: TemplateArtifacts):
146
155
  fs.writeFileSync(path.join(ssrDir, `${art.name}.slots.ts`), art.slotsModule);
147
156
  }
148
157
 
149
- /** A file-safe, identifier-ish name for a route pattern (`/u/:name` -> `u_name`). */
150
- export function routeTemplateName(pattern: string): string {
151
- const n = pattern.replace(/[^A-Za-z0-9]+/g, '_').replace(/^_+|_+$/g, '');
152
- return n.length > 0 ? n : 'index';
158
+ /**
159
+ * The server source dir (where toilscript-compiled modules live): the dir of the first toilconfig
160
+ * entry, else `<root>/server`. Mirrors the same resolution in `emails.ts` (`_emails.ts` lives here).
161
+ */
162
+ function serverDir(root: string): string {
163
+ try {
164
+ const cfg = JSON.parse(fs.readFileSync(path.join(root, 'toilconfig.json'), 'utf8')) as {
165
+ entries?: unknown;
166
+ };
167
+ const first = Array.isArray(cfg.entries)
168
+ ? cfg.entries.find((e): e is string => typeof e === 'string')
169
+ : undefined;
170
+ if (first) return path.dirname(path.resolve(root, first));
171
+ } catch {
172
+ // fall through to the default
173
+ }
174
+ return path.join(root, 'server');
153
175
  }
154
176
 
155
- /** Synthesize a sample param set for a pattern's dynamic segments. */
156
- function sampleParams(pattern: string): Record<string, string> {
157
- const params: Record<string, string> = {};
158
- for (const m of pattern.matchAll(/[:*]+([A-Za-z0-9_]+)/g)) {
159
- params[m[1]] = 'sample';
177
+ /**
178
+ * The build-owned dir that holds the GENERATED, server-importable `<name>.slots.ts` modules
179
+ * (the `Slot` enum + `HASH` the guest `render` imports). It sits inside the server source tree so
180
+ * toilscript compiles it, named `_ssr` to match the generated `_emails.ts` convention, and is
181
+ * gitignored + regenerated every build — never hand-edited. The server `render` imports
182
+ * `./_ssr/<name>.slots`.
183
+ */
184
+ export function serverSlotsDir(root: string): string {
185
+ return path.join(serverDir(root), '_ssr');
186
+ }
187
+
188
+ /**
189
+ * (Re)write the generated `<server>/_ssr/<name>.slots.ts` module(s) the server `render` imports.
190
+ * Returns the map of `name -> module source` actually on disk afterwards, so the caller can detect
191
+ * whether a later authoritative extraction changed the `HASH` and a server recompile is needed.
192
+ * Only rewrites a file whose content actually changed (keeps mtimes stable for the dev watcher).
193
+ */
194
+ export function writeServerSlotsModules(
195
+ root: string,
196
+ modules: { name: string; slotsModule: string }[],
197
+ ): Map<string, string> {
198
+ const dir = serverSlotsDir(root);
199
+ const written = new Map<string, string>();
200
+ if (modules.length === 0) return written;
201
+ fs.mkdirSync(dir, { recursive: true });
202
+ for (const m of modules) {
203
+ const file = path.join(dir, `${m.name}.slots.ts`);
204
+ let prev: string | null = null;
205
+ try {
206
+ prev = fs.readFileSync(file, 'utf8');
207
+ } catch {
208
+ // first build for this route: no prior module
209
+ }
210
+ if (prev !== m.slotsModule) fs.writeFileSync(file, m.slotsModule);
211
+ written.set(m.name, m.slotsModule);
160
212
  }
161
- return params;
213
+ return written;
162
214
  }
163
215
 
164
- interface RouteModule {
165
- default: ComponentType;
166
- ssr?: boolean;
167
- loader?: (args: { params: Record<string, string>; searchParams: URLSearchParams }) => unknown;
216
+ /** A rendered SSR route plus the artifacts the build emits for it. */
217
+ interface RenderedRoute {
218
+ pattern: string;
219
+ art: TemplateArtifacts;
168
220
  }
169
221
 
170
222
  /**
171
- * Render every `export const ssr = true` route to `<outDir>/_ssr/<name>.{tmpl,
172
- * slots,slots.ts}` + a `templates.json` index, and copy the `.tmpl`/`.slots`
173
- * into the edge host bundle at `hosts/<host>/_tmpl/`. Returns the patterns
174
- * generated. Skips (with a warning) any route that throws under static markup.
223
+ * Spin up a short-lived Vite SSR server, render every `export const ssr = true` route under its
224
+ * layout chain (with sample loader data, in sentinel mode) against `shell`, and return the
225
+ * per-route artifacts. Shared by the pre-pass (`extractServerSlots`, slots only) and the final
226
+ * `extractTemplates` (full artifacts). A route (or layout) that throws under static markup is
227
+ * skipped with a warning and omitted from the result, so it falls back to client rendering.
175
228
  */
176
- export async function extractTemplates(
177
- cfg: ResolvedToilConfig,
178
- hostName = 'edge',
179
- ): Promise<string[]> {
229
+ async function renderSsrRoutes(cfg: ResolvedToilConfig, shell: string): Promise<RenderedRoute[]> {
180
230
  const routes = scanRoutes(cfg.routesAbsDir).filter((r) => r.slot === undefined && !r.intercept);
181
231
  if (routes.length === 0) return [];
182
232
 
183
- const outDir = path.resolve(cfg.root, cfg.outDir);
184
- const stashed = path.join(cfg.toilDir, 'shell.html');
185
- const shellPath = fs.existsSync(stashed) ? stashed : path.join(outDir, 'index.html');
186
- if (!fs.existsSync(shellPath)) return [];
187
- const shell = fs.readFileSync(shellPath, 'utf8');
188
-
189
233
  const warn = (msg: string): void => {
190
234
  process.stderr.write(` toil: SSR ${msg}\n`);
191
235
  };
@@ -223,11 +267,7 @@ export async function extractTemplates(
223
267
  renderToStaticMarkup: typeof renderToStaticMarkup;
224
268
  };
225
269
 
226
- const ssrDir = path.join(outDir, '_ssr');
227
- const hostsTmplDir = path.join(cfg.root, 'hosts', hostName, '_tmpl');
228
- const generated: string[] = [];
229
- const index: { route: string; name: string; hash: string }[] = [];
230
-
270
+ const rendered: RenderedRoute[] = [];
231
271
  try {
232
272
  for (const r of routes) {
233
273
  let mod: RouteModule;
@@ -270,18 +310,7 @@ export async function extractTemplates(
270
310
  createElement: react.createElement,
271
311
  renderToStaticMarkup: reactDomServer.renderToStaticMarkup,
272
312
  });
273
- writeTemplateArtifacts(ssrDir, art);
274
- fs.mkdirSync(hostsTmplDir, { recursive: true });
275
- fs.copyFileSync(
276
- path.join(ssrDir, `${name}.tmpl`),
277
- path.join(hostsTmplDir, `${name}.tmpl`),
278
- );
279
- fs.copyFileSync(
280
- path.join(ssrDir, `${name}.slots`),
281
- path.join(hostsTmplDir, `${name}.slots`),
282
- );
283
- index.push({ route: r.pattern, name, hash: art.hash.toString('hex') });
284
- generated.push(r.pattern);
313
+ rendered.push({ pattern: r.pattern, art });
285
314
  } catch (err) {
286
315
  warn(
287
316
  `skipped ${r.pattern} (render failed: ${
@@ -293,6 +322,178 @@ export async function extractTemplates(
293
322
  } finally {
294
323
  await server.close();
295
324
  }
325
+ return rendered;
326
+ }
327
+
328
+ /**
329
+ * Resolve the HTML shell to splice routes into. The authoritative shell is the BUILT (post-Vite)
330
+ * `index.html`, whose hashed `<script>`/`<link>` tags are part of the template and therefore the
331
+ * coherence `HASH`. `preferBuilt` (the final extraction) demands it; the slots PRE-PASS (which runs
332
+ * before Vite) falls back to the previous build's shell when present (so an unchanged rebuild's
333
+ * pre-pass `HASH` already matches the final one and no server recompile is needed), and finally to
334
+ * the un-built `.toil/index.html` template on a first clean build. Returns `null` when no shell
335
+ * exists at all (no client build yet and no template), so the caller no-ops.
336
+ */
337
+ function resolveShell(cfg: ResolvedToilConfig, preferBuilt: boolean): string | null {
338
+ const outDir = path.resolve(cfg.root, cfg.outDir);
339
+ const builtIndex = path.join(outDir, 'index.html');
340
+ const stashed = path.join(cfg.toilDir, 'shell.html');
341
+ const templateIndex = path.join(cfg.toilDir, 'index.html');
342
+ const order = preferBuilt
343
+ ? [stashed, builtIndex]
344
+ : // Pre-pass (before Vite): use a prior build's shell if it exists so the HASH is stable
345
+ // across rebuilds; otherwise the generated (un-built) template, which still yields the
346
+ // correct Slot ids (the final pass reconciles the HASH).
347
+ [builtIndex, stashed, templateIndex];
348
+ for (const p of order) {
349
+ if (fs.existsSync(p)) return fs.readFileSync(p, 'utf8');
350
+ }
351
+ return null;
352
+ }
353
+
354
+ /**
355
+ * SLOTS PRE-PASS (runs BEFORE the server build): render every `ssr = true` route to its `Slot`
356
+ * enum + `HASH` and write the server-importable `<server>/_ssr/<name>.slots.ts` module(s), so the
357
+ * server `render` can import them when toilscript compiles it. On a clean build no `.slots.ts`
358
+ * exists yet, so this is what bootstraps it; on a rebuild it refreshes them. The `Slot` ids are
359
+ * always correct (they depend only on the route's hole structure); the `HASH` is final only when a
360
+ * prior build's shell is reused, so the final `extractTemplates` reconciles it (and the caller
361
+ * recompiles the server if it rotated). Returns the modules written keyed by route name.
362
+ */
363
+ export async function extractServerSlots(cfg: ResolvedToilConfig): Promise<Map<string, string>> {
364
+ const shell = resolveShell(cfg, false);
365
+ if (shell === null) return new Map();
366
+ const rendered = await renderSsrRoutes(cfg, shell);
367
+ return writeServerSlotsModules(
368
+ cfg.root,
369
+ rendered.map((r) => ({ name: r.art.name, slotsModule: r.art.slotsModule })),
370
+ );
371
+ }
372
+
373
+ /** One SSR route's in-memory dev template: the spliceable `.tmpl` plus its
374
+ * top-level slot insertion points (numeric id -> byte offset), parsed from the
375
+ * `.slots` manifest. Used by the dev server to serve SSR without a prod build. */
376
+ export interface DevSsrTemplate {
377
+ pattern: string;
378
+ name: string;
379
+ tmpl: Buffer;
380
+ entries: { id: number; offset: number }[];
381
+ }
382
+
383
+ /**
384
+ * Render every `ssr = true` route against the given (live, Vite-transformed) dev
385
+ * `shell` into an in-memory template + slot offsets, for the dev server to splice
386
+ * the guest `render` values into. Unlike {@link extractTemplates} this has NO
387
+ * side effects (it writes nothing and does not touch `server/_ssr` or the host
388
+ * bundle); the dev server builds together with the guest, so there is no hash to
389
+ * reconcile. Returns `[]` when no route opts in.
390
+ */
391
+ export async function extractDevSsrTemplates(
392
+ cfg: ResolvedToilConfig,
393
+ shell: string,
394
+ ): Promise<DevSsrTemplate[]> {
395
+ const rendered = await renderSsrRoutes(cfg, shell);
396
+ return rendered.map(({ pattern, art }) => {
397
+ // Parse the `.slots` manifest: 46-byte header (n_slots at offset 44),
398
+ // then 8-byte entries (u32 offset, u16 id, u8 kind, u8 reserved).
399
+ const bin = art.slotsBin;
400
+ const n = bin.readUInt16LE(44);
401
+ const entries: { id: number; offset: number }[] = [];
402
+ let o = 46;
403
+ for (let i = 0; i < n; i++) {
404
+ entries.push({ offset: bin.readUInt32LE(o), id: bin.readUInt16LE(o + 4) });
405
+ o += 8;
406
+ }
407
+ return { pattern, name: art.name, tmpl: art.tmpl, entries };
408
+ });
409
+ }
410
+
411
+ /** A file-safe, identifier-ish name for a route pattern (`/u/:name` -> `u_name`). */
412
+ export function routeTemplateName(pattern: string): string {
413
+ const n = pattern.replace(/[^A-Za-z0-9]+/g, '_').replace(/^_+|_+$/g, '');
414
+ return n.length > 0 ? n : 'index';
415
+ }
416
+
417
+ /** Synthesize a sample param set for a pattern's dynamic segments. */
418
+ function sampleParams(pattern: string): Record<string, string> {
419
+ const params: Record<string, string> = {};
420
+ for (const m of pattern.matchAll(/[:*]+([A-Za-z0-9_]+)/g)) {
421
+ params[m[1]] = 'sample';
422
+ }
423
+ return params;
424
+ }
425
+
426
+ interface RouteModule {
427
+ default: ComponentType;
428
+ ssr?: boolean;
429
+ loader?: (args: { params: Record<string, string>; searchParams: URLSearchParams }) => unknown;
430
+ }
431
+
432
+ /** The outcome of the final {@link extractTemplates} pass. */
433
+ export interface ExtractResult {
434
+ /** The route patterns that produced a template. */
435
+ readonly generated: string[];
436
+ /**
437
+ * Whether the AUTHORITATIVE `<server>/_ssr/<name>.slots.ts` module(s) just written differ from
438
+ * the ones the server was already compiled against (`priorServerSlots`). True means the `HASH`
439
+ * (or `Slot` ids) rotated after the server build, so the server must be recompiled to bake the
440
+ * correct `HASH` (otherwise the host rejects the guest's stale hash as a deploy skew).
441
+ */
442
+ readonly serverSlotsChanged: boolean;
443
+ }
444
+
445
+ /**
446
+ * FINAL pass (runs AFTER the client/Vite build): render every `export const ssr = true` route to
447
+ * `<outDir>/_ssr/<name>.{tmpl,slots,slots.ts}` + a `templates.json` index, copy the `.tmpl`/`.slots`
448
+ * into the edge host bundle at `hosts/<host>/_tmpl/`, AND (re)write the server-importable
449
+ * `<server>/_ssr/<name>.slots.ts` module(s) — now against the real built shell, so the `HASH` is
450
+ * authoritative. Skips (with a warning) any route that throws under static markup.
451
+ *
452
+ * `priorServerSlots` is the module map the slots PRE-PASS wrote (what the server was compiled
453
+ * against); compared against the authoritative modules here to report whether a server recompile is
454
+ * needed (see {@link ExtractResult.serverSlotsChanged}).
455
+ */
456
+ export async function extractTemplates(
457
+ cfg: ResolvedToilConfig,
458
+ hostName = 'edge',
459
+ priorServerSlots: Map<string, string> = new Map(),
460
+ ): Promise<ExtractResult> {
461
+ const shell = resolveShell(cfg, true);
462
+ if (shell === null) return { generated: [], serverSlotsChanged: false };
463
+
464
+ const rendered = await renderSsrRoutes(cfg, shell);
465
+
466
+ const outDir = path.resolve(cfg.root, cfg.outDir);
467
+ const ssrDir = path.join(outDir, '_ssr');
468
+ const hostsTmplDir = path.join(cfg.root, 'hosts', hostName, '_tmpl');
469
+ const generated: string[] = [];
470
+ const index: { route: string; name: string; hash: string }[] = [];
471
+
472
+ for (const { pattern, art } of rendered) {
473
+ writeTemplateArtifacts(ssrDir, art);
474
+ fs.mkdirSync(hostsTmplDir, { recursive: true });
475
+ fs.copyFileSync(
476
+ path.join(ssrDir, `${art.name}.tmpl`),
477
+ path.join(hostsTmplDir, `${art.name}.tmpl`),
478
+ );
479
+ fs.copyFileSync(
480
+ path.join(ssrDir, `${art.name}.slots`),
481
+ path.join(hostsTmplDir, `${art.name}.slots`),
482
+ );
483
+ index.push({ route: pattern, name: art.name, hash: art.hash.toString('hex') });
484
+ generated.push(pattern);
485
+ }
486
+
487
+ // Write the AUTHORITATIVE server-importable slots module(s) (the real built-shell HASH) and
488
+ // detect whether they changed since the pre-pass the server was compiled against.
489
+ const authoritative = writeServerSlotsModules(
490
+ cfg.root,
491
+ rendered.map((r) => ({ name: r.art.name, slotsModule: r.art.slotsModule })),
492
+ );
493
+ let serverSlotsChanged = false;
494
+ for (const [name, mod] of authoritative) {
495
+ if (priorServerSlots.get(name) !== mod) serverSlotsChanged = true;
496
+ }
296
497
 
297
498
  if (generated.length > 0) {
298
499
  fs.mkdirSync(ssrDir, { recursive: true });
@@ -303,5 +504,5 @@ export async function extractTemplates(
303
504
  }\n`,
304
505
  );
305
506
  }
306
- return generated;
507
+ return { generated, serverSlotsChanged };
307
508
  }