@uniweb/build 0.10.3 → 0.11.3
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/content/index.js +24 -5
- 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 +107 -33
- 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.3",
|
|
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.5",
|
|
61
|
+
"@uniweb/content-reader": "1.1.6",
|
|
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.4"
|
|
72
72
|
},
|
|
73
73
|
"peerDependenciesMeta": {
|
|
74
74
|
"vite": {
|
package/src/content/index.js
CHANGED
|
@@ -1,13 +1,32 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Clean Content Entry Point
|
|
3
3
|
*
|
|
4
|
-
* Exposes
|
|
5
|
-
* (Node builtins, js-yaml, @uniweb/theming). No Vite,
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
4
|
+
* Exposes content-collection helpers and their clean dependency chain
|
|
5
|
+
* (Node builtins, js-yaml, @uniweb/theming, @uniweb/core). No Vite,
|
|
6
|
+
* sharp, or React. Used by Studio's sidecar (Bun-compiled binary) and
|
|
7
|
+
* by @uniweb/unipress (CLI compile path) where those heavy peer/native
|
|
8
|
+
* dependencies aren't available.
|
|
9
9
|
*
|
|
10
10
|
* @module @uniweb/build/content
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
13
|
export { collectSiteContent } from '../site/content-collector.js'
|
|
14
|
+
|
|
15
|
+
// Collections + fetch resolution. Pure functions on the clean dep
|
|
16
|
+
// chain — re-exported here so headless callers (unipress, sidecars)
|
|
17
|
+
// can resolve `collections:` declarations without importing
|
|
18
|
+
// `@uniweb/build/site` (which pulls Vite via its plugin index).
|
|
19
|
+
export {
|
|
20
|
+
processCollections,
|
|
21
|
+
writeCollectionFiles,
|
|
22
|
+
getCollectionLastModified,
|
|
23
|
+
} from '../site/collection-processor.js'
|
|
24
|
+
export {
|
|
25
|
+
parseFetchConfig,
|
|
26
|
+
executeFetch,
|
|
27
|
+
applyFilter,
|
|
28
|
+
applySort,
|
|
29
|
+
applyPostProcessing,
|
|
30
|
+
mergeDataIntoContent,
|
|
31
|
+
singularize,
|
|
32
|
+
} from '../site/data-fetcher.js'
|
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'
|
|
@@ -181,13 +181,19 @@ async function readYamlFile(filePath) {
|
|
|
181
181
|
/**
|
|
182
182
|
* Extract inset references from a ProseMirror document.
|
|
183
183
|
*
|
|
184
|
-
* Walks
|
|
185
|
-
* for `{params}`
|
|
186
|
-
*
|
|
187
|
-
*
|
|
184
|
+
* Walks the document recursively for `inset_ref` nodes (produced by
|
|
185
|
+
* content-reader for the `{params}` /
|
|
186
|
+
* `[text](@ComponentName){params}` / `[@key]{params}` forms). Each ref
|
|
187
|
+
* is removed and replaced in-place with an `inset_placeholder` node
|
|
188
|
+
* carrying a unique refId. The extracted refs are returned as an array.
|
|
189
|
+
*
|
|
190
|
+
* Inline insets (mid-paragraph) are kept as inline placeholders so the
|
|
191
|
+
* paragraph's text flow is preserved; block-level insets (own line)
|
|
192
|
+
* stay at the document root. Both share the same refId/getInset(refId)
|
|
193
|
+
* lookup machinery — only the position differs.
|
|
188
194
|
*
|
|
189
195
|
* @param {Object} doc - ProseMirror document (mutated in place)
|
|
190
|
-
* @returns {Array} Array of { refId, type, params, description }
|
|
196
|
+
* @returns {Array} Array of { refId, type, params, description, embedKind }
|
|
191
197
|
*/
|
|
192
198
|
function extractInsets(doc) {
|
|
193
199
|
if (!doc?.content || !Array.isArray(doc.content)) return []
|
|
@@ -195,25 +201,34 @@ function extractInsets(doc) {
|
|
|
195
201
|
const insets = []
|
|
196
202
|
let refIndex = 0
|
|
197
203
|
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
const
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
204
|
+
function visit(nodes) {
|
|
205
|
+
if (!Array.isArray(nodes)) return
|
|
206
|
+
for (let i = 0; i < nodes.length; i++) {
|
|
207
|
+
const node = nodes[i]
|
|
208
|
+
if (!node) continue
|
|
209
|
+
if (node.type === 'inset_ref') {
|
|
210
|
+
const { component, alt, embedKind, ...params } = node.attrs || {}
|
|
211
|
+
const refId = `inset_${refIndex++}`
|
|
212
|
+
insets.push({
|
|
213
|
+
refId,
|
|
214
|
+
type: component,
|
|
215
|
+
embedKind: embedKind || 'visual',
|
|
216
|
+
params: Object.keys(params).length > 0 ? params : {},
|
|
217
|
+
title: alt || null,
|
|
218
|
+
})
|
|
219
|
+
nodes[i] = {
|
|
220
|
+
type: 'inset_placeholder',
|
|
221
|
+
attrs: { refId, embedKind: embedKind || 'visual' },
|
|
222
|
+
}
|
|
223
|
+
continue
|
|
224
|
+
}
|
|
225
|
+
if (Array.isArray(node.content)) {
|
|
226
|
+
visit(node.content)
|
|
213
227
|
}
|
|
214
228
|
}
|
|
215
229
|
}
|
|
216
230
|
|
|
231
|
+
visit(doc.content)
|
|
217
232
|
return insets
|
|
218
233
|
}
|
|
219
234
|
|
|
@@ -256,6 +271,33 @@ function isIgnoredFolder(name) {
|
|
|
256
271
|
return name.startsWith('_')
|
|
257
272
|
}
|
|
258
273
|
|
|
274
|
+
/**
|
|
275
|
+
* Workspace-root content profiles, dispatched on the top-level config filename.
|
|
276
|
+
*
|
|
277
|
+
* - `site.yml` → site profile: content under `pages/`, page mode (multiple
|
|
278
|
+
* sections per page) by default, ordering field is `pages:`. The historical
|
|
279
|
+
* default; used for websites.
|
|
280
|
+
* - `document.yml` → document profile: content under `content/`, folder mode
|
|
281
|
+
* (each .md file is its own page/chapter) by default, ordering field is
|
|
282
|
+
* `content:` (with `pages:` accepted as a fallback alias). Used by document
|
|
283
|
+
* tools like unipress where the natural noun is "content" / "chapter."
|
|
284
|
+
*
|
|
285
|
+
* Per-folder folder.yml/page.yml overrides keep working in both profiles —
|
|
286
|
+
* the profile sets workspace-root defaults; per-folder configs take precedence.
|
|
287
|
+
* An explicit `paths: { pages: ... }` in either config also overrides the
|
|
288
|
+
* default content directory.
|
|
289
|
+
*
|
|
290
|
+
* Internal mode values match readFolderConfig: 'pages' (folder mode), 'sections' (page mode).
|
|
291
|
+
*/
|
|
292
|
+
const CONTENT_PROFILES = {
|
|
293
|
+
'site.yml': { contentDir: 'pages', defaultMode: 'sections', orderField: 'pages' },
|
|
294
|
+
'document.yml': { contentDir: 'content', defaultMode: 'pages', orderField: 'content' }
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
function getContentProfile(configFile) {
|
|
298
|
+
return CONTENT_PROFILES[configFile] || CONTENT_PROFILES['site.yml']
|
|
299
|
+
}
|
|
300
|
+
|
|
259
301
|
/**
|
|
260
302
|
* Read folder configuration, determining content mode from config file presence.
|
|
261
303
|
*
|
|
@@ -1926,18 +1968,34 @@ async function collectLayouts(layoutDir, siteRoot, layoutNames = new Set()) {
|
|
|
1926
1968
|
* @param {string} sitePath - Path to site directory
|
|
1927
1969
|
* @param {Object} options - Collection options
|
|
1928
1970
|
* @param {string} options.foundationPath - Path to foundation directory (for theme vars)
|
|
1971
|
+
* @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
1972
|
* @returns {Promise<Object>} Site content object with assets manifest
|
|
1930
1973
|
*/
|
|
1931
1974
|
export async function collectSiteContent(sitePath, options = {}) {
|
|
1932
|
-
const { foundationPath } = options
|
|
1975
|
+
const { foundationPath, configFile = 'site.yml' } = options
|
|
1933
1976
|
|
|
1934
1977
|
// Read site config and raw theme config
|
|
1935
|
-
const siteConfig = await readYamlFile(join(sitePath,
|
|
1936
|
-
|
|
1937
|
-
//
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
|
|
1978
|
+
const siteConfig = await readYamlFile(join(sitePath, configFile))
|
|
1979
|
+
|
|
1980
|
+
// Profile selects workspace-root defaults: site.yml → pages/ + page mode +
|
|
1981
|
+
// pages: ordering; document.yml → content/ + folder mode + content: ordering.
|
|
1982
|
+
const profile = getContentProfile(configFile)
|
|
1983
|
+
|
|
1984
|
+
// Resolve content paths from <config>.paths: group, defaulting per profile.
|
|
1985
|
+
// Backward compatibility: when the document profile's `content/` is missing
|
|
1986
|
+
// but a `pages/` directory exists, fall back to `pages/` so existing unipress
|
|
1987
|
+
// projects (and any document.yml-using setup that predates the profile)
|
|
1988
|
+
// keep working without edits.
|
|
1989
|
+
let pagesPath
|
|
1990
|
+
if (siteConfig.paths?.pages) {
|
|
1991
|
+
pagesPath = resolve(sitePath, siteConfig.paths.pages)
|
|
1992
|
+
} else {
|
|
1993
|
+
const profileDefault = join(sitePath, profile.contentDir)
|
|
1994
|
+
const legacyFallback = join(sitePath, 'pages')
|
|
1995
|
+
pagesPath = (profile.contentDir !== 'pages' && !existsSync(profileDefault) && existsSync(legacyFallback))
|
|
1996
|
+
? legacyFallback
|
|
1997
|
+
: profileDefault
|
|
1998
|
+
}
|
|
1941
1999
|
|
|
1942
2000
|
const mounts = resolveMounts(siteConfig.paths, sitePath, pagesPath)
|
|
1943
2001
|
|
|
@@ -1968,14 +2026,21 @@ export async function collectSiteContent(sitePath, options = {}) {
|
|
|
1968
2026
|
}
|
|
1969
2027
|
}
|
|
1970
2028
|
|
|
1971
|
-
// Extract page ordering config from
|
|
2029
|
+
// Extract page ordering config from the top-level config.
|
|
2030
|
+
// Document profile reads the `content:` field (with `pages:` as a back-compat
|
|
2031
|
+
// alias); site profile reads `pages:`. Either way the internal field name is
|
|
2032
|
+
// `pages` so collectPagesRecursive doesn't need to care about profile.
|
|
1972
2033
|
const siteOrderConfig = {
|
|
1973
|
-
pages:
|
|
2034
|
+
pages: profile.orderField === 'pages'
|
|
2035
|
+
? siteConfig.pages
|
|
2036
|
+
: (siteConfig[profile.orderField] ?? siteConfig.pages),
|
|
1974
2037
|
index: siteConfig.index
|
|
1975
2038
|
}
|
|
1976
2039
|
|
|
1977
|
-
//
|
|
1978
|
-
|
|
2040
|
+
// Root content mode default comes from the profile (folder mode for
|
|
2041
|
+
// documents, page mode for sites). A folder.yml/page.yml in the pages root
|
|
2042
|
+
// overrides the profile default per readFolderConfig.
|
|
2043
|
+
const { mode: rootContentMode } = await readFolderConfig(pagesPath, profile.defaultMode)
|
|
1979
2044
|
|
|
1980
2045
|
// Collect layout areas from layout/ directory (including named layout subdirectories)
|
|
1981
2046
|
const { layouts } = await collectLayouts(layoutPath, sitePath, layoutNames)
|
|
@@ -1985,9 +2050,18 @@ export async function collectSiteContent(sitePath, options = {}) {
|
|
|
1985
2050
|
: siteConfig.layout?.name || null
|
|
1986
2051
|
|
|
1987
2052
|
// Recursively collect all pages
|
|
1988
|
-
|
|
2053
|
+
let { pages, assetCollection, iconCollection, notFound, versionedScopes } =
|
|
1989
2054
|
await collectPagesRecursive(pagesPath, '/', sitePath, siteOrderConfig, null, null, rootContentMode, mounts, siteLayoutName)
|
|
1990
2055
|
|
|
2056
|
+
// Merge top-level config assets (e.g. document.yml's book.covers.front,
|
|
2057
|
+
// banner images, logos) into the manifest. The compile pipeline reads
|
|
2058
|
+
// website.assets[<original src>] to resolve these to filesystem paths
|
|
2059
|
+
// (Node) or URLs (browser) without doing its own I/O.
|
|
2060
|
+
const configAssets = collectConfigAssets(siteConfig, sitePath)
|
|
2061
|
+
if (Object.keys(configAssets).length > 0) {
|
|
2062
|
+
assetCollection = mergeAssetCollections(assetCollection, { assets: configAssets })
|
|
2063
|
+
}
|
|
2064
|
+
|
|
1991
2065
|
// Deduplicate: at the root level, homepage promotion can create a route
|
|
1992
2066
|
// collision between the promoted page and a content-less container.
|
|
1993
2067
|
// 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
|
|