@uniweb/build 0.14.24 → 0.14.26
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 +6 -6
- package/src/prerender.js +69 -7
- package/src/schema.js +6 -2
- package/src/site/plugin.js +42 -5
- package/src/vite-foundation-plugin.js +137 -10
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@uniweb/build",
|
|
3
|
-
"version": "0.14.
|
|
3
|
+
"version": "0.14.26",
|
|
4
4
|
"description": "Build tooling for the Uniweb Component Web Platform",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -59,13 +59,13 @@
|
|
|
59
59
|
"js-yaml": "^4.1.0",
|
|
60
60
|
"sharp": "^0.33.2",
|
|
61
61
|
"yaml": "^2.5.0",
|
|
62
|
-
"@uniweb/theming": "0.1.
|
|
62
|
+
"@uniweb/theming": "0.1.6",
|
|
63
63
|
"@uniweb/content-writer": "0.2.6"
|
|
64
64
|
},
|
|
65
65
|
"optionalDependencies": {
|
|
66
|
-
"@uniweb/
|
|
67
|
-
"@uniweb/
|
|
68
|
-
"@uniweb/
|
|
66
|
+
"@uniweb/content-reader": "1.1.12",
|
|
67
|
+
"@uniweb/runtime": "0.8.24",
|
|
68
|
+
"@uniweb/schemas": "0.2.4"
|
|
69
69
|
},
|
|
70
70
|
"peerDependencies": {
|
|
71
71
|
"vite": "^5.0.0 || ^6.0.0 || ^7.0.0",
|
|
@@ -74,7 +74,7 @@
|
|
|
74
74
|
"@tailwindcss/vite": "^4.0.0",
|
|
75
75
|
"@vitejs/plugin-react": "^4.0.0 || ^5.0.0",
|
|
76
76
|
"vite-plugin-svgr": "^4.0.0",
|
|
77
|
-
"@uniweb/core": "0.7.
|
|
77
|
+
"@uniweb/core": "0.7.18"
|
|
78
78
|
},
|
|
79
79
|
"peerDependenciesMeta": {
|
|
80
80
|
"vite": {
|
package/src/prerender.js
CHANGED
|
@@ -96,7 +96,7 @@ async function executeAllFetches(siteContent, siteDir, onProgress, localeInfo) {
|
|
|
96
96
|
onProgress(` Fetching site data: ${cfg.path || cfg.url}`)
|
|
97
97
|
const result = await executeFetch(cfg, opts)
|
|
98
98
|
if (result.data && !result.error) {
|
|
99
|
-
fetchedData.push({ config: cfg, data: result.data })
|
|
99
|
+
fetchedData.push({ config: cfg, data: result.data, _scope: '__site__' })
|
|
100
100
|
}
|
|
101
101
|
}
|
|
102
102
|
|
|
@@ -112,7 +112,7 @@ async function executeAllFetches(siteContent, siteDir, onProgress, localeInfo) {
|
|
|
112
112
|
onProgress(` Fetching page data for ${page.route}: ${cfg.path || cfg.url}`)
|
|
113
113
|
const result = await executeFetch(cfg, opts)
|
|
114
114
|
if (result.data && !result.error) {
|
|
115
|
-
fetchedData.push({ config: cfg, data: result.data })
|
|
115
|
+
fetchedData.push({ config: cfg, data: result.data, _scope: page.route })
|
|
116
116
|
// Store for dynamic route expansion
|
|
117
117
|
pageFetchedData.set(page.route, {
|
|
118
118
|
schema: pageFetch.schema,
|
|
@@ -212,13 +212,19 @@ export function expandDynamicPages(pages, pageFetchedData, onProgress) {
|
|
|
212
212
|
concretePage.paramName = undefined
|
|
213
213
|
concretePage.parentSchema = undefined
|
|
214
214
|
|
|
215
|
-
// Store the dynamic route context for runtime data resolution
|
|
215
|
+
// Store the dynamic route context for runtime data resolution. Only the
|
|
216
|
+
// keys the runtime actually uses — the entity cascade re-finds the record
|
|
217
|
+
// from the fetched collection by paramName/paramValue/schema. The record
|
|
218
|
+
// (`currentItem`) and the full sibling list (`allItems`) are deliberately
|
|
219
|
+
// NOT baked in: nothing reads them (the documented dynamicContext shape is
|
|
220
|
+
// { paramName, paramValue, schema }; the record is delivered via
|
|
221
|
+
// content.data and siblings via `fetch: { refine: true, detail: false }`),
|
|
222
|
+
// and embedding `allItems` duplicated the whole collection onto every
|
|
223
|
+
// prerendered page in split mode.
|
|
216
224
|
concretePage.dynamicContext = {
|
|
217
225
|
paramName,
|
|
218
226
|
paramValue,
|
|
219
227
|
schema, // Plural: 'articles'
|
|
220
|
-
currentItem: item, // The item for this specific route
|
|
221
|
-
allItems: items, // All items from parent
|
|
222
228
|
}
|
|
223
229
|
|
|
224
230
|
// Use item data for page metadata if available
|
|
@@ -323,6 +329,42 @@ async function discoverLocaleContents(distDir, defaultContent) {
|
|
|
323
329
|
return locales
|
|
324
330
|
}
|
|
325
331
|
|
|
332
|
+
/**
|
|
333
|
+
* Strip the internal `_scope` tag off a fetchedData entry, leaving the clean
|
|
334
|
+
* `{ config, data }` shape the runtime's hydrateDataStore expects.
|
|
335
|
+
*/
|
|
336
|
+
function stripFetchScope(entry) {
|
|
337
|
+
const { _scope, ...clean } = entry
|
|
338
|
+
return clean
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
/**
|
|
342
|
+
* Scope `fetchedData` for a single page's inline __SITE_CONTENT__.
|
|
343
|
+
*
|
|
344
|
+
* Every fetchedData entry is tagged (in executeAllFetches) with `_scope`:
|
|
345
|
+
* '__site__' for a site-level fetch, or the owning page's route for a
|
|
346
|
+
* page-level fetch. A page's first render only ever reads the cascade
|
|
347
|
+
* block → page → page.parent → site (see @uniweb/core EntityStore), so in
|
|
348
|
+
* split-content mode we embed just the entries that cascade can reach —
|
|
349
|
+
* site-level plus the page's own route and its parent's route (`scopeRoutes`).
|
|
350
|
+
* Other pages' data, and dynamic detail data reached only by client-side
|
|
351
|
+
* navigation, is fetched on demand and must not ride in every page's payload.
|
|
352
|
+
*
|
|
353
|
+
* `scopeRoutes == null` means "don't scope" (non-split mode, or the SPA
|
|
354
|
+
* fallback): keep every entry. Either way the internal `_scope` tag is stripped.
|
|
355
|
+
*
|
|
356
|
+
* @param {Array<{config: Object, data: any, _scope?: string}>} fetchedData
|
|
357
|
+
* @param {Set<string>|null} scopeRoutes - Routes whose entries to keep, or null.
|
|
358
|
+
* @returns {Array<{config: Object, data: any}>}
|
|
359
|
+
*/
|
|
360
|
+
export function scopeFetchedData(fetchedData, scopeRoutes) {
|
|
361
|
+
if (!Array.isArray(fetchedData)) return fetchedData
|
|
362
|
+
if (!scopeRoutes) return fetchedData.map(stripFetchScope)
|
|
363
|
+
return fetchedData
|
|
364
|
+
.filter((e) => e._scope === '__site__' || scopeRoutes.has(e._scope))
|
|
365
|
+
.map(stripFetchScope)
|
|
366
|
+
}
|
|
367
|
+
|
|
326
368
|
/**
|
|
327
369
|
* Inject build-specific data into HTML (theme CSS, __SITE_CONTENT__, icon cache).
|
|
328
370
|
* Called after the shared injectPageContent for build-specific additions.
|
|
@@ -334,7 +376,7 @@ async function discoverLocaleContents(distDir, defaultContent) {
|
|
|
334
376
|
* @param {string|null} [options.currentRoute=null] - Route of the page this HTML is for
|
|
335
377
|
* @returns {string} HTML with build-specific data injected
|
|
336
378
|
*/
|
|
337
|
-
function injectBuildData(html, siteContent, { splitContent = false, currentRoute = null } = {}) {
|
|
379
|
+
function injectBuildData(html, siteContent, { splitContent = false, currentRoute = null, scopeRoutes = null } = {}) {
|
|
338
380
|
let result = html
|
|
339
381
|
|
|
340
382
|
// Inject theme CSS if not already present
|
|
@@ -367,6 +409,16 @@ function injectBuildData(html, siteContent, { splitContent = false, currentRoute
|
|
|
367
409
|
}
|
|
368
410
|
}
|
|
369
411
|
|
|
412
|
+
// Scope fetched (collection / API) data to this page's cascade in split mode,
|
|
413
|
+
// so a page never carries other pages' collections. Non-split keeps it all
|
|
414
|
+
// (single-file inline). Either way the internal `_scope` tag is stripped.
|
|
415
|
+
if (Array.isArray(contentForJson.fetchedData)) {
|
|
416
|
+
contentForJson = {
|
|
417
|
+
...contentForJson,
|
|
418
|
+
fetchedData: scopeFetchedData(contentForJson.fetchedData, splitContent ? scopeRoutes : null),
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
|
|
370
422
|
const contentScript = `<script id="__SITE_CONTENT__" type="application/json">${JSON.stringify(contentForJson).replace(/</g, '\\u003c')}</script>`
|
|
371
423
|
if (result.includes('__SITE_CONTENT__')) {
|
|
372
424
|
// Replace existing site content with updated version (includes expanded dynamic routes)
|
|
@@ -667,10 +719,14 @@ export async function prerenderSite(siteDir, options = {}) {
|
|
|
667
719
|
sectionOverrideCSS: result.sectionOverrideCSS,
|
|
668
720
|
})
|
|
669
721
|
|
|
670
|
-
// Build-specific: theme CSS, __SITE_CONTENT__, icon cache
|
|
722
|
+
// Build-specific: theme CSS, __SITE_CONTENT__, icon cache.
|
|
723
|
+
// scopeRoutes mirrors the runtime data cascade (page → page.parent → site)
|
|
724
|
+
// so split-mode pages embed only the collection data their first render reads.
|
|
725
|
+
const scopeRoutes = new Set([page.route, page.parent?.route].filter(Boolean))
|
|
671
726
|
html = injectBuildData(html, siteContent, {
|
|
672
727
|
splitContent,
|
|
673
728
|
currentRoute: page.route,
|
|
729
|
+
scopeRoutes,
|
|
674
730
|
})
|
|
675
731
|
|
|
676
732
|
// Output to the locale-prefixed route
|
|
@@ -686,6 +742,7 @@ export async function prerenderSite(siteDir, options = {}) {
|
|
|
686
742
|
const fallbackBaseHtml = injectBuildData(htmlShell, siteContent, {
|
|
687
743
|
splitContent,
|
|
688
744
|
currentRoute: null, // 404 has no current page — manifest only
|
|
745
|
+
scopeRoutes: new Set(), // SPA fallback carries site-level fetched data only
|
|
689
746
|
})
|
|
690
747
|
const { html: notFoundHtml, hasNotFoundPage } = generate404Html({
|
|
691
748
|
baseHtml: fallbackBaseHtml,
|
|
@@ -710,6 +767,11 @@ export async function prerenderSite(siteDir, options = {}) {
|
|
|
710
767
|
return metadata
|
|
711
768
|
})
|
|
712
769
|
}
|
|
770
|
+
// The manifest is a single (non-per-page) file, so it keeps all fetched
|
|
771
|
+
// data — but the internal `_scope` tag must never leak into it.
|
|
772
|
+
if (Array.isArray(manifest.fetchedData)) {
|
|
773
|
+
manifest.fetchedData = manifest.fetchedData.map(stripFetchScope)
|
|
774
|
+
}
|
|
713
775
|
await writeFile(localeContentPath, JSON.stringify(manifest))
|
|
714
776
|
onProgress('Rewrote site-content.json as lightweight manifest')
|
|
715
777
|
}
|
package/src/schema.js
CHANGED
|
@@ -92,9 +92,13 @@ export async function loadPackageJson(srcDir) {
|
|
|
92
92
|
const content = await readFile(packagePath, 'utf-8')
|
|
93
93
|
const pkg = JSON.parse(content)
|
|
94
94
|
|
|
95
|
-
// Extract only identity fields for schema
|
|
95
|
+
// Extract only identity fields for schema.
|
|
96
|
+
// `uniweb.id` is the REGISTERED name (registry identity), decoupled from the
|
|
97
|
+
// workspace package `name` (pnpm linking / file: deps / site.yml). It lets a
|
|
98
|
+
// foundation keep a scaffold-default package name like "src" while registering
|
|
99
|
+
// under a distinct id (e.g. "docs" → @org/docs). Falls back to `name`.
|
|
96
100
|
return {
|
|
97
|
-
name: pkg.name,
|
|
101
|
+
name: pkg.uniweb?.id || pkg.name,
|
|
98
102
|
version: pkg.version,
|
|
99
103
|
description: pkg.description,
|
|
100
104
|
}
|
package/src/site/plugin.js
CHANGED
|
@@ -74,8 +74,45 @@ function resolveFaviconHref(configFavicon, publicDir, basePath) {
|
|
|
74
74
|
}
|
|
75
75
|
|
|
76
76
|
/**
|
|
77
|
-
*
|
|
78
|
-
*
|
|
77
|
+
* Should this fetch config be pre-executed at build time and embedded into the
|
|
78
|
+
* dev boot payload, or left for the runtime to fetch live?
|
|
79
|
+
*
|
|
80
|
+
* Dev has no prerender step, so there is no server-rendered HTML to hydrate and
|
|
81
|
+
* nothing to gain from embedding fetched data — while embedding it *costs*
|
|
82
|
+
* freshness: the boot payload is a one-time snapshot, so editing a local
|
|
83
|
+
* collection regenerates `/data/*.json` but the runtime keeps reading the stale
|
|
84
|
+
* embed until the dev server is restarted.
|
|
85
|
+
*
|
|
86
|
+
* So in dev we embed only what the browser genuinely cannot fetch itself:
|
|
87
|
+
* - Local `path:` sources (file-based collections → `/data/*.json`) are served
|
|
88
|
+
* by Vite and always browser-reachable, so we never embed them — the runtime
|
|
89
|
+
* fetches them live and picks up edits on reload. This also gives local
|
|
90
|
+
* collections true parity with a real backend fetcher.
|
|
91
|
+
* - `prerender: false` opts a source out of build-time fetching by definition.
|
|
92
|
+
* - Remote `url:` sources with `prerender !== false` may target a
|
|
93
|
+
* build-time-only endpoint (server-side auth, no CORS), so we keep
|
|
94
|
+
* pre-fetching and embedding them, mirroring what prod prerender does.
|
|
95
|
+
*
|
|
96
|
+
* Prod is unaffected: prerender still embeds fetched data for flash-free SSG
|
|
97
|
+
* hydration (see build/src/prerender.js executeAllFetches / injectBuildData).
|
|
98
|
+
*
|
|
99
|
+
* @param {Object|null} cfg - A normalized fetch config (parseFetchConfig output)
|
|
100
|
+
* @returns {boolean}
|
|
101
|
+
*/
|
|
102
|
+
export function shouldPrefetchInDev(cfg) {
|
|
103
|
+
if (!cfg) return false
|
|
104
|
+
if (!cfg.path && !cfg.url) return false // refinement / nothing to fetch
|
|
105
|
+
if (cfg.prerender === false) return false // author opted into runtime fetch
|
|
106
|
+
if (cfg.path && !cfg.url) return false // local file — runtime fetches it live
|
|
107
|
+
return true // remote build-time fetch — keep embedding in dev
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Execute the dev fetches that should be embedded (see shouldPrefetchInDev) and
|
|
112
|
+
* collect them as `fetchedData` for DataStore pre-population. Local file-based
|
|
113
|
+
* collections and `prerender: false` sources are intentionally skipped so the
|
|
114
|
+
* runtime fetches them live — keeping the dev payload free of dynamic data and
|
|
115
|
+
* ensuring edits to `/data/*.json` show up on reload without a dev restart.
|
|
79
116
|
*
|
|
80
117
|
* @param {Object} siteContent - The collected site content
|
|
81
118
|
* @param {string} siteDir - Path to site directory
|
|
@@ -86,7 +123,7 @@ async function executeDevFetches(siteContent, siteDir) {
|
|
|
86
123
|
|
|
87
124
|
// Site-level fetch
|
|
88
125
|
const siteFetch = siteContent.config?.fetch
|
|
89
|
-
if (siteFetch) {
|
|
126
|
+
if (shouldPrefetchInDev(siteFetch)) {
|
|
90
127
|
const result = await executeFetch(siteFetch, fetchOptions)
|
|
91
128
|
if (result.data && !result.error) {
|
|
92
129
|
fetchedData.push({ config: siteFetch, data: result.data })
|
|
@@ -97,7 +134,7 @@ async function executeDevFetches(siteContent, siteDir) {
|
|
|
97
134
|
for (const page of siteContent.pages || []) {
|
|
98
135
|
// Page-level fetch
|
|
99
136
|
const pageFetch = page.fetch
|
|
100
|
-
if (pageFetch) {
|
|
137
|
+
if (shouldPrefetchInDev(pageFetch)) {
|
|
101
138
|
const result = await executeFetch(pageFetch, fetchOptions)
|
|
102
139
|
if (result.data && !result.error) {
|
|
103
140
|
fetchedData.push({ config: pageFetch, data: result.data })
|
|
@@ -125,7 +162,7 @@ async function processDevSectionFetches(sections, fetchOptions) {
|
|
|
125
162
|
for (const section of sections) {
|
|
126
163
|
// Execute section-level fetch
|
|
127
164
|
const sectionFetch = section.fetch
|
|
128
|
-
if (sectionFetch) {
|
|
165
|
+
if (shouldPrefetchInDev(sectionFetch)) {
|
|
129
166
|
const result = await executeFetch(sectionFetch, fetchOptions)
|
|
130
167
|
if (result.data && !result.error) {
|
|
131
168
|
// Merge fetched data into section's parsedContent (not cascadedData)
|
|
@@ -340,6 +340,132 @@ async function emitFoundationVarsCss(outDir, schema) {
|
|
|
340
340
|
console.log(`Emitted ${Object.keys(flatVars).length} foundation theme-var default(s) to assets/style.css`)
|
|
341
341
|
}
|
|
342
342
|
|
|
343
|
+
/**
|
|
344
|
+
* Externals for the SSR bundle (`dist/entry-ssr.js`).
|
|
345
|
+
*
|
|
346
|
+
* Same set the browser foundation build externalizes (DEFAULT_EXTERNALS in
|
|
347
|
+
* foundation/config.js — react/react-dom/react-dom-server/jsx-runtime/core),
|
|
348
|
+
* which the Cloudflare edge isolate resolves to the SHARED runtime's React/core
|
|
349
|
+
* (worker-runtime.js) via its shims — so React stays deduped and runtime patches
|
|
350
|
+
* still propagate without a foundation rebuild.
|
|
351
|
+
*
|
|
352
|
+
* PLUS the client-only libraries kit code-splits via dynamic import:
|
|
353
|
+
* - shiki / shiki/bundle/full — syntax highlighting (kit Code renderer)
|
|
354
|
+
* - fuse.js — client search index
|
|
355
|
+
* Both hydrate in the browser and never run during renderToString (CLAUDE.md
|
|
356
|
+
* gotcha #12). Keeping them external drops the ~10 MB Shiki language graph from
|
|
357
|
+
* the SSR bundle and leaves them as DORMANT dynamic imports the isolate never
|
|
358
|
+
* awaits — so no extra modules-map entry is needed for them edge-side.
|
|
359
|
+
*/
|
|
360
|
+
const SSR_DEFAULT_EXTERNALS = [
|
|
361
|
+
'react',
|
|
362
|
+
'react-dom',
|
|
363
|
+
'react-dom/server',
|
|
364
|
+
'react/jsx-runtime',
|
|
365
|
+
'react/jsx-dev-runtime',
|
|
366
|
+
'@uniweb/core',
|
|
367
|
+
]
|
|
368
|
+
|
|
369
|
+
function isSSRExternal(id) {
|
|
370
|
+
if (SSR_DEFAULT_EXTERNALS.includes(id)) return true
|
|
371
|
+
if (id === 'shiki' || id.startsWith('shiki/')) return true
|
|
372
|
+
if (id === 'fuse.js' || id.startsWith('fuse.js/')) return true
|
|
373
|
+
return false
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
/**
|
|
377
|
+
* Emit `dist/entry-ssr.js` — the single-file SSR twin of the (code-split)
|
|
378
|
+
* browser `dist/entry.js`.
|
|
379
|
+
*
|
|
380
|
+
* The modern browser `entry.js` is a facade that re-exports from
|
|
381
|
+
* `_entry.generated-*.js` and lazily code-splits kit's client-only features
|
|
382
|
+
* (Shiki, Fuse) into hundreds of chunks — a graph the Cloudflare Dynamic Worker
|
|
383
|
+
* isolate can't resolve (it loads a single `foundation` module). This builds the
|
|
384
|
+
* SAME source entry into ONE file, inlining the foundation's own graph and
|
|
385
|
+
* externalizing the runtime/React set (→ the isolate's shared worker-runtime)
|
|
386
|
+
* and the client-only Shiki/Fuse libs. Result: a ~foundation-sized ESM module
|
|
387
|
+
* (no React, no Shiki) the edge loads as `foundation` for request-time SSR.
|
|
388
|
+
*
|
|
389
|
+
* Built from source (not by re-bundling the built `entry.js`, whose Shiki
|
|
390
|
+
* specifier is already rewritten to a relative chunk path that couldn't be
|
|
391
|
+
* externalized) via a secondary Vite build into a temp dir; only the JS is
|
|
392
|
+
* copied out (the throwaway CSS is discarded — the SSR bundle needs no styles).
|
|
393
|
+
*
|
|
394
|
+
* Best-effort: a failure warns and emits nothing, so the edge simply serves
|
|
395
|
+
* the client-render shell for this foundation (existence-gated) — no regression.
|
|
396
|
+
*
|
|
397
|
+
* @param {string} foundationRoot - foundation project root (vite `root`).
|
|
398
|
+
* @param {string} entrySourcePath - absolute path to `_entry.generated.js`.
|
|
399
|
+
* @param {string} outDir - dist/ directory to write `entry-ssr.js` into.
|
|
400
|
+
*/
|
|
401
|
+
async function buildEntrySSR(foundationRoot, entrySourcePath, outDir) {
|
|
402
|
+
if (_buildingSSRBundle) return
|
|
403
|
+
_buildingSSRBundle = true
|
|
404
|
+
|
|
405
|
+
const { rm, cp, stat } = await import('node:fs/promises')
|
|
406
|
+
const tmpDir = join(outDir, '.entry-ssr-tmp')
|
|
407
|
+
|
|
408
|
+
try {
|
|
409
|
+
if (!existsSync(entrySourcePath)) {
|
|
410
|
+
console.warn(`Skipping entry-ssr.js: entry source not found at ${entrySourcePath}`)
|
|
411
|
+
return
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
const { build: viteBuild } = await import('vite')
|
|
415
|
+
|
|
416
|
+
// Same transform plugins as the browser foundation build (JSX, SVGR, and —
|
|
417
|
+
// best-effort — Tailwind), but WITHOUT foundationPlugin: no schema/entry
|
|
418
|
+
// regeneration and no writeBundle recursion. CSS output is discarded.
|
|
419
|
+
const plugins = []
|
|
420
|
+
try {
|
|
421
|
+
const tailwindcss = (await import('@tailwindcss/vite')).default
|
|
422
|
+
plugins.push(tailwindcss())
|
|
423
|
+
} catch {
|
|
424
|
+
// Tailwind optional / not installed — the SSR bundle discards CSS anyway.
|
|
425
|
+
}
|
|
426
|
+
const react = (await import('@vitejs/plugin-react')).default
|
|
427
|
+
const svgr = (await import('vite-plugin-svgr')).default
|
|
428
|
+
plugins.push(react(), svgr())
|
|
429
|
+
|
|
430
|
+
await viteBuild({
|
|
431
|
+
root: foundationRoot,
|
|
432
|
+
configFile: false,
|
|
433
|
+
logLevel: 'warn',
|
|
434
|
+
plugins,
|
|
435
|
+
build: {
|
|
436
|
+
outDir: tmpDir,
|
|
437
|
+
emptyOutDir: true,
|
|
438
|
+
sourcemap: false,
|
|
439
|
+
cssCodeSplit: false,
|
|
440
|
+
lib: {
|
|
441
|
+
entry: entrySourcePath,
|
|
442
|
+
formats: ['es'],
|
|
443
|
+
fileName: () => 'entry-ssr.js',
|
|
444
|
+
},
|
|
445
|
+
rollupOptions: {
|
|
446
|
+
external: isSSRExternal,
|
|
447
|
+
output: { inlineDynamicImports: true },
|
|
448
|
+
},
|
|
449
|
+
},
|
|
450
|
+
})
|
|
451
|
+
|
|
452
|
+
const built = join(tmpDir, 'entry-ssr.js')
|
|
453
|
+
if (!existsSync(built)) {
|
|
454
|
+
console.warn('Warning: entry-ssr.js build produced no JS output — skipped.')
|
|
455
|
+
return
|
|
456
|
+
}
|
|
457
|
+
const dest = join(outDir, 'entry-ssr.js')
|
|
458
|
+
await cp(built, dest)
|
|
459
|
+
const size = ((await stat(dest)).size / 1024).toFixed(1)
|
|
460
|
+
console.log(`Generated entry-ssr.js (${size} KB)`)
|
|
461
|
+
} catch (err) {
|
|
462
|
+
console.warn(`Warning: entry-ssr.js build failed: ${err.message}`)
|
|
463
|
+
} finally {
|
|
464
|
+
await rm(tmpDir, { recursive: true, force: true }).catch(() => {})
|
|
465
|
+
_buildingSSRBundle = false
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
|
|
343
469
|
/**
|
|
344
470
|
* Vite plugin for foundation builds
|
|
345
471
|
*/
|
|
@@ -418,17 +544,18 @@ export function foundationBuildPlugin(options = {}) {
|
|
|
418
544
|
// automatically once the edge is updated.
|
|
419
545
|
await emitRuntimePin(outDir, resolvedRoot)
|
|
420
546
|
|
|
421
|
-
//
|
|
422
|
-
//
|
|
423
|
-
// runtime
|
|
424
|
-
//
|
|
425
|
-
//
|
|
547
|
+
// Emit dist/entry-ssr.js — the single-file SSR twin of the (code-split)
|
|
548
|
+
// browser dist/entry.js — for the Cloudflare edge isolate. React + the
|
|
549
|
+
// runtime stay externalized (resolved to the isolate's SHARED
|
|
550
|
+
// worker-runtime, so runtime patches propagate without a rebuild — the
|
|
551
|
+
// Strategy S win); the client-only Shiki/Fuse libs are externalized so the
|
|
552
|
+
// ~10 MB Shiki graph stays out. The edge loads this as its single
|
|
553
|
+
// `foundation` module for request-time SSR, gated on its presence.
|
|
426
554
|
//
|
|
427
|
-
// The buildSSRBundle()
|
|
428
|
-
//
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
// await buildSSRBundle(outDir)
|
|
555
|
+
// (The legacy self-contained buildSSRBundle() — React + runtime INLINED,
|
|
556
|
+
// ~14 MB with Shiki — is retained below, unused, for reference only.)
|
|
557
|
+
const entrySourcePath = join(resolvedSrcDir, entryFileName)
|
|
558
|
+
await buildEntrySSR(resolvedRoot, entrySourcePath, outDir)
|
|
432
559
|
},
|
|
433
560
|
|
|
434
561
|
async closeBundle() {
|