uniweb 0.16.3 → 0.16.5
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 +7 -7
- package/partials/agents.md +12 -4
- package/src/backend/site-sync.js +71 -3
- package/src/commands/clone.js +14 -4
- package/src/commands/publish.js +27 -4
- package/src/commands/push.js +14 -3
- package/src/framework-index.json +8 -8
- package/src/index.js +5 -0
- package/src/utils/uwx-read.js +157 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "uniweb",
|
|
3
|
-
"version": "0.16.
|
|
3
|
+
"version": "0.16.5",
|
|
4
4
|
"description": "Create structured Vite + React sites with content/code separation",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -41,14 +41,14 @@
|
|
|
41
41
|
"js-yaml": "^4.1.0",
|
|
42
42
|
"prompts": "^2.4.2",
|
|
43
43
|
"tar": "^7.0.0",
|
|
44
|
-
"@uniweb/core": "0.8.
|
|
45
|
-
"@uniweb/
|
|
46
|
-
"@uniweb/
|
|
44
|
+
"@uniweb/core": "^0.8.5",
|
|
45
|
+
"@uniweb/kit": "^0.11.3",
|
|
46
|
+
"@uniweb/runtime": "^0.11.5"
|
|
47
47
|
},
|
|
48
48
|
"peerDependencies": {
|
|
49
|
-
"@uniweb/build": "0.18.
|
|
50
|
-
"@uniweb/
|
|
51
|
-
"@uniweb/
|
|
49
|
+
"@uniweb/build": "^0.18.5",
|
|
50
|
+
"@uniweb/content-reader": "^1.2.2",
|
|
51
|
+
"@uniweb/semantic-parser": "^1.2.2"
|
|
52
52
|
},
|
|
53
53
|
"peerDependenciesMeta": {
|
|
54
54
|
"@uniweb/build": {
|
package/partials/agents.md
CHANGED
|
@@ -1986,13 +1986,21 @@ Platform-specific configuration that doesn't belong in npm-standard fields. All
|
|
|
1986
1986
|
| `namespace` | `uniweb register` | none | Legacy explicit org-namespace override; equivalent to a scoped `package.json::name`. Rarely needed. |
|
|
1987
1987
|
| `runtimePolicy` | `dist/runtime-pin.json` | unset | Declares how far past the recorded runtime version a host may move a site. |
|
|
1988
1988
|
|
|
1989
|
-
**Runtime
|
|
1989
|
+
**Runtime updates — handled for you.** Your foundation's code links against the runtime: it externalizes `react`, `react-dom`, `react-dom/server`, both JSX runtimes and `@uniweb/core`, and the runtime supplies all of them at load time. So a build is bound to *that* React and *that* core API.
|
|
1990
1990
|
|
|
1991
|
-
|
|
1991
|
+
You don't declare anything for this. At build time your foundation records the `@uniweb/runtime` version it actually linked against (read from `node_modules`, not chosen) into `dist/runtime-pin.json`, and `uniweb register` sends it with your foundation, so the version you built against travels with it. **Whether two runtime versions are compatible is our determination, not a setting** — you have no way to know whether we changed anything your code can reach, so we don't ask you to guess.
|
|
1992
1992
|
|
|
1993
|
-
|
|
1993
|
+
⚠️ **Nothing acts on the recorded version yet** — today it is stated and carried, not enforced. Don't design around it having an effect.
|
|
1994
1994
|
|
|
1995
|
-
**
|
|
1995
|
+
**The escape hatch,** for the one thing only you can know: if your foundation reaches into undocumented runtime internals, or you have audited it against exactly one runtime build and want no movement at all, set
|
|
1996
|
+
|
|
1997
|
+
```json
|
|
1998
|
+
{ "uniweb": { "runtimePolicy": "exact" } }
|
|
1999
|
+
```
|
|
2000
|
+
|
|
2001
|
+
That freezes sites using your foundation on the recorded version. Only reach for it if one of those two things is actually true — a frozen foundation stops receiving runtime fixes, including security ones. Sites cannot override your choice.
|
|
2002
|
+
|
|
2003
|
+
> Older releases documented `auto-patch` and `auto-minor` here as well. They asked you to pick a compatibility rule out of our version numbers, which was never something you were in a position to evaluate — that determination is ours. Both values are still accepted so existing foundations keep working; neither is worth setting.
|
|
1996
2004
|
|
|
1997
2005
|
`@uniweb/runtime` arrives **transitively** through `@uniweb/build`, so your foundation pins a runtime version without declaring one — that's intentional. **Don't add `@uniweb/runtime` to your foundation's dependencies**; to bump the pinned version, bump `@uniweb/build`. If `@uniweb/runtime` isn't resolvable at build time the pin is simply not emitted, and the build still succeeds — nothing in the toolchain requires it.
|
|
1998
2006
|
|
package/src/backend/site-sync.js
CHANGED
|
@@ -18,6 +18,7 @@ import { hasUncommittedContent } from '../utils/git.js'
|
|
|
18
18
|
import {
|
|
19
19
|
backfillEntityUuids,
|
|
20
20
|
writeSiteEntityUuid,
|
|
21
|
+
writeSiteOrg,
|
|
21
22
|
emitSyncPackages,
|
|
22
23
|
readZip,
|
|
23
24
|
diffSiteUnits,
|
|
@@ -390,6 +391,59 @@ export function writeItemUuids(siteDir, map) {
|
|
|
390
391
|
updateSyncCache(siteDir, { itemUuids: map })
|
|
391
392
|
}
|
|
392
393
|
|
|
394
|
+
/**
|
|
395
|
+
* The org this site was created under, as `@handle`, or null.
|
|
396
|
+
*
|
|
397
|
+
* Read back from `site.yml::$org` (stored bare — see `writeSiteOrg`) and re-dressed
|
|
398
|
+
* with the `@` the CLI and the wire both use. Callers pass it as `--as-org`'s default
|
|
399
|
+
* so an org named once, at create, does not have to be re-typed on every later push.
|
|
400
|
+
*
|
|
401
|
+
* Deliberately NOT a fallback for the flag: an explicit `--as-org` always wins and
|
|
402
|
+
* rides verbatim, so this can only add a value where the CLI previously sent none.
|
|
403
|
+
*
|
|
404
|
+
* @param {string} siteDir
|
|
405
|
+
* @returns {string|null}
|
|
406
|
+
*/
|
|
407
|
+
export function readSiteOrg(siteDir) {
|
|
408
|
+
try {
|
|
409
|
+
const y = yaml.load(readFileSync(join(siteDir, 'site.yml'), 'utf8'))
|
|
410
|
+
const h = y && typeof y === 'object' ? y.$org : null
|
|
411
|
+
return typeof h === 'string' && h.trim()
|
|
412
|
+
? `@${h.trim().replace(/^@/, '')}`
|
|
413
|
+
: null
|
|
414
|
+
} catch {
|
|
415
|
+
return null
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
/**
|
|
420
|
+
* Record the org a just-minted site was created under, if one was named.
|
|
421
|
+
*
|
|
422
|
+
* Only what we were TOLD is recorded — when no `--as-org` was passed the backend
|
|
423
|
+
* chose the owner and its create response carries no org, so there is nothing to
|
|
424
|
+
* write and guessing one would be worse than the gap. Returns the display form for
|
|
425
|
+
* the caller's "here's what resolved" line, or null when nothing was recorded.
|
|
426
|
+
*
|
|
427
|
+
* @param {string} siteDir
|
|
428
|
+
* @param {string|null|undefined} asOrg - the `--as-org` value, `@handle` or bare
|
|
429
|
+
* @returns {string|null}
|
|
430
|
+
*/
|
|
431
|
+
function recordSiteOrg(siteDir, asOrg) {
|
|
432
|
+
const handle = String(asOrg || '')
|
|
433
|
+
.replace(/^@/, '')
|
|
434
|
+
.replace(/\/.*$/, '')
|
|
435
|
+
.trim()
|
|
436
|
+
if (!handle) return null
|
|
437
|
+
try {
|
|
438
|
+
writeSiteOrg(siteDir, handle)
|
|
439
|
+
return `@${handle}`
|
|
440
|
+
} catch {
|
|
441
|
+
// The uuid is the load-bearing back-fill; losing the org note must never
|
|
442
|
+
// fail a push that already succeeded on the backend.
|
|
443
|
+
return null
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
|
|
393
447
|
/**
|
|
394
448
|
* Guarantee the site EXISTS on the backend before anything is uploaded against it.
|
|
395
449
|
*
|
|
@@ -429,7 +483,7 @@ export async function ensureSiteExists({
|
|
|
429
483
|
/* unreadable site.yml — treat as un-synced and let the create decide */
|
|
430
484
|
}
|
|
431
485
|
if (typeof siteYml.$uuid === 'string') {
|
|
432
|
-
return { uuid: siteYml.$uuid, created: false }
|
|
486
|
+
return { uuid: siteYml.$uuid, created: false, org: readSiteOrg(siteDir) }
|
|
433
487
|
}
|
|
434
488
|
|
|
435
489
|
// Both are required by the create. Catching it here turns a 400 into a sentence
|
|
@@ -484,8 +538,16 @@ export async function ensureSiteExists({
|
|
|
484
538
|
// this point cannot leave a cache pointing at a different site with no way to
|
|
485
539
|
// detect it.
|
|
486
540
|
updateSyncCache(siteDir, { siteUuid: minted })
|
|
487
|
-
|
|
488
|
-
|
|
541
|
+
// Ownership is decided HERE and nowhere else — this is the one call that reads
|
|
542
|
+
// `as_org`. Recording it beside the uuid is what makes the answer readable later,
|
|
543
|
+
// and naming it now is the "show what resolved" half of the ask-once pattern.
|
|
544
|
+
const org = recordSiteOrg(siteDir, asOrg)
|
|
545
|
+
note?.(
|
|
546
|
+
org
|
|
547
|
+
? `Created the site on the backend under ${org} (recorded $uuid + $org in site.yml).`
|
|
548
|
+
: `Created the site on the backend (recorded $uuid in site.yml).`
|
|
549
|
+
)
|
|
550
|
+
return { uuid: minted, created: true, org }
|
|
489
551
|
}
|
|
490
552
|
|
|
491
553
|
/**
|
|
@@ -838,6 +900,12 @@ export async function pushSyncPackages({
|
|
|
838
900
|
updateSyncCache(siteDir, { siteUuid: minted })
|
|
839
901
|
boundSiteUuid = minted
|
|
840
902
|
wrote.push('recorded site $uuid in site.yml')
|
|
903
|
+
// The OTHER create path (a media-less push never reaches `ensureSiteExists`,
|
|
904
|
+
// which is gated on the site having local media). Both mint a site, so both
|
|
905
|
+
// owe the same record — recording it in only one place would make `$org`
|
|
906
|
+
// present or absent depending on whether the site happens to have images.
|
|
907
|
+
const createdOrg = recordSiteOrg(siteDir, asOrg)
|
|
908
|
+
if (createdOrg) wrote.push(`recorded site $org (${createdOrg}) in site.yml`)
|
|
841
909
|
const createdFinalized = extractFinalized(payload)
|
|
842
910
|
harvest(createdFinalized)
|
|
843
911
|
siteFinalizedDoc = createdFinalized?.[0]?.document || null
|
package/src/commands/clone.js
CHANGED
|
@@ -56,6 +56,7 @@ import { detectWorkspacePm, installCmd } from '../utils/pm.js'
|
|
|
56
56
|
import { BackendClient } from '../backend/client.js'
|
|
57
57
|
import { isNonInteractive, getCliPrefix } from '../utils/interactive.js'
|
|
58
58
|
import { extractFoundationRef } from '../utils/site-content-refs.js'
|
|
59
|
+
import { readUwxDocuments } from '../utils/uwx-read.js'
|
|
59
60
|
|
|
60
61
|
const colors = {
|
|
61
62
|
reset: '\x1b[0m',
|
|
@@ -189,11 +190,19 @@ export async function clone(args = [], deps = {}) {
|
|
|
189
190
|
command: 'Cloning'
|
|
190
191
|
})
|
|
191
192
|
|
|
192
|
-
// 1. GET the site-content document
|
|
193
|
+
// 1. GET the site-content document.
|
|
194
|
+
//
|
|
195
|
+
// ⛔ The body is a `.uwx` ZIP, not JSON — `application/vnd.uniweb.exchange.
|
|
196
|
+
// entity+zip`, on `content` and `folder` pulls alike. This used to call
|
|
197
|
+
// `res.json()`, which failed on the ZIP magic and reported *"Could not reach
|
|
198
|
+
// the backend: Unexpected token 'P'"* — blaming the network for a body we had
|
|
199
|
+
// received intact and mis-read. Decode with the local reader: this command runs
|
|
200
|
+
// before a project exists, so `@uniweb/build/uwx`'s `readZip` is out of reach
|
|
201
|
+
// (see `utils/uwx-read.js`).
|
|
193
202
|
info(
|
|
194
203
|
`Reading site ${colors.bright}${siteUuid}${colors.reset} from ${colors.dim}${client.origin}${colors.reset} …`
|
|
195
204
|
)
|
|
196
|
-
let
|
|
205
|
+
let documents
|
|
197
206
|
try {
|
|
198
207
|
const res = await client.pullSiteContent(siteUuid)
|
|
199
208
|
if (res.status === 404) {
|
|
@@ -206,13 +215,14 @@ export async function clone(args = [], deps = {}) {
|
|
|
206
215
|
note('Run `uniweb login` first (or pass --token <bearer>).')
|
|
207
216
|
return { exitCode: 1 }
|
|
208
217
|
}
|
|
209
|
-
|
|
218
|
+
documents = readUwxDocuments(Buffer.from(await res.arrayBuffer()))
|
|
210
219
|
} catch (err) {
|
|
211
220
|
error(`Could not reach the backend at ${client.origin}: ${err.message}`)
|
|
212
221
|
return { exitCode: 1 }
|
|
213
222
|
}
|
|
214
223
|
|
|
215
|
-
const document =
|
|
224
|
+
const document =
|
|
225
|
+
documents.map(extractDocument).find(Boolean) || null
|
|
216
226
|
if (!document) {
|
|
217
227
|
error('The site-content response carried no recognizable document.')
|
|
218
228
|
return { exitCode: 1 }
|
package/src/commands/publish.js
CHANGED
|
@@ -28,6 +28,12 @@
|
|
|
28
28
|
* uniweb publish --dry-run Resolve everything; POST nothing
|
|
29
29
|
* uniweb publish --yes Skip confirmations (CI); never block on a prompt
|
|
30
30
|
* uniweb publish --force Overwrite upstream app-side edits (drop the push gate)
|
|
31
|
+
* uniweb publish --as-org @org Publish under @org (membership-gated). Only the
|
|
32
|
+
* FIRST publish of a site reads it — that create is
|
|
33
|
+
* what decides which org owns the site and whose
|
|
34
|
+
* storage its assets are charged to. It is then
|
|
35
|
+
* recorded as `site.yml::$org` and replayed, so it
|
|
36
|
+
* never has to be re-typed.
|
|
31
37
|
* uniweb publish --no-save Skip the deploy.yml lastDeploy auto-save
|
|
32
38
|
* uniweb publish --backend <url> Override the backend origin
|
|
33
39
|
* uniweb publish --token <bearer> Auth bearer (skips `uniweb login`)
|
|
@@ -66,7 +72,8 @@ import {
|
|
|
66
72
|
ensureItemUuids,
|
|
67
73
|
ensureSiteExists,
|
|
68
74
|
clearRemoteSyncStateIfUnbound,
|
|
69
|
-
pushSyncPackages
|
|
75
|
+
pushSyncPackages,
|
|
76
|
+
readSiteOrg
|
|
70
77
|
} from '../backend/site-sync.js'
|
|
71
78
|
import { uploadDataBundle } from '../backend/data-bundle.js'
|
|
72
79
|
import { uploadSiteMedia, describeAssetRefusal } from '../backend/site-media.js'
|
|
@@ -167,11 +174,16 @@ async function persistLastDeploy(siteDir, opts) {
|
|
|
167
174
|
export async function publish(args = []) {
|
|
168
175
|
const dryRun = args.includes('--dry-run')
|
|
169
176
|
const noSave = args.includes('--no-save')
|
|
170
|
-
const asOrg = readFlagValue(args, '--as-org')
|
|
171
177
|
const foundationDir = readFlagValue(args, '--foundation') // optional local foundation for Model schemas
|
|
172
178
|
|
|
173
179
|
const siteDir = await resolveSiteDir(args, 'publish')
|
|
174
180
|
|
|
181
|
+
// The acting org: the flag verbatim, else the one this site was CREATED under
|
|
182
|
+
// (`site.yml::$org`). Only the create reads `as_org`, so replaying the recorded
|
|
183
|
+
// handle reasserts existing ownership rather than choosing new ownership. Absent
|
|
184
|
+
// both, no `as_org` is sent — unchanged from before the record existed.
|
|
185
|
+
const asOrg = readFlagValue(args, '--as-org') || readSiteOrg(siteDir)
|
|
186
|
+
|
|
175
187
|
// Advisory only — warns and ships. See utils/conformance.js for why this
|
|
176
188
|
// is not a gate.
|
|
177
189
|
await warnIfContentDoesNotConform(siteDir, { args })
|
|
@@ -210,11 +222,22 @@ export async function publish(args = []) {
|
|
|
210
222
|
installed.length &&
|
|
211
223
|
!installed.includes(siteYml.runtime)
|
|
212
224
|
) {
|
|
225
|
+
// ⚠️ Leads with REMOVING the pin, deliberately. A site ships no JS, so it
|
|
226
|
+
// has no basis for holding a runtime version, and `runtime:` is an
|
|
227
|
+
// operator-level override that is no longer part of the documented
|
|
228
|
+
// authoring surface. This message used to say "pin one of these in
|
|
229
|
+
// site.yml" — which pushed a reader deeper into a mechanism they should
|
|
230
|
+
// not be using, and named a key the docs no longer describe. Keep the
|
|
231
|
+
// installed list (it is the actionable part when a pin IS intended), but
|
|
232
|
+
// do not restore pin-first phrasing.
|
|
213
233
|
say.err(
|
|
214
|
-
`Runtime ${siteYml.runtime} (
|
|
234
|
+
`Runtime ${siteYml.runtime} (pinned in site.yml) is not installed on the backend.`
|
|
235
|
+
)
|
|
236
|
+
say.dim(
|
|
237
|
+
`Remove the \`runtime:\` pin and the backend chooses — a site ships no code, so it has no reason to hold one.`
|
|
215
238
|
)
|
|
216
239
|
say.dim(
|
|
217
|
-
`Installed: ${installed.join(', ') || '(none)'} — pin one of
|
|
240
|
+
`Installed: ${installed.join(', ') || '(none)'} — or pin one of those, or have ${siteYml.runtime} installed.`
|
|
218
241
|
)
|
|
219
242
|
if (!dryRun) return { exitCode: 1 }
|
|
220
243
|
}
|
package/src/commands/push.js
CHANGED
|
@@ -26,7 +26,11 @@
|
|
|
26
26
|
*
|
|
27
27
|
* Usage:
|
|
28
28
|
* uniweb push Build, push both lanes, back-fill $uuid
|
|
29
|
-
* uniweb push --as-org @org Act as @org (membership-gated)
|
|
29
|
+
* uniweb push --as-org @org Act as @org (membership-gated). Needed only
|
|
30
|
+
* for the FIRST push of a site — it decides
|
|
31
|
+
* which org owns it (and whose storage its
|
|
32
|
+
* assets are charged to). Recorded as
|
|
33
|
+
* `site.yml::$org` and replayed after that.
|
|
30
34
|
* uniweb push --dry-run Report what would be pushed; submit nothing
|
|
31
35
|
* uniweb push -o out.uwx Write the .uwx file(s) per lane; submit nothing
|
|
32
36
|
* uniweb push --registry <url> Override the backend origin
|
|
@@ -68,7 +72,8 @@ import {
|
|
|
68
72
|
ensureItemUuids,
|
|
69
73
|
ensureSiteExists,
|
|
70
74
|
clearRemoteSyncStateIfUnbound,
|
|
71
|
-
pushSyncPackages
|
|
75
|
+
pushSyncPackages,
|
|
76
|
+
readSiteOrg
|
|
72
77
|
} from '../backend/site-sync.js'
|
|
73
78
|
|
|
74
79
|
// Re-exported for downstream importers (pull.js, push.test.js) that read these
|
|
@@ -106,7 +111,6 @@ export async function push(args = []) {
|
|
|
106
111
|
const dryRun = args.includes('--dry-run')
|
|
107
112
|
const output = flagValue(args, '-o') || flagValue(args, '--output')
|
|
108
113
|
const tokenFlag = flagValue(args, '--token')
|
|
109
|
-
const asOrg = flagValue(args, '--as-org')
|
|
110
114
|
const foundationDir = flagValue(args, '--foundation')
|
|
111
115
|
const sendAll = args.includes('--all') // bypass the send-only-changed cache
|
|
112
116
|
// --force drops the optimistic-concurrency precondition, making the push
|
|
@@ -118,6 +122,13 @@ export async function push(args = []) {
|
|
|
118
122
|
|
|
119
123
|
const siteDir = await resolveSiteDir(args, 'push')
|
|
120
124
|
|
|
125
|
+
// The acting org: the flag verbatim, else the one this site was CREATED under
|
|
126
|
+
// (`site.yml::$org`). The org is consumed by the create that mints `$uuid`, so
|
|
127
|
+
// a site that already exists is already owned — replaying the recorded handle
|
|
128
|
+
// reasserts that rather than choosing anything new. A site with no `$org`
|
|
129
|
+
// recorded sends no `as_org`, exactly as before.
|
|
130
|
+
const asOrg = flagValue(args, '--as-org') || readSiteOrg(siteDir)
|
|
131
|
+
|
|
121
132
|
// Advisory only — warns and pushes. A malformed data block otherwise rides
|
|
122
133
|
// the sync wire unchecked; see utils/conformance.js.
|
|
123
134
|
await warnIfContentDoesNotConform(siteDir, { args })
|
package/src/framework-index.json
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schemaVersion": 1,
|
|
3
|
-
"generatedAt": "2026-08-
|
|
3
|
+
"generatedAt": "2026-08-12T22:01:32.128Z",
|
|
4
4
|
"packages": {
|
|
5
5
|
"@uniweb/build": {
|
|
6
|
-
"version": "0.18.
|
|
6
|
+
"version": "0.18.5",
|
|
7
7
|
"path": "framework/build",
|
|
8
8
|
"deps": [
|
|
9
9
|
"@uniweb/content-reader",
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
"deps": []
|
|
29
29
|
},
|
|
30
30
|
"@uniweb/core": {
|
|
31
|
-
"version": "0.8.
|
|
31
|
+
"version": "0.8.5",
|
|
32
32
|
"path": "framework/core",
|
|
33
33
|
"deps": [
|
|
34
34
|
"@uniweb/semantic-parser",
|
|
@@ -46,7 +46,7 @@
|
|
|
46
46
|
"deps": []
|
|
47
47
|
},
|
|
48
48
|
"@uniweb/kit": {
|
|
49
|
-
"version": "0.11.
|
|
49
|
+
"version": "0.11.3",
|
|
50
50
|
"path": "framework/kit",
|
|
51
51
|
"deps": [
|
|
52
52
|
"@uniweb/core",
|
|
@@ -65,7 +65,7 @@
|
|
|
65
65
|
"deps": []
|
|
66
66
|
},
|
|
67
67
|
"@uniweb/projections": {
|
|
68
|
-
"version": "0.2.
|
|
68
|
+
"version": "0.2.8",
|
|
69
69
|
"path": "framework/projections",
|
|
70
70
|
"deps": [
|
|
71
71
|
"@uniweb/content-writer",
|
|
@@ -73,7 +73,7 @@
|
|
|
73
73
|
]
|
|
74
74
|
},
|
|
75
75
|
"@uniweb/runtime": {
|
|
76
|
-
"version": "0.11.
|
|
76
|
+
"version": "0.11.5",
|
|
77
77
|
"path": "framework/runtime",
|
|
78
78
|
"deps": [
|
|
79
79
|
"@uniweb/core",
|
|
@@ -96,7 +96,7 @@
|
|
|
96
96
|
"deps": []
|
|
97
97
|
},
|
|
98
98
|
"@uniweb/semantic-parser": {
|
|
99
|
-
"version": "1.2.
|
|
99
|
+
"version": "1.2.2",
|
|
100
100
|
"path": "framework/semantic-parser",
|
|
101
101
|
"deps": []
|
|
102
102
|
},
|
|
@@ -111,7 +111,7 @@
|
|
|
111
111
|
"deps": []
|
|
112
112
|
},
|
|
113
113
|
"@uniweb/unipress": {
|
|
114
|
-
"version": "0.8.
|
|
114
|
+
"version": "0.8.4",
|
|
115
115
|
"path": "framework/unipress",
|
|
116
116
|
"deps": [
|
|
117
117
|
"@uniweb/build",
|
package/src/index.js
CHANGED
|
@@ -1318,6 +1318,10 @@ ${colors.bright}Options:${colors.reset}
|
|
|
1318
1318
|
--yes Skip confirmations (CI); never block on a prompt
|
|
1319
1319
|
--no-save Skip the deploy.yml lastDeploy auto-save
|
|
1320
1320
|
--no-validate Skip the content-conformance check (it only warns)
|
|
1321
|
+
--as-org @org Publish under @org (membership-gated). Read only on a site's
|
|
1322
|
+
FIRST publish — that create decides which org owns the site,
|
|
1323
|
+
and whose storage its assets are charged to. Recorded as
|
|
1324
|
+
site.yml \$org and replayed, so it is never re-typed.
|
|
1321
1325
|
--backend <url> Backend origin (default: \$UNIWEB_REGISTER_URL or built-in)
|
|
1322
1326
|
--token <bearer> Auth bearer (skips \`uniweb login\`)
|
|
1323
1327
|
`,
|
|
@@ -1693,6 +1697,7 @@ ${colors.bright}Global Options:${colors.reset}
|
|
|
1693
1697
|
${colors.bright}Publish Options:${colors.reset}
|
|
1694
1698
|
--dry-run Resolve everything; release/sync/POST nothing
|
|
1695
1699
|
--yes Skip confirmations (CI); never block on a prompt
|
|
1700
|
+
--as-org @org Publish under @org (first publish only; then remembered)
|
|
1696
1701
|
--no-save Skip the deploy.yml lastDeploy auto-save
|
|
1697
1702
|
--no-validate Skip the content-conformance check (it only warns)
|
|
1698
1703
|
--backend <url> Backend origin (default: \$UNIWEB_REGISTER_URL or built-in)
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Minimal `.uwx` reader — for the commands that run BEFORE a project exists.
|
|
3
|
+
*
|
|
4
|
+
* ## Why this exists rather than importing the real one
|
|
5
|
+
*
|
|
6
|
+
* Every other `.uwx` reader in the CLI (`pull.js`, `site-sync.js`, `content.js`)
|
|
7
|
+
* imports `readZip` from `@uniweb/build/uwx`. That is correct for them: they run
|
|
8
|
+
* *inside* a project, where `@uniweb/build` resolves from the project's own
|
|
9
|
+
* `node_modules`.
|
|
10
|
+
*
|
|
11
|
+
* **`@uniweb/build` is not a dependency of this package** (see `package.json`),
|
|
12
|
+
* so a command that runs where no project exists cannot reach it — statically or
|
|
13
|
+
* dynamically. `uniweb clone` is exactly that command: its whole job is to turn a
|
|
14
|
+
* backend site into a local project that does not exist yet.
|
|
15
|
+
*
|
|
16
|
+
* So this is a deliberate second implementation, kept minimal and dependency-free
|
|
17
|
+
* (`node:zlib` is a builtin), the same way `clone.js` keeps its own
|
|
18
|
+
* `extractDocument` for the same reason. **Do not import this from a command that
|
|
19
|
+
* runs inside a project** — use `@uniweb/build/uwx`'s `readZip` there, so the
|
|
20
|
+
* producer and the consumer of a `.uwx` stay one implementation wherever they can.
|
|
21
|
+
*
|
|
22
|
+
* ## Two things measured on a real payload, both of which would break a naive reader
|
|
23
|
+
*
|
|
24
|
+
* 1. ⛔ **The archive is NOT "stored".** `pull.js` describes the format as *"our
|
|
25
|
+
* Stored ZIP"*, and that is true of `manifest.json` and false of the entity
|
|
26
|
+
* files — measured on a live pull: `manifest.json` method 0 (STORED), the
|
|
27
|
+
* entity JSON method 8 (DEFLATED). A stored-only reader silently yields the
|
|
28
|
+
* manifest and drops the document, i.e. it returns success and no entities.
|
|
29
|
+
* **Both methods are handled below and the tests cover a mixed archive.**
|
|
30
|
+
* 2. The entity payload is the *only* thing wanted; `manifest.json` is skipped, as
|
|
31
|
+
* every other reader in the tree does.
|
|
32
|
+
*
|
|
33
|
+
* We read the **central directory** rather than walking local file headers,
|
|
34
|
+
* because a local header may carry zeroed sizes when the general-purpose bit 3
|
|
35
|
+
* flag defers them to a trailing data descriptor. The central directory always
|
|
36
|
+
* carries the real sizes.
|
|
37
|
+
*/
|
|
38
|
+
|
|
39
|
+
import { inflateRawSync } from 'node:zlib'
|
|
40
|
+
|
|
41
|
+
const SIG_EOCD = 0x06054b50
|
|
42
|
+
const SIG_CENTRAL = 0x02014b50
|
|
43
|
+
const SIG_LOCAL = 0x04034b50
|
|
44
|
+
const METHOD_STORED = 0
|
|
45
|
+
const METHOD_DEFLATED = 8
|
|
46
|
+
|
|
47
|
+
/** ZIP local-file-header magic, "PK\x03\x04" — the first two bytes are enough. */
|
|
48
|
+
export function looksLikeZip(buf) {
|
|
49
|
+
return Boolean(buf) && buf.length >= 2 && buf[0] === 0x50 && buf[1] === 0x4b
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Locate the end-of-central-directory record.
|
|
54
|
+
*
|
|
55
|
+
* Scanned backwards because the record is last and variable-length (it carries an
|
|
56
|
+
* optional trailing comment).
|
|
57
|
+
*
|
|
58
|
+
* @param {Buffer} buf
|
|
59
|
+
* @returns {number} offset of the EOCD, or -1
|
|
60
|
+
*/
|
|
61
|
+
function findEocd(buf) {
|
|
62
|
+
const min = Math.max(0, buf.length - 22 - 0xffff)
|
|
63
|
+
for (let i = buf.length - 22; i >= min; i--) {
|
|
64
|
+
if (buf.readUInt32LE(i) === SIG_EOCD) return i
|
|
65
|
+
}
|
|
66
|
+
return -1
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Read every entry out of a ZIP as `[name, Buffer]` pairs.
|
|
71
|
+
*
|
|
72
|
+
* @param {Buffer} buf
|
|
73
|
+
* @returns {Array<[string, Buffer]>} empty when the buffer is not a readable ZIP
|
|
74
|
+
*/
|
|
75
|
+
export function readUwxZip(buf) {
|
|
76
|
+
if (!looksLikeZip(buf)) return []
|
|
77
|
+
const eocd = findEocd(buf)
|
|
78
|
+
if (eocd < 0) return []
|
|
79
|
+
|
|
80
|
+
const count = buf.readUInt16LE(eocd + 10)
|
|
81
|
+
let ptr = buf.readUInt32LE(eocd + 16)
|
|
82
|
+
const out = []
|
|
83
|
+
|
|
84
|
+
for (let i = 0; i < count; i++) {
|
|
85
|
+
if (ptr + 46 > buf.length || buf.readUInt32LE(ptr) !== SIG_CENTRAL) break
|
|
86
|
+
|
|
87
|
+
const method = buf.readUInt16LE(ptr + 10)
|
|
88
|
+
const csize = buf.readUInt32LE(ptr + 20)
|
|
89
|
+
const nameLen = buf.readUInt16LE(ptr + 28)
|
|
90
|
+
const extraLen = buf.readUInt16LE(ptr + 30)
|
|
91
|
+
const commentLen = buf.readUInt16LE(ptr + 32)
|
|
92
|
+
const localOff = buf.readUInt32LE(ptr + 42)
|
|
93
|
+
const name = buf.subarray(ptr + 46, ptr + 46 + nameLen).toString('utf8')
|
|
94
|
+
|
|
95
|
+
// The local header's name/extra lengths are independent of the central
|
|
96
|
+
// directory's — read them where the data actually starts, not from above.
|
|
97
|
+
if (localOff + 30 <= buf.length && buf.readUInt32LE(localOff) === SIG_LOCAL) {
|
|
98
|
+
const lNameLen = buf.readUInt16LE(localOff + 26)
|
|
99
|
+
const lExtraLen = buf.readUInt16LE(localOff + 28)
|
|
100
|
+
const start = localOff + 30 + lNameLen + lExtraLen
|
|
101
|
+
const raw = buf.subarray(start, start + csize)
|
|
102
|
+
try {
|
|
103
|
+
if (method === METHOD_STORED) out.push([name, Buffer.from(raw)])
|
|
104
|
+
else if (method === METHOD_DEFLATED) out.push([name, inflateRawSync(raw)])
|
|
105
|
+
// any other method: skip rather than guess
|
|
106
|
+
} catch {
|
|
107
|
+
/* a corrupt entry must not lose the readable ones */
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
ptr += 46 + nameLen + extraLen + commentLen
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return out
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* The entity `$`-documents inside a `.uwx`, or a JSON body's documents.
|
|
119
|
+
*
|
|
120
|
+
* Mirrors `pull.js`'s `readPullDocuments` in behaviour — including its JSON
|
|
121
|
+
* fallback, so a future envelope change does not break this lane either — but
|
|
122
|
+
* without the `@uniweb/build` import. See the header for why that matters.
|
|
123
|
+
*
|
|
124
|
+
* @param {Buffer} buf
|
|
125
|
+
* @returns {object[]} parsed documents, possibly empty
|
|
126
|
+
*/
|
|
127
|
+
export function readUwxDocuments(buf) {
|
|
128
|
+
if (!buf || buf.length === 0) return []
|
|
129
|
+
|
|
130
|
+
if (looksLikeZip(buf)) {
|
|
131
|
+
const docs = []
|
|
132
|
+
for (const [name, data] of readUwxZip(buf)) {
|
|
133
|
+
if (name === 'manifest.json' || !name.endsWith('.json')) continue
|
|
134
|
+
try {
|
|
135
|
+
docs.push(JSON.parse(data.toString('utf8')))
|
|
136
|
+
} catch {
|
|
137
|
+
/* skip a non-document entry */
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
return docs
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
let payload
|
|
144
|
+
try {
|
|
145
|
+
payload = JSON.parse(buf.toString('utf8'))
|
|
146
|
+
} catch {
|
|
147
|
+
return []
|
|
148
|
+
}
|
|
149
|
+
if (Array.isArray(payload)) return payload.filter(Boolean)
|
|
150
|
+
const list = Array.isArray(payload?.entities)
|
|
151
|
+
? payload.entities
|
|
152
|
+
: Array.isArray(payload?.documents)
|
|
153
|
+
? payload.documents
|
|
154
|
+
: null
|
|
155
|
+
if (list) return list.filter(Boolean)
|
|
156
|
+
return payload ? [payload] : []
|
|
157
|
+
}
|