@uniweb/kit 0.13.4 → 0.13.6
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.6",
|
|
4
4
|
"description": "Standard component library for Uniweb foundations",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -44,8 +44,8 @@
|
|
|
44
44
|
"shiki": "^3.0.0",
|
|
45
45
|
"tailwind-merge": "^3.6.0",
|
|
46
46
|
"temml": "^0.13.2",
|
|
47
|
-
"@uniweb/
|
|
48
|
-
"@uniweb/
|
|
47
|
+
"@uniweb/semantic-parser": "^1.3.1",
|
|
48
|
+
"@uniweb/core": "^0.12.1",
|
|
49
49
|
"@uniweb/scene": "^0.1.3"
|
|
50
50
|
},
|
|
51
51
|
"peerDependencies": {
|
package/src/search/client.js
CHANGED
|
@@ -10,12 +10,28 @@
|
|
|
10
10
|
*
|
|
11
11
|
* Resolution order:
|
|
12
12
|
* 1. `site.yml search.provider` — the author's explicit choice
|
|
13
|
-
* 2.
|
|
13
|
+
* 2. the HOST's offer — `config.services.search` in the payload,
|
|
14
|
+
* read through `resolveService` → `endpoint`
|
|
15
|
+
* 3. `'index'` — the free default, works on any host
|
|
14
16
|
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
17
|
+
* ⛔ Tier 2 SHIPPED. This block described it as "a future tier … deliberately
|
|
18
|
+
* not invented here" until 2026-08-25, while `resolveService` was imported at
|
|
19
|
+
* the top of this file and read ~100 lines below. The wire key is
|
|
20
|
+
* `config.services.search`, resolved by `@uniweb/core/services` on the same
|
|
21
|
+
* two-tier rule every site service uses (site-authored wins, host fills gaps).
|
|
22
|
+
*
|
|
23
|
+
* ⚠️ That stale sentence had a cost outside this repo. The hosting lane could
|
|
24
|
+
* not tell from reading whether kit consumed the block, asked in a collab
|
|
25
|
+
* channel, and the answer mattered: on the sync/link lane the framework emits
|
|
26
|
+
* NO search index at all (removed 2026-08-01, `@uniweb/build`
|
|
27
|
+
* `site/build-site-data.js`), so tier 3 requests a `search-index.json` that
|
|
28
|
+
* nothing on that lane writes. A reader of this comment concluded the host tier
|
|
29
|
+
* did not exist and the block was decoration; both halves were wrong.
|
|
30
|
+
*
|
|
31
|
+
* ⭐ So tier 3 is "the free default" only where something EMITS the index —
|
|
32
|
+
* the bundle lane and any static host. It is not a universal fallback, and a
|
|
33
|
+
* host that answers search at its own address must be declared for kit to
|
|
34
|
+
* reach it.
|
|
19
35
|
*
|
|
20
36
|
* Provider contract (duck-typed, matching how the FetcherDispatcher treats
|
|
21
37
|
* transports):
|
|
@@ -126,9 +142,69 @@ export function createSearchClient(website, options = {}) {
|
|
|
126
142
|
// host serves none.
|
|
127
143
|
const authoredProvider = website?.config?.search?.provider
|
|
128
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
|
+
|
|
176
|
+
// ⛔ THE HOST ANSWERED AND DECLINED. `resolveService` returns
|
|
177
|
+
// `{ url: null, source: 'host' }` when a host declares the service NAME
|
|
178
|
+
// while offering no address — the decline shape `@uniweb/core/services`
|
|
179
|
+
// documents ("absence is a behavioural decision rather than a message").
|
|
180
|
+
//
|
|
181
|
+
// Falling through to `'index'` here would be wrong twice: the host has said
|
|
182
|
+
// it does not serve search, and on a host-served lane the framework emits no
|
|
183
|
+
// index to fall back to, so the fallback resolves to a 404. Draw nothing
|
|
184
|
+
// instead — `isEnabled()` reports false and `query()` returns empty without
|
|
185
|
+
// a request.
|
|
186
|
+
//
|
|
187
|
+
// ⚠️ Only a DECLINE does this. `source: null` — nobody declared the service
|
|
188
|
+
// at all — still means `'index'`, which is correct and is the static-host
|
|
189
|
+
// path: there the build DOES emit the index.
|
|
190
|
+
//
|
|
191
|
+
// ⛔ DO NOT ADD A FALLBACK THAT FIRES ON THIS SHAPE. The contract, settled
|
|
192
|
+
// with a host implementation 2026-08-25: **a host that OFFERS a service must
|
|
193
|
+
// declare it WITH an address** — offered is `{endpoint}`, declined is `{}`.
|
|
194
|
+
// Routing `{}` to `endpoint-provider`'s `DEFAULT_ENDPOINT` looks like a free
|
|
195
|
+
// saving and makes an offered service indistinguishable from a declined one:
|
|
196
|
+
// no search box, no error, nothing to grep for.
|
|
197
|
+
//
|
|
198
|
+
// ⭐ A site's OWN declaration still wins — `resolveService` answers tier 1
|
|
199
|
+
// with `source: 'site'`, so an operator running self-hosted search on a host
|
|
200
|
+
// that does not sell it is untouched. Pinned in
|
|
201
|
+
// `tests/search-host-decline.test.js`; that is the case worth re-running if
|
|
202
|
+
// this branch is ever edited, because the headline case never breaks.
|
|
203
|
+
const hostDeclined = service.source === 'host' && !service.url
|
|
204
|
+
|
|
129
205
|
const declared =
|
|
130
206
|
providerOverride ||
|
|
131
|
-
|
|
207
|
+
authoredVeto ||
|
|
132
208
|
(service.source === 'host' ? 'endpoint' : 'index')
|
|
133
209
|
|
|
134
210
|
// One in-flight resolution shared by every caller.
|
|
@@ -193,11 +269,21 @@ export function createSearchClient(website, options = {}) {
|
|
|
193
269
|
|
|
194
270
|
return {
|
|
195
271
|
/**
|
|
196
|
-
* Check if search is enabled
|
|
272
|
+
* Check if search is enabled.
|
|
273
|
+
*
|
|
274
|
+
* Two independent ways it is off, and a caller needs neither to tell them
|
|
275
|
+
* apart: the SITE disabled it (`search: false` / `search: { enabled:
|
|
276
|
+
* false }`), or the HOST declared the service and offered no address —
|
|
277
|
+
* see `hostDeclined` above. Either way a foundation draws no search UI.
|
|
278
|
+
*
|
|
279
|
+
* ⛔ Deliberately no reason string. Which services a site is provisioned
|
|
280
|
+
* for is not a visitor's business and could not be localized from a public
|
|
281
|
+
* package anyway — the rule `@uniweb/core/services` states at length.
|
|
282
|
+
*
|
|
197
283
|
* @returns {boolean}
|
|
198
284
|
*/
|
|
199
285
|
isEnabled() {
|
|
200
|
-
return website.isSearchEnabled()
|
|
286
|
+
return website.isSearchEnabled() && !hostDeclined
|
|
201
287
|
},
|
|
202
288
|
|
|
203
289
|
/**
|
|
@@ -273,6 +359,13 @@ export function createSearchClient(website, options = {}) {
|
|
|
273
359
|
return { results: [], total: 0 }
|
|
274
360
|
}
|
|
275
361
|
|
|
362
|
+
// The host declared the service and offered no address. Return empty
|
|
363
|
+
// rather than requesting anything — the alternative is a fetch we
|
|
364
|
+
// already know the answer to. Silent: a foundation that checked
|
|
365
|
+
// `isEnabled()` never reaches this, and one that did not should not fill
|
|
366
|
+
// a visitor's console over a service the site was never provisioned for.
|
|
367
|
+
if (hostDeclined) return { results: [], total: 0 }
|
|
368
|
+
|
|
276
369
|
const opts = { limit, type, route, signal }
|
|
277
370
|
|
|
278
371
|
try {
|
|
@@ -16,7 +16,29 @@
|
|
|
16
16
|
import { emptyResult } from './result.js'
|
|
17
17
|
import { resolveServiceUrl } from '../../utils/services.js'
|
|
18
18
|
|
|
19
|
-
/**
|
|
19
|
+
/**
|
|
20
|
+
* Path used when a site declares `provider: endpoint` without an `endpoint:`.
|
|
21
|
+
*
|
|
22
|
+
* ⛔ THIS COVERS THE AUTHORED TIER ONLY — it is unreachable for a HOST-declared
|
|
23
|
+
* service, and it must stay that way.
|
|
24
|
+
*
|
|
25
|
+
* A host that declares the service name while offering no address is a
|
|
26
|
+
* **decline** (`@uniweb/core/services`), and `client.js` short-circuits on it
|
|
27
|
+
* before any provider is selected. So `{ "search": {} }` from a host means
|
|
28
|
+
* "draw nothing", never "use the default".
|
|
29
|
+
*
|
|
30
|
+
* ⇒ **The contract, settled with a host implementation 2026-08-25: a host that
|
|
31
|
+
* OFFERS a service must declare it WITH an address.** Offered is `{endpoint}`;
|
|
32
|
+
* declined is `{}`. The two are one byte apart and the failure is silent — an
|
|
33
|
+
* entitled site drawing no search box.
|
|
34
|
+
*
|
|
35
|
+
* ⚠️ **The saving that looks available from either side, and is not.** From
|
|
36
|
+
* here: *"a host need not send an address, kit defaults to `_search` anyway."*
|
|
37
|
+
* From the emitter: *"why emit `/_search` when kit defaults to it?"* **Both
|
|
38
|
+
* collapse the two states into one.** It was proposed and withdrawn the day
|
|
39
|
+
* this was written, by the lane that had just shipped the decline branch — the
|
|
40
|
+
* hazard does not look like a hazard while you are authoring it.
|
|
41
|
+
*/
|
|
20
42
|
const DEFAULT_ENDPOINT = '_search'
|
|
21
43
|
|
|
22
44
|
/**
|
|
@@ -126,27 +148,52 @@ export function createEndpointProvider(website, options = {}) {
|
|
|
126
148
|
})
|
|
127
149
|
|
|
128
150
|
if (!response.ok) {
|
|
129
|
-
//
|
|
130
|
-
// visitor rather than a status code — "Search is not available on this
|
|
131
|
-
// site." Read it and carry it, the same way `submitForm` does with a
|
|
132
|
-
// rejected submission.
|
|
151
|
+
// ⛔ A HOST'S DECLINE TEXT IS A DIAGNOSTIC, NEVER VISITOR COPY.
|
|
133
152
|
//
|
|
134
|
-
// This
|
|
135
|
-
//
|
|
136
|
-
//
|
|
137
|
-
//
|
|
138
|
-
//
|
|
153
|
+
// This block used to argue the opposite — that an endpoint "explains
|
|
154
|
+
// why, in words meant for a visitor", so kit should carry that sentence
|
|
155
|
+
// up for a foundation to render. That is wrong, and it was ruled
|
|
156
|
+
// against on 2026-08-25: a control for a service the site does not have
|
|
157
|
+
// must not be DRAWN in the first place, so there is no place for an
|
|
158
|
+
// apology and nothing for a visitor to read. A site with no search is
|
|
159
|
+
// not broken; it simply has no search, the same way it has no contact
|
|
160
|
+
// form when submissions are not enabled.
|
|
161
|
+
//
|
|
162
|
+
// Two reasons it cannot be visitor copy even if one wanted it to be:
|
|
163
|
+
// it reports the operator's provisioning state to the public, and it
|
|
164
|
+
// arrives in one language from a service layer, bypassing the site's
|
|
165
|
+
// localization entirely. Any text a visitor reads is site content.
|
|
166
|
+
//
|
|
167
|
+
// ⇒ The message is kept because a developer reading a console needs
|
|
168
|
+
// more than "403". `isEnabled()` is what a foundation asks, and it is
|
|
169
|
+
// false long before this line runs.
|
|
170
|
+
//
|
|
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.
|
|
139
186
|
let declined
|
|
140
187
|
try {
|
|
141
188
|
declined = (await response.json())?.error
|
|
142
189
|
} catch {
|
|
143
190
|
/* not JSON — the status is all there is */
|
|
144
191
|
}
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
)
|
|
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}`)
|
|
150
197
|
}
|
|
151
198
|
|
|
152
199
|
const payload = await response.json()
|