@uniweb/build 0.47.2 → 0.48.0
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 +6 -6
- package/src/prerender.js +124 -47
- package/src/site/content-collector.js +121 -190
- package/src/site/data-fetcher.js +294 -188
- package/src/site/fetch-shapes.js +9 -8
- package/src/site/layout-folder.js +221 -0
- package/src/site/plugin.js +23 -19
- package/src/site/queries-config.js +80 -5
- package/src/site/query-processor.js +23 -25
- package/src/utils/content-files.js +38 -0
- package/src/uwx/records-project.js +7 -2
- package/src/uwx/site-diff.js +7 -2
- package/src/uwx/site-project.js +73 -41
- package/src/uwx/site.js +54 -40
- package/src/validate-data.js +7 -4
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@uniweb/build",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.48.0",
|
|
4
4
|
"description": "Build tooling for the Uniweb Component Web Platform",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -57,15 +57,15 @@
|
|
|
57
57
|
"js-yaml": "^4.1.0",
|
|
58
58
|
"sharp": "^0.35.3",
|
|
59
59
|
"yaml": "^2.5.0",
|
|
60
|
-
"@uniweb/projections": "^0.6.2",
|
|
61
|
-
"@uniweb/content-writer": "^0.3.4",
|
|
62
60
|
"@uniweb/content-reader": "^1.2.4",
|
|
63
61
|
"@uniweb/schemas": "^0.2.13",
|
|
62
|
+
"@uniweb/projections": "^0.7.0",
|
|
63
|
+
"@uniweb/theming": "^0.1.15",
|
|
64
64
|
"@uniweb/semantic-parser": "^1.4.0",
|
|
65
|
-
"@uniweb/
|
|
65
|
+
"@uniweb/content-writer": "^0.3.4"
|
|
66
66
|
},
|
|
67
67
|
"optionalDependencies": {
|
|
68
|
-
"@uniweb/runtime": "^0.
|
|
68
|
+
"@uniweb/runtime": "^0.23.0"
|
|
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.
|
|
77
|
+
"@uniweb/core": "^0.27.0"
|
|
78
78
|
},
|
|
79
79
|
"peerDependenciesMeta": {
|
|
80
80
|
"vite": {
|
package/src/prerender.js
CHANGED
|
@@ -15,8 +15,8 @@ import {
|
|
|
15
15
|
resolveDefaultLocale,
|
|
16
16
|
resolveFetchConfigs,
|
|
17
17
|
joinPathCapture,
|
|
18
|
-
|
|
19
|
-
|
|
18
|
+
pageRouteQuery,
|
|
19
|
+
routeSelection,
|
|
20
20
|
routeParamValues,
|
|
21
21
|
routeParamName,
|
|
22
22
|
routeBinding,
|
|
@@ -128,17 +128,55 @@ export async function executeAllFetches(siteContent, siteDir, onProgress, locale
|
|
|
128
128
|
// What each level fetched, by binding key — what `expandDynamicPages` iterates.
|
|
129
129
|
const fetched = { site: new Map(), pages: new Map(), sections: new Map() }
|
|
130
130
|
|
|
131
|
+
/**
|
|
132
|
+
* Read one resolved config ONCE, as two things: the records it selects, uncut
|
|
133
|
+
* (`routeSelection`), and the view it delivers — the same records cut by its
|
|
134
|
+
* `limit`. ⭐ The view is what a page renders and hydrates; the selection is what
|
|
135
|
+
* a parametric page expands over, because a `limit` is how many a list shows,
|
|
136
|
+
* never which records have a page (ruled 2026-09-13 [Diego]). ⛔ Until then the
|
|
137
|
+
* expansion read the cut list, so a query's `limit: 2` made two detail pages of
|
|
138
|
+
* five. Null when the read failed.
|
|
139
|
+
*/
|
|
140
|
+
const read = async (cfg, oneFetch) => {
|
|
141
|
+
const result = await executeFetch(routeSelection(cfg), optionsFor(cfg, oneFetch))
|
|
142
|
+
if (!result.data || result.error) return null
|
|
143
|
+
const selection = result.data
|
|
144
|
+
const view = Array.isArray(selection) && typeof cfg.limit === 'number' && cfg.limit > 0
|
|
145
|
+
? selection.slice(0, cfg.limit)
|
|
146
|
+
: selection
|
|
147
|
+
return { selection, view }
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* The first binding of a key at one level is the one every lane uses
|
|
152
|
+
* (`resolveFetchConfigs`); a later one under the same key is ignored — and the
|
|
153
|
+
* parser has said so (`warnDuplicateBindings`). Counted BEFORE `prerender: false`
|
|
154
|
+
* is honoured, so a deferred first binding is not replaced by a baked second.
|
|
155
|
+
*/
|
|
156
|
+
const firstOfKey = () => {
|
|
157
|
+
const seen = new Set()
|
|
158
|
+
return (as) => {
|
|
159
|
+
if (seen.has(as)) return false
|
|
160
|
+
seen.add(as)
|
|
161
|
+
return true
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
131
165
|
// 1. Site-level fetch. ⛔ `toFetchList` rather than a property read: a `fetch:`
|
|
132
166
|
// or `query:` LIST parses to an array, and `siteFetch.prerender` on one is
|
|
133
167
|
// `undefined` — which passes the `!== false` test and then fetches nothing.
|
|
168
|
+
const firstSite = firstOfKey()
|
|
134
169
|
for (const oneFetch of toFetchList(siteContent.config?.fetch)) {
|
|
135
|
-
if (oneFetch.
|
|
170
|
+
if (!firstSite(oneFetch.as)) continue
|
|
171
|
+
// ⭐ `prerender` is read off the RESOLVED config: an external query's binding is the
|
|
172
|
+
// browser's unless it says otherwise, which only the resolver knows.
|
|
136
173
|
const cfg = resolveForBuild(oneFetch)
|
|
174
|
+
if (cfg.prerender === false) continue
|
|
137
175
|
onProgress(` Fetching site data: ${cfg.path || cfg.url}`)
|
|
138
|
-
const
|
|
139
|
-
if (
|
|
140
|
-
fetchedData.push(entry(cfg,
|
|
141
|
-
|
|
176
|
+
const got = await read(cfg, oneFetch)
|
|
177
|
+
if (got) {
|
|
178
|
+
fetchedData.push(entry(cfg, got.view, '__site__'))
|
|
179
|
+
fetched.site.set(oneFetch.as, got.selection)
|
|
142
180
|
}
|
|
143
181
|
}
|
|
144
182
|
|
|
@@ -157,19 +195,33 @@ export async function executeAllFetches(siteContent, siteDir, onProgress, locale
|
|
|
157
195
|
|
|
158
196
|
for (const page of siteContent.pages || []) {
|
|
159
197
|
// Page-level fetch — every declaration on the page.
|
|
198
|
+
const firstPage = firstOfKey()
|
|
160
199
|
for (const oneFetch of toFetchList(page.fetch)) {
|
|
161
|
-
if (oneFetch.
|
|
200
|
+
if (!firstPage(oneFetch.as)) continue
|
|
162
201
|
const cfg = resolveForBuild(oneFetch)
|
|
202
|
+
if (cfg.prerender === false) continue
|
|
163
203
|
onProgress(` Fetching page data for ${page.route}: ${cfg.path || cfg.url}`)
|
|
164
|
-
const
|
|
165
|
-
if (
|
|
166
|
-
fetchedData.push(entry(cfg,
|
|
167
|
-
keep(fetched.pages, page.route, oneFetch.as,
|
|
204
|
+
const got = await read(cfg, oneFetch)
|
|
205
|
+
if (got) {
|
|
206
|
+
fetchedData.push(entry(cfg, got.view, page.route))
|
|
207
|
+
keep(fetched.pages, page.route, oneFetch.as, got.selection)
|
|
168
208
|
}
|
|
169
209
|
}
|
|
170
210
|
|
|
171
|
-
//
|
|
172
|
-
|
|
211
|
+
// Section-level fetches — a section's own, into its `parsedContent.data`.
|
|
212
|
+
// ⛔ Not on a parametric page: there the sections are the runtime's to fill,
|
|
213
|
+
// because what one receives depends on the URL — the record under the route
|
|
214
|
+
// key, a view its route binds under any other. A list baked into the template
|
|
215
|
+
// was cloned into every expanded page and outranked both (measured 2026-09-13:
|
|
216
|
+
// `/blog/b` rendered all three records). Its views are read per expanded page
|
|
217
|
+
// instead (`readRouteBoundViews`).
|
|
218
|
+
await processSectionFetches(page.sections, {
|
|
219
|
+
resolve: resolveForBuild,
|
|
220
|
+
read,
|
|
221
|
+
onProgress,
|
|
222
|
+
record: (as, data) => keep(fetched.sections, page.route, as, data),
|
|
223
|
+
bake: !page.isDynamic,
|
|
224
|
+
})
|
|
173
225
|
}
|
|
174
226
|
|
|
175
227
|
/**
|
|
@@ -179,8 +231,8 @@ export async function executeAllFetches(siteContent, siteDir, onProgress, locale
|
|
|
179
231
|
* browser's, as it is by default. Null when there is nothing to embed.
|
|
180
232
|
*/
|
|
181
233
|
const bake = async (cfg) => {
|
|
182
|
-
if (cfg.prerender === false || typeof cfg.path !== 'string') return null
|
|
183
|
-
const options = isNonDefaultLocale && cfg.path.startsWith(`/${localeInfo.locale}/`) ? localizedFetchOptions : fetchOptions
|
|
234
|
+
if (cfg.prerender === false || (typeof cfg.path !== 'string' && typeof cfg.url !== 'string')) return null
|
|
235
|
+
const options = isNonDefaultLocale && typeof cfg.path === 'string' && cfg.path.startsWith(`/${localeInfo.locale}/`) ? localizedFetchOptions : fetchOptions
|
|
184
236
|
const result = await executeFetch(cfg, options)
|
|
185
237
|
if (!result.data || result.error) return null
|
|
186
238
|
return { config: cfg, data: result.data, meta: { whole: cfg.whole } }
|
|
@@ -196,8 +248,9 @@ export async function executeAllFetches(siteContent, siteDir, onProgress, locale
|
|
|
196
248
|
*
|
|
197
249
|
* @param {Array} pages - Original pages array
|
|
198
250
|
* @param {{ site?: Map, pages?: Map, sections?: Map }} fetched - what each level
|
|
199
|
-
*
|
|
200
|
-
* `sections` route → (key → data)
|
|
251
|
+
* selected, by binding key (`executeAllFetches`): `site` key → data; `pages` and
|
|
252
|
+
* `sections` route → (key → data). Every record a binding selects, uncut by its
|
|
253
|
+
* `limit` — a record past a list's count still gets its page
|
|
201
254
|
* @param {function} onProgress - Progress callback
|
|
202
255
|
* @param {Object} [stats] - receives `unrouted[route]`, the records with no param value
|
|
203
256
|
* @param {Object} [options]
|
|
@@ -255,6 +308,10 @@ export function expandDynamicPages(pages, fetched, onProgress = () => {}, stats
|
|
|
255
308
|
)
|
|
256
309
|
const byRoute = new Map(pages.filter((p) => p?.route).map((p) => [p.route, p]))
|
|
257
310
|
const has = (route) => byRoute.has(route)
|
|
311
|
+
const parentOf = (p) => {
|
|
312
|
+
const route = parentRouteOf(p.route, { declared: p.parent ?? null, has })
|
|
313
|
+
return route ? byRoute.get(route) : null
|
|
314
|
+
}
|
|
258
315
|
const levels = {
|
|
259
316
|
site: fetched?.site ?? new Map(),
|
|
260
317
|
pages: fetched?.pages ?? new Map(),
|
|
@@ -268,19 +325,21 @@ export function expandDynamicPages(pages, fetched, onProgress = () => {}, stats
|
|
|
268
325
|
continue
|
|
269
326
|
}
|
|
270
327
|
|
|
271
|
-
// ⭐ THE ROUTE QUERY, by the rule every lane reads it with (`
|
|
272
|
-
// `@uniweb/core/fetch-config`), off the
|
|
273
|
-
// (`parentRouteOf`) — the page
|
|
274
|
-
//
|
|
275
|
-
//
|
|
276
|
-
//
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
328
|
+
// ⭐ THE ROUTE QUERY, by the rule every lane reads it with (`pageRouteQuery`,
|
|
329
|
+
// `@uniweb/core/fetch-config`), off the parents every lane finds
|
|
330
|
+
// (`parentRouteOf`) — chosen at the page that captured the URL's variable: its
|
|
331
|
+
// own query, its parent's, the site's, or its sections' shared key. A page
|
|
332
|
+
// nested inside a parametric page expands over its capturing page's records.
|
|
333
|
+
// The records it expands over are that query's, at the level it came from.
|
|
334
|
+
// ⛔ Until 2026-09-11 this read `parentSchema` and always expanded over the
|
|
335
|
+
// parent route's first prerendered fetch; until 2026-09-13 a nested page looked
|
|
336
|
+
// one parent up, and expanded only when the `[slug]` page declared the query.
|
|
337
|
+
const route = pageRouteQuery(page, {
|
|
338
|
+
routeOf: (p) => p.route,
|
|
339
|
+
parentOf,
|
|
340
|
+
fetchOf: (p) => p.fetch,
|
|
341
|
+
sectionsOf: (p) => p.sections,
|
|
282
342
|
site: siteFetch,
|
|
283
|
-
sections: sectionFetches(page.sections),
|
|
284
343
|
})
|
|
285
344
|
|
|
286
345
|
if (!route) {
|
|
@@ -289,10 +348,11 @@ export function expandDynamicPages(pages, fetched, onProgress = () => {}, stats
|
|
|
289
348
|
continue
|
|
290
349
|
}
|
|
291
350
|
|
|
292
|
-
const
|
|
293
|
-
|
|
351
|
+
const capturing = route.capturing
|
|
352
|
+
const data = route.level === 'page' ? levels.pages.get(capturing.route)?.get(route.key)
|
|
353
|
+
: route.level === 'parent' ? levels.pages.get(parentOf(capturing)?.route)?.get(route.key)
|
|
294
354
|
: route.level === 'site' ? levels.site.get(route.key)
|
|
295
|
-
: levels.sections.get(
|
|
355
|
+
: levels.sections.get(capturing.route)?.get(route.key)
|
|
296
356
|
|
|
297
357
|
if (!Array.isArray(data)) {
|
|
298
358
|
// No build-time data available (e.g., prerender: false on the route query).
|
|
@@ -382,7 +442,7 @@ export function expandDynamicPages(pages, fetched, onProgress = () => {}, stats
|
|
|
382
442
|
// route. ⛔ No `schema`: the key the URL narrows is worked out where it is
|
|
383
443
|
// read (deleted 2026-09-11). The record (`currentItem`) and the full sibling
|
|
384
444
|
// list (`allItems`) are deliberately NOT baked in: the record is delivered
|
|
385
|
-
// via content.data and
|
|
445
|
+
// via content.data and the others via `fetch: { query, current: exclude }`,
|
|
386
446
|
// and embedding `allItems` duplicated the whole collection onto every
|
|
387
447
|
// prerendered page in split mode.
|
|
388
448
|
const binding = routeBinding(page.route, catchAll ? { [catchAll]: capture } : { [paramName]: paramValue }, paramName)
|
|
@@ -417,28 +477,45 @@ export function expandDynamicPages(pages, fetched, onProgress = () => {}, stats
|
|
|
417
477
|
* Process fetch configs for sections (and subsections recursively)
|
|
418
478
|
* Section-level fetches merge data into parsedContent.data (not cascaded).
|
|
419
479
|
*
|
|
480
|
+
* ⭐ RESOLVED BY THE RUNTIME'S RULE, as page fetches are (`resolve`). ⛔ Until
|
|
481
|
+
* 2026-09-13 a section's fetch was executed as authored: a non-default locale baked
|
|
482
|
+
* the default locale's records (measured: a French page's section held the English
|
|
483
|
+
* file), and a named query's routed clauses and, once its `limit` stopped being
|
|
484
|
+
* compiled into the file, its `limit` never applied.
|
|
485
|
+
*
|
|
420
486
|
* @param {Array} sections - Array of section objects
|
|
421
|
-
* @param {Object}
|
|
422
|
-
* @param {
|
|
487
|
+
* @param {Object} options
|
|
488
|
+
* @param {(cfg: Object) => Object} options.resolve - resolves one binding
|
|
489
|
+
* @param {(cfg: Object, authored: Object) => Promise<{selection: any, view: any}|null>} options.read
|
|
490
|
+
* @param {function} options.onProgress - Progress callback
|
|
491
|
+
* @param {(as: string, data: any) => void} [options.record] - receives each key's selection
|
|
492
|
+
* @param {boolean} [options.bake=true] - write the view into `parsedContent.data`
|
|
423
493
|
*/
|
|
424
|
-
async function processSectionFetches(sections,
|
|
494
|
+
async function processSectionFetches(sections, { resolve, read, onProgress, record = null, bake = true }) {
|
|
425
495
|
if (!sections || !Array.isArray(sections)) return
|
|
426
496
|
|
|
427
497
|
for (const section of sections) {
|
|
428
498
|
// Execute every section-level fetch. Each merges under its own key, so
|
|
429
499
|
// several accumulate into one `parsedContent.data` — the same keyed map the
|
|
430
|
-
// runtime's EntityStore builds.
|
|
500
|
+
// runtime's EntityStore builds. ⛔ The FIRST binding of a key, as there: each
|
|
501
|
+
// replaced the last until 2026-09-13, so the build delivered the last binding
|
|
502
|
+
// and the runtime the first.
|
|
503
|
+
const seen = new Set()
|
|
431
504
|
for (const sectionFetch of toFetchList(section.fetch)) {
|
|
432
|
-
if (sectionFetch.
|
|
433
|
-
|
|
434
|
-
const
|
|
435
|
-
if (
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
505
|
+
if (seen.has(sectionFetch.as)) continue
|
|
506
|
+
seen.add(sectionFetch.as)
|
|
507
|
+
const cfg = resolve(sectionFetch)
|
|
508
|
+
if (cfg.prerender === false) continue
|
|
509
|
+
onProgress(` Fetching section data: ${cfg.path || cfg.url}`)
|
|
510
|
+
const got = await read(cfg, sectionFetch)
|
|
511
|
+
if (!got) continue
|
|
512
|
+
// What a section fetched, by key — the route query of a parametric page
|
|
513
|
+
// whose sections alone declare it (`routeQuery`).
|
|
514
|
+
if (record) record(sectionFetch.as, got.selection)
|
|
515
|
+
if (bake) {
|
|
439
516
|
section.parsedContent = mergeDataIntoContent(
|
|
440
517
|
section.parsedContent || {},
|
|
441
|
-
|
|
518
|
+
got.view,
|
|
442
519
|
sectionFetch.as,
|
|
443
520
|
sectionFetch.merge
|
|
444
521
|
)
|
|
@@ -447,7 +524,7 @@ async function processSectionFetches(sections, fetchOptions, onProgress, record
|
|
|
447
524
|
|
|
448
525
|
// Process subsections recursively
|
|
449
526
|
if (section.subsections && section.subsections.length > 0) {
|
|
450
|
-
await processSectionFetches(section.subsections,
|
|
527
|
+
await processSectionFetches(section.subsections, { resolve, read, onProgress, record, bake })
|
|
451
528
|
}
|
|
452
529
|
}
|
|
453
530
|
}
|