@uniweb/build 0.6.17 → 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/i18n/index.js +2 -6
- package/src/site/content-collector.js +409 -42
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/schemas": "0.2.1",
|
|
54
53
|
"@uniweb/content-reader": "1.1.2",
|
|
55
|
-
"@uniweb/runtime": "0.5.21"
|
|
54
|
+
"@uniweb/runtime": "0.5.21",
|
|
55
|
+
"@uniweb/schemas": "0.2.1"
|
|
56
56
|
},
|
|
57
57
|
"peerDependencies": {
|
|
58
58
|
"vite": "^5.0.0 || ^6.0.0 || ^7.0.0",
|
package/src/i18n/index.js
CHANGED
|
@@ -169,21 +169,17 @@ async function resolveLocales(configLocales, localesPath) {
|
|
|
169
169
|
/**
|
|
170
170
|
* Extract manifest from site content and write to file
|
|
171
171
|
* @param {string} siteRoot - Site root directory
|
|
172
|
+
* @param {Object} siteContent - Collected site content (from collectSiteContent)
|
|
172
173
|
* @param {Object} options - Options
|
|
173
174
|
* @returns {Object} { manifest, report }
|
|
174
175
|
*/
|
|
175
|
-
export async function extractManifest(siteRoot, options = {}) {
|
|
176
|
+
export async function extractManifest(siteRoot, siteContent, options = {}) {
|
|
176
177
|
const {
|
|
177
178
|
localesDir = DEFAULTS.localesDir,
|
|
178
|
-
siteContentPath = join(siteRoot, 'dist', 'site-content.json'),
|
|
179
179
|
verbose = false,
|
|
180
180
|
dryRun = false
|
|
181
181
|
} = options
|
|
182
182
|
|
|
183
|
-
// Load site content
|
|
184
|
-
const siteContentRaw = await readFile(siteContentPath, 'utf-8')
|
|
185
|
-
const siteContent = JSON.parse(siteContentRaw)
|
|
186
|
-
|
|
187
183
|
// Extract translatable content
|
|
188
184
|
const manifest = extractTranslatableContent(siteContent)
|
|
189
185
|
|
|
@@ -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:
|
|
@@ -271,23 +367,130 @@ function compareFilenames(a, b) {
|
|
|
271
367
|
}
|
|
272
368
|
|
|
273
369
|
/**
|
|
274
|
-
*
|
|
275
|
-
*
|
|
370
|
+
* Extract the name from a config array item.
|
|
371
|
+
* Handles both string entries ("hero") and object entries ({ features: [...] }).
|
|
372
|
+
* @param {*} item - Array item from sections: or pages: config
|
|
373
|
+
* @returns {string|null} The name, or null if not a valid entry
|
|
374
|
+
*/
|
|
375
|
+
function extractItemName(item) {
|
|
376
|
+
if (typeof item === 'string') return item
|
|
377
|
+
if (typeof item === 'object' && item !== null) {
|
|
378
|
+
const keys = Object.keys(item)
|
|
379
|
+
if (keys.length === 1) return keys[0]
|
|
380
|
+
}
|
|
381
|
+
return null
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
/**
|
|
385
|
+
* Parse a config array that may contain '...' rest markers.
|
|
386
|
+
*
|
|
387
|
+
* Returns structured info:
|
|
388
|
+
* - mode 'strict': no '...' — only listed items visible in navigation
|
|
389
|
+
* - mode 'inclusive': '...' present — pinned items + auto-discovered rest
|
|
390
|
+
* - mode 'all': array is just ['...'] — equivalent to omitting config
|
|
391
|
+
*
|
|
392
|
+
* @param {Array} arr - Config array (may contain '...' strings and/or objects)
|
|
393
|
+
* @returns {{ mode: 'strict'|'inclusive'|'all', before: Array, after: Array }|null}
|
|
394
|
+
*/
|
|
395
|
+
function parseWildcardArray(arr) {
|
|
396
|
+
if (!Array.isArray(arr) || arr.length === 0) return null
|
|
397
|
+
|
|
398
|
+
const firstRestIndex = arr.indexOf('...')
|
|
399
|
+
if (firstRestIndex === -1) {
|
|
400
|
+
return { mode: 'strict', before: [...arr], after: [] }
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
// Find last '...' index
|
|
404
|
+
let lastRestIndex = firstRestIndex
|
|
405
|
+
for (let i = arr.length - 1; i >= 0; i--) {
|
|
406
|
+
if (arr[i] === '...') { lastRestIndex = i; break }
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
const before = arr.slice(0, firstRestIndex).filter(x => x !== '...')
|
|
410
|
+
const after = arr.slice(lastRestIndex + 1).filter(x => x !== '...')
|
|
411
|
+
|
|
412
|
+
if (before.length === 0 && after.length === 0) {
|
|
413
|
+
return { mode: 'all', before: [], after: [] }
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
return { mode: 'inclusive', before, after }
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
/**
|
|
420
|
+
* Apply wildcard-aware ordering to a list of named items.
|
|
276
421
|
*
|
|
277
|
-
*
|
|
278
|
-
*
|
|
422
|
+
* - strict: listed items first in listed order, then unlisted (all items returned)
|
|
423
|
+
* - inclusive: before items, then rest (in existing order), then after items
|
|
424
|
+
* - all/null: return items unchanged
|
|
279
425
|
*
|
|
280
426
|
* @param {Array} items - Items with a .name property
|
|
281
|
-
* @param {Array
|
|
282
|
-
* @returns {Array} Reordered items
|
|
427
|
+
* @param {{ mode: string, before: Array, after: Array }|null} parsed - From parseWildcardArray
|
|
428
|
+
* @returns {Array} Reordered items
|
|
429
|
+
*/
|
|
430
|
+
function applyWildcardOrder(items, parsed) {
|
|
431
|
+
if (!parsed || parsed.mode === 'all') return items
|
|
432
|
+
|
|
433
|
+
const itemMap = new Map(items.map(i => [i.name, i]))
|
|
434
|
+
const beforeNames = parsed.before.map(extractItemName).filter(Boolean)
|
|
435
|
+
const afterNames = parsed.after.map(extractItemName).filter(Boolean)
|
|
436
|
+
const allPinnedNames = new Set([...beforeNames, ...afterNames])
|
|
437
|
+
|
|
438
|
+
const beforeItems = beforeNames.filter(n => itemMap.has(n)).map(n => itemMap.get(n))
|
|
439
|
+
const afterItems = afterNames.filter(n => itemMap.has(n)).map(n => itemMap.get(n))
|
|
440
|
+
const rest = items.filter(i => !allPinnedNames.has(i.name))
|
|
441
|
+
|
|
442
|
+
if (parsed.mode === 'strict') {
|
|
443
|
+
// Listed items first, then unlisted (hiding is applied separately)
|
|
444
|
+
return [...beforeItems, ...rest]
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
// Inclusive: before + rest + after
|
|
448
|
+
return [...beforeItems, ...rest, ...afterItems]
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
/**
|
|
452
|
+
* Find the markdown file for a section name, handling numeric prefixes.
|
|
453
|
+
* Tries exact match first ("hero.md"), then prefix-based ("1-hero.md").
|
|
454
|
+
*
|
|
455
|
+
* @param {string} pagePath - Directory containing section files
|
|
456
|
+
* @param {string} sectionName - Logical section name (e.g., 'hero')
|
|
457
|
+
* @param {string[]} [cachedFiles] - Pre-read directory listing (optimization)
|
|
458
|
+
* @returns {{ filePath: string, stableName: string, prefix: string|null }|null}
|
|
459
|
+
*/
|
|
460
|
+
function findSectionFile(pagePath, sectionName, cachedFiles) {
|
|
461
|
+
const exactPath = join(pagePath, `${sectionName}.md`)
|
|
462
|
+
if (existsSync(exactPath)) {
|
|
463
|
+
return { filePath: exactPath, stableName: sectionName, prefix: null }
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
const files = cachedFiles || []
|
|
467
|
+
for (const file of files) {
|
|
468
|
+
if (!isMarkdownFile(file)) continue
|
|
469
|
+
const { name } = parse(file)
|
|
470
|
+
const { prefix, name: parsedName } = parseNumericPrefix(name)
|
|
471
|
+
if (parsedName === sectionName) {
|
|
472
|
+
return { filePath: join(pagePath, file), stableName: sectionName, prefix }
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
return null
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
/**
|
|
480
|
+
* Extract a direct child's folder name from its route, relative to parentRoute.
|
|
481
|
+
* Returns null for the index page (route === parentRoute) or non-direct-children.
|
|
482
|
+
*
|
|
483
|
+
* @param {string} route - Page route (e.g., '/about')
|
|
484
|
+
* @param {string} parentRoute - Parent route (e.g., '/')
|
|
485
|
+
* @returns {string|null}
|
|
283
486
|
*/
|
|
284
|
-
function
|
|
285
|
-
if (!
|
|
286
|
-
const
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
return
|
|
487
|
+
function getDirectChildName(route, parentRoute) {
|
|
488
|
+
if (!route || route === parentRoute) return null
|
|
489
|
+
const prefix = parentRoute === '/' ? '/' : parentRoute + '/'
|
|
490
|
+
if (!route.startsWith(prefix)) return null
|
|
491
|
+
const rest = route.slice(prefix.length)
|
|
492
|
+
if (rest.includes('/')) return null
|
|
493
|
+
return rest
|
|
291
494
|
}
|
|
292
495
|
|
|
293
496
|
/**
|
|
@@ -482,6 +685,9 @@ async function processExplicitSections(sectionsConfig, pagePath, siteRoot, paren
|
|
|
482
685
|
}
|
|
483
686
|
let lastModified = null
|
|
484
687
|
|
|
688
|
+
// Cache directory listing for prefix-based file resolution
|
|
689
|
+
const cachedFiles = await readdir(pagePath)
|
|
690
|
+
|
|
485
691
|
let index = 1
|
|
486
692
|
for (const item of sectionsConfig) {
|
|
487
693
|
let sectionName
|
|
@@ -507,13 +713,14 @@ async function processExplicitSections(sectionsConfig, pagePath, siteRoot, paren
|
|
|
507
713
|
// Build section ID
|
|
508
714
|
const id = parentId ? `${parentId}.${index}` : String(index)
|
|
509
715
|
|
|
510
|
-
// Look for the markdown file
|
|
511
|
-
const
|
|
512
|
-
if (!
|
|
716
|
+
// Look for the markdown file (exact match or prefix-based, e.g., "hero" → "1-hero.md")
|
|
717
|
+
const found = findSectionFile(pagePath, sectionName, cachedFiles)
|
|
718
|
+
if (!found) {
|
|
513
719
|
console.warn(`[content-collector] Section file not found: ${sectionName}.md`)
|
|
514
720
|
index++
|
|
515
721
|
continue
|
|
516
722
|
}
|
|
723
|
+
const filePath = found.filePath
|
|
517
724
|
|
|
518
725
|
// Process the section
|
|
519
726
|
// Use sectionName as stable ID for scroll targeting (e.g., "hero", "features")
|
|
@@ -579,8 +786,9 @@ async function processPage(pagePath, pageName, siteRoot, { isIndex = false, pare
|
|
|
579
786
|
|
|
580
787
|
// Check for explicit sections configuration
|
|
581
788
|
const { sections: sectionsConfig } = pageConfig
|
|
789
|
+
const sectionsParsed = Array.isArray(sectionsConfig) ? parseWildcardArray(sectionsConfig) : null
|
|
582
790
|
|
|
583
|
-
if (sectionsConfig === undefined || sectionsConfig === '*') {
|
|
791
|
+
if (sectionsConfig === undefined || sectionsConfig === '*' || sectionsParsed?.mode === 'all') {
|
|
584
792
|
// Default behavior: discover all .md files, sort by numeric prefix
|
|
585
793
|
const files = await readdir(pagePath)
|
|
586
794
|
const mdFiles = files.filter(isMarkdownFile).sort(compareFilenames)
|
|
@@ -609,8 +817,79 @@ async function processPage(pagePath, pageName, siteRoot, { isIndex = false, pare
|
|
|
609
817
|
// Build hierarchy from dot notation
|
|
610
818
|
hierarchicalSections = buildSectionHierarchy(sections)
|
|
611
819
|
|
|
820
|
+
} else if (sectionsParsed?.mode === 'inclusive') {
|
|
821
|
+
// Inclusive: pinned sections + auto-discovered rest via '...' wildcard
|
|
822
|
+
const files = await readdir(pagePath)
|
|
823
|
+
const mdFiles = files.filter(isMarkdownFile).sort(compareFilenames)
|
|
824
|
+
|
|
825
|
+
// Build name → file info map from discovered files
|
|
826
|
+
const discoveredMap = new Map()
|
|
827
|
+
for (const file of mdFiles) {
|
|
828
|
+
const { name } = parse(file)
|
|
829
|
+
const { prefix, name: stableName } = parseNumericPrefix(name)
|
|
830
|
+
const key = stableName || name
|
|
831
|
+
if (!discoveredMap.has(key)) {
|
|
832
|
+
discoveredMap.set(key, { file, prefix, stableName: key })
|
|
833
|
+
}
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
// Create items with .name property for applyWildcardOrder
|
|
837
|
+
const allItems = [...discoveredMap.keys()].map(name => ({ name }))
|
|
838
|
+
const ordered = applyWildcardOrder(allItems, sectionsParsed)
|
|
839
|
+
|
|
840
|
+
// Collect subsection configs from the original array (e.g., { features: [a, b] })
|
|
841
|
+
const subsectionConfigs = new Map()
|
|
842
|
+
for (const item of [...sectionsParsed.before, ...sectionsParsed.after]) {
|
|
843
|
+
if (typeof item === 'object' && item !== null) {
|
|
844
|
+
const keys = Object.keys(item)
|
|
845
|
+
if (keys.length === 1) {
|
|
846
|
+
subsectionConfigs.set(keys[0], item[keys[0]])
|
|
847
|
+
}
|
|
848
|
+
}
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
// Process sections in wildcard-expanded order
|
|
852
|
+
const sections = []
|
|
853
|
+
let sectionIndex = 1
|
|
854
|
+
for (const { name } of ordered) {
|
|
855
|
+
const entry = discoveredMap.get(name)
|
|
856
|
+
if (!entry) {
|
|
857
|
+
console.warn(`[content-collector] Section '${name}' not found in ${pagePath}`)
|
|
858
|
+
continue
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
const id = String(sectionIndex)
|
|
862
|
+
const { section, assetCollection: sectionAssets, iconCollection: sectionIcons } =
|
|
863
|
+
await processMarkdownFile(join(pagePath, entry.file), id, siteRoot, entry.stableName)
|
|
864
|
+
sections.push(section)
|
|
865
|
+
pageAssetCollection = mergeAssetCollections(pageAssetCollection, sectionAssets)
|
|
866
|
+
pageIconCollection = mergeIconCollections(pageIconCollection, sectionIcons)
|
|
867
|
+
|
|
868
|
+
// Track last modified
|
|
869
|
+
const fileStat = await stat(join(pagePath, entry.file))
|
|
870
|
+
if (!lastModified || fileStat.mtime > lastModified) {
|
|
871
|
+
lastModified = fileStat.mtime
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
// Process subsections if configured (e.g., { features: [logocloud, stats] })
|
|
875
|
+
const subsections = subsectionConfigs.get(name)
|
|
876
|
+
if (Array.isArray(subsections) && subsections.length > 0) {
|
|
877
|
+
const subResult = await processExplicitSections(subsections, pagePath, siteRoot, id)
|
|
878
|
+
section.subsections = subResult.sections
|
|
879
|
+
pageAssetCollection = mergeAssetCollections(pageAssetCollection, subResult.assetCollection)
|
|
880
|
+
pageIconCollection = mergeIconCollections(pageIconCollection, subResult.iconCollection)
|
|
881
|
+
if (subResult.lastModified && (!lastModified || subResult.lastModified > lastModified)) {
|
|
882
|
+
lastModified = subResult.lastModified
|
|
883
|
+
}
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
sectionIndex++
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
hierarchicalSections = buildSectionHierarchy(sections)
|
|
890
|
+
|
|
612
891
|
} else if (Array.isArray(sectionsConfig) && sectionsConfig.length > 0) {
|
|
613
|
-
//
|
|
892
|
+
// Strict: explicit sections array (only listed sections processed)
|
|
614
893
|
const result = await processExplicitSections(sectionsConfig, pagePath, siteRoot)
|
|
615
894
|
hierarchicalSections = result.sections
|
|
616
895
|
pageAssetCollection = result.assetCollection
|
|
@@ -722,9 +1001,13 @@ async function processPage(pagePath, pageName, siteRoot, { isIndex = false, pare
|
|
|
722
1001
|
function determineIndexPage(orderConfig, availableFolders) {
|
|
723
1002
|
const { pages: pagesArray, index: indexName } = orderConfig || {}
|
|
724
1003
|
|
|
725
|
-
// 1. Explicit pages array - first item is index
|
|
1004
|
+
// 1. Explicit pages array - first non-'...' item is index
|
|
726
1005
|
if (Array.isArray(pagesArray) && pagesArray.length > 0) {
|
|
727
|
-
|
|
1006
|
+
const parsed = parseWildcardArray(pagesArray)
|
|
1007
|
+
if (parsed && parsed.before.length > 0) {
|
|
1008
|
+
return extractItemName(parsed.before[0])
|
|
1009
|
+
}
|
|
1010
|
+
// Array starts with '...' or is ['...'] — no index from pages, fall through
|
|
728
1011
|
}
|
|
729
1012
|
|
|
730
1013
|
// 2. Explicit index property
|
|
@@ -762,7 +1045,7 @@ function determineIndexPage(orderConfig, availableFolders) {
|
|
|
762
1045
|
* @param {string} contentMode - 'sections' (default) or 'pages' (md files are child pages)
|
|
763
1046
|
* @returns {Promise<Object>} { pages, assetCollection, iconCollection, notFound, versionedScopes }
|
|
764
1047
|
*/
|
|
765
|
-
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) {
|
|
766
1049
|
const entries = await readdir(dirPath)
|
|
767
1050
|
const pages = []
|
|
768
1051
|
let assetCollection = {
|
|
@@ -788,7 +1071,6 @@ async function collectPagesRecursive(dirPath, parentRoute, siteRoot, orderConfig
|
|
|
788
1071
|
// Read folder.yml or page.yml to determine mode and get config
|
|
789
1072
|
const { config: dirConfig, mode: dirMode } = await readFolderConfig(entryPath, contentMode)
|
|
790
1073
|
const numericOrder = typeof dirConfig.order === 'number' ? dirConfig.order : undefined
|
|
791
|
-
const childOrderArray = Array.isArray(dirConfig.order) ? dirConfig.order : undefined
|
|
792
1074
|
|
|
793
1075
|
pageFolders.push({
|
|
794
1076
|
name: entry,
|
|
@@ -798,12 +1080,31 @@ async function collectPagesRecursive(dirPath, parentRoute, siteRoot, orderConfig
|
|
|
798
1080
|
dirMode,
|
|
799
1081
|
childOrderConfig: {
|
|
800
1082
|
pages: dirConfig.pages,
|
|
801
|
-
index: dirConfig.index
|
|
802
|
-
order: childOrderArray
|
|
1083
|
+
index: dirConfig.index
|
|
803
1084
|
}
|
|
804
1085
|
})
|
|
805
1086
|
}
|
|
806
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
|
+
|
|
807
1108
|
// Sort page folders by order (ascending), then alphabetically
|
|
808
1109
|
// Pages without explicit order come after ordered pages (order ?? Infinity)
|
|
809
1110
|
pageFolders.sort((a, b) => {
|
|
@@ -813,8 +1114,20 @@ async function collectPagesRecursive(dirPath, parentRoute, siteRoot, orderConfig
|
|
|
813
1114
|
return a.name.localeCompare(b.name)
|
|
814
1115
|
})
|
|
815
1116
|
|
|
816
|
-
// Apply
|
|
817
|
-
|
|
1117
|
+
// Apply ordering: pages: (wildcard-aware) > order: [array] (backward compat) > default
|
|
1118
|
+
let orderedFolders
|
|
1119
|
+
let strictPageNames = null
|
|
1120
|
+
|
|
1121
|
+
const pagesParsed = Array.isArray(orderConfig?.pages) ? parseWildcardArray(orderConfig.pages) : null
|
|
1122
|
+
|
|
1123
|
+
if (pagesParsed && pagesParsed.mode !== 'all') {
|
|
1124
|
+
orderedFolders = applyWildcardOrder(pageFolders, pagesParsed)
|
|
1125
|
+
if (pagesParsed.mode === 'strict') {
|
|
1126
|
+
strictPageNames = new Set(pagesParsed.before.map(extractItemName).filter(Boolean))
|
|
1127
|
+
}
|
|
1128
|
+
} else {
|
|
1129
|
+
orderedFolders = pageFolders
|
|
1130
|
+
}
|
|
818
1131
|
|
|
819
1132
|
// Check if this directory contains version folders (versioned section)
|
|
820
1133
|
const folderNames = orderedFolders.map(f => f.name)
|
|
@@ -879,12 +1192,28 @@ async function collectPagesRecursive(dirPath, parentRoute, siteRoot, orderConfig
|
|
|
879
1192
|
}
|
|
880
1193
|
}
|
|
881
1194
|
|
|
882
|
-
// Apply
|
|
883
|
-
|
|
1195
|
+
// Apply ordering: pages: (wildcard-aware) > order: [array] (backward compat) > default
|
|
1196
|
+
let orderedMdPages
|
|
1197
|
+
let strictPageNamesFM = null
|
|
884
1198
|
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
1199
|
+
const pagesParsedFM = Array.isArray(orderConfig?.pages) ? parseWildcardArray(orderConfig.pages) : null
|
|
1200
|
+
|
|
1201
|
+
if (pagesParsedFM && pagesParsedFM.mode !== 'all') {
|
|
1202
|
+
orderedMdPages = applyWildcardOrder(mdPageItems, pagesParsedFM)
|
|
1203
|
+
if (pagesParsedFM.mode === 'strict') {
|
|
1204
|
+
strictPageNamesFM = new Set(pagesParsedFM.before.map(extractItemName).filter(Boolean))
|
|
1205
|
+
}
|
|
1206
|
+
} else {
|
|
1207
|
+
orderedMdPages = mdPageItems
|
|
1208
|
+
}
|
|
1209
|
+
|
|
1210
|
+
// In folder mode, determine index: pages: first item, or explicit index:
|
|
1211
|
+
let indexName = null
|
|
1212
|
+
if (pagesParsedFM && pagesParsedFM.before.length > 0) {
|
|
1213
|
+
indexName = extractItemName(pagesParsedFM.before[0])
|
|
1214
|
+
} else {
|
|
1215
|
+
indexName = orderConfig?.index || null
|
|
1216
|
+
}
|
|
888
1217
|
|
|
889
1218
|
// Add md-file-pages
|
|
890
1219
|
for (const { name, result } of orderedMdPages) {
|
|
@@ -920,9 +1249,10 @@ async function collectPagesRecursive(dirPath, parentRoute, siteRoot, orderConfig
|
|
|
920
1249
|
pages.push(page)
|
|
921
1250
|
|
|
922
1251
|
// Recurse into subdirectories (page mode)
|
|
1252
|
+
const childDirPath = mounts?.get(entry) || entryPath
|
|
923
1253
|
const childParentRoute = isIndex ? parentRoute : page.route
|
|
924
1254
|
const childFetch = page.fetch || parentFetch
|
|
925
|
-
const subResult = await collectPagesRecursive(
|
|
1255
|
+
const subResult = await collectPagesRecursive(childDirPath, childParentRoute, siteRoot, childOrderConfig, childFetch, versionContext, 'sections', null)
|
|
926
1256
|
pages.push(...subResult.pages)
|
|
927
1257
|
assetCollection = mergeAssetCollections(assetCollection, subResult.assetCollection)
|
|
928
1258
|
iconCollection = mergeIconCollections(iconCollection, subResult.iconCollection)
|
|
@@ -974,7 +1304,8 @@ async function collectPagesRecursive(dirPath, parentRoute, siteRoot, orderConfig
|
|
|
974
1304
|
pages.push(containerPage)
|
|
975
1305
|
|
|
976
1306
|
// Recurse in folder mode
|
|
977
|
-
const
|
|
1307
|
+
const childDirPath = mounts?.get(entry) || entryPath
|
|
1308
|
+
const subResult = await collectPagesRecursive(childDirPath, containerRoute, siteRoot, childOrderConfig, parentFetch, versionContext, 'pages', null)
|
|
978
1309
|
pages.push(...subResult.pages)
|
|
979
1310
|
assetCollection = mergeAssetCollections(assetCollection, subResult.assetCollection)
|
|
980
1311
|
iconCollection = mergeIconCollections(iconCollection, subResult.iconCollection)
|
|
@@ -984,6 +1315,17 @@ async function collectPagesRecursive(dirPath, parentRoute, siteRoot, orderConfig
|
|
|
984
1315
|
}
|
|
985
1316
|
}
|
|
986
1317
|
|
|
1318
|
+
// When pages: is strict (no '...'), hide unlisted direct children from navigation
|
|
1319
|
+
if (strictPageNamesFM) {
|
|
1320
|
+
for (const page of pages) {
|
|
1321
|
+
const childName = getDirectChildName(page.route, parentRoute)
|
|
1322
|
+
|| (page.sourcePath ? getDirectChildName(page.sourcePath, parentRoute) : null)
|
|
1323
|
+
if (childName && !strictPageNamesFM.has(childName) && !page.hidden) {
|
|
1324
|
+
page.hidden = true
|
|
1325
|
+
}
|
|
1326
|
+
}
|
|
1327
|
+
}
|
|
1328
|
+
|
|
987
1329
|
return { pages, assetCollection, iconCollection, notFound, versionedScopes }
|
|
988
1330
|
}
|
|
989
1331
|
|
|
@@ -1049,7 +1391,8 @@ async function collectPagesRecursive(dirPath, parentRoute, siteRoot, orderConfig
|
|
|
1049
1391
|
pages.push(containerPage)
|
|
1050
1392
|
}
|
|
1051
1393
|
|
|
1052
|
-
const
|
|
1394
|
+
const childDirPath = mounts?.get(entry) || entryPath
|
|
1395
|
+
const subResult = await collectPagesRecursive(childDirPath, containerRoute, siteRoot, childOrderConfig, parentFetch, versionContext, 'pages', null)
|
|
1053
1396
|
pages.push(...subResult.pages)
|
|
1054
1397
|
assetCollection = mergeAssetCollections(assetCollection, subResult.assetCollection)
|
|
1055
1398
|
iconCollection = mergeIconCollections(iconCollection, subResult.iconCollection)
|
|
@@ -1076,11 +1419,12 @@ async function collectPagesRecursive(dirPath, parentRoute, siteRoot, orderConfig
|
|
|
1076
1419
|
|
|
1077
1420
|
// Recursively process subdirectories
|
|
1078
1421
|
{
|
|
1422
|
+
const childDirPath = mounts?.get(entry) || entryPath
|
|
1079
1423
|
const childParentRoute = isIndex
|
|
1080
1424
|
? (hasExplicitOrder ? parentRoute : (page.sourcePath || page.route))
|
|
1081
1425
|
: page.route
|
|
1082
1426
|
const childFetch = page.fetch || parentFetch
|
|
1083
|
-
const subResult = await collectPagesRecursive(
|
|
1427
|
+
const subResult = await collectPagesRecursive(childDirPath, childParentRoute, siteRoot, childOrderConfig, childFetch, versionContext, dirMode, null)
|
|
1084
1428
|
pages.push(...subResult.pages)
|
|
1085
1429
|
assetCollection = mergeAssetCollections(assetCollection, subResult.assetCollection)
|
|
1086
1430
|
iconCollection = mergeIconCollections(iconCollection, subResult.iconCollection)
|
|
@@ -1092,6 +1436,17 @@ async function collectPagesRecursive(dirPath, parentRoute, siteRoot, orderConfig
|
|
|
1092
1436
|
}
|
|
1093
1437
|
}
|
|
1094
1438
|
|
|
1439
|
+
// When pages: is strict (no '...'), hide unlisted direct children from navigation
|
|
1440
|
+
if (strictPageNames) {
|
|
1441
|
+
for (const page of pages) {
|
|
1442
|
+
const childName = getDirectChildName(page.route, parentRoute)
|
|
1443
|
+
|| (page.sourcePath ? getDirectChildName(page.sourcePath, parentRoute) : null)
|
|
1444
|
+
if (childName && !strictPageNames.has(childName) && !page.hidden) {
|
|
1445
|
+
page.hidden = true
|
|
1446
|
+
}
|
|
1447
|
+
}
|
|
1448
|
+
}
|
|
1449
|
+
|
|
1095
1450
|
return { pages, assetCollection, iconCollection, notFound, versionedScopes }
|
|
1096
1451
|
}
|
|
1097
1452
|
|
|
@@ -1202,6 +1557,8 @@ export async function collectSiteContent(sitePath, options = {}) {
|
|
|
1202
1557
|
? resolve(sitePath, siteConfig.paths.pages)
|
|
1203
1558
|
: join(sitePath, 'pages')
|
|
1204
1559
|
|
|
1560
|
+
const mounts = resolveMounts(siteConfig.paths, sitePath, pagesPath)
|
|
1561
|
+
|
|
1205
1562
|
const layoutPath = siteConfig.paths?.layout
|
|
1206
1563
|
? resolve(sitePath, siteConfig.paths.layout)
|
|
1207
1564
|
: join(sitePath, 'layout')
|
|
@@ -1232,8 +1589,7 @@ export async function collectSiteContent(sitePath, options = {}) {
|
|
|
1232
1589
|
// Extract page ordering config from site.yml
|
|
1233
1590
|
const siteOrderConfig = {
|
|
1234
1591
|
pages: siteConfig.pages,
|
|
1235
|
-
index: siteConfig.index
|
|
1236
|
-
order: Array.isArray(siteConfig.order) ? siteConfig.order : undefined
|
|
1592
|
+
index: siteConfig.index
|
|
1237
1593
|
}
|
|
1238
1594
|
|
|
1239
1595
|
// Determine root content mode from folder.yml/page.yml presence in pages directory
|
|
@@ -1244,7 +1600,7 @@ export async function collectSiteContent(sitePath, options = {}) {
|
|
|
1244
1600
|
|
|
1245
1601
|
// Recursively collect all pages
|
|
1246
1602
|
const { pages, assetCollection, iconCollection, notFound, versionedScopes } =
|
|
1247
|
-
await collectPagesRecursive(pagesPath, '/', sitePath, siteOrderConfig, null, null, rootContentMode)
|
|
1603
|
+
await collectPagesRecursive(pagesPath, '/', sitePath, siteOrderConfig, null, null, rootContentMode, mounts)
|
|
1248
1604
|
|
|
1249
1605
|
// Deduplicate: remove content-less container pages whose route duplicates
|
|
1250
1606
|
// a content-bearing page (e.g., a promoted index page)
|
|
@@ -1291,8 +1647,11 @@ export async function collectSiteContent(sitePath, options = {}) {
|
|
|
1291
1647
|
page.parent = parentPage ? parentPage.route : null
|
|
1292
1648
|
}
|
|
1293
1649
|
|
|
1294
|
-
//
|
|
1295
|
-
|
|
1650
|
+
// Page order is determined by per-level sorting during collection:
|
|
1651
|
+
// 1. Numeric 'order' property in page.yml (lower first, within each level)
|
|
1652
|
+
// 2. pages: array in parent config (wildcard-aware, overrides numeric order)
|
|
1653
|
+
// 3. order: [array] in parent config (non-strict, backward compat)
|
|
1654
|
+
// No global re-sort — collection order is authoritative.
|
|
1296
1655
|
|
|
1297
1656
|
// Log asset summary
|
|
1298
1657
|
const assetCount = Object.keys(assetCollection.assets).length
|
|
@@ -1335,4 +1694,12 @@ export async function collectSiteContent(sitePath, options = {}) {
|
|
|
1335
1694
|
}
|
|
1336
1695
|
}
|
|
1337
1696
|
|
|
1697
|
+
// Exported for testing
|
|
1698
|
+
export {
|
|
1699
|
+
extractItemName,
|
|
1700
|
+
parseWildcardArray,
|
|
1701
|
+
applyWildcardOrder,
|
|
1702
|
+
getDirectChildName
|
|
1703
|
+
}
|
|
1704
|
+
|
|
1338
1705
|
export default collectSiteContent
|