@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@uniweb/build",
3
- "version": "0.6.18",
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/schemas": "0.2.1",
53
+ "@uniweb/content-reader": "1.1.2",
54
54
  "@uniweb/runtime": "0.5.21",
55
- "@uniweb/content-reader": "1.1.2"
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(entryPath, childParentRoute, siteRoot, childOrderConfig, childFetch, versionContext, 'sections')
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 subResult = await collectPagesRecursive(entryPath, containerRoute, siteRoot, childOrderConfig, parentFetch, versionContext, 'pages')
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 subResult = await collectPagesRecursive(entryPath, containerRoute, siteRoot, childOrderConfig, parentFetch, versionContext, 'pages')
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(entryPath, childParentRoute, siteRoot, childOrderConfig, childFetch, versionContext, dirMode)
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)