@uniweb/build 0.56.0 → 0.57.0
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 +7 -7
- package/src/content/index.js +3 -0
- package/src/i18n/data-strings.js +1 -1
- package/src/i18n/freeform.js +20 -19
- package/src/i18n/index.js +4 -4
- package/src/i18n/records.js +9 -5
- package/src/site/build-site-data.js +4 -3
- package/src/site/config.js +1 -1
- package/src/site/content-collector.js +4 -3
- package/src/site/entity-pool.js +136 -42
- package/src/site/plugin.js +12 -9
- package/src/site/queries-config.js +15 -5
- package/src/site/query-processor.js +126 -112
- package/src/site/record-draft.js +40 -0
- package/src/site/records-config.js +101 -93
- package/src/uwx/asset-map.js +2 -1
- package/src/uwx/backfill.js +2 -2
- package/src/uwx/entity-source.js +3 -3
- package/src/uwx/folder.js +89 -49
- package/src/uwx/index.js +9 -9
- package/src/uwx/records-project.js +95 -64
- package/src/uwx/records.js +310 -214
- package/src/uwx/self-scope.js +5 -5
- package/src/uwx/site-project.js +3 -3
- package/src/uwx/site.js +45 -39
- package/src/uwx/sync-package.js +19 -15
- package/src/validate-data.js +3 -1
package/src/uwx/self-scope.js
CHANGED
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
// (the foundation's scope, not the site's publish org) and stays there.
|
|
27
27
|
|
|
28
28
|
/**
|
|
29
|
-
* An org handle as `--org` or `
|
|
29
|
+
* An org handle as `--org` or `sync.json` gives it — `@acme`, `acme`, or
|
|
30
30
|
* `@acme/…` — reduced to the bare handle, or `''`.
|
|
31
31
|
*
|
|
32
32
|
* @param {unknown} org
|
|
@@ -56,15 +56,15 @@ export function resolveSelfScope(ref, org) {
|
|
|
56
56
|
* The inverse, for a pull: `@<org>/x` → `@/x` for the site's own org.
|
|
57
57
|
*
|
|
58
58
|
* ⛔ WITHOUT THIS THE ROUND TRIP IS NOT A FIXED POINT, and the failure is silent
|
|
59
|
-
* on both ends. A record authored under `
|
|
60
|
-
* `@acme/article` and, placed literally, lands under `
|
|
59
|
+
* on both ends. A record authored under `records/article/` comes back as
|
|
60
|
+
* `@acme/article` and, placed literally, lands under `records/acme/article/` — a
|
|
61
61
|
* different schema folder, which the next build reads as a different schema. A
|
|
62
62
|
* query declared `schema: '@/member'` comes back as `@acme/member`, and one that
|
|
63
63
|
* relied on the query-name default comes back with an explicit schema it never
|
|
64
64
|
* had.
|
|
65
65
|
*
|
|
66
|
-
* ⭐ The site records its own org at create (`site.
|
|
67
|
-
* which is exactly the inverse. A model scoped to ANOTHER org is left alone: it
|
|
66
|
+
* ⭐ The site records its own org at create (`site.org` in `sync.json`, per backend —
|
|
67
|
+
* "whose this is"), which is exactly the inverse. A model scoped to ANOTHER org is left alone: it
|
|
68
68
|
* genuinely is that org's, and `@/` would be a lie.
|
|
69
69
|
*
|
|
70
70
|
* @param {unknown} ref
|
package/src/uwx/site-project.js
CHANGED
|
@@ -151,7 +151,7 @@ const INFO_TO_SITE_YML = {
|
|
|
151
151
|
// ⭐ `preview` — the card image, verbatim: the app's timestamp, an author's URL, or
|
|
152
152
|
// the path an author wrote for an image in the project, which `restoreAssetRefs`
|
|
153
153
|
// has already put back by recognizing the serve URL the push recorded in
|
|
154
|
-
// `
|
|
154
|
+
// `sync.json`'s asset map (asset-map.js). [Diego, 2026-09-10]
|
|
155
155
|
//
|
|
156
156
|
// ⚠️ It was listed here as "DELIBERATELY ABSENT… BACKEND-STAMPED" until
|
|
157
157
|
// 2026-09-10. Leaving an app-written field out of this map is what lets the next
|
|
@@ -349,8 +349,8 @@ export function siteInfoToConfig({ document, siteRoot, backend = null, sourceLoc
|
|
|
349
349
|
// `data_bundle` and `foundation_schema`. It had been a build-derived upload
|
|
350
350
|
// manifest that a pull deliberately never wrote back; framework never populated
|
|
351
351
|
// it. Nothing to project, and nothing here to remove — the note is kept because
|
|
352
|
-
//
|
|
353
|
-
// two get confused.
|
|
352
|
+
// the committed local path → id map (each backend's `assets` in `sync.json`;
|
|
353
|
+
// `assets.json` until 2026-09-20) is a DIFFERENT thing and the two get confused.
|
|
354
354
|
|
|
355
355
|
return result
|
|
356
356
|
}
|
package/src/uwx/site.js
CHANGED
|
@@ -14,13 +14,16 @@
|
|
|
14
14
|
// carries the verbatim `site.yml::foundation` string (the round-trip source of
|
|
15
15
|
// truth).
|
|
16
16
|
//
|
|
17
|
-
// IDENTITY. The ENTITY `$uuid`
|
|
18
|
-
//
|
|
19
|
-
//
|
|
17
|
+
// IDENTITY. The ENTITY `$uuid` is a backend's, recorded per backend in `sync.json`
|
|
18
|
+
// (`backends.<origin>.site.uuid`): we read the one for the backend a document is
|
|
19
|
+
// produced FOR, send it, and record the minted value there. Nested pages/sections
|
|
20
|
+
// carry a `$id` handle AND a per-item `$uuid`. ⚠️ The entity `$uuid` lived in
|
|
21
|
+
// `site.yml` until 2026-09-20 — one value, with no way to say which backend minted it.
|
|
20
22
|
//
|
|
21
23
|
// The per-item uuid is NOT authored — author files never carry sync uuids. It is
|
|
22
|
-
// stamped at emit (`stampUnitUuids`) from
|
|
23
|
-
// the backend last reported: a pull, or a push response's
|
|
24
|
+
// stamped at emit (`stampUnitUuids`) from that backend's `items` map in `sync.json`,
|
|
25
|
+
// written from whatever the backend last reported: a pull, or a push response's
|
|
26
|
+
// `finalized[].document`. (The map sat in the gitignored sync cache until 2026-09-20.)
|
|
24
27
|
// This is load-bearing, not bookkeeping: the backend matches records by uuid, and
|
|
25
28
|
// `pages` / `page_sections` / `layout_sections` are all `multi` sections, where a
|
|
26
29
|
// uuid-less record is read as NEW — inserted, with its stored counterpart deleted as
|
|
@@ -385,7 +388,7 @@ const CATCH_ALL_MARKER = '...path'
|
|
|
385
388
|
// `page_sections`). Cross-section parentage is pure structure, never `$parent`.
|
|
386
389
|
// - `$id` (the stableId — the in-file handle) rides at every item level as the
|
|
387
390
|
// wire-only closure handle. Per-item `$uuid` is NOT authored here: it is
|
|
388
|
-
// stamped on by `stampUnitUuids` at emit, from the
|
|
391
|
+
// stamped on by `stampUnitUuids` at emit, from the backend's `items` map in `sync.json`,
|
|
389
392
|
// because the backend matches records by uuid and a uuid-less record in a
|
|
390
393
|
// `multi` section is read as new (inserted, stored counterpart deleted). See
|
|
391
394
|
// the IDENTITY note in the file header and `site-diff.js`.
|
|
@@ -401,8 +404,8 @@ const SITE_MODEL_NAME = '@uniweb/site-content'
|
|
|
401
404
|
// `fields` already carries `stable_id` (the Model field); `$id` is the same value.
|
|
402
405
|
// Both are kept: `$id` is the sync handle, `stable_id` is the declared content field
|
|
403
406
|
// the editor/render reads. Per-item `$uuid` is added later by `stampUnitUuids` (it
|
|
404
|
-
// comes from
|
|
405
|
-
//
|
|
407
|
+
// comes from `sync.json`, not from the authored files) — see the IDENTITY note in
|
|
408
|
+
// the file header.
|
|
406
409
|
function withIdentity(id, fields) {
|
|
407
410
|
return Object.assign({ $id: id }, fields)
|
|
408
411
|
}
|
|
@@ -1214,14 +1217,15 @@ function settingsNested(siteYml, { headHtml, themeYml, sourceLocale, translation
|
|
|
1214
1217
|
/**
|
|
1215
1218
|
* Map a file site project to the nested `@uniweb/site-content` `$`-document
|
|
1216
1219
|
* (see the lane header above). PURE — reads the project, never mints, never writes.
|
|
1217
|
-
* The entity `$uuid`
|
|
1218
|
-
* nested items carry `$id` only.
|
|
1220
|
+
* The entity `$uuid` is `opts.backend`'s, from `sync.json` (recorded after the first
|
|
1221
|
+
* sync to it); nested items carry `$id` only.
|
|
1219
1222
|
*
|
|
1220
1223
|
* @param {string} siteRoot - directory containing site.yml
|
|
1221
1224
|
* @param {object} [opts]
|
|
1222
1225
|
* @param {string} [opts.backend] - whose `$uuid` to put on the wire (sync.json)
|
|
1223
1226
|
* @param {string} [opts.entityUuid] - override the entity `$uuid` (tests); default
|
|
1224
|
-
* is `site
|
|
1227
|
+
* is `opts.backend`'s site uuid in `sync.json` (absent on a first sync to it,
|
|
1228
|
+
* or with no backend — a `$id`-only document).
|
|
1225
1229
|
* @param {string} [opts.sourceLocale] - localized-field wrap locale. Defaults to
|
|
1226
1230
|
* the site's effective default locale (`defaultLanguage || languages[0] ||
|
|
1227
1231
|
* 'en'` — the shared `resolveDefaultLocale` rule), NOT a bare 'en'.
|
|
@@ -1308,9 +1312,10 @@ export async function siteProjectToDocument(siteRoot, opts = {}) {
|
|
|
1308
1312
|
// deploy-derived `info` field framework sends is `foundation`, via `injectInfo`
|
|
1309
1313
|
// in `publish`.
|
|
1310
1314
|
//
|
|
1311
|
-
// ⚠️ The note stays because
|
|
1312
|
-
//
|
|
1313
|
-
// thing with a similar name, and
|
|
1315
|
+
// ⚠️ The note stays because the COMMITTED local map from an author's asset path
|
|
1316
|
+
// to the backend's content-addressed id — each backend's `assets` in `sync.json`,
|
|
1317
|
+
// `assets.json` until 2026-09-20 — is a different thing with a similar name, and
|
|
1318
|
+
// it is very much alive.
|
|
1314
1319
|
setIf(info, 'favicon', siteYml.favicon)
|
|
1315
1320
|
// Site-level SEO/social metadata — the same shape as page.yml's `seo:` + the
|
|
1316
1321
|
// top-level `keywords`, hoisted to the site root so the homepage social card
|
|
@@ -1421,7 +1426,7 @@ export async function siteProjectToDocument(siteRoot, opts = {}) {
|
|
|
1421
1426
|
// a site-root path to an image in the project, which push uploads like any
|
|
1422
1427
|
// content image and sends as its serve URL, and which pull puts back as the
|
|
1423
1428
|
// path the author wrote (`restoreAssetRefs` recognizes the URL by the
|
|
1424
|
-
// fingerprint `
|
|
1429
|
+
// fingerprint recorded for it in `sync.json`'s asset map).
|
|
1425
1430
|
// The app leaves an author's value alone.
|
|
1426
1431
|
//
|
|
1427
1432
|
// ⚠️ Until 2026-09-10 framework emitted no preview, on the premise that the host
|
|
@@ -1502,8 +1507,7 @@ export async function siteProjectToDocument(siteRoot, opts = {}) {
|
|
|
1502
1507
|
// config (the records themselves are separate entities; this is just the config).
|
|
1503
1508
|
const colConfig = await resolveQueriesConfig(siteRoot, { siteYml })
|
|
1504
1509
|
|
|
1505
|
-
// `$uuid?` then `$id` `$model`, then sections in Model-declared order.
|
|
1506
|
-
// `$uuid` lives in site.yml (back-filled after first sync); absent on first sync.
|
|
1510
|
+
// `$uuid?` then `$id` `$model`, then sections in Model-declared order.
|
|
1507
1511
|
const doc = {}
|
|
1508
1512
|
// ⭐ From `sync.json`, keyed by the backend this document is being produced FOR.
|
|
1509
1513
|
// It was `site.yml::$uuid` — one scalar with no way to say which backend minted it.
|
|
@@ -1593,25 +1597,30 @@ export async function emitSiteSyncPackage(siteRoot, opts = {}) {
|
|
|
1593
1597
|
// Identity back-fill.
|
|
1594
1598
|
//
|
|
1595
1599
|
// The ENTITY `$uuid` (the backend's identity for the whole site-content entity) is
|
|
1596
|
-
//
|
|
1597
|
-
//
|
|
1598
|
-
//
|
|
1599
|
-
//
|
|
1600
|
+
// recorded after the first sync to a backend, in that backend's section of
|
|
1601
|
+
// `sync.json`, beside the org the site was created under. Per-item uuids round-trip
|
|
1602
|
+
// too, through the same section's `items` map — see the IDENTITY note in the file
|
|
1603
|
+
// header. ⚠️ Until 2026-09-21 this note put the entity `$uuid` in `site.yml`, where it
|
|
1604
|
+
// lived until 2026-09-20, and said nested items have no per-item uuid round-trip,
|
|
1605
|
+
// which the file header says is not so.
|
|
1600
1606
|
// ===========================================================================
|
|
1601
1607
|
|
|
1602
1608
|
/**
|
|
1603
|
-
*
|
|
1604
|
-
*
|
|
1609
|
+
* Record the minted site-content entity `$uuid` as `backend`'s site, in `sync.json`
|
|
1610
|
+
* (`backends.<origin>.site.uuid`). Until 2026-09-20 it was back-filled into
|
|
1611
|
+
* `site.yml` as a top-level `$uuid`.
|
|
1605
1612
|
* @param {string} siteRoot
|
|
1613
|
+
* @param {string} backend - the origin of the backend that minted it
|
|
1606
1614
|
* @param {string} uuid - the entity uuid the backend minted/echoed
|
|
1607
|
-
* @returns {boolean} true if
|
|
1615
|
+
* @returns {boolean} true if sync.json changed
|
|
1608
1616
|
*/
|
|
1609
1617
|
export function writeSiteEntityUuid(siteRoot, backend, uuid) {
|
|
1610
1618
|
return updateBackendState(siteRoot, backend, { site: { uuid } })
|
|
1611
1619
|
}
|
|
1612
1620
|
|
|
1613
1621
|
/**
|
|
1614
|
-
* Record the org the site was CREATED under
|
|
1622
|
+
* Record the org the site was CREATED under — `backend`'s `site.org` in `sync.json`,
|
|
1623
|
+
* beside the site's uuid. (It was `site.yml::$org` until 2026-09-20.)
|
|
1615
1624
|
*
|
|
1616
1625
|
* The org is consumed at exactly one moment — the `as_org` on the create that
|
|
1617
1626
|
* mints `$uuid` — and after that the uuid carries the ownership binding. So this
|
|
@@ -1619,15 +1628,11 @@ export function writeSiteEntityUuid(siteRoot, backend, uuid) {
|
|
|
1619
1628
|
* this site's storage charged to?"*, which `$uuid` alone cannot answer and which
|
|
1620
1629
|
* otherwise costs a backend round-trip (or is simply unknowable from the repo).
|
|
1621
1630
|
*
|
|
1622
|
-
* Stored as the BARE handle, never `@handle
|
|
1623
|
-
*
|
|
1624
|
-
*
|
|
1625
|
-
*
|
|
1626
|
-
*
|
|
1627
|
-
* reader re-adds.
|
|
1628
|
-
*
|
|
1629
|
-
* Safe to add to `site.yml` because the sync lane is an explicit allowlist
|
|
1630
|
-
* (`info.*` above is built key by key), so this never rides the wire.
|
|
1631
|
+
* Stored as the BARE handle, never `@handle` — the canonical form everywhere else
|
|
1632
|
+
* (`deriveScope` returns it, `createOrg` echoes it as `org.handle`,
|
|
1633
|
+
* `validateHandle` validates it); the `@` is display sugar the reader re-adds. In
|
|
1634
|
+
* `site.yml` it was also the only form that parsed: `@` is a reserved YAML
|
|
1635
|
+
* indicator, so `$org: @acme` is an error.
|
|
1631
1636
|
*
|
|
1632
1637
|
* ⛔ NOT `deploy.yml`, though that file already holds the bound `backend` and the
|
|
1633
1638
|
* two look like the same class of fact. Four reasons, and the first is the one that
|
|
@@ -1635,9 +1640,9 @@ export function writeSiteEntityUuid(siteRoot, backend, uuid) {
|
|
|
1635
1640
|
*
|
|
1636
1641
|
* 1. CARDINALITY. `deploy.yml` is multi-TARGET and `backend` sits *under* a target,
|
|
1637
1642
|
* so its shape says "this may vary per target." An org may not: one site has one
|
|
1638
|
-
* owning org, fixed at create and preserved on replace.
|
|
1639
|
-
*
|
|
1640
|
-
* encode a freedom that does not exist.
|
|
1643
|
+
* owning org, fixed at create and preserved on replace. The org is a property of
|
|
1644
|
+
* the site's uuid, and sits beside it in the same backend's section — filing it
|
|
1645
|
+
* under a target would encode a freedom that does not exist.
|
|
1641
1646
|
* 2. WHO WRITES. Three paths mint a site (`ensureSiteExists`, the media-less push's
|
|
1642
1647
|
* content-lane create, and `clone` seeding an existing one) and **none of them
|
|
1643
1648
|
* write `deploy.yml`** — only `deploy` and `publish` call `recordLastDeploy`. The
|
|
@@ -1650,11 +1655,12 @@ export function writeSiteEntityUuid(siteRoot, backend, uuid) {
|
|
|
1650
1655
|
* ever being deployed. Ownership does not belong in a record of a deploy that
|
|
1651
1656
|
* may not have happened.
|
|
1652
1657
|
*
|
|
1653
|
-
* `backend` answers *where this ships*;
|
|
1658
|
+
* `backend` answers *where this ships*; `site.org` answers *whose this is*.
|
|
1654
1659
|
*
|
|
1655
1660
|
* @param {string} siteRoot
|
|
1661
|
+
* @param {string} backend - the origin of the backend the site was created on
|
|
1656
1662
|
* @param {string} handle - the bare org handle (no leading `@`)
|
|
1657
|
-
* @returns {boolean} true if
|
|
1663
|
+
* @returns {boolean} true if sync.json changed
|
|
1658
1664
|
*/
|
|
1659
1665
|
export function writeSiteOrg(siteRoot, backend, handle) {
|
|
1660
1666
|
return updateBackendState(siteRoot, backend, { site: { org: handle } })
|
package/src/uwx/sync-package.js
CHANGED
|
@@ -290,16 +290,20 @@ export async function emitSyncPackages(siteRoot, opts = {}) {
|
|
|
290
290
|
const folder = buildFolderEntity({
|
|
291
291
|
recordEntities: col.entities,
|
|
292
292
|
...(sourceLocale ? { sourceLocale } : {}),
|
|
293
|
-
// ⭐
|
|
294
|
-
//
|
|
295
|
-
//
|
|
296
|
-
|
|
297
|
-
//
|
|
298
|
-
//
|
|
299
|
-
|
|
300
|
-
//
|
|
301
|
-
//
|
|
302
|
-
|
|
293
|
+
// ⭐ Every record in the directory, at the top of the folder or in the
|
|
294
|
+
// sub-folder `records.yml` places it in — and NOTHING when there is no folder to
|
|
295
|
+
// state. ⛔ Gated here as well as by `declared`, because the builder sends any
|
|
296
|
+
// non-empty tree: a `records.yml` whose folders hold only records that resolved
|
|
297
|
+
// no schema is a tree of empty branches, and sending it would replace the
|
|
298
|
+
// backend's folder with them (measured on the `dynamic` template, 2026-09-21).
|
|
299
|
+
folderNodes: col.sendFolder === true ? (col.folder?.nodes ?? []) : [],
|
|
300
|
+
// ⛔ Whether the file side HAS a folder to state (`records.js::sendsFolder`) —
|
|
301
|
+
// not whether it holds anything. A site with no records directory sends none
|
|
302
|
+
// and leaves the backend's alone; an empty directory sends an empty folder,
|
|
303
|
+
// which REMOVES (the CLI asks first). Compared as `=== true`: an absent value
|
|
304
|
+
// must read as "nothing to state", which is how a site with no records.yml once
|
|
305
|
+
// emitted a folder that would have emptied the backend's.
|
|
306
|
+
declared: col.sendFolder === true,
|
|
303
307
|
// Placement identity from the folder document a previous push returned.
|
|
304
308
|
// Absent on a first push — every item is genuinely new then. Its ABSENCE on a
|
|
305
309
|
// later push is what made `publish` after `push` fail: send-only-changed skips
|
|
@@ -308,9 +312,9 @@ export async function emitSyncPackages(siteRoot, opts = {}) {
|
|
|
308
312
|
...(opts.folderItemUuids ? { itemUuids: opts.folderItemUuids } : {}),
|
|
309
313
|
})
|
|
310
314
|
// ⚠️ A placement that produced no entity is dropped by the builder rather than
|
|
311
|
-
// sent pointing at nothing — but it must still be SAID.
|
|
312
|
-
//
|
|
313
|
-
//
|
|
315
|
+
// sent pointing at nothing — but it must still be SAID. `buildRecordEntities`
|
|
316
|
+
// places only records it produced, so this is a guard on the pairing rather than
|
|
317
|
+
// an expected path.
|
|
314
318
|
if (folder?.warnings?.length) warnings.push(...folder.warnings)
|
|
315
319
|
|
|
316
320
|
// `queryUuids` — identity for the `queries` section, keyed by query
|
|
@@ -404,8 +408,8 @@ export async function emitSyncPackages(siteRoot, opts = {}) {
|
|
|
404
408
|
// ⛔ No identity attrs on `info`: it is a Section whose fields the host declares,
|
|
405
409
|
// and `preview` is a real ASSET_SLOTS slot, so stamping would add
|
|
406
410
|
// `previewAssetId`/`previewAssetExt` — fields the host refuses. Its bare strings
|
|
407
|
-
// are still swapped for their serve URLs, which `
|
|
408
|
-
// by fingerprint (asset-map.js → `servedFingerprint`).
|
|
411
|
+
// are still swapped for their serve URLs, which the asset map in `sync.json`
|
|
412
|
+
// recognizes on pull by fingerprint (asset-map.js → `servedFingerprint`).
|
|
409
413
|
if (siteDoc) rewriteEntityAssets(siteDoc, assetRewrite, assetIds, siteDoc.info)
|
|
410
414
|
for (const e of col.entities) rewriteEntityAssets(e.document, assetRewrite, assetIds)
|
|
411
415
|
}
|
package/src/validate-data.js
CHANGED
|
@@ -39,6 +39,7 @@ import { validateAndNormalizeSchema } from './resolve-data-schema.js'
|
|
|
39
39
|
// reach it here) even though the implementation moved next to the vocabulary.
|
|
40
40
|
export { validateItem, isStaticallyCheckable } from '@uniweb/schemas/conform'
|
|
41
41
|
import { buildSchema } from './schema.js'
|
|
42
|
+
import { resolveRecordsDir } from './site/entity-pool.js'
|
|
42
43
|
import { toFetchList } from './site/data-fetcher.js'
|
|
43
44
|
import { resolveFoundationSrcPath } from './utils/foundation-source-root.js'
|
|
44
45
|
import { collectSiteContent } from './site/content-collector.js'
|
|
@@ -94,7 +95,8 @@ export async function validateDataInputs({ siteRoot, foundationPath }) {
|
|
|
94
95
|
let byQuery = {}
|
|
95
96
|
if (config.queries && typeof config.queries === 'object') {
|
|
96
97
|
|
|
97
|
-
|
|
98
|
+
// Drafts included: a draft is content the site will deliver, so it is checked now.
|
|
99
|
+
byQuery = await processQueries(siteRoot, config.queries, resolveRecordsDir(siteRoot, config.paths).rel, basePath, { includeDrafts: true })
|
|
98
100
|
}
|
|
99
101
|
|
|
100
102
|
// Declared here rather than beside pass 2's other accumulators because pass 1
|