uniweb 0.69.0 → 0.69.2
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 +143 -6
- package/src/commands/publish.js +35 -20
- package/src/commands/pull.js +46 -3
- package/src/commands/push.js +49 -32
- package/src/framework-index.json +3 -3
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "uniweb",
|
|
3
|
-
"version": "0.69.
|
|
3
|
+
"version": "0.69.2",
|
|
4
4
|
"description": "Create structured Vite + React sites with content/code separation",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -41,15 +41,15 @@
|
|
|
41
41
|
"js-yaml": "^4.1.0",
|
|
42
42
|
"prompts": "^2.4.2",
|
|
43
43
|
"tar": "^7.0.0",
|
|
44
|
-
"@uniweb/
|
|
44
|
+
"@uniweb/content-writer": "^0.3.4",
|
|
45
45
|
"@uniweb/core": "^0.29.8",
|
|
46
46
|
"@uniweb/kit": "^0.19.4",
|
|
47
|
+
"@uniweb/runtime": "^0.27.4",
|
|
47
48
|
"@uniweb/semantic-parser": "^1.4.1",
|
|
48
|
-
"@uniweb/schemas": "^0.7.0"
|
|
49
|
-
"@uniweb/content-writer": "^0.3.4"
|
|
49
|
+
"@uniweb/schemas": "^0.7.0"
|
|
50
50
|
},
|
|
51
51
|
"peerDependencies": {
|
|
52
|
-
"@uniweb/build": "^0.63.
|
|
52
|
+
"@uniweb/build": "^0.63.2",
|
|
53
53
|
"@uniweb/content-reader": "^1.2.5",
|
|
54
54
|
"@uniweb/semantic-parser": "^1.4.1"
|
|
55
55
|
},
|
package/src/backend/site-sync.js
CHANGED
|
@@ -42,7 +42,10 @@ import {
|
|
|
42
42
|
updateBackendMap,
|
|
43
43
|
clearBackendSections,
|
|
44
44
|
normalizeBackendOrigin,
|
|
45
|
-
removeYamlScalar
|
|
45
|
+
removeYamlScalar,
|
|
46
|
+
harvestRecordItems,
|
|
47
|
+
storedRecordItems,
|
|
48
|
+
reprintRecordItems
|
|
46
49
|
} from '@uniweb/build/uwx'
|
|
47
50
|
|
|
48
51
|
// First entity `$`-document out of a `.uwx` we produced or the backend served.
|
|
@@ -62,6 +65,21 @@ function entityDocFromUwx(buf) {
|
|
|
62
65
|
return null
|
|
63
66
|
}
|
|
64
67
|
|
|
68
|
+
// Every entity `$`-document of a `.uwx` — the folder lane holds the folder and its records.
|
|
69
|
+
function entityDocsFromUwx(buf) {
|
|
70
|
+
const docs = []
|
|
71
|
+
if (!buf || buf.length < 2 || buf[0] !== 0x50 || buf[1] !== 0x4b) return docs
|
|
72
|
+
for (const [name, data] of readZip(buf)) {
|
|
73
|
+
if (name === 'manifest.json' || !name.endsWith('.json')) continue
|
|
74
|
+
try {
|
|
75
|
+
docs.push(JSON.parse(data.toString('utf8')))
|
|
76
|
+
} catch {
|
|
77
|
+
// not a document
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
return docs
|
|
81
|
+
}
|
|
82
|
+
|
|
65
83
|
/**
|
|
66
84
|
* Turn an entity-grained staleness refusal into a file-level account.
|
|
67
85
|
*
|
|
@@ -793,6 +811,73 @@ export function writeItemUuids(siteDir, backend, map) {
|
|
|
793
811
|
updateBackendMap(siteDir, backend, 'items', map)
|
|
794
812
|
}
|
|
795
813
|
|
|
814
|
+
/**
|
|
815
|
+
* The identity of each record's list items:
|
|
816
|
+
* `{ <record $uuid>: { <place>: "<item $uuid> <fingerprint>" } }`.
|
|
817
|
+
*
|
|
818
|
+
* ⛔ THE FOURTH MAP, FOR THE SAME REASON AS THE OTHER THREE. An item of a record's `many`
|
|
819
|
+
* section — or of a list nested in one, or of a tree's `$children` — is a `multi` row with no
|
|
820
|
+
* file and no name, so neither a path-keyed map nor the record's own file can hold it. Sent
|
|
821
|
+
* without it, the record is refused whole (`identity_required`), since applying it would
|
|
822
|
+
* replace every stored item: measured 2026-09-25 on an edit of a talk's title. Banked per
|
|
823
|
+
* record from a push's finalized documents (paired by place with what was sent) and from a
|
|
824
|
+
* pull; matched on the next send by content, then by place (`@uniweb/build`'s
|
|
825
|
+
* `record-items.js`). Each record's entry is replaced whole.
|
|
826
|
+
*/
|
|
827
|
+
export function readRecordItemUuids(siteDir, backend) {
|
|
828
|
+
return readBackendState(siteDir, backend).recordItems || {}
|
|
829
|
+
}
|
|
830
|
+
export function writeRecordItemUuids(siteDir, backend, map) {
|
|
831
|
+
if (!map || !Object.keys(map).length) return
|
|
832
|
+
updateBackendMap(siteDir, backend, 'recordItems', map)
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
// The records' list items, banked from the documents the backend returned for them —
|
|
836
|
+
// `returned` by `$uuid` — paired by place with the lists as sent, which ride on each record's
|
|
837
|
+
// index entry (`lists`). A record keeps an entry only while it has list items, or had one.
|
|
838
|
+
function bankRecordItems(siteDir, backend, pairs) {
|
|
839
|
+
const prior = readRecordItemUuids(siteDir, backend)
|
|
840
|
+
const bank = {}
|
|
841
|
+
for (const { uuid, lists, returned } of pairs) {
|
|
842
|
+
if (!uuid || !lists || !returned) continue
|
|
843
|
+
const items = harvestRecordItems(lists, returned)
|
|
844
|
+
if (Object.keys(items).length || prior[uuid]) bank[uuid] = items
|
|
845
|
+
}
|
|
846
|
+
writeRecordItemUuids(siteDir, backend, bank)
|
|
847
|
+
return Object.keys(bank).length
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
/**
|
|
851
|
+
* Recover the list-item identity of records the backend holds and this copy never banked —
|
|
852
|
+
* `uuids`, the emit's `recordItemIdentity.unbanked` — from the backend's own documents: one read
|
|
853
|
+
* of the folder lane, and nothing written but `sync.json`. The send that follows matches their
|
|
854
|
+
* items by place alone (`storedRecordItems`): what was sent before is unknown here.
|
|
855
|
+
*
|
|
856
|
+
* Best-effort, like `ensureItemUuids`: a failure leaves them unbanked, and the backend refuses
|
|
857
|
+
* the send rather than replace their items.
|
|
858
|
+
*
|
|
859
|
+
* @returns {Promise<number>} how many records were recovered
|
|
860
|
+
*/
|
|
861
|
+
export async function recoverRecordItemUuids({ client, siteDir, uuids, note }) {
|
|
862
|
+
const siteUuid = readBackendState(siteDir, client.origin).site?.uuid || null
|
|
863
|
+
if (!siteUuid || !uuids?.length) return 0
|
|
864
|
+
try {
|
|
865
|
+
const res = await client.pullFolder(siteUuid)
|
|
866
|
+
if (!res?.ok) return 0
|
|
867
|
+
const wanted = new Set(uuids)
|
|
868
|
+
const bank = {}
|
|
869
|
+
for (const doc of entityDocsFromUwx(Buffer.from(await res.arrayBuffer()))) {
|
|
870
|
+
if (wanted.has(doc?.$uuid)) bank[doc.$uuid] = storedRecordItems(doc)
|
|
871
|
+
}
|
|
872
|
+
writeRecordItemUuids(siteDir, client.origin, bank)
|
|
873
|
+
const n = Object.keys(bank).length
|
|
874
|
+
if (n) note?.(`Recovered the identity of the list items of ${n} record(s) from the backend.`)
|
|
875
|
+
return n
|
|
876
|
+
} catch {
|
|
877
|
+
return 0
|
|
878
|
+
}
|
|
879
|
+
}
|
|
880
|
+
|
|
796
881
|
/**
|
|
797
882
|
* The workspace this site was created in on `backend` — its owner, as the create
|
|
798
883
|
* answered it — in the header's form: `@handle`, a unit's bare uuid when that unit has
|
|
@@ -1219,7 +1304,7 @@ export async function probeUnpushed(siteDir, { backend = null, sendAll = false }
|
|
|
1219
1304
|
*/
|
|
1220
1305
|
async function comparisonEmit(
|
|
1221
1306
|
siteDir,
|
|
1222
|
-
{ backend = null, priorHashes = {}, sendAll = false } = {}
|
|
1307
|
+
{ backend = null, priorHashes = {}, sendAll = false, declarations = null } = {}
|
|
1223
1308
|
) {
|
|
1224
1309
|
const applied = readAppliedInjections(siteDir, backend)
|
|
1225
1310
|
// ⛔ Per backend: an asset id is minted by one and means nothing to another, so a
|
|
@@ -1229,7 +1314,13 @@ async function comparisonEmit(
|
|
|
1229
1314
|
const queryUuids = readQueryUuids(siteDir, backend)
|
|
1230
1315
|
return emitSyncPackages(siteDir, {
|
|
1231
1316
|
backend,
|
|
1232
|
-
|
|
1317
|
+
// Offline, always. A caller that has just read the Models from the backend — a pull —
|
|
1318
|
+
// may hand them in: the declarations a push would resolve, so the hashes still compare.
|
|
1319
|
+
// Without them a site whose foundation is not in the project (a clone's) resolves none
|
|
1320
|
+
// of its Models here, and nothing is banked.
|
|
1321
|
+
resolveModel: declarations
|
|
1322
|
+
? async (name) => declarations.get(name) ?? null
|
|
1323
|
+
: makeModelResolver({ client: null, offline: true }),
|
|
1233
1324
|
priorHashes,
|
|
1234
1325
|
sendAll,
|
|
1235
1326
|
...applied,
|
|
@@ -1260,16 +1351,38 @@ async function comparisonEmit(
|
|
|
1260
1351
|
* ⭐ Re-banking is the correct answer rather than clearing, because after a pull the
|
|
1261
1352
|
* on-disk state IS the agreed state: it came from the backend. A push with no edits
|
|
1262
1353
|
* in between should send nothing, and clearing would make it send everything.
|
|
1354
|
+
*
|
|
1355
|
+
* @param {object} [opts]
|
|
1356
|
+
* @param {object[]} [opts.recordDocs] - the record documents the pull took; their list items'
|
|
1357
|
+
* identity is banked against the files just written (`readRecordItemUuids`)
|
|
1358
|
+
* @param {Map<string,object>} [opts.declarations] - the Models the pull read, by name — so a
|
|
1359
|
+
* site whose foundation is not in the project (a clone's) can be re-banked offline
|
|
1263
1360
|
*/
|
|
1264
|
-
export async function rebankSyncHashes(siteDir, backend = null) {
|
|
1265
|
-
const pkg = await comparisonEmit(siteDir, { backend, sendAll: true })
|
|
1361
|
+
export async function rebankSyncHashes(siteDir, backend = null, { recordDocs, declarations } = {}) {
|
|
1362
|
+
const pkg = await comparisonEmit(siteDir, { backend, sendAll: true, declarations })
|
|
1266
1363
|
writeSyncCache(siteDir, backend, pkg.hashes || {}, pkg.applied || {})
|
|
1364
|
+
// ⭐ And the identity of the records' list items, from the documents the pull just took:
|
|
1365
|
+
// the files it wrote hold the stored items in stored order, so the emit over them pairs
|
|
1366
|
+
// with each document item for item.
|
|
1367
|
+
if (recordDocs?.length) {
|
|
1368
|
+
const byUuid = new Map(recordDocs.filter((d) => d?.$uuid).map((d) => [d.$uuid, d]))
|
|
1369
|
+
const recordUuids = readBackendState(siteDir, backend).records || {}
|
|
1370
|
+
bankRecordItems(
|
|
1371
|
+
siteDir,
|
|
1372
|
+
backend,
|
|
1373
|
+
(pkg.records?.index || []).map((entry) => {
|
|
1374
|
+
const uuid = entry.ownId ? recordUuids[entry.ownId] : null
|
|
1375
|
+
return { uuid, lists: entry.lists, returned: uuid ? byUuid.get(uuid) : null }
|
|
1376
|
+
})
|
|
1377
|
+
)
|
|
1378
|
+
}
|
|
1267
1379
|
return Object.keys(pkg.hashes || {}).length
|
|
1268
1380
|
}
|
|
1269
1381
|
|
|
1270
1382
|
/**
|
|
1271
1383
|
* Re-bank the hashes of the records a push sent naming a record it created — `keys`,
|
|
1272
|
-
* the emit's `namesNew` — as the next push will hash them
|
|
1384
|
+
* the emit's `namesNew` — as the next push will hash them, and fingerprint their list
|
|
1385
|
+
* items the same way.
|
|
1273
1386
|
*
|
|
1274
1387
|
* ⭐ Such a record names its new record by `$ref`, and the backend stores the uuid it
|
|
1275
1388
|
* mints. The next push names it by that uuid: the same reference, and another hash — so
|
|
@@ -1287,6 +1400,18 @@ export async function rebankRecordHashes(siteDir, backend, keys) {
|
|
|
1287
1400
|
const hashes = { ...readSyncCache(siteDir, backend) }
|
|
1288
1401
|
for (const key of keys) if (pkg.hashes?.[key]) hashes[key] = pkg.hashes[key]
|
|
1289
1402
|
writeSyncCache(siteDir, backend, hashes, readAppliedInjections(siteDir, backend))
|
|
1403
|
+
// Their list items too: an item that named a new record is fingerprinted anew, each uuid kept
|
|
1404
|
+
// at its place (`reprintRecordItems`) — or the next send finds it by place alone.
|
|
1405
|
+
const wanted = new Set(keys)
|
|
1406
|
+
const recordUuids = readBackendState(siteDir, backend).records || {}
|
|
1407
|
+
const bank = readRecordItemUuids(siteDir, backend)
|
|
1408
|
+
const reprinted = {}
|
|
1409
|
+
for (const entry of pkg.records?.index || []) {
|
|
1410
|
+
if (!wanted.has(`${entry.model} ${entry.id}`)) continue
|
|
1411
|
+
const uuid = entry.ownId ? recordUuids[entry.ownId] : null
|
|
1412
|
+
if (uuid && bank[uuid]) reprinted[uuid] = reprintRecordItems(bank[uuid], entry.lists)
|
|
1413
|
+
}
|
|
1414
|
+
writeRecordItemUuids(siteDir, backend, reprinted)
|
|
1290
1415
|
}
|
|
1291
1416
|
|
|
1292
1417
|
/**
|
|
@@ -1766,6 +1891,18 @@ export async function pushSyncPackages({
|
|
|
1766
1891
|
}
|
|
1767
1892
|
for (const w of bf.warnings) note(`! ${w}`)
|
|
1768
1893
|
for (const d of bf.deferred) note(`↷ ${d.id ?? `#${d.index}`}: ${d.reason}`)
|
|
1894
|
+
// ⭐ BANK THE RECORDS' LIST ITEMS: each item's `$uuid` in the document the backend returned,
|
|
1895
|
+
// paired by place with the lists as sent — so the next send of a record updates its items
|
|
1896
|
+
// rather than being refused for replacing them (`readRecordItemUuids`).
|
|
1897
|
+
bankRecordItems(
|
|
1898
|
+
siteDir,
|
|
1899
|
+
client.origin,
|
|
1900
|
+
finalized.map((f) => ({
|
|
1901
|
+
uuid: f?.uuid,
|
|
1902
|
+
lists: Number.isInteger(f?.index) ? records.index?.[f.index]?.lists : null,
|
|
1903
|
+
returned: f?.document
|
|
1904
|
+
}))
|
|
1905
|
+
)
|
|
1769
1906
|
draftsNotKept = bf.notKeptAsDrafts || []
|
|
1770
1907
|
if (bf.updated.length)
|
|
1771
1908
|
wrote.push(`wrote ${bf.updated.length} record file(s)`)
|
package/src/commands/publish.js
CHANGED
|
@@ -86,6 +86,8 @@ import {
|
|
|
86
86
|
readItemBaseVersions,
|
|
87
87
|
ensureItemUuids,
|
|
88
88
|
readFolderItemUuids,
|
|
89
|
+
readRecordItemUuids,
|
|
90
|
+
recoverRecordItemUuids,
|
|
89
91
|
ensureSiteExists,
|
|
90
92
|
refuseUnsendableRecords,
|
|
91
93
|
clearRemoteSyncStateIfUnbound,
|
|
@@ -946,28 +948,41 @@ export async function publish(args = []) {
|
|
|
946
948
|
// edited since this clone last synced, the push is refused rather than
|
|
947
949
|
// overwriting them, and nothing goes live. `--force` drops the precondition.
|
|
948
950
|
const forced = args.includes('--force')
|
|
951
|
+
const emitOptions = {
|
|
952
|
+
backend: client.origin,
|
|
953
|
+
...(declaration.declare ? {} : { declareServices: false }),
|
|
954
|
+
// Placement identity for the folder — see writeFolderItemUuids.
|
|
955
|
+
folderItemUuids: readFolderItemUuids(siteDir, client.origin),
|
|
956
|
+
// Identity for the records' list items — see readRecordItemUuids.
|
|
957
|
+
recordItemUuids: readRecordItemUuids(siteDir, client.origin),
|
|
958
|
+
...(foundationDir ? { foundationDir } : {}),
|
|
959
|
+
resolveModel,
|
|
960
|
+
priorHashes,
|
|
961
|
+
itemUuids,
|
|
962
|
+
...(forced
|
|
963
|
+
? {}
|
|
964
|
+
: {
|
|
965
|
+
baseVersions: readBaseVersions(siteDir, client.origin),
|
|
966
|
+
itemBaseVersions: readItemBaseVersions(siteDir, client.origin)
|
|
967
|
+
}),
|
|
968
|
+
...(Object.keys(injectInfo).length ? { injectInfo } : {}),
|
|
969
|
+
...(Object.keys(ext.pins).length ? { injectExtensions: ext.pins } : {}),
|
|
970
|
+
...(assetRewrite ? { assetRewrite } : {}),
|
|
971
|
+
...(assetIds ? { assetIds } : {})
|
|
972
|
+
}
|
|
949
973
|
let pkg
|
|
950
974
|
try {
|
|
951
|
-
pkg = await emitSyncPackages(siteDir,
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
: {
|
|
963
|
-
baseVersions: readBaseVersions(siteDir, client.origin),
|
|
964
|
-
itemBaseVersions: readItemBaseVersions(siteDir, client.origin)
|
|
965
|
-
}),
|
|
966
|
-
...(Object.keys(injectInfo).length ? { injectInfo } : {}),
|
|
967
|
-
...(Object.keys(ext.pins).length ? { injectExtensions: ext.pins } : {}),
|
|
968
|
-
...(assetRewrite ? { assetRewrite } : {}),
|
|
969
|
-
...(assetIds ? { assetIds } : {})
|
|
970
|
-
})
|
|
975
|
+
pkg = await emitSyncPackages(siteDir, emitOptions)
|
|
976
|
+
// Records the backend holds whose list items this copy never banked — see `uniweb push`.
|
|
977
|
+
const unbanked = pkg.recordItemIdentity?.unbanked
|
|
978
|
+
if (unbanked?.length) {
|
|
979
|
+
if (await recoverRecordItemUuids({ client, siteDir, uuids: unbanked, note: (m) => say.dim(m) })) {
|
|
980
|
+
pkg = await emitSyncPackages(siteDir, {
|
|
981
|
+
...emitOptions,
|
|
982
|
+
recordItemUuids: readRecordItemUuids(siteDir, client.origin)
|
|
983
|
+
})
|
|
984
|
+
}
|
|
985
|
+
}
|
|
971
986
|
} catch (err) {
|
|
972
987
|
say.err(`Could not build the sync package: ${err.message}`)
|
|
973
988
|
return { exitCode: 1 }
|
package/src/commands/pull.js
CHANGED
|
@@ -77,6 +77,7 @@ import {
|
|
|
77
77
|
computeUnitHashes,
|
|
78
78
|
collectUnitUuids,
|
|
79
79
|
collectQueryUuids,
|
|
80
|
+
collectFolderItemUuids,
|
|
80
81
|
documentSchema
|
|
81
82
|
} from '@uniweb/build/uwx'
|
|
82
83
|
import {
|
|
@@ -91,7 +92,8 @@ import {
|
|
|
91
92
|
mergeBaseVersions,
|
|
92
93
|
mergeItemBaseVersions,
|
|
93
94
|
writeUnitBases,
|
|
94
|
-
writeItemUuids
|
|
95
|
+
writeItemUuids,
|
|
96
|
+
writeFolderItemUuids
|
|
95
97
|
} from '../backend/site-sync.js'
|
|
96
98
|
import { resolveWorkspace } from '../backend/workspace.js'
|
|
97
99
|
import {
|
|
@@ -849,6 +851,9 @@ export async function pull(args = [], deps = {}) {
|
|
|
849
851
|
client.pullSiteContent(siteContentUuid, { etag: conditional ? etagContent : undefined })
|
|
850
852
|
)
|
|
851
853
|
if (content?.refused) return { exitCode: 1 }
|
|
854
|
+
// The Models the pulled site's queries name, as the push qualified them — the re-bank
|
|
855
|
+
// below resolves with them too (see there).
|
|
856
|
+
let pulledQueryModels = []
|
|
852
857
|
if (content && !content.notModified) {
|
|
853
858
|
const siteDoc =
|
|
854
859
|
content.docs &&
|
|
@@ -856,6 +861,9 @@ export async function pull(args = [], deps = {}) {
|
|
|
856
861
|
content.docs[0] ||
|
|
857
862
|
null)
|
|
858
863
|
if (siteDoc) {
|
|
864
|
+
pulledQueryModels = (Array.isArray(siteDoc.queries) ? siteDoc.queries : [])
|
|
865
|
+
.map((q) => q?.schema)
|
|
866
|
+
.filter((s) => typeof s === 'string' && /^@[^/]+\//.test(s))
|
|
859
867
|
// Re-base the page attribution. The pulled document IS the backend's own
|
|
860
868
|
// representation, so it becomes the remote base directly. We deliberately
|
|
861
869
|
// CLEAR the local base rather than reuse it: our source files were just
|
|
@@ -962,6 +970,12 @@ export async function pull(args = [], deps = {}) {
|
|
|
962
970
|
// holds a folder uuid). Models are resolved by name (async) up front, so
|
|
963
971
|
// recordsToProject keeps its synchronous contract. A 304 leaves files as-is.
|
|
964
972
|
let folderLane = null
|
|
973
|
+
// The record documents this pull took — the re-bank below pairs their list items with the
|
|
974
|
+
// files just written.
|
|
975
|
+
let pulledRecordDocs = []
|
|
976
|
+
// The Models this pull read from the backend, by name — the re-bank below resolves with
|
|
977
|
+
// them, since a clone's foundation is not in the project to resolve them offline.
|
|
978
|
+
let pulledDeclarations = null
|
|
965
979
|
if (!noRecords) {
|
|
966
980
|
const folder = await getDocs('records', () =>
|
|
967
981
|
client.pullFolder(siteContentUuid, { etag: conditional ? etagFolder : undefined })
|
|
@@ -970,8 +984,10 @@ export async function pull(args = [], deps = {}) {
|
|
|
970
984
|
if (folder?.refused) return { exitCode: 1 }
|
|
971
985
|
if (folder && !folder.notModified && folder.docs?.length) {
|
|
972
986
|
const { folderDoc, recordDocs } = splitRecordsPull(folder.docs)
|
|
987
|
+
pulledRecordDocs = recordDocs
|
|
973
988
|
const resolveModel = makeModelResolver({ client })
|
|
974
989
|
const declByModel = new Map()
|
|
990
|
+
pulledDeclarations = declByModel
|
|
975
991
|
for (const model of [
|
|
976
992
|
...new Set(recordDocs.map((d) => documentSchema(d)).filter(Boolean))
|
|
977
993
|
]) {
|
|
@@ -998,6 +1014,12 @@ export async function pull(args = [], deps = {}) {
|
|
|
998
1014
|
scope: await scopeFor(null)
|
|
999
1015
|
}
|
|
1000
1016
|
})
|
|
1017
|
+
// ⭐ THE FOLDER'S PLACEMENT IDENTITY, from the folder this pull took — as a push banks
|
|
1018
|
+
// it from the folder it sent. ⛔ Only a push banked it until 2026-09-25, so a copy that
|
|
1019
|
+
// had never pushed (every clone) sent its first folder with its placements uuid-less,
|
|
1020
|
+
// and the backend refused that push (`identity_required`, section `contents`) —
|
|
1021
|
+
// measured on a template clone's first edit.
|
|
1022
|
+
if (folderDoc) writeFolderItemUuids(siteDir, client.origin, collectFolderItemUuids(folderDoc))
|
|
1001
1023
|
// The folder's organization, `folder.yml` in the records directory — named as
|
|
1002
1024
|
// the author would write it (`records/folder.yml`, or under `paths.records`).
|
|
1003
1025
|
if (report.records === 'updated') {
|
|
@@ -1069,9 +1091,30 @@ export async function pull(args = [], deps = {}) {
|
|
|
1069
1091
|
//
|
|
1070
1092
|
// Best-effort: a failure here costs an unnecessary re-send on the next push, never
|
|
1071
1093
|
// wrong content, and must not fail a pull whose files are already written.
|
|
1072
|
-
|
|
1094
|
+
// Nothing projected — both lanes unchanged (304) — leaves the banked state describing the
|
|
1095
|
+
// files as they are, so there is nothing to re-bank. Re-banking anyway only failed, noisily,
|
|
1096
|
+
// where the Models cannot be resolved offline (a clone's).
|
|
1097
|
+
const projected = Boolean(content && !content.notModified) || Boolean(folderLane && !folderLane.notModified)
|
|
1098
|
+
if (!dryRun && projected) {
|
|
1073
1099
|
try {
|
|
1074
|
-
|
|
1100
|
+
// ⭐ With the Models this pull read, and those its queries name — a query may name a
|
|
1101
|
+
// schema no record uses yet. The re-bank is offline, and a site whose foundation is not
|
|
1102
|
+
// in the project (a clone's) resolves none of them otherwise: every template clone
|
|
1103
|
+
// said "could not re-bank" until 2026-09-25, and banked nothing.
|
|
1104
|
+
const declarations = pulledDeclarations || new Map()
|
|
1105
|
+
const readModel = makeModelResolver({ client })
|
|
1106
|
+
for (const model of pulledQueryModels) {
|
|
1107
|
+
if (declarations.has(model)) continue
|
|
1108
|
+
try {
|
|
1109
|
+
declarations.set(model, await readModel(model))
|
|
1110
|
+
} catch {
|
|
1111
|
+
// Unreadable here: the re-bank says so if it needs it.
|
|
1112
|
+
}
|
|
1113
|
+
}
|
|
1114
|
+
await rebankSyncHashes(siteDir, client.origin, {
|
|
1115
|
+
recordDocs: pulledRecordDocs,
|
|
1116
|
+
...(declarations.size ? { declarations } : {})
|
|
1117
|
+
})
|
|
1075
1118
|
} catch (err) {
|
|
1076
1119
|
note(`! could not re-bank the sync cache: ${err.message}`)
|
|
1077
1120
|
note(' The next push will re-send content that is already current.')
|
package/src/commands/push.js
CHANGED
|
@@ -82,6 +82,8 @@ import {
|
|
|
82
82
|
readItemUuids,
|
|
83
83
|
readFolderItemUuids,
|
|
84
84
|
readQueryUuids,
|
|
85
|
+
readRecordItemUuids,
|
|
86
|
+
recoverRecordItemUuids,
|
|
85
87
|
ensureItemUuids,
|
|
86
88
|
refuseUnsendableRecords,
|
|
87
89
|
ensureSiteExists,
|
|
@@ -496,40 +498,55 @@ export async function push(args = [], deps = {}) {
|
|
|
496
498
|
output || dryRun
|
|
497
499
|
? readItemUuids(siteDir, client.origin)
|
|
498
500
|
: await ensureItemUuids({ client, siteDir, note })
|
|
501
|
+
const emitOptions = {
|
|
502
|
+
backend: client.origin,
|
|
503
|
+
// Placement identity for the folder — see writeFolderItemUuids.
|
|
504
|
+
folderItemUuids: readFolderItemUuids(siteDir, client.origin),
|
|
505
|
+
// Identity for the `queries` section — see readQueryUuids. Keyed by
|
|
506
|
+
// name, because a declaration has no file for a path-keyed map to hold.
|
|
507
|
+
queryUuids: readQueryUuids(siteDir, client.origin),
|
|
508
|
+
// Identity for the records' list items — see readRecordItemUuids.
|
|
509
|
+
recordItemUuids: readRecordItemUuids(siteDir, client.origin),
|
|
510
|
+
...(foundationDir ? { foundationDir } : {}),
|
|
511
|
+
resolveModel: makeModelResolver({
|
|
512
|
+
client,
|
|
513
|
+
offline: Boolean(output) || dryRun
|
|
514
|
+
}),
|
|
515
|
+
priorHashes,
|
|
516
|
+
sendAll,
|
|
517
|
+
itemUuids,
|
|
518
|
+
// The PINNED foundation ref from the bring-along above, stamped over the
|
|
519
|
+
// authored `site.yml` string. Delivery is version-pinned end to end, so an
|
|
520
|
+
// unpinned local name on the wire names code no host can serve. Absent when
|
|
521
|
+
// the site already references a registry ref or URL — then site.yml's own
|
|
522
|
+
// value rides verbatim.
|
|
523
|
+
...(fnd.ref ? { injectInfo: { foundation: fnd.ref } } : {}),
|
|
524
|
+
// Both grains are dropped together by --force: one flag, one meaning,
|
|
525
|
+
// no partial-force mode.
|
|
526
|
+
...(force
|
|
527
|
+
? {}
|
|
528
|
+
: {
|
|
529
|
+
baseVersions: readBaseVersions(siteDir, client.origin),
|
|
530
|
+
itemBaseVersions: readItemBaseVersions(siteDir, client.origin)
|
|
531
|
+
}),
|
|
532
|
+
...(assetRewrite ? { assetRewrite } : {}),
|
|
533
|
+
...(assetIds ? { assetIds } : {})
|
|
534
|
+
}
|
|
499
535
|
let pkg
|
|
500
536
|
try {
|
|
501
|
-
pkg = await emitSyncPackages(siteDir,
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
}
|
|
513
|
-
|
|
514
|
-
sendAll,
|
|
515
|
-
itemUuids,
|
|
516
|
-
// The PINNED foundation ref from the bring-along above, stamped over the
|
|
517
|
-
// authored `site.yml` string. Delivery is version-pinned end to end, so an
|
|
518
|
-
// unpinned local name on the wire names code no host can serve. Absent when
|
|
519
|
-
// the site already references a registry ref or URL — then site.yml's own
|
|
520
|
-
// value rides verbatim.
|
|
521
|
-
...(fnd.ref ? { injectInfo: { foundation: fnd.ref } } : {}),
|
|
522
|
-
// Both grains are dropped together by --force: one flag, one meaning,
|
|
523
|
-
// no partial-force mode.
|
|
524
|
-
...(force
|
|
525
|
-
? {}
|
|
526
|
-
: {
|
|
527
|
-
baseVersions: readBaseVersions(siteDir, client.origin),
|
|
528
|
-
itemBaseVersions: readItemBaseVersions(siteDir, client.origin)
|
|
529
|
-
}),
|
|
530
|
-
...(assetRewrite ? { assetRewrite } : {}),
|
|
531
|
-
...(assetIds ? { assetIds } : {})
|
|
532
|
-
})
|
|
537
|
+
pkg = await emitSyncPackages(siteDir, emitOptions)
|
|
538
|
+
// A record the backend holds whose list items this copy never banked — pushed before the
|
|
539
|
+
// bank existed, say — is refused if sent without them. Recover them from the backend, and
|
|
540
|
+
// build the package again with them.
|
|
541
|
+
const unbanked = pkg.recordItemIdentity?.unbanked
|
|
542
|
+
if (unbanked?.length && !output && !dryRun) {
|
|
543
|
+
if (await recoverRecordItemUuids({ client, siteDir, uuids: unbanked, note })) {
|
|
544
|
+
pkg = await emitSyncPackages(siteDir, {
|
|
545
|
+
...emitOptions,
|
|
546
|
+
recordItemUuids: readRecordItemUuids(siteDir, client.origin)
|
|
547
|
+
})
|
|
548
|
+
}
|
|
549
|
+
}
|
|
533
550
|
} catch (err) {
|
|
534
551
|
error(`Could not build the sync package: ${err.message}`)
|
|
535
552
|
return { exitCode: 2 }
|
package/src/framework-index.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schemaVersion": 1,
|
|
3
|
-
"generatedAt": "2026-09-
|
|
3
|
+
"generatedAt": "2026-09-25T13:50:26.904Z",
|
|
4
4
|
"packages": {
|
|
5
5
|
"@uniweb/api": {
|
|
6
6
|
"version": "0.6.2",
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
]
|
|
11
11
|
},
|
|
12
12
|
"@uniweb/build": {
|
|
13
|
-
"version": "0.63.
|
|
13
|
+
"version": "0.63.2",
|
|
14
14
|
"path": "framework/build",
|
|
15
15
|
"deps": [
|
|
16
16
|
"@uniweb/content-reader",
|
|
@@ -116,7 +116,7 @@
|
|
|
116
116
|
"deps": []
|
|
117
117
|
},
|
|
118
118
|
"@uniweb/templates": {
|
|
119
|
-
"version": "0.17.
|
|
119
|
+
"version": "0.17.2",
|
|
120
120
|
"path": "framework/templates",
|
|
121
121
|
"deps": []
|
|
122
122
|
},
|