@uniweb/core 0.7.32 → 0.7.33
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 +2 -1
- package/src/entity-store.js +27 -86
- package/src/fetch-config.js +153 -0
package/package.json
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@uniweb/core",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.33",
|
|
4
4
|
"description": "Core classes for the Uniweb platform - Uniweb, Website, Page, Block",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
7
7
|
".": "./src/index.js",
|
|
8
|
+
"./fetch-config": "./src/fetch-config.js",
|
|
8
9
|
"./locale-config": "./src/locale-config.js",
|
|
9
10
|
"./section-id": "./src/section-id.js"
|
|
10
11
|
},
|
package/src/entity-store.js
CHANGED
|
@@ -15,16 +15,18 @@
|
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
17
|
import { substitutePlaceholders } from './substitute-placeholders.js'
|
|
18
|
+
import { isFetchRefinement, resolveFetchConfigs } from './fetch-config.js'
|
|
18
19
|
|
|
19
20
|
/**
|
|
20
21
|
* Is `block.fetch` a per-instance refinement of the ancestor's fetch config
|
|
21
22
|
* rather than a new source? The canonical spelling is `refine: true`; the
|
|
22
23
|
* legacy spelling `inherit: true` is still honored for one release with a
|
|
23
24
|
* dev-mode warning.
|
|
25
|
+
*
|
|
26
|
+
* The predicate itself lives in `./fetch-config.js` with the rest of the
|
|
27
|
+
* cascade rule; this alias keeps the local call sites reading as they did.
|
|
24
28
|
*/
|
|
25
|
-
|
|
26
|
-
return bf?.refine === true || bf?.inherit === true
|
|
27
|
-
}
|
|
29
|
+
const isRefinement = isFetchRefinement
|
|
28
30
|
|
|
29
31
|
let inheritDeprecationWarned = false
|
|
30
32
|
function warnInheritDeprecation(block) {
|
|
@@ -95,54 +97,39 @@ export default class EntityStore {
|
|
|
95
97
|
return []
|
|
96
98
|
}
|
|
97
99
|
|
|
98
|
-
/**
|
|
99
|
-
* Return a localized copy of a fetch config for collection data.
|
|
100
|
-
* Non-default locales get /{locale} prefixed onto /data/ paths so the
|
|
101
|
-
* client fetches the translated JSON (/fr/data/articles.json).
|
|
102
|
-
*/
|
|
103
|
-
_localizeConfig(cfg, website) {
|
|
104
|
-
if (!cfg.path || !website) return cfg
|
|
105
|
-
const locale = website.getActiveLocale?.()
|
|
106
|
-
const defaultLocale = website.getDefaultLocale?.()
|
|
107
|
-
if (!locale || locale === defaultLocale) return cfg
|
|
108
|
-
if (!cfg.path.startsWith('/data/')) return cfg
|
|
109
|
-
return { ...cfg, path: `/${locale}${cfg.path}` }
|
|
110
|
-
}
|
|
111
|
-
|
|
112
100
|
/**
|
|
113
101
|
* Walk the four-level hierarchy and collect fetch configs for the
|
|
114
102
|
* requested schemas. First match per schema wins.
|
|
103
|
+
*
|
|
104
|
+
* This method's job is to read the four source slots off the object graph;
|
|
105
|
+
* the rule applied to them (precedence, first-match-per-schema, locale
|
|
106
|
+
* normalization, deferred-detail injection) lives in `./fetch-config.js`,
|
|
107
|
+
* shared with every other host that has to answer the same question. Do not
|
|
108
|
+
* re-inline it here — divergence between copies is what the extraction
|
|
109
|
+
* exists to prevent.
|
|
115
110
|
*/
|
|
116
111
|
_findFetchConfigs(block, requested) {
|
|
117
|
-
const configs = new Map()
|
|
118
|
-
const collectAll = requested.length === 0
|
|
119
|
-
const sources = []
|
|
120
|
-
|
|
121
112
|
if (block.fetch?.inherit === true && block.fetch?.refine !== true) {
|
|
122
113
|
warnInheritDeprecation(block)
|
|
123
114
|
}
|
|
124
|
-
if (block.fetch && !isRefinement(block.fetch)) sources.push(block.fetch)
|
|
125
|
-
const page = block.page
|
|
126
|
-
if (page?.fetch) sources.push(page.fetch)
|
|
127
|
-
if (page?.parent?.fetch) sources.push(page.parent.fetch)
|
|
128
|
-
const siteFetch = block.website?.config?.fetch
|
|
129
|
-
if (siteFetch) sources.push(siteFetch)
|
|
130
115
|
|
|
116
|
+
const page = block.page
|
|
131
117
|
const website = block.website
|
|
132
118
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
119
|
+
return resolveFetchConfigs(
|
|
120
|
+
[
|
|
121
|
+
block.fetch && !isRefinement(block.fetch) ? block.fetch : null,
|
|
122
|
+
page?.fetch,
|
|
123
|
+
page?.parent?.fetch,
|
|
124
|
+
website?.config?.fetch,
|
|
125
|
+
],
|
|
126
|
+
{
|
|
127
|
+
schemas: requested,
|
|
128
|
+
locale: website?.getActiveLocale?.() ?? null,
|
|
129
|
+
defaultLocale: website?.getDefaultLocale?.() ?? null,
|
|
130
|
+
collections: website?.config?.collections ?? null,
|
|
131
|
+
},
|
|
132
|
+
)
|
|
146
133
|
}
|
|
147
134
|
|
|
148
135
|
/**
|
|
@@ -167,52 +154,6 @@ export default class EntityStore {
|
|
|
167
154
|
}
|
|
168
155
|
}
|
|
169
156
|
|
|
170
|
-
/**
|
|
171
|
-
* Auto-inject `detail:` on collection refs whose collection has
|
|
172
|
-
* `deferred:` declared. The detail pattern points at the per-record
|
|
173
|
-
* source so the existing dynamic-route singular flow fetches a record
|
|
174
|
-
* with all fields (including the deferred ones) instead of the
|
|
175
|
-
* matched-item-from-the-cascade-collection (without).
|
|
176
|
-
*
|
|
177
|
-
* Two patterns:
|
|
178
|
-
*
|
|
179
|
-
* - Markdown-backed collections (the build emits per-record files
|
|
180
|
-
* at `/data/<name>/<slug>.json`): the auto-injected pattern is
|
|
181
|
-
* that path. `isLocalPath` resolution downstream gives the
|
|
182
|
-
* fetch a `path:` shape.
|
|
183
|
-
*
|
|
184
|
-
* - API-backed collections (the source is a remote URL; the build
|
|
185
|
-
* emits no per-record files): the author declares a `detailUrl:`
|
|
186
|
-
* on the collection — e.g., `/api/articles/{slug}` — and the
|
|
187
|
-
* auto-injected pattern uses it. `isLocalPath` resolution
|
|
188
|
-
* downstream gives the fetch a `url:` shape (because the
|
|
189
|
-
* collection itself has `url:`, not `path:`).
|
|
190
|
-
*
|
|
191
|
-
* Conventions:
|
|
192
|
-
* - Per-record sources are keyed by `item.slug`. The injected
|
|
193
|
-
* pattern uses the `{slug}` placeholder; substitution works when
|
|
194
|
-
* the dynamic route's paramName is 'slug' (the documented
|
|
195
|
-
* convention). Routes with other param names need an explicit
|
|
196
|
-
* author-written `detail:` value.
|
|
197
|
-
* - Author-supplied `cfg.detail` always wins. This helper only fills
|
|
198
|
-
* in the default for collections that have declared deferred fields.
|
|
199
|
-
* - Per-record files are not currently localized; sites needing
|
|
200
|
-
* localized deferred collections write their own `detail:` URL.
|
|
201
|
-
*/
|
|
202
|
-
_applyDeferredDetail(cfg, website) {
|
|
203
|
-
if (cfg.detail !== undefined) return cfg
|
|
204
|
-
const schema = cfg.schema
|
|
205
|
-
if (!schema) return cfg
|
|
206
|
-
const collConfig = website?.config?.collections?.[schema]
|
|
207
|
-
if (!collConfig || typeof collConfig !== 'object') return cfg
|
|
208
|
-
const deferred = Array.isArray(collConfig.deferred) ? collConfig.deferred : null
|
|
209
|
-
if (!deferred || deferred.length === 0) return cfg
|
|
210
|
-
const pattern = typeof collConfig.detailUrl === 'string'
|
|
211
|
-
? collConfig.detailUrl
|
|
212
|
-
: `/data/${schema}/{slug}.json`
|
|
213
|
-
return { ...cfg, detail: pattern }
|
|
214
|
-
}
|
|
215
|
-
|
|
216
157
|
/**
|
|
217
158
|
* Build a detail-URL fetch config from a collection config + dynamic context.
|
|
218
159
|
*
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fetch-config resolution — the shared rule, in one place.
|
|
3
|
+
*
|
|
4
|
+
* "Which fetch configs apply here?" is a framework concept. Authors declare
|
|
5
|
+
* `fetch:` at the section, page, folder and site levels; the framework decides
|
|
6
|
+
* which declaration wins per schema, how a local data path is localized, and
|
|
7
|
+
* when a collection with deferred fields gets a detail pattern injected.
|
|
8
|
+
*
|
|
9
|
+
* Every host that renders a page needs that answer — the browser runtime, the
|
|
10
|
+
* build-time prerenderer, and any server-side renderer. The rule had grown
|
|
11
|
+
* more than one implementation, and they had diverged in both directions (each
|
|
12
|
+
* carrying a level or a semantic the other lacked). This module is the single
|
|
13
|
+
* definition they call.
|
|
14
|
+
*
|
|
15
|
+
* INTENTIONALLY A LEAF: no imports, so it is safe to load anywhere — including
|
|
16
|
+
* environments with no DOM, no filesystem, and a hard bundle-size ceiling.
|
|
17
|
+
* Importing anything here (or from the package root) would defeat that. Keep
|
|
18
|
+
* it dependency-free.
|
|
19
|
+
*
|
|
20
|
+
* WHAT THIS DOES NOT OWN: where the sources come from. A caller holding a live
|
|
21
|
+
* object graph reads them off the graph; a caller holding a content document
|
|
22
|
+
* reads them off the JSON. Both hand the same ordered array to
|
|
23
|
+
* `resolveFetchConfigs`. That difference is real and stays with the caller.
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Is this fetch declaration a per-instance *refinement* of an ancestor's
|
|
28
|
+
* config rather than a new source of its own?
|
|
29
|
+
*
|
|
30
|
+
* The canonical spelling is `refine: true`. The legacy spelling `inherit: true`
|
|
31
|
+
* is still honored; callers that want to warn about it should test for the key
|
|
32
|
+
* themselves — this predicate stays silent so it is safe in any environment.
|
|
33
|
+
*
|
|
34
|
+
* @param {Object} cfg - a fetch declaration
|
|
35
|
+
* @returns {boolean}
|
|
36
|
+
*/
|
|
37
|
+
export function isFetchRefinement(cfg) {
|
|
38
|
+
return cfg?.refine === true || cfg?.inherit === true
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Localize a fetch config that reads a local data path.
|
|
43
|
+
*
|
|
44
|
+
* Non-default locales get `/{locale}` prefixed onto `/data/` paths so the
|
|
45
|
+
* caller reads the translated JSON (`/fr/data/articles.json`). Configs with no
|
|
46
|
+
* `path` (remote `url:` sources), or paths outside `/data/`, pass through
|
|
47
|
+
* untouched — a remote endpoint's localization is the author's business.
|
|
48
|
+
*
|
|
49
|
+
* @param {Object} cfg
|
|
50
|
+
* @param {string|null} locale - the locale being rendered
|
|
51
|
+
* @param {string|null} defaultLocale - the site's default locale
|
|
52
|
+
* @returns {Object} the original config, or a localized copy
|
|
53
|
+
*/
|
|
54
|
+
function localizeConfig(cfg, locale, defaultLocale) {
|
|
55
|
+
if (!cfg.path) return cfg
|
|
56
|
+
if (!locale || locale === defaultLocale) return cfg
|
|
57
|
+
if (!cfg.path.startsWith('/data/')) return cfg
|
|
58
|
+
return { ...cfg, path: `/${locale}${cfg.path}` }
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Auto-inject `detail:` on a collection ref whose collection declares
|
|
63
|
+
* `deferred:` fields.
|
|
64
|
+
*
|
|
65
|
+
* A deferred collection ships a lean list payload, so the full record has to
|
|
66
|
+
* come from somewhere else. Two patterns, picked by what the collection
|
|
67
|
+
* declares:
|
|
68
|
+
*
|
|
69
|
+
* - the collection has `detailUrl:` → use it verbatim (a remote source);
|
|
70
|
+
* - otherwise → `/data/<schema>/{slug}.json`, the per-record file emitted
|
|
71
|
+
* alongside the lean list.
|
|
72
|
+
*
|
|
73
|
+
* Conventions carried from the original implementation:
|
|
74
|
+
* - Per-record sources are keyed by `item.slug`, and the injected pattern
|
|
75
|
+
* uses the `{slug}` placeholder. Substitution works when the dynamic
|
|
76
|
+
* route's paramName is `slug` (the documented convention); a route using
|
|
77
|
+
* another param name needs an explicit author-written `detail:`.
|
|
78
|
+
* - Per-record files are not currently localized. A site needing localized
|
|
79
|
+
* deferred collections writes its own `detail:` URL.
|
|
80
|
+
*
|
|
81
|
+
* An author-supplied `cfg.detail` always wins; this only fills the default.
|
|
82
|
+
* With no `collections` map available the config passes through untouched —
|
|
83
|
+
* deferred-detail injection is an enhancement, never a correctness
|
|
84
|
+
* requirement, so a caller that does not have collection metadata still gets
|
|
85
|
+
* a usable config. That matters for hosts whose content projection may not
|
|
86
|
+
* carry collection metadata at all.
|
|
87
|
+
*
|
|
88
|
+
* @param {Object} cfg
|
|
89
|
+
* @param {Object|null} collections - the site's `config.collections` map
|
|
90
|
+
* @returns {Object} the original config, or a copy carrying `detail`
|
|
91
|
+
*/
|
|
92
|
+
function applyDeferredDetail(cfg, collections) {
|
|
93
|
+
if (cfg.detail !== undefined) return cfg
|
|
94
|
+
const schema = cfg.schema
|
|
95
|
+
if (!schema || !collections) return cfg
|
|
96
|
+
const collConfig = collections[schema]
|
|
97
|
+
if (!collConfig || typeof collConfig !== 'object') return cfg
|
|
98
|
+
const deferred = Array.isArray(collConfig.deferred) ? collConfig.deferred : null
|
|
99
|
+
if (!deferred || deferred.length === 0) return cfg
|
|
100
|
+
const pattern = typeof collConfig.detailUrl === 'string'
|
|
101
|
+
? collConfig.detailUrl
|
|
102
|
+
: `/data/${schema}/{slug}.json`
|
|
103
|
+
return { ...cfg, detail: pattern }
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Resolve the applicable fetch configs from an ordered list of sources.
|
|
108
|
+
*
|
|
109
|
+
* The rule: walk the sources in precedence order and take the FIRST match per
|
|
110
|
+
* schema. Sources are the framework's cascade, most specific first — typically
|
|
111
|
+
* section → page → parent page → site. A source may be a single config or an
|
|
112
|
+
* array of them; arrays are walked in order.
|
|
113
|
+
*
|
|
114
|
+
* First-match-per-schema (rather than first-match-wins-outright) is what lets
|
|
115
|
+
* a page needing two schemas inherit one from the site and declare the other
|
|
116
|
+
* itself. Collapsing that to a single winner is a real behavior change, not a
|
|
117
|
+
* simplification.
|
|
118
|
+
*
|
|
119
|
+
* @param {Array<Object|Array<Object>>} sources - ordered, most specific first.
|
|
120
|
+
* Falsy entries are skipped, so callers can pass optional levels directly.
|
|
121
|
+
* @param {Object} [options]
|
|
122
|
+
* @param {string[]} [options.schemas] - restrict to these schema names.
|
|
123
|
+
* Empty (the default) collects every schema found.
|
|
124
|
+
* @param {string|null} [options.locale] - the locale being rendered
|
|
125
|
+
* @param {string|null} [options.defaultLocale] - the site's default locale
|
|
126
|
+
* @param {Object|null} [options.collections] - the site's `config.collections`
|
|
127
|
+
* @returns {Map<string, Object>} schema name → resolved config
|
|
128
|
+
*/
|
|
129
|
+
export function resolveFetchConfigs(sources, options = {}) {
|
|
130
|
+
const {
|
|
131
|
+
schemas = [],
|
|
132
|
+
locale = null,
|
|
133
|
+
defaultLocale = null,
|
|
134
|
+
collections = null,
|
|
135
|
+
} = options
|
|
136
|
+
|
|
137
|
+
const configs = new Map()
|
|
138
|
+
const collectAll = schemas.length === 0
|
|
139
|
+
|
|
140
|
+
for (const source of sources) {
|
|
141
|
+
if (!source) continue
|
|
142
|
+
const configList = Array.isArray(source) ? source : [source]
|
|
143
|
+
for (const cfg of configList) {
|
|
144
|
+
if (!cfg?.schema) continue
|
|
145
|
+
if (configs.has(cfg.schema)) continue
|
|
146
|
+
if (!collectAll && !schemas.includes(cfg.schema)) continue
|
|
147
|
+
const localized = localizeConfig(cfg, locale, defaultLocale)
|
|
148
|
+
configs.set(cfg.schema, applyDeferredDetail(localized, collections))
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return configs
|
|
153
|
+
}
|