@uniweb/build 0.14.22 → 0.14.24

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.14.22",
3
+ "version": "0.14.24",
4
4
  "description": "Build tooling for the Uniweb Component Web Platform",
5
5
  "type": "module",
6
6
  "exports": {
@@ -59,13 +59,13 @@
59
59
  "js-yaml": "^4.1.0",
60
60
  "sharp": "^0.33.2",
61
61
  "yaml": "^2.5.0",
62
- "@uniweb/theming": "0.1.4",
62
+ "@uniweb/theming": "0.1.5",
63
63
  "@uniweb/content-writer": "0.2.6"
64
64
  },
65
65
  "optionalDependencies": {
66
- "@uniweb/content-reader": "1.1.12",
66
+ "@uniweb/runtime": "0.8.22",
67
67
  "@uniweb/schemas": "0.2.4",
68
- "@uniweb/runtime": "0.8.21"
68
+ "@uniweb/content-reader": "1.1.12"
69
69
  },
70
70
  "peerDependencies": {
71
71
  "vite": "^5.0.0 || ^6.0.0 || ^7.0.0",
@@ -74,7 +74,7 @@
74
74
  "@tailwindcss/vite": "^4.0.0",
75
75
  "@vitejs/plugin-react": "^4.0.0 || ^5.0.0",
76
76
  "vite-plugin-svgr": "^4.0.0",
77
- "@uniweb/core": "0.7.15"
77
+ "@uniweb/core": "0.7.16"
78
78
  },
