@decocms/start 5.3.0-rc.2 → 5.4.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.
@@ -49,6 +49,7 @@ import { RequestContext } from "./requestContext";
49
49
  import { cleanPathForCacheKey } from "./urlUtils";
50
50
  import { type Device, isMobileUA } from "./useDevice";
51
51
  import { getAppMiddleware } from "./setupApps";
52
+ import { isDevMode } from "./env";
52
53
 
53
54
  /**
54
55
  * Build-time identifier injected by `decoVitePlugin()` (see
@@ -802,8 +803,9 @@ export function createDecoWorkerEntry(
802
803
 
803
804
  const geoVariants = body.countries ?? [];
804
805
 
805
- const cache =
806
- typeof caches !== "undefined"
806
+ const cache = isDevMode()
807
+ ? null
808
+ : typeof caches !== "undefined"
807
809
  ? ((caches as unknown as { default?: Cache }).default ?? null)
808
810
  : null;
809
811
 
@@ -1200,8 +1202,9 @@ export function createDecoWorkerEntry(
1200
1202
  request.method === "POST" &&
1201
1203
  (url.pathname.startsWith("/_serverFn/") || url.pathname.startsWith("/_server/"))
1202
1204
  ) {
1203
- const serverFnCache =
1204
- typeof caches !== "undefined"
1205
+ const serverFnCache = isDevMode()
1206
+ ? null
1207
+ : typeof caches !== "undefined"
1205
1208
  ? ((caches as unknown as { default?: Cache }).default ?? null)
1206
1209
  : null;
1207
1210
 
@@ -1422,9 +1425,10 @@ export function createDecoWorkerEntry(
1422
1425
  return resp;
1423
1426
  }
1424
1427
 
1425
- // Check Cache API (may not be available in local dev / miniflare)
1426
- const cache =
1427
- typeof caches !== "undefined"
1428
+ // Check Cache API disabled in local dev to avoid stale responses
1429
+ const cache = isDevMode()
1430
+ ? null
1431
+ : typeof caches !== "undefined"
1428
1432
  ? ((caches as unknown as { default?: Cache }).default ?? null)
1429
1433
  : null;
1430
1434
 
@@ -32,7 +32,8 @@
32
32
  * ```
33
33
  */
34
34
  import { execFileSync } from "node:child_process";
35
- import { existsSync, readFileSync } from "node:fs";
35
+ import { existsSync, readFileSync, readdirSync, statSync } from "node:fs";
36
+ import path from "node:path";
36
37
 
