@uniweb/build 0.24.2 → 0.24.4

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.24.2",
3
+ "version": "0.24.4",
4
4
  "description": "Build tooling for the Uniweb Component Web Platform",
5
5
  "type": "module",
6
6
  "exports": {
@@ -59,17 +59,17 @@
59
59
  "js-yaml": "^4.1.0",
60
60
  "sharp": "^0.35.3",
61
61
  "yaml": "^2.5.0",
62
- "@uniweb/projections": "^0.3.3",
63
- "@uniweb/semantic-parser": "^1.2.3",
62
+ "@uniweb/content-writer": "^0.3.3",
64
63
  "@uniweb/schemas": "^0.2.10",
64
+ "@uniweb/semantic-parser": "^1.2.3",
65
65
  "@uniweb/theming": "^0.1.15",
66
- "@uniweb/content-writer": "^0.3.3"
66
+ "@uniweb/projections": "^0.3.3"
67
67
  },
68
68
  "optionalDependencies": {
69
- "@uniweb/content-reader": "^1.2.3",
70
69
  "@uniweb/runtime": "^0.12.2",
70
+ "@uniweb/schemas": "^0.2.10",
71
71
  "@uniweb/semantic-parser": "^1.2.3",
72
- "@uniweb/schemas": "^0.2.10"
72
+ "@uniweb/content-reader": "^1.2.3"
73
73
  },
