@uniweb/core 0.37.1 → 0.38.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 +3 -3
- package/src/data-keys.js +40 -34
- package/src/index.js +1 -1
- package/src/page-data.js +39 -17
- package/src/records-service.js +10 -7
- package/src/services.js +5 -4
- package/src/website.js +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@uniweb/core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.38.0",
|
|
4
4
|
"description": "Core classes for the Uniweb platform - Uniweb, Website, Page, Block",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -49,8 +49,8 @@
|
|
|
49
49
|
"vitest": "^4.1.7"
|
|
50
50
|
},
|
|
51
51
|
"dependencies": {
|
|
52
|
-
"@uniweb/
|
|
53
|
-
"@uniweb/
|
|
52
|
+
"@uniweb/semantic-parser": "^1.5.1",
|
|
53
|
+
"@uniweb/theming": "^0.2.0"
|
|
54
54
|
},
|
|
55
55
|
"scripts": {
|
|
56
56
|
"test": "vitest run"
|
package/src/data-keys.js
CHANGED
|
@@ -24,49 +24,55 @@
|
|
|
24
24
|
|
|
25
25
|
import { fetchEntries } from './fetch-config.js'
|
|
26
26
|
|
|
27
|
-
/** The suffix that asks for whole records: `'@std/article/*'`. */
|
|
28
|
-
export const WHOLE_SUFFIX = '/*'
|
|
29
|
-
|
|
30
27
|
/**
|
|
31
|
-
* A declared key's schema ref, and
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
* ⭐
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
* `
|
|
45
|
-
*
|
|
28
|
+
* A declared key's schema ref, and its two flags — as `data:` spells them: a ref string
|
|
29
|
+
* (`'@std/article'`), the long form (`{ schema: '@std/article', single: true, whole: true }`), or
|
|
30
|
+
* neither for an inline shape — a field map or a form — which names no schema.
|
|
31
|
+
*
|
|
32
|
+
* ⭐ TWO FLAGS, INDEPENDENT (ruled 2026-10-07 [Diego]):
|
|
33
|
+
*
|
|
34
|
+
* - **`single`** — how many: one record, or `null`, where a key holds a list by default. The
|
|
35
|
+
* runtime asks for the first record the binding selects (`./page-data.js`) and delivers it.
|
|
36
|
+
* - **`whole`** — how much of each: the record as stored, one key per section, where a key holds
|
|
37
|
+
* briefs by default, the brief's fields at the top (ruled 2026-09-27). The runtime asks each
|
|
38
|
+
* key's question accordingly; a key declared whole that its fetch cannot fill whole is `null`.
|
|
39
|
+
*
|
|
40
|
+
* ⛔ `'@std/article/*'` asked for whole records until 2026-10-07, and is retired: the build and
|
|
41
|
+
* `register` refuse it, so no artifact carries it, and it is not read here.
|
|
42
|
+
*
|
|
43
|
+
* Who passes what. The runtime: a section type's LEAN `data` — a ref string, `{ schema, single?,
|
|
44
|
+
* whole? }` or null, from the runtime schema the build extracts (`@uniweb/build`'s
|
|
45
|
+
* `runtime-schema.js`) — and the foundation's `main.js` `data:` leaned the same way. A build-time
|
|
46
|
+
* reader: a `meta.js` value as written, or a registered foundation schema's in its normalized form
|
|
47
|
+
* (`schemaFormat: 3`, `@uniweb/schemas`'s `normalizeFoundationSchema`), which writes every ref as
|
|
48
|
+
* `{ kind: 'schema', schema, single, whole }` — the ref qualified and both flags said outright.
|
|
46
49
|
*
|
|
47
50
|
* @param {*} value - one `data:` entry's value, authored, leaned or normalized
|
|
48
|
-
* @returns {{ ref: string|null, whole: boolean }}
|
|
51
|
+
* @returns {{ ref: string|null, single: boolean, whole: boolean }}
|
|
49
52
|
*/
|
|
50
53
|
export function dataRefOf(value) {
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
return { ref:
|
|
54
|
+
if (typeof value === 'string') return { ref: value || null, single: false, whole: false }
|
|
55
|
+
// The long form's `schema` is a ref; an inline field map may have a field called `schema`.
|
|
56
|
+
if (value && typeof value === 'object' && typeof value.schema === 'string' && value.schema.startsWith('@')) {
|
|
57
|
+
return { ref: value.schema, single: value.single === true, whole: value.whole === true }
|
|
58
|
+
}
|
|
59
|
+
return { ref: null, single: false, whole: false }
|
|
57
60
|
}
|
|
58
61
|
|
|
59
62
|
/**
|
|
60
|
-
* The keys a section's component receives, in order, each with its schema ref and
|
|
61
|
-
*
|
|
62
|
-
*
|
|
63
|
+
* The keys a section's component receives, in order, each with its schema ref and its two flags
|
|
64
|
+
* (`dataRefOf`) — its component's `data:` first, then its foundation's. A key both declare is the
|
|
65
|
+
* component's.
|
|
66
|
+
*
|
|
67
|
+
* `data: false`, a missing `data:` or anything that is not a map declares nothing. Each value
|
|
68
|
+
* may be as authored, as the build leans it, or as a registered schema normalizes it.
|
|
63
69
|
*
|
|
64
|
-
* `
|
|
65
|
-
*
|
|
70
|
+
* ⚠️ `single` is the FOURTH member, appended 2026-10-07, so a caller destructuring the first three
|
|
71
|
+
* reads what it always did.
|
|
66
72
|
*
|
|
67
73
|
* @param {Object|false|null|undefined} componentData - the component's `data:`
|
|
68
74
|
* @param {Object|false|null|undefined} [foundationData] - its foundation's `main.js` `data:`
|
|
69
|
-
* @returns {Array<[string, string|null, boolean]>} `[key, ref, whole]`
|
|
75
|
+
* @returns {Array<[string, string|null, boolean, boolean]>} `[key, ref, whole, single]`
|
|
70
76
|
*/
|
|
71
77
|
export function declaredKeys(componentData, foundationData = null) {
|
|
72
78
|
const out = []
|
|
@@ -76,8 +82,8 @@ export function declaredKeys(componentData, foundationData = null) {
|
|
|
76
82
|
for (const [key, value] of Object.entries(data)) {
|
|
77
83
|
if (!key || seen.has(key)) continue
|
|
78
84
|
seen.add(key)
|
|
79
|
-
const { ref, whole } = dataRefOf(value)
|
|
80
|
-
out.push([key, ref, whole])
|
|
85
|
+
const { ref, single, whole } = dataRefOf(value)
|
|
86
|
+
out.push([key, ref, whole, single])
|
|
81
87
|
}
|
|
82
88
|
}
|
|
83
89
|
return out
|
|
@@ -137,7 +143,7 @@ function parseRef(ref) {
|
|
|
137
143
|
* The mapping says where an answer lands; it renames no fetch. A fetch keeps its `as`
|
|
138
144
|
* for its question, its cache key and its prefetch.
|
|
139
145
|
*
|
|
140
|
-
* @param {Array<[string, string|null, boolean]>} declared - `declaredKeys`
|
|
146
|
+
* @param {Array<[string, string|null, boolean, boolean]>} declared - `declaredKeys`
|
|
141
147
|
* @param {Array<*>} levels - each level's `fetch`, most specific first; falsy levels skipped
|
|
142
148
|
* @param {Object} [options]
|
|
143
149
|
* @param {Object|null} [options.queries] - the site's `config.queries`, for each fetch's
|
package/src/index.js
CHANGED
|
@@ -24,7 +24,7 @@ export { substitutePlaceholders } from './substitute-placeholders.js'
|
|
|
24
24
|
// reads them and a consumer that needs them imports `@uniweb/core/records-service`.
|
|
25
25
|
export { resolveFetchConfigs, routeQuery, pageRouteQuery, routeSelection, sectionFetches, siteReaches, withoutRouteVariables, CURRENT_MODES, currentFor } from './fetch-config.js'
|
|
26
26
|
export { buildDetailConfig, ROUTE_HANDLE_KEY } from './detail-url.js'
|
|
27
|
-
export { declaredKeys, sameSchema, fillDeclaredKeys, dataRefOf
|
|
27
|
+
export { declaredKeys, sameSchema, fillDeclaredKeys, dataRefOf } from './data-keys.js'
|
|
28
28
|
// ⭐ What a page needs — the one rule. `fetchLevels` is the part every lane has an answer for
|
|
29
29
|
// (which fetches reach a block); the drivers and the per-key program are `@uniweb/core/page-data`.
|
|
30
30
|
export { fetchLevels } from './page-data.js'
|
package/src/page-data.js
CHANGED
|
@@ -80,13 +80,14 @@ export function fetchLevels({ own = null, page = null, parent = null, route = nu
|
|
|
80
80
|
* Each filling fetch is resolved on its own, so two fetches of one `as` can fill two keys.
|
|
81
81
|
*
|
|
82
82
|
* @param {Object} input
|
|
83
|
-
* @param {Array<[string, string|null, boolean]>} input.declared - `declaredKeys(meta)`
|
|
83
|
+
* @param {Array<[string, string|null, boolean, boolean]>} input.declared - `declaredKeys(meta)`
|
|
84
84
|
* @param {Array<*>} input.levels - `fetchLevels`
|
|
85
85
|
* @param {Object} [input.holds] - what the block already holds, by key
|
|
86
86
|
* @param {Object|null} [input.queries] - the site's `config.queries`
|
|
87
87
|
* @param {Object} [input.options] - what each fetch is resolved with (`resolveFetchConfigs`)
|
|
88
|
-
* @returns {Map<string, { cfg: Object, held: *, whole: boolean }>} declared key → its
|
|
89
|
-
* and whether its component expects whole records,
|
|
88
|
+
* @returns {Map<string, { cfg: Object, held: *, whole: boolean, single: boolean }>} declared key → its
|
|
89
|
+
* filling config, and its two flags — whether its component expects whole records, and one record —
|
|
90
|
+
* in the order `declared` lists the keys
|
|
90
91
|
*/
|
|
91
92
|
export function blockFills({ declared, levels, holds = {}, queries = null, options = {} }) {
|
|
92
93
|
const out = new Map()
|
|
@@ -102,12 +103,12 @@ export function blockFills({ declared, levels, holds = {}, queries = null, optio
|
|
|
102
103
|
if (!firstOfKey.has(fetch.as)) firstOfKey.set(fetch.as, `${at}:${index}`)
|
|
103
104
|
}))
|
|
104
105
|
|
|
105
|
-
const
|
|
106
|
+
const flags = new Map(declared.map(([key, , whole, single]) => [key, { whole: whole === true, single: single === true }]))
|
|
106
107
|
for (const [key, { fetch, level, index }] of fills) {
|
|
107
108
|
const cfg = resolveFetchConfigs([fetch], options).get(fetch.as)
|
|
108
109
|
if (!cfg) continue
|
|
109
110
|
const held = firstOfKey.get(fetch.as) === `${level}:${index}` ? holds[fetch.as] : undefined
|
|
110
|
-
out.set(key, { cfg, held,
|
|
111
|
+
out.set(key, { cfg, held, ...flags.get(key) })
|
|
111
112
|
}
|
|
112
113
|
return out
|
|
113
114
|
}
|
|
@@ -159,13 +160,19 @@ function heldWhole(peekRecord, match) {
|
|
|
159
160
|
* reading the cache never does, because the cache holds answers only.
|
|
160
161
|
*
|
|
161
162
|
* ⭐ **What is asked follows what the component expects** (`whole`, from its `data:` —
|
|
162
|
-
* `
|
|
163
|
-
* as it is, a compiled file's list, and on a parametric page the record FOUND in its query's
|
|
164
|
-
* Whole records when declared: the question carries `whole: true`, and a list off the service is
|
|
163
|
+
* `{ schema, whole: true }`; ruled 2026-09-27 [Diego]). Briefs by default: the records service's
|
|
164
|
+
* question as it is, a compiled file's list, and on a parametric page the record FOUND in its query's
|
|
165
|
+
* set. Whole records when declared: the question carries `whole: true`, and a list off the service is
|
|
165
166
|
* followed by each record's own request. A key declared whole that cannot be filled whole — its
|
|
166
167
|
* source has no request for one record — is `null`: the contract cannot be met. An external query's
|
|
167
168
|
* records have no data schema and so no brief: what its source answers is each record whole.
|
|
168
169
|
*
|
|
170
|
+
* ⭐ **And how many** (`single`, ruled 2026-10-07 [Diego]): a key that holds one record asks for one —
|
|
171
|
+
* the binding's question cut to its first record (`narrow.limit: 1`), so a whole one is fetched whole
|
|
172
|
+
* alone, never each record of the set. The answer is still a list, of one or none, which the runtime
|
|
173
|
+
* makes the record or `null` (`runtime/src/prepare-props.js`). A parametric page's record is found in
|
|
174
|
+
* the query's whole set as before: `current: only` asks for one already.
|
|
175
|
+
*
|
|
169
176
|
* ⛔ **A failed request delivers NOTHING under its key, and says so.** The key is absent from the
|
|
170
177
|
* returned value and the message rides on `error`, because `[]` is a value: a request that failed
|
|
171
178
|
* must not be indistinguishable from a query with no records. ⛔ Until 2026-09-27 a parametric
|
|
@@ -188,9 +195,10 @@ function heldWhole(peekRecord, match) {
|
|
|
188
195
|
* and a record found there answers with the brief it holds. No value at all means *unknown*,
|
|
189
196
|
* which is not the same as `[]`, *not in the set*.
|
|
190
197
|
* @param {boolean} [where.whole] - the component expects whole records
|
|
198
|
+
* @param {boolean} [where.single] - the component expects one record
|
|
191
199
|
* @returns {Generator<Object|Object[], { value?: *, error?: string, errorConfig?: Object }>}
|
|
192
200
|
*/
|
|
193
|
-
export function* keyProgram(cfg, { dynamicContext = null, route = null, held = undefined, peekRecord = null, current: forced = null, opportunistic = false, whole = false } = {}) {
|
|
201
|
+
export function* keyProgram(cfg, { dynamicContext = null, route = null, held = undefined, peekRecord = null, current: forced = null, opportunistic = false, whole = false, single = false } = {}) {
|
|
194
202
|
/** A step this caller can do without: a miss answers `{ missing: true }` instead of stopping. */
|
|
195
203
|
const maybe = (request) => (opportunistic ? { request, optional: true } : request)
|
|
196
204
|
// The block holds this fetch's answer already — a static build prerendered it.
|
|
@@ -207,14 +215,19 @@ export function* keyProgram(cfg, { dynamicContext = null, route = null, held = u
|
|
|
207
215
|
// How this fetch uses the page's record: by the query it names (`currentFor`).
|
|
208
216
|
const current = forced ?? (dynamicContext ? currentFor(cfg, route) : null)
|
|
209
217
|
|
|
218
|
+
// What a list of the binding's records is asked as — its first only, for a key holding one — and
|
|
219
|
+
// the answer cut to it here too, since a source need not honour `narrow` (a foundation's transport).
|
|
220
|
+
const listed = single ? firstOnly(cfg) : cfg
|
|
221
|
+
const cut = (data) => (single && Array.isArray(data) ? data.slice(0, 1) : data)
|
|
222
|
+
|
|
210
223
|
if (current === 'exclude') {
|
|
211
224
|
// The query's records without this page's, its `limit` counting the others.
|
|
212
|
-
const view = othersView(
|
|
225
|
+
const view = othersView(listed)
|
|
213
226
|
const asked = whole && cfg.ask ? { ...view, whole: true } : view
|
|
214
227
|
const answer = yield asked
|
|
215
228
|
if (answer.error) return { error: answer.error, errorConfig: asked }
|
|
216
229
|
if (answer.data === undefined || answer.data === null) return {}
|
|
217
|
-
const others = othersOf(answer.data,
|
|
230
|
+
const others = othersOf(answer.data, listed, isPageRecord(dynamicContext, cfg.routeField))
|
|
218
231
|
if (!whole || cfg.ask) return { value: others }
|
|
219
232
|
return yield* wholeRecordsOf(cfg, others)
|
|
220
233
|
}
|
|
@@ -273,12 +286,21 @@ export function* keyProgram(cfg, { dynamicContext = null, route = null, held = u
|
|
|
273
286
|
|
|
274
287
|
// A fetch the page's record plays no part in, and `current: include`: the records as the fetch
|
|
275
288
|
// describes them, the page's among the rest.
|
|
276
|
-
const asked = whole && cfg.ask ? { ...
|
|
289
|
+
const asked = whole && cfg.ask ? { ...listed, whole: true } : listed
|
|
277
290
|
const answer = yield asked
|
|
278
291
|
if (answer.error) return { error: answer.error, errorConfig: asked }
|
|
279
292
|
if (answer.data === undefined || answer.data === null) return {}
|
|
280
|
-
if (!whole || cfg.ask) return { value: answer.data }
|
|
281
|
-
return yield* wholeRecordsOf(cfg, answer.data)
|
|
293
|
+
if (!whole || cfg.ask) return { value: cut(answer.data) }
|
|
294
|
+
return yield* wholeRecordsOf(cfg, cut(answer.data))
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* A `single` key's question: the binding's own, cut to its first record. The fetch's `narrow` is
|
|
299
|
+
* where a cut belongs — applied after the query's set on every lane — and a `limit` of its own is at
|
|
300
|
+
* least one, so the first record is the same either way.
|
|
301
|
+
*/
|
|
302
|
+
function firstOnly(cfg) {
|
|
303
|
+
return { ...cfg, narrow: { ...(cfg.narrow || {}), limit: 1 } }
|
|
282
304
|
}
|
|
283
305
|
|
|
284
306
|
/**
|
|
@@ -340,14 +362,14 @@ function asSteps(value) {
|
|
|
340
362
|
/**
|
|
341
363
|
* Every filling key's program, ready to run.
|
|
342
364
|
*
|
|
343
|
-
* @param {Map<string, { cfg: Object, held: *, whole?: boolean }>} fills - `blockFills`
|
|
365
|
+
* @param {Map<string, { cfg: Object, held: *, whole?: boolean, single?: boolean }>} fills - `blockFills`
|
|
344
366
|
* @param {Object} where - passed to each `keyProgram`
|
|
345
367
|
* @returns {Map<string, Generator>}
|
|
346
368
|
*/
|
|
347
369
|
export function planFor(fills, where = {}) {
|
|
348
370
|
const plan = new Map()
|
|
349
|
-
for (const [key, { cfg, held, whole = false }] of fills) {
|
|
350
|
-
plan.set(key, keyProgram(cfg, { ...where, held, whole }))
|
|
371
|
+
for (const [key, { cfg, held, whole = false, single = false }] of fills) {
|
|
372
|
+
plan.set(key, keyProgram(cfg, { ...where, held, whole, single }))
|
|
351
373
|
}
|
|
352
374
|
return plan
|
|
353
375
|
}
|
package/src/records-service.js
CHANGED
|
@@ -36,13 +36,16 @@
|
|
|
36
36
|
*
|
|
37
37
|
* ## ⛔ The HOST tier only — and the reason is mechanical, not a policy
|
|
38
38
|
*
|
|
39
|
-
* `resolveService` reads the
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
* not
|
|
39
|
+
* `resolveService` reads two tiers: the host's offer, then the site's authored
|
|
40
|
+
* declaration for what the host does not offer. *(This said "the site's authored
|
|
41
|
+
* tier first" until 2026-10-06; the host's offer has come first since 2026-09-10.)*
|
|
42
|
+
* This reads the host's alone, because it cannot read the other: its caller
|
|
43
|
+
* `resolveFetchConfigs` is a leaf that runs in an SSR isolate over a plain
|
|
44
|
+
* content object with no `Website` to hand. It reads `config.services` directly,
|
|
45
|
+
* so the authored tier is not available here. ⇒ **A site does not declare its
|
|
46
|
+
* own record provider**, which is also the standing ruling — a third-party
|
|
47
|
+
* source is a foundation transport, not site-level config *(2026-09-04, the
|
|
48
|
+
* retired `fetcher:` vocabulary)*.
|
|
46
49
|
*
|
|
47
50
|
* ## ⛔ A pattern, not a base — and the reason is a deleted function
|
|
48
51
|
*
|
package/src/services.js
CHANGED
|
@@ -16,10 +16,11 @@ import { applyBasePath } from './base-path.js'
|
|
|
16
16
|
* deployment offers, which the site never had to know about. When a host
|
|
17
17
|
* offers a service, that is the answer: it is the authority on what a
|
|
18
18
|
* hosted site is given.
|
|
19
|
-
* 2. **The site**, authored —
|
|
20
|
-
*
|
|
21
|
-
* does not provide, and on a static site, where
|
|
22
|
-
* answer.
|
|
19
|
+
* 2. **The site**, authored — its entry under `services:` in site.yml, which a
|
|
20
|
+
* built site carries as `config.<name>`. The operator's own declaration —
|
|
21
|
+
* used for anything the host does not provide, and on a static site, where
|
|
22
|
+
* no host speaks, the whole answer. *(This named the top-level `search:`,
|
|
23
|
+
* `submit:`, `assistant:` and `tracking:` keys, retired 2026-10-06.)*
|
|
23
24
|
*
|
|
24
25
|
* ## ⛔ THE TWO TIERS ARE NOT TWO SOURCES OF ONE KIND OF THING
|
|
25
26
|
*
|
package/src/website.js
CHANGED
|
@@ -1158,7 +1158,7 @@ export default class Website {
|
|
|
1158
1158
|
* The registry is open: any name resolves, whether or not the framework ships
|
|
1159
1159
|
* a client for it.
|
|
1160
1160
|
*
|
|
1161
|
-
* @param {string} name - the service name — `search`, `submit`, `
|
|
1161
|
+
* @param {string} name - the service name — `search`, `submit`, `backend`, …
|
|
1162
1162
|
* @returns {boolean}
|
|
1163
1163
|
*/
|
|
1164
1164
|
isServiceEnabled(name) {
|