@uniweb/build 0.16.10 → 0.16.12

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/build",
3
- "version": "0.16.10",
3
+ "version": "0.16.12",
4
4
  "description": "Build tooling for the Uniweb Component Web Platform",
5
5
  "type": "module",
6
6
  "exports": {
@@ -59,15 +59,15 @@
59
59
  "js-yaml": "^4.1.0",
60
60
  "sharp": "^0.35.3",
61
61
  "yaml": "^2.5.0",
62
- "@uniweb/content-writer": "0.3.3",
62
+ "@uniweb/theming": "0.1.15",
63
63
  "@uniweb/projections": "0.2.5",
64
- "@uniweb/theming": "0.1.15"
64
+ "@uniweb/content-writer": "0.3.3"
65
65
  },
66
66
  "optionalDependencies": {
67
67
  "@uniweb/content-reader": "1.2.2",
68
- "@uniweb/runtime": "0.9.4",
69
- "@uniweb/semantic-parser": "1.2.1",
70
- "@uniweb/schemas": "0.2.5"
68
+ "@uniweb/schemas": "0.2.5",
69
+ "@uniweb/runtime": "0.9.5",
70
+ "@uniweb/semantic-parser": "1.2.1"
71
71
  },
72
72
  "peerDependencies": {
73
73
  "vite": "^5.0.0 || ^6.0.0 || ^7.0.0",
@@ -27,10 +27,6 @@ import { processCollections, writeCollectionFiles } from './collection-processor
27
27
  import { processAssets, rewriteSiteContentPaths } from './asset-processor.js'
28
28
  import { processAdvancedAssets } from './advanced-processors.js'
29
29
  import {
30
- generateSearchIndex,
31
- generateCollectionIndex,
32
- mergeSearchIndexes,
33
- getSearchIndexFilename,
34
30
  renderSiteIndex,
35
31
  renderPageMarkdown,
36
32
  resolveAgentsConfig,
@@ -205,58 +201,26 @@ export async function buildSiteData({
205
201
  const contentPath = join(resolvedDistDir, 'site-content.json')
206
202
  await writeFile(contentPath, JSON.stringify(finalContent, null, 2))
207
203
 
208
- // 5. Generate `_search/{locale}/pages.json` and per-collection indexes.
209
- // Gated by `features: [search]` in site.yml consistent with the billing
210
- // model where features: is the single declaration of intent. The hosting
211
- // worker has its own entitlement gate (searchEnabled) that decides whether
212
- // to store these files, but no point generating them when the site hasn't
213
- // declared the feature at all.
214
- const siteFeatures = finalContent.config?.features || []
215
- if (Array.isArray(siteFeatures) && siteFeatures.includes('search')) {
216
- const defaultLocale = resolveDefaultLocale(finalContent.config)
217
- const searchDir = join(resolvedDistDir, '_search', defaultLocale)
218
- await mkdir(searchDir, { recursive: true })
219
-
220
- // pages.json static pages + sections
221
- const pagesIndex = generateSearchIndex(finalContent, {
222
- locale: defaultLocale,
223
- search: finalContent.config?.search,
224
- })
225
- const { version: _v, count: _c, ...pagesRest } = pagesIndex
226
- await writeFile(join(searchDir, 'pages.json'), JSON.stringify({ type: 'pages', ...pagesRest }))
227
-
228
- // Collection indexes — one per routed + search-configured collection
229
- const collections = finalContent.config?.collections || {}
230
- const collectionIndexes = []
231
- for (const [collName, collConfig] of Object.entries(collections)) {
232
- if (!collConfig.search?.enabled || !collConfig.route) continue
233
- const cascadeFile = join(resolvedDistDir, DATA_DIR, `${collName}.json`)
234
- if (!existsSync(cascadeFile)) continue
235
- let collectionData
236
- try {
237
- collectionData = JSON.parse(await readFile(cascadeFile, 'utf8'))
238
- } catch {
239
- continue
240
- }
241
- const collIndex = generateCollectionIndex(collName, collConfig, collectionData, defaultLocale)
242
- collectionIndexes.push(collIndex)
243
- await writeFile(join(searchDir, `${collName}.json`), JSON.stringify(collIndex))
244
- }
245
-
246
- // The single-file form, for the BROWSER lane.
247
- //
248
- // The split files above serve a server that loads only the parts a query
249
- // needs. Kit's client-side `index` provider needs all of it, and asks for
250
- // `search-index.json` — so without this, a site published through this lane
251
- // 404s on its own search index and degrades to no results. Emitting both
252
- // from the same entries is one extra serialization and means the client
253
- // works identically on every lane, with no host configuration describing
254
- // where the index lives.
255
- await writeFile(
256
- join(resolvedDistDir, getSearchIndexFilename(defaultLocale, defaultLocale)),
257
- JSON.stringify(mergeSearchIndexes(pagesIndex, collectionIndexes))
258
- )
259
- }
204
+ // 5. (removed 2026-08-01) This lane used to emit a search index — the split
205
+ // `_search/{locale}/*.json` for a server, and `search-index.json` for the
206
+ // browser gated by `features: [search]`.
207
+ //
208
+ // Both are gone because only ONE of the two publishers produced them. A
209
+ // CLI deploy did; a CMS publish did not, so a site's search existed or
210
+ // vanished depending on who published it last. That is the flicker rule,
211
+ // and the fix is not to make the app produce them too — it is that a host
212
+ // storing the content derives search from it, one input that exists
213
+ // identically on both lanes.
214
+ //
215
+ // The browser one was doubly dead: nothing ever uploaded it. `dist/` on
216
+ // this lane reaches a backend only through the data ball and the media
217
+ // refs, and the ball read `dist/data` and `dist/_search` — never the dist
218
+ // root. So it was serialized on every publish and dropped.
219
+ //
220
+ // ⚠️ The static index for hosts with NO backend is untouched and still
221
+ // emitted by the bundle lane (`site/plugin.js` `search-index.json`).
222
+ // That is what GitHub Pages and every other static target serve, and the
223
+ // framework has more targets than one backend.
260
224
 
261
225
  // 6. Agent projections — `llms.txt` and one `.md` per page.
262
226
  //
@@ -3,17 +3,17 @@
3
3
  * collections partition by schema presence: a collection that resolves a data schema
4
4
  * syncs as folder entities; a SCHEMA-LESS collection has no entity model, so its built
5
5
  * `dist/data/<name>.json` (cascade + any `deferred:` per-record files) is delivered
6
- * statically. This bundles that schema-less subset of `dist/data/**` plus the whole
7
- * `dist/_search/**` index into one JSON doc the deploy uploads as a single
8
- * content-addressed asset; the backend unwraps it into the `/data/*` + `/_search/*`
9
- * bytes the gateway serves.
6
+ * statically. This bundles that schema-less subset of `dist/data/**` into one JSON doc
7
+ * the deploy uploads as a single content-addressed asset; the backend unwraps it into
8
+ * the `/data/*` bytes the gateway serves.
10
9
  *
11
- * { data: { "<relpath-under-data>": <json> }, // schema-less collections only
12
- * search: { "<relpath-under-_search>": <json> } } // the whole (baked) index
10
+ * { data: { "<relpath-under-data>": <json> } } // schema-less collections only
13
11
  *
14
- * Search is NOT filtered: the index is baked over all content (the live/baked seam —
15
- * schema-backed lists are served live from entities, but their search entries are baked
16
- * here until the next deploy).
12
+ * **A search index used to ride here too, and deliberately no longer does** (2026-08-01).
13
+ * Only a CLI deploy produced one a CMS publish produced none — so a site's search
14
+ * existed or vanished depending on who published it, which is the flicker rule exactly.
15
+ * A host that wants search derives it from the content it already stores. See the note
16
+ * at the removal point below, and `collab/context/site-derived-artifacts.md`.
17
17
  */
18
18
 
19
19
  import { existsSync } from 'node:fs'
@@ -54,8 +54,7 @@ function collectionOf(relPath) {
54
54
  * @param {string} distDir - the site's built dist/ directory
55
55
  * @param {string[]} [schemalessNames] - collection names with no data schema (from
56
56
  * `emitSyncPackages(...).schemaless`); only these contribute `data`.
57
- * @returns {Promise<{ data: Object, search: Object }|null>} null when there is nothing
58
- * to deliver (no schema-less data AND no search index).
57
+ * @returns {Promise<{ data: Object }|null>} null when there is nothing to deliver.
59
58
  */
60
59
  export async function assembleDataBall(distDir, schemalessNames = []) {
61
60
  const schemaless = new Set(schemalessNames)
@@ -64,8 +63,6 @@ export async function assembleDataBall(distDir, schemalessNames = []) {
64
63
  for (const [relPath, value] of Object.entries(allData)) {
65
64
  if (schemaless.has(collectionOf(relPath))) data[relPath] = value
66
65
  }
67
- const search = await readJsonTree(join(distDir, '_search'))
68
-
69
66
  // Agent projections deliberately do NOT ride the ball.
70
67
  //
71
68
  // A backend that stores the site's content derives them itself at publish —
@@ -78,10 +75,41 @@ export async function assembleDataBall(distDir, schemalessNames = []) {
78
75
  // (An opt-in `projections` bucket lived here while the delivery contract was
79
76
  // open. It never shipped enabled, and the one-producer ruling closed the
80
77
  // question — removed rather than left as a flag nobody may turn on.)
78
+ //
79
+ // THE SEARCH INDEX IS ONE OF THOSE PROJECTIONS, and it rode this ball anyway
80
+ // — `const search = readJsonTree(dist/_search)` sat four lines above this
81
+ // comment, doing the exact thing the comment forbids. Removed 2026-08-01.
82
+ //
83
+ // What made it wrong is not symmetry, it is the flicker rule: a CLI deploy
84
+ // produced the index and a CMS publish produced none, so a site's search
85
+ // oscillated with whoever published it. Deriving it from stored content — one
86
+ // input that exists identically on both lanes — makes that unexpressible,
87
+ // which is a stronger guarantee than any producer agreement.
88
+ //
89
+ // The static index for hosts with no backend is UNCHANGED and still emitted:
90
+ // `search-index.json`, bundle lane, what GitHub Pages and every other static
91
+ // target serve. The framework has more targets than one backend, and that is
92
+ // the artifact for the rest of them.
81
93
 
82
- if (Object.keys(data).length === 0 && Object.keys(search).length === 0) return null
94
+ if (Object.keys(data).length === 0) return null
83
95
 
84
- return { data, search }
96
+ // The `search` key is gone too, and that was the SECOND step of a two-step
97
+ // retirement, not an afterthought.
98
+ //
99
+ // Step one shipped `search: {}` — content removed, key kept — because an
100
+ // absent field and an empty one are different shapes to a strict
101
+ // deserializer, and a missing required field fails exactly as loudly as an
102
+ // unknown one. (The producer half of this same contract broke pushes earlier
103
+ // the same day by emitting a key the consumer had not declared. Same failure,
104
+ // opposite direction.) Step two is this: the consumer retired the field on
105
+ // 2026-08-01 and said an older CLI still sending it deploys fine, so the key
106
+ // now has nowhere to land and dropping it is safe in both directions.
107
+ //
108
+ // Worth keeping as a shape: **announce, then remove** — the mirror of
109
+ // "declare, then emit". Neither half of a wire can be changed in one step by
110
+ // one side, and which side moves first is decided by which direction the
111
+ // strictness runs.
112
+ return { data }
85
113
  }
86
114
 
87
115
  // --- local media in the ball -------------------------------------------------
@@ -97,7 +125,7 @@ export async function assembleDataBall(distDir, schemalessNames = []) {
97
125
  * Site-root local asset refs anywhere in the ball (`/images/x.png`, `/collections/...`).
98
126
  * Built `dist/data` refs are already site-root (the collection processor copied
99
127
  * co-located assets to `public/collections/**`), so only `/`-prefixed refs are collected.
100
- * @param {{data:object,search:object}|null} ball
128
+ * @param {{data:object}|null} ball
101
129
  * @returns {string[]} deduped refs to upload
102
130
  */
103
131
  export function collectBallAssets(ball) {
@@ -118,9 +146,9 @@ export function collectBallAssets(ball) {
118
146
  * Rewrite the ball: replace every local ref the map covers with its serve URL. Pure —
119
147
  * returns a NEW ball (the input is reused elsewhere). A ref the map omits (upload
120
148
  * failed/skipped) is left untouched — never a broken URL.
121
- * @param {{data:object,search:object}|null} ball
149
+ * @param {{data:object}|null} ball
122
150
  * @param {Record<string,string>} map - ref → serve URL
123
- * @returns {{data:object,search:object}|null} a new ball, or the input when there's nothing to do
151
+ * @returns {{data:object}|null} a new ball, or the input when there's nothing to do
124
152
  */
125
153
  export function rewriteBallAssets(ball, map) {
126
154
  if (!ball || !map || Object.keys(map).length === 0) return ball