uniweb 0.33.0 → 0.33.1
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 +5 -5
- package/src/backend/site-sync.js +134 -12
- package/src/commands/pull.js +35 -1
- package/src/commands/push.js +4 -0
- package/src/framework-index.json +3 -3
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "uniweb",
|
|
3
|
-
"version": "0.33.
|
|
3
|
+
"version": "0.33.1",
|
|
4
4
|
"description": "Create structured Vite + React sites with content/code separation",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -41,13 +41,13 @@
|
|
|
41
41
|
"js-yaml": "^4.1.0",
|
|
42
42
|
"prompts": "^2.4.2",
|
|
43
43
|
"tar": "^7.0.0",
|
|
44
|
-
"@uniweb/runtime": "^0.13.1",
|
|
45
|
-
"@uniweb/semantic-parser": "^1.3.1",
|
|
46
44
|
"@uniweb/core": "^0.13.1",
|
|
47
|
-
"@uniweb/kit": "^0.14.0"
|
|
45
|
+
"@uniweb/kit": "^0.14.0",
|
|
46
|
+
"@uniweb/semantic-parser": "^1.3.1",
|
|
47
|
+
"@uniweb/runtime": "^0.13.1"
|
|
48
48
|
},
|
|
49
49
|
"peerDependencies": {
|
|
50
|
-
"@uniweb/build": "^0.29.
|
|
50
|
+
"@uniweb/build": "^0.29.1",
|
|
51
51
|
"@uniweb/content-reader": "^1.2.4",
|
|
52
52
|
"@uniweb/semantic-parser": "^1.3.1"
|
|
53
53
|
},
|
package/src/backend/site-sync.js
CHANGED
|
@@ -27,6 +27,7 @@ import {
|
|
|
27
27
|
computeUnitHashes,
|
|
28
28
|
collectUnitUuids,
|
|
29
29
|
collectFolderItemUuids,
|
|
30
|
+
collectCollectionUuids,
|
|
30
31
|
readAssetMap
|
|
31
32
|
} from '@uniweb/build/uwx'
|
|
32
33
|
|
|
@@ -241,6 +242,7 @@ export function clearRemoteSyncState(siteDir, siteUuid = null) {
|
|
|
241
242
|
const prior = readSyncCacheFile(siteDir)
|
|
242
243
|
const dropped = [
|
|
243
244
|
'itemUuids',
|
|
245
|
+
'collectionUuids',
|
|
244
246
|
'hashes',
|
|
245
247
|
'baseVersions',
|
|
246
248
|
'unitBases',
|
|
@@ -248,6 +250,9 @@ export function clearRemoteSyncState(siteDir, siteUuid = null) {
|
|
|
248
250
|
].filter((k) => prior[k] && Object.keys(prior[k]).length)
|
|
249
251
|
updateSyncCache(siteDir, {
|
|
250
252
|
itemUuids: {},
|
|
253
|
+
// Remote-derived exactly like itemUuids — it holds the OLD site's collection
|
|
254
|
+
// ids, and surviving the drop it would offer them for the new site's sections.
|
|
255
|
+
collectionUuids: {},
|
|
251
256
|
hashes: {},
|
|
252
257
|
baseVersions: {},
|
|
253
258
|
unitBases: {},
|
|
@@ -468,6 +473,32 @@ export function readItemUuids(siteDir) {
|
|
|
468
473
|
export function readFolderItemUuids(siteDir) {
|
|
469
474
|
return readMap(siteDir, 'folderItemUuids')
|
|
470
475
|
}
|
|
476
|
+
|
|
477
|
+
/**
|
|
478
|
+
* Collection-declaration identity: `{ <collection name>: <backend $uuid> }`.
|
|
479
|
+
*
|
|
480
|
+
* ⛔ THE THIRD MAP, AND EACH ONE EXISTS FOR THE SAME REASON. `itemUuids` is keyed by
|
|
481
|
+
* the file an item projects to. Two kinds of item have no file of their own — a
|
|
482
|
+
* folder's placements, and a collection DECLARATION (they all live in one
|
|
483
|
+
* `collections/collections.yml`) — so a path-keyed map has no shape either could
|
|
484
|
+
* occupy, and a push re-sends that whole section uuid-less.
|
|
485
|
+
*
|
|
486
|
+
* The backend refuses an all-blank section over stored items rather than applying it,
|
|
487
|
+
* because applying it would insert every record fresh and delete every stored row —
|
|
488
|
+
* content survives, identity does not. So `push` worked once and every push after was
|
|
489
|
+
* refused. Measured 2026-08-29; collab framework-backend-812b.
|
|
490
|
+
*
|
|
491
|
+
* ⭐ Keyed by NAME, which the backend enforces unique within the section and uses as
|
|
492
|
+
* its own join key. ⛔ Not `$id`: it holds the same string but is a payload-local
|
|
493
|
+
* handle the backend skips on parse and never stores.
|
|
494
|
+
*/
|
|
495
|
+
export function readCollectionUuids(siteDir) {
|
|
496
|
+
return readMap(siteDir, 'collectionUuids')
|
|
497
|
+
}
|
|
498
|
+
export function writeCollectionUuids(siteDir, map) {
|
|
499
|
+
if (!map || !Object.keys(map).length) return
|
|
500
|
+
updateSyncCache(siteDir, { collectionUuids: map })
|
|
501
|
+
}
|
|
471
502
|
export function writeFolderItemUuids(siteDir, map) {
|
|
472
503
|
if (!map || !Object.keys(map).length) return
|
|
473
504
|
updateSyncCache(siteDir, { folderItemUuids: map })
|
|
@@ -948,20 +979,72 @@ export async function probeUnpushed(siteDir, { sendAll = false } = {}) {
|
|
|
948
979
|
// and others never settling, which reads like a content problem rather than a
|
|
949
980
|
// resolution one. Measured on matinee 2026-08-29: `status` reported 4 changed
|
|
950
981
|
// immediately after a successful push; passing the org took it to 1.
|
|
982
|
+
const pkg = await comparisonEmit(siteDir, { priorHashes, sendAll })
|
|
983
|
+
const changed =
|
|
984
|
+
(pkg.siteContent?.entityCount || 0) + (pkg.collections?.entityCount || 0)
|
|
985
|
+
return { changed, unchanged: pkg.skipped || 0, warnings: pkg.warnings || [] }
|
|
986
|
+
}
|
|
987
|
+
|
|
988
|
+
/**
|
|
989
|
+
* The emit whose hashes are comparable with the banked ones — ONE definition.
|
|
990
|
+
*
|
|
991
|
+
* ⛔ EVERY DEFECT IN THIS AREA HAS BEEN A WRITER AND A READER DISAGREEING ABOUT
|
|
992
|
+
* WHICH DOCUMENT A HASH DESCRIBES, so the option set that makes them agree must
|
|
993
|
+
* have a single home. Assembled from three sources, each for a stated reason:
|
|
994
|
+
*
|
|
995
|
+
* · BANKED the injections the push applied before hashing — serve URLs and
|
|
996
|
+
* the pinned foundation ref, which only a round trip produces.
|
|
997
|
+
* · RE-DERIVED asset identity, from the COMMITTED `assets.json`, so a moved map
|
|
998
|
+
* reads as changed rather than matching a copy of itself.
|
|
999
|
+
* · RECORDED the site's org, which resolves a foundation-relative `@/x` into
|
|
1000
|
+
* the `@org/x` the push keyed its hashes by, and the collection
|
|
1001
|
+
* identity a push stamps — so `status` hashes the same document a
|
|
1002
|
+
* push would send rather than one missing a section's `$uuid`s.
|
|
1003
|
+
*
|
|
1004
|
+
* Offline by design — measured at zero HTTP requests, a property the cross-client
|
|
1005
|
+
* flows rely on.
|
|
1006
|
+
*/
|
|
1007
|
+
async function comparisonEmit(siteDir, { priorHashes = {}, sendAll = false } = {}) {
|
|
951
1008
|
const applied = readAppliedInjections(siteDir)
|
|
952
1009
|
const assetIds = readAssetMap(siteDir)
|
|
953
1010
|
const org = readSiteOrg(siteDir)
|
|
954
|
-
const
|
|
1011
|
+
const collectionUuids = readCollectionUuids(siteDir)
|
|
1012
|
+
return emitSyncPackages(siteDir, {
|
|
955
1013
|
resolveModel: makeModelResolver({ client: null, offline: true }),
|
|
956
1014
|
priorHashes,
|
|
957
1015
|
sendAll,
|
|
958
1016
|
...applied,
|
|
1017
|
+
...(Object.keys(collectionUuids).length ? { collectionUuids } : {}),
|
|
959
1018
|
...(Object.keys(assetIds).length ? { assetIds } : {}),
|
|
960
1019
|
...(org ? { org } : {})
|
|
961
1020
|
})
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
1021
|
+
}
|
|
1022
|
+
|
|
1023
|
+
/**
|
|
1024
|
+
* Re-bank the send-only-changed hashes over the files as they NOW stand.
|
|
1025
|
+
*
|
|
1026
|
+
* ⛔ FOR A WRITER THAT REWRITES THE WORKING TREE — today, `uniweb pull`.
|
|
1027
|
+
*
|
|
1028
|
+
* A pull projects the backend's document into source files, and that projection is
|
|
1029
|
+
* canonical rather than byte-identical to what was there: it moves section ordering
|
|
1030
|
+
* out of filename prefixes (`1-hero.md` → `hero.md` plus an explicit `sections:`
|
|
1031
|
+
* list) and stamps each section's `id`. Lossless, and a different document.
|
|
1032
|
+
*
|
|
1033
|
+
* ⚠️ `pull` already knew this for the OTHER map — it clears the `local` unit base
|
|
1034
|
+
* because "what we would emit from them is not byte-identical to it, so the old
|
|
1035
|
+
* local base no longer describes anything". That reasoning was never carried to the
|
|
1036
|
+
* hashes, which were left STALE rather than unknown: the next `uniweb status`
|
|
1037
|
+
* reported the site as having unpushed content immediately after a pull, forever.
|
|
1038
|
+
* Measured on matinee 2026-08-29 — `push → pull` reported 1 changed of 8.
|
|
1039
|
+
*
|
|
1040
|
+
* ⭐ Re-banking is the correct answer rather than clearing, because after a pull the
|
|
1041
|
+
* on-disk state IS the agreed state: it came from the backend. A push with no edits
|
|
1042
|
+
* in between should send nothing, and clearing would make it send everything.
|
|
1043
|
+
*/
|
|
1044
|
+
export async function rebankSyncHashes(siteDir) {
|
|
1045
|
+
const pkg = await comparisonEmit(siteDir, { sendAll: true })
|
|
1046
|
+
writeSyncCache(siteDir, pkg.hashes || {}, pkg.applied || {})
|
|
1047
|
+
return Object.keys(pkg.hashes || {}).length
|
|
965
1048
|
}
|
|
966
1049
|
|
|
967
1050
|
/**
|
|
@@ -1063,18 +1146,47 @@ export async function pushSyncPackages({
|
|
|
1063
1146
|
return null
|
|
1064
1147
|
}
|
|
1065
1148
|
if (problem?.reason === 'identity_required') {
|
|
1149
|
+
// ⛔ THE REFUSAL IS PER-SECTION, NOT PER-PUSH, AND WE USED TO SAY OTHERWISE.
|
|
1150
|
+
//
|
|
1151
|
+
// The backend asks, of each section independently: is this section's ENTIRE
|
|
1152
|
+
// incoming record set uuid-less while the stored entity already has items in
|
|
1153
|
+
// it? So a push can carry correct identity for most of the document and still
|
|
1154
|
+
// be refused over one section it stamps none of — and the old wording ("this
|
|
1155
|
+
// copy has no record of the site's item identity", "the recovery … could not
|
|
1156
|
+
// reach the backend", "`uniweb pull` also restores it") was wrong on all
|
|
1157
|
+
// three counts in exactly that case: the copy has identity, no recovery was
|
|
1158
|
+
// attempted, and a pull changes nothing.
|
|
1159
|
+
//
|
|
1160
|
+
// ⭐ THE BACKEND ALREADY SENDS WHAT LOCATES IT — `section_id`,
|
|
1161
|
+
// `records_without_uuid`, `stored_items` — and this branch discarded every
|
|
1162
|
+
// one of them, so every refusal anyone collected was missing the only fields
|
|
1163
|
+
// that say WHERE. (Named by backend in collab `framework-backend-812b`,
|
|
1164
|
+
// 2026-08-28: "the offending section and both counts have been in the body of
|
|
1165
|
+
// every refusal you have collected".)
|
|
1166
|
+
const n = problem.records_without_uuid
|
|
1167
|
+
const stored = problem.stored_items
|
|
1168
|
+
// `section_name` landed backend-side 2026-08-28 (`90e7cd7e`), replacing an
|
|
1169
|
+
// i64 row id we could not resolve. The fallback stays for an older backend,
|
|
1170
|
+
// not because the swap is pending — it is done.
|
|
1171
|
+
const where = problem.section_name ?? problem.section_id
|
|
1066
1172
|
error(
|
|
1067
|
-
`${label} push refused —
|
|
1068
|
-
|
|
1069
|
-
note(
|
|
1070
|
-
'Nothing was written. Pushing without it would have replaced the identity of every stored item.'
|
|
1071
|
-
)
|
|
1072
|
-
note(
|
|
1073
|
-
'The recovery normally runs automatically, so it likely could not reach the backend.'
|
|
1173
|
+
`${label} push refused — one section carries no item identity` +
|
|
1174
|
+
(where === undefined ? '.' : ` (section ${where}).`)
|
|
1074
1175
|
)
|
|
1176
|
+
if (Number.isInteger(n) && Number.isInteger(stored)) {
|
|
1177
|
+
note(
|
|
1178
|
+
`That section sent ${n} record(s), none carrying a \`$uuid\`, while ${stored} item(s) are already stored there.`
|
|
1179
|
+
)
|
|
1180
|
+
}
|
|
1075
1181
|
note(
|
|
1076
|
-
'
|
|
1182
|
+
'Nothing was written. Applying it would have replaced the identity of every stored item in that section.'
|
|
1077
1183
|
)
|
|
1184
|
+
// ⚠️ Deliberately NOT "run `uniweb pull`". A pull re-harvests identity from
|
|
1185
|
+
// the backend's own document, so it fixes a LOST cache — and does nothing at
|
|
1186
|
+
// all when the cache is intact and one section simply has no entry in it,
|
|
1187
|
+
// which is the case this message now names. Suggesting it there sends the
|
|
1188
|
+
// user to re-fetch a map they already have.
|
|
1189
|
+
if (problem.detail) note(String(problem.detail))
|
|
1078
1190
|
return null
|
|
1079
1191
|
}
|
|
1080
1192
|
if (problem?.reason === 'stale_base') {
|
|
@@ -1264,6 +1376,16 @@ export async function pushSyncPackages({
|
|
|
1264
1376
|
}
|
|
1265
1377
|
harvest(finalized)
|
|
1266
1378
|
siteFinalizedDoc = finalized[0]?.document || null
|
|
1379
|
+
// ⭐ BANK COLLECTION-DECLARATION IDENTITY, the sibling of the folder's
|
|
1380
|
+
// placements below. These items have no file to back-fill into — they all
|
|
1381
|
+
// come from one `collections/collections.yml` — so the only place their
|
|
1382
|
+
// `$uuid` can live is the cache, keyed by the name the backend enforces
|
|
1383
|
+
// unique. Without it every push after the first re-sends the whole
|
|
1384
|
+
// `collections` section uuid-less and is refused.
|
|
1385
|
+
if (siteFinalizedDoc) {
|
|
1386
|
+
const collectionIds = collectCollectionUuids(siteFinalizedDoc)
|
|
1387
|
+
if (Object.keys(collectionIds).length) writeCollectionUuids(siteDir, collectionIds)
|
|
1388
|
+
}
|
|
1267
1389
|
finalizedTotal += finalized.length
|
|
1268
1390
|
} else {
|
|
1269
1391
|
const payload = await postLane('site-content', () =>
|
package/src/commands/pull.js
CHANGED
|
@@ -75,7 +75,8 @@ import {
|
|
|
75
75
|
resolveCollectionsConfig,
|
|
76
76
|
readZip,
|
|
77
77
|
computeUnitHashes,
|
|
78
|
-
collectUnitUuids
|
|
78
|
+
collectUnitUuids,
|
|
79
|
+
collectCollectionUuids
|
|
79
80
|
} from '@uniweb/build/uwx'
|
|
80
81
|
import {
|
|
81
82
|
readWritten,
|
|
@@ -84,6 +85,8 @@ import {
|
|
|
84
85
|
} from '../utils/pull-written.js'
|
|
85
86
|
import {
|
|
86
87
|
makeModelResolver,
|
|
88
|
+
rebankSyncHashes,
|
|
89
|
+
writeCollectionUuids,
|
|
87
90
|
mergeBaseVersions,
|
|
88
91
|
mergeItemBaseVersions,
|
|
89
92
|
writeUnitBases,
|
|
@@ -686,6 +689,10 @@ export async function pull(args = [], deps = {}) {
|
|
|
686
689
|
// Per-item identity for the next push. Without it the backend reads our
|
|
687
690
|
// records as new and re-mints every page and section row.
|
|
688
691
|
writeItemUuids(siteDir, collectUnitUuids(siteDoc))
|
|
692
|
+
// The collections section's identity has no file to live in either — same
|
|
693
|
+
// reason, same remedy, keyed by name. A pull is the other route by which a
|
|
694
|
+
// copy can recover it (see readCollectionUuids).
|
|
695
|
+
writeCollectionUuids(siteDir, collectCollectionUuids(siteDoc))
|
|
689
696
|
// Bring the media down BEFORE projecting: a newly-landed asset gains a map
|
|
690
697
|
// entry, and the projection reads that map to put authored paths back. Run
|
|
691
698
|
// after, and this pull's new assets would project as URLs and only restore
|
|
@@ -840,6 +847,33 @@ export async function pull(args = [], deps = {}) {
|
|
|
840
847
|
removed
|
|
841
848
|
)
|
|
842
849
|
|
|
850
|
+
// ⛔ RE-BANK THE SEND-ONLY-CHANGED HASHES OVER WHAT WE JUST WROTE.
|
|
851
|
+
//
|
|
852
|
+
// The projection above is canonical, not byte-identical to what was on disk: it
|
|
853
|
+
// moves section ordering out of filename prefixes (`1-hero.md` → `hero.md` plus an
|
|
854
|
+
// explicit `sections:` list) and stamps each section's `id`. Lossless, and a
|
|
855
|
+
// different document — which is exactly why the `local` unit base is cleared above.
|
|
856
|
+
//
|
|
857
|
+
// ⚠️ That same reasoning was never carried to the hashes, so they were left STALE
|
|
858
|
+
// rather than unknown, and `uniweb status` reported unpushed content immediately
|
|
859
|
+
// after a pull, permanently. Measured on matinee 2026-08-29: push → pull reported
|
|
860
|
+
// 1 changed of 8, with nothing edited in between.
|
|
861
|
+
//
|
|
862
|
+
// Re-banking rather than clearing, because after a pull the on-disk state IS the
|
|
863
|
+
// agreed state — it came from the backend, so a push with no edits should send
|
|
864
|
+
// nothing. Clearing would make it send everything.
|
|
865
|
+
//
|
|
866
|
+
// Best-effort: a failure here costs an unnecessary re-send on the next push, never
|
|
867
|
+
// wrong content, and must not fail a pull whose files are already written.
|
|
868
|
+
if (!dryRun) {
|
|
869
|
+
try {
|
|
870
|
+
await rebankSyncHashes(siteDir)
|
|
871
|
+
} catch (err) {
|
|
872
|
+
note(`! could not re-bank the sync cache: ${err.message}`)
|
|
873
|
+
note(' The next push will re-send content that is already current.')
|
|
874
|
+
}
|
|
875
|
+
}
|
|
876
|
+
|
|
843
877
|
success(
|
|
844
878
|
`Pulled — ${pages} page(s), ${sections} section(s), ${records} record(s)` +
|
|
845
879
|
(deleted ? `, ${deleted} deleted` : '')
|
package/src/commands/push.js
CHANGED
|
@@ -85,6 +85,7 @@ import {
|
|
|
85
85
|
readItemBaseVersions,
|
|
86
86
|
readItemUuids,
|
|
87
87
|
readFolderItemUuids,
|
|
88
|
+
readCollectionUuids,
|
|
88
89
|
ensureItemUuids,
|
|
89
90
|
ensureSiteExists,
|
|
90
91
|
clearRemoteSyncStateIfUnbound,
|
|
@@ -474,6 +475,9 @@ export async function push(args = [], deps = {}) {
|
|
|
474
475
|
pkg = await emitSyncPackages(siteDir, {
|
|
475
476
|
// Placement identity for the folder — see writeFolderItemUuids.
|
|
476
477
|
folderItemUuids: readFolderItemUuids(siteDir),
|
|
478
|
+
// Identity for the `collections` section — see readCollectionUuids. Keyed by
|
|
479
|
+
// name, because a declaration has no file for a path-keyed map to hold.
|
|
480
|
+
collectionUuids: readCollectionUuids(siteDir),
|
|
477
481
|
// Resolves a foundation-relative `@/x` model ref into `@org/x`.
|
|
478
482
|
...(asOrg ? { org: asOrg } : {}),
|
|
479
483
|
...(foundationDir ? { foundationDir } : {}),
|
package/src/framework-index.json
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schemaVersion": 1,
|
|
3
|
-
"generatedAt": "2026-08-
|
|
3
|
+
"generatedAt": "2026-08-28T23:49:23.885Z",
|
|
4
4
|
"packages": {
|
|
5
5
|
"@uniweb/build": {
|
|
6
|
-
"version": "0.29.
|
|
6
|
+
"version": "0.29.1",
|
|
7
7
|
"path": "framework/build",
|
|
8
8
|
"deps": [
|
|
9
9
|
"@uniweb/content-reader",
|
|
@@ -112,7 +112,7 @@
|
|
|
112
112
|
"deps": []
|
|
113
113
|
},
|
|
114
114
|
"@uniweb/unipress": {
|
|
115
|
-
"version": "0.8.
|
|
115
|
+
"version": "0.8.16",
|
|
116
116
|
"path": "framework/unipress",
|
|
117
117
|
"deps": [
|
|
118
118
|
"@uniweb/build",
|