uniweb 0.66.0 → 0.68.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 +23 -4
- package/src/backend/foundation-bring-along.js +4 -3
- package/src/backend/site-sync.js +80 -1
- package/src/commands/publish.js +30 -16
- package/src/commands/push.js +21 -8
- package/src/commands/validate.js +5 -4
- package/src/framework-index.json +6 -6
- package/src/index.js +3 -3
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "uniweb",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.68.0",
|
|
4
4
|
"description": "Create structured Vite + React sites with content/code separation",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -41,17 +41,17 @@
|
|
|
41
41
|
"js-yaml": "^4.1.0",
|
|
42
42
|
"prompts": "^2.4.2",
|
|
43
43
|
"tar": "^7.0.0",
|
|
44
|
-
"@uniweb/kit": "^0.19.4",
|
|
45
|
-
"@uniweb/runtime": "^0.27.4",
|
|
46
44
|
"@uniweb/content-writer": "^0.3.4",
|
|
47
|
-
"@uniweb/schemas": "^0.5.0",
|
|
48
45
|
"@uniweb/core": "^0.29.8",
|
|
46
|
+
"@uniweb/runtime": "^0.27.4",
|
|
47
|
+
"@uniweb/kit": "^0.19.4",
|
|
48
|
+
"@uniweb/schemas": "^0.7.0",
|
|
49
49
|
"@uniweb/semantic-parser": "^1.4.1"
|
|
50
50
|
},
|
|
51
51
|
"peerDependencies": {
|
|
52
|
+
"@uniweb/build": "^0.62.0",
|
|
52
53
|
"@uniweb/content-reader": "^1.2.5",
|
|
53
|
-
"@uniweb/semantic-parser": "^1.4.1"
|
|
54
|
-
"@uniweb/build": "^0.60.0"
|
|
54
|
+
"@uniweb/semantic-parser": "^1.4.1"
|
|
55
55
|
},
|
|
56
56
|
"peerDependenciesMeta": {
|
|
57
57
|
"@uniweb/build": {
|
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 (a second pass 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,11 +1885,11 @@ 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
|
|
|
1873
|
-
**Validate before shipping.** `uniweb validate` checks file-based data against your declared schemas — missing required fields, type/enum/format mismatches, nested fields. A violation fails it (`--lax` only reports); it checks every record file in `records/`, and `push` / `publish` refuse the same findings before sending anything. Distinct from `uniweb doctor` (project structure): `validate` checks your *data* against the schemas you *declared*. External queries (`url:`)
|
|
1892
|
+
**Validate before shipping.** `uniweb validate` checks file-based data against your declared schemas — missing required fields, type/enum/format mismatches, nested fields. A violation fails it (`--lax` only reports); it checks every record file in `records/`, and `push` / `publish` refuse the same findings before sending anything. Distinct from `uniweb doctor` (project structure): `validate` checks your *data* against the schemas you *declared*. External queries (`url:`) are reported as deferred — their data isn't in the project; the value of a `ref`/`options` field is not checked against the entity it names.
|
|
1874
1893
|
|
|
1875
1894
|
### Fetching from other sources (`fetcher:`)
|
|
1876
1895
|
|
|
@@ -2609,7 +2628,7 @@ Foundations have their own free path too: `uniweb add ci --target foundation` pu
|
|
|
2609
2628
|
|
|
2610
2629
|
Either side can publish. Nothing about this changes how you build: the same foundation and the same site run under `uniweb dev`, `uniweb export`, or a CI deploy with no account at all.
|
|
2611
2630
|
|
|
2612
|
-
**Publishing vs registering.** Foundations on Uniweb Cloud live in the catalog as `@org/name@version` — `name` from the foundation's `main.js`. When a foundation powers a single site, **don't run `uniweb register` yourself** — `uniweb publish` from the site directory releases the local foundation to the catalog (when its code changed) and goes live in one step. A registered version is immutable, so when the foundation's code changed under a version already registered, `push` and `publish` release the change under the next version and write it into the foundation's `package.json` — commit that file (`--no-release` sends the content without the code). They stop when the registry holds a version newer than yours, released from another copy: pull that change first, or pass `--bump` to release yours above it. Register deliberately only when the foundation is a product meant for multiple sites; consuming sites then pin `foundation: '@org/name@1.2.3'`. **The catalog is private and access-segregated, not a public package registry** — people see only the foundations licensed to sites they own or edit. The *site* carries the license, and it rides along with site ownership when a developer hands a site to a client. Don't describe publishing as making a foundation publicly discoverable. Schemas can also be registered on their own from a schemas-only package (`@uniweb/schemas`, any `@org/schemas`, or a bare folder of `schemas/*.{yml,json,js}`) — that's how `@std` schemas are published. Auth via `uniweb login` (`uniweb login --backend <url> --token <bearer>` without a terminal) or `UNIWEB_TOKEN`; preview with `--dry-run`.
|
|
2631
|
+
**Publishing vs registering.** Foundations on Uniweb Cloud live in the catalog as `@org/name@version` — `name` from the foundation's `main.js`. When a foundation powers a single site, **don't run `uniweb register` yourself** — `uniweb publish` from the site directory releases the local foundation to the catalog (when its code changed) and goes live in one step. A registered version is immutable, so when the foundation's code changed under a version already registered, `push` and `publish` release the change under the next version and write it into the foundation's `package.json` — commit that file (`--no-release` sends the content without the code). The version it picks is always the next patch: for a change that is not backwards compatible, set a higher minor or major version in the foundation's `package.json` yourself before you push. They stop when the registry holds a version newer than yours, released from another copy: pull that change first, or pass `--bump` to release yours above it. Register deliberately only when the foundation is a product meant for multiple sites; consuming sites then pin `foundation: '@org/name@1.2.3'`. **The catalog is private and access-segregated, not a public package registry** — people see only the foundations licensed to sites they own or edit. The *site* carries the license, and it rides along with site ownership when a developer hands a site to a client. Don't describe publishing as making a foundation publicly discoverable. Schemas can also be registered on their own from a schemas-only package (`@uniweb/schemas`, any `@org/schemas`, or a bare folder of `schemas/*.{yml,json,js}`) — that's how `@std` schemas are published. Auth via `uniweb login` (`uniweb login --backend <url> --token <bearer>` without a terminal) or `UNIWEB_TOKEN`; preview with `--dry-run`.
|
|
2613
2632
|
|
|
2614
2633
|
### Staying current
|
|
2615
2634
|
|
|
@@ -20,9 +20,10 @@
|
|
|
20
20
|
* above the registered one, written into the package's `package.json` first. ⛔ This
|
|
21
21
|
* reverses the version-bump release gate of 2026-06-23, whose reasoning — a deliberate,
|
|
22
22
|
* npm-style release — fits a foundation other sites use as a product; those sites pin
|
|
23
|
-
* a catalog ref and never reach this code.
|
|
24
|
-
*
|
|
25
|
-
*
|
|
23
|
+
* a catalog ref and never reach this code. Its author's demo site, which names it by
|
|
24
|
+
* path, does — and gets the same default, by ruling [Diego, 2026-09-24]. The release is
|
|
25
|
+
* always the next patch, so a change that is not backwards compatible needs a higher
|
|
26
|
+
* version set in `package.json` by hand.
|
|
26
27
|
*
|
|
27
28
|
* ⛔ **A NEWER registered version still stops it.** It was released from another copy
|
|
28
29
|
* of the project, whose code this one may not have, and releasing above it would make
|
package/src/backend/site-sync.js
CHANGED
|
@@ -1186,8 +1186,12 @@ 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.
|
|
1189
1191
|
const changed =
|
|
1190
|
-
(pkg.siteContent?.entityCount || 0) +
|
|
1192
|
+
(pkg.siteContent?.entityCount || 0) +
|
|
1193
|
+
(pkg.records?.entityCount || 0) +
|
|
1194
|
+
(pkg.waiting || []).filter((w) => w.held).length
|
|
1191
1195
|
return { changed, unchanged: pkg.skipped || 0, warnings: pkg.warnings || [] }
|
|
1192
1196
|
}
|
|
1193
1197
|
|
|
@@ -1846,3 +1850,78 @@ export async function pushSyncPackages({
|
|
|
1846
1850
|
}
|
|
1847
1851
|
return { exitCode: 0, boundSiteUuid, finalizedTotal, wrote }
|
|
1848
1852
|
}
|
|
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,7 +90,8 @@ import {
|
|
|
90
90
|
refuseUnsendableRecords,
|
|
91
91
|
clearRemoteSyncStateIfUnbound,
|
|
92
92
|
dropSiteBoundValues,
|
|
93
|
-
|
|
93
|
+
readItemUuids,
|
|
94
|
+
pushInPasses
|
|
94
95
|
} from '../backend/site-sync.js'
|
|
95
96
|
import { resolveWorkspace, describeWorkspace, SOURCE_LABEL } from '../backend/workspace.js'
|
|
96
97
|
import { uploadSiteMedia, describeAssetRefusal } from '../backend/site-media.js'
|
|
@@ -767,12 +768,6 @@ export async function publish(args = []) {
|
|
|
767
768
|
// (It stamped `info.data_bundle` until 2026-08-18; the ball is gone and
|
|
768
769
|
// collection data now lands at its serving tail, so nothing records it.)
|
|
769
770
|
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
771
|
// Per-item identity, recovered from the backend when this clone has never seen it.
|
|
777
772
|
// Without it the backend re-mints every page and section row (see readItemUuids).
|
|
778
773
|
const itemUuids = await ensureItemUuids({
|
|
@@ -948,9 +943,14 @@ export async function publish(args = []) {
|
|
|
948
943
|
say.dim('Services unchanged.')
|
|
949
944
|
}
|
|
950
945
|
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
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
|
+
// publish rides the same gated push as `uniweb push`: if an app author has
|
|
950
|
+
// edited since this clone last synced, the push is refused rather than
|
|
951
|
+
// overwriting them, and nothing goes live. `--force` drops the precondition.
|
|
952
|
+
const forced = args.includes('--force')
|
|
953
|
+
const emitOptions = ({ priorHashes, itemUuids }) => ({
|
|
954
954
|
backend: client.origin,
|
|
955
955
|
...(declaration.declare ? {} : { declareServices: false }),
|
|
956
956
|
// Placement identity for the folder — see writeFolderItemUuids.
|
|
@@ -959,14 +959,20 @@ export async function publish(args = []) {
|
|
|
959
959
|
resolveModel,
|
|
960
960
|
priorHashes,
|
|
961
961
|
itemUuids,
|
|
962
|
-
...(
|
|
963
|
-
? {
|
|
964
|
-
: {
|
|
962
|
+
...(forced
|
|
963
|
+
? {}
|
|
964
|
+
: {
|
|
965
|
+
baseVersions: readBaseVersions(siteDir, client.origin),
|
|
966
|
+
itemBaseVersions: readItemBaseVersions(siteDir, client.origin)
|
|
967
|
+
}),
|
|
965
968
|
...(Object.keys(injectInfo).length ? { injectInfo } : {}),
|
|
966
969
|
...(Object.keys(ext.pins).length ? { injectExtensions: ext.pins } : {}),
|
|
967
970
|
...(assetRewrite ? { assetRewrite } : {}),
|
|
968
971
|
...(assetIds ? { assetIds } : {})
|
|
969
|
-
|
|
972
|
+
})
|
|
973
|
+
let pkg
|
|
974
|
+
try {
|
|
975
|
+
pkg = await emitSyncPackages(siteDir, emitOptions({ priorHashes, itemUuids }))
|
|
970
976
|
} catch (err) {
|
|
971
977
|
say.err(`Could not build the sync package: ${err.message}`)
|
|
972
978
|
return { exitCode: 1 }
|
|
@@ -981,11 +987,19 @@ export async function publish(args = []) {
|
|
|
981
987
|
error: (m) => say.err(m),
|
|
982
988
|
dim: (s) => `${c.dim}${s}${c.reset}`
|
|
983
989
|
}
|
|
984
|
-
const pushResult = await
|
|
990
|
+
const pushResult = await pushInPasses({
|
|
985
991
|
client,
|
|
986
992
|
siteDir,
|
|
987
993
|
pkg,
|
|
988
|
-
report
|
|
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
1003
|
})
|
|
990
1004
|
if (pushResult.exitCode !== 0) return { exitCode: pushResult.exitCode }
|
|
991
1005
|
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
|
+
pushInPasses
|
|
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
|
+
// The emit's options, read afresh for each pass (`pushInPasses`): a pass banks
|
|
500
|
+
// hashes, identity and base versions that the next one must build on.
|
|
501
|
+
const emitOptions = ({ priorHashes, sendAll, itemUuids }) => ({
|
|
502
502
|
backend: client.origin,
|
|
503
503
|
// Placement identity for the folder — see writeFolderItemUuids.
|
|
504
504
|
folderItemUuids: readFolderItemUuids(siteDir, client.origin),
|
|
@@ -529,7 +529,10 @@ export async function push(args = [], deps = {}) {
|
|
|
529
529
|
}),
|
|
530
530
|
...(assetRewrite ? { assetRewrite } : {}),
|
|
531
531
|
...(assetIds ? { assetIds } : {})
|
|
532
|
-
|
|
532
|
+
})
|
|
533
|
+
let pkg
|
|
534
|
+
try {
|
|
535
|
+
pkg = await emitSyncPackages(siteDir, emitOptions({ priorHashes, sendAll, itemUuids }))
|
|
533
536
|
} catch (err) {
|
|
534
537
|
error(`Could not build the sync package: ${err.message}`)
|
|
535
538
|
return { exitCode: 2 }
|
|
@@ -595,8 +598,9 @@ export async function push(args = [], deps = {}) {
|
|
|
595
598
|
}
|
|
596
599
|
|
|
597
600
|
// Submit both lanes, back-fill the minted uuids, and persist the send-only-changed
|
|
598
|
-
// cache
|
|
599
|
-
|
|
601
|
+
// cache — again for any record whose reference names a record this push created.
|
|
602
|
+
// Shared with `uniweb publish` via ../backend/site-sync.js.
|
|
603
|
+
const result = await pushInPasses({
|
|
600
604
|
client,
|
|
601
605
|
siteDir,
|
|
602
606
|
pkg,
|
|
@@ -605,7 +609,16 @@ export async function push(args = [], deps = {}) {
|
|
|
605
609
|
note,
|
|
606
610
|
error,
|
|
607
611
|
dim: (s) => `${colors.dim}${s}${colors.reset}`
|
|
608
|
-
}
|
|
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
|
+
)
|
|
609
622
|
})
|
|
610
623
|
if (result.exitCode !== 0) return { exitCode: result.exitCode }
|
|
611
624
|
success(
|
package/src/commands/validate.js
CHANGED
|
@@ -12,9 +12,10 @@
|
|
|
12
12
|
* ⭐ A violation fails it (exit 1); `--lax` reports without failing. It warned by
|
|
13
13
|
* default until 2026-09-24, with `--strict` to fail — a gate that passes is one
|
|
14
14
|
* nobody reads, and `push` / `publish` now refuse the same findings. The live render
|
|
15
|
-
* path stays tolerant.
|
|
16
|
-
*
|
|
17
|
-
*
|
|
15
|
+
* path stays tolerant. Remote (`url:`) inputs are not in the project, so they're
|
|
16
|
+
* reported as deferred, never silently skipped. The value of an entity reference is
|
|
17
|
+
* not checked against the entity it names. ⛔ This said references were "reported as
|
|
18
|
+
* deferred" until 2026-09-24; no check ever reported them.
|
|
18
19
|
*/
|
|
19
20
|
|
|
20
21
|
import { existsSync, readFileSync } from 'node:fs'
|
|
@@ -171,7 +172,7 @@ function printSiteHuman(result) {
|
|
|
171
172
|
}
|
|
172
173
|
|
|
173
174
|
if (deferred.length > 0) {
|
|
174
|
-
log(` ${colors.dim}↪ deferred (not
|
|
175
|
+
log(` ${colors.dim}↪ deferred (not checked):${colors.reset}`)
|
|
175
176
|
for (const d of deferred) {
|
|
176
177
|
const extra = d.url
|
|
177
178
|
? ` ${colors.dim}(${d.url})${colors.reset}`
|
package/src/framework-index.json
CHANGED
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schemaVersion": 1,
|
|
3
|
-
"generatedAt": "2026-09-
|
|
3
|
+
"generatedAt": "2026-09-24T21:52:18.260Z",
|
|
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.62.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.5",
|
|
130
130
|
"path": "framework/unipress",
|
|
131
131
|
"deps": [
|
|
132
132
|
"@uniweb/build",
|
package/src/index.js
CHANGED
|
@@ -1697,9 +1697,9 @@ project against framework conventions.
|
|
|
1697
1697
|
|
|
1698
1698
|
Every record file is checked against the schema its folder names, whether or
|
|
1699
1699
|
not a section reads it. A violation fails the run; \`push\` and \`publish\` refuse
|
|
1700
|
-
the same findings. The live render path stays tolerant.
|
|
1701
|
-
inputs
|
|
1702
|
-
|
|
1700
|
+
the same findings. The live render path stays tolerant. Remote (\`url:\`)
|
|
1701
|
+
inputs are not in the project, so they're reported as deferred, never silently
|
|
1702
|
+
skipped. A reference field's value is not checked against the entity it names.
|
|
1703
1703
|
|
|
1704
1704
|
${colors.bright}Options:${colors.reset}
|
|
1705
1705
|
--lax Report violations without failing (exit 0)
|