@uniweb/build 0.8.12 → 0.8.14

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.8.12",
3
+ "version": "0.8.14",
4
4
  "description": "Build tooling for the Uniweb Component Web Platform",
5
5
  "type": "module",
6
6
  "exports": {
@@ -11,6 +11,7 @@
11
11
  "./vite-plugin": "./src/vite-foundation-plugin.js",
12
12
  "./foundation": "./src/foundation/index.js",
13
13
  "./foundation/config": "./src/foundation/config.js",
14
+ "./content": "./src/content/index.js",
14
15
  "./site": "./src/site/index.js",
15
16
  "./site/config": "./src/site/config.js",
16
17
  "./dev": "./src/dev/index.js",
@@ -47,11 +48,12 @@
47
48
  },
48
49
  "dependencies": {
49
50
  "js-yaml": "^4.1.0",
50
- "sharp": "^0.33.2"
51
+ "sharp": "^0.33.2",
52
+ "@uniweb/theming": "0.1.1"
51
53
  },
52
54
  "optionalDependencies": {
53
55
  "@uniweb/content-reader": "1.1.4",
54
- "@uniweb/runtime": "0.6.8",
56
+ "@uniweb/runtime": "0.6.10",
55
57
  "@uniweb/schemas": "0.2.1"
56
58
  },
57
59
  "peerDependencies": {
@@ -61,7 +63,7 @@
61
63
  "@tailwindcss/vite": "^4.0.0",
62
64
  "@vitejs/plugin-react": "^4.0.0 || ^5.0.0",
63
65
  "vite-plugin-svgr": "^4.0.0",
64
- "@uniweb/core": "0.5.9"
66
+ "@uniweb/core": "0.5.10"
65
67
  },
