@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 +6 -6
- package/src/site/build-site-data.js +20 -56
- package/src/site/data-ball.js +46 -18
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@uniweb/build",
|
|
3
|
-
"version": "0.16.
|
|
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/
|
|
62
|
+
"@uniweb/theming": "0.1.15",
|
|
63
63
|
"@uniweb/projections": "0.2.5",
|
|
64
|
-
"@uniweb/
|
|
64
|
+
"@uniweb/content-writer": "0.3.3"
|
|
65
65
|
},
|
|
66
66
|
"optionalDependencies": {
|
|
67
67
|
"@uniweb/content-reader": "1.2.2",
|
|
68
|
-
"@uniweb/
|
|
69
|
-
"@uniweb/
|
|
70
|
-
"@uniweb/
|
|
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.
|
|
209
|
-
//
|
|
210
|
-
//
|
|
211
|
-
//
|
|
212
|
-
//
|
|
213
|
-
//
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
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
|
//
|
package/src/site/data-ball.js
CHANGED
|
@@ -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/**`
|
|
7
|
-
*
|
|
8
|
-
*
|
|
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:
|
|
12
|
-
* search: { "<relpath-under-_search>": <json> } } // the whole (baked) index
|
|
10
|
+
* { data: { "<relpath-under-data>": <json> } } // schema-less collections only
|
|
13
11
|
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
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
|
|
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
|
|
94
|
+
if (Object.keys(data).length === 0) return null
|
|
83
95
|
|
|
84
|
-
|
|
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
|
|
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
|
|
149
|
+
* @param {{data:object}|null} ball
|
|
122
150
|
* @param {Record<string,string>} map - ref → serve URL
|
|
123
|
-
* @returns {{data:object
|
|
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
|