79
79
  "peerDependenciesMeta": {
80
80
  "vite": {
@@ -45,10 +45,13 @@ const adapter = {
45
45
  onProgress('Wrote .nojekyll (opts out of Jekyll on GitHub Pages)')
46
46
  },
47
47
 
48
- async initCi({ site, packageManager = 'pnpm', nodeVersion = '20', domain = null }) {
48
+ // `pnpmVersion` is the pnpm major for the generated CI. The CLI passes the
49
+ // authoritative value (versions.js::PNPM_VERSION); the default here is only a
50
+ // fallback for direct/test callers.
51
+ async initCi({ site, packageManager = 'pnpm', nodeVersion = '20', pnpmVersion = '11', domain = null }) {
49
52
  const sitePath = site.path
50
53
  const workflowPath = `.github/workflows/deploy-github-pages.yml`
51
- const yaml = renderWorkflow({ sitePath, packageManager, nodeVersion, domain })
54
+ const yaml = renderWorkflow({ sitePath, packageManager, nodeVersion, pnpmVersion, domain })
52
55
 
53
56
  const files = [{ path: workflowPath, content: yaml }]
54
57
  if (domain) {
@@ -91,12 +94,12 @@ const adapter = {
91
94
  },
92
95
  }
93
96
 
94
- function renderWorkflow({ sitePath, packageManager, nodeVersion, domain }) {
97
+ function renderWorkflow({ sitePath, packageManager, nodeVersion, pnpmVersion, domain }) {
95
98
  const isPnpm = packageManager === 'pnpm'
96
99
  const setupSteps = isPnpm
97
100
  ? ` - uses: pnpm/action-setup@v4
98
101
  with:
99
- version: 10
102
+ version: ${pnpmVersion}
100
103
  - uses: actions/setup-node@v4
101
104
  with:
102
105
  node-version: '${nodeVersion}'
package/src/prerender.js CHANGED
@@ -138,9 +138,20 @@ async function executeAllFetches(siteContent, siteDir, onProgress, localeInfo) {
138
138
  * @param {function} onProgress - Progress callback
139
139
  * @returns {Array} Expanded pages array with dynamic pages replaced by concrete instances
140
140
  */
141
- function expandDynamicPages(pages, pageFetchedData, onProgress) {
141
+ export function expandDynamicPages(pages, pageFetchedData, onProgress) {
142
142
  const expandedPages = []
143
143
 
144
+ // Static pages win over the dynamic `[slug]` catch-all, matching the SPA's
145
+ // route resolution (Website.getPage checks exact static routes before the
146
+ // `:param` loop). Without this guard, a record whose param value collides
147
+ // with a static sibling's segment (e.g. slug:'about' + a static /blog/about)
148
+ // would emit a duplicate concrete route; the write loops are keyed on
149
+ // page.route and last-writer-wins, silently clobbering the static page's
150
+ // HTML. Collect the static routes up front so we can skip + warn on collision.
151
+ const staticRoutes = new Set(
152
+ pages.filter((p) => !p.isDynamic).map((p) => p.route)
153
+ )
154
+
144
155
  for (const page of pages) {
145
156
  if (!page.isDynamic) {
146
157
  // Regular page - include as-is
@@ -187,6 +198,13 @@ function expandDynamicPages(pages, pageFetchedData, onProgress) {
187
198
  // Create concrete route: /blog/:slug → /blog/my-post
188
199
  const concreteRoute = page.route.replace(`:${paramName}`, paramValue)
189
200
 
201
+ // Static sibling wins: skip a record whose concrete route collides with
202
+ // an existing static page rather than overwriting its HTML at write time.
203
+ if (staticRoutes.has(concreteRoute)) {
204
+ onProgress(` Skipping ${concreteRoute} — a static page already claims this route (${paramName}:'${paramValue}')`)
205
+ continue
206
+ }
207
+
190
208
  // Deep clone the page with modifications
191
209
  const concretePage = JSON.parse(JSON.stringify(page))
192
210
  concretePage.route = concreteRoute
@@ -95,7 +95,9 @@ export async function buildSiteData({
95
95
 
96
96
  // 1. Collect content (pages, sections, theme, config, assets manifest).
97
97
  // No vite needed — collectSiteContent is a plain async function.
98
- let siteContent = await collectSiteContent(resolvedSiteRoot, { foundationPath })
98
+ // dropUnpublished: link mode is always a published deploy — prune hidden
99
+ // pages + their subtree so drafts never reach the served site.
100
+ let siteContent = await collectSiteContent(resolvedSiteRoot, { foundationPath, dropUnpublished: true })
99
101
 
100
102
  // 2. Compile content collections (file-based markdown/yaml/json).
101
103
  // `writeCollectionFiles` lands them under `<siteRoot>/public/data/`;
@@ -29,7 +29,7 @@ import { existsSync, statSync, realpathSync } from 'node:fs'
29
29
  import yaml from 'js-yaml'
30
30
  import { collectSectionAssets, mergeAssetCollections, collectConfigAssets } from './assets.js'
31
31
  import { collectSectionIcons, mergeIconCollections, buildIconManifest } from './icons.js'
32
- import { normalizeHideIn } from './nav-visibility.js'
32
+ import { normalizeHideIn, dropUnpublishedPages } from './nav-visibility.js'
33
33
  import { parseFetchConfig, singularize } from './data-fetcher.js'
34
34
  import { buildTheme, extractFoundationVars } from '../theme/index.js'
35
35
 
@@ -1660,13 +1660,16 @@ async function collectPagesRecursive(dirPath, parentRoute, siteRoot, orderConfig
1660
1660
  }
1661
1661
  }
1662
1662
 
1663
- // When pages: is strict (no '...'), hide unlisted direct children from navigation
1663
+ // When pages: is strict (no '...'), suppress unlisted direct children from
1664
+ // every nav area — but keep them ROUTED (strict is a nav filter, not a draft
1665
+ // toggle). This is the nav axis (`hideIn: ['*']`), not the reachability axis
1666
+ // (`hidden`), which would drop the page from the published output entirely.
1664
1667
  if (strictPageNamesFM) {
1665
1668
  for (const page of pages) {
1666
1669
  const childName = getDirectChildName(page.route, parentRoute)
1667
1670
  || (page.sourcePath ? getDirectChildName(page.sourcePath, parentRoute) : null)
1668
- if (childName && !strictPageNamesFM.has(childName) && !page.hidden) {
1669
- page.hidden = true
1671
+ if (childName && !strictPageNamesFM.has(childName) && !page.hideIn?.includes('*')) {
1672
+ page.hideIn = [...(page.hideIn || []), '*']
1670
1673
  }
1671
1674
  }
1672
1675
  }
@@ -1797,13 +1800,15 @@ async function collectPagesRecursive(dirPath, parentRoute, siteRoot, orderConfig
1797
1800
  }
1798
1801
  }
1799
1802
 
1800
- // When pages: is strict (no '...'), hide unlisted direct children from navigation
1803
+ // When pages: is strict (no '...'), suppress unlisted direct children from every
1804
+ // nav area but keep them ROUTED — the nav axis (`hideIn: ['*']`), not the
1805
+ // reachability axis (`hidden`), which would drop them from the published output.
1801
1806
  if (strictPageNames) {
1802
1807
  for (const page of pages) {
1803
1808
  const childName = getDirectChildName(page.route, parentRoute)
1804
1809
  || (page.sourcePath ? getDirectChildName(page.sourcePath, parentRoute) : null)
1805
- if (childName && !strictPageNames.has(childName) && !page.hidden) {
1806
- page.hidden = true
1810
+ if (childName && !strictPageNames.has(childName) && !page.hideIn?.includes('*')) {
1811
+ page.hideIn = [...(page.hideIn || []), '*']
1807
1812
  }
1808
1813
  }
1809
1814
  }
@@ -2008,7 +2013,7 @@ async function collectLayouts(layoutDir, siteRoot, layoutNames = new Set()) {
2008
2013
  * @returns {Promise<Object>} Site content object with assets manifest
2009
2014
  */
2010
2015
  export async function collectSiteContent(sitePath, options = {}) {
2011
- const { foundationPath, configFile = 'site.yml', profile: profileName } = options
2016
+ const { foundationPath, configFile = 'site.yml', profile: profileName, dropUnpublished = false } = options
2012
2017
 
2013
2018
  // Read site config and raw theme config
2014
2019
  const siteConfig = await readYamlFile(join(sitePath, configFile))
@@ -2205,7 +2210,9 @@ export async function collectSiteContent(sitePath, options = {}) {
2205
2210
  ...processedTheme,
2206
2211
  css: themeCSS
2207
2212
  },
2208
- pages,
2213
+ // Reachability axis: on the published build paths, drop `hidden` pages and
2214
+ // their whole subtree (cascade). Dev keeps them so drafts stay previewable.
2215
+ pages: dropUnpublished ? dropUnpublishedPages(pages) : pages,
2209
2216
  // Layout area sets: { default: { header: page, footer: page, ... }, marketing: { ... } }
2210
2217
  layouts,
2211
2218
  notFound,
@@ -300,6 +300,9 @@ export function parseFetchConfig(fetch) {
300
300
  where: fetch.where,
301
301
  limit: fetch.limit,
302
302
  sort: fetch.sort,
303
+ // Canonical detail page for a list card's href (page:<stable_id> ref;
304
+ // resolved to a route template + interpolated per record at runtime).
305
+ detailPage: fetch.detailPage,
303
306
  // Legacy post-processing (deprecated, see warning above)
304
307
  filter: fetch.filter,
305
308
  }
@@ -313,6 +316,7 @@ export function parseFetchConfig(fetch) {
313
316
  merge = false,
314
317
  transform,
315
318
  detail,
319
+ detailPage,
316
320
  // Query operators
317
321
  where,
318
322
  limit,
@@ -334,6 +338,8 @@ export function parseFetchConfig(fetch) {
334
338
  merge,
335
339
  transform,
336
340
  detail,
341
+ // Canonical detail page for a list card's href (page:<stable_id>).
342
+ detailPage,
337
343
  // Query operators
338
344
  where,
339
345
  limit,
@@ -1,12 +1,20 @@
1
+ // Page-visibility helpers — the two orthogonal axes a page carries:
2
+ // • Reachability — `hidden: bool`. `true` = excluded from the PUBLISHED output
3
+ // entirely (see `dropUnpublishedPages`). Still previewable in `uniweb dev`.
4
+ // • Nav placement — `hideIn: string[]`. Which nav areas suppress the page while
5
+ // it IS routed (see `normalizeHideIn`). `['*']` = suppressed from every area.
6
+
1
7
  /**
2
8
  * Normalize a page/folder config's navigation visibility into a `hideIn` array —
3
9
  * the list of named nav areas a page is suppressed from (layout-area names like
4
10
  * 'header', 'footer', or any foundation-declared area). The canonical form behind
5
11
  * the runtime `hideIn`, the sync `hide_in` field, and `getPageHierarchy({ for })`.
12
+ * The sentinel `'*'` means "suppressed from every nav area" (still routed) — it is
13
+ * a normal array element, interpreted by the runtime, not special-cased here.
6
14
  *
7
15
  * Reads the canonical `hideIn` (an array, or a single string for convenience) and
8
16
  * folds in the legacy `hideInHeader` / `hideInFooter` booleans for back-compat.
9
- * Deduped; declaration order preserved. `hidden` (all-nav exclusion) is separate.
17
+ * Deduped; declaration order preserved. `hidden` (reachability) is a separate axis.
10
18
  *
11
19
  * @param {object} config - a parsed page.yml / folder.yml config (or page data)
12
20
  * @returns {string[]}
@@ -27,3 +35,35 @@ export function normalizeHideIn(config = {}) {
27
35
  if (config.hideInFooter) add('footer')
28
36
  return out
29
37
  }
38
+
39
+ /**
40
+ * Drop unpublished pages — the reachability axis. A page with `hidden: true` is
41
+ * excluded from the PUBLISHED output entirely, and the exclusion CASCADES to its
42
+ * whole subtree: drafting a container drafts the branch. Applied only on the
43
+ * published build paths (`uniweb build` / link-mode deploy); `uniweb dev` keeps
44
+ * hidden pages so in-progress work stays previewable by direct URL.
45
+ *
46
+ * Cascade is resolved via the parent-route chain (the same `parent` route strings
47
+ * the hierarchy is built from), so no surviving page is ever left pointing at a
48
+ * pruned parent — this is what avoids orphaned routes / dangling parent references
49
+ * that a per-node drop would create.
50
+ *
51
+ * @param {Array<object>} pages - collected page data (each with `route`, `parent`)
52
+ * @returns {Array<object>} pages with hidden pages and their descendants removed
53
+ */
54
+ export function dropUnpublishedPages(pages) {
55
+ if (!Array.isArray(pages) || pages.length === 0) return pages
56
+ const byRoute = new Map(pages.map((p) => [p.route, p]))
57
+ const cache = new Map()
58
+ const isUnpublished = (page, seen = new Set()) => {
59
+ if (!page) return false
60
+ if (cache.has(page.route)) return cache.get(page.route)
61
+ if (seen.has(page.route)) return false // defensive cycle guard (trees don't cycle)
62
+ seen.add(page.route)
63
+ const parent = page.parent ? byRoute.get(page.parent) : null
64
+ const result = page.hidden ? true : (parent ? isUnpublished(parent, seen) : false)
65
+ cache.set(page.route, result)
66
+ return result
67
+ }
68
+ return pages.filter((p) => !isUnpublished(p))
69
+ }
@@ -582,7 +582,9 @@ export function siteContentPlugin(options = {}) {
582
582
  async buildStart() {
583
583
  // Collect content at build start
584
584
  try {
585
- siteContent = await collectSiteContent(resolvedSitePath, { foundationPath })
585
+ // dropUnpublished only on a production build — in dev (serve) hidden
586
+ // pages stay in the graph so in-progress drafts remain previewable.
587
+ siteContent = await collectSiteContent(resolvedSitePath, { foundationPath, dropUnpublished: isProduction })
586
588
  headHtml = await loadHeadHtml()
587
589
  console.log(`[site-content] Collected ${siteContent.pages?.length || 0} pages`)
588
590