uniweb 0.27.1 → 0.28.1
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 +2 -2
- package/src/backend/client.js +25 -37
- package/src/commands/publish.js +26 -73
- package/src/framework-index.json +7 -7
- package/src/index.js +0 -10
- package/src/commands/runtime.js +0 -249
- package/src/utils/runtime-upload.js +0 -201
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "uniweb",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.28.1",
|
|
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.12.
|
|
45
|
-
"@uniweb/
|
|
46
|
-
"@uniweb/core": "^0.11.
|
|
47
|
-
"@uniweb/
|
|
44
|
+
"@uniweb/runtime": "^0.12.9",
|
|
45
|
+
"@uniweb/kit": "^0.13.3",
|
|
46
|
+
"@uniweb/core": "^0.11.2",
|
|
47
|
+
"@uniweb/semantic-parser": "^1.3.0"
|
|
48
48
|
},
|
|
49
49
|
"peerDependencies": {
|
|
50
|
-
"@uniweb/content-reader": "^1.2.4",
|
|
51
50
|
"@uniweb/semantic-parser": "^1.3.0",
|
|
52
|
-
"@uniweb/
|
|
51
|
+
"@uniweb/content-reader": "^1.2.4",
|
|
52
|
+
"@uniweb/build": "^0.25.2"
|
|
53
53
|
},
|
|
54
54
|
"peerDependenciesMeta": {
|
|
55
55
|
"@uniweb/build": {
|
package/partials/agents.md
CHANGED
|
@@ -2218,7 +2218,7 @@ uniweb inspect <path> # Show parsed content for a section or page (-
|
|
|
2218
2218
|
uniweb <command> --help # Per-command flags — no side effects. Prefer this over guessing.
|
|
2219
2219
|
```
|
|
2220
2220
|
|
|
2221
|
-
**
|
|
2221
|
+
**Three verbs dispatch but are deliberately not listed above.** `invite`, `handoff` and `template` are **reserved names with no implementation** — the flows they named ran against a backend the CLI no longer talks to, and the names are held so a future rebuild doesn't need a breaking change. Running any of them is not useful; their absence from this list is the answer, not an omission to fix.
|
|
2222
2222
|
|
|
2223
2223
|
### Where a site can live
|
|
2224
2224
|
|
|
@@ -2231,7 +2231,7 @@ uniweb <command> --help # Per-command flags — no side effects. Prefe
|
|
|
2231
2231
|
| Any static host at all | `uniweb export` | full control; no Uniweb account needed |
|
|
2232
2232
|
| Uniweb Cloud | `uniweb publish` | teams with non-technical content authors, or client work |
|
|
2233
2233
|
|
|
2234
|
-
`uniweb deploy` never assumes a host: with nothing configured it opens a picker listing only destinations it can act on, and records the choice in `deploy.yml` so later runs go straight there. On Cloudflare Pages / Netlify / Vercel, `add ci` also adds per-PR previews that comment the URL. Adapters: `github-pages`, `cloudflare-pages`, `netlify`, `vercel`, plus `s3-cloudfront` for `deploy`.
|
|
2234
|
+
`uniweb deploy` never assumes a host: with nothing configured it opens a picker listing only destinations it can act on, and records the choice in `deploy.yml` so later runs go straight there. On Cloudflare Pages / Netlify / Vercel, `add ci` also adds per-PR previews that comment the URL. Adapters: `github-pages`, `cloudflare-pages`, `netlify`, `vercel`, plus `s3-cloudfront` for `deploy`. That record is `deploy.yml`, beside `site.yml` — **the CLI writes it, you don't create it**: the first successful deploy scaffolds the whole file, and later deploys rewrite only its `lastDeploy:` block. Edit `targets:` when you want to change where a site ships. Host credentials come from the environment, never from that committed file.
|
|
2235
2235
|
|
|
2236
2236
|
Foundations have their own free path too: `uniweb add ci --target foundation` publishes to permanent versioned URLs on GitHub Pages.
|
|
2237
2237
|
|
package/src/backend/client.js
CHANGED
|
@@ -40,7 +40,6 @@ import {
|
|
|
40
40
|
} from '../utils/registry-orgs.js'
|
|
41
41
|
import { uploadFoundationCode } from '../utils/code-upload.js'
|
|
42
42
|
import { uploadSiteAssets } from '../utils/asset-upload.js'
|
|
43
|
-
import { uploadRuntime } from '../utils/runtime-upload.js'
|
|
44
43
|
|
|
45
44
|
/**
|
|
46
45
|
* Resolve the backend origin via the resolution ladder (highest precedence
|
|
@@ -85,8 +84,7 @@ export function resolveBackendOrigin(flag, { siteBackend } = {}) {
|
|
|
85
84
|
/**
|
|
86
85
|
* The fallback capability doc when `GET /dev/config` is absent or unreachable
|
|
87
86
|
* (an older backend, or no backend at all). Keeps the client non-breaking: the
|
|
88
|
-
* bases mirror a self-serve dev backend
|
|
89
|
-
* runtime resolution requires an explicit pin.
|
|
87
|
+
* bases mirror a self-serve dev backend.
|
|
90
88
|
*/
|
|
91
89
|
export const DISCOVERY_DEFAULTS = {
|
|
92
90
|
// ⛔ No serve-root default, and no `assetBase`. Serve locations are read from
|
|
@@ -99,10 +97,18 @@ export const DISCOVERY_DEFAULTS = {
|
|
|
99
97
|
// production host, hardcoded, applied to EVERY deployment the CLI can be
|
|
100
98
|
// pointed at. It was only ever read to compose an asset URL, which the plan
|
|
101
99
|
// already returns as `serve_url`; both the reader and the composer are gone.
|
|
100
|
+
//
|
|
101
|
+
// No `runtime` entry, and there must not be one. A backend does not hold
|
|
102
|
+
// runtimes: a version is acquired from a CDN — the official mirror, the
|
|
103
|
+
// distribution channel, or a local server — so there is no installed set for
|
|
104
|
+
// it to report and nothing here to default. `runtime.installed` lived here
|
|
105
|
+
// until 2026-08-22, alongside a `uniweb runtime register` verb that pushed
|
|
106
|
+
// builds to a backend; both are gone. The runtime a site gets follows from
|
|
107
|
+
// its foundation's floor (`info.runtime`, stated at register), not from
|
|
108
|
+
// anything the CLI asks a backend about.
|
|
102
109
|
auth: { loginPath: '/dev/auth/login', required: true },
|
|
103
110
|
delivery: { deploy: true, publish: true, broker: 'self-serve' },
|
|
104
|
-
assets: { supported: false }
|
|
105
|
-
runtime: { installed: [] }
|
|
111
|
+
assets: { supported: false }
|
|
106
112
|
}
|
|
107
113
|
|
|
108
114
|
export class BackendClient {
|
|
@@ -224,8 +230,7 @@ export class BackendClient {
|
|
|
224
230
|
* lifetime; a missing route or any transport/parse error falls back to
|
|
225
231
|
* DISCOVERY_DEFAULTS (non-breaking — an older backend still works). Lets the
|
|
226
232
|
* CLI hardcode nothing about a backend but its origin and discover the rest:
|
|
227
|
-
* `auth`, `delivery` (deploy/publish? broker), `assets` (lane built yet?)
|
|
228
|
-
* `runtime.installed` (the default-runtime source replacing the old /runtime/latest).
|
|
233
|
+
* `auth`, `delivery` (deploy/publish? broker), `assets` (lane built yet?).
|
|
229
234
|
* @returns {Promise<object>}
|
|
230
235
|
*/
|
|
231
236
|
async discover() {
|
|
@@ -428,8 +433,8 @@ export class BackendClient {
|
|
|
428
433
|
|
|
429
434
|
/**
|
|
430
435
|
* POST /dev/deploy — dumb, file-built delivery. Body is the deploy payload (the
|
|
431
|
-
* runtime-init JSON `build-site-data.js` produces — foundation,
|
|
432
|
-
*
|
|
436
|
+
* runtime-init JSON `build-site-data.js` produces — foundation, theme,
|
|
437
|
+
* languages, locales, optional dataFiles/searchFiles) plus an optional
|
|
433
438
|
* `site_uuid`. First deploy of a never-synced site omits it → the backend mints
|
|
434
439
|
* a uuid and returns it for write-back to deploy.yml; later deploys resend it so
|
|
435
440
|
* the site's published URL stays stable. Returns the raw Response so the caller
|
|
@@ -451,27 +456,23 @@ export class BackendClient {
|
|
|
451
456
|
* POST /dev/site/publish/{uuid} — CMS-publish a synced site (make its CURRENT
|
|
452
457
|
* backend state live; it does NOT push local files). `{uuid}` is the site-content
|
|
453
458
|
* uuid (`site.yml::$uuid`); a never-synced site 404s (sync first, or use deploy).
|
|
454
|
-
*
|
|
455
|
-
*
|
|
456
|
-
*
|
|
459
|
+
* Languages, when present, go in the body; absent → no body. Returns the raw
|
|
460
|
+
* Response ({ deploy_uuid, url, published_folder_uuid, status } on 200).
|
|
461
|
+
*
|
|
462
|
+
* ⛔ Sends NOTHING about a runtime. A `?runtime=<version>` param rode here until
|
|
463
|
+
* 2026-08-22, carrying a `site.yml::runtime` pin — a vestigial prop [Diego]: no
|
|
464
|
+
* template ever set it, no public doc described it, and the backend may already
|
|
465
|
+
* have been ignoring it. A site is codeless and has no basis for naming a
|
|
466
|
+
* runtime; the binding party is the FOUNDATION, whose floor travels as
|
|
467
|
+
* `info.runtime` at register. Do not reintroduce the param.
|
|
457
468
|
* @param {string} uuid - the site-content uuid
|
|
458
|
-
* @param {object} opts
|
|
459
|
-
* @param {string} opts.runtimeVersion
|
|
469
|
+
* @param {object} [opts]
|
|
460
470
|
* @param {string[]} [opts.languages]
|
|
461
471
|
* @returns {Promise<Response>}
|
|
462
472
|
*/
|
|
463
|
-
async publishSite(uuid, {
|
|
464
|
-
// Runtime rides as a query param (?runtime=<version>) per the shipped /dev
|
|
465
|
-
// route (D3, "request-carried"), NOT the body. Languages, when present, go in
|
|
466
|
-
// the body; absent → no body (the route only requires the runtime).
|
|
467
|
-
// Omitted when the site pins no runtime — silence is NOT a request to change
|
|
468
|
-
// it, so the backend keeps the site on its current resolved version (its
|
|
469
|
-
// order: body → current → UNIWEBD_DEFAULT_RUNTIME → highest installed → 400).
|
|
470
|
-
// Sending a locally-computed guess instead would undo a propagation walk on
|
|
471
|
-
// the next publish.
|
|
473
|
+
async publishSite(uuid, { languages } = {}) {
|
|
472
474
|
return this.request(`/dev/site/publish/${encodeURIComponent(uuid)}`, {
|
|
473
475
|
method: 'POST',
|
|
474
|
-
...(runtimeVersion ? { query: { runtime: runtimeVersion } } : {}),
|
|
475
476
|
...(languages ? { body: JSON.stringify({ languages }) } : {})
|
|
476
477
|
})
|
|
477
478
|
}
|
|
@@ -548,19 +549,6 @@ export class BackendClient {
|
|
|
548
549
|
})
|
|
549
550
|
}
|
|
550
551
|
|
|
551
|
-
/**
|
|
552
|
-
* Upload a built `@uniweb/runtime` to the runtime registry (plan → PUT-per-file),
|
|
553
|
-
* served by the backend at a location it reports. @std-gated on the backend. Thin
|
|
554
|
-
* pass-through to utils/runtime-upload.js with this client's origin + token.
|
|
555
|
-
* @param {object} opts - { version, distDir, files?, onProgress? }
|
|
556
|
-
*/
|
|
557
|
-
async uploadRuntime(opts) {
|
|
558
|
-
return uploadRuntime({
|
|
559
|
-
apiBase: this.origin,
|
|
560
|
-
token: await this.token(),
|
|
561
|
-
...opts
|
|
562
|
-
})
|
|
563
|
-
}
|
|
564
552
|
}
|
|
565
553
|
|
|
566
554
|
/** `@scope/name` → /dev/registry/data-schemas/{scope}/{name}; a bare name → …/{name}. */
|
package/src/commands/publish.js
CHANGED
|
@@ -220,76 +220,33 @@ export async function publish(args = []) {
|
|
|
220
220
|
return { exitCode: 1 }
|
|
221
221
|
}
|
|
222
222
|
|
|
223
|
-
//
|
|
224
|
-
//
|
|
225
|
-
// a
|
|
226
|
-
const installed = Array.isArray(config?.runtime?.installed)
|
|
227
|
-
? config.runtime.installed
|
|
228
|
-
: []
|
|
229
|
-
if (
|
|
230
|
-
siteYml.runtime &&
|
|
231
|
-
installed.length &&
|
|
232
|
-
!installed.includes(siteYml.runtime)
|
|
233
|
-
) {
|
|
234
|
-
// ⚠️ Leads with REMOVING the pin, deliberately. A site ships no JS, so it
|
|
235
|
-
// has no basis for holding a runtime version, and `runtime:` is an
|
|
236
|
-
// operator-level override that is no longer part of the documented
|
|
237
|
-
// authoring surface. This message used to say "pin one of these in
|
|
238
|
-
// site.yml" — which pushed a reader deeper into a mechanism they should
|
|
239
|
-
// not be using, and named a key the docs no longer describe. Keep the
|
|
240
|
-
// installed list (it is the actionable part when a pin IS intended), but
|
|
241
|
-
// do not restore pin-first phrasing.
|
|
242
|
-
say.err(
|
|
243
|
-
`Runtime ${siteYml.runtime} (pinned in site.yml) is not installed on the backend.`
|
|
244
|
-
)
|
|
245
|
-
say.dim(
|
|
246
|
-
`Remove the \`runtime:\` pin and the backend chooses — a site ships no code, so it has no reason to hold one.`
|
|
247
|
-
)
|
|
248
|
-
say.dim(
|
|
249
|
-
`Installed: ${installed.join(', ') || '(none)'} — or pin one of those, or have ${siteYml.runtime} installed.`
|
|
250
|
-
)
|
|
251
|
-
if (!dryRun) return { exitCode: 1 }
|
|
252
|
-
}
|
|
253
|
-
// An explicit pin is sent; NOTHING is synthesized when site.yml is silent.
|
|
254
|
-
//
|
|
255
|
-
// This used to fall back to the highest version the backend reported
|
|
256
|
-
// installed. That is the producer guessing at a fact the control plane owns —
|
|
257
|
-
// and once propagation moves sites, actively wrong: a walk advances a site to
|
|
258
|
-
// X, and the next publish would restate a *different* version the producer
|
|
259
|
-
// computed locally, silently undoing it.
|
|
223
|
+
// ⛔ NOTHING about a runtime is sent from here. `site.yml::runtime` was a
|
|
224
|
+
// vestigial prop and is no longer read [Diego, 2026-08-22]; `?runtime=` is no
|
|
225
|
+
// longer a query param on publish. Do not reintroduce either.
|
|
260
226
|
//
|
|
261
|
-
// The
|
|
227
|
+
// The chain is SITE → FOUNDATION → RUNTIME. A link-mode site is CODELESS: it
|
|
262
228
|
// ships no JS, so it has nothing that binds to a runtime version and cannot
|
|
263
229
|
// break when the runtime moves. Asking it to name one is not a hard question,
|
|
264
230
|
// it is a malformed one. The party that binds is the FOUNDATION, whose build
|
|
265
231
|
// externalizes react / react-dom / jsx-runtime / @uniweb/core — which is why
|
|
266
|
-
// the compatibility floor rides on the foundation (`
|
|
267
|
-
// `register
|
|
268
|
-
//
|
|
269
|
-
//
|
|
270
|
-
//
|
|
271
|
-
// Silence is therefore not a request to change the runtime, and the backend
|
|
272
|
-
// resolves it: an explicit pin → the site's CURRENT resolved runtime →
|
|
273
|
-
// UNIWEBD_DEFAULT_RUNTIME → (self-serve) highest installed.
|
|
232
|
+
// the compatibility floor rides on the foundation (`dist/runtime-pin.json` →
|
|
233
|
+
// `info.runtime`, stated on every register) and never here. Whoever resolves a
|
|
234
|
+
// whole site picks a runtime satisfying max() of the floors its foundation and
|
|
235
|
+
// extensions declare, and fetches it from a CDN — the official mirror, the
|
|
236
|
+
// distribution channel, or a local server. A backend never holds one.
|
|
274
237
|
//
|
|
275
|
-
//
|
|
276
|
-
//
|
|
277
|
-
//
|
|
278
|
-
//
|
|
279
|
-
//
|
|
280
|
-
//
|
|
281
|
-
//
|
|
282
|
-
//
|
|
283
|
-
//
|
|
284
|
-
//
|
|
285
|
-
//
|
|
286
|
-
//
|
|
287
|
-
//
|
|
288
|
-
// ⚠️ Do NOT reintroduce a local fallback. Sending our own guess when the site
|
|
289
|
-
// did not ask is what makes an unpinned republish regress. (The pinned path is
|
|
290
|
-
// unaffected — an explicit pin is still validated fail-closed above, and the
|
|
291
|
-
// backend refuses a backward move unless forced.)
|
|
292
|
-
const runtimeVersion = siteYml.runtime || null
|
|
238
|
+
// History, because each removal undid a different bad idea:
|
|
239
|
+
// • a local fallback to the highest version the backend reported installed —
|
|
240
|
+
// the producer guessing at a fact the control plane owns, and actively
|
|
241
|
+
// wrong once propagation moves sites (a walk advances a site to X; the
|
|
242
|
+
// next publish would restate a locally-computed value, silently undoing
|
|
243
|
+
// it). Removed in `6d32d94`.
|
|
244
|
+
// • fail-closed validation of a pin against `/dev/config`'s
|
|
245
|
+
// `runtime.installed`, and the `uniweb runtime register` verb that
|
|
246
|
+
// populated that field. Removed 2026-08-22 with the concept.
|
|
247
|
+
// • the pin itself, and the `?runtime=` it rode on. Removed 2026-08-22 —
|
|
248
|
+
// nothing scaffolded ever set it, it is in no public doc, and the backend
|
|
249
|
+
// may already have been ignoring it.
|
|
293
250
|
|
|
294
251
|
// deploy.yml target (the Uniweb hosting memory). No --target on publish — it
|
|
295
252
|
// always targets Uniweb hosting; resolveTarget gives us the target name +
|
|
@@ -351,9 +308,6 @@ export async function publish(args = []) {
|
|
|
351
308
|
if (dryRun) {
|
|
352
309
|
say.info('Dry run — would bring the foundation along, sync, and go live:')
|
|
353
310
|
say.dim(`Backend : ${client.origin}`)
|
|
354
|
-
say.dim(
|
|
355
|
-
`Runtime : ${runtimeVersion || '(not pinned — the backend keeps this site on its current runtime)'}`
|
|
356
|
-
)
|
|
357
311
|
say.dim(
|
|
358
312
|
`site_uuid : ${siteYml.$uuid || '(none — the site is created before anything uploads)'}`
|
|
359
313
|
)
|
|
@@ -718,7 +672,6 @@ export async function publish(args = []) {
|
|
|
718
672
|
let pubRes
|
|
719
673
|
try {
|
|
720
674
|
pubRes = await client.publishSite(siteUuid, {
|
|
721
|
-
runtimeVersion,
|
|
722
675
|
...(languages ? { languages } : {})
|
|
723
676
|
})
|
|
724
677
|
} catch (err) {
|
|
@@ -781,11 +734,11 @@ export async function publish(args = []) {
|
|
|
781
734
|
...(recordedRef ? { ref: recordedRef } : {}),
|
|
782
735
|
released: fnd.released
|
|
783
736
|
},
|
|
784
|
-
//
|
|
785
|
-
//
|
|
786
|
-
//
|
|
787
|
-
//
|
|
788
|
-
|
|
737
|
+
// No `runtime` here. `deploy.yml` records what this publish DID, and a
|
|
738
|
+
// publish sends nothing about a runtime — the site's runtime follows from
|
|
739
|
+
// its foundation's floor and is resolved by whoever serves it. Recording a
|
|
740
|
+
// value would be a snapshot that silently goes stale, of a decision this
|
|
741
|
+
// command does not make.
|
|
789
742
|
locales: Array.isArray(result.locales) ? result.locales : languages
|
|
790
743
|
}
|
|
791
744
|
})
|
package/src/framework-index.json
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schemaVersion": 1,
|
|
3
|
-
"generatedAt": "2026-08-
|
|
3
|
+
"generatedAt": "2026-08-24T14:55:17.484Z",
|
|
4
4
|
"packages": {
|
|
5
5
|
"@uniweb/build": {
|
|
6
|
-
"version": "0.25.
|
|
6
|
+
"version": "0.25.2",
|
|
7
7
|
"path": "framework/build",
|
|
8
8
|
"deps": [
|
|
9
9
|
"@uniweb/content-reader",
|
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
"deps": []
|
|
30
30
|
},
|
|
31
31
|
"@uniweb/core": {
|
|
32
|
-
"version": "0.11.
|
|
32
|
+
"version": "0.11.2",
|
|
33
33
|
"path": "framework/core",
|
|
34
34
|
"deps": [
|
|
35
35
|
"@uniweb/semantic-parser",
|
|
@@ -37,7 +37,7 @@
|
|
|
37
37
|
]
|
|
38
38
|
},
|
|
39
39
|
"@uniweb/frame-bridge": {
|
|
40
|
-
"version": "0.
|
|
40
|
+
"version": "0.3.0",
|
|
41
41
|
"path": "framework/frame-bridge",
|
|
42
42
|
"deps": []
|
|
43
43
|
},
|
|
@@ -49,7 +49,7 @@
|
|
|
49
49
|
]
|
|
50
50
|
},
|
|
51
51
|
"@uniweb/kit": {
|
|
52
|
-
"version": "0.13.
|
|
52
|
+
"version": "0.13.3",
|
|
53
53
|
"path": "framework/kit",
|
|
54
54
|
"deps": [
|
|
55
55
|
"@uniweb/core",
|
|
@@ -76,7 +76,7 @@
|
|
|
76
76
|
]
|
|
77
77
|
},
|
|
78
78
|
"@uniweb/runtime": {
|
|
79
|
-
"version": "0.12.
|
|
79
|
+
"version": "0.12.9",
|
|
80
80
|
"path": "framework/runtime",
|
|
81
81
|
"deps": [
|
|
82
82
|
"@uniweb/core",
|
|
@@ -114,7 +114,7 @@
|
|
|
114
114
|
"deps": []
|
|
115
115
|
},
|
|
116
116
|
"@uniweb/unipress": {
|
|
117
|
-
"version": "0.8.
|
|
117
|
+
"version": "0.8.12",
|
|
118
118
|
"path": "framework/unipress",
|
|
119
119
|
"deps": [
|
|
120
120
|
"@uniweb/build",
|
package/src/index.js
CHANGED
|
@@ -843,15 +843,6 @@ async function main() {
|
|
|
843
843
|
return
|
|
844
844
|
}
|
|
845
845
|
|
|
846
|
-
// Handle runtime command — `runtime register` uploads a built @uniweb/runtime to
|
|
847
|
-
// the backend's runtime registry; @std-gated. Where it is served from is the
|
|
848
|
-
// backend's to report, not ours to know.
|
|
849
|
-
if (command === 'runtime') {
|
|
850
|
-
const { runtime } = await import('./commands/runtime.js')
|
|
851
|
-
const result = await runtime(args.slice(1))
|
|
852
|
-
process.exit(result?.exitCode ?? 0)
|
|
853
|
-
}
|
|
854
|
-
|
|
855
846
|
// Handle invite command
|
|
856
847
|
if (command === 'invite') {
|
|
857
848
|
await invite(args.slice(1))
|
|
@@ -1721,7 +1712,6 @@ ${colors.bright}Commands:${colors.reset}
|
|
|
1721
1712
|
export Export a self-contained site for third-party hosting
|
|
1722
1713
|
register Register a foundation + its data schemas with the backend registry
|
|
1723
1714
|
release Release a foundation version (synonym of register)
|
|
1724
|
-
runtime register Escape hatch: push a LOCAL/unreleased runtime build (@std only)
|
|
1725
1715
|
push Push a site's content to the backend
|
|
1726
1716
|
pull Pull a site's content from the backend
|
|
1727
1717
|
refresh Catch up with the git remote AND the backend (never pushes)
|
package/src/commands/runtime.js
DELETED
|
@@ -1,249 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* uniweb runtime register — upload a built `@uniweb/runtime` to the backend so it
|
|
3
|
-
* can serve the runtime version. The runtime is a SYSTEM artifact: registering it
|
|
4
|
-
* requires **@std membership** (a non-@std bearer 403s).
|
|
5
|
-
*
|
|
6
|
-
* ─────────────────────────────────────────────────────────────────────────────
|
|
7
|
-
* ⛔ THIS IS AN ESCAPE HATCH. IT IS NOT HOW A RUNTIME NORMALLY REACHES A BACKEND.
|
|
8
|
-
* ─────────────────────────────────────────────────────────────────────────────
|
|
9
|
-
*
|
|
10
|
-
* The normal path is **publish to npm**, which CI mirrors into the public
|
|
11
|
-
* distribution channel, from which a backend acquires the version itself — no
|
|
12
|
-
* login, no per-backend push, no credentials leaving the backend.
|
|
13
|
-
*
|
|
14
|
-
* This verb pushes ONE build to ONE backend, by hand. Its cost is not the
|
|
15
|
-
* upload, it is that the result is **invisible to every other backend and to
|
|
16
|
-
* the channel**: a version installed this way exists nowhere else, cannot be
|
|
17
|
-
* verified against a published digest, and will not be what a second
|
|
18
|
-
* environment resolves. Two backends can end up serving different bytes under
|
|
19
|
-
* one version number, which is the exact defect the channel's immutability
|
|
20
|
-
* exists to make impossible.
|
|
21
|
-
*
|
|
22
|
-
* It stays because three cases have no other path, and they are all LOCAL or
|
|
23
|
-
* pre-release:
|
|
24
|
-
*
|
|
25
|
-
* • **an unpublished build** — npm versions are immutable, so you cannot
|
|
26
|
-
* iterate on a published one. Testing a runtime before releasing it goes
|
|
27
|
-
* through here.
|
|
28
|
-
* • **local workspace development** — `@uniweb/runtime` is a pnpm symlink to
|
|
29
|
-
* the working tree; there is no published artifact for the code you are
|
|
30
|
-
* editing.
|
|
31
|
-
* • **a deployment that cannot pull** — air-gapped or policy-restricted, or
|
|
32
|
-
* any environment without the egress the channel needs.
|
|
33
|
-
*
|
|
34
|
-
* ⇒ **If the version you are pushing is on npm, you almost certainly want the
|
|
35
|
-
* channel instead.** The command warns at runtime for that reason.
|
|
36
|
-
*
|
|
37
|
-
* A foundation emits `dist/runtime-pin.json`. `uniweb register` now reads it and
|
|
38
|
-
* ships it as the foundation's `info.runtime` — but **NOTHING ENFORCES IT** in
|
|
39
|
-
* any lane. An earlier version of this comment claimed `uniweb register` of a
|
|
40
|
-
* foundation fails when its pinned version isn't registered. That was never
|
|
41
|
-
* true, and it propagated into internal notes AND into four public
|
|
42
|
-
* documentation surfaces before anyone checked it against `commands/register.js`
|
|
43
|
-
* three files away.
|
|
44
|
-
*
|
|
45
|
-
* The pin is a **compatibility floor**, not a selector: a site loads a primary
|
|
46
|
-
* foundation plus N extensions, each emitting its own pin, and a site has exactly
|
|
47
|
-
* one runtime — so pins are plural and the selector must be singular. The selector
|
|
48
|
-
* is `site.yml::runtime` (see commands/publish.js), which the backend stamps into
|
|
49
|
-
* the site's meta at publish. The pin's use is VALIDATION — is the selected
|
|
50
|
-
* runtime at or above max() of every loaded foundation's floor? — and it belongs
|
|
51
|
-
* wherever all of a site's foundations are held. The producer publishes one
|
|
52
|
-
* foundation at a time and cannot see the others, so it STATES its floor and
|
|
53
|
-
* something holding the whole set does the arithmetic.
|
|
54
|
-
*
|
|
55
|
-
* Contract AGREED with the backend (2026-06-14): `POST /dev/runtime`, @std-gated,
|
|
56
|
-
* manifest-last. Wire + the two-half artifact set (SPA + ssr-edge isolate, the
|
|
57
|
-
* orchestrator stays platform-owned): utils/runtime-upload.js.
|
|
58
|
-
*
|
|
59
|
-
* Usage (escape hatch — prefer publishing to npm; see above):
|
|
60
|
-
* uniweb runtime register From framework/runtime (or --path <dir>)
|
|
61
|
-
* uniweb runtime register --path <dir> The @uniweb/runtime package dir
|
|
62
|
-
* uniweb runtime register --version <v> Override dist/app/manifest.json's version
|
|
63
|
-
* uniweb runtime register --backend <url> Override the backend origin
|
|
64
|
-
* uniweb runtime register --token <bearer> Auth bearer (skips `uniweb login`)
|
|
65
|
-
* uniweb runtime register --dry-run Print the version + file plan; upload nothing
|
|
66
|
-
*/
|
|
67
|
-
|
|
68
|
-
import { readFileSync } from 'node:fs'
|
|
69
|
-
import { resolve, join } from 'node:path'
|
|
70
|
-
|
|
71
|
-
import { BackendClient } from '../backend/client.js'
|
|
72
|
-
import {
|
|
73
|
-
collectRuntimeFiles,
|
|
74
|
-
hasWorkerRuntime,
|
|
75
|
-
hasShims
|
|
76
|
-
} from '../utils/runtime-upload.js'
|
|
77
|
-
import { readFlagValue } from '../utils/args.js'
|
|
78
|
-
|
|
79
|
-
const c = {
|
|
80
|
-
reset: '\x1b[0m',
|
|
81
|
-
bold: '\x1b[1m',
|
|
82
|
-
dim: '\x1b[2m',
|
|
83
|
-
cyan: '\x1b[36m',
|
|
84
|
-
green: '\x1b[32m',
|
|
85
|
-
yellow: '\x1b[33m',
|
|
86
|
-
red: '\x1b[31m'
|
|
87
|
-
}
|
|
88
|
-
const say = {
|
|
89
|
-
ok: (m) => console.log(`${c.green}✓${c.reset} ${m}`),
|
|
90
|
-
info: (m) => console.log(`${c.cyan}→${c.reset} ${m}`),
|
|
91
|
-
warn: (m) => console.log(`${c.yellow}⚠${c.reset} ${m}`),
|
|
92
|
-
err: (m) => console.error(`${c.red}✗${c.reset} ${m}`),
|
|
93
|
-
dim: (m) => console.log(` ${c.dim}${m}${c.reset}`)
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
// The runtime package dir: --path, else the cwd when it IS @uniweb/runtime.
|
|
97
|
-
function resolveRuntimeDir(args) {
|
|
98
|
-
const pathFlag = readFlagValue(args, '--path')
|
|
99
|
-
if (pathFlag) return resolve(pathFlag)
|
|
100
|
-
try {
|
|
101
|
-
if (
|
|
102
|
-
JSON.parse(readFileSync(join(process.cwd(), 'package.json'), 'utf8'))
|
|
103
|
-
.name === '@uniweb/runtime'
|
|
104
|
-
) {
|
|
105
|
-
return process.cwd()
|
|
106
|
-
}
|
|
107
|
-
} catch {
|
|
108
|
-
// no readable package.json — fall through
|
|
109
|
-
}
|
|
110
|
-
return null
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
export async function runtime(args = []) {
|
|
114
|
-
const sub = args[0]
|
|
115
|
-
if (sub !== 'register') {
|
|
116
|
-
say.err(
|
|
117
|
-
sub
|
|
118
|
-
? `Unknown subcommand: runtime ${sub}`
|
|
119
|
-
: 'Usage: uniweb runtime register'
|
|
120
|
-
)
|
|
121
|
-
say.dim(
|
|
122
|
-
'uniweb runtime register — upload the built @uniweb/runtime to the backend (@std only).'
|
|
123
|
-
)
|
|
124
|
-
return { exitCode: sub ? 1 : 0 }
|
|
125
|
-
}
|
|
126
|
-
const rest = args.slice(1)
|
|
127
|
-
const dryRun = rest.includes('--dry-run')
|
|
128
|
-
|
|
129
|
-
// Say it at the moment of use, not only in the header. A verb that works
|
|
130
|
-
// fine is a verb people keep reaching for: this one succeeds quietly and
|
|
131
|
-
// leaves a version that exists on exactly one backend, unverifiable against
|
|
132
|
-
// any published digest.
|
|
133
|
-
say.warn('`runtime register` is an escape hatch, not the normal path.')
|
|
134
|
-
say.dim('A published runtime reaches a backend through the distribution channel — no push, no login.')
|
|
135
|
-
say.dim('Use this for a build npm does not have: an unreleased version, a local workspace tree,')
|
|
136
|
-
say.dim('or a deployment that cannot reach the channel. Pushing a version that IS on npm installs')
|
|
137
|
-
say.dim('bytes nothing else can verify, and a second environment will resolve something different.')
|
|
138
|
-
console.log('')
|
|
139
|
-
|
|
140
|
-
const runtimeDir = resolveRuntimeDir(rest)
|
|
141
|
-
if (!runtimeDir) {
|
|
142
|
-
say.err('Not an @uniweb/runtime package.')
|
|
143
|
-
say.dim('Run from framework/runtime, or pass --path <dir>.')
|
|
144
|
-
return { exitCode: 2 }
|
|
145
|
-
}
|
|
146
|
-
const distDir = join(runtimeDir, 'dist')
|
|
147
|
-
const files = collectRuntimeFiles(distDir)
|
|
148
|
-
if (!files.length) {
|
|
149
|
-
say.err('No built runtime found (dist/app/).')
|
|
150
|
-
say.dim('Build it first: `pnpm build` in framework/runtime.')
|
|
151
|
-
return { exitCode: 2 }
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
// Version from the SPA build's manifest (the backend keys the version on it);
|
|
155
|
-
// --version overrides, parity with the backend's runtime install --version.
|
|
156
|
-
let version = readFlagValue(rest, '--version')
|
|
157
|
-
if (!version) {
|
|
158
|
-
try {
|
|
159
|
-
version = JSON.parse(
|
|
160
|
-
readFileSync(join(distDir, 'app', 'manifest.json'), 'utf8')
|
|
161
|
-
).version
|
|
162
|
-
} catch (err) {
|
|
163
|
-
say.err(`Could not read dist/app/manifest.json: ${err.message}`)
|
|
164
|
-
return { exitCode: 2 }
|
|
165
|
-
}
|
|
166
|
-
if (!version) {
|
|
167
|
-
say.err(
|
|
168
|
-
'dist/app/manifest.json has no "version" field — rebuild the runtime.'
|
|
169
|
-
)
|
|
170
|
-
return { exitCode: 2 }
|
|
171
|
-
}
|
|
172
|
-
}
|
|
173
|
-
// The ssr-edge artifact is a SET: worker-runtime.js + its shims/*.js. Warn when
|
|
174
|
-
// the set is absent or incomplete (a worker without shims can't resolve react).
|
|
175
|
-
if (!hasWorkerRuntime(files)) {
|
|
176
|
-
say.warn(
|
|
177
|
-
"dist/worker-runtime.js is missing — the SSR isolate bundle won't be uploaded."
|
|
178
|
-
)
|
|
179
|
-
say.dim(
|
|
180
|
-
'Build it first: `pnpm build:worker` in @uniweb/runtime (after `pnpm build`).'
|
|
181
|
-
)
|
|
182
|
-
} else if (!hasShims(files)) {
|
|
183
|
-
say.warn(
|
|
184
|
-
'dist/worker-runtime.js is present but dist/shims/ is missing — the SSR isolate set is incomplete.'
|
|
185
|
-
)
|
|
186
|
-
say.dim(
|
|
187
|
-
'The isolate resolves react/jsx-runtime/@uniweb/core through those shims; re-run `pnpm build:worker`.'
|
|
188
|
-
)
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
if (dryRun) {
|
|
192
|
-
say.info(
|
|
193
|
-
`Would register ${c.bold}@uniweb/runtime@${version}${c.reset} (${files.length} files):`
|
|
194
|
-
)
|
|
195
|
-
for (const f of files)
|
|
196
|
-
say.dim(`${f.path} ${f.size} bytes ${f.content_type}`)
|
|
197
|
-
return { exitCode: 0 }
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
const client = new BackendClient({
|
|
201
|
-
originFlag:
|
|
202
|
-
readFlagValue(rest, '--backend') || readFlagValue(rest, '--registry'),
|
|
203
|
-
token: readFlagValue(rest, '--token') || undefined,
|
|
204
|
-
args: rest,
|
|
205
|
-
command: 'Registering the runtime'
|
|
206
|
-
})
|
|
207
|
-
|
|
208
|
-
say.info(
|
|
209
|
-
`Registering ${c.bold}@uniweb/runtime@${version}${c.reset} → ${c.dim}${client.origin}${c.reset} (${files.length} files)…`
|
|
210
|
-
)
|
|
211
|
-
let result
|
|
212
|
-
try {
|
|
213
|
-
result = await client.uploadRuntime({
|
|
214
|
-
version,
|
|
215
|
-
distDir,
|
|
216
|
-
files,
|
|
217
|
-
onProgress: (m) => say.dim(m)
|
|
218
|
-
})
|
|
219
|
-
} catch (err) {
|
|
220
|
-
if (err.status === 403) {
|
|
221
|
-
say.err(
|
|
222
|
-
'Not authorized — registering a runtime version requires @std membership.'
|
|
223
|
-
)
|
|
224
|
-
return { exitCode: 1 }
|
|
225
|
-
}
|
|
226
|
-
say.err(`Runtime registration failed: ${err.message}`)
|
|
227
|
-
say.dim(
|
|
228
|
-
'Set the origin with --backend <url>; auth with `uniweb login` or --token <bearer>.'
|
|
229
|
-
)
|
|
230
|
-
return { exitCode: 1 }
|
|
231
|
-
}
|
|
232
|
-
if (result.failed.length) {
|
|
233
|
-
say.err(`${result.failed.length} file(s) failed to upload:`)
|
|
234
|
-
for (const f of result.failed)
|
|
235
|
-
say.dim(`${f.path} — HTTP ${f.status} ${f.detail}`)
|
|
236
|
-
say.dim(
|
|
237
|
-
'Re-run `uniweb runtime register` to resume — completed files dedupe.'
|
|
238
|
-
)
|
|
239
|
-
return { exitCode: 1 }
|
|
240
|
-
}
|
|
241
|
-
console.log('')
|
|
242
|
-
say.ok(
|
|
243
|
-
`Registered ${c.bold}@uniweb/runtime@${version}${c.reset} (${result.uploaded.length} files, ${result.mode} mode)`
|
|
244
|
-
)
|
|
245
|
-
if (result.serveBase) say.dim(`served at ${result.serveBase}`)
|
|
246
|
-
return { exitCode: 0 }
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
export default runtime
|
|
@@ -1,201 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Runtime registration — the framework half of `uniweb runtime register`.
|
|
3
|
-
*
|
|
4
|
-
* Uploads a built `@uniweb/runtime` to the backend so it can serve the runtime
|
|
5
|
-
* version. Mirrors the foundation code lane (utils/code-upload.js): plan →
|
|
6
|
-
* PUT-per-file, one mode-aware auth rule.
|
|
7
|
-
*
|
|
8
|
-
* The runtime is a SYSTEM artifact — registering it requires **@std membership**
|
|
9
|
-
* (a non-@std bearer 403s). Versioned by version alone (no scope/name).
|
|
10
|
-
*
|
|
11
|
-
* A runtime version is ONE unit with two halves, BOTH uploaded here (the backend
|
|
12
|
-
* stages the bytes; where/how it serves them is its decision — we don't assume a
|
|
13
|
-
* serve path, we read `serve_base` back from the plan):
|
|
14
|
-
* - dist/app/** the browser SPA (_importmap/, assets/, index.html,
|
|
15
|
-
* manifest.json) — boots + client-renders a site.
|
|
16
|
-
* - dist/worker-runtime.js + dist/shims/*.js the ssr-edge isolate set (4
|
|
17
|
-
* files): the inlined SSR bundle + its 3 globalThis-bridge
|
|
18
|
-
* shims (react, react/jsx-runtime, @uniweb/core). The
|
|
19
|
-
* isolate can't resolve react without the shims.
|
|
20
|
-
*
|
|
21
|
-
* NOT uploaded: the SSR *orchestrator* (the isolate's `entry.js` boot module). It's
|
|
22
|
-
* a serverless-isolate fetch handler encoding the platform's isolate dispatch
|
|
23
|
-
* protocol — owned by the platform's SSR layer, not a framework artifact. The
|
|
24
|
-
* framework ships the render API the orchestrator imports (worker-runtime.js
|
|
25
|
-
* exports initPrerenderForLocale / renderPage / injectPageContent / hydrateDataStore).
|
|
26
|
-
*
|
|
27
|
-
* Contract — AGREED with the backend (2026-06-14):
|
|
28
|
-
* PLAN POST {apiBase}/dev/runtime
|
|
29
|
-
* { version, files: [{ path, content_type, size, sha256 }] }
|
|
30
|
-
* → { mode, expires_in, serve_base, uploads: [{ path, method, url, headers }] }
|
|
31
|
-
* bearer; @std required (else 403, RFC7807 problem+json, op "runtime-register").
|
|
32
|
-
* UPLOAD PUT each file (direct → bearer; presigned → none; x-uniweb-sha256).
|
|
33
|
-
* MANIFEST LAST — discovery keys a version's existence on manifest.json,
|
|
34
|
-
* so a partial upload never advertises a half-delivered version.
|
|
35
|
-
* SERVE the backend's call (read `serve_base` from the plan; we never construct it).
|
|
36
|
-
*/
|
|
37
|
-
|
|
38
|
-
import { createHash } from 'node:crypto'
|
|
39
|
-
import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs'
|
|
40
|
-
import { join } from 'node:path'
|
|
41
|
-
import { contentTypeFor } from './code-upload.js'
|
|
42
|
-
|
|
43
|
-
const WORKER_RUNTIME = 'worker-runtime.js'
|
|
44
|
-
const SHIMS_DIR = 'shims'
|
|
45
|
-
const MANIFEST = 'manifest.json'
|
|
46
|
-
|
|
47
|
-
function fileEntry(diskPath, path) {
|
|
48
|
-
const bytes = readFileSync(diskPath)
|
|
49
|
-
return {
|
|
50
|
-
path,
|
|
51
|
-
content_type: contentTypeFor(path),
|
|
52
|
-
size: bytes.length,
|
|
53
|
-
sha256: createHash('sha256').update(bytes).digest('hex'),
|
|
54
|
-
diskPath
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
/**
|
|
59
|
-
* Collect a built runtime's upload set from `distDir` (framework/runtime/dist):
|
|
60
|
-
* everything under `dist/app/**` at the root, plus `dist/worker-runtime.js` and
|
|
61
|
-
* the `dist/shims/*.js` it depends on. Sourcemaps (`*.map`) are excluded (dev-only,
|
|
62
|
-
* not CDN-served). Returns `[]` when `dist/app/` is missing (the runtime isn't
|
|
63
|
-
* built). The worker bundle + shims are collected when present (graceful when not).
|
|
64
|
-
*
|
|
65
|
-
* @param {string} distDir - framework/runtime/dist
|
|
66
|
-
* @returns {Array<{ path, content_type, size, sha256, diskPath }>}
|
|
67
|
-
*/
|
|
68
|
-
export function collectRuntimeFiles(distDir) {
|
|
69
|
-
const appDir = join(distDir, 'app')
|
|
70
|
-
if (!existsSync(appDir)) return []
|
|
71
|
-
const files = []
|
|
72
|
-
const walk = (dir, prefix) => {
|
|
73
|
-
for (const name of readdirSync(dir).sort()) {
|
|
74
|
-
const full = join(dir, name)
|
|
75
|
-
const rel = prefix ? `${prefix}/${name}` : name
|
|
76
|
-
const st = statSync(full)
|
|
77
|
-
if (st.isDirectory()) walk(full, rel)
|
|
78
|
-
else if (st.isFile() && !rel.endsWith('.map'))
|
|
79
|
-
files.push(fileEntry(full, rel))
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
walk(appDir, '')
|
|
83
|
-
const worker = join(distDir, WORKER_RUNTIME)
|
|
84
|
-
if (existsSync(worker)) files.push(fileEntry(worker, WORKER_RUNTIME))
|
|
85
|
-
// The SSR isolate's globalThis-bridge shims ride alongside worker-runtime.js,
|
|
86
|
-
// served at shims/*.js. They're part of the ssr-edge artifact SET — the isolate
|
|
87
|
-
// can't resolve `react` without them — so collect the whole dir when present.
|
|
88
|
-
const shimsDir = join(distDir, SHIMS_DIR)
|
|
89
|
-
if (existsSync(shimsDir)) walk(shimsDir, SHIMS_DIR)
|
|
90
|
-
// MANIFEST LAST: the backend keys a version's existence on manifest.json (no
|
|
91
|
-
// server confirm step), so uploading it last means a partial delivery never
|
|
92
|
-
// advertises a half-built version. Reorder regardless of walk order.
|
|
93
|
-
const manifest = files.filter((f) => f.path === MANIFEST)
|
|
94
|
-
if (!manifest.length) return files
|
|
95
|
-
return [...files.filter((f) => f.path !== MANIFEST), ...manifest]
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
/** True when the worker SSR bundle is in the collected set. */
|
|
99
|
-
export function hasWorkerRuntime(files) {
|
|
100
|
-
return files.some((f) => f.path === WORKER_RUNTIME)
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
/** True when any SSR-isolate shim (shims/*.js) is in the collected set. */
|
|
104
|
-
export function hasShims(files) {
|
|
105
|
-
return files.some((f) => f.path.startsWith(`${SHIMS_DIR}/`))
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
/**
|
|
109
|
-
* Plan + upload a built runtime. Throws on a plan-level failure (the caller maps
|
|
110
|
-
* 403 → "@std only"); per-file PUT failures surface in `failed`.
|
|
111
|
-
*
|
|
112
|
-
* @param {object} opts - { apiBase, token, version, distDir, files?, onProgress? }
|
|
113
|
-
* @returns {Promise<{ mode, uploaded: string[], failed: Array, serveBase: string|null }>}
|
|
114
|
-
*/
|
|
115
|
-
export async function uploadRuntime({
|
|
116
|
-
apiBase,
|
|
117
|
-
token,
|
|
118
|
-
version,
|
|
119
|
-
distDir,
|
|
120
|
-
files,
|
|
121
|
-
onProgress = () => {}
|
|
122
|
-
}) {
|
|
123
|
-
const list = files || collectRuntimeFiles(distDir)
|
|
124
|
-
if (!list.length)
|
|
125
|
-
return { mode: 'none', uploaded: [], failed: [], serveBase: null }
|
|
126
|
-
|
|
127
|
-
const origin = apiBase.replace(/\/$/, '')
|
|
128
|
-
const planRes = await fetch(`${origin}/dev/runtime`, {
|
|
129
|
-
method: 'POST',
|
|
130
|
-
headers: {
|
|
131
|
-
'Content-Type': 'application/json',
|
|
132
|
-
Authorization: `Bearer ${token}`
|
|
133
|
-
},
|
|
134
|
-
body: JSON.stringify({
|
|
135
|
-
version,
|
|
136
|
-
files: list.map(({ path, content_type, size, sha256 }) => ({
|
|
137
|
-
path,
|
|
138
|
-
content_type,
|
|
139
|
-
size,
|
|
140
|
-
sha256
|
|
141
|
-
}))
|
|
142
|
-
})
|
|
143
|
-
})
|
|
144
|
-
if (!planRes.ok) {
|
|
145
|
-
const detail = await planRes.text().catch(() => '')
|
|
146
|
-
const err = new Error(
|
|
147
|
-
`runtime plan rejected: HTTP ${planRes.status}${detail ? ` — ${detail.slice(0, 300)}` : ''}`
|
|
148
|
-
)
|
|
149
|
-
err.status = planRes.status
|
|
150
|
-
throw err
|
|
151
|
-
}
|
|
152
|
-
const plan = await planRes.json()
|
|
153
|
-
const targets = new Map((plan.uploads || []).map((u) => [u.path, u]))
|
|
154
|
-
// The one mode-aware bit: direct PUTs are bearer-authed backend routes;
|
|
155
|
-
// presigned URLs are self-authorizing and must NOT carry a foreign bearer.
|
|
156
|
-
const authHeaders =
|
|
157
|
-
plan.mode === 'presigned' ? {} : { Authorization: `Bearer ${token}` }
|
|
158
|
-
|
|
159
|
-
const uploaded = []
|
|
160
|
-
const failed = []
|
|
161
|
-
for (const f of list) {
|
|
162
|
-
const target = targets.get(f.path)
|
|
163
|
-
if (!target) {
|
|
164
|
-
failed.push({
|
|
165
|
-
path: f.path,
|
|
166
|
-
status: 0,
|
|
167
|
-
detail: 'no upload target in plan'
|
|
168
|
-
})
|
|
169
|
-
continue
|
|
170
|
-
}
|
|
171
|
-
onProgress(`↑ ${f.path}`)
|
|
172
|
-
let res
|
|
173
|
-
try {
|
|
174
|
-
res = await fetch(new URL(target.url, origin), {
|
|
175
|
-
method: target.method || 'PUT',
|
|
176
|
-
headers: {
|
|
177
|
-
...(target.headers || {}),
|
|
178
|
-
...authHeaders,
|
|
179
|
-
'x-uniweb-sha256': f.sha256
|
|
180
|
-
},
|
|
181
|
-
body: readFileSync(f.diskPath)
|
|
182
|
-
})
|
|
183
|
-
} catch (err) {
|
|
184
|
-
failed.push({ path: f.path, status: 0, detail: err.message })
|
|
185
|
-
continue
|
|
186
|
-
}
|
|
187
|
-
if (res.ok) uploaded.push(f.path)
|
|
188
|
-
else
|
|
189
|
-
failed.push({
|
|
190
|
-
path: f.path,
|
|
191
|
-
status: res.status,
|
|
192
|
-
detail: (await res.text().catch(() => '')).slice(0, 200)
|
|
193
|
-
})
|
|
194
|
-
}
|
|
195
|
-
return {
|
|
196
|
-
mode: plan.mode || 'direct',
|
|
197
|
-
uploaded,
|
|
198
|
-
failed,
|
|
199
|
-
serveBase: plan.serve_base || null
|
|
200
|
-
}
|
|
201
|
-
}
|