74
74
  "peerDependencies": {
75
75
  "vite": "^5.0.0 || ^6.0.0 || ^7.0.0",
@@ -126,8 +126,9 @@ export async function buildSiteData({
126
126
  // in the vite plugin path that's fine because vite copies
127
127
  // `public/` into `dist/` at build time. The link-mode pipeline
128
128
  // has no vite, so we mirror that copy ourselves into
129
- // `<distDir>/data/` — the path `uniweb deploy::collectDataFiles`
130
- // walks at upload time. Same output bytes, same paths, just
129
+ // `<distDir>/data/` — the set the CLI's `site-data-upload` lane walks at
130
+ // publish time. (This named `uniweb deploy::collectDataFiles` until
131
+ // 2026-08-18; no such function has existed for some time.) Same output bytes, same paths, just
131
132
  // without the vite intermediary.
132
133
  if (siteContent.config?.collections) {
133
134
  const collectionsBase = siteContent.config?.paths?.collections
package/src/site/index.js CHANGED
@@ -42,7 +42,7 @@ export {
42
42
  writeCollectionFiles,
43
43
  getCollectionLastModified
44
44
  } from './collection-processor.js'
45
- export { assembleDataBall, collectBallAssets, rewriteBallAssets } from './data-ball.js'
45
+ export { collectSchemalessData, collectSchemalessDataAssets, rewriteSchemalessDataAssets } from './schemaless-data.js'
46
46
  export {
47
47
  parseFetchConfig,
48
48
  executeFetch,
@@ -1,19 +1,39 @@
1
1
  /**
2
- * Data ball — the static-delivery half of a composite `uniweb deploy`. A site's
3
- * collections partition by schema presence: a collection that resolves a data schema
4
- * syncs as folder entities; a SCHEMA-LESS collection has no entity model, so its built
5
- * `dist/data/<name>.json` (cascade + any `deferred:` per-record files) is delivered
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.
2
+ * Schema-less collection data — the set, and its local media.
9
3
  *
10
- * { data: { "<relpath-under-data>": <json> } } // schema-less collections only
4
+ * **The name carries the scope on purpose.** This is NOT "the site's static
5
+ * data" in general. A site's collections partition by **schema presence**: one
6
+ * that resolves a data schema syncs as folder entities — content a backend
7
+ * genuinely consumes, queryable, editable, with a `brief:` for its lean shape.
8
+ * A **schema-less** collection has no entity model, so its compiled
9
+ * `dist/data/<name>.json` (plus any `deferred:` per-record files) is delivered
10
+ * as files instead. That fallback tier is all this module is about.
11
11
  *
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`.
12
+ * { data: { "<relpath under dist/data>": <json> } } // schema-less only
13
+ *
14
+ * It is an **in-memory enumeration**, not an artifact. `collectSchemalessData`
15
+ * gathers the set; the CLI's `site-data-upload` lane PUTs each file to the
16
+ * target the backend returns, landing at its serving tail.
17
+ *
18
+ * ## ⛔ It used to be a "data ball", and that is retired (2026-08-18)
19
+ *
20
+ * This merged the whole set into ONE uploaded asset that the backend then had
21
+ * to fetch, parse and fan out. It was the **only aggregate the CLI produced** —
22
+ * media, foundation code and the runtime all upload one object per file — and
23
+ * the only place these bytes ever transited the backend. Nothing in the code or
24
+ * the docs ever justified the bundling.
25
+ *
26
+ * ⇒ **Do not reintroduce a bundle here.** One object per file is what the
27
+ * reader's static arm already assumes: a plain object GET on the verbatim tail,
28
+ * with nothing anywhere that unbundles. Full account, including the endpoint
29
+ * contract: `kb/framework/build/data-ball-retirement.md`.
30
+ *
31
+ * **A search index used to ride here too, and deliberately no longer does**
32
+ * (2026-08-01). Only a CLI deploy produced one — a CMS publish produced none —
33
+ * so a site's search existed or vanished depending on who published it, which
34
+ * is the artifact-flicker rule exactly. A host that wants search derives it from
35
+ * the content it already stores. See the note at the removal point below and
36
+ * `collab/context/site-derived-artifacts.md`.
17
37
  */
18
38
 
19
39
  import { existsSync } from 'node:fs'
@@ -56,7 +76,7 @@ function collectionOf(relPath) {
56
76
  * `emitSyncPackages(...).schemaless`); only these contribute `data`.
57
77
  * @returns {Promise<{ data: Object }|null>} null when there is nothing to deliver.
58
78
  */
59
- export async function assembleDataBall(distDir, schemalessNames = []) {
79
+ export async function collectSchemalessData(distDir, schemalessNames = []) {
60
80
  const schemaless = new Set(schemalessNames)
61
81
  const allData = await readJsonTree(join(distDir, DATA_DIR))
62
82
  const data = {}
@@ -128,7 +148,7 @@ export async function assembleDataBall(distDir, schemalessNames = []) {
128
148
  * @param {{data:object}|null} ball
129
149
  * @returns {string[]} deduped refs to upload
130
150
  */
131
- export function collectBallAssets(ball) {
151
+ export function collectSchemalessDataAssets(ball) {
132
152
  const refs = new Set()
133
153
  const walk = (n) => {
134
154
  if (typeof n === 'string') {
@@ -150,7 +170,7 @@ export function collectBallAssets(ball) {
150
170
  * @param {Record<string,string>} map - ref → serve URL
151
171
  * @returns {{data:object}|null} a new ball, or the input when there's nothing to do
152
172
  */
153
- export function rewriteBallAssets(ball, map) {
173
+ export function rewriteSchemalessDataAssets(ball, map) {
154
174
  if (!ball || !map || Object.keys(map).length === 0) return ball
155
175
  const walk = (n) => {
156
176
  if (typeof n === 'string') return map[n] || n
@@ -237,11 +237,16 @@ function rewriteEntityAssets(node, map, ids) {
237
237
  * siteContent: { buffer, entityCount, index, models }|null,
238
238
  * collections: { buffer, entityCount, index, models }|null,
239
239
  * hashes: Object<string,string>, warnings: string[], skipped: number,
240
- * schemaless: Array<{name: string, model: string}>, localAssets: string[] }>}
240
+ * schemaless: Array<{name: string, model: string}>, localAssets: string[],
241
+ * applied: object }>}
241
242
  * `schemaless` lists collections that resolved no data schema (soft-skipped from
242
243
  * the sync) — the composite deploy delivers these statically via the data ball.
243
244
  * `localAssets` lists the site-root local media refs (`/images/x.png`) the deploy
244
245
  * must upload + rewrite to serve URLs; co-located refs are warned and skipped.
246
+ * `applied` echoes the hash-affecting injections this emit actually used
247
+ * (`assetRewrite` / `assetIds` / `injectInfo` / `injectExtensions`), ready to be
248
+ * passed straight back as opts. A caller that banks the `hashes` must bank this
249
+ * beside them, or an offline re-emit cannot reproduce the document they describe.
245
250
  * Each lane is null when it has nothing to push. The collections `index` keeps a
246
251
  * leading `{ kind: 'folder' }` placeholder (submission position 0 → the folder
247
252
  * entity) so record back-fill stays positionally aligned; the folder itself has no
@@ -275,8 +280,10 @@ export async function emitSyncPackages(siteRoot, opts = {}) {
275
280
  // stamped here — NOT authored in site.yml, so they ride the wire but never project
276
281
  // back on pull (the `info.assets` precedent). They are part of the hashed content,
277
282
  // so a changed bundle URL correctly re-fires the site-content lane.
278
- if (siteDoc && opts.injectInfo && typeof opts.injectInfo === 'object') {
279
- siteDoc.info = { ...siteDoc.info, ...opts.injectInfo }
283
+ const injectInfo =
284
+ opts.injectInfo && typeof opts.injectInfo === 'object' ? opts.injectInfo : null
285
+ if (siteDoc && injectInfo) {
286
+ siteDoc.info = { ...siteDoc.info, ...injectInfo }
280
287
  }
281
288
  // Same idea one level out: `extensions` is a sibling of `info`, not a field in
282
289
  // it, so a pinned extension ref cannot ride `injectInfo`. `publish` releases a
@@ -285,9 +292,13 @@ export async function emitSyncPackages(siteRoot, opts = {}) {
285
292
  // for the same reason: delivery is version-pinned, so an unpinned local name on
286
293
  // the wire points at code the host cannot serve. Keyed by `$id` (the authored
287
294
  // declaration), so entries the publish didn't touch are left verbatim.
288
- if (siteDoc && opts.injectExtensions && Array.isArray(siteDoc.extensions)) {
295
+ const injectExtensions =
296
+ opts.injectExtensions && typeof opts.injectExtensions === 'object'
297
+ ? opts.injectExtensions
298
+ : null
299
+ if (siteDoc && injectExtensions && Array.isArray(siteDoc.extensions)) {
289
300
  siteDoc.extensions = siteDoc.extensions.map((e) => {
290
- const pinned = opts.injectExtensions[e?.$id]
301
+ const pinned = injectExtensions[e?.$id]
291
302
  if (!pinned) return e
292
303
  const { url: _dropped, ...rest } = e
293
304
  return { ...rest, ref: pinned }
@@ -402,9 +413,33 @@ export async function emitSyncPackages(siteRoot, opts = {}) {
402
413
  // the content lane by its presence/absence.
403
414
  const siteContentUuid = siteDoc?.$uuid
404
415
 
416
+ // ⛔ EVERY INJECTION ABOVE IS PART OF THE HASHED DOCUMENT — so a reader that
417
+ // cannot reproduce them cannot compare against the hashes a push banked.
418
+ //
419
+ // `uniweb status` is exactly such a reader: it re-emits OFFLINE, and both an asset
420
+ // serve URL and a pinned foundation ref are things only a backend round-trip can
421
+ // produce. Left implicit, it reports every entity as changed FOREVER — measured
422
+ // 2026-08-19 on a site with one `/images/*.svg` reference, where push banked the
423
+ // rewritten hash, status re-hashed the authored one, and `changed` never dropped
424
+ // below 1 no matter how many times you pushed. `push.js` had already written the
425
+ // rule down for its own two emits ("or every entity reads as changed forever") and
426
+ // the third reader of the same cache never got it.
427
+ //
428
+ // ⭐ So the EMITTER reports what it applied, the pusher banks it beside the hashes,
429
+ // and the offline reader replays it. Reporting it here rather than having each
430
+ // caller remember to pass its own injections along is the whole point: what gets
431
+ // banked is then, by construction, what was hashed. A future injection is covered
432
+ // by adding one line here — not by finding every reader.
433
+ const applied = {
434
+ ...(assetRewrite ? { assetRewrite } : {}),
435
+ ...(assetIds ? { assetIds } : {}),
436
+ ...(injectInfo ? { injectInfo } : {}),
437
+ ...(injectExtensions ? { injectExtensions } : {})
438
+ }
439
+
405
440
  return {
406
441
  siteContent, collections, siteContentUuid, hashes, warnings, skipped,
407
- schemaless: col.schemaless, localAssets,
442
+ schemaless: col.schemaless, localAssets, applied,
408
443
  // { stamped, unknown } when identity was applied; null when the caller passed
409
444
  // no map. `unknown > 0` with `stamped === 0` on a site that has been pushed
410
445
  // before is the index-loss signature the backend refuses — the caller reports it.