uniweb 0.67.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 +6 -6
- package/partials/agents.md +21 -2
- package/src/backend/site-sync.js +33 -1
- package/src/commands/publish.js +10 -9
- package/src/framework-index.json +6 -6
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/
|
|
44
|
+
"@uniweb/runtime": "^0.27.4",
|
|
45
45
|
"@uniweb/core": "^0.29.8",
|
|
46
46
|
"@uniweb/kit": "^0.19.4",
|
|
47
|
-
"@uniweb/
|
|
48
|
-
"@uniweb/
|
|
49
|
-
"@uniweb/
|
|
47
|
+
"@uniweb/semantic-parser": "^1.4.1",
|
|
48
|
+
"@uniweb/schemas": "^0.7.0",
|
|
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
|
@@ -879,6 +879,25 @@ Keep those folders flat: `records/article/design-tips.md` works; `records/articl
|
|
|
879
879
|
|
|
880
880
|
Item frontmatter conventionally uses `title`, `date`, `tags`, `image`, `description`, `author` — plus any fields your content needs (`price`, `role`, `order`). Images can sit beside the item file and be referenced with `./`. **`draft: true` keeps a record off the live site without deleting it**: it is still a record, in `records/`, and `pnpm dev` shows it so you can preview it, but a build for hosting leaves it out, and a backend you `uniweb push` it to keeps it in the site's folder without delivering it. (`published: false`, an older spelling, is refused.)
|
|
881
881
|
|
|
882
|
+
**A record's file follows its data schema's sections.** A schema with one section (the `fields:` form) is written flat, as above. A schema with more than one is written **by section** — each field under its section's name — and a field left at the top stops the build and a push, naming where it goes. `@std/article` has two, its card `article` and `article_body`:
|
|
883
|
+
|
|
884
|
+
```markdown
|
|
885
|
+
<!-- records/std/article/hello.md -->
|
|
886
|
+
---
|
|
887
|
+
article:
|
|
888
|
+
title: Hello
|
|
889
|
+
date: 2026-05-01
|
|
890
|
+
article_body:
|
|
891
|
+
author: Ada
|
|
892
|
+
---
|
|
893
|
+
|
|
894
|
+
The body — the value of the schema's content field, `article_body.content`.
|
|
895
|
+
```
|
|
896
|
+
|
|
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
|
+
|
|
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
|
+
|
|
882
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:
|
|
883
902
|
|
|
884
903
|
```yaml
|
|
@@ -946,7 +965,7 @@ pages/blog/
|
|
|
946
965
|
|
|
947
966
|
**Two options for bigger sets:**
|
|
948
967
|
|
|
949
|
-
`deferred: [body]` strips heavy fields from the list payload — cards stay light, while a `[slug]` page still receives the full record automatically and other components fetch the whole record on demand with `useWholeRecord`. File-based records emit per-record files at `/data/<name>/<slug>.json` and need no configuration; an external query names its one-record request with `record:` instead (*Fetching from other sources* in Part 4).
|
|
968
|
+
`deferred: [body]` strips heavy fields from the list payload — cards stay light, while a `[slug]` page still receives the full record automatically and other components fetch the whole record on demand with `useWholeRecord`. It names keys of the record as a component receives it — a field, or a section's name — and a schema with more than one section implies it: every section but the brief (`[article_body]` for `@std/article`), so it is rarely written. File-based records emit per-record files at `/data/<name>/<slug>.json` and need no configuration; an external query names its one-record request with `record:` instead (*Fetching from other sources* in Part 4).
|
|
950
969
|
|
|
951
970
|
`queryable:` declares which fields a reader may filter on, with enough metadata for the foundation to render controls:
|
|
952
971
|
|
|
@@ -1866,7 +1885,7 @@ fetch:
|
|
|
1866
1885
|
limit: 3
|
|
1867
1886
|
```
|
|
1868
1887
|
|
|
1869
|
-
**Lean lists with `deferred:`.** A query over records with heavy fields (article bodies, large nested arrays) can declare `deferred: [body]
|
|
1888
|
+
**Lean lists with `deferred:`.** A query over records with heavy fields (article bodies, large nested arrays) can declare `deferred: [body]` — keys of the record as a component receives it, a section's name included; a schema with more than one section implies every section but its brief. The list payload omits those fields; per-record full files are emitted at `/data/<name>/<slug>.json`. (An external query declares no `deferred:` — its whole record comes from `record:`, below.) On a parametric page the record's full data is delivered automatically; elsewhere components fetch the whole record on demand with `useWholeRecord(record, { query })`. The hook is safe to call on any query: when the query has no separate source for the whole record it returns the record you passed in, because nothing was stripped from it.
|
|
1870
1889
|
|
|
1871
1890
|
**Component-side fetching.** When a component genuinely needs to fetch on its own (a search box, "load more", a lazy popover), use the kit hooks — `useFetched`, `useCacheEntry`, `useWholeRecord`. They share the framework's cache and dispatcher with declarative fetches; same-key requests dedupe automatically.
|
|
1872
1891
|
|
package/src/backend/site-sync.js
CHANGED
|
@@ -1187,7 +1187,8 @@ export async function probeUnpushed(siteDir, { backend = null, sendAll = false }
|
|
|
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
1189
|
const changed =
|
|
1190
|
-
(pkg.siteContent?.entityCount || 0) +
|
|
1190
|
+
(pkg.siteContent?.entityCount || 0) +
|
|
1191
|
+
(pkg.records?.entityCount || 0)
|
|
1191
1192
|
return { changed, unchanged: pkg.skipped || 0, warnings: pkg.warnings || [] }
|
|
1192
1193
|
}
|
|
1193
1194
|
|
|
@@ -1266,6 +1267,28 @@ export async function rebankSyncHashes(siteDir, backend = null) {
|
|
|
1266
1267
|
return Object.keys(pkg.hashes || {}).length
|
|
1267
1268
|
}
|
|
1268
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
|
+
|
|
1269
1292
|
/**
|
|
1270
1293
|
* What a push did with the site's `template:` intent — read from the backend's own
|
|
1271
1294
|
* reply, never inferred from what we sent.
|
|
@@ -1770,6 +1793,15 @@ export async function pushSyncPackages({
|
|
|
1770
1793
|
// Entities absent from finalized[] (skipped, or not editable) keep their cached
|
|
1771
1794
|
// value — absence is not invalidation.
|
|
1772
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
|
+
}
|
|
1773
1805
|
mergeHarvested()
|
|
1774
1806
|
// Re-base the page attribution: our emitted document and the backend's post-write
|
|
1775
1807
|
// copy of it are the two sides' new agreed state. Only when the site-content lane
|
package/src/commands/publish.js
CHANGED
|
@@ -767,12 +767,6 @@ export async function publish(args = []) {
|
|
|
767
767
|
// (It stamped `info.data_bundle` until 2026-08-18; the ball is gone and
|
|
768
768
|
// collection data now lands at its serving tail, so nothing records it.)
|
|
769
769
|
const priorHashes = readSyncCache(siteDir, client.origin)
|
|
770
|
-
// publish rides the same gated push as `uniweb push`: if an app author has
|
|
771
|
-
// edited since this clone last synced, the push is refused rather than
|
|
772
|
-
// overwriting them, and nothing goes live. `--force` drops the precondition.
|
|
773
|
-
const baseVersions = args.includes('--force')
|
|
774
|
-
? null
|
|
775
|
-
: readBaseVersions(siteDir, client.origin)
|
|
776
770
|
// Per-item identity, recovered from the backend when this clone has never seen it.
|
|
777
771
|
// Without it the backend re-mints every page and section row (see readItemUuids).
|
|
778
772
|
const itemUuids = await ensureItemUuids({
|
|
@@ -948,6 +942,10 @@ export async function publish(args = []) {
|
|
|
948
942
|
say.dim('Services unchanged.')
|
|
949
943
|
}
|
|
950
944
|
|
|
945
|
+
// publish rides the same gated push as `uniweb push`: if an app author has
|
|
946
|
+
// edited since this clone last synced, the push is refused rather than
|
|
947
|
+
// overwriting them, and nothing goes live. `--force` drops the precondition.
|
|
948
|
+
const forced = args.includes('--force')
|
|
951
949
|
let pkg
|
|
952
950
|
try {
|
|
953
951
|
pkg = await emitSyncPackages(siteDir, {
|
|
@@ -959,9 +957,12 @@ export async function publish(args = []) {
|
|
|
959
957
|
resolveModel,
|
|
960
958
|
priorHashes,
|
|
961
959
|
itemUuids,
|
|
962
|
-
...(
|
|
963
|
-
? {
|
|
964
|
-
: {
|
|
960
|
+
...(forced
|
|
961
|
+
? {}
|
|
962
|
+
: {
|
|
963
|
+
baseVersions: readBaseVersions(siteDir, client.origin),
|
|
964
|
+
itemBaseVersions: readItemBaseVersions(siteDir, client.origin)
|
|
965
|
+
}),
|
|
965
966
|
...(Object.keys(injectInfo).length ? { injectInfo } : {}),
|
|
966
967
|
...(Object.keys(ext.pins).length ? { injectExtensions: ext.pins } : {}),
|
|
967
968
|
...(assetRewrite ? { assetRewrite } : {}),
|
package/src/framework-index.json
CHANGED
|
@@ -1,16 +1,16 @@
|
|
|
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
|
-
"version": "0.6.
|
|
6
|
+
"version": "0.6.2",
|
|
7
7
|
"path": "framework/api",
|
|
8
8
|
"deps": [
|
|
9
9
|
"@uniweb/core"
|
|
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",
|
|
@@ -96,7 +96,7 @@
|
|
|
96
96
|
"deps": []
|
|
97
97
|
},
|
|
98
98
|
"@uniweb/schemas": {
|
|
99
|
-
"version": "0.
|
|
99
|
+
"version": "0.7.0",
|
|
100
100
|
"path": "framework/schemas",
|
|
101
101
|
"deps": []
|
|
102
102
|
},
|
|
@@ -116,7 +116,7 @@
|
|
|
116
116
|
"deps": []
|
|
117
117
|
},
|
|
118
118
|
"@uniweb/templates": {
|
|
119
|
-
"version": "0.
|
|
119
|
+
"version": "0.17.0",
|
|
120
120
|
"path": "framework/templates",
|
|
121
121
|
"deps": []
|
|
122
122
|
},
|
|
@@ -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",
|