@decocms/start 0.30.0 → 0.30.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@decocms/start",
3
- "version": "0.30.0",
3
+ "version": "0.30.2",
4
4
  "type": "module",
5
5
  "description": "Deco framework for TanStack Start - CMS bridge, admin protocol, hooks, schema generation",
6
6
  "main": "./src/index.ts",
@@ -1,6 +1,13 @@
1
1
  #!/usr/bin/env tsx
2
2
  /**
3
- * Reads .deco/blocks/*.json and emits a TypeScript barrel.
3
+ * Reads .deco/blocks/*.json and emits:
4
+ * 1. blocks.gen.json — compact JSON data (the source of truth)
5
+ * 2. blocks.gen.ts — thin TypeScript re-export for editor tooling
6
+ *
7
+ * At runtime the Vite plugin (src/vite/plugin.js) intercepts `blocks.gen.ts`
8
+ * imports and replaces them with `JSON.parse(...)` of the .json file. This
9
+ * avoids Vite's SSR module runner hanging on large (13MB+) JS object literals
10
+ * and lets V8 use its fast JSON parser instead of the full JS parser.
4
11
  *
5
12
  * Usage (from site root):
6
13
  * npx tsx node_modules/@decocms/start/scripts/generate-blocks.ts
@@ -20,6 +27,7 @@ function arg(name: string, fallback: string): string {
20
27
 
21
28
  const blocksDir = path.resolve(process.cwd(), arg("blocks-dir", ".deco/blocks"));
22
29
  const outFile = path.resolve(process.cwd(), arg("out-file", "src/server/cms/blocks.gen.ts"));
30
+ const jsonFile = outFile.replace(/\.ts$/, ".json");
23
31
 
24
32
  function decodeBlockName(filename: string): string {
25
33
  let name = filename.replace(/\.json$/, "");
@@ -35,13 +43,20 @@ function decodeBlockName(filename: string): string {
35
43
  return name;
36
44
  }
37
45
 
46
+ const TS_STUB = [
47
+ "// Auto-generated — thin wrapper around blocks.gen.json.",
48
+ "// The Vite plugin replaces this at load time with JSON.parse(...).",
49
+ "// Do not edit manually.",
50
+ "",
51
+ "export const blocks: Record<string, any> = {};",
52
+ "",
53
+ ].join("\n");
54
+
38
55
  if (!fs.existsSync(blocksDir)) {
39
56
  console.warn(`Blocks directory not found: ${blocksDir} — generating empty barrel.`);
40
57
  fs.mkdirSync(path.dirname(outFile), { recursive: true });
41
- fs.writeFileSync(
42
- outFile,
43
- `// Auto-generated — no blocks found\nexport const blocks: Record<string, any> = {};\n`,
44
- );
58
+ fs.writeFileSync(jsonFile, "{}");
59
+ fs.writeFileSync(outFile, TS_STUB);
45
60
  process.exit(0);
46
61
  }
47
62
 
@@ -70,10 +85,16 @@ for (const [name, file] of Object.entries(blockFiles)) {
70
85
  }
71
86
  }
72
87
 
73
- const output = `// Auto-generated from .deco/blocks/*.json\n// Do not edit manually.\n\nexport const blocks: Record<string, any> = ${JSON.stringify(blocks, null, 2)};\n`;
74
-
75
88
  fs.mkdirSync(path.dirname(outFile), { recursive: true });
76
- fs.writeFileSync(outFile, output);
89
+
90
+ // 1. Compact JSON — the real data (no pretty-printing to save ~40% size)
91
+ const jsonStr = JSON.stringify(blocks);
92
+ fs.writeFileSync(jsonFile, jsonStr);
93
+
94
+ // 2. Thin TS wrapper — just for TypeScript tooling and as a Vite load target
95
+ fs.writeFileSync(outFile, TS_STUB);
96
+
97
+ const jsonSizeMB = (Buffer.byteLength(jsonStr) / 1_048_576).toFixed(1);
77
98
  console.log(
78
- `Generated ${Object.keys(blocks).length} blocks → ${path.relative(process.cwd(), outFile)}`,
99
+ `Generated ${Object.keys(blocks).length} blocks → ${path.relative(process.cwd(), jsonFile)} (${jsonSizeMB} MB)`,
79
100
  );
@@ -61,7 +61,12 @@ export const ANALYTICS_SCRIPT = `
61
61
  }
62
62
 
63
63
  observeAll();
64
- new MutationObserver(observeAll).observe(document.body, { childList: true, subtree: true });
64
+ var mo = new MutationObserver(observeAll);
65
+ if (typeof requestIdleCallback !== 'undefined') {
66
+ requestIdleCallback(function() { mo.observe(document.body, { childList: true, subtree: true }); });
67
+ } else {
68
+ setTimeout(function() { mo.observe(document.body, { childList: true, subtree: true }); }, 0);
69
+ }
65
70
  })();
66
71
  `;
67
72
 
@@ -34,6 +34,23 @@ import {
34
34
  import { buildHtmlShell } from "./htmlShell";
35
35
  import { cleanPathForCacheKey } from "./urlUtils";
36
36
  import { isMobileUA } from "./useDevice";
37
+ import { getRenderShellConfig } from "../admin/setup";
38
+
39
+ /**
40
+ * Append Link preload headers for CSS and fonts so the browser starts
41
+ * fetching them before parsing HTML. Only applied to HTML responses.
42
+ */
43
+ function appendResourceHints(resp: Response): void {
44
+ const ct = resp.headers.get("content-type");
45
+ if (!ct || !ct.includes("text/html")) return;
46
+ const { cssHref, fontHrefs } = getRenderShellConfig();
47
+ if (cssHref) {
48
+ resp.headers.append("Link", `<${cssHref}>; rel=preload; as=style`);
49
+ }
50
+ for (const href of fontHrefs) {
51
+ resp.headers.append("Link", `<${href}>; rel=preload; as=font; crossorigin`);
52
+ }
53
+ }
37
54
 
