@uniweb/build 1.6.0 → 1.6.2

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": "1.6.0",
3
+ "version": "1.6.2",
4
4
  "description": "Build tooling for the Uniweb Component Web Platform",
5
5
  "type": "module",
6
6
  "exports": {
@@ -59,12 +59,12 @@
59
59
  "@uniweb/semantic-parser": "~1.12.2",
60
60
  "@uniweb/content-reader": "~1.5.4",
61
61
  "@uniweb/theming": "~1.2.0",
62
+ "@uniweb/schemas": "~1.5.2",
62
63
  "@uniweb/content-writer": "~1.3.0",
63
- "@uniweb/projections": "~1.1.4",
64
- "@uniweb/schemas": "~1.5.0"
64
+ "@uniweb/projections": "~1.1.5"
65
65
  },
66
66
  "optionalDependencies": {
67
- "@uniweb/runtime": "~1.1.11"
67
+ "@uniweb/runtime": "~1.1.12"
68
68
  },
69
69
  "peerDependencies": {
70
70
  "vite": "^5.0.0 || ^6.0.0 || ^7.0.0",
@@ -73,8 +73,8 @@
73
73
  "@tailwindcss/vite": "^4.0.0",
74
74
  "@vitejs/plugin-react": "^4.0.0 || ^5.0.0",
75
75
  "vite-plugin-svgr": "^4.0.0",
76
- "@uniweb/core": "~1.3.1",
77
- "@uniweb/runtime": "~1.1.11"
76
+ "@uniweb/core": "~1.3.2",
77
+ "@uniweb/runtime": "~1.1.12"
78
78
  },
