@uniweb/build 0.6.18 → 0.6.19
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 +3 -3
- package/src/site/content-collector.js +130 -8
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@uniweb/build",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.19",
|
|
4
4
|
"description": "Build tooling for the Uniweb Component Web Platform",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -50,9 +50,9 @@
|
|
|
50
50
|
"sharp": "^0.33.2"
|
|
51
51
|
},
|
|
52
52
|
"optionalDependencies": {
|
|
53
|
-
"@uniweb/
|
|
53
|
+
"@uniweb/content-reader": "1.1.2",
|
|
54
54
|
"@uniweb/runtime": "0.5.21",
|
|
55
|
-
"@uniweb/
|
|
55
|
+
"@uniweb/schemas": "0.2.1"
|
|
56
56
|
},
|
|
57
57
|
"peerDependencies": {
|
|
58
58
|
"vite": "^5.0.0 || ^6.0.0 || ^7.0.0",
|
|
@@ -24,8 +24,8 @@
|
|
|
24
24
|
*/
|
|
25
25
|
|
|
26
26
|
import { readFile, readdir, stat } from 'node:fs/promises'
|
|
27
|
-
import { join, parse, resolve } from 'node:path'
|
|
28
|
-
import { existsSync } from 'node:fs'
|
|
27
|
+
import { join, parse, resolve, sep } from 'node:path'
|
|
28
|
+
import { existsSync, statSync, realpathSync } from 'node:fs'
|
|
29
29
|
import yaml from 'js-yaml'
|
|
30
30
|
import { collectSectionAssets, mergeAssetCollections } from './assets.js'
|
|
31
31
|
import { collectSectionIcons, mergeIconCollections, buildIconManifest } from './icons.js'
|
|
@@ -229,6 +229,102 @@ async function readFolderConfig(dirPath, inheritedMode) {
|
|
|
229
229
|
return { config: {}, mode: inheritedMode, source: 'inherited' }
|
|
230
230
|
}
|
|
231
231
|
|
|
232
|
+
/**
|
|
233
|
+
* Extract page mounts from site.yml paths: config.
|
|
234
|
+
*
|
|
235
|
+
* Keys like `pages/docs: ../../../docs` map a route segment to an external
|
|
236
|
+
* directory. All validation happens upfront before any page collection begins.
|
|
237
|
+
*
|
|
238
|
+
* @param {Object} pathsConfig - The paths: object from site.yml
|
|
239
|
+
* @param {string} sitePath - Absolute path to the site directory
|
|
240
|
+
* @param {string} pagesPath - Resolved absolute path to the pages directory
|
|
241
|
+
* @returns {Map<string, string>|null} Route segment → canonical absolute path, or null
|
|
242
|
+
*/
|
|
243
|
+
function resolveMounts(pathsConfig, sitePath, pagesPath) {
|
|
244
|
+
if (!pathsConfig || typeof pathsConfig !== 'object') return null
|
|
245
|
+
|
|
246
|
+
// Extract entries with "pages/" prefix (e.g., "pages/docs": "../../../docs")
|
|
247
|
+
const mountEntries = Object.entries(pathsConfig)
|
|
248
|
+
.filter(([key]) => key.startsWith('pages/'))
|
|
249
|
+
.map(([key, value]) => [key.slice('pages/'.length), value])
|
|
250
|
+
|
|
251
|
+
if (mountEntries.length === 0) return null
|
|
252
|
+
|
|
253
|
+
const resolved = new Map()
|
|
254
|
+
const canonicalPagesPath = existsSync(pagesPath) ? realpathSync(pagesPath) : resolve(pagesPath)
|
|
255
|
+
|
|
256
|
+
for (const [routeSegment, relativePath] of mountEntries) {
|
|
257
|
+
// Validate route segment (simple name, no slashes, no special chars)
|
|
258
|
+
if (!routeSegment || routeSegment.includes('/') || routeSegment.startsWith('.') || routeSegment.startsWith('_')) {
|
|
259
|
+
throw new Error(
|
|
260
|
+
`[content-collector] Invalid mount "pages/${routeSegment}" in site.yml paths.\n` +
|
|
261
|
+
` The segment after "pages/" must be a simple name (no slashes, dots, or underscores prefix).`
|
|
262
|
+
)
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
const absolutePath = resolve(sitePath, relativePath)
|
|
266
|
+
|
|
267
|
+
// Check existence
|
|
268
|
+
if (!existsSync(absolutePath)) {
|
|
269
|
+
throw new Error(
|
|
270
|
+
`[content-collector] External pages path does not exist: ${absolutePath}\n` +
|
|
271
|
+
` Declared in site.yml: pages/${routeSegment}: ${relativePath}`
|
|
272
|
+
)
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// Check it's a directory
|
|
276
|
+
if (!statSync(absolutePath).isDirectory()) {
|
|
277
|
+
throw new Error(
|
|
278
|
+
`[content-collector] External pages path is not a directory: ${absolutePath}\n` +
|
|
279
|
+
` Declared in site.yml: pages/${routeSegment}: ${relativePath}`
|
|
280
|
+
)
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
const canonical = realpathSync(absolutePath)
|
|
284
|
+
|
|
285
|
+
// Reject node_modules
|
|
286
|
+
if (canonical.includes(`${sep}node_modules${sep}`)) {
|
|
287
|
+
throw new Error(
|
|
288
|
+
`[content-collector] External pages path must not be inside node_modules: ${canonical}\n` +
|
|
289
|
+
` Declared in site.yml: pages/${routeSegment}: ${relativePath}`
|
|
290
|
+
)
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// Self-inclusion: must not overlap with site pages directory
|
|
294
|
+
if (
|
|
295
|
+
canonical === canonicalPagesPath ||
|
|
296
|
+
canonical.startsWith(canonicalPagesPath + sep) ||
|
|
297
|
+
canonicalPagesPath.startsWith(canonical + sep)
|
|
298
|
+
) {
|
|
299
|
+
throw new Error(
|
|
300
|
+
`[content-collector] External pages path overlaps with site pages directory:\n` +
|
|
301
|
+
` Path: ${canonical}\n` +
|
|
302
|
+
` Site pages: ${canonicalPagesPath}\n` +
|
|
303
|
+
` Declared in site.yml: pages/${routeSegment}`
|
|
304
|
+
)
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// Cross-mount overlap: no mount target should be ancestor/descendant of another
|
|
308
|
+
for (const [otherKey, otherPath] of resolved) {
|
|
309
|
+
if (
|
|
310
|
+
canonical === otherPath ||
|
|
311
|
+
canonical.startsWith(otherPath + sep) ||
|
|
312
|
+
otherPath.startsWith(canonical + sep)
|
|
313
|
+
) {
|
|
314
|
+
throw new Error(
|
|
315
|
+
`[content-collector] External pages paths overlap:\n` +
|
|
316
|
+
` "pages/${routeSegment}" → ${canonical}\n` +
|
|
317
|
+
` "pages/${otherKey}" → ${otherPath}`
|
|
318
|
+
)
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
resolved.set(routeSegment, canonical)
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
return resolved.size > 0 ? resolved : null
|
|
326
|
+
}
|
|
327
|
+
|
|
232
328
|
/**
|
|
233
329
|
* Parse numeric prefix from filename (e.g., "1-hero.md" → { prefix: "1", name: "hero" })
|
|
234
330
|
* Supports:
|
|
@@ -949,7 +1045,7 @@ function determineIndexPage(orderConfig, availableFolders) {
|
|
|
949
1045
|
* @param {string} contentMode - 'sections' (default) or 'pages' (md files are child pages)
|
|
950
1046
|
* @returns {Promise<Object>} { pages, assetCollection, iconCollection, notFound, versionedScopes }
|
|
951
1047
|
*/
|
|
952
|
-
async function collectPagesRecursive(dirPath, parentRoute, siteRoot, orderConfig = {}, parentFetch = null, versionContext = null, contentMode = 'sections') {
|
|
1048
|
+
async function collectPagesRecursive(dirPath, parentRoute, siteRoot, orderConfig = {}, parentFetch = null, versionContext = null, contentMode = 'sections', mounts = null) {
|
|
953
1049
|
const entries = await readdir(dirPath)
|
|
954
1050
|
const pages = []
|
|
955
1051
|
let assetCollection = {
|
|
@@ -989,6 +1085,26 @@ async function collectPagesRecursive(dirPath, parentRoute, siteRoot, orderConfig
|
|
|
989
1085
|
})
|
|
990
1086
|
}
|
|
991
1087
|
|
|
1088
|
+
// Inject virtual entries for mounts without physical directories
|
|
1089
|
+
if (mounts) {
|
|
1090
|
+
for (const [routeSegment, mountPath] of mounts) {
|
|
1091
|
+
if (!pageFolders.some(f => f.name === routeSegment)) {
|
|
1092
|
+
const { config: mountConfig } = await readFolderConfig(mountPath, 'pages')
|
|
1093
|
+
pageFolders.push({
|
|
1094
|
+
name: routeSegment,
|
|
1095
|
+
path: mountPath,
|
|
1096
|
+
order: typeof mountConfig.order === 'number' ? mountConfig.order : undefined,
|
|
1097
|
+
dirConfig: { title: mountConfig.title || routeSegment, ...mountConfig },
|
|
1098
|
+
dirMode: 'pages',
|
|
1099
|
+
childOrderConfig: {
|
|
1100
|
+
pages: mountConfig.pages,
|
|
1101
|
+
index: mountConfig.index
|
|
1102
|
+
}
|
|
1103
|
+
})
|
|
1104
|
+
}
|
|
1105
|
+
}
|
|
1106
|
+
}
|
|
1107
|
+
|
|
992
1108
|
// Sort page folders by order (ascending), then alphabetically
|
|
993
1109
|
// Pages without explicit order come after ordered pages (order ?? Infinity)
|
|
994
1110
|
pageFolders.sort((a, b) => {
|
|
@@ -1133,9 +1249,10 @@ async function collectPagesRecursive(dirPath, parentRoute, siteRoot, orderConfig
|
|
|
1133
1249
|
pages.push(page)
|
|
1134
1250
|
|
|
1135
1251
|
// Recurse into subdirectories (page mode)
|
|
1252
|
+
const childDirPath = mounts?.get(entry) || entryPath
|
|
1136
1253
|
const childParentRoute = isIndex ? parentRoute : page.route
|
|
1137
1254
|
const childFetch = page.fetch || parentFetch
|
|
1138
|
-
const subResult = await collectPagesRecursive(
|
|
1255
|
+
const subResult = await collectPagesRecursive(childDirPath, childParentRoute, siteRoot, childOrderConfig, childFetch, versionContext, 'sections', null)
|
|
1139
1256
|
pages.push(...subResult.pages)
|
|
1140
1257
|
assetCollection = mergeAssetCollections(assetCollection, subResult.assetCollection)
|
|
1141
1258
|
iconCollection = mergeIconCollections(iconCollection, subResult.iconCollection)
|
|
@@ -1187,7 +1304,8 @@ async function collectPagesRecursive(dirPath, parentRoute, siteRoot, orderConfig
|
|
|
1187
1304
|
pages.push(containerPage)
|
|
1188
1305
|
|
|
1189
1306
|
// Recurse in folder mode
|
|
1190
|
-
const
|
|
1307
|
+
const childDirPath = mounts?.get(entry) || entryPath
|
|
1308
|
+
const subResult = await collectPagesRecursive(childDirPath, containerRoute, siteRoot, childOrderConfig, parentFetch, versionContext, 'pages', null)
|
|
1191
1309
|
pages.push(...subResult.pages)
|
|
1192
1310
|
assetCollection = mergeAssetCollections(assetCollection, subResult.assetCollection)
|
|
1193
1311
|
iconCollection = mergeIconCollections(iconCollection, subResult.iconCollection)
|
|
@@ -1273,7 +1391,8 @@ async function collectPagesRecursive(dirPath, parentRoute, siteRoot, orderConfig
|
|
|
1273
1391
|
pages.push(containerPage)
|
|
1274
1392
|
}
|
|
1275
1393
|
|
|
1276
|
-
const
|
|
1394
|
+
const childDirPath = mounts?.get(entry) || entryPath
|
|
1395
|
+
const subResult = await collectPagesRecursive(childDirPath, containerRoute, siteRoot, childOrderConfig, parentFetch, versionContext, 'pages', null)
|
|
1277
1396
|
pages.push(...subResult.pages)
|
|
1278
1397
|
assetCollection = mergeAssetCollections(assetCollection, subResult.assetCollection)
|
|
1279
1398
|
iconCollection = mergeIconCollections(iconCollection, subResult.iconCollection)
|
|
@@ -1300,11 +1419,12 @@ async function collectPagesRecursive(dirPath, parentRoute, siteRoot, orderConfig
|
|
|
1300
1419
|
|
|
1301
1420
|
// Recursively process subdirectories
|
|
1302
1421
|
{
|
|
1422
|
+
const childDirPath = mounts?.get(entry) || entryPath
|
|
1303
1423
|
const childParentRoute = isIndex
|
|
1304
1424
|
? (hasExplicitOrder ? parentRoute : (page.sourcePath || page.route))
|
|
1305
1425
|
: page.route
|
|
1306
1426
|
const childFetch = page.fetch || parentFetch
|
|
1307
|
-
const subResult = await collectPagesRecursive(
|
|
1427
|
+
const subResult = await collectPagesRecursive(childDirPath, childParentRoute, siteRoot, childOrderConfig, childFetch, versionContext, dirMode, null)
|
|
1308
1428
|
pages.push(...subResult.pages)
|
|
1309
1429
|
assetCollection = mergeAssetCollections(assetCollection, subResult.assetCollection)
|
|
1310
1430
|
iconCollection = mergeIconCollections(iconCollection, subResult.iconCollection)
|
|
@@ -1437,6 +1557,8 @@ export async function collectSiteContent(sitePath, options = {}) {
|
|
|
1437
1557
|
? resolve(sitePath, siteConfig.paths.pages)
|
|
1438
1558
|
: join(sitePath, 'pages')
|
|
1439
1559
|
|
|
1560
|
+
const mounts = resolveMounts(siteConfig.paths, sitePath, pagesPath)
|
|
1561
|
+
|
|
1440
1562
|
const layoutPath = siteConfig.paths?.layout
|
|
1441
1563
|
? resolve(sitePath, siteConfig.paths.layout)
|
|
1442
1564
|
: join(sitePath, 'layout')
|
|
@@ -1478,7 +1600,7 @@ export async function collectSiteContent(sitePath, options = {}) {
|
|
|
1478
1600
|
|
|
1479
1601
|
// Recursively collect all pages
|
|
1480
1602
|
const { pages, assetCollection, iconCollection, notFound, versionedScopes } =
|
|
1481
|
-
await collectPagesRecursive(pagesPath, '/', sitePath, siteOrderConfig, null, null, rootContentMode)
|
|
1603
|
+
await collectPagesRecursive(pagesPath, '/', sitePath, siteOrderConfig, null, null, rootContentMode, mounts)
|
|
1482
1604
|
|
|
1483
1605
|
// Deduplicate: remove content-less container pages whose route duplicates
|
|
1484
1606
|
// a content-bearing page (e.g., a promoted index page)
|