@uniweb/build 0.3.0 → 0.3.2

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.3.0",
3
+ "version": "0.3.2",
4
4
  "description": "Build tooling for the Uniweb Component Web Platform",
5
5
  "type": "module",
6
6
  "exports": {
@@ -50,8 +50,8 @@
50
50
  "sharp": "^0.33.2"
51
51
  },
52
52
  "optionalDependencies": {
53
- "@uniweb/content-reader": "1.0.7",
54
- "@uniweb/runtime": "0.3.1"
53
+ "@uniweb/content-reader": "1.0.8",
54
+ "@uniweb/runtime": "0.4.3"
55
55
  },
56
56
  "peerDependencies": {
57
57
  "vite": "^5.0.0 || ^6.0.0 || ^7.0.0",
@@ -60,7 +60,7 @@
60
60
  "@tailwindcss/vite": "^4.0.0",
61
61
  "@vitejs/plugin-react": "^4.0.0 || ^5.0.0",
62
62
  "vite-plugin-svgr": "^4.0.0",
63
- "@uniweb/core": "0.2.3"
63
+ "@uniweb/core": "0.2.5"
64
64
  },
65
65
  "peerDependenciesMeta": {
66
66
  "vite": {
package/src/prerender.js CHANGED
@@ -9,7 +9,7 @@
9
9
  */
10
10
 
11
11
  import { readFile, writeFile, mkdir } from 'node:fs/promises'
12
- import { existsSync } from 'node:fs'
12
+ import { existsSync, readdirSync, statSync } from 'node:fs'
13
13
  import { join, dirname, resolve } from 'node:path'
14
14
  import { pathToFileURL } from 'node:url'
15
15
  import { createRequire } from 'node:module'
@@ -301,8 +301,7 @@ function renderBlock(block) {
301
301
  const componentProps = {
302
302
  content,
303
303
  params,
304
- block,
305
- input: block.input
304
+ block
306
305
  }
307
306
 
308
307
  // Wrapper props
@@ -381,6 +380,62 @@ function createPageElement(page, website) {
381
380
  )
382
381
  }
383
382
 
383
+ /**
384
+ * Discover all locale content files in the dist directory
385
+ * Returns an array of { locale, contentPath, htmlPath, isDefault }
386
+ *
387
+ * @param {string} distDir - Path to dist directory
388
+ * @param {Object} defaultContent - Default site content (to get default locale)
389
+ * @returns {Array} Locale configurations
390
+ */
391
+ async function discoverLocaleContents(distDir, defaultContent) {
392
+ const locales = []
393
+ const defaultLocale = defaultContent.config?.defaultLanguage || 'en'
394
+
395
+ // Add the default locale (root level)
396
+ locales.push({
397
+ locale: defaultLocale,
398
+ contentPath: join(distDir, 'site-content.json'),
399
+ htmlPath: join(distDir, 'index.html'),
400
+ isDefault: true,
401
+ routePrefix: ''
402
+ })
403
+
404
+ // Check for locale subdirectories with site-content.json
405
+ try {
406
+ const entries = readdirSync(distDir)
407
+ for (const entry of entries) {
408
+ const entryPath = join(distDir, entry)
409
+ // Skip if not a directory
410
+ if (!statSync(entryPath).isDirectory()) continue
411
+
412
+ // Check if this looks like a locale code (2-3 letter code)
413
+ if (!/^[a-z]{2,3}(-[A-Z]{2})?$/.test(entry)) continue
414
+
415
+ // Check if it has a site-content.json
416
+ const localeContentPath = join(entryPath, 'site-content.json')
417
+ const localeHtmlPath = join(entryPath, 'index.html')
418
+
419
+ if (existsSync(localeContentPath)) {
420
+ locales.push({
421
+ locale: entry,
422
+ contentPath: localeContentPath,
423
+ htmlPath: localeHtmlPath,
424
+ isDefault: false,
425
+ routePrefix: `/${entry}`
426
+ })
427
+ }
428
+ }
429
+ } catch (err) {
430
+ // Ignore errors reading directory
431
+ if (process.env.DEBUG) {
432
+ console.error('Error discovering locale contents:', err.message)
433
+ }
434
+ }
435
+
436
+ return locales
437
+ }
438
+
384
439
  /**
385
440
  * Pre-render all pages in a built site to static HTML
386
441
  *
@@ -407,33 +462,21 @@ export async function prerenderSite(siteDir, options = {}) {
407
462
  onProgress('Loading dependencies...')
408
463
  await loadDependencies(siteDir)
409
464
 
410
- // Load site content
465
+ // Load default site content
411
466
  onProgress('Loading site content...')
412
467
  const contentPath = join(distDir, 'site-content.json')
413
468
  if (!existsSync(contentPath)) {
414
469
  throw new Error(`site-content.json not found at: ${contentPath}`)
415
470
  }
416
- const siteContent = JSON.parse(await readFile(contentPath, 'utf8'))
417
-
418
- // Execute data fetches (site, page, section levels)
419
- onProgress('Executing data fetches...')
420
- const { siteCascadedData, pageFetchedData } = await executeAllFetches(siteContent, siteDir, onProgress)
421
-
422
- // Expand dynamic pages (e.g., /blog/:slug → /blog/post-1, /blog/post-2)
423
- if (siteContent.pages?.some(p => p.isDynamic)) {
424
- onProgress('Expanding dynamic routes...')
425
- siteContent.pages = expandDynamicPages(siteContent.pages, pageFetchedData, onProgress)
426
- }
471
+ const defaultSiteContent = JSON.parse(await readFile(contentPath, 'utf8'))
427
472
 
428
- // Load the HTML shell
429
- onProgress('Loading HTML shell...')
430
- const shellPath = join(distDir, 'index.html')
431
- if (!existsSync(shellPath)) {
432
- throw new Error(`index.html not found at: ${shellPath}`)
473
+ // Discover all locale content files
474
+ const localeConfigs = await discoverLocaleContents(distDir, defaultSiteContent)
475
+ if (localeConfigs.length > 1) {
476
+ onProgress(`Found ${localeConfigs.length} locales: ${localeConfigs.map(l => l.locale).join(', ')}`)
433
477
  }
434
- const htmlShell = await readFile(shellPath, 'utf8')
435
478
 
436
- // Load the foundation module
479
+ // Load the foundation module (shared across all locales)
437
480
  onProgress('Loading foundation...')
438
481
  const foundationPath = join(foundationDir, 'dist', 'foundation.js')
439
482
  if (!existsSync(foundationPath)) {
@@ -442,58 +485,87 @@ export async function prerenderSite(siteDir, options = {}) {
442
485
  const foundationUrl = pathToFileURL(foundationPath).href
443
486
  const foundation = await import(foundationUrl)
444
487
 
445
- // Initialize the Uniweb runtime (this sets globalThis.uniweb)
446
- onProgress('Initializing runtime...')
447
- const uniweb = createUniweb(siteContent)
448
- uniweb.setFoundation(foundation)
488
+ // Pre-render each locale
489
+ const renderedFiles = []
449
490
 
450
- // Set foundation capabilities (Layout, props, etc.)
451
- if (foundation.capabilities) {
452
- uniweb.setFoundationConfig(foundation.capabilities)
453
- }
491
+ for (const localeConfig of localeConfigs) {
492
+ const { locale, contentPath: localeContentPath, htmlPath, isDefault, routePrefix } = localeConfig
454
493
 
455
- // Pre-render each page
456
- const renderedFiles = []
457
- const pages = uniweb.activeWebsite.pages
458
- const website = uniweb.activeWebsite
494
+ onProgress(`\nRendering ${isDefault ? 'default' : locale} locale...`)
459
495
 
460
- for (const page of pages) {
461
- // Each page has a single canonical route
462
- // Index pages already have their parent route as their canonical route
463
- onProgress(`Rendering ${page.route}...`)
464
-
465
- // Set this as the active page
466
- uniweb.activeWebsite.setActivePage(page.route)
467
-
468
- // Create the page element using inline SSR rendering
469
- // (uses React from prerender's scope to avoid module resolution issues)
470
- const element = createPageElement(page, website)
471
-
472
- // Render to HTML string
473
- let renderedContent
474
- try {
475
- renderedContent = renderToString(element)
476
- } catch (err) {
477
- console.warn(`Warning: Failed to render ${page.route}: ${err.message}`)
478
- if (process.env.DEBUG) {
479
- console.error(err.stack)
480
- }
481
- continue
496
+ // Load locale-specific content
497
+ const siteContent = JSON.parse(await readFile(localeContentPath, 'utf8'))
498
+
499
+ // Set the active locale in the content
500
+ siteContent.config = siteContent.config || {}
501
+ siteContent.config.activeLocale = locale
502
+
503
+ // Execute data fetches (site, page, section levels)
504
+ onProgress('Executing data fetches...')
505
+ const { siteCascadedData, pageFetchedData } = await executeAllFetches(siteContent, siteDir, onProgress)
506
+
507
+ // Expand dynamic pages (e.g., /blog/:slug → /blog/post-1, /blog/post-2)
508
+ if (siteContent.pages?.some(p => p.isDynamic)) {
509
+ onProgress('Expanding dynamic routes...')
510
+ siteContent.pages = expandDynamicPages(siteContent.pages, pageFetchedData, onProgress)
511
+ }
512
+
513
+ // Load the HTML shell for this locale
514
+ const shellPath = existsSync(htmlPath) ? htmlPath : join(distDir, 'index.html')
515
+ const htmlShell = await readFile(shellPath, 'utf8')
516
+
517
+ // Initialize the Uniweb runtime for this locale
518
+ onProgress('Initializing runtime...')
519
+ const uniweb = createUniweb(siteContent)
520
+ uniweb.setFoundation(foundation)
521
+
522
+ // Set foundation capabilities (Layout, props, etc.)
523
+ if (foundation.capabilities) {
524
+ uniweb.setFoundationConfig(foundation.capabilities)
482
525
  }
483
526
 
484
- // Inject into shell
485
- const html = injectContent(htmlShell, renderedContent, page, siteContent)
527
+ // Pre-render each page
528
+ const pages = uniweb.activeWebsite.pages
529
+ const website = uniweb.activeWebsite
486
530
 
487
- // Output to the canonical route
488
- const outputPath = getOutputPath(distDir, page.route)
489
- await mkdir(dirname(outputPath), { recursive: true })
490
- await writeFile(outputPath, html)
531
+ for (const page of pages) {
532
+ // Build the output route with locale prefix
533
+ const outputRoute = routePrefix + page.route
491
534
 
492
- renderedFiles.push(outputPath)
493
- onProgress(` → ${outputPath.replace(distDir, 'dist')}`)
535
+ onProgress(`Rendering ${outputRoute}...`)
536
+
537
+ // Set this as the active page
538
+ uniweb.activeWebsite.setActivePage(page.route)
539
+
540
+ // Create the page element using inline SSR rendering
541
+ const element = createPageElement(page, website)
542
+
543
+ // Render to HTML string
544
+ let renderedContent
545
+ try {
546
+ renderedContent = renderToString(element)
547
+ } catch (err) {
548
+ console.warn(`Warning: Failed to render ${outputRoute}: ${err.message}`)
549
+ if (process.env.DEBUG) {
550
+ console.error(err.stack)
551
+ }
552
+ continue
553
+ }
554
+
555
+ // Inject into shell
556
+ const html = injectContent(htmlShell, renderedContent, page, siteContent)
557
+
558
+ // Output to the locale-prefixed route
559
+ const outputPath = getOutputPath(distDir, outputRoute)
560
+ await mkdir(dirname(outputPath), { recursive: true })
561
+ await writeFile(outputPath, html)
562
+
563
+ renderedFiles.push(outputPath)
564
+ onProgress(` → ${outputPath.replace(distDir, 'dist')}`)
565
+ }
494
566
  }
495
567
 
496
- onProgress(`Pre-rendered ${renderedFiles.length} pages`)
568
+ onProgress(`\nPre-rendered ${renderedFiles.length} pages across ${localeConfigs.length} locale(s)`)
497
569
 
498
570
  return {
499
571
  pages: renderedFiles.length,
@@ -523,7 +523,6 @@ async function processPage(pagePath, pageName, siteRoot, { isIndex = false, pare
523
523
  title: pageConfig.title || pageName,
524
524
  description: pageConfig.description || '',
525
525
  label: pageConfig.label || null, // Short label for navigation (defaults to title)
526
- order: pageConfig.order,
527
526
  lastModified: lastModified?.toISOString(),
528
527
 
529
528
  // Dynamic route metadata
@@ -599,8 +598,9 @@ function determineIndexPage(orderConfig, availableFolders) {
599
598
 
600
599
  const sorted = [...staticFolders].sort((a, b) => {
601
600
  // Sort by order (lower first), then alphabetically
602
- const orderA = a.order ?? 999
603
- const orderB = b.order ?? 999
601
+ // Pages without explicit order come after ordered pages
602
+ const orderA = a.order ?? Infinity
603
+ const orderB = b.order ?? Infinity
604
604
  if (orderA !== orderB) return orderA - orderB
605
605
  return a.name.localeCompare(b.name)
606
606
  })
@@ -655,6 +655,15 @@ async function collectPagesRecursive(dirPath, parentRoute, siteRoot, orderConfig
655
655
  })
656
656
  }
657
657
 
658
+ // Sort page folders by order (ascending), then alphabetically
659
+ // Pages without explicit order come after ordered pages (order ?? Infinity)
660
+ pageFolders.sort((a, b) => {
661
+ const orderA = a.order ?? Infinity
662
+ const orderB = b.order ?? Infinity
663
+ if (orderA !== orderB) return orderA - orderB
664
+ return a.name.localeCompare(b.name)
665
+ })
666
+
658
667
  // Check if this directory contains version folders (versioned section)
659
668
  const folderNames = pageFolders.map(f => f.name)
660
669
  const detectedVersions = detectVersions(folderNames)
@@ -93,16 +93,20 @@ async function processDevSectionFetches(sections, cascadedData, fetchOptions) {
93
93
  if (sectionFetch) {
94
94
  const result = await executeFetch(sectionFetch, fetchOptions)
95
95
  if (result.data && !result.error) {
96
- // Merge fetched data into cascadedData for this section
97
- cascadedData = {
98
- ...cascadedData,
99
- [sectionFetch.schema]: result.data
100
- }
96
+ // Merge fetched data into section's parsedContent (not cascadedData)
97
+ // This matches prerender behavior - section's own fetch goes to content.data
98
+ section.parsedContent = mergeDataIntoContent(
99
+ section.parsedContent || {},
100
+ result.data,
101
+ sectionFetch.schema,
102
+ sectionFetch.merge
103
+ )
101
104
  }
102
105
  }
103
106
 
104
- // Attach cascaded data to section
105
- section.cascadedData = { ...cascadedData }
107
+ // Attach cascaded data for components with inheritData
108
+ // Note: cascadedData is from page/site level only, not section's own fetch
109
+ section.cascadedData = cascadedData
106
110
 
107
111
  // Process subsections recursively
108
112
  if (section.subsections && section.subsections.length > 0) {