uniweb 0.48.5 → 0.48.6
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
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "uniweb",
|
|
3
|
-
"version": "0.48.
|
|
3
|
+
"version": "0.48.6",
|
|
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/runtime": "^0.19.5",
|
|
45
44
|
"@uniweb/core": "^0.24.4",
|
|
46
45
|
"@uniweb/kit": "^0.18.2",
|
|
47
|
-
"@uniweb/semantic-parser": "^1.4.0"
|
|
46
|
+
"@uniweb/semantic-parser": "^1.4.0",
|
|
47
|
+
"@uniweb/runtime": "^0.19.5"
|
|
48
48
|
},
|
|
49
49
|
"peerDependencies": {
|
|
50
|
-
"@uniweb/
|
|
50
|
+
"@uniweb/build": "^0.44.5",
|
|
51
51
|
"@uniweb/content-reader": "^1.2.4",
|
|
52
|
-
"@uniweb/
|
|
52
|
+
"@uniweb/semantic-parser": "^1.4.0"
|
|
53
53
|
},
|
|
54
54
|
"peerDependenciesMeta": {
|
|
55
55
|
"@uniweb/build": {
|
package/partials/agents.md
CHANGED
|
@@ -2447,6 +2447,7 @@ uniweb add ci --target foundation # Publish a foundation for free at permanent v
|
|
|
2447
2447
|
# (GitHub Pages → foundations/<name>/<version>/entry.js)
|
|
2448
2448
|
|
|
2449
2449
|
uniweb push / pull / clone / status # Git-style content sync with the Uniweb backend
|
|
2450
|
+
uniweb refresh / sync # Catch up (git + backend, never pushes) / catch up, then push
|
|
2450
2451
|
uniweb push --org @acme # First push/publish of a site: who owns it (see below)
|
|
2451
2452
|
uniweb register [--scope @org] # Register a foundation + its data schemas to the registry
|
|
2452
2453
|
uniweb login / logout # Start or clear the backend session the verbs above reuse
|
|
@@ -2516,7 +2517,7 @@ Foundations have their own free path too: `uniweb add ci --target foundation` pu
|
|
|
2516
2517
|
- **Git is the reviewed, durable record** — you `pull` content back, read it with `git diff`, and commit.
|
|
2517
2518
|
- **Authors never push or pull.** For them, content simply updates, whether the change came from another author or from a developer's CLI.
|
|
2518
2519
|
|
|
2519
|
-
**
|
|
2520
|
+
**Nobody's work is overwritten without asking.** `uniweb push` is refused if the site changed on the backend since your last pull — usually an author editing in the app — and reports what changed; nothing is written. Edits to different sections never collide. `uniweb pull` overwrites rather than merges, so it refuses while you have uncommitted changes. The recovery for both is `uniweb pull --merge` (or `uniweb refresh`, which runs `git pull` first): changes to different parts of a file combine silently, and a genuine overlap leaves conflict markers and a non-zero exit — so `uniweb pull --merge && uniweb push` never ships markers. `--force` is the deliberate overwrite (on `push` it replaces the backend's changes, on `pull` it discards yours); don't use it to get past a refusal. `uniweb sync` is `refresh` then `push`; neither publishes.
|
|
2520
2521
|
|
|
2521
2522
|
**The Cloud also provides a real backend for structured data:** a database for every registered data schema, and a CMS that edits both static page content and dynamic data entities typed by those schemas. That's the piece that makes it viable for teams and client work — the client manages records, not markdown files.
|
|
2522
2523
|
|
|
@@ -34,7 +34,11 @@
|
|
|
34
34
|
import { mkdirSync, existsSync, writeFileSync } from 'node:fs'
|
|
35
35
|
import { dirname, join } from 'node:path'
|
|
36
36
|
import { ASSET_SLOTS } from '@uniweb/semantic-parser'
|
|
37
|
-
import {
|
|
37
|
+
import {
|
|
38
|
+
readAssetMap,
|
|
39
|
+
updateAssetMap,
|
|
40
|
+
servedFingerprint
|
|
41
|
+
} from '@uniweb/build/uwx'
|
|
38
42
|
|
|
39
43
|
/**
|
|
40
44
|
* Collect every `{ id, ext, url }` an entity document references.
|
|
@@ -43,7 +47,7 @@ import { readAssetMap, updateAssetMap } from '@uniweb/build/uwx'
|
|
|
43
47
|
* carrying the reference — so a ProseMirror image node's attrs and a section
|
|
44
48
|
* background's media object are both found by one walk.
|
|
45
49
|
*/
|
|
46
|
-
export function collectAssetRefs(document) {
|
|
50
|
+
export function collectAssetRefs(document, map = {}) {
|
|
47
51
|
const found = new Map() // id → { id, ext, url }
|
|
48
52
|
const visit = (node) => {
|
|
49
53
|
if (Array.isArray(node)) return node.forEach(visit)
|
|
@@ -59,6 +63,28 @@ export function collectAssetRefs(document) {
|
|
|
59
63
|
for (const v of Object.values(node)) visit(v)
|
|
60
64
|
}
|
|
61
65
|
visit(document)
|
|
66
|
+
// A BARE-STRING reference (`info.preview`, `seo.image`, a section param) has no
|
|
67
|
+
// object to carry identity beside it. The map recognizes it by the fingerprint the
|
|
68
|
+
// push recorded for its serve URL — see `servedFingerprint` in @uniweb/build/uwx.
|
|
69
|
+
const byServed = new Map()
|
|
70
|
+
for (const v of Object.values(map || {})) {
|
|
71
|
+
if (v?.served && v.id && !byServed.has(v.served)) byServed.set(v.served, v)
|
|
72
|
+
}
|
|
73
|
+
if (byServed.size) {
|
|
74
|
+
const visitStrings = (node) => {
|
|
75
|
+
if (typeof node === 'string') {
|
|
76
|
+
if (!node.startsWith('/') && !/^https?:\/\//i.test(node)) return
|
|
77
|
+
const entry = byServed.get(servedFingerprint(node))
|
|
78
|
+
if (entry && !found.has(entry.id))
|
|
79
|
+
found.set(entry.id, { id: entry.id, ext: entry.ext || '', url: node })
|
|
80
|
+
return
|
|
81
|
+
}
|
|
82
|
+
if (Array.isArray(node)) return node.forEach(visitStrings)
|
|
83
|
+
if (node && typeof node === 'object')
|
|
84
|
+
for (const v of Object.values(node)) visitStrings(v)
|
|
85
|
+
}
|
|
86
|
+
visitStrings(document)
|
|
87
|
+
}
|
|
62
88
|
return [...found.values()]
|
|
63
89
|
}
|
|
64
90
|
|
|
@@ -89,11 +115,12 @@ export async function downloadMissingAssets({
|
|
|
89
115
|
warn = () => {}
|
|
90
116
|
}) {
|
|
91
117
|
const doFetch = fetchImpl || ((u) => globalThis.fetch(u))
|
|
92
|
-
|
|
118
|
+
// Read first: a bare-string reference is only recognizable through the map.
|
|
119
|
+
const map = readAssetMap(siteDir)
|
|
120
|
+
const refs = collectAssetRefs(document, map)
|
|
93
121
|
const out = { downloaded: [], present: [], failed: [], skipped: [] }
|
|
94
122
|
if (!refs.length) return out
|
|
95
123
|
|
|
96
|
-
const map = readAssetMap(siteDir)
|
|
97
124
|
const byId = new Map()
|
|
98
125
|
for (const [ref, v] of Object.entries(map)) if (v?.id) byId.set(v.id, ref)
|
|
99
126
|
|
|
@@ -16,6 +16,7 @@ import { createHash } from 'node:crypto'
|
|
|
16
16
|
import { existsSync, readFileSync } from 'node:fs'
|
|
17
17
|
import { basename } from 'node:path'
|
|
18
18
|
import { resolveAssetPath } from '@uniweb/build/site'
|
|
19
|
+
import { servedFingerprint } from '@uniweb/build/uwx'
|
|
19
20
|
import { contentTypeFor } from '../utils/code-upload.js'
|
|
20
21
|
import { humanBytes } from '../utils/bytes.js'
|
|
21
22
|
|
|
@@ -79,7 +80,17 @@ export async function uploadSiteMedia(
|
|
|
79
80
|
for (const ref of refs) {
|
|
80
81
|
const entry = result.assetsByLocalUrl[ref]
|
|
81
82
|
if (!entry) continue
|
|
82
|
-
|
|
83
|
+
// `served` — a fingerprint of the address, never the address. It is how a pull
|
|
84
|
+
// recognizes this asset where the reference is a bare string with no room for an
|
|
85
|
+
// id beside it (`info.preview`, `seo.image`, a section param); `assets.json`'s
|
|
86
|
+
// header says why a hash and not the URL.
|
|
87
|
+
if (entry.id) {
|
|
88
|
+
ids[ref] = {
|
|
89
|
+
id: entry.id,
|
|
90
|
+
ext: entry.ext || '',
|
|
91
|
+
...(entry.serveUrl ? { served: servedFingerprint(entry.serveUrl) } : {})
|
|
92
|
+
}
|
|
93
|
+
}
|
|
83
94
|
// The backend's canonical serve URL, READ — never composed. An entry without
|
|
84
95
|
// one is an asset we cannot address, and inventing a location for it is the
|
|
85
96
|
// exact failure this lane exists to avoid: a guessed host is SILENTLY wrong,
|
package/src/backend/site-sync.js
CHANGED
|
@@ -29,7 +29,8 @@ import {
|
|
|
29
29
|
collectUnitUuids,
|
|
30
30
|
collectFolderItemUuids,
|
|
31
31
|
collectQueryUuids,
|
|
32
|
-
readAssetMap
|
|
32
|
+
readAssetMap,
|
|
33
|
+
removeYamlScalar
|
|
33
34
|
} from '@uniweb/build/uwx'
|
|
34
35
|
|
|
35
36
|
// First entity `$`-document out of a `.uwx` we produced or the backend served.
|
|
@@ -323,6 +324,51 @@ export function clearRemoteSyncStateIfUnbound(siteDir) {
|
|
|
323
324
|
return stale
|
|
324
325
|
}
|
|
325
326
|
|
|
327
|
+
/**
|
|
328
|
+
* Drop the `site.yml` values that describe ONE PARTICULAR backend site, when this
|
|
329
|
+
* project is bound to none (no `$uuid`) — a brand-new site, or the state our own
|
|
330
|
+
* "clear `$uuid` to re-publish as a new site" recovery puts you in:
|
|
331
|
+
*
|
|
332
|
+
* · `$url` — where the PREVIOUS site was live. The new one is not live anywhere
|
|
333
|
+
* yet; its first publish records its own.
|
|
334
|
+
* · `preview` in the APP's form — a timestamp naming the previous site's generated
|
|
335
|
+
* card image, which is keyed by that site's uuid and would dangle on this one.
|
|
336
|
+
*
|
|
337
|
+
* ⛔ An AUTHOR's preview is theirs and stays: a URL, or a path to an image in the
|
|
338
|
+
* project, is as good for the new site as for the old.
|
|
339
|
+
*
|
|
340
|
+
* The site.yml sibling of `clearRemoteSyncStateIfUnbound`, called beside it for the
|
|
341
|
+
* same reason — before the create mints a uuid and makes the project look bound.
|
|
342
|
+
*
|
|
343
|
+
* @param {string} siteDir
|
|
344
|
+
* @returns {string[]} the keys removed
|
|
345
|
+
*/
|
|
346
|
+
export function dropSiteBoundValues(siteDir) {
|
|
347
|
+
const file = join(siteDir, 'site.yml')
|
|
348
|
+
let y
|
|
349
|
+
try {
|
|
350
|
+
y = yaml.load(readFileSync(file, 'utf8'))
|
|
351
|
+
} catch {
|
|
352
|
+
return []
|
|
353
|
+
}
|
|
354
|
+
if (!y || typeof y !== 'object' || typeof y.$uuid === 'string') return []
|
|
355
|
+
const dropped = []
|
|
356
|
+
if (y.$url !== undefined && removeYamlScalar(file, '$url')) dropped.push('$url')
|
|
357
|
+
if (
|
|
358
|
+
y.preview !== undefined &&
|
|
359
|
+
!isAuthoredPreview(y.preview) &&
|
|
360
|
+
removeYamlScalar(file, 'preview')
|
|
361
|
+
) {
|
|
362
|
+
dropped.push('preview')
|
|
363
|
+
}
|
|
364
|
+
return dropped
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
// An author's preview is an address — a URL, or a site-root / relative path. Anything
|
|
368
|
+
// else is the app's generated-image token.
|
|
369
|
+
const isAuthoredPreview = (v) =>
|
|
370
|
+
typeof v === 'string' && (/^https?:\/\//i.test(v) || /^\.{0,2}\//.test(v))
|
|
371
|
+
|
|
326
372
|
export function readSyncCache(siteDir) {
|
|
327
373
|
return readMap(siteDir, 'hashes')
|
|
328
374
|
}
|
package/src/commands/publish.js
CHANGED
|
@@ -87,6 +87,7 @@ import {
|
|
|
87
87
|
readFolderItemUuids,
|
|
88
88
|
ensureSiteExists,
|
|
89
89
|
clearRemoteSyncStateIfUnbound,
|
|
90
|
+
dropSiteBoundValues,
|
|
90
91
|
pushSyncPackages,
|
|
91
92
|
resolveSiteOrgForCreate
|
|
92
93
|
} from '../backend/site-sync.js'
|
|
@@ -578,6 +579,12 @@ export async function publish(args = []) {
|
|
|
578
579
|
`Cleared stale sync state from a previous site (${droppedState.join(', ')}).`
|
|
579
580
|
)
|
|
580
581
|
}
|
|
582
|
+
const droppedValues = dropSiteBoundValues(siteDir)
|
|
583
|
+
if (droppedValues.length) {
|
|
584
|
+
say.dim(
|
|
585
|
+
`Dropped the previous site's ${droppedValues.join(' and ')} from site.yml.`
|
|
586
|
+
)
|
|
587
|
+
}
|
|
581
588
|
|
|
582
589
|
// 3b. Make sure the SITE EXISTS before a single byte is uploaded.
|
|
583
590
|
//
|
|
@@ -1106,6 +1113,11 @@ export async function publish(args = []) {
|
|
|
1106
1113
|
say.dim(` site.yml lists ${unserved.join(', ')}, and ${unserved.length === 1 ? 'it was' : 'they were'} not published.`)
|
|
1107
1114
|
}
|
|
1108
1115
|
if (serveUrl) console.log(` ${c.cyan}${serveUrl}${c.reset}`)
|
|
1116
|
+
// ⛔ No site.yml write for where it went live. The backend records the reply's
|
|
1117
|
+
// `url` on `info.url` at every publish, and pull brings it into `site.yml::$url`
|
|
1118
|
+
// like any other key. A copy written here was a second writer that had to match
|
|
1119
|
+
// the backend's byte for byte (the address printed above is made absolute, so it
|
|
1120
|
+
// did not), and it left site.yml modified — which a plain `pull` refuses over.
|
|
1109
1121
|
if (result.deploy_uuid) say.dim(`deploy: ${result.deploy_uuid}`)
|
|
1110
1122
|
return { exitCode: 0 }
|
|
1111
1123
|
}
|
package/src/commands/push.js
CHANGED
|
@@ -89,6 +89,7 @@ import {
|
|
|
89
89
|
ensureItemUuids,
|
|
90
90
|
ensureSiteExists,
|
|
91
91
|
clearRemoteSyncStateIfUnbound,
|
|
92
|
+
dropSiteBoundValues,
|
|
92
93
|
pushSyncPackages,
|
|
93
94
|
resolveSiteOrgForCreate
|
|
94
95
|
} from '../backend/site-sync.js'
|
|
@@ -303,6 +304,10 @@ export async function push(args = [], deps = {}) {
|
|
|
303
304
|
`Cleared stale sync state from a previous site (${dropped.join(', ')}).`
|
|
304
305
|
)
|
|
305
306
|
}
|
|
307
|
+
const stale = dropSiteBoundValues(siteDir)
|
|
308
|
+
if (stale.length) {
|
|
309
|
+
note(`Dropped the previous site's ${stale.join(' and ')} from site.yml.`)
|
|
310
|
+
}
|
|
306
311
|
}
|
|
307
312
|
const priorHashes = readSyncCache(siteDir)
|
|
308
313
|
// Per-item identity, without which the backend reads every record as new and
|
package/src/framework-index.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schemaVersion": 1,
|
|
3
|
-
"generatedAt": "2026-09-
|
|
3
|
+
"generatedAt": "2026-09-10T23:52:15.147Z",
|
|
4
4
|
"packages": {
|
|
5
5
|
"@uniweb/api": {
|
|
6
6
|
"version": "0.3.1",
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
]
|
|
11
11
|
},
|
|
12
12
|
"@uniweb/build": {
|
|
13
|
-
"version": "0.44.
|
|
13
|
+
"version": "0.44.5",
|
|
14
14
|
"path": "framework/build",
|
|
15
15
|
"deps": [
|
|
16
16
|
"@uniweb/content-reader",
|
|
@@ -110,7 +110,7 @@
|
|
|
110
110
|
"deps": []
|
|
111
111
|
},
|
|
112
112
|
"@uniweb/templates": {
|
|
113
|
-
"version": "0.12.
|
|
113
|
+
"version": "0.12.5",
|
|
114
114
|
"path": "framework/templates",
|
|
115
115
|
"deps": []
|
|
116
116
|
},
|