@uniweb/kit 0.13.4 → 0.13.5

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.4",
3
+ "version": "0.13.5",
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/core": "^0.11.3",
48
- "@uniweb/semantic-parser": "^1.3.0",
47
+ "@uniweb/core": "^0.12.0",
48
+ "@uniweb/semantic-parser": "^1.3.1",
49
49
  "@uniweb/scene": "^0.1.3"
50
50
  },
51
51
  "peerDependencies": {
@@ -10,12 +10,28 @@
10
10
  *
11
11
  * Resolution order:
12
12
  * 1. `site.yml search.provider` — the author's explicit choice
13
- * 2. `'index'` — the free default, works on any host
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
- * A future tier sits between them: a host that serves search declaring it in
16
- * the payload it already sends, the way `config.base` already tells the runtime
17
- * where the site is being served from. That wire key is the host's to specify,
18
- * so it is deliberately not invented here.
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,6 +142,35 @@ export function createSearchClient(website, options = {}) {
126
142
  // host serves none.
127
143
  const authoredProvider = website?.config?.search?.provider
128
144
 
145
+ // ⛔ THE HOST ANSWERED AND DECLINED. `resolveService` returns
146
+ // `{ url: null, source: 'host' }` when a host declares the service NAME
147
+ // while offering no address — the decline shape `@uniweb/core/services`
148
+ // documents ("absence is a behavioural decision rather than a message").
149
+ //
150
+ // Falling through to `'index'` here would be wrong twice: the host has said
151
+ // it does not serve search, and on a host-served lane the framework emits no
152
+ // index to fall back to, so the fallback resolves to a 404. Draw nothing
153
+ // instead — `isEnabled()` reports false and `query()` returns empty without
154
+ // a request.
155
+ //
156
+ // ⚠️ Only a DECLINE does this. `source: null` — nobody declared the service
157
+ // at all — still means `'index'`, which is correct and is the static-host
158
+ // path: there the build DOES emit the index.
159
+ //
160
+ // ⛔ DO NOT ADD A FALLBACK THAT FIRES ON THIS SHAPE. The contract, settled
161
+ // with a host implementation 2026-08-25: **a host that OFFERS a service must
162
+ // declare it WITH an address** — offered is `{endpoint}`, declined is `{}`.
163
+ // Routing `{}` to `endpoint-provider`'s `DEFAULT_ENDPOINT` looks like a free
164
+ // saving and makes an offered service indistinguishable from a declined one:
165
+ // no search box, no error, nothing to grep for.
166
+ //
167
+ // ⭐ A site's OWN declaration still wins — `resolveService` answers tier 1
168
+ // with `source: 'site'`, so an operator running self-hosted search on a host
169
+ // that does not sell it is untouched. Pinned in
170
+ // `tests/search-host-decline.test.js`; that is the case worth re-running if
171
+ // this branch is ever edited, because the headline case never breaks.
172
+ const hostDeclined = service.source === 'host' && !service.url
173
+
129
174
  const declared =
130
175
  providerOverride ||
131
176
  authoredProvider ||
@@ -193,11 +238,21 @@ export function createSearchClient(website, options = {}) {
193
238
 
194
239
  return {
195
240
  /**
196
- * Check if search is enabled
241
+ * Check if search is enabled.
242
+ *
243
+ * Two independent ways it is off, and a caller needs neither to tell them
244
+ * apart: the SITE disabled it (`search: false` / `search: { enabled:
245
+ * false }`), or the HOST declared the service and offered no address —
246
+ * see `hostDeclined` above. Either way a foundation draws no search UI.
247
+ *
248
+ * ⛔ Deliberately no reason string. Which services a site is provisioned
249
+ * for is not a visitor's business and could not be localized from a public
250
+ * package anyway — the rule `@uniweb/core/services` states at length.
251
+ *
197
252
  * @returns {boolean}
198
253
  */
199
254
  isEnabled() {
200
- return website.isSearchEnabled()
255
+ return website.isSearchEnabled() && !hostDeclined
201
256
  },
202
257
 
203
258
  /**
@@ -273,6 +328,13 @@ export function createSearchClient(website, options = {}) {
273
328
  return { results: [], total: 0 }
274
329
  }
275
330
 
331
+ // The host declared the service and offered no address. Return empty
332
+ // rather than requesting anything — the alternative is a fetch we
333
+ // already know the answer to. Silent: a foundation that checked
334
+ // `isEnabled()` never reaches this, and one that did not should not fill
335
+ // a visitor's console over a service the site was never provisioned for.
336
+ if (hostDeclined) return { results: [], total: 0 }
337
+
276
338
  const opts = { limit, type, route, signal }
277
339
 
278
340
  try {
@@ -16,7 +16,29 @@
16
16
  import { emptyResult } from './result.js'
17
17
  import { resolveServiceUrl } from '../../utils/services.js'
18
18
 
19
- /** Path used when a site declares `provider: endpoint` without an `endpoint:`. */
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,16 +148,30 @@ export function createEndpointProvider(website, options = {}) {
126
148
  })
127
149
 
128
150
  if (!response.ok) {
129
- // An endpoint that declines usually explains why, in words meant for a
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.
152
+ //
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.
133
170
  //
134
- // This does not change the degradation: a failed search still falls
135
- // back to the local index, because a failed READ should degrade and
136
- // that guarantee is older than this. What changes is that when there is
137
- // no index to fall back to, the error a foundation can surface says
138
- // something a person understands instead of a number.
171
+ // ⚠️ Ordering, if the local-index fallback in `client.js` is ever
172
+ // removed: that fallback is currently what stops this string
173
+ // propagating, so removing it would surface this message where nothing
174
+ // renders one today. Settle where a provider error may surface FIRST.
139
175
  let declined
140
176
  try {
141
177
  declined = (await response.json())?.error