79
79
  "peerDependenciesMeta": {
80
80
  "vite": {
@@ -34,6 +34,17 @@
34
34
  * which is exactly the invisible failure this whole mechanism exists to stop.
35
35
  * Never widen the scan set to "everything Rollup saw".
36
36
  *
37
+ * ⛔ **And within a survivor, read only the code it kept.** A module survives
38
+ * when ONE of its exports is used, and Rollup drops the rest of it — so walking
39
+ * the module's ORIGINAL source counts every function in it. Measured 2026-10-19
40
+ * on `@std/academic`, whose code reaches only `isSearchEnabled()`: kit's
41
+ * `servicePredicates.js` survived for that one predicate, its source holds all
42
+ * five, and the foundation published `assistant, backend, search, submit` — a
43
+ * bundle with one `isServiceEnabled('search')` and none of the other three. ⇒
44
+ * Each survivor's RENDERED code (`chunk.modules[id].code`) is what is parsed and
45
+ * walked. A survivor whose rendered code cannot be read is blindness, never a
46
+ * fall back to its source.
47
+ *
37
48
  * ## ⭐ The service name travels with the code, so there is almost no map
38
49
  *
39
50
  * `kit/src/utils/submitTarget.js` contains `resolveService(website, 'submit')`
@@ -172,11 +183,17 @@ function readModuleConsts(ast) {
172
183
  return consts
173
184
  }
174
185
 
175
- /** `resolveService(...)`, called bare or through a namespace. */
186
+ /**
187
+ * `resolveService(...)`, called bare or through a namespace.
188
+ *
189
+ * The bare form is read from RENDERED code, where Rollup may have renamed an
190
+ * imported binding to keep it apart from another of the same name in the chunk
191
+ * (`resolveService$1`) — so the suffix it adds is accepted.
192
+ */
176
193
  function isResolveServiceCall(node) {
177
194
  if (node.type !== 'CallExpression') return false
178
195
  const callee = node.callee
179
- if (callee?.type === 'Identifier') return callee.name === 'resolveService'
196
+ if (callee?.type === 'Identifier') return /^resolveService(\$\d+)?$/.test(callee.name)
180
197
  if (callee?.type === 'MemberExpression' && !callee.computed) {
181
198
  return callee.property?.name === 'resolveService'
182
199
  }
@@ -218,23 +235,38 @@ function isSearchEnabledCall(node) {
218
235
  }
219
236
 
220
237
  /**
221
- * The set of modules that contributed code to the written bundle.
238
+ * The modules that contributed code to the written bundle, each with the code
239
+ * it contributed.
222
240
  *
223
241
  * `renderedLength > 0` is the discriminator: a module whose every binding was
224
242
  * shaken out contributes nothing, and counting it would reintroduce exactly the
225
- * over-reporting this module exists to avoid.
243
+ * over-reporting this module exists to avoid. ⛔ And what a survivor contributed
244
+ * is its RENDERED code (`code`), not its source: a module kept for one export
245
+ * keeps only that export (header, *Post-tree-shake only*).
246
+ *
247
+ * @returns {Map<string, string|null>} module id → its rendered code
226
248
  */
227
249
  function collectSurvivors(bundle) {
228
- const survivors = new Set()
250
+ const survivors = new Map()
229
251
  for (const chunk of Object.values(bundle || {})) {
230
252
  if (chunk?.type !== 'chunk' || !chunk.modules) continue
231
253
  for (const [id, mod] of Object.entries(chunk.modules)) {
232
- if (mod?.renderedLength > 0) survivors.add(id)
254
+ if (mod?.renderedLength > 0) survivors.set(id, typeof mod.code === 'string' ? mod.code : null)
233
255
  }
234
256
  }
235
257
  return survivors
236
258
  }
237
259
 
260
+ /** A survivor's rendered code as an ESTree tree, or null when it cannot be read. */
261
+ function renderedAst(code, ctx) {
262
+ if (typeof code !== 'string' || typeof ctx?.parse !== 'function') return null
263
+ try {
264
+ return ctx.parse(code)
265
+ } catch {
266
+ return null
267
+ }
268
+ }
269
+
238
270
  /**
239
271
  * Derive the services this foundation reaches for.
240
272
  *
@@ -245,22 +277,26 @@ function collectSurvivors(bundle) {
245
277
  * `isSearchEnabled()`. Names in `NEVER_EMITTED` are dropped from the result.
246
278
  *
247
279
  * @param {object} bundle - Rollup's bundle, as handed to `writeBundle`
248
- * @param {object} ctx - the Rollup plugin context (`this` in the hook)
280
+ * @param {object} ctx - the Rollup plugin context (`this` in the hook): its
281
+ * `parse` reads each survivor's rendered code
249
282
  * @returns {{services: string[], blind: boolean, blindAt: string[]}}
250
283
  * `services` is sorted and de-duplicated, and is a **lower bound**.
251
284
  * `blind` is true when a `resolveService` call names its service with
252
- * something this cannot read — the one case where the lower bound is known to
253
- * be incomplete, and the caller must not report an empty result as a proven
254
- * "none". `blindAt` names the modules, for the warning.
285
+ * something this cannot read, or a survivor's rendered code cannot be read at
286
+ * all — the cases where the lower bound is known to be incomplete, and the
287
+ * caller must not report an empty result as a proven "none". `blindAt` names
288
+ * the modules, for the warning.
255
289
  */
256
290
  export function deriveSupports(bundle, ctx) {
257
291
  const services = new Set()
258
292
  const blindAt = new Set()
259
293
 
260
- for (const id of collectSurvivors(bundle)) {
261
- const info = ctx?.getModuleInfo?.(id)
262
- const ast = info?.ast
263
- if (!ast) continue
294
+ for (const [id, code] of collectSurvivors(bundle)) {
295
+ const ast = renderedAst(code, ctx)
296
+ if (!ast) {
297
+ blindAt.add(id)
298
+ continue
299
+ }
264
300
 
265
301
  const consts = readModuleConsts(ast)
266
302
 
@@ -39,6 +39,7 @@ import { resolveExtensionUrls } from './extension-urls.js'
39
39
  import { buildTheme, extractFoundationVars } from '../theme/index.js'
40
40
  import { resolveDefaultLocale, resolvePublishableLocales, validateLanguageConfig, pageRouteQuery, currentFor } from '@uniweb/core'
41
41
  import { authoredRedirectTarget } from '@uniweb/core/resolve-route'
42
+ import { parentRouteOf } from '@uniweb/core/route-match'
42
43
  import { parseFrontmatter } from '../utils/frontmatter.js'
43
44
  import { parseGrid } from '@uniweb/schemas/grid'
44
45
  import { runtimeServicesConfig, refuseRetiredServiceKeys, refuseUnreadableServices } from '../uwx/services-request.js'
@@ -2719,8 +2720,10 @@ export async function collectSiteContent(sitePath, options = {}) {
2719
2720
  }
2720
2721
  }
2721
2722
 
2722
- // Compute parent route for each page (hierarchy declaration)
2723
- // This runs once at build time so runtime doesn't need to re-derive hierarchy
2723
+ // Compute parent route for each page (hierarchy declaration), by the one parent rule
2724
+ // (`parentRouteOf`) — so the parent declared here is the one every lane derives, past a
2725
+ // version folder included. This runs once at build time so runtime doesn't need to
2726
+ // re-derive hierarchy.
2724
2727
  const pageRouteMap = new Map()
2725
2728
  for (const page of pages) {
2726
2729
  pageRouteMap.set(page.route, page)
@@ -2728,15 +2731,10 @@ export async function collectSiteContent(sitePath, options = {}) {
2728
2731
  pageRouteMap.set(page.sourcePath, page)
2729
2732
  }
2730
2733
  }
2734
+ const hasPage = (route) => pageRouteMap.has(route)
2731
2735
  for (const page of pages) {
2732
- const segments = page.route.split('/').filter(Boolean)
2733
- if (segments.length <= 1) {
2734
- page.parent = null
2735
- continue
2736
- }
2737
- const parentRoute = '/' + segments.slice(0, -1).join('/')
2738
- const parentPage = pageRouteMap.get(parentRoute)
2739
- page.parent = parentPage ? parentPage.route : null
2736
+ const parentRoute = parentRouteOf(page.route, { has: hasPage })
2737
+ page.parent = parentRoute ? pageRouteMap.get(parentRoute).route : null
2740
2738
  }
2741
2739
 
2742
2740
  const siteFetch = parseFetchConfig(...fetchIn(siteConfig, 'site.yml', 'site'))