@koehler8/cms 1.0.0-beta.35 → 1.0.0-beta.37
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/bin/cms-ssg-build.js +13 -0
- package/package.json +4 -3
- package/scripts/ssg-build/index.mjs +197 -0
- package/src/main.js +15 -0
- package/src/utils/loadConfig.js +18 -2
- package/src/utils/ssgRoutes.js +40 -0
- package/vite-plugin.js +19 -2
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Thin wrapper that runs the memory-bounded SSG build orchestrator from the
|
|
3
|
+
// consuming site's cwd. The script does its own argument handling, spawns the
|
|
4
|
+
// per-shard `vite-ssg build` processes, and prints its own status.
|
|
5
|
+
// See scripts/ssg-build/index.mjs and SSG-MEMORY-PLAN.md.
|
|
6
|
+
|
|
7
|
+
import path from 'node:path';
|
|
8
|
+
import { fileURLToPath } from 'node:url';
|
|
9
|
+
|
|
10
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
11
|
+
const __dirname = path.dirname(__filename);
|
|
12
|
+
|
|
13
|
+
await import(path.resolve(__dirname, '..', 'scripts', 'ssg-build', 'index.mjs'));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@koehler8/cms",
|
|
3
|
-
"version": "1.0.0-beta.
|
|
3
|
+
"version": "1.0.0-beta.37",
|
|
4
4
|
"description": "Vertex CMS — lightweight, config-driven Vue 3 framework with theming, extensions, and SSG support",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -54,6 +54,7 @@
|
|
|
54
54
|
},
|
|
55
55
|
"bin": {
|
|
56
56
|
"cms-generate-public-assets": "./bin/cms-generate-public-assets.js",
|
|
57
|
+
"cms-ssg-build": "./bin/cms-ssg-build.js",
|
|
57
58
|
"cms-migrate-image-variants": "./bin/cms-migrate-image-variants.js",
|
|
58
59
|
"cms-validate-themes": "./bin/cms-validate-themes.js",
|
|
59
60
|
"cms-validate-extensions": "./bin/cms-validate-extensions.js",
|
|
@@ -87,13 +88,13 @@
|
|
|
87
88
|
"dotenv": "^17.2.3",
|
|
88
89
|
"pinia": "^3.0.1",
|
|
89
90
|
"png-to-ico": "^3.0.1",
|
|
90
|
-
"sharp": "^0.
|
|
91
|
+
"sharp": "^0.35.3"
|
|
91
92
|
},
|
|
92
93
|
"devDependencies": {
|
|
93
94
|
"@vue/test-utils": "^2.4.10",
|
|
94
95
|
"happy-dom": "^20.9.0",
|
|
95
96
|
"vitest": "^4.1.6",
|
|
96
|
-
"vue": "^3.5.
|
|
97
|
+
"vue": "^3.5.39",
|
|
97
98
|
"vue-router": "^5.0.7"
|
|
98
99
|
},
|
|
99
100
|
"engines": {
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* cms-ssg-build — memory-bounded SSG pre-render (the durable fix for the
|
|
3
|
+
* per-route heap accumulation in vite-ssg's single-process render; see
|
|
4
|
+
* SSG-MEMORY-PLAN.md).
|
|
5
|
+
*
|
|
6
|
+
* vite-ssg renders every route in one Node process and leaves a fixed retained
|
|
7
|
+
* cost per route, so peak heap scales with total routes (pages × locales) and
|
|
8
|
+
* large sites OOM. This orchestrator runs the render as N separate
|
|
9
|
+
* `vite-ssg build` processes, each handling a slice of the routes (via the
|
|
10
|
+
* framework's CMS_SSG_SHARD hook) into its own outDir, then merges the shards
|
|
11
|
+
* into dist/. Because each shard is its own process, memory is reclaimed
|
|
12
|
+
* between shards — peak heap is bounded by SLICE size, not total routes,
|
|
13
|
+
* independent of how large the site grows.
|
|
14
|
+
*
|
|
15
|
+
* Shards run sequentially by default: that gives the LOWEST peak memory (one
|
|
16
|
+
* shard live at a time), which is the whole point. Parallelism would trade that
|
|
17
|
+
* back for build speed and also races on the plugin's shared writes to the site
|
|
18
|
+
* root, so it stays opt-in/experimental.
|
|
19
|
+
*
|
|
20
|
+
* Small sites (≤ one shard) fall through to a single ordinary `vite-ssg build`,
|
|
21
|
+
* byte-identical to today.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import { spawn } from 'node:child_process';
|
|
25
|
+
import fs from 'node:fs';
|
|
26
|
+
import fsp from 'node:fs/promises';
|
|
27
|
+
import path from 'node:path';
|
|
28
|
+
import os from 'node:os';
|
|
29
|
+
|
|
30
|
+
const CWD = process.cwd();
|
|
31
|
+
const TAG = '[cms-ssg-build]';
|
|
32
|
+
|
|
33
|
+
function parseArgs(argv) {
|
|
34
|
+
const opts = {
|
|
35
|
+
shardSize: 150, // routes per shard
|
|
36
|
+
concurrency: 1, // sequential by default (lowest peak memory + no shared-write race)
|
|
37
|
+
heap: 2048, // per-shard --max-old-space-size (MB); a slice fits comfortably
|
|
38
|
+
siteDir: './site',
|
|
39
|
+
passthrough: [], // forwarded verbatim to `vite-ssg build`
|
|
40
|
+
};
|
|
41
|
+
for (let i = 0; i < argv.length; i++) {
|
|
42
|
+
const a = argv[i];
|
|
43
|
+
if (a === '--shard-size') opts.shardSize = Number(argv[++i]) || opts.shardSize;
|
|
44
|
+
else if (a === '--concurrency') opts.concurrency = Math.max(1, Number(argv[++i]) || 1);
|
|
45
|
+
else if (a === '--heap') opts.heap = Number(argv[++i]) || opts.heap;
|
|
46
|
+
else if (a === '--site-dir') opts.siteDir = argv[++i] || opts.siteDir;
|
|
47
|
+
else opts.passthrough.push(a);
|
|
48
|
+
}
|
|
49
|
+
return opts;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Estimate the route count the same way the Vite plugin discovers routes
|
|
53
|
+
// (base-locale pages × content locales, + /404 + /admin). Only used to choose
|
|
54
|
+
// the shard count N; the framework's CMS_SSG_SHARD filter partitions whatever
|
|
55
|
+
// routes actually exist, so an approximate N still yields a correct, complete
|
|
56
|
+
// dist/ — it just tunes slice sizes.
|
|
57
|
+
function estimateRouteCount(siteDir) {
|
|
58
|
+
const contentDir = path.join(siteDir, 'content');
|
|
59
|
+
let baseLocale = 'en';
|
|
60
|
+
try {
|
|
61
|
+
baseLocale = JSON.parse(fs.readFileSync(path.join(contentDir, 'content.config.json'), 'utf8')).baseLocale || 'en';
|
|
62
|
+
} catch { /* default en */ }
|
|
63
|
+
|
|
64
|
+
let pages = 1;
|
|
65
|
+
try {
|
|
66
|
+
pages = fs.readdirSync(path.join(contentDir, baseLocale, 'pages')).filter((f) => f.endsWith('.json')).length || 1;
|
|
67
|
+
} catch { /* single page */ }
|
|
68
|
+
|
|
69
|
+
let locales = 1;
|
|
70
|
+
try {
|
|
71
|
+
locales = fs.readdirSync(contentDir, { withFileTypes: true })
|
|
72
|
+
.filter((d) => d.isDirectory())
|
|
73
|
+
.filter((d) => {
|
|
74
|
+
const dir = path.join(contentDir, d.name);
|
|
75
|
+
return fs.existsSync(path.join(dir, 'site.json'))
|
|
76
|
+
|| fs.existsSync(path.join(dir, 'shared.json'))
|
|
77
|
+
|| fs.existsSync(path.join(dir, 'pages'));
|
|
78
|
+
}).length || 1;
|
|
79
|
+
} catch { /* single locale */ }
|
|
80
|
+
|
|
81
|
+
return pages * locales + 2; // + /404 + /admin
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function resolveViteSsgBin() {
|
|
85
|
+
const local = path.join(CWD, 'node_modules', '.bin', 'vite-ssg');
|
|
86
|
+
return fs.existsSync(local) ? local : 'vite-ssg';
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function runViteSsg({ shard, outDir, heap, passthrough }) {
|
|
90
|
+
return new Promise((resolve) => {
|
|
91
|
+
const env = { ...process.env };
|
|
92
|
+
if (shard) env.CMS_SSG_SHARD = shard;
|
|
93
|
+
if (outDir) env.CMS_SSG_OUTDIR = outDir;
|
|
94
|
+
// A shard renders a bounded slice, so a modest heap suffices — this is what
|
|
95
|
+
// lets sites drop the 8 GB stop-loss flag. Strip any inherited
|
|
96
|
+
// --max-old-space-size and set our own.
|
|
97
|
+
const base = (process.env.NODE_OPTIONS || '').replace(/--max-old-space-size=\d+/g, '').trim();
|
|
98
|
+
env.NODE_OPTIONS = `${base} --max-old-space-size=${heap}`.trim();
|
|
99
|
+
|
|
100
|
+
const child = spawn(resolveViteSsgBin(), ['build', ...passthrough], { cwd: CWD, env });
|
|
101
|
+
let out = '';
|
|
102
|
+
child.stdout.on('data', (d) => { out += d; });
|
|
103
|
+
child.stderr.on('data', (d) => { out += d; });
|
|
104
|
+
child.on('close', (code) => resolve({ code, out }));
|
|
105
|
+
child.on('error', (err) => resolve({ code: 1, out: String(err && err.stack ? err.stack : err) }));
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
async function pool(items, concurrency, worker) {
|
|
110
|
+
const results = new Array(items.length);
|
|
111
|
+
let next = 0;
|
|
112
|
+
const runners = Array.from({ length: Math.min(concurrency, items.length) }, async () => {
|
|
113
|
+
while (next < items.length) {
|
|
114
|
+
const i = next++;
|
|
115
|
+
results[i] = await worker(items[i], i);
|
|
116
|
+
}
|
|
117
|
+
});
|
|
118
|
+
await Promise.all(runners);
|
|
119
|
+
return results;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Union-merge every shard's output into dist/. Shared files (assets/, sitemap,
|
|
123
|
+
// robots, manifest, favicon, 404.html) are byte-identical across shards, and
|
|
124
|
+
// per-route HTML files are disjoint — so a plain overwrite-merge is a clean
|
|
125
|
+
// union. The ONE exception is dist/index.html: only the shard that rendered `/`
|
|
126
|
+
// produced the real home page; every other shard's client build left a
|
|
127
|
+
// hydration shell there. So after merging, restore the rendered home
|
|
128
|
+
// explicitly (identified by the data-server-rendered marker).
|
|
129
|
+
async function mergeShards(shardDirs, finalDist) {
|
|
130
|
+
await fsp.rm(finalDist, { recursive: true, force: true });
|
|
131
|
+
await fsp.mkdir(finalDist, { recursive: true });
|
|
132
|
+
for (const dir of shardDirs) {
|
|
133
|
+
await fsp.cp(dir, finalDist, { recursive: true, force: true });
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
let renderedHome = null;
|
|
137
|
+
for (const dir of shardDirs) {
|
|
138
|
+
try {
|
|
139
|
+
const html = await fsp.readFile(path.join(dir, 'index.html'), 'utf8');
|
|
140
|
+
if (html.includes('data-server-rendered="true"')) { renderedHome = html; break; }
|
|
141
|
+
} catch { /* shard has no index.html */ }
|
|
142
|
+
}
|
|
143
|
+
if (renderedHome != null) {
|
|
144
|
+
await fsp.writeFile(path.join(finalDist, 'index.html'), renderedHome, 'utf8');
|
|
145
|
+
} else {
|
|
146
|
+
console.warn(`${TAG} warning: no shard produced a server-rendered home page; dist/index.html left as the client shell.`);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
async function main() {
|
|
151
|
+
const opts = parseArgs(process.argv.slice(2));
|
|
152
|
+
const siteDir = path.resolve(CWD, opts.siteDir);
|
|
153
|
+
const estRoutes = estimateRouteCount(siteDir);
|
|
154
|
+
const N = Math.max(1, Math.ceil(estRoutes / opts.shardSize));
|
|
155
|
+
|
|
156
|
+
if (N <= 1) {
|
|
157
|
+
console.log(`${TAG} ~${estRoutes} routes ≤ shard size ${opts.shardSize}: running a single vite-ssg build (no sharding needed).`);
|
|
158
|
+
const r = await runViteSsg({ heap: opts.heap, passthrough: opts.passthrough });
|
|
159
|
+
process.stdout.write(r.out);
|
|
160
|
+
process.exit(r.code);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
console.log(`${TAG} ~${estRoutes} routes → ${N} shards of ≤${opts.shardSize}, ${opts.heap} MB heap each, ${opts.concurrency === 1 ? 'sequential' : `${opts.concurrency}-way parallel`}. Peak heap is bounded per shard.`);
|
|
164
|
+
if (opts.concurrency > 1) {
|
|
165
|
+
console.warn(`${TAG} warning: --concurrency > 1 is experimental — parallel shards race on the plugin's shared site-root writes and raise peak memory. Sequential is recommended.`);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const tmp = path.join(CWD, '.cms-ssg');
|
|
169
|
+
await fsp.rm(tmp, { recursive: true, force: true });
|
|
170
|
+
await fsp.mkdir(tmp, { recursive: true });
|
|
171
|
+
|
|
172
|
+
const shards = Array.from({ length: N }, (_, k) => ({ k, outDir: path.join('.cms-ssg', `dist-${k}`) }));
|
|
173
|
+
const t0 = Date.now();
|
|
174
|
+
const results = await pool(shards, opts.concurrency, async (s) => {
|
|
175
|
+
const started = Date.now();
|
|
176
|
+
process.stdout.write(`${TAG} shard ${s.k + 1}/${N} rendering…\n`);
|
|
177
|
+
const r = await runViteSsg({ shard: `${s.k}/${N}`, outDir: s.outDir, heap: opts.heap, passthrough: opts.passthrough });
|
|
178
|
+
process.stdout.write(`${TAG} shard ${s.k + 1}/${N} ${r.code === 0 ? 'ok' : 'FAILED'} (${((Date.now() - started) / 1000).toFixed(1)}s)\n`);
|
|
179
|
+
return { ...r, k: s.k };
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
const failed = results.filter((r) => r.code !== 0);
|
|
183
|
+
if (failed.length) {
|
|
184
|
+
console.error(`${TAG} ${failed.length}/${N} shard(s) failed:`);
|
|
185
|
+
for (const f of failed) {
|
|
186
|
+
console.error(`${TAG} ---- shard ${f.k} output (tail) ----`);
|
|
187
|
+
console.error(f.out.slice(-4000));
|
|
188
|
+
}
|
|
189
|
+
process.exit(1);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
await mergeShards(shards.map((s) => path.join(CWD, s.outDir)), path.join(CWD, 'dist'));
|
|
193
|
+
await fsp.rm(tmp, { recursive: true, force: true });
|
|
194
|
+
console.log(`${TAG} merged ${N} shards → dist/ in ${((Date.now() - t0) / 1000).toFixed(1)}s. Done.`);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
main().catch((err) => { console.error(TAG, err); process.exit(1); });
|
package/src/main.js
CHANGED
|
@@ -135,6 +135,21 @@ export function createCmsApp() {
|
|
|
135
135
|
};
|
|
136
136
|
|
|
137
137
|
const extractCurrentRoute = () => {
|
|
138
|
+
// During SSG, vite-ssg passes the route being pre-rendered as a string
|
|
139
|
+
// on `ctx.routePath` and only pushes the router AFTER this setup fn runs
|
|
140
|
+
// — so `router.currentRoute` is still vue-router's START_LOCATION ('/')
|
|
141
|
+
// here, identically on every page. Resolve `routePath` through the router
|
|
142
|
+
// (a pure match, no navigation) to recover the real path + `params.locale`.
|
|
143
|
+
// Without this, `resolveCurrentPageId` sees '/' for every route, so the
|
|
144
|
+
// payload trim targets the wrong page — or, when '/' is ambiguous, none.
|
|
145
|
+
const routePath = ctx?.routePath;
|
|
146
|
+
if (typeof routePath === 'string' && routePath) {
|
|
147
|
+
try {
|
|
148
|
+
return router.resolve(routePath);
|
|
149
|
+
} catch {
|
|
150
|
+
return { path: routePath, params: {} };
|
|
151
|
+
}
|
|
152
|
+
}
|
|
138
153
|
if (ctx?.route) {
|
|
139
154
|
return ctx.route;
|
|
140
155
|
}
|
package/src/utils/loadConfig.js
CHANGED
|
@@ -70,8 +70,24 @@ export function resolvePageIdForRoute(config, routePath, localeParam) {
|
|
|
70
70
|
(typeof p === 'string' ? p : '/').replace(/\/+/g, '/').replace(/\/+$/, '') || '/';
|
|
71
71
|
target = normalize(target);
|
|
72
72
|
const pages = (config && config.pages) || {};
|
|
73
|
-
|
|
74
|
-
|
|
73
|
+
|
|
74
|
+
// The SSG pre-renders `/404` from a site's custom `pages/404.json` (mirrors
|
|
75
|
+
// usePageConfig.selectPage's `/404` → `404` mapping). That page is
|
|
76
|
+
// conventionally path-less, so it can't be matched by `path` below — map the
|
|
77
|
+
// route to it directly. It is also deliberately NOT treated as living at `/`
|
|
78
|
+
// (see the id === '404' guard in the matcher) so it never collides with
|
|
79
|
+
// `home` there — the collision that otherwise makes `/` ambiguous on any
|
|
80
|
+
// site shipping a custom 404 page, suppressing the home-page trim.
|
|
81
|
+
const notFound = pages['404'];
|
|
82
|
+
const notFoundPathless = notFound && !(typeof notFound.path === 'string' && notFound.path.trim());
|
|
83
|
+
if (target === '/404' && notFoundPathless) return '404';
|
|
84
|
+
|
|
85
|
+
const matches = Object.entries(pages).filter(([id, data]) => {
|
|
86
|
+
const hasPath = data && typeof data.path === 'string' && data.path.trim();
|
|
87
|
+
// A page with no explicit `path` conventionally lives at `/` (home). The
|
|
88
|
+
// SSG-only `404` page is the exception: it is served at `/404`, never `/`.
|
|
89
|
+
const pagePath = hasPath ? data.path : (id === '404' ? null : '/');
|
|
90
|
+
if (pagePath === null) return false;
|
|
75
91
|
return normalize(pagePath) === target;
|
|
76
92
|
});
|
|
77
93
|
return matches.length === 1 ? matches[0][0] : null;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
// Route sharding for `cms-ssg-build` — the opt-in durable memory bound for the
|
|
2
|
+
// SSG pre-render. vite-ssg renders every route in one Node process and leaves a
|
|
3
|
+
// fixed retained cost per route, so peak heap scales with total routes;
|
|
4
|
+
// `cms-ssg-build` runs the render in N separate processes, each handling a slice
|
|
5
|
+
// of the routes, so peak heap is bounded by slice size instead. This module is
|
|
6
|
+
// the pure partition function so the slicing rules are unit-testable without a
|
|
7
|
+
// build. See SSG-MEMORY-PLAN.md.
|
|
8
|
+
|
|
9
|
+
// Routes every shard renders regardless of its slice. `/404` must be present in
|
|
10
|
+
// each shard's output dir so that shard's `onFinished` can copy 404/index.html
|
|
11
|
+
// → 404.html; `/admin` is a cheap vestigial placeholder kept alongside it.
|
|
12
|
+
export const ALWAYS_SHARDED_ROUTES = ['/404', '/admin'];
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Return shard `k` of `N` from the full SSG route list.
|
|
16
|
+
*
|
|
17
|
+
* `shardSpec` is a `"k/N"` string (e.g. `"2/5"`). Shard k = the always-present
|
|
18
|
+
* routes + every Nth remaining route offset by k, so the N shards partition the
|
|
19
|
+
* non-always routes exactly once each (no route rendered twice, none dropped).
|
|
20
|
+
*
|
|
21
|
+
* Returns the full list unchanged when `shardSpec` is absent or malformed —
|
|
22
|
+
* this is the normal `vite-ssg build` path and must stay byte-identical.
|
|
23
|
+
*/
|
|
24
|
+
export function applyRouteShard(allRoutes, shardSpec) {
|
|
25
|
+
if (!Array.isArray(allRoutes)) return allRoutes;
|
|
26
|
+
const match = typeof shardSpec === 'string' ? shardSpec.trim().match(/^(\d+)\/(\d+)$/) : null;
|
|
27
|
+
if (!match) return allRoutes;
|
|
28
|
+
|
|
29
|
+
const k = Number(match[1]);
|
|
30
|
+
const n = Number(match[2]);
|
|
31
|
+
if (!(n > 0) || k < 0 || k >= n) return allRoutes;
|
|
32
|
+
|
|
33
|
+
const always = new Set(ALWAYS_SHARDED_ROUTES);
|
|
34
|
+
const alwaysRoutes = allRoutes.filter((route) => always.has(route));
|
|
35
|
+
const shardRoutes = allRoutes
|
|
36
|
+
.filter((route) => !always.has(route))
|
|
37
|
+
.filter((_, i) => i % n === k);
|
|
38
|
+
|
|
39
|
+
return Array.from(new Set([...alwaysRoutes, ...shardRoutes]));
|
|
40
|
+
}
|
package/vite-plugin.js
CHANGED
|
@@ -17,6 +17,7 @@ import { buildRobotsTxt } from './src/utils/robotsGenerator.js';
|
|
|
17
17
|
import { buildSitemap, getSitemapUrl } from './src/utils/sitemapGenerator.js';
|
|
18
18
|
import { buildWebAppManifest } from './src/utils/webAppManifest.js';
|
|
19
19
|
import { sha256Hex } from './src/utils/sha256.js';
|
|
20
|
+
import { applyRouteShard } from './src/utils/ssgRoutes.js';
|
|
20
21
|
import {
|
|
21
22
|
computeVariantCacheDir,
|
|
22
23
|
resolveImageVariantConfig,
|
|
@@ -539,6 +540,14 @@ export default function cmsPlugin(options = {}) {
|
|
|
539
540
|
publicDir: path.join(siteRoot, 'public'),
|
|
540
541
|
build: {
|
|
541
542
|
modulePreload: false,
|
|
543
|
+
// `cms-ssg-build` renders each route shard as a separate build into
|
|
544
|
+
// its own outDir (e.g. `.cms-ssg/dist-0`), then merges them into
|
|
545
|
+
// `dist/`. Honor the override for the CLIENT build only — vite-ssg's
|
|
546
|
+
// SSR build sets its own temp outDir and then imports the server
|
|
547
|
+
// bundle back from there, so overriding it would break that import.
|
|
548
|
+
...(process.env.CMS_SSG_OUTDIR && !(env?.isSsrBuild || userConfig?.build?.ssr)
|
|
549
|
+
? { outDir: process.env.CMS_SSG_OUTDIR }
|
|
550
|
+
: {}),
|
|
542
551
|
rollupOptions: {
|
|
543
552
|
input: tempIndexPath,
|
|
544
553
|
output: {
|
|
@@ -590,7 +599,7 @@ export default function cmsPlugin(options = {}) {
|
|
|
590
599
|
// unmatched URL with HTTP status 404, so this gives sites a
|
|
591
600
|
// proper not-found response without per-site config.
|
|
592
601
|
onFinished() {
|
|
593
|
-
const distDir = path.join(siteRoot, 'dist');
|
|
602
|
+
const distDir = path.join(siteRoot, process.env.CMS_SSG_OUTDIR || 'dist');
|
|
594
603
|
const nestedNotFound = path.join(distDir, '404', 'index.html');
|
|
595
604
|
const flatNotFound = path.join(distDir, '404.html');
|
|
596
605
|
if (fs.existsSync(nestedNotFound)) {
|
|
@@ -630,7 +639,15 @@ export default function cmsPlugin(options = {}) {
|
|
|
630
639
|
...Array.from(staticRoutes),
|
|
631
640
|
...Array.from(localizedRoutes),
|
|
632
641
|
];
|
|
633
|
-
|
|
642
|
+
const allRoutes = Array.from(new Set(candidates));
|
|
643
|
+
|
|
644
|
+
// Opt-in durable memory bound: when `cms-ssg-build` sets
|
|
645
|
+
// CMS_SSG_SHARD=k/N in the environment, render only this shard's
|
|
646
|
+
// slice of routes so peak SSG heap is bounded by slice size, not
|
|
647
|
+
// total route count. Unset (the normal `vite-ssg build` path) →
|
|
648
|
+
// the full list, byte-identical to before. (applyRouteShard keeps
|
|
649
|
+
// /404 + /admin in every shard so each can emit its own 404.html.)
|
|
650
|
+
return applyRouteShard(allRoutes, process.env.CMS_SSG_SHARD);
|
|
634
651
|
},
|
|
635
652
|
},
|
|
636
653
|
};
|