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