@uniweb/build 0.1.19 → 0.1.21

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.1.19",
3
+ "version": "0.1.21",
4
4
  "description": "Build tooling for the Uniweb Component Web Platform",
5
5
  "type": "module",
6
6
  "exports": {
@@ -59,7 +59,7 @@
59
59
  "@tailwindcss/vite": "^4.0.0",
60
60
  "@vitejs/plugin-react": "^4.0.0 || ^5.0.0",
61
61
  "vite-plugin-svgr": "^4.0.0",
62
- "@uniweb/core": "0.1.8"
62
+ "@uniweb/core": "0.1.9"
63
63
  },
64
64
  "peerDependenciesMeta": {
65
65
  "vite": {
@@ -79,9 +79,14 @@ function isMarkdownFile(filename) {
79
79
 
80
80
  /**
81
81
  * Parse numeric prefix from filename (e.g., "1-hero.md" → { prefix: "1", name: "hero" })
82
+ * Supports:
83
+ * - Simple: "1", "2", "3"
84
+ * - Decimal ordering: "1.5" (between 1 and 2), "2.5" (between 2 and 3)
85
+ * - Hierarchy via comma: "1,1" (child of 1), "1,2" (second child of 1)
86
+ * - Mixed: "1.5,1" (child of section 1.5)
82
87
  */
83
88
  function parseNumericPrefix(filename) {
84
- const match = filename.match(/^(\d+(?:\.\d+)*)-?(.*)$/)
89
+ const match = filename.match(/^(\d+(?:[.,]\d+)*)-?(.*)$/)
85
90
  if (match) {
86
91
  return { prefix: match[1], name: match[2] || match[1] }
87
92
  }
@@ -89,7 +94,9 @@ function parseNumericPrefix(filename) {
89
94
  }
90
95
 
91
96
  /**
92
- * Compare filenames for sorting by numeric prefix
97
+ * Compare filenames for sorting by numeric prefix.
98
+ * Both . and , are treated as separators for sorting purposes.
99
+ * This ensures correct ordering: 1, 1,1, 1.5, 2, 2,1, etc.
93
100
  */
94
101
  function compareFilenames(a, b) {
95
102
  const { prefix: prefixA } = parseNumericPrefix(parse(a).name)
@@ -99,8 +106,8 @@ function compareFilenames(a, b) {
99
106
  if (!prefixA) return 1
100
107
  if (!prefixB) return -1
101
108
 
102
- const partsA = prefixA.split('.').map(Number)
103
- const partsB = prefixB.split('.').map(Number)
109
+ const partsA = prefixA.split(/[.,]/).map(Number)
110
+ const partsB = prefixB.split(/[.,]/).map(Number)
104
111
 
105
112
  for (let i = 0; i < Math.max(partsA.length, partsB.length); i++) {
106
113
  const numA = partsA[i] ?? 0
@@ -155,7 +162,11 @@ async function processMarkdownFile(filePath, id, siteRoot) {
155
162
  }
156
163
 
157
164
  /**
158
- * Build section hierarchy from flat list
165
+ * Build section hierarchy from flat list.
166
+ * Hierarchy is determined by comma separators:
167
+ * - "1", "1.5", "2" → all top-level (dots are for ordering)
168
+ * - "1,1", "1,2" → children of section "1"
169
+ * - "1.5,1" → child of section "1.5"
159
170
  */
160
171
  function buildSectionHierarchy(sections) {
161
172
  const sectionMap = new Map()
@@ -166,15 +177,15 @@ function buildSectionHierarchy(sections) {
166
177
  sectionMap.set(section.id, section)
167
178
  }
168
179
 
169
- // Second pass: build hierarchy
180
+ // Second pass: build hierarchy (comma = hierarchy)
170
181
  for (const section of sections) {
171
- if (!section.id.includes('.')) {
182
+ if (!section.id.includes(',')) {
172
183
  topLevel.push(section)
173
184
  continue
174
185
  }
175
186
 
176
- const parts = section.id.split('.')
177
- const parentId = parts.slice(0, -1).join('.')
187
+ const parts = section.id.split(',')
188
+ const parentId = parts.slice(0, -1).join(',')
178
189
  const parent = sectionMap.get(parentId)
179
190
 
180
191
  if (parent) {
@@ -277,11 +288,14 @@ async function processExplicitSections(sectionsConfig, pagePath, siteRoot, paren
277
288
  * Process a page directory
278
289
  *
279
290
  * @param {string} pagePath - Path to page directory
280
- * @param {string} pageName - Name of the page
291
+ * @param {string} pageName - Name of the page (folder name, not full path)
281
292
  * @param {string} siteRoot - Site root directory for asset resolution
293
+ * @param {Object} options - Route options
294
+ * @param {boolean} options.isIndex - Whether this page is the index for its parent route
295
+ * @param {string} options.parentRoute - The parent route (e.g., '/' or '/docs')
282
296
  * @returns {Object} Page data with assets manifest
283
297
  */
284
- async function processPage(pagePath, pageName, siteRoot) {
298
+ async function processPage(pagePath, pageName, siteRoot, { isIndex = false, parentRoute = '/' } = {}) {
285
299
  const pageConfig = await readYamlFile(join(pagePath, 'page.yml'))
286
300
 
287
301
  // Note: We no longer skip hidden pages here - they still exist as valid pages,
@@ -337,11 +351,16 @@ async function processPage(pagePath, pageName, siteRoot) {
337
351
  }
338
352
 
339
353
  // Determine route
340
- let route = '/' + pageName
341
- if (pageName === 'home' || pageName === 'index') {
342
- route = '/'
354
+ let route
355
+ if (isIndex) {
356
+ // Index page gets the parent route
357
+ route = parentRoute
343
358
  } else if (pageName.startsWith('@')) {
344
- route = '/' + pageName
359
+ // Special pages (layout areas) keep their @ prefix
360
+ route = parentRoute === '/' ? `/@${pageName.slice(1)}` : `${parentRoute}/@${pageName.slice(1)}`
361
+ } else {
362
+ // Normal pages get parent + their name
363
+ route = parentRoute === '/' ? `/${pageName}` : `${parentRoute}/${pageName}`
345
364
  }
346
365
 
347
366
  // Extract configuration
@@ -381,15 +400,50 @@ async function processPage(pagePath, pageName, siteRoot) {
381
400
  }
382
401
  }
383
402
 
403
+ /**
404
+ * Determine the index page name from ordering config
405
+ *
406
+ * @param {Object} orderConfig - { pages: [...], index: 'name' } from parent
407
+ * @param {Array} availableFolders - Array of { name, order } for folders at this level
408
+ * @returns {string|null} The folder name that should be the index, or null
409
+ */
410
+ function determineIndexPage(orderConfig, availableFolders) {
411
+ const { pages: pagesArray, index: indexName } = orderConfig || {}
412
+
413
+ // 1. Explicit pages array - first item is index
414
+ if (Array.isArray(pagesArray) && pagesArray.length > 0) {
415
+ return pagesArray[0]
416
+ }
417
+
418
+ // 2. Explicit index property
419
+ if (indexName) {
420
+ return indexName
421
+ }
422
+
423
+ // 3. Fallback: lowest order value, or first alphabetically
424
+ if (availableFolders.length === 0) return null
425
+
426
+ const sorted = [...availableFolders].sort((a, b) => {
427
+ // Sort by order (lower first), then alphabetically
428
+ const orderA = a.order ?? 999
429
+ const orderB = b.order ?? 999
430
+ if (orderA !== orderB) return orderA - orderB
431
+ return a.name.localeCompare(b.name)
432
+ })
433
+
434
+ return sorted[0].name
435
+ }
436
+
384
437
  /**
385
438
  * Recursively collect pages from a directory
386
439
  *
387
440
  * @param {string} dirPath - Directory to scan
388
- * @param {string} routePrefix - Route prefix for nested pages
441
+ * @param {string} parentRoute - Parent route (e.g., '/' or '/docs')
389
442
  * @param {string} siteRoot - Site root directory for asset resolution
443
+ * @param {Object} orderConfig - { pages: [...], index: 'name' } from parent's config
390
444
  * @returns {Promise<Object>} { pages, assetCollection, header, footer, left, right }
391
445
  */
392
- async function collectPagesRecursive(dirPath, routePrefix, siteRoot) {
446
+ async function collectPagesRecursive(dirPath, parentRoute, siteRoot, orderConfig = {}) {
393
447
  const entries = await readdir(dirPath)
394
448
  const pages = []
395
449
  let assetCollection = {
@@ -402,30 +456,55 @@ async function collectPagesRecursive(dirPath, routePrefix, siteRoot) {
402
456
  let left = null
403
457
  let right = null
404
458
 
459
+ // First pass: discover all page folders and read their order values
460
+ const pageFolders = []
405
461
  for (const entry of entries) {
406
462
  const entryPath = join(dirPath, entry)
407
463
  const stats = await stat(entryPath)
408
-
409
464
  if (!stats.isDirectory()) continue
410
465
 
411
- // Build the page name/route
412
- const pageName = routePrefix ? `${routePrefix}/${entry}` : entry
466
+ // Read page.yml to get order and child page config
467
+ const pageConfig = await readYamlFile(join(entryPath, 'page.yml'))
468
+ pageFolders.push({
469
+ name: entry,
470
+ path: entryPath,
471
+ order: pageConfig.order,
472
+ childOrderConfig: {
473
+ pages: pageConfig.pages,
474
+ index: pageConfig.index
475
+ }
476
+ })
477
+ }
478
+
479
+ // Determine which page is the index for this level
480
+ const regularFolders = pageFolders.filter(f => !f.name.startsWith('@'))
481
+ const indexPageName = determineIndexPage(orderConfig, regularFolders)
482
+
483
+ // Second pass: process each page folder
484
+ for (const folder of pageFolders) {
485
+ const { name: entry, path: entryPath, childOrderConfig } = folder
486
+ const isIndex = entry === indexPageName
487
+ const isSpecial = entry.startsWith('@')
413
488
 
414
489
  // Process this directory as a page
415
- const result = await processPage(entryPath, pageName, siteRoot)
490
+ const result = await processPage(entryPath, entry, siteRoot, {
491
+ isIndex: isIndex && !isSpecial,
492
+ parentRoute
493
+ })
494
+
416
495
  if (result) {
417
496
  const { page, assetCollection: pageAssets } = result
418
497
  assetCollection = mergeAssetCollections(assetCollection, pageAssets)
419
498
 
420
499
  // Handle special pages (layout areas) - only at root level
421
- if (!routePrefix) {
422
- if (entry === '@header' || page.route === '/@header') {
500
+ if (parentRoute === '/') {
501
+ if (entry === '@header') {
423
502
  header = page
424
- } else if (entry === '@footer' || page.route === '/@footer') {
503
+ } else if (entry === '@footer') {
425
504
  footer = page
426
- } else if (entry === '@left' || page.route === '/@left') {
505
+ } else if (entry === '@left') {
427
506
  left = page
428
- } else if (entry === '@right' || page.route === '/@right') {
507
+ } else if (entry === '@right') {
429
508
  right = page
430
509
  } else {
431
510
  pages.push(page)
@@ -433,13 +512,15 @@ async function collectPagesRecursive(dirPath, routePrefix, siteRoot) {
433
512
  } else {
434
513
  pages.push(page)
435
514
  }
436
- }
437
515
 
438
- // Recursively process subdirectories (but not special @ directories)
439
- if (!entry.startsWith('@')) {
440
- const subResult = await collectPagesRecursive(entryPath, pageName, siteRoot)
441
- pages.push(...subResult.pages)
442
- assetCollection = mergeAssetCollections(assetCollection, subResult.assetCollection)
516
+ // Recursively process subdirectories (but not special @ directories)
517
+ if (!isSpecial) {
518
+ // The child route depends on whether this page is the index
519
+ const childParentRoute = isIndex ? parentRoute : page.route
520
+ const subResult = await collectPagesRecursive(entryPath, childParentRoute, siteRoot, childOrderConfig)
521
+ pages.push(...subResult.pages)
522
+ assetCollection = mergeAssetCollections(assetCollection, subResult.assetCollection)
523
+ }
443
524
  }
444
525
  }
445
526
 
@@ -469,9 +550,15 @@ export async function collectSiteContent(sitePath) {
469
550
  }
470
551
  }
471
552
 
553
+ // Extract page ordering config from site.yml
554
+ const siteOrderConfig = {
555
+ pages: siteConfig.pages,
556
+ index: siteConfig.index
557
+ }
558
+
472
559
  // Recursively collect all pages
473
560
  const { pages, assetCollection, header, footer, left, right } =
474
- await collectPagesRecursive(pagesPath, '', sitePath)
561
+ await collectPagesRecursive(pagesPath, '/', sitePath, siteOrderConfig)
475
562
 
476
563
  // Sort pages by order
477
564
  pages.sort((a, b) => (a.order ?? 999) - (b.order ?? 999))