@uniweb/build 0.4.0 → 0.4.1

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.4.0",
3
+ "version": "0.4.1",
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.1.0",
54
- "@uniweb/runtime": "0.4.4"
53
+ "@uniweb/content-reader": "1.1.1",
54
+ "@uniweb/runtime": "0.5.0"
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.3.0"
63
+ "@uniweb/core": "0.3.1"
64
64
  },
65
65
  "peerDependenciesMeta": {
66
66
  "vite": {
@@ -27,6 +27,7 @@ import { join, parse } from 'node:path'
27
27
  import { existsSync } from 'node:fs'
28
28
  import yaml from 'js-yaml'
29
29
  import { collectSectionAssets, mergeAssetCollections } from './assets.js'
30
+ import { collectSectionIcons, mergeIconCollections, buildIconManifest } from './icons.js'
30
31
  import { parseFetchConfig, singularize } from './data-fetcher.js'
31
32
  import { buildTheme, extractFoundationVars } from '../theme/index.js'
32
33
 
@@ -280,7 +281,10 @@ async function processMarkdownFile(filePath, id, siteRoot, defaultStableId = nul
280
281
  // Collect assets referenced in this section
281
282
  const assetCollection = collectSectionAssets(section, filePath, siteRoot)
282
283
 
283
- return { section, assetCollection }
284
+ // Collect icons referenced in this section
285
+ const iconCollection = collectSectionIcons(section, filePath)
286
+
287
+ return { section, assetCollection, iconCollection }
284
288
  }
285
289
 
286
290
  /**
@@ -335,7 +339,7 @@ function buildSectionHierarchy(sections) {
335
339
  * @param {string} pagePath - Path to page directory
336
340
  * @param {string} siteRoot - Site root for asset resolution
337
341
  * @param {string} parentId - Parent section ID for building hierarchy
338
- * @returns {Object} { sections, assetCollection, lastModified }
342
+ * @returns {Object} { sections, assetCollection, iconCollection, lastModified }
339
343
  */
340
344
  async function processExplicitSections(sectionsConfig, pagePath, siteRoot, parentId = '') {
341
345
  const sections = []
@@ -344,6 +348,10 @@ async function processExplicitSections(sectionsConfig, pagePath, siteRoot, paren
344
348
  hasExplicitPoster: new Set(),
345
349
  hasExplicitPreview: new Set()
346
350
  }
351
+ let iconCollection = {
352
+ icons: new Set(),
353
+ bySource: new Map()
354
+ }
347
355
  let lastModified = null
348
356
 
349
357
  let index = 1
@@ -381,8 +389,9 @@ async function processExplicitSections(sectionsConfig, pagePath, siteRoot, paren
381
389
 
382
390
  // Process the section
383
391
  // Use sectionName as stable ID for scroll targeting (e.g., "hero", "features")
384
- const { section, assetCollection: sectionAssets } = await processMarkdownFile(filePath, id, siteRoot, sectionName)
392
+ const { section, assetCollection: sectionAssets, iconCollection: sectionIcons } = await processMarkdownFile(filePath, id, siteRoot, sectionName)
385
393
  assetCollection = mergeAssetCollections(assetCollection, sectionAssets)
394
+ iconCollection = mergeIconCollections(iconCollection, sectionIcons)
386
395
 
387
396
  // Track last modified
388
397
  const fileStat = await stat(filePath)
@@ -395,6 +404,7 @@ async function processExplicitSections(sectionsConfig, pagePath, siteRoot, paren
395
404
  const subResult = await processExplicitSections(subsections, pagePath, siteRoot, id)
396
405
  section.subsections = subResult.sections
397
406
  assetCollection = mergeAssetCollections(assetCollection, subResult.assetCollection)
407
+ iconCollection = mergeIconCollections(iconCollection, subResult.iconCollection)
398
408
  if (subResult.lastModified && (!lastModified || subResult.lastModified > lastModified)) {
399
409
  lastModified = subResult.lastModified
400
410
  }
@@ -404,7 +414,7 @@ async function processExplicitSections(sectionsConfig, pagePath, siteRoot, paren
404
414
  index++
405
415
  }
406
416
 
407
- return { sections, assetCollection, lastModified }
417
+ return { sections, assetCollection, iconCollection, lastModified }
408
418
  }
409
419
 
410
420
  /**
@@ -433,6 +443,10 @@ async function processPage(pagePath, pageName, siteRoot, { isIndex = false, pare
433
443
  hasExplicitPoster: new Set(),
434
444
  hasExplicitPreview: new Set()
435
445
  }
446
+ let pageIconCollection = {
447
+ icons: new Set(),
448
+ bySource: new Map()
449
+ }
436
450
  let lastModified = null
437
451
 
438
452
  // Check for explicit sections configuration
@@ -452,9 +466,10 @@ async function processPage(pagePath, pageName, siteRoot, { isIndex = false, pare
452
466
  // e.g., "1-intro.md" → stableId: "intro", "2-features.md" → stableId: "features"
453
467
  const stableId = stableName || name
454
468
 
455
- const { section, assetCollection } = await processMarkdownFile(join(pagePath, file), id, siteRoot, stableId)
469
+ const { section, assetCollection, iconCollection } = await processMarkdownFile(join(pagePath, file), id, siteRoot, stableId)
456
470
  sections.push(section)
457
471
  pageAssetCollection = mergeAssetCollections(pageAssetCollection, assetCollection)
472
+ pageIconCollection = mergeIconCollections(pageIconCollection, iconCollection)
458
473
 
459
474
  // Track last modified time for sitemap
460
475
  const fileStat = await stat(join(pagePath, file))
@@ -471,6 +486,7 @@ async function processPage(pagePath, pageName, siteRoot, { isIndex = false, pare
471
486
  const result = await processExplicitSections(sectionsConfig, pagePath, siteRoot)
472
487
  hierarchicalSections = result.sections
473
488
  pageAssetCollection = result.assetCollection
489
+ pageIconCollection = result.iconCollection
474
490
  lastModified = result.lastModified
475
491
 
476
492
  } else {
@@ -566,7 +582,8 @@ async function processPage(pagePath, pageName, siteRoot, { isIndex = false, pare
566
582
 
567
583
  sections: hierarchicalSections
568
584
  },
569
- assetCollection: pageAssetCollection
585
+ assetCollection: pageAssetCollection,
586
+ iconCollection: pageIconCollection
570
587
  }
571
588
  }
572
589
 
@@ -617,7 +634,7 @@ function determineIndexPage(orderConfig, availableFolders) {
617
634
  * @param {Object} orderConfig - { pages: [...], index: 'name' } from parent's config
618
635
  * @param {Object} parentFetch - Parent page's fetch config (for dynamic child routes)
619
636
  * @param {Object} versionContext - Version context from parent { version, versionMeta }
620
- * @returns {Promise<Object>} { pages, assetCollection, header, footer, left, right, notFound, versionedScopes }
637
+ * @returns {Promise<Object>} { pages, assetCollection, iconCollection, header, footer, left, right, notFound, versionedScopes }
621
638
  */
622
639
  async function collectPagesRecursive(dirPath, parentRoute, siteRoot, orderConfig = {}, parentFetch = null, versionContext = null) {
623
640
  const entries = await readdir(dirPath)
@@ -627,6 +644,10 @@ async function collectPagesRecursive(dirPath, parentRoute, siteRoot, orderConfig
627
644
  hasExplicitPoster: new Set(),
628
645
  hasExplicitPreview: new Set()
629
646
  }
647
+ let iconCollection = {
648
+ icons: new Set(),
649
+ bySource: new Map()
650
+ }
630
651
  let header = null
631
652
  let footer = null
632
653
  let left = null
@@ -711,6 +732,7 @@ async function collectPagesRecursive(dirPath, parentRoute, siteRoot, orderConfig
711
732
 
712
733
  pages.push(...subResult.pages)
713
734
  assetCollection = mergeAssetCollections(assetCollection, subResult.assetCollection)
735
+ iconCollection = mergeIconCollections(iconCollection, subResult.iconCollection)
714
736
  // Merge any nested versioned scopes (shouldn't happen often, but possible)
715
737
  for (const [scope, meta] of subResult.versionedScopes) {
716
738
  versionedScopes.set(scope, meta)
@@ -727,12 +749,13 @@ async function collectPagesRecursive(dirPath, parentRoute, siteRoot, orderConfig
727
749
  if (result) {
728
750
  pages.push(result.page)
729
751
  assetCollection = mergeAssetCollections(assetCollection, result.assetCollection)
752
+ iconCollection = mergeIconCollections(iconCollection, result.iconCollection)
730
753
  }
731
754
  }
732
755
  }
733
756
 
734
757
  // Return early - we've handled all children
735
- return { pages, assetCollection, header, footer, left, right, notFound, versionedScopes }
758
+ return { pages, assetCollection, iconCollection, header, footer, left, right, notFound, versionedScopes }
736
759
  }
737
760
 
738
761
  // Determine which page is the index for this level
@@ -755,8 +778,9 @@ async function collectPagesRecursive(dirPath, parentRoute, siteRoot, orderConfig
755
778
  })
756
779
 
757
780
  if (result) {
758
- const { page, assetCollection: pageAssets } = result
781
+ const { page, assetCollection: pageAssets, iconCollection: pageIcons } = result
759
782
  assetCollection = mergeAssetCollections(assetCollection, pageAssets)
783
+ iconCollection = mergeIconCollections(iconCollection, pageIcons)
760
784
 
761
785
  // Handle special pages (layout areas and 404) - only at root level
762
786
  if (parentRoute === '/') {
@@ -787,6 +811,7 @@ async function collectPagesRecursive(dirPath, parentRoute, siteRoot, orderConfig
787
811
  const subResult = await collectPagesRecursive(entryPath, childParentRoute, siteRoot, childOrderConfig, childFetch, versionContext)
788
812
  pages.push(...subResult.pages)
789
813
  assetCollection = mergeAssetCollections(assetCollection, subResult.assetCollection)
814
+ iconCollection = mergeIconCollections(iconCollection, subResult.iconCollection)
790
815
  // Merge any versioned scopes from children
791
816
  for (const [scope, meta] of subResult.versionedScopes) {
792
817
  versionedScopes.set(scope, meta)
@@ -795,7 +820,7 @@ async function collectPagesRecursive(dirPath, parentRoute, siteRoot, orderConfig
795
820
  }
796
821
  }
797
822
 
798
- return { pages, assetCollection, header, footer, left, right, notFound, versionedScopes }
823
+ return { pages, assetCollection, iconCollection, header, footer, left, right, notFound, versionedScopes }
799
824
  }
800
825
 
801
826
  /**
@@ -873,7 +898,7 @@ export async function collectSiteContent(sitePath, options = {}) {
873
898
  }
874
899
 
875
900
  // Recursively collect all pages
876
- const { pages, assetCollection, header, footer, left, right, notFound, versionedScopes } =
901
+ const { pages, assetCollection, iconCollection, header, footer, left, right, notFound, versionedScopes } =
877
902
  await collectPagesRecursive(pagesPath, '/', sitePath, siteOrderConfig)
878
903
 
879
904
  // Sort pages by order
@@ -886,6 +911,12 @@ export async function collectSiteContent(sitePath, options = {}) {
886
911
  console.log(`[content-collector] Found ${assetCount} asset references${explicitCount > 0 ? ` (${explicitCount} with explicit poster/preview)` : ''}`)
887
912
  }
888
913
 
914
+ // Build icon manifest from collected icons
915
+ const iconManifest = buildIconManifest(iconCollection)
916
+ if (iconManifest.count > 0) {
917
+ console.log(`[content-collector] Found ${iconManifest.count} icon references from ${iconManifest.families.length} families: ${iconManifest.families.join(', ')}`)
918
+ }
919
+
889
920
  // Convert versionedScopes Map to plain object for JSON serialization
890
921
  const versionedScopesObj = Object.fromEntries(versionedScopes)
891
922
 
@@ -908,7 +939,9 @@ export async function collectSiteContent(sitePath, options = {}) {
908
939
  versionedScopes: versionedScopesObj,
909
940
  assets: assetCollection.assets,
910
941
  hasExplicitPoster: assetCollection.hasExplicitPoster,
911
- hasExplicitPreview: assetCollection.hasExplicitPreview
942
+ hasExplicitPreview: assetCollection.hasExplicitPreview,
943
+ // Icon manifest for preloading
944
+ icons: iconManifest
912
945
  }
913
946
  }
914
947
 
@@ -0,0 +1,180 @@
1
+ /**
2
+ * Icon Collection Utilities
3
+ *
4
+ * Extracts icon references from ProseMirror content during build.
5
+ * This enables:
6
+ * - Preloading hints for faster icon loading
7
+ * - Build-time validation (warn about missing icons)
8
+ * - Tooling support (see which icons are used)
9
+ *
10
+ * Icons are stored as image nodes with:
11
+ * - role: "icon"
12
+ * - library: "lu" (or "lucide", "hi", etc.)
13
+ * - name: "house" (icon name)
14
+ */
15
+
16
+ /**
17
+ * Map friendly family names to short codes (react-icons format)
18
+ * Same mapping as runtime/content-reader for consistency
19
+ */
20
+ const FAMILY_MAP = {
21
+ lucide: 'lu',
22
+ heroicons: 'hi',
23
+ heroicons2: 'hi2',
24
+ phosphor: 'pi',
25
+ tabler: 'tb',
26
+ feather: 'fi',
27
+ fa: 'fa',
28
+ fa6: 'fa6',
29
+ bootstrap: 'bs',
30
+ 'material-design': 'md',
31
+ 'ant-design': 'ai',
32
+ remix: 'ri',
33
+ 'simple-icons': 'si',
34
+ vscode: 'vsc',
35
+ weather: 'wi',
36
+ game: 'gi',
37
+ // Direct codes map to themselves
38
+ lu: 'lu',
39
+ hi: 'hi',
40
+ hi2: 'hi2',
41
+ pi: 'pi',
42
+ tb: 'tb',
43
+ fi: 'fi',
44
+ bs: 'bs',
45
+ md: 'md',
46
+ ai: 'ai',
47
+ ri: 'ri',
48
+ si: 'si',
49
+ vsc: 'vsc',
50
+ wi: 'wi',
51
+ gi: 'gi'
52
+ }
53
+
54
+ /**
55
+ * Normalize a library name to its short code
56
+ * @param {string} library - Library name (e.g., "lucide" or "lu")
57
+ * @returns {string} Short code (e.g., "lu")
58
+ */
59
+ function normalizeLibrary(library) {
60
+ return FAMILY_MAP[library?.toLowerCase()] || library?.toLowerCase() || null
61
+ }
62
+
63
+ /**
64
+ * Walk a ProseMirror document and collect icon references
65
+ *
66
+ * @param {Object} doc - ProseMirror document
67
+ * @param {Function} visitor - Callback for each icon: (library, name) => void
68
+ */
69
+ export function walkContentIcons(doc, visitor) {
70
+ if (!doc) return
71
+
72
+ // Check for image nodes with role="icon"
73
+ if (doc.type === 'image' && doc.attrs?.role === 'icon') {
74
+ const { library, name } = doc.attrs
75
+ if (library && name) {
76
+ visitor(library, name)
77
+ }
78
+ }
79
+
80
+ // Recurse into content
81
+ if (doc.content && Array.isArray(doc.content)) {
82
+ doc.content.forEach(child => walkContentIcons(child, visitor))
83
+ }
84
+ }
85
+
86
+ /**
87
+ * Collect all icon references from a section's content
88
+ *
89
+ * @param {Object} section - Section object with content
90
+ * @param {string} sourcePath - Path to source file (for bySource tracking)
91
+ * @returns {Object} Icon collection result
92
+ * - icons: Set of normalized icon references (e.g., "lu:house")
93
+ * - bySource: Map of icon references to source files
94
+ */
95
+ export function collectSectionIcons(section, sourcePath) {
96
+ const icons = new Set()
97
+ const bySource = new Map()
98
+
99
+ if (section.content) {
100
+ walkContentIcons(section.content, (library, name) => {
101
+ const normalizedLibrary = normalizeLibrary(library)
102
+ if (!normalizedLibrary) return
103
+
104
+ const iconRef = `${normalizedLibrary}:${name}`
105
+ icons.add(iconRef)
106
+
107
+ // Track which files use this icon
108
+ if (!bySource.has(iconRef)) {
109
+ bySource.set(iconRef, [])
110
+ }
111
+ bySource.get(iconRef).push(sourcePath)
112
+ })
113
+ }
114
+
115
+ return { icons, bySource }
116
+ }
117
+
118
+ /**
119
+ * Merge multiple icon collection results
120
+ *
121
+ * @param {...Object} collections - Icon collection results
122
+ * @returns {Object} Merged collection
123
+ */
124
+ export function mergeIconCollections(...collections) {
125
+ const merged = {
126
+ icons: new Set(),
127
+ bySource: new Map()
128
+ }
129
+
130
+ for (const collection of collections) {
131
+ if (!collection) continue
132
+
133
+ // Merge icons set
134
+ if (collection.icons) {
135
+ collection.icons.forEach(icon => merged.icons.add(icon))
136
+ }
137
+
138
+ // Merge bySource map
139
+ if (collection.bySource) {
140
+ for (const [iconRef, sources] of collection.bySource) {
141
+ if (!merged.bySource.has(iconRef)) {
142
+ merged.bySource.set(iconRef, [])
143
+ }
144
+ merged.bySource.get(iconRef).push(...sources)
145
+ }
146
+ }
147
+ }
148
+
149
+ return merged
150
+ }
151
+
152
+ /**
153
+ * Build icon manifest from collected icons
154
+ *
155
+ * @param {Object} iconCollection - Merged icon collection
156
+ * @returns {Object} Icon manifest for site-content.json
157
+ */
158
+ export function buildIconManifest(iconCollection) {
159
+ const { icons, bySource } = iconCollection
160
+
161
+ // Get unique families used
162
+ const families = new Set()
163
+ for (const iconRef of icons) {
164
+ const [family] = iconRef.split(':')
165
+ families.add(family)
166
+ }
167
+
168
+ // Convert bySource Map to plain object for JSON serialization
169
+ const bySourceObj = {}
170
+ for (const [iconRef, sources] of bySource) {
171
+ bySourceObj[iconRef] = [...new Set(sources)] // dedupe sources
172
+ }
173
+
174
+ return {
175
+ used: [...icons].sort(),
176
+ families: [...families].sort(),
177
+ bySource: bySourceObj,
178
+ count: icons.size
179
+ }
180
+ }