@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 +4 -4
- package/src/site/content-collector.js +45 -12
- package/src/site/icons.js +180 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@uniweb/build",
|
|
3
|
-
"version": "0.4.
|
|
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.
|
|
54
|
-
"@uniweb/runtime": "0.
|
|
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.
|
|
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
|
-
|
|
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
|
+
}
|