38
55
  // ---------------------------------------------------------------------------
39
56
  // Types
@@ -736,6 +753,7 @@ export function createDecoWorkerEntry(
736
753
  // CDN auto-caching would bypass that versioning and serve stale HTML
737
754
  // after deploys (referencing old CSS/JS fingerprinted filenames).
738
755
  hit.headers.set("CDN-Cache-Control", "no-store");
756
+ appendResourceHints(hit);
739
757
  return hit;
740
758
  }
741
759
  } catch {
@@ -750,6 +768,7 @@ export function createDecoWorkerEntry(
750
768
  const resp = new Response(origin.body, origin);
751
769
  resp.headers.set("X-Cache", "BYPASS");
752
770
  resp.headers.set("X-Cache-Reason", `status:${origin.status}`);
771
+ appendResourceHints(resp);
753
772
  return resp;
754
773
  }
755
774
 
@@ -764,6 +783,7 @@ export function createDecoWorkerEntry(
764
783
  resp.headers.delete("CDN-Cache-Control");
765
784
  resp.headers.set("X-Cache", "BYPASS");
766
785
  resp.headers.set("X-Cache-Reason", "set-cookie");
786
+ appendResourceHints(resp);
767
787
  return resp;
768
788
  }
769
789
 
@@ -776,6 +796,7 @@ export function createDecoWorkerEntry(
776
796
  resp.headers.set("Cache-Control", "private, no-cache, no-store, must-revalidate");
777
797
  resp.headers.set("X-Cache", "BYPASS");
778
798
  resp.headers.set("X-Cache-Reason", `profile:${profile}`);
799
+ appendResourceHints(resp);
779
800
  return resp;
780
801
  }
781
802
 
@@ -804,6 +825,7 @@ export function createDecoWorkerEntry(
804
825
  // BUILD_HASH-versioned keys; CDN auto-caching would bypass versioning
805
826
  // and serve stale HTML after deploys.
806
827
  toReturn.headers.set("CDN-Cache-Control", "no-store");
828
+ appendResourceHints(toReturn);
807
829
 
808
830
  // For Cache API storage, use sMaxAge as max-age since the Cache API
809
831
  // ignores s-maxage and only respects max-age for TTL decisions.
@@ -5,12 +5,21 @@
5
5
  * are eliminated from the browser bundle. This consolidates stubs that
6
6
  * every Deco site previously had to copy into its own vite.config.ts.
7
7
  *
8
+ * blocks.gen.ts handling:
9
+ * The CMS block registry can be 10MB+. Inlining it as a JS object literal
10
+ * causes Vite's SSR module runner to hang on dynamic imports (transport
11
+ * serialization bottleneck) and is slow to parse even with static imports
12
+ * (V8 full JS parser). Instead, generate-blocks.ts writes a .json data
13
+ * file, and this plugin intercepts the .ts import to return JSON.parse(...)
14
+ * — V8's JSON parser is 2-10x faster than the JS parser for large data.
15
+ *
8
16
  * Usage:
9
17
  * ```ts
10
18
  * import { decoVitePlugin } from "@decocms/start/vite";
11
19
  * export default defineConfig({ plugins: [decoVitePlugin(), ...] });
12
20
  * ```
13
21
  */
22
+ import { readFileSync, existsSync } from "node:fs";
14
23
 
15
24
  // Bare-specifier stubs resolved by ID before Vite touches them.
16
25
  /** @type {Record<string, string>} */
@@ -69,16 +78,81 @@ export function decoVitePlugin() {
69
78
  },
70
79
 
71
80
  load(id, options) {
72
- // blocks.gen.ts — the CMS block registry (often 500KB+ compiled).
73
- // Only the server needs it; the client receives pre-resolved sections.
74
- if (!options?.ssr && id.endsWith("blocks.gen.ts")) {
75
- return "export const blocks = {};";
81
+ // blocks.gen.ts — the CMS block registry (can be 10MB+).
82
+ if (id.endsWith("blocks.gen.ts")) {
83
+ // Client: stub — the browser receives pre-resolved sections.
84
+ if (!options?.ssr) {
85
+ return "export const blocks = {};";
86
+ }
87
+
88
+ // SSR: read .json sibling and emit JSON.parse(...) wrapper.
89
+ // This avoids the Vite SSR module runner hanging on large dynamic
90
+ // imports and lets V8 use its fast JSON parser (~2-10x vs object literal).
91
+ const jsonPath = id.replace(/\.ts$/, ".json");
92
+ if (existsSync(jsonPath)) {
93
+ const raw = readFileSync(jsonPath, "utf-8");
94
+ return `export const blocks = JSON.parse(${JSON.stringify(raw)});`;
95
+ }
96
+
97
+ // Fallback: if .json doesn't exist yet (pre-generate-blocks), let
98
+ // Vite load the .ts file normally (may contain inline data for
99
+ // backward-compatible sites that haven't regenerated).
76
100
  }
77
101
 
78
102
  // Virtual module stubs.
79
103
  return STUB_SOURCE[id];
80
104
  },
81
105
 
106
+ configureServer(server) {
107
+ // When blocks.gen.json changes on disk, invalidate the .ts module
108
+ // so Vite re-runs our load() hook with the fresh data.
109
+ server.watcher.on("change", (file) => {
110
+ if (file.endsWith("blocks.gen.json")) {
111
+ const tsId = file.replace(/\.json$/, ".ts");
112
+ const mod = server.environments?.ssr?.moduleGraph?.getModuleById(tsId);
113
+ if (mod) {
114
+ server.environments.ssr.moduleGraph.invalidateModule(mod);
115
+ }
116
+ }
117
+ });
118
+ },
119
+
120
+ config(_cfg, { command }) {
121
+ // Only split chunks for production builds — dev uses unbundled ESM.
122
+ if (command !== "build") return;
123
+ return {
124
+ build: {
125
+ rollupOptions: {
126
+ output: {
127
+ manualChunks(id) {
128
+ if (
129
+ id.includes("node_modules/react-dom") ||
130
+ id.includes("node_modules/react/")
131
+ ) {
132
+ return "vendor-react";
133
+ }
134
+ if (
135
+ id.includes("@tanstack/react-router") ||
136
+ id.includes("@tanstack/start")
137
+ ) {
138
+ return "vendor-router";
139
+ }
140
+ if (id.includes("@tanstack/react-query")) {
141
+ return "vendor-query";
142
+ }
143
+ if (id.includes("@decocms/start")) {
144
+ return "vendor-deco";
145
+ }
146
+ if (id.includes("@decocms/apps")) {
147
+ return "vendor-commerce";
148
+ }
149
+ },
150
+ },
151
+ },
152
+ },
153
+ };
154
+ },
155
+
82
156
  configEnvironment(name, env) {
83
157
  if (name === "ssr" || name === "client") {
84
158
  env.optimizeDeps = env.optimizeDeps || {};