@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 +1 -1
- package/scripts/generate-blocks.ts +30 -9
- package/src/sdk/analytics.ts +6 -1
- package/src/sdk/workerEntry.ts +22 -0
- package/src/vite/plugin.js +78 -4
package/package.json
CHANGED
|
@@ -1,6 +1,13 @@
|
|
|
1
1
|
#!/usr/bin/env tsx
|
|
2
2
|
/**
|
|
3
|
-
* Reads .deco/blocks/*.json and emits
|
|
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
|
-
|
|
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
|
-
|
|
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(),
|
|
99
|
+
`Generated ${Object.keys(blocks).length} blocks → ${path.relative(process.cwd(), jsonFile)} (${jsonSizeMB} MB)`,
|
|
79
100
|
);
|
package/src/sdk/analytics.ts
CHANGED
|
@@ -61,7 +61,12 @@ export const ANALYTICS_SCRIPT = `
|
|
|
61
61
|
}
|
|
62
62
|
|
|
63
63
|
observeAll();
|
|
64
|
-
new MutationObserver(observeAll)
|
|
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
|
|
package/src/sdk/workerEntry.ts
CHANGED
|
@@ -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.
|
package/src/vite/plugin.js
CHANGED
|
@@ -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 (
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
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 || {};
|