uniweb 0.68.0 → 0.69.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 +5 -5
- package/partials/agents.md +1 -1
- package/src/backend/site-sync.js +32 -79
- package/src/commands/publish.js +7 -20
- package/src/commands/push.js +8 -21
- package/src/framework-index.json +3 -3
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "uniweb",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.69.0",
|
|
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/content-writer": "^0.3.4",
|
|
45
|
-
"@uniweb/core": "^0.29.8",
|
|
46
44
|
"@uniweb/runtime": "^0.27.4",
|
|
45
|
+
"@uniweb/core": "^0.29.8",
|
|
47
46
|
"@uniweb/kit": "^0.19.4",
|
|
47
|
+
"@uniweb/semantic-parser": "^1.4.1",
|
|
48
48
|
"@uniweb/schemas": "^0.7.0",
|
|
49
|
-
"@uniweb/
|
|
49
|
+
"@uniweb/content-writer": "^0.3.4"
|
|
50
50
|
},
|
|
51
51
|
"peerDependencies": {
|
|
52
|
-
"@uniweb/build": "^0.
|
|
52
|
+
"@uniweb/build": "^0.63.0",
|
|
53
53
|
"@uniweb/content-reader": "^1.2.5",
|
|
54
54
|
"@uniweb/semantic-parser": "^1.4.1"
|
|
55
55
|
},
|
package/partials/agents.md
CHANGED
|
@@ -896,7 +896,7 @@ The body — the value of the schema's content field, `article_body.content`.
|
|
|
896
896
|
|
|
897
897
|
**A component receives it differently from how the file writes it**, the same way on a static site and a hosted one: the brief's fields at the top (`article.title`), every other section under its name (`article.article_body.content`, `article.article_body.author`), and `$name`, the handle. Lists carry the brief alone unless the query says otherwise (`deferred:`, below). Full rules: `reference/entity-content.md`.
|
|
898
898
|
|
|
899
|
-
**A reference names the record it points at.** A `{ ref: '@/speaker' }` field is written as that record's name — `speaker: ada` for `records/speaker/ada.yml` (its file's name, or its `slug:`) — and a `many` one as a list of names. A component receives `{ entity, brief }`: the record reduced to its brief, so `talk.speaker.brief.name`. `uniweb validate` and a push report a name that matches no record; a push sends each reference to the record it names (
|
|
899
|
+
**A reference names the record it points at.** A `{ ref: '@/speaker' }` field is written as that record's name — `speaker: ada` for `records/speaker/ada.yml` (its file's name, or its `slug:`) — and a `many` one as a list of names. A component receives `{ entity, brief }`: the record reduced to its brief, so `talk.speaker.brief.name`. `uniweb validate` and a push report a name that matches no record; a push sends each reference to the record it names (in the same push when that record is new too), and a pull writes the name back.
|
|
900
900
|
|
|
901
901
|
**`records/folder.yml` — optional, and only for folders.** It is the one file in `records/` that is not a record, it moves with the directory (`paths.records`), and like a pages folder's `folder.yml` it describes the folder it sits in. Every record sits at the top of the site's records folder unless `folder.yml` places it in a sub-folder. Add one only when a query needs to ask for a *slice* of the records rather than all of them — most sites never do:
|
|
902
902
|
|
package/src/backend/site-sync.js
CHANGED
|
@@ -1186,12 +1186,9 @@ export async function probeUnpushed(siteDir, { backend = null, sendAll = false }
|
|
|
1186
1186
|
// ⚠️ Until 2026-09-23 this said the site's recorded ORG resolves it and must be
|
|
1187
1187
|
// passed in — true before 2026-09-22, and read as a reason to pass it.
|
|
1188
1188
|
const pkg = await comparisonEmit(siteDir, { backend, priorHashes, sendAll })
|
|
1189
|
-
// A record held back for a reference (`pushInPasses`) is not in either lane, and is
|
|
1190
|
-
// still unpushed.
|
|
1191
1189
|
const changed =
|
|
1192
1190
|
(pkg.siteContent?.entityCount || 0) +
|
|
1193
|
-
(pkg.records?.entityCount || 0)
|
|
1194
|
-
(pkg.waiting || []).filter((w) => w.held).length
|
|
1191
|
+
(pkg.records?.entityCount || 0)
|
|
1195
1192
|
return { changed, unchanged: pkg.skipped || 0, warnings: pkg.warnings || [] }
|
|
1196
1193
|
}
|
|
1197
1194
|
|
|
@@ -1270,6 +1267,28 @@ export async function rebankSyncHashes(siteDir, backend = null) {
|
|
|
1270
1267
|
return Object.keys(pkg.hashes || {}).length
|
|
1271
1268
|
}
|
|
1272
1269
|
|
|
1270
|
+
/**
|
|
1271
|
+
* 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.
|
|
1273
|
+
*
|
|
1274
|
+
* ⭐ Such a record names its new record by `$ref`, and the backend stores the uuid it
|
|
1275
|
+
* mints. The next push names it by that uuid: the same reference, and another hash — so
|
|
1276
|
+
* the hash the push banked describes a document no later emit builds, and the next push
|
|
1277
|
+
* would send every one of these records again. Re-sending is not harmless: a record's
|
|
1278
|
+
* list-section items go without the identity a re-send needs (`identity_required`).
|
|
1279
|
+
*
|
|
1280
|
+
* ⚖️ Only these keys, over the hashes the push just banked: the site-content lane and
|
|
1281
|
+
* the folder are banked as that push sent them, and a caller's own emit options (a
|
|
1282
|
+
* publish that does not declare services) are not the comparison emit's.
|
|
1283
|
+
*/
|
|
1284
|
+
export async function rebankRecordHashes(siteDir, backend, keys) {
|
|
1285
|
+
if (!keys?.length) return
|
|
1286
|
+
const pkg = await comparisonEmit(siteDir, { backend, sendAll: true })
|
|
1287
|
+
const hashes = { ...readSyncCache(siteDir, backend) }
|
|
1288
|
+
for (const key of keys) if (pkg.hashes?.[key]) hashes[key] = pkg.hashes[key]
|
|
1289
|
+
writeSyncCache(siteDir, backend, hashes, readAppliedInjections(siteDir, backend))
|
|
1290
|
+
}
|
|
1291
|
+
|
|
1273
1292
|
/**
|
|
1274
1293
|
* What a push did with the site's `template:` intent — read from the backend's own
|
|
1275
1294
|
* reply, never inferred from what we sent.
|
|
@@ -1774,6 +1793,15 @@ export async function pushSyncPackages({
|
|
|
1774
1793
|
// Entities absent from finalized[] (skipped, or not editable) keep their cached
|
|
1775
1794
|
// value — absence is not invalidation.
|
|
1776
1795
|
writeSyncCache(siteDir, client.origin, hashes, applied)
|
|
1796
|
+
// A record that named a record this push created is banked as the next push will send
|
|
1797
|
+
// it — by that record's uuid, which the back-fill above just recorded.
|
|
1798
|
+
if (records && pkg.namesNew?.length) {
|
|
1799
|
+
try {
|
|
1800
|
+
await rebankRecordHashes(siteDir, client.origin, pkg.namesNew)
|
|
1801
|
+
} catch (err) {
|
|
1802
|
+
note(`! Could not re-bank ${pkg.namesNew.length} record(s) that name a new record: ${err.message}. The next push sends them again.`)
|
|
1803
|
+
}
|
|
1804
|
+
}
|
|
1777
1805
|
mergeHarvested()
|
|
1778
1806
|
// Re-base the page attribution: our emitted document and the backend's post-write
|
|
1779
1807
|
// copy of it are the two sides' new agreed state. Only when the site-content lane
|
|
@@ -1850,78 +1878,3 @@ export async function pushSyncPackages({
|
|
|
1850
1878
|
}
|
|
1851
1879
|
return { exitCode: 0, boundSiteUuid, finalizedTotal, wrote }
|
|
1852
1880
|
}
|
|
1853
|
-
|
|
1854
|
-
/**
|
|
1855
|
-
* Push, then push again while records WAIT for a reference's record — the verbs' way to
|
|
1856
|
-
* send a package (`push`, `publish`).
|
|
1857
|
-
*
|
|
1858
|
-
* ⭐ A reference is sent as the uuid the backend minted for the record it names, so a
|
|
1859
|
-
* record that points at one created by the same push cannot be completed in that push
|
|
1860
|
-
* (`@uniweb/build`'s `emitSyncPackages` → `waiting`). The first pass creates what it can —
|
|
1861
|
-
* records whose waiting references are not required go without them — and each further
|
|
1862
|
-
* pass, emitted afresh from the state the last one banked, sends what has become
|
|
1863
|
-
* nameable. A chain of required references takes one pass per link; two records that
|
|
1864
|
-
* each require the other can never be completed, and the push stops, naming them.
|
|
1865
|
-
*
|
|
1866
|
-
* @param {object} params
|
|
1867
|
-
* @param {object} params.pkg - the first pass's package (`emitSyncPackages`)
|
|
1868
|
-
* @param {() => Promise<object>} params.reemit - the next pass's package, emitted afresh
|
|
1869
|
-
* @param {Function} [params.push] - sends one package; `pushSyncPackages` (a test's seam)
|
|
1870
|
-
* @returns {Promise<{ exitCode: number, boundSiteUuid?: string, finalizedTotal: number, wrote: string[], passes: number }>}
|
|
1871
|
-
*/
|
|
1872
|
-
export async function pushInPasses({ client, siteDir, pkg, report, reemit, maxPasses = 12, push = pushSyncPackages }) {
|
|
1873
|
-
const { note, error } = report
|
|
1874
|
-
let current = pkg
|
|
1875
|
-
let passes = 1
|
|
1876
|
-
let result = await push({ client, siteDir, pkg: current, report })
|
|
1877
|
-
let finalizedTotal = result.finalizedTotal || 0
|
|
1878
|
-
const wrote = [...(result.wrote || [])]
|
|
1879
|
-
while (result.exitCode === 0 && current.waiting?.length) {
|
|
1880
|
-
const n = current.waiting.length
|
|
1881
|
-
note(
|
|
1882
|
-
`${n} record${n === 1 ? '' : 's'} name${n === 1 ? 's' : ''} a record this push created — ` +
|
|
1883
|
-
`pushing again to complete ${n === 1 ? 'its references' : 'their references'}.`
|
|
1884
|
-
)
|
|
1885
|
-
let next
|
|
1886
|
-
try {
|
|
1887
|
-
next = await reemit()
|
|
1888
|
-
} catch (err) {
|
|
1889
|
-
error(`Could not build the next pass: ${err.message}`)
|
|
1890
|
-
return { ...result, exitCode: 1, finalizedTotal, wrote, passes }
|
|
1891
|
-
}
|
|
1892
|
-
if (refuseUnsendableRecords(next.refusals, report)) {
|
|
1893
|
-
return { ...result, exitCode: 1, finalizedTotal, wrote, passes }
|
|
1894
|
-
}
|
|
1895
|
-
// Nothing became nameable: every record still waiting waits on one that waits too.
|
|
1896
|
-
if (waitingKey(next.waiting) === waitingKey(current.waiting) || passes >= maxPasses) {
|
|
1897
|
-
error(
|
|
1898
|
-
'These records could not be completed — each requires a reference to a record that ' +
|
|
1899
|
-
'is itself waiting to be created:'
|
|
1900
|
-
)
|
|
1901
|
-
for (const w of next.waiting || []) {
|
|
1902
|
-
const refs = w.pending.map((p) => `${p.path} → "${p.name}"`).join(', ')
|
|
1903
|
-
note(` ${w.id} (${w.model}): ${refs}`)
|
|
1904
|
-
}
|
|
1905
|
-
note(
|
|
1906
|
-
'Push one of them without that reference first — or make it optional in the schema — ' +
|
|
1907
|
-
'then add it back and push again.'
|
|
1908
|
-
)
|
|
1909
|
-
return { ...result, exitCode: 1, finalizedTotal, wrote, passes }
|
|
1910
|
-
}
|
|
1911
|
-
current = next
|
|
1912
|
-
passes++
|
|
1913
|
-
result = await push({ client, siteDir, pkg: current, report })
|
|
1914
|
-
finalizedTotal += result.finalizedTotal || 0
|
|
1915
|
-
for (const w of result.wrote || []) if (!wrote.includes(w)) wrote.push(w)
|
|
1916
|
-
}
|
|
1917
|
-
return { ...result, finalizedTotal, wrote, passes }
|
|
1918
|
-
}
|
|
1919
|
-
|
|
1920
|
-
// Which records wait, and on what — two passes that leave it the same made no progress.
|
|
1921
|
-
function waitingKey(waiting) {
|
|
1922
|
-
return JSON.stringify(
|
|
1923
|
-
(waiting || [])
|
|
1924
|
-
.map((w) => [w.model, w.id, w.pending.map((p) => `${p.path}:${p.name}`).sort()])
|
|
1925
|
-
.sort()
|
|
1926
|
-
)
|
|
1927
|
-
}
|
package/src/commands/publish.js
CHANGED
|
@@ -90,8 +90,7 @@ import {
|
|
|
90
90
|
refuseUnsendableRecords,
|
|
91
91
|
clearRemoteSyncStateIfUnbound,
|
|
92
92
|
dropSiteBoundValues,
|
|
93
|
-
|
|
94
|
-
pushInPasses
|
|
93
|
+
pushSyncPackages
|
|
95
94
|
} from '../backend/site-sync.js'
|
|
96
95
|
import { resolveWorkspace, describeWorkspace, SOURCE_LABEL } from '../backend/workspace.js'
|
|
97
96
|
import { uploadSiteMedia, describeAssetRefusal } from '../backend/site-media.js'
|
|
@@ -943,14 +942,13 @@ export async function publish(args = []) {
|
|
|
943
942
|
say.dim('Services unchanged.')
|
|
944
943
|
}
|
|
945
944
|
|
|
946
|
-
// The emit's options, read afresh for each pass (`pushInPasses`): a pass banks
|
|
947
|
-
// hashes, identity and base versions that the next one must build on.
|
|
948
|
-
//
|
|
949
945
|
// publish rides the same gated push as `uniweb push`: if an app author has
|
|
950
946
|
// edited since this clone last synced, the push is refused rather than
|
|
951
947
|
// overwriting them, and nothing goes live. `--force` drops the precondition.
|
|
952
948
|
const forced = args.includes('--force')
|
|
953
|
-
|
|
949
|
+
let pkg
|
|
950
|
+
try {
|
|
951
|
+
pkg = await emitSyncPackages(siteDir, {
|
|
954
952
|
backend: client.origin,
|
|
955
953
|
...(declaration.declare ? {} : { declareServices: false }),
|
|
956
954
|
// Placement identity for the folder — see writeFolderItemUuids.
|
|
@@ -969,10 +967,7 @@ export async function publish(args = []) {
|
|
|
969
967
|
...(Object.keys(ext.pins).length ? { injectExtensions: ext.pins } : {}),
|
|
970
968
|
...(assetRewrite ? { assetRewrite } : {}),
|
|
971
969
|
...(assetIds ? { assetIds } : {})
|
|
972
|
-
|
|
973
|
-
let pkg
|
|
974
|
-
try {
|
|
975
|
-
pkg = await emitSyncPackages(siteDir, emitOptions({ priorHashes, itemUuids }))
|
|
970
|
+
})
|
|
976
971
|
} catch (err) {
|
|
977
972
|
say.err(`Could not build the sync package: ${err.message}`)
|
|
978
973
|
return { exitCode: 1 }
|
|
@@ -987,19 +982,11 @@ export async function publish(args = []) {
|
|
|
987
982
|
error: (m) => say.err(m),
|
|
988
983
|
dim: (s) => `${c.dim}${s}${c.reset}`
|
|
989
984
|
}
|
|
990
|
-
const pushResult = await
|
|
985
|
+
const pushResult = await pushSyncPackages({
|
|
991
986
|
client,
|
|
992
987
|
siteDir,
|
|
993
988
|
pkg,
|
|
994
|
-
report
|
|
995
|
-
reemit: () =>
|
|
996
|
-
emitSyncPackages(
|
|
997
|
-
siteDir,
|
|
998
|
-
emitOptions({
|
|
999
|
-
priorHashes: readSyncCache(siteDir, client.origin),
|
|
1000
|
-
itemUuids: readItemUuids(siteDir, client.origin)
|
|
1001
|
-
})
|
|
1002
|
-
)
|
|
989
|
+
report
|
|
1003
990
|
})
|
|
1004
991
|
if (pushResult.exitCode !== 0) return { exitCode: pushResult.exitCode }
|
|
1005
992
|
const siteUuid = pushResult.boundSiteUuid
|
package/src/commands/push.js
CHANGED
|
@@ -87,7 +87,7 @@ import {
|
|
|
87
87
|
ensureSiteExists,
|
|
88
88
|
clearRemoteSyncStateIfUnbound,
|
|
89
89
|
dropSiteBoundValues,
|
|
90
|
-
|
|
90
|
+
pushSyncPackages
|
|
91
91
|
} from '../backend/site-sync.js'
|
|
92
92
|
import { resolveWorkspace, describeWorkspace, SOURCE_LABEL } from '../backend/workspace.js'
|
|
93
93
|
|
|
@@ -496,9 +496,9 @@ export async function push(args = [], deps = {}) {
|
|
|
496
496
|
output || dryRun
|
|
497
497
|
? readItemUuids(siteDir, client.origin)
|
|
498
498
|
: await ensureItemUuids({ client, siteDir, note })
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
499
|
+
let pkg
|
|
500
|
+
try {
|
|
501
|
+
pkg = await emitSyncPackages(siteDir, {
|
|
502
502
|
backend: client.origin,
|
|
503
503
|
// Placement identity for the folder — see writeFolderItemUuids.
|
|
504
504
|
folderItemUuids: readFolderItemUuids(siteDir, client.origin),
|
|
@@ -529,10 +529,7 @@ export async function push(args = [], deps = {}) {
|
|
|
529
529
|
}),
|
|
530
530
|
...(assetRewrite ? { assetRewrite } : {}),
|
|
531
531
|
...(assetIds ? { assetIds } : {})
|
|
532
|
-
|
|
533
|
-
let pkg
|
|
534
|
-
try {
|
|
535
|
-
pkg = await emitSyncPackages(siteDir, emitOptions({ priorHashes, sendAll, itemUuids }))
|
|
532
|
+
})
|
|
536
533
|
} catch (err) {
|
|
537
534
|
error(`Could not build the sync package: ${err.message}`)
|
|
538
535
|
return { exitCode: 2 }
|
|
@@ -598,9 +595,8 @@ export async function push(args = [], deps = {}) {
|
|
|
598
595
|
}
|
|
599
596
|
|
|
600
597
|
// Submit both lanes, back-fill the minted uuids, and persist the send-only-changed
|
|
601
|
-
// cache
|
|
602
|
-
|
|
603
|
-
const result = await pushInPasses({
|
|
598
|
+
// cache. Shared with `uniweb publish` via ../backend/site-sync.js.
|
|
599
|
+
const result = await pushSyncPackages({
|
|
604
600
|
client,
|
|
605
601
|
siteDir,
|
|
606
602
|
pkg,
|
|
@@ -609,16 +605,7 @@ export async function push(args = [], deps = {}) {
|
|
|
609
605
|
note,
|
|
610
606
|
error,
|
|
611
607
|
dim: (s) => `${colors.dim}${s}${colors.reset}`
|
|
612
|
-
}
|
|
613
|
-
reemit: () =>
|
|
614
|
-
emitSyncPackages(
|
|
615
|
-
siteDir,
|
|
616
|
-
emitOptions({
|
|
617
|
-
priorHashes: readSyncCache(siteDir, client.origin),
|
|
618
|
-
sendAll: false,
|
|
619
|
-
itemUuids: readItemUuids(siteDir, client.origin)
|
|
620
|
-
})
|
|
621
|
-
)
|
|
608
|
+
}
|
|
622
609
|
})
|
|
623
610
|
if (result.exitCode !== 0) return { exitCode: result.exitCode }
|
|
624
611
|
success(
|
package/src/framework-index.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schemaVersion": 1,
|
|
3
|
-
"generatedAt": "2026-09-
|
|
3
|
+
"generatedAt": "2026-09-25T11:51:15.075Z",
|
|
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.
|
|
13
|
+
"version": "0.63.0",
|
|
14
14
|
"path": "framework/build",
|
|
15
15
|
"deps": [
|
|
16
16
|
"@uniweb/content-reader",
|
|
@@ -126,7 +126,7 @@
|
|
|
126
126
|
"deps": []
|
|
127
127
|
},
|
|
128
128
|
"@uniweb/unipress": {
|
|
129
|
-
"version": "0.10.
|
|
129
|
+
"version": "0.10.6",
|
|
130
130
|
"path": "framework/unipress",
|
|
131
131
|
"deps": [
|
|
132
132
|
"@uniweb/build",
|