@uniweb/build 0.4.1 → 0.4.3
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 +3 -3
- package/src/prerender.js +56 -0
- package/src/site/config.js +4 -0
- package/src/site/content-collector.js +57 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@uniweb/build",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.3",
|
|
4
4
|
"description": "Build tooling for the Uniweb Component Web Platform",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -51,7 +51,7 @@
|
|
|
51
51
|
},
|
|
52
52
|
"optionalDependencies": {
|
|
53
53
|
"@uniweb/content-reader": "1.1.1",
|
|
54
|
-
"@uniweb/runtime": "0.5.
|
|
54
|
+
"@uniweb/runtime": "0.5.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.3.
|
|
63
|
+
"@uniweb/core": "0.3.4"
|
|
64
64
|
},
|
|
65
65
|
"peerDependenciesMeta": {
|
|
66
66
|
"vite": {
|
package/src/prerender.js
CHANGED
|
@@ -263,6 +263,43 @@ async function loadDependencies(siteDir) {
|
|
|
263
263
|
guaranteeContentStructureSSR = runtimeMod.guaranteeContentStructure
|
|
264
264
|
}
|
|
265
265
|
|
|
266
|
+
/**
|
|
267
|
+
* Pre-fetch icons from CDN and populate the Uniweb icon cache.
|
|
268
|
+
* This allows the Icon component to render SVGs synchronously during SSR
|
|
269
|
+
* instead of producing empty placeholders.
|
|
270
|
+
*/
|
|
271
|
+
async function prefetchIcons(siteContent, uniweb, onProgress) {
|
|
272
|
+
const icons = siteContent.icons?.used || []
|
|
273
|
+
if (icons.length === 0) return
|
|
274
|
+
|
|
275
|
+
const cdnBase = siteContent.config?.icons?.cdnUrl || 'https://uniweb.github.io/icons'
|
|
276
|
+
|
|
277
|
+
onProgress(`Fetching ${icons.length} icons for SSR...`)
|
|
278
|
+
|
|
279
|
+
const results = await Promise.allSettled(
|
|
280
|
+
icons.map(async (iconRef) => {
|
|
281
|
+
const [family, name] = iconRef.split(':')
|
|
282
|
+
const url = `${cdnBase}/${family}/${family}-${name}.svg`
|
|
283
|
+
const response = await fetch(url)
|
|
284
|
+
if (!response.ok) throw new Error(`HTTP ${response.status}`)
|
|
285
|
+
const svg = await response.text()
|
|
286
|
+
uniweb.iconCache.set(`${family}:${name}`, svg)
|
|
287
|
+
})
|
|
288
|
+
)
|
|
289
|
+
|
|
290
|
+
const succeeded = results.filter(r => r.status === 'fulfilled').length
|
|
291
|
+
const failed = results.filter(r => r.status === 'rejected').length
|
|
292
|
+
if (failed > 0) {
|
|
293
|
+
console.warn(`[prerender] Fetched ${succeeded}/${icons.length} icons (${failed} failed)`)
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// Store icon cache on siteContent for embedding in HTML
|
|
297
|
+
// This allows the client runtime to populate the cache before rendering
|
|
298
|
+
if (uniweb.iconCache.size > 0) {
|
|
299
|
+
siteContent._iconCache = Object.fromEntries(uniweb.iconCache)
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
266
303
|
/**
|
|
267
304
|
* Inline BlockRenderer for SSR
|
|
268
305
|
* Uses React from prerender's scope to avoid module resolution issues
|
|
@@ -524,6 +561,9 @@ export async function prerenderSite(siteDir, options = {}) {
|
|
|
524
561
|
uniweb.setFoundationConfig(foundation.capabilities)
|
|
525
562
|
}
|
|
526
563
|
|
|
564
|
+
// Pre-fetch icons for SSR embedding
|
|
565
|
+
await prefetchIcons(siteContent, uniweb, onProgress)
|
|
566
|
+
|
|
527
567
|
// Pre-render each page
|
|
528
568
|
const pages = uniweb.activeWebsite.pages
|
|
529
569
|
const website = uniweb.activeWebsite
|
|
@@ -640,6 +680,22 @@ function injectContent(shell, renderedContent, page, siteContent) {
|
|
|
640
680
|
)
|
|
641
681
|
}
|
|
642
682
|
|
|
683
|
+
// Inject icon cache so client can render icons immediately without CDN fetches
|
|
684
|
+
if (siteContent._iconCache) {
|
|
685
|
+
const iconScript = `<script id="__ICON_CACHE__" type="application/json">${JSON.stringify(siteContent._iconCache)}</script>`
|
|
686
|
+
if (html.includes('__ICON_CACHE__')) {
|
|
687
|
+
html = html.replace(
|
|
688
|
+
/<script[^>]*id="__ICON_CACHE__"[^>]*>[\s\S]*?<\/script>/,
|
|
689
|
+
iconScript
|
|
690
|
+
)
|
|
691
|
+
} else {
|
|
692
|
+
html = html.replace(
|
|
693
|
+
'</head>',
|
|
694
|
+
` ${iconScript}\n </head>`
|
|
695
|
+
)
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
|
|
643
699
|
return html
|
|
644
700
|
}
|
|
645
701
|
|
package/src/site/config.js
CHANGED
|
@@ -287,6 +287,10 @@ export async function defineSiteConfig(options = {}) {
|
|
|
287
287
|
},
|
|
288
288
|
|
|
289
289
|
resolve: {
|
|
290
|
+
// Deduplicate React packages to prevent dual-instance issues
|
|
291
|
+
// Foundation externalizes React; when site bundles it, CJS and ESM
|
|
292
|
+
// copies can coexist without this, causing "useRef of null" errors
|
|
293
|
+
dedupe: ['react', 'react-dom', 'react/jsx-runtime', 'react/jsx-dev-runtime'],
|
|
290
294
|
alias: {
|
|
291
295
|
...alias,
|
|
292
296
|
...resolveOverrides?.alias
|
|
@@ -759,8 +759,13 @@ async function collectPagesRecursive(dirPath, parentRoute, siteRoot, orderConfig
|
|
|
759
759
|
}
|
|
760
760
|
|
|
761
761
|
// Determine which page is the index for this level
|
|
762
|
+
// A directory with its own .md content is a real page, not a container —
|
|
763
|
+
// never promote a child as index, even if explicit config says so
|
|
764
|
+
// (that config is likely a leftover from before the directory had content)
|
|
762
765
|
const regularFolders = pageFolders.filter(f => !f.name.startsWith('@'))
|
|
763
|
-
const
|
|
766
|
+
const hasExplicitOrder = orderConfig?.index || (Array.isArray(orderConfig?.pages) && orderConfig.pages.length > 0)
|
|
767
|
+
const hasMdContent = entries.some(e => isMarkdownFile(e))
|
|
768
|
+
const indexPageName = hasMdContent ? null : determineIndexPage(orderConfig, regularFolders)
|
|
764
769
|
|
|
765
770
|
// Second pass: process each page folder
|
|
766
771
|
for (const folder of pageFolders) {
|
|
@@ -804,7 +809,12 @@ async function collectPagesRecursive(dirPath, parentRoute, siteRoot, orderConfig
|
|
|
804
809
|
// Recursively process subdirectories (but not special @ directories)
|
|
805
810
|
if (!isSpecial) {
|
|
806
811
|
// The child route depends on whether this page is the index
|
|
807
|
-
|
|
812
|
+
// For explicit index (from site.yml `index:` or `pages:`), children use parentRoute
|
|
813
|
+
// since that's a true structural promotion. For auto-detected index, children use
|
|
814
|
+
// the page's original folder path so they nest correctly under it.
|
|
815
|
+
const childParentRoute = isIndex
|
|
816
|
+
? (hasExplicitOrder ? parentRoute : (page.sourcePath || page.route))
|
|
817
|
+
: page.route
|
|
808
818
|
// Pass this page's fetch config to children (for dynamic routes that inherit parent data)
|
|
809
819
|
const childFetch = page.fetch || parentFetch
|
|
810
820
|
// Pass version context to children (maintains version scope)
|
|
@@ -901,6 +911,51 @@ export async function collectSiteContent(sitePath, options = {}) {
|
|
|
901
911
|
const { pages, assetCollection, iconCollection, header, footer, left, right, notFound, versionedScopes } =
|
|
902
912
|
await collectPagesRecursive(pagesPath, '/', sitePath, siteOrderConfig)
|
|
903
913
|
|
|
914
|
+
// Deduplicate: remove content-less container pages whose route duplicates
|
|
915
|
+
// a content-bearing page (e.g., a promoted index page)
|
|
916
|
+
const routeCounts = new Map()
|
|
917
|
+
for (const page of pages) {
|
|
918
|
+
const existing = routeCounts.get(page.route)
|
|
919
|
+
if (!existing) {
|
|
920
|
+
routeCounts.set(page.route, [page])
|
|
921
|
+
} else {
|
|
922
|
+
existing.push(page)
|
|
923
|
+
}
|
|
924
|
+
}
|
|
925
|
+
for (const [route, group] of routeCounts) {
|
|
926
|
+
if (group.length > 1) {
|
|
927
|
+
// Keep the page with content, remove content-less duplicates
|
|
928
|
+
const withContent = group.filter(p => p.sections && p.sections.length > 0)
|
|
929
|
+
const toRemove = withContent.length > 0
|
|
930
|
+
? group.filter(p => !p.sections || p.sections.length === 0)
|
|
931
|
+
: group.slice(1) // If none have content, keep first
|
|
932
|
+
for (const page of toRemove) {
|
|
933
|
+
const idx = pages.indexOf(page)
|
|
934
|
+
if (idx !== -1) pages.splice(idx, 1)
|
|
935
|
+
}
|
|
936
|
+
}
|
|
937
|
+
}
|
|
938
|
+
|
|
939
|
+
// Compute parent route for each page (hierarchy declaration)
|
|
940
|
+
// This runs once at build time so runtime doesn't need to re-derive hierarchy
|
|
941
|
+
const pageRouteMap = new Map()
|
|
942
|
+
for (const page of pages) {
|
|
943
|
+
pageRouteMap.set(page.route, page)
|
|
944
|
+
if (page.sourcePath) {
|
|
945
|
+
pageRouteMap.set(page.sourcePath, page)
|
|
946
|
+
}
|
|
947
|
+
}
|
|
948
|
+
for (const page of pages) {
|
|
949
|
+
const segments = page.route.split('/').filter(Boolean)
|
|
950
|
+
if (segments.length <= 1) {
|
|
951
|
+
page.parent = null
|
|
952
|
+
continue
|
|
953
|
+
}
|
|
954
|
+
const parentRoute = '/' + segments.slice(0, -1).join('/')
|
|
955
|
+
const parentPage = pageRouteMap.get(parentRoute)
|
|
956
|
+
page.parent = parentPage ? parentPage.route : null
|
|
957
|
+
}
|
|
958
|
+
|
|
904
959
|
// Sort pages by order
|
|
905
960
|
pages.sort((a, b) => (a.order ?? 999) - (b.order ?? 999))
|
|
906
961
|
|