66
68
  "peerDependenciesMeta": {
67
69
  "vite": {
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Clean Content Entry Point
3
+ *
4
+ * Exposes only collectSiteContent and its clean dependency chain
5
+ * (Node builtins, js-yaml, @uniweb/theming). No Vite, sharp, or React.
6
+ *
7
+ * Used by Studio's sidecar (Bun-compiled binary) where those heavy
8
+ * peer/native dependencies aren't available.
9
+ *
10
+ * @module @uniweb/build/content
11
+ */
12
+
13
+ export { collectSiteContent } from '../site/content-collector.js'
@@ -37,13 +37,18 @@ export function extractTranslatableContent(siteContent) {
37
37
  }
38
38
  }
39
39
 
40
- // Extract from shared layout pages (header, footer, left, right panels)
41
- for (const layoutKey of ['header', 'footer', 'left', 'right']) {
42
- const layoutPage = siteContent[layoutKey]
43
- if (layoutPage?.sections) {
44
- const pageRoute = layoutPage.route || `/layout/${layoutKey}`
45
- for (const section of layoutPage.sections) {
46
- extractFromSection(section, pageRoute, units)
40
+ // Extract from layout areas (header, footer, left, right panels)
41
+ // Layouts are nested under siteContent.layouts: { default: { header, footer, ... }, marketing: { ... } }
42
+ if (siteContent.layouts) {
43
+ for (const [layoutName, areas] of Object.entries(siteContent.layouts)) {
44
+ if (!areas || typeof areas !== 'object') continue
45
+ for (const [areaKey, layoutPage] of Object.entries(areas)) {
46
+ if (layoutPage?.sections) {
47
+ const pageRoute = layoutPage.route || `/layout/${layoutName === 'default' ? '' : layoutName + '/'}${areaKey}`
48
+ for (const section of layoutPage.sections) {
49
+ extractFromSection(section, pageRoute, units)
50
+ }
51
+ }
47
52
  }
48
53
  }
49
54
  }
package/src/i18n/merge.js CHANGED
@@ -76,13 +76,18 @@ function mergeTranslationsSync(siteContent, translations, fallbackToSource) {
76
76
  }
77
77
  }
78
78
 
79
- // Translate shared layout sections (header, footer, sidebars)
80
- for (const layoutKey of ['header', 'footer', 'left', 'right']) {
81
- const layoutPage = translated[layoutKey]
82
- if (layoutPage?.sections) {
83
- const pageRoute = layoutPage.route || `/layout/${layoutKey}`
84
- for (const section of layoutPage.sections) {
85
- translateSectionSync(section, pageRoute, translations, fallbackToSource)
79
+ // Translate layout sections (header, footer, sidebars)
80
+ // Layouts are nested under translated.layouts: { default: { header, footer, ... }, marketing: { ... } }
81
+ if (translated.layouts) {
82
+ for (const [layoutName, areas] of Object.entries(translated.layouts)) {
83
+ if (!areas || typeof areas !== 'object') continue
84
+ for (const [areaKey, layoutPage] of Object.entries(areas)) {
85
+ if (layoutPage?.sections) {
86
+ const pageRoute = layoutPage.route || `/layout/${layoutName === 'default' ? '' : layoutName + '/'}${areaKey}`
87
+ for (const section of layoutPage.sections) {
88
+ translateSectionSync(section, pageRoute, translations, fallbackToSource)
89
+ }
90
+ }
86
91
  }
87
92
  }
88
93
  }
@@ -128,18 +133,22 @@ async function mergeTranslationsAsync(siteContent, translations, options) {
128
133
  }
129
134
  }
130
135
 
131
- // Translate shared layout sections (header, footer, sidebars)
132
- for (const layoutKey of ['header', 'footer', 'left', 'right']) {
133
- const layoutPage = translated[layoutKey]
134
- if (layoutPage?.sections) {
135
- // Ensure route is set for context matching
136
- if (!layoutPage.route) layoutPage.route = `/layout/${layoutKey}`
137
- for (const section of layoutPage.sections) {
138
- await translateSectionAsync(section, layoutPage, translations, {
139
- fallbackToSource,
140
- locale,
141
- localesDir
142
- })
136
+ // Translate layout sections (header, footer, sidebars)
137
+ // Layouts are nested under translated.layouts: { default: { header, footer, ... }, marketing: { ... } }
138
+ if (translated.layouts) {
139
+ for (const [layoutName, areas] of Object.entries(translated.layouts)) {
140
+ if (!areas || typeof areas !== 'object') continue
141
+ for (const [areaKey, layoutPage] of Object.entries(areas)) {
142
+ if (layoutPage?.sections) {
143
+ if (!layoutPage.route) layoutPage.route = `/layout/${layoutName === 'default' ? '' : layoutName + '/'}${areaKey}`
144
+ for (const section of layoutPage.sections) {
145
+ await translateSectionAsync(section, layoutPage, translations, {
146
+ fallbackToSource,
147
+ locale,
148
+ localesDir
149
+ })
150
+ }
151
+ }
143
152
  }
144
153
  }
145
154
  }
@@ -56,7 +56,7 @@ function isPdfPath(src) {
56
56
  * @param {string} value - String to check
57
57
  * @returns {boolean} True if it looks like a local asset path
58
58
  */
59
- function isLocalAssetPath(value) {
59
+ export function isLocalAssetPath(value) {
60
60
  if (typeof value !== 'string' || !value) return false
61
61
 
62
62
  // Skip external URLs
@@ -30,7 +30,7 @@ import { join, basename, extname, dirname, relative, resolve } from 'node:path'
30
30
  import { existsSync } from 'node:fs'
31
31
  import yaml from 'js-yaml'
32
32
  import { applyFilter, applySort } from './data-fetcher.js'
33
- import { resolveAssetPath, walkContentAssets } from './assets.js'
33
+ import { resolveAssetPath, walkContentAssets, isLocalAssetPath } from './assets.js'
34
34
 
35
35
  // Try to import content-reader for markdown parsing
36
36
  let markdownToProseMirror
@@ -222,7 +222,7 @@ function isExternalUrl(src) {
222
222
  * @param {string} collectionName - Name of the collection (e.g., 'articles')
223
223
  * @returns {Promise<Object>} Asset manifest for this item
224
224
  */
225
- async function processCollectionAssets(content, itemPath, siteRoot, collectionName) {
225
+ async function processCollectionAssets(content, itemPath, siteRoot, collectionName, basePath) {
226
226
  const assets = {}
227
227
  const itemDir = dirname(itemPath)
228
228
  const publicDir = join(siteRoot, 'public')
@@ -259,7 +259,7 @@ async function processCollectionAssets(content, itemPath, siteRoot, collectionNa
259
259
  await copyFile(result.resolved, targetPath)
260
260
 
261
261
  // Update path to site-root-relative
262
- finalPath = `/collections/${collectionName}/${assetFilename}`
262
+ finalPath = `${basePath}collections/${collectionName}/${assetFilename}`
263
263
 
264
264
  assets[src] = {
265
265
  original: src,
@@ -294,7 +294,7 @@ async function processCollectionAssets(content, itemPath, siteRoot, collectionNa
294
294
  const posterTarget = join(targetDir, posterFilename)
295
295
  await mkdir(targetDir, { recursive: true })
296
296
  await copyFile(posterResult.resolved, posterTarget)
297
- node.attrs.poster = `/collections/${collectionName}/${posterFilename}`
297
+ node.attrs.poster = `${basePath}collections/${collectionName}/${posterFilename}`
298
298
  }
299
299
  }
300
300
 
@@ -305,7 +305,7 @@ async function processCollectionAssets(content, itemPath, siteRoot, collectionNa
305
305
  const previewTarget = join(targetDir, previewFilename)
306
306
  await mkdir(targetDir, { recursive: true })
307
307
  await copyFile(previewResult.resolved, previewTarget)
308
- node.attrs.preview = `/collections/${collectionName}/${previewFilename}`
308
+ node.attrs.preview = `${basePath}collections/${collectionName}/${previewFilename}`
309
309
  }
310
310
  }
311
311
  }
@@ -313,6 +313,53 @@ async function processCollectionAssets(content, itemPath, siteRoot, collectionNa
313
313
  return assets
314
314
  }
315
315
 
316
+ /**
317
+ * Process assets in a data item (YAML/JSON)
318
+ * - Recursively walks the data object looking for local asset paths
319
+ * - Copies co-located assets to public/collections/<collection>/
320
+ * - Rewrites paths to absolute URLs (with base path)
321
+ *
322
+ * @param {Object} data - Parsed data object (mutated in place)
323
+ * @param {string} itemPath - Path to the data file
324
+ * @param {string} siteRoot - Site root directory
325
+ * @param {string} collectionName - Name of the collection
326
+ * @param {string} basePath - Site base path (e.g., '/' or '/docs/')
327
+ */
328
+ async function processDataItemAssets(data, itemPath, siteRoot, collectionName, basePath) {
329
+ const targetDir = join(siteRoot, 'public', 'collections', collectionName)
330
+
331
+ async function walk(parent, key) {
332
+ const val = parent[key]
333
+ if (typeof val === 'string' && isLocalAssetPath(val)) {
334
+ if (val.startsWith('./') || val.startsWith('../')) {
335
+ const resolved = resolve(dirname(itemPath), val)
336
+ if (existsSync(resolved)) {
337
+ const filename = basename(resolved)
338
+ await mkdir(targetDir, { recursive: true })
339
+ await copyFile(resolved, join(targetDir, filename))
340
+ parent[key] = `${basePath}collections/${collectionName}/${filename}`
341
+ }
342
+ } else if (val.startsWith('/')) {
343
+ // Absolute site path — just prepend base
344
+ parent[key] = `${basePath}${val.slice(1)}`
345
+ }
346
+ return
347
+ }
348
+ if (Array.isArray(val)) {
349
+ for (let i = 0; i < val.length; i++) await walk(val, i)
350
+ return
351
+ }
352
+ if (val && typeof val === 'object') {
353
+ for (const k of Object.keys(val)) await walk(val, k)
354
+ }
355
+ }
356
+
357
+ for (const key of Object.keys(data)) {
358
+ if (key === 'slug') continue
359
+ await walk(data, key)
360
+ }
361
+ }
362
+
316
363
  // Filter and sort utilities are imported from data-fetcher.js
317
364
 
318
365
  /**
@@ -325,7 +372,7 @@ async function processCollectionAssets(content, itemPath, siteRoot, collectionNa
325
372
  * @param {string} filename - YAML filename (.yml or .yaml)
326
373
  * @returns {Promise<Object|null>} Processed item or null if unpublished
327
374
  */
328
- async function processDataItem(dir, filename) {
375
+ async function processDataItem(dir, filename, siteRoot, collectionName, basePath) {
329
376
  const filepath = join(dir, filename)
330
377
  const raw = await readFile(filepath, 'utf-8')
331
378
  const slug = basename(filename, extname(filename))
@@ -334,7 +381,9 @@ async function processDataItem(dir, filename) {
334
381
  // Skip unpublished items
335
382
  if (data.published === false) return null
336
383
 
337
- return { slug, ...data }
384
+ const item = { slug, ...data }
385
+ await processDataItemAssets(item, filepath, siteRoot, collectionName, basePath)
386
+ return item
338
387
  }
339
388
 
340
389
  /**
@@ -348,18 +397,27 @@ async function processDataItem(dir, filename) {
348
397
  * @param {string} filename - JSON filename
349
398
  * @returns {Promise<Object|Array|null>} Processed item(s) or null if unpublished
350
399
  */
351
- async function processJsonItem(dir, filename) {
400
+ async function processJsonItem(dir, filename, siteRoot, collectionName, basePath) {
352
401
  const filepath = join(dir, filename)
353
402
  const raw = await readFile(filepath, 'utf-8')
354
403
  const slug = basename(filename, '.json')
355
404
  const data = JSON.parse(raw)
356
405
 
357
406
  // Array → multiple items (single-file collection)
358
- if (Array.isArray(data)) return data
407
+ if (Array.isArray(data)) {
408
+ for (const item of data) {
409
+ if (item && typeof item === 'object') {
410
+ await processDataItemAssets(item, filepath, siteRoot, collectionName, basePath)
411
+ }
412
+ }
413
+ return data
414
+ }
359
415
 
360
416
  // Object → single item
361
417
  if (data.published === false) return null
362
- return { slug, ...data }
418
+ const item = { slug, ...data }
419
+ await processDataItemAssets(item, filepath, siteRoot, collectionName, basePath)
420
+ return item
363
421
  }
364
422
 
365
423
  /**
@@ -371,7 +429,7 @@ async function processJsonItem(dir, filename) {
371
429
  * @param {string} siteRoot - Site root directory for asset resolution
372
430
  * @returns {Promise<Object|null>} Processed item or null if unpublished
373
431
  */
374
- async function processContentItem(dir, filename, config, siteRoot) {
432
+ async function processContentItem(dir, filename, config, siteRoot, basePath) {
375
433
  const filepath = join(dir, filename)
376
434
  const raw = await readFile(filepath, 'utf-8')
377
435
  const slug = basename(filename, extname(filename))
@@ -389,7 +447,7 @@ async function processContentItem(dir, filename, config, siteRoot) {
389
447
 
390
448
  // Process assets (resolve paths, copy co-located files)
391
449
  // This modifies content in place, updating paths to site-root-relative
392
- await processCollectionAssets(content, filepath, siteRoot, config.name)
450
+ await processCollectionAssets(content, filepath, siteRoot, config.name, basePath)
393
451
 
394
452
  // Extract excerpt
395
453
  const excerpt = extractExcerpt(frontmatter, content, config.excerpt)
@@ -398,19 +456,12 @@ async function processContentItem(dir, filename, config, siteRoot) {
398
456
  // Note: paths in content have already been updated by processCollectionAssets
399
457
  const image = frontmatter.image || extractFirstImage(content)
400
458
 
401
- // Get file stats for lastModified
402
- const fileStat = await stat(filepath)
403
-
404
459
  return {
405
460
  slug,
406
461
  ...frontmatter,
407
462
  excerpt,
408
463
  image,
409
- // Include both raw markdown body (for simple rendering)
410
- // and ProseMirror content (for rich rendering)
411
- body: body.trim(),
412
- content,
413
- lastModified: fileStat.mtime.toISOString()
464
+ content
414
465
  }
415
466
  }
416
467
 
@@ -421,7 +472,7 @@ async function processContentItem(dir, filename, config, siteRoot) {
421
472
  * @param {Object} config - Parsed collection config
422
473
  * @returns {Promise<Array>} Array of processed items
423
474
  */
424
- async function collectItems(siteDir, config, collectionsBase) {
475
+ async function collectItems(siteDir, config, collectionsBase, basePath) {
425
476
  const base = collectionsBase || siteDir
426
477
  const collectionDir = resolve(base, config.path)
427
478
 
@@ -441,12 +492,12 @@ async function collectItems(siteDir, config, collectionsBase) {
441
492
  let items = await Promise.all(
442
493
  itemFiles.map(file => {
443
494
  if (file.endsWith('.json')) {
444
- return processJsonItem(collectionDir, file)
495
+ return processJsonItem(collectionDir, file, siteDir, config.name, basePath)
445
496
  }
446
497
  if (file.endsWith('.yml') || file.endsWith('.yaml')) {
447
- return processDataItem(collectionDir, file)
498
+ return processDataItem(collectionDir, file, siteDir, config.name, basePath)
448
499
  }
449
- return processContentItem(collectionDir, file, config, siteDir)
500
+ return processContentItem(collectionDir, file, config, siteDir, basePath)
450
501
  })
451
502
  )
452
503
 
@@ -497,7 +548,7 @@ async function collectItems(siteDir, config, collectionsBase) {
497
548
  * })
498
549
  * // { articles: [...], products: [...] }
499
550
  */
500
- export async function processCollections(siteDir, collectionsConfig, collectionsBase) {
551
+ export async function processCollections(siteDir, collectionsConfig, collectionsBase, basePath = '/') {
501
552
  if (!collectionsConfig || typeof collectionsConfig !== 'object') {
502
553
  return {}
503
554
  }
@@ -506,7 +557,7 @@ export async function processCollections(siteDir, collectionsConfig, collections
506
557
 
507
558
  for (const [name, config] of Object.entries(collectionsConfig)) {
508
559
  const parsed = parseCollectionConfig(name, config)
509
- const items = await collectItems(siteDir, parsed, collectionsBase)
560
+ const items = await collectItems(siteDir, parsed, collectionsBase, basePath)
510
561
  results[name] = items
511
562
  console.log(`[collection-processor] Processed ${name}: ${items.length} items`)
512
563
  }
@@ -381,6 +381,7 @@ export function siteContentPlugin(options = {}) {
381
381
  let resolvedLayoutPath = null // Resolved from site.yml layoutDir or default
382
382
  let resolvedCollectionsBase = null // Resolved from site.yml collectionsDir
383
383
  let headHtml = '' // Contents of site/head.html for injection
384
+ let basePath = '/' // Vite's config.base, always has trailing slash
384
385
 
385
386
  /**
386
387
  * Load translations for a specific locale
@@ -494,6 +495,7 @@ export function siteContentPlugin(options = {}) {
494
495
  resolvedSitePath = resolve(config.root, sitePath)
495
496
  resolvedOutDir = resolve(config.root, config.build.outDir)
496
497
  isProduction = config.command === 'build'
498
+ basePath = config.base || '/'
497
499
 
498
500
  // In dev mode, process collections early so JSON files exist before server starts
499
501
  // This runs before configureServer, ensuring data is available immediately
@@ -517,7 +519,7 @@ export function siteContentPlugin(options = {}) {
517
519
 
518
520
  if (collectionsConfig) {
519
521
  console.log('[site-content] Processing content collections...')
520
- const collections = await processCollections(resolvedSitePath, collectionsConfig, resolvedCollectionsBase)
522
+ const collections = await processCollections(resolvedSitePath, collectionsConfig, resolvedCollectionsBase, basePath)
521
523
  await writeCollectionFiles(resolvedSitePath, collections)
522
524
  }
523
525
  } catch (err) {
@@ -554,7 +556,7 @@ export function siteContentPlugin(options = {}) {
554
556
  // In production, do it here
555
557
  if (isProduction && siteContent.config?.collections) {
556
558
  console.log('[site-content] Processing content collections...')
557
- const collections = await processCollections(resolvedSitePath, siteContent.config.collections, resolvedCollectionsBase)
559
+ const collections = await processCollections(resolvedSitePath, siteContent.config.collections, resolvedCollectionsBase, basePath)
558
560
  await writeCollectionFiles(resolvedSitePath, collections)
559
561
  }
560
562
 
@@ -617,7 +619,7 @@ export function siteContentPlugin(options = {}) {
617
619
  // Use collectionsConfig (cached from configResolved) or siteContent
618
620
  const collections = collectionsConfig || siteContent?.config?.collections
619
621
  if (collections) {
620
- const processed = await processCollections(resolvedSitePath, collections, resolvedCollectionsBase)
622
+ const processed = await processCollections(resolvedSitePath, collections, resolvedCollectionsBase, basePath)
621
623
  await writeCollectionFiles(resolvedSitePath, processed)
622
624
  }
623
625
  // Send full reload to client
@@ -1,65 +1,10 @@
1
1
  /**
2
- * Theme Module
2
+ * Theme Module — re-export shim
3
3
  *
4
- * Exports all theme-related utilities for the build process.
4
+ * All theming logic has moved to @uniweb/theming.
5
+ * This shim preserves backward compatibility for internal imports.
5
6
  *
6
7
  * @module @uniweb/build/theme
7
8
  */
8
9
 
9
- // Shade generation
10
- export {
11
- parseColor,
12
- formatOklch,
13
- formatHex,
14
- generateShades,
15
- generatePalettes,
16
- isValidColor,
17
- getShadeLevels,
18
- } from './shade-generator.js'
19
-
20
- // CSS generation
21
- export {
22
- generateThemeCSS,
23
- generateContextCSS,
24
- generatePaletteVars,
25
- getDefaultContextTokens,
26
- getDefaultColors,
27
- } from './css-generator.js'
28
-
29
- // Theme processing
30
- export {
31
- validateThemeConfig,
32
- processTheme,
33
- extractFoundationVars,
34
- foundationHasVars,
35
- } from './processor.js'
36
-
37
- // Default export for convenience
38
- import { processTheme } from './processor.js'
39
- import { generateThemeCSS } from './css-generator.js'
40
-
41
- /**
42
- * Process theme configuration and generate CSS in one step
43
- *
44
- * @param {Object} themeYml - Raw theme.yml content
45
- * @param {Object} options - Processing options
46
- * @param {Object} options.foundationVars - Foundation variables
47
- * @returns {{ css: string, config: Object, errors: string[], warnings: string[] }}
48
- */
49
- export function buildTheme(themeYml = {}, options = {}) {
50
- const { config, errors, warnings } = processTheme(themeYml, options)
51
- const css = generateThemeCSS(config)
52
-
53
- return {
54
- css,
55
- config,
56
- errors,
57
- warnings,
58
- }
59
- }
60
-
61
- export default {
62
- buildTheme,
63
- processTheme,
64
- generateThemeCSS,
65
- }
10
+ export * from '@uniweb/theming'