@uniweb/build 0.10.3 → 0.11.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 +4 -4
- package/src/foundation/config.js +1 -0
- package/src/generate-entry.js +68 -0
- package/src/import-map-plugin.js +1 -0
- package/src/prerender.js +23 -0
- package/src/site/assets.js +62 -0
- package/src/site/config.js +20 -2
- package/src/site/content-collector.js +72 -13
- package/src/vite-foundation-plugin.js +10 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@uniweb/build",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.11.2",
|
|
4
4
|
"description": "Build tooling for the Uniweb Component Web Platform",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -57,8 +57,8 @@
|
|
|
57
57
|
"@uniweb/theming": "0.1.3"
|
|
58
58
|
},
|
|
59
59
|
"optionalDependencies": {
|
|
60
|
-
"@uniweb/runtime": "0.8.
|
|
61
|
-
"@uniweb/content-reader": "1.1.
|
|
60
|
+
"@uniweb/runtime": "0.8.4",
|
|
61
|
+
"@uniweb/content-reader": "1.1.5",
|
|
62
62
|
"@uniweb/schemas": "0.2.1"
|
|
63
63
|
},
|
|
64
64
|
"peerDependencies": {
|
|
@@ -68,7 +68,7 @@
|
|
|
68
68
|
"@tailwindcss/vite": "^4.0.0",
|
|
69
69
|
"@vitejs/plugin-react": "^4.0.0 || ^5.0.0",
|
|
70
70
|
"vite-plugin-svgr": "^4.0.0",
|
|
71
|
-
"@uniweb/core": "0.7.
|
|
71
|
+
"@uniweb/core": "0.7.3"
|
|
72
72
|
},
|
|
73
73
|
"peerDependenciesMeta": {
|
|
74
74
|
"vite": {
|
package/src/foundation/config.js
CHANGED
package/src/generate-entry.js
CHANGED
|
@@ -25,6 +25,53 @@ import { join, dirname } from 'node:path'
|
|
|
25
25
|
import { discoverComponents, discoverLayoutsInPath } from './schema.js'
|
|
26
26
|
import { extractAllRuntimeSchemas, extractAllLayoutRuntimeSchemas } from './runtime-schema.js'
|
|
27
27
|
|
|
28
|
+
/**
|
|
29
|
+
* Packages that may be bundled inside a foundation but require single-instance
|
|
30
|
+
* access from a host environment (currently: unipress). When a foundation
|
|
31
|
+
* declares any of these as a dependency, we re-export the named symbols
|
|
32
|
+
* from the generated entry so the host can reach the foundation's bundled
|
|
33
|
+
* copy instead of importing its own — avoiding the dual-instance trap
|
|
34
|
+
* (each side gets its own React.createContext, registrations land in a
|
|
35
|
+
* context the other side can't see).
|
|
36
|
+
*
|
|
37
|
+
* Detection is by `dependencies` / `peerDependencies` declaration. Vite
|
|
38
|
+
* would fail to resolve an undeclared bare import anyway, so "declared"
|
|
39
|
+
* and "imported" stay aligned in practice. A foundation that declares one
|
|
40
|
+
* of these and never actually imports it pays the cost of bundling it (the
|
|
41
|
+
* re-export keeps the symbols alive) — minor and easily fixed by removing
|
|
42
|
+
* the unused dep.
|
|
43
|
+
*
|
|
44
|
+
* The export list is a public-API contract: removing a symbol here breaks
|
|
45
|
+
* hosts compiled against older built foundations that re-exported it. Add
|
|
46
|
+
* conservatively.
|
|
47
|
+
*/
|
|
48
|
+
const HOST_SHAREABLE_PACKAGES = {
|
|
49
|
+
'@uniweb/press': ['compileSubtree', 'compileDocument']
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Detect which HOST_SHAREABLE_PACKAGES the foundation declares as a dep.
|
|
54
|
+
* Reads the foundation's own package.json (one level above srcDir).
|
|
55
|
+
* Returns [] if package.json is missing or unreadable.
|
|
56
|
+
*/
|
|
57
|
+
async function detectHostShareableImports(srcDir) {
|
|
58
|
+
const packages = Object.keys(HOST_SHAREABLE_PACKAGES)
|
|
59
|
+
if (packages.length === 0) return []
|
|
60
|
+
|
|
61
|
+
const pkgPath = join(dirname(srcDir), 'package.json')
|
|
62
|
+
if (!existsSync(pkgPath)) return []
|
|
63
|
+
|
|
64
|
+
let pkg
|
|
65
|
+
try {
|
|
66
|
+
pkg = JSON.parse(await readFile(pkgPath, 'utf-8'))
|
|
67
|
+
} catch {
|
|
68
|
+
return []
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const declared = { ...pkg.dependencies, ...pkg.peerDependencies }
|
|
72
|
+
return packages.filter(p => p in declared)
|
|
73
|
+
}
|
|
74
|
+
|
|
28
75
|
/**
|
|
29
76
|
* Detect foundation config file (for props, vars, etc.)
|
|
30
77
|
*
|
|
@@ -79,6 +126,7 @@ function generateEntrySource(components, options = {}) {
|
|
|
79
126
|
meta = {},
|
|
80
127
|
layouts = {},
|
|
81
128
|
layoutMeta = {},
|
|
129
|
+
hostShareableImports = [],
|
|
82
130
|
} = options
|
|
83
131
|
|
|
84
132
|
const componentNames = Object.keys(components).sort()
|
|
@@ -148,6 +196,18 @@ function generateEntrySource(components, options = {}) {
|
|
|
148
196
|
// Default export — non-component data (naturally unforgeable key)
|
|
149
197
|
lines.push('')
|
|
150
198
|
lines.push('export default { meta, capabilities, layoutMeta }')
|
|
199
|
+
|
|
200
|
+
// Re-export host-shareable packages the foundation actually imports.
|
|
201
|
+
// Lets unipress reach the foundation's bundled copy instead of importing
|
|
202
|
+
// its own (which would create a dual-instance React-context trap).
|
|
203
|
+
if (hostShareableImports.length > 0) {
|
|
204
|
+
lines.push('')
|
|
205
|
+
for (const pkg of hostShareableImports) {
|
|
206
|
+
const symbols = HOST_SHAREABLE_PACKAGES[pkg]
|
|
207
|
+
lines.push(`export { ${symbols.join(', ')} } from '${pkg}'`)
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
151
211
|
lines.push('')
|
|
152
212
|
|
|
153
213
|
return lines.join('\n')
|
|
@@ -239,6 +299,9 @@ export async function generateEntryPoint(srcDir, outputPath = null, options = {}
|
|
|
239
299
|
// Extract per-layout runtime metadata from meta.js files
|
|
240
300
|
const layoutMeta = extractAllLayoutRuntimeSchemas(layouts)
|
|
241
301
|
|
|
302
|
+
// Detect which host-shareable packages the foundation imports
|
|
303
|
+
const hostShareableImports = await detectHostShareableImports(srcDir)
|
|
304
|
+
|
|
242
305
|
// Generate source
|
|
243
306
|
const source = generateEntrySource(components, {
|
|
244
307
|
cssPath,
|
|
@@ -246,6 +309,7 @@ export async function generateEntryPoint(srcDir, outputPath = null, options = {}
|
|
|
246
309
|
meta,
|
|
247
310
|
layouts,
|
|
248
311
|
layoutMeta,
|
|
312
|
+
hostShareableImports,
|
|
249
313
|
})
|
|
250
314
|
|
|
251
315
|
// Write to file (skip if content unchanged to avoid unnecessary watcher triggers)
|
|
@@ -272,6 +336,9 @@ export async function generateEntryPoint(srcDir, outputPath = null, options = {}
|
|
|
272
336
|
if (foundationExports) {
|
|
273
337
|
console.log(` - Foundation exports found: ${foundationExports.path}`)
|
|
274
338
|
}
|
|
339
|
+
if (hostShareableImports.length > 0) {
|
|
340
|
+
console.log(` - Host-shareable re-exports: ${hostShareableImports.join(', ')}`)
|
|
341
|
+
}
|
|
275
342
|
|
|
276
343
|
return {
|
|
277
344
|
outputPath: output,
|
|
@@ -280,6 +347,7 @@ export async function generateEntryPoint(srcDir, outputPath = null, options = {}
|
|
|
280
347
|
foundationExports,
|
|
281
348
|
meta,
|
|
282
349
|
layoutMeta,
|
|
350
|
+
hostShareableImports,
|
|
283
351
|
}
|
|
284
352
|
}
|
|
285
353
|
|
package/src/import-map-plugin.js
CHANGED
package/src/prerender.js
CHANGED
|
@@ -434,6 +434,29 @@ export async function prerenderSite(siteDir, options = {}) {
|
|
|
434
434
|
}
|
|
435
435
|
const defaultSiteContent = JSON.parse(await readFile(contentPath, 'utf8'))
|
|
436
436
|
|
|
437
|
+
// Link-mode detection: if site.yml's foundation is a registry scoped ref
|
|
438
|
+
// or a URL, the foundation lives on the hosting edge, not on disk. Static
|
|
439
|
+
// prerender (which writes dist/<route>/index.html) has no local JS to
|
|
440
|
+
// execute, so we skip cleanly here. This is the right call for CLI deploy
|
|
441
|
+
// (where the Worker SSRs from R2 at serve time) and for any site whose
|
|
442
|
+
// foundation is deliberately remote. Sites that still need prerender +
|
|
443
|
+
// registry foundation would need a fetch-and-execute path, tracked as
|
|
444
|
+
// future work.
|
|
445
|
+
const fndRef = defaultSiteContent?.config?.foundation
|
|
446
|
+
const isLinkModeFoundation = (
|
|
447
|
+
(typeof fndRef === 'string' && (
|
|
448
|
+
/^@[a-z0-9_-]+\/[a-z0-9_-]+@.+$/.test(fndRef) ||
|
|
449
|
+
fndRef.startsWith('http://') ||
|
|
450
|
+
fndRef.startsWith('https://')
|
|
451
|
+
)) ||
|
|
452
|
+
(fndRef && typeof fndRef === 'object' && fndRef.url)
|
|
453
|
+
)
|
|
454
|
+
if (isLinkModeFoundation) {
|
|
455
|
+
onProgress(`Link-mode foundation (${typeof fndRef === 'string' ? fndRef : fndRef.url || fndRef.name}) — skipping prerender.`)
|
|
456
|
+
onProgress('(HTML will be rendered by the serving worker / runtime.)')
|
|
457
|
+
return { pages: 0, files: [] }
|
|
458
|
+
}
|
|
459
|
+
|
|
437
460
|
// Discover all locale content files
|
|
438
461
|
const localeConfigs = await discoverLocaleContents(distDir, defaultSiteContent)
|
|
439
462
|
if (localeConfigs.length > 1) {
|
package/src/site/assets.js
CHANGED
|
@@ -322,6 +322,68 @@ export function collectSectionAssets(section, markdownPath, siteRoot) {
|
|
|
322
322
|
return { assets, hasExplicitPoster, hasExplicitPreview }
|
|
323
323
|
}
|
|
324
324
|
|
|
325
|
+
/**
|
|
326
|
+
* Walk the top-level config object (site.yml / document.yml) for asset
|
|
327
|
+
* references and resolve each into a manifest entry. Catches things like
|
|
328
|
+
* book.covers.front, banner images, logos in metadata blocks — anything
|
|
329
|
+
* declared in the config that points at a local file by path.
|
|
330
|
+
*
|
|
331
|
+
* Result is keyed by the original source string (the value as it appears
|
|
332
|
+
* in the config). Foundations look up `website.assets[src]` at compile
|
|
333
|
+
* time and resolve `entry.resolved` to a filesystem path or `entry.url`
|
|
334
|
+
* to a URL, depending on the runtime context. This keeps foundations
|
|
335
|
+
* environment-agnostic — they don't need to know whether the compile
|
|
336
|
+
* pipeline runs in Node (unipress) or in the browser (editor).
|
|
337
|
+
*
|
|
338
|
+
* `walkDataAssets` already filters via `isLocalAssetPath` to skip
|
|
339
|
+
* non-asset strings (titles, descriptions, etc.).
|
|
340
|
+
*
|
|
341
|
+
* @param {Object} siteConfig - Parsed top-level config
|
|
342
|
+
* @param {string} siteRoot - Site/document root directory
|
|
343
|
+
* @returns {Object} Asset manifest keyed by original src string
|
|
344
|
+
*/
|
|
345
|
+
export function collectConfigAssets(siteConfig, siteRoot) {
|
|
346
|
+
const assets = {}
|
|
347
|
+
if (!siteConfig || typeof siteConfig !== 'object') return assets
|
|
348
|
+
|
|
349
|
+
// Anchor for relative-path resolution — `dirname(anchor)` must equal siteRoot
|
|
350
|
+
// so `assets/front.png` resolves to `<siteRoot>/assets/front.png`.
|
|
351
|
+
const anchor = `${siteRoot}/_config_anchor`
|
|
352
|
+
|
|
353
|
+
// Walk siteConfig with a more permissive filter than `isLocalAssetPath`:
|
|
354
|
+
// config asset paths often appear without a `./` prefix (e.g.
|
|
355
|
+
// `book.covers.front: assets/front.png`), which is the natural spelling
|
|
356
|
+
// for authors. Accept any string with a media extension that isn't an
|
|
357
|
+
// external URL.
|
|
358
|
+
const visit = (data) => {
|
|
359
|
+
if (typeof data === 'string') {
|
|
360
|
+
if (isExternalUrl(data)) return
|
|
361
|
+
if (!(isImagePath(data) || isVideoPath(data) || isPdfPath(data))) return
|
|
362
|
+
const result = resolveAssetPath(data, anchor, siteRoot)
|
|
363
|
+
if (!result.external && result.resolved) {
|
|
364
|
+
assets[data] = {
|
|
365
|
+
original: data,
|
|
366
|
+
resolved: result.resolved,
|
|
367
|
+
isImage: result.isImage,
|
|
368
|
+
isVideo: result.isVideo,
|
|
369
|
+
isPdf: result.isPdf
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
return
|
|
373
|
+
}
|
|
374
|
+
if (Array.isArray(data)) {
|
|
375
|
+
data.forEach(visit)
|
|
376
|
+
return
|
|
377
|
+
}
|
|
378
|
+
if (data && typeof data === 'object') {
|
|
379
|
+
Object.values(data).forEach(visit)
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
visit(siteConfig)
|
|
384
|
+
return assets
|
|
385
|
+
}
|
|
386
|
+
|
|
325
387
|
/**
|
|
326
388
|
* Merge multiple asset collection results
|
|
327
389
|
*
|
package/src/site/config.js
CHANGED
|
@@ -88,6 +88,24 @@ function detectFoundationType(foundation, siteRoot) {
|
|
|
88
88
|
}
|
|
89
89
|
}
|
|
90
90
|
|
|
91
|
+
// Registry scoped ref: "@namespace/name@version". By definition link-mode —
|
|
92
|
+
// the foundation lives on the hosting edge (R2) and is loaded at runtime.
|
|
93
|
+
// Surfacing this as `type: 'url'` makes Vite skip the local-foundation
|
|
94
|
+
// bundling path and use the noop virtual module (no need for VITE_FOUNDATION_MODE=runtime
|
|
95
|
+
// env var or a local foundation folder). Base URL defaults to the production
|
|
96
|
+
// worker but is overridable via UNIWEB_REGISTRY_URL for self-hosted / staging
|
|
97
|
+
// environments.
|
|
98
|
+
const scopedMatch = /^@([a-z0-9_-]+)\/([a-z0-9_-]+)@(.+)$/.exec(name)
|
|
99
|
+
if (scopedMatch) {
|
|
100
|
+
const [, ns, fn, ver] = scopedMatch
|
|
101
|
+
const base = process.env.UNIWEB_REGISTRY_URL || 'https://site-router.uniweb-edge.workers.dev'
|
|
102
|
+
return {
|
|
103
|
+
type: 'url',
|
|
104
|
+
url: `${base}/foundations/${ns}/${fn}/${ver}/foundation.js`,
|
|
105
|
+
cssUrl: `${base}/foundations/${ns}/${fn}/${ver}/assets/foundation.css`
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
91
109
|
// Check if it's a local workspace sibling (directory name matches package name)
|
|
92
110
|
const localPath = resolve(siteRoot, '..', name)
|
|
93
111
|
if (existsSync(localPath)) {
|
|
@@ -456,7 +474,7 @@ export async function defineSiteConfig(options = {}) {
|
|
|
456
474
|
// Deduplicate React packages to prevent dual-instance issues
|
|
457
475
|
// Foundation externalizes React; when site bundles it, CJS and ESM
|
|
458
476
|
// copies can coexist without this, causing "useRef of null" errors
|
|
459
|
-
dedupe: ['react', 'react-dom', 'react/jsx-runtime', 'react/jsx-dev-runtime'],
|
|
477
|
+
dedupe: ['react', 'react-dom', 'react-dom/server', 'react/jsx-runtime', 'react/jsx-dev-runtime'],
|
|
460
478
|
alias: {
|
|
461
479
|
...alias,
|
|
462
480
|
...resolveOverrides?.alias
|
|
@@ -491,7 +509,7 @@ export async function defineSiteConfig(options = {}) {
|
|
|
491
509
|
},
|
|
492
510
|
|
|
493
511
|
optimizeDeps: {
|
|
494
|
-
include: ['react', 'react-dom', 'react-dom/client', 'react-router-dom'],
|
|
512
|
+
include: ['react', 'react-dom', 'react-dom/client', 'react-dom/server', 'react-router-dom'],
|
|
495
513
|
exclude: ['#foundation']
|
|
496
514
|
},
|
|
497
515
|
|
|
@@ -27,7 +27,7 @@ import { readFile, readdir, stat } from 'node:fs/promises'
|
|
|
27
27
|
import { join, parse, resolve, sep } from 'node:path'
|
|
28
28
|
import { existsSync, statSync, realpathSync } from 'node:fs'
|
|
29
29
|
import yaml from 'js-yaml'
|
|
30
|
-
import { collectSectionAssets, mergeAssetCollections } from './assets.js'
|
|
30
|
+
import { collectSectionAssets, mergeAssetCollections, collectConfigAssets } from './assets.js'
|
|
31
31
|
import { collectSectionIcons, mergeIconCollections, buildIconManifest } from './icons.js'
|
|
32
32
|
import { parseFetchConfig, singularize } from './data-fetcher.js'
|
|
33
33
|
import { buildTheme, extractFoundationVars } from '../theme/index.js'
|
|
@@ -256,6 +256,33 @@ function isIgnoredFolder(name) {
|
|
|
256
256
|
return name.startsWith('_')
|
|
257
257
|
}
|
|
258
258
|
|
|
259
|
+
/**
|
|
260
|
+
* Workspace-root content profiles, dispatched on the top-level config filename.
|
|
261
|
+
*
|
|
262
|
+
* - `site.yml` → site profile: content under `pages/`, page mode (multiple
|
|
263
|
+
* sections per page) by default, ordering field is `pages:`. The historical
|
|
264
|
+
* default; used for websites.
|
|
265
|
+
* - `document.yml` → document profile: content under `content/`, folder mode
|
|
266
|
+
* (each .md file is its own page/chapter) by default, ordering field is
|
|
267
|
+
* `content:` (with `pages:` accepted as a fallback alias). Used by document
|
|
268
|
+
* tools like unipress where the natural noun is "content" / "chapter."
|
|
269
|
+
*
|
|
270
|
+
* Per-folder folder.yml/page.yml overrides keep working in both profiles —
|
|
271
|
+
* the profile sets workspace-root defaults; per-folder configs take precedence.
|
|
272
|
+
* An explicit `paths: { pages: ... }` in either config also overrides the
|
|
273
|
+
* default content directory.
|
|
274
|
+
*
|
|
275
|
+
* Internal mode values match readFolderConfig: 'pages' (folder mode), 'sections' (page mode).
|
|
276
|
+
*/
|
|
277
|
+
const CONTENT_PROFILES = {
|
|
278
|
+
'site.yml': { contentDir: 'pages', defaultMode: 'sections', orderField: 'pages' },
|
|
279
|
+
'document.yml': { contentDir: 'content', defaultMode: 'pages', orderField: 'content' }
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
function getContentProfile(configFile) {
|
|
283
|
+
return CONTENT_PROFILES[configFile] || CONTENT_PROFILES['site.yml']
|
|
284
|
+
}
|
|
285
|
+
|
|
259
286
|
/**
|
|
260
287
|
* Read folder configuration, determining content mode from config file presence.
|
|
261
288
|
*
|
|
@@ -1926,18 +1953,34 @@ async function collectLayouts(layoutDir, siteRoot, layoutNames = new Set()) {
|
|
|
1926
1953
|
* @param {string} sitePath - Path to site directory
|
|
1927
1954
|
* @param {Object} options - Collection options
|
|
1928
1955
|
* @param {string} options.foundationPath - Path to foundation directory (for theme vars)
|
|
1956
|
+
* @param {string} [options.configFile='site.yml'] - Name of the top-level config file inside sitePath. Defaults to 'site.yml'. Document tools (unipress) pass 'document.yml'.
|
|
1929
1957
|
* @returns {Promise<Object>} Site content object with assets manifest
|
|
1930
1958
|
*/
|
|
1931
1959
|
export async function collectSiteContent(sitePath, options = {}) {
|
|
1932
|
-
const { foundationPath } = options
|
|
1960
|
+
const { foundationPath, configFile = 'site.yml' } = options
|
|
1933
1961
|
|
|
1934
1962
|
// Read site config and raw theme config
|
|
1935
|
-
const siteConfig = await readYamlFile(join(sitePath,
|
|
1936
|
-
|
|
1937
|
-
//
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
|
|
1963
|
+
const siteConfig = await readYamlFile(join(sitePath, configFile))
|
|
1964
|
+
|
|
1965
|
+
// Profile selects workspace-root defaults: site.yml → pages/ + page mode +
|
|
1966
|
+
// pages: ordering; document.yml → content/ + folder mode + content: ordering.
|
|
1967
|
+
const profile = getContentProfile(configFile)
|
|
1968
|
+
|
|
1969
|
+
// Resolve content paths from <config>.paths: group, defaulting per profile.
|
|
1970
|
+
// Backward compatibility: when the document profile's `content/` is missing
|
|
1971
|
+
// but a `pages/` directory exists, fall back to `pages/` so existing unipress
|
|
1972
|
+
// projects (and any document.yml-using setup that predates the profile)
|
|
1973
|
+
// keep working without edits.
|
|
1974
|
+
let pagesPath
|
|
1975
|
+
if (siteConfig.paths?.pages) {
|
|
1976
|
+
pagesPath = resolve(sitePath, siteConfig.paths.pages)
|
|
1977
|
+
} else {
|
|
1978
|
+
const profileDefault = join(sitePath, profile.contentDir)
|
|
1979
|
+
const legacyFallback = join(sitePath, 'pages')
|
|
1980
|
+
pagesPath = (profile.contentDir !== 'pages' && !existsSync(profileDefault) && existsSync(legacyFallback))
|
|
1981
|
+
? legacyFallback
|
|
1982
|
+
: profileDefault
|
|
1983
|
+
}
|
|
1941
1984
|
|
|
1942
1985
|
const mounts = resolveMounts(siteConfig.paths, sitePath, pagesPath)
|
|
1943
1986
|
|
|
@@ -1968,14 +2011,21 @@ export async function collectSiteContent(sitePath, options = {}) {
|
|
|
1968
2011
|
}
|
|
1969
2012
|
}
|
|
1970
2013
|
|
|
1971
|
-
// Extract page ordering config from
|
|
2014
|
+
// Extract page ordering config from the top-level config.
|
|
2015
|
+
// Document profile reads the `content:` field (with `pages:` as a back-compat
|
|
2016
|
+
// alias); site profile reads `pages:`. Either way the internal field name is
|
|
2017
|
+
// `pages` so collectPagesRecursive doesn't need to care about profile.
|
|
1972
2018
|
const siteOrderConfig = {
|
|
1973
|
-
pages:
|
|
2019
|
+
pages: profile.orderField === 'pages'
|
|
2020
|
+
? siteConfig.pages
|
|
2021
|
+
: (siteConfig[profile.orderField] ?? siteConfig.pages),
|
|
1974
2022
|
index: siteConfig.index
|
|
1975
2023
|
}
|
|
1976
2024
|
|
|
1977
|
-
//
|
|
1978
|
-
|
|
2025
|
+
// Root content mode default comes from the profile (folder mode for
|
|
2026
|
+
// documents, page mode for sites). A folder.yml/page.yml in the pages root
|
|
2027
|
+
// overrides the profile default per readFolderConfig.
|
|
2028
|
+
const { mode: rootContentMode } = await readFolderConfig(pagesPath, profile.defaultMode)
|
|
1979
2029
|
|
|
1980
2030
|
// Collect layout areas from layout/ directory (including named layout subdirectories)
|
|
1981
2031
|
const { layouts } = await collectLayouts(layoutPath, sitePath, layoutNames)
|
|
@@ -1985,9 +2035,18 @@ export async function collectSiteContent(sitePath, options = {}) {
|
|
|
1985
2035
|
: siteConfig.layout?.name || null
|
|
1986
2036
|
|
|
1987
2037
|
// Recursively collect all pages
|
|
1988
|
-
|
|
2038
|
+
let { pages, assetCollection, iconCollection, notFound, versionedScopes } =
|
|
1989
2039
|
await collectPagesRecursive(pagesPath, '/', sitePath, siteOrderConfig, null, null, rootContentMode, mounts, siteLayoutName)
|
|
1990
2040
|
|
|
2041
|
+
// Merge top-level config assets (e.g. document.yml's book.covers.front,
|
|
2042
|
+
// banner images, logos) into the manifest. The compile pipeline reads
|
|
2043
|
+
// website.assets[<original src>] to resolve these to filesystem paths
|
|
2044
|
+
// (Node) or URLs (browser) without doing its own I/O.
|
|
2045
|
+
const configAssets = collectConfigAssets(siteConfig, sitePath)
|
|
2046
|
+
if (Object.keys(configAssets).length > 0) {
|
|
2047
|
+
assetCollection = mergeAssetCollections(assetCollection, { assets: configAssets })
|
|
2048
|
+
}
|
|
2049
|
+
|
|
1991
2050
|
// Deduplicate: at the root level, homepage promotion can create a route
|
|
1992
2051
|
// collision between the promoted page and a content-less container.
|
|
1993
2052
|
// At deeper levels, 1:1 mapping means collisions shouldn't happen — warn.
|
|
@@ -221,6 +221,16 @@ export function foundationBuildPlugin(options = {}) {
|
|
|
221
221
|
// Build self-contained SSR bundle for edge rendering (Dynamic Workers)
|
|
222
222
|
await buildSSRBundle(outDir)
|
|
223
223
|
},
|
|
224
|
+
|
|
225
|
+
async closeBundle() {
|
|
226
|
+
// esbuild spawns a long-lived service child process on first build() and
|
|
227
|
+
// keeps it running. Its stop() is the documented teardown for hosts that
|
|
228
|
+
// need to exit cleanly. Best-effort — never fail the build over this.
|
|
229
|
+
try {
|
|
230
|
+
const { stop } = await import('esbuild')
|
|
231
|
+
await stop?.()
|
|
232
|
+
} catch {}
|
|
233
|
+
},
|
|
224
234
|
}
|
|
225
235
|
}
|
|
226
236
|
|