37
38
  /**
38
39
  * Resolve a per-build identifier for cache-key versioning.
@@ -197,17 +198,135 @@ export function decoVitePlugin() {
197
198
  },
198
199
 
199
200
  configureServer(server) {
200
- // When blocks.gen.json changes on disk, invalidate the .ts module
201
- // so Vite re-runs our load() hook with the fresh data.
202
- server.watcher.on("change", (file) => {
203
- if (file.endsWith("blocks.gen.json")) {
204
- const tsId = file.replace(/\.json$/, ".ts");
205
- const mod = server.environments?.ssr?.moduleGraph?.getModuleById(tsId);
206
- if (mod) {
207
- server.environments.ssr.moduleGraph.invalidateModule(mod);
201
+ // Watch `.deco/blocks/**/*.json` and regenerate `blocks.gen.json` when
202
+ // CMS content changes (manual edit, sync-decofile, daemon PATCH).
203
+ // After regen, we POST the new blocks to the dev server's own
204
+ // /.decofile endpoint — this calls setBlocks() inside the workerd SSR
205
+ // runtime without any module invalidation (which breaks TanStack
206
+ // Start/Router state).
207
+ //
208
+ // Generator is loaded lazily via tsImport (same pattern as the daemon
209
+ // below) so we don't depend on the consumer's TS loader.
210
+ const cwd = process.cwd();
211
+ const blocksDir = path.resolve(cwd, ".deco/blocks");
212
+ const outFile = path.resolve(cwd, "src/server/cms/blocks.gen.ts");
213
+ const jsonFile = outFile.replace(/\.ts$/, ".json");
214
+
215
+ let generateBlocksFn;
216
+ const loadGenerator = () => {
217
+ if (generateBlocksFn) return Promise.resolve(generateBlocksFn);
218
+ // Same tsImport pattern as the daemon loader below — keeps `tsx`
219
+ // scoped to this single import instead of registering a global hook.
220
+ return import("tsx/esm/api")
221
+ .then(({ tsImport }) =>
222
+ tsImport("../../scripts/generate-blocks.ts", import.meta.url),
223
+ )
224
+ .then((mod) => {
225
+ generateBlocksFn = mod.generateBlocks;
226
+ return generateBlocksFn;
227
+ });
228
+ };
229
+
230
+ let regenTimer = null;
231
+ let regenInFlight = false;
232
+ let regenQueued = false;
233
+ const runRegen = async () => {
234
+ if (regenInFlight) {
235
+ regenQueued = true;
236
+ return;
237
+ }
238
+ regenInFlight = true;
239
+ try {
240
+ const fn = await loadGenerator();
241
+ const start = Date.now();
242
+ const result = await fn({ blocksDir, outFile, silent: true });
243
+ const ms = Date.now() - start;
244
+ if (result.empty) {
245
+ console.warn(`[deco] .deco/blocks not found — emitted empty blocks.gen.json`);
246
+ } else {
247
+ console.log(`[deco] regenerated ${result.count} blocks in ${ms}ms`);
248
+ // POST blocks to the dev server's /.decofile endpoint so
249
+ // setBlocks() runs inside the workerd SSR runtime. No module
250
+ // invalidation — that would cascade through the route tree and
251
+ // break TanStack Start server functions.
252
+ try {
253
+ const addr = server.httpServer?.address();
254
+ const port = typeof addr === "object" && addr ? addr.port : 5173;
255
+ const blocksJson = readFileSync(jsonFile, "utf-8");
256
+ const res = await fetch(`http://localhost:${port}/.decofile`, {
257
+ method: "POST",
258
+ headers: { "Content-Type": "application/json" },
259
+ body: blocksJson,
260
+ });
261
+ if (res.ok) {
262
+ server.hot?.send({ type: "full-reload", path: "*" });
263
+ } else {
264
+ console.warn(`[deco] blocks reload failed: ${res.status}`);
265
+ }
266
+ } catch (reloadErr) {
267
+ console.warn("[deco] blocks reload request failed:", reloadErr?.message);
268
+ }
269
+ }
270
+ } catch (err) {
271
+ console.warn("[deco] failed to regenerate blocks:", err?.message ?? err);
272
+ } finally {
273
+ regenInFlight = false;
274
+ if (regenQueued) {
275
+ regenQueued = false;
276
+ scheduleRegen();
277
+ }
278
+ }
279
+ };
280
+ const scheduleRegen = () => {
281
+ if (regenTimer) clearTimeout(regenTimer);
282
+ regenTimer = setTimeout(() => {
283
+ regenTimer = null;
284
+ runRegen();
285
+ }, 150);
286
+ };
287
+
288
+ // chokidar (Vite's watcher) needs the directory added explicitly because
289
+ // `.deco/` lives outside the module graph it walks by default.
290
+ if (existsSync(blocksDir)) {
291
+ server.watcher.add(blocksDir);
292
+ }
293
+ const handleBlocksDirEvent = (file) => {
294
+ if (!file.endsWith(".json")) return;
295
+ if (!file.startsWith(blocksDir + path.sep) && file !== blocksDir) return;
296
+ scheduleRegen();
297
+ };
298
+ server.watcher.on("add", handleBlocksDirEvent);
299
+ server.watcher.on("change", handleBlocksDirEvent);
300
+ server.watcher.on("unlink", handleBlocksDirEvent);
301
+
302
+ // Cold-start bootstrap: regenerate once if the artifact is missing or
303
+ // older than the newest source file. Skips work on a clean build where
304
+ // `npm run build` already produced a current artifact.
305
+ try {
306
+ const needsBootstrap = (() => {
307
+ if (!existsSync(jsonFile)) return existsSync(blocksDir);
308
+ if (!existsSync(blocksDir)) return false;
309
+ const artifactMtime = statSync(jsonFile).mtimeMs;
310
+ for (const entry of readdirSync(blocksDir)) {
311
+ if (!entry.endsWith(".json")) continue;
312
+ try {
313
+ if (statSync(path.join(blocksDir, entry)).mtimeMs > artifactMtime) {
314
+ return true;
315
+ }
316
+ } catch {
317
+ // skip unreadable entries
318
+ }
208
319
  }
320
+ return false;
321
+ })();
322
+ if (needsBootstrap) {
323
+ // Fire and forget — the next request that touches blocks.gen.ts
324
+ // will see the fresh artifact thanks to the change listener above.
325
+ runRegen();
209
326
  }
210
- });
327
+ } catch (err) {
328
+ console.warn("[deco] blocks bootstrap check failed:", err?.message ?? err);
329
+ }
211
330
 
212
331
  // Tunnel + daemon: connect local dev to admin.deco.cx
213
332
  // Activated only when both DECO_SITE_NAME and DECO_ENV_NAME are set.