@uniweb/kit 0.13.5 → 0.13.7
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/kit",
|
|
3
|
-
"version": "0.13.
|
|
3
|
+
"version": "0.13.7",
|
|
4
4
|
"description": "Standard component library for Uniweb foundations",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -44,7 +44,7 @@
|
|
|
44
44
|
"shiki": "^3.0.0",
|
|
45
45
|
"tailwind-merge": "^3.6.0",
|
|
46
46
|
"temml": "^0.13.2",
|
|
47
|
-
"@uniweb/core": "^0.
|
|
47
|
+
"@uniweb/core": "^0.13.0",
|
|
48
48
|
"@uniweb/semantic-parser": "^1.3.1",
|
|
49
49
|
"@uniweb/scene": "^0.1.3"
|
|
50
50
|
},
|
|
@@ -44,7 +44,7 @@
|
|
|
44
44
|
* }
|
|
45
45
|
*/
|
|
46
46
|
|
|
47
|
-
import { getUniweb,
|
|
47
|
+
import { getUniweb, resolveFetchConfigs, buildDetailConfig } from '@uniweb/core'
|
|
48
48
|
import { useFetched } from './useFetched.js'
|
|
49
49
|
|
|
50
50
|
/**
|
|
@@ -60,46 +60,68 @@ import { useFetched } from './useFetched.js'
|
|
|
60
60
|
export function useEntityDetail(record, options = {}) {
|
|
61
61
|
const collection = options?.collection
|
|
62
62
|
const request = buildDetailRequest(record, collection)
|
|
63
|
-
|
|
63
|
+
const result = useFetched(request)
|
|
64
|
+
|
|
65
|
+
// No separate detail source for this collection — nothing was stripped from
|
|
66
|
+
// the cascade, so the record the caller already holds IS the whole record.
|
|
67
|
+
// Returning it beats both alternatives: `null` makes every caller null-check
|
|
68
|
+
// a case that cannot fail, and requesting the per-record file anyway is a
|
|
69
|
+
// guaranteed 404, because that file is only written for a `deferred:`
|
|
70
|
+
// collection. `useFetched` is still called above — unconditionally, as the
|
|
71
|
+
// rules of hooks require — and simply skips on a null request.
|
|
72
|
+
if (record && collection && !request) {
|
|
73
|
+
return { data: record, error: null, loading: false }
|
|
74
|
+
}
|
|
75
|
+
return result
|
|
64
76
|
}
|
|
65
77
|
|
|
66
78
|
/**
|
|
67
79
|
* Build the fetch request for one record's full payload.
|
|
68
80
|
*
|
|
69
|
-
*
|
|
70
|
-
*
|
|
71
|
-
*
|
|
72
|
-
*
|
|
81
|
+
* ⛔ THIS MUST NOT DECIDE THE ADDRESS ITSELF, and it used to. It read
|
|
82
|
+
* `config.collections[name].detailUrl` directly and otherwise composed
|
|
83
|
+
* `/data/<name>/<slug>.json` by hand, which was wrong three ways: it 404'd on
|
|
84
|
+
* any collection without `deferred:` (that file is only written for one), it
|
|
85
|
+
* could not see a host's live record lane at all, and its hand-rolled `{slug}`
|
|
86
|
+
* replace ignored a route whose param is named anything else.
|
|
87
|
+
*
|
|
88
|
+
* Every one of those is already solved once, in the resolution the runtime and
|
|
89
|
+
* the prerenderer share. So this hands the collection to that resolution and
|
|
90
|
+
* asks it the same question they ask: `resolveFetchConfigs` decides where the
|
|
91
|
+
* collection lives (a host's lane, or the compiled artifact) and what its
|
|
92
|
+
* per-record source is, then `buildDetailConfig` turns that plus a param into a
|
|
93
|
+
* request. A fourth answer computed here is a fourth thing to drift.
|
|
94
|
+
*
|
|
95
|
+
* Returns null when the collection has no separate detail source — the common
|
|
96
|
+
* case, and not a failure. The caller's record is already whole.
|
|
97
|
+
*
|
|
98
|
+
* Exported for tests only — not re-exported from the package index.
|
|
73
99
|
*
|
|
74
100
|
* @param {Object|null} record
|
|
75
101
|
* @param {string} collection
|
|
76
|
-
* @returns {{path?: string, url?: string, schema: string}|null}
|
|
102
|
+
* @returns {{path?: string, url?: string, endpoint?: string, schema: string}|null}
|
|
77
103
|
*/
|
|
78
104
|
export function buildDetailRequest(record, collection) {
|
|
79
105
|
const slug = record?.slug
|
|
80
106
|
if (!slug || !collection) return null
|
|
81
107
|
|
|
82
|
-
// Look up the collection's per-record source pattern. Authors with
|
|
83
|
-
// API-backed collections declare `detailUrl:` in site.yml; markdown
|
|
84
|
-
// collections leave it null and use the static-file default.
|
|
85
108
|
const website = getUniweb()?.activeWebsite
|
|
86
|
-
const
|
|
87
|
-
const detailUrl = (collConfig && typeof collConfig.detailUrl === 'string')
|
|
88
|
-
? collConfig.detailUrl
|
|
89
|
-
: null
|
|
109
|
+
const config = website?.config
|
|
90
110
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
111
|
+
// One synthetic source, resolved by the shared rule — same inputs the
|
|
112
|
+
// EntityStore passes, so the hook cannot disagree with the page it sits on.
|
|
113
|
+
const resolved = resolveFetchConfigs([{ collection, schema: collection }], {
|
|
114
|
+
collections: config?.collections ?? null,
|
|
115
|
+
records: config?.records ?? null,
|
|
116
|
+
locale: website?.getActiveLocale?.() ?? null,
|
|
117
|
+
defaultLocale: website?.getDefaultLocale?.() ?? null,
|
|
118
|
+
}).get(collection)
|
|
119
|
+
|
|
120
|
+
if (!resolved) return null
|
|
97
121
|
|
|
98
|
-
//
|
|
99
|
-
// `
|
|
100
|
-
|
|
101
|
-
// same feature: the three must resolve one record to one URL.
|
|
102
|
-
return { path: recordDataUrl(collection, slug), schema: collection }
|
|
122
|
+
// `slug` is this hook's contract with its caller (the record must carry one).
|
|
123
|
+
// The `{param}` alias means a host-written record pattern resolves too.
|
|
124
|
+
return buildDetailConfig(resolved, { paramName: 'slug', paramValue: slug })
|
|
103
125
|
}
|
|
104
126
|
|
|
105
127
|
export default useEntityDetail
|
package/src/search/client.js
CHANGED
|
@@ -142,6 +142,37 @@ export function createSearchClient(website, options = {}) {
|
|
|
142
142
|
// host serves none.
|
|
143
143
|
const authoredProvider = website?.config?.search?.provider
|
|
144
144
|
|
|
145
|
+
// ⛔ …BUT AN AUTHORED `index` IS NOT A CHOICE, IT IS THE DEFAULT SPELLED OUT.
|
|
146
|
+
// `getSearchConfig()` fills `provider: 'index'` when the author said nothing,
|
|
147
|
+
// and `docs/authoring/search.md` shows it in its options block labelled
|
|
148
|
+
// "default" — so a site that copied that block to set `include:` or
|
|
149
|
+
// `exclude:` carries it without having decided anything.
|
|
150
|
+
//
|
|
151
|
+
// Treating it as a veto pinned exactly those sites to the local index. On a
|
|
152
|
+
// host-served site that index does not exist (the build stops emitting one on
|
|
153
|
+
// that lane), so an ENTITLED site — one whose host does offer search — drew a
|
|
154
|
+
// box, queried `search-index.json`, and got a 404. They had bought the
|
|
155
|
+
// feature and could not use it.
|
|
156
|
+
//
|
|
157
|
+
// ⇒ An authored `index` no longer outranks a host's offer. Any OTHER authored
|
|
158
|
+
// provider still does: `endpoint` and a foundation transport are real choices
|
|
159
|
+
// and the comment above still applies to them.
|
|
160
|
+
//
|
|
161
|
+
// ⚠️ This removes one thing an author could previously express — "use the
|
|
162
|
+
// local index even though my host offers search". On a host-served lane that
|
|
163
|
+
// preference resolves to a 404 either way, and on a static host `source` is
|
|
164
|
+
// never 'host' so this branch cannot fire and the local index is untouched.
|
|
165
|
+
//
|
|
166
|
+
// ⛔ REACHES NEW AND REBUILT FOUNDATIONS ONLY. A foundation bundles its own
|
|
167
|
+
// frozen copy of this file, so an already-published one keeps the old
|
|
168
|
+
// precedence. Unlike the host-decline check — which lives in `@uniweb/core`
|
|
169
|
+
// precisely so the runtime carries it to every foundation regardless of age —
|
|
170
|
+
// provider selection is kit's, and frozen kit reads `website.config` directly.
|
|
171
|
+
// There is no honest core-side fix: it would mean rewriting authored config
|
|
172
|
+
// to steer a consumer's branch.
|
|
173
|
+
const authoredVeto =
|
|
174
|
+
authoredProvider && authoredProvider !== 'index' ? authoredProvider : null
|
|
175
|
+
|
|
145
176
|
// ⛔ THE HOST ANSWERED AND DECLINED. `resolveService` returns
|
|
146
177
|
// `{ url: null, source: 'host' }` when a host declares the service NAME
|
|
147
178
|
// while offering no address — the decline shape `@uniweb/core/services`
|
|
@@ -173,7 +204,7 @@ export function createSearchClient(website, options = {}) {
|
|
|
173
204
|
|
|
174
205
|
const declared =
|
|
175
206
|
providerOverride ||
|
|
176
|
-
|
|
207
|
+
authoredVeto ||
|
|
177
208
|
(service.source === 'host' ? 'endpoint' : 'index')
|
|
178
209
|
|
|
179
210
|
// One in-flight resolution shared by every caller.
|
|
@@ -168,21 +168,32 @@ export function createEndpointProvider(website, options = {}) {
|
|
|
168
168
|
// more than "403". `isEnabled()` is what a foundation asks, and it is
|
|
169
169
|
// false long before this line runs.
|
|
170
170
|
//
|
|
171
|
-
//
|
|
172
|
-
//
|
|
173
|
-
//
|
|
174
|
-
//
|
|
171
|
+
// ⛔ THE HOST'S SENTENCE GOES TO THE CONSOLE, NEVER INTO THE THROW.
|
|
172
|
+
//
|
|
173
|
+
// It used to become the Error's message, and `useSearch` exposes
|
|
174
|
+
// `error` — so a foundation rendering it would have shown a visitor a
|
|
175
|
+
// host-authored English sentence about someone's provisioning state.
|
|
176
|
+
// The only thing preventing that was `client.js`'s local-index
|
|
177
|
+
// fallback swallowing the throw, which is incidental: that fallback
|
|
178
|
+
// exists to DEGRADE, and removing it (reasonable — on a host-served
|
|
179
|
+
// lane there is no index to fall back to) would have surfaced the
|
|
180
|
+
// sentence as its first act.
|
|
181
|
+
//
|
|
182
|
+
// ⇒ Splitting the two registers removes the ordering hazard entirely
|
|
183
|
+
// rather than documenting it: whatever reaches `error` is now
|
|
184
|
+
// framework-authored and carries no host prose, so the fallback can be
|
|
185
|
+
// removed on its own merits whenever someone wants to.
|
|
175
186
|
let declined
|
|
176
187
|
try {
|
|
177
188
|
declined = (await response.json())?.error
|
|
178
189
|
} catch {
|
|
179
190
|
/* not JSON — the status is all there is */
|
|
180
191
|
}
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
)
|
|
192
|
+
if (typeof declined === 'string' && declined.trim()) {
|
|
193
|
+
// Developer register: a console reader needs more than a number.
|
|
194
|
+
console.warn(`[uniweb] search endpoint declined: ${declined.trim()}`)
|
|
195
|
+
}
|
|
196
|
+
throw new Error(`Search endpoint returned ${response.status}`)
|
|
186
197
|
}
|
|
187
198
|
|
|
188
199
|
const payload = await response.json()
|