uniweb 0.12.26 → 0.12.28
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 +3 -0
- package/src/backend/client.js +339 -0
- package/src/commands/clone.js +18 -32
- package/src/commands/deploy.js +218 -1783
- package/src/commands/handoff.js +9 -246
- package/src/commands/invite.js +10 -318
- package/src/commands/org.js +6 -8
- package/src/commands/publish.js +128 -1153
- package/src/commands/pull.js +22 -36
- package/src/commands/push.js +43 -101
- package/src/commands/register.js +184 -39
- package/src/commands/runtime.js +141 -0
- package/src/commands/template.js +13 -221
- package/src/framework-index.json +18 -7
- package/src/index.js +74 -100
- package/src/utils/asset-upload.js +162 -0
- package/src/utils/code-upload.js +245 -0
- package/src/utils/config.js +11 -44
- package/src/utils/registry-auth.js +35 -1
- package/src/utils/registry-orgs.js +141 -73
- package/src/utils/runtime-upload.js +163 -0
- package/src/commands/login.js +0 -230
- package/src/utils/auth.js +0 -212
- package/src/utils/registry.js +0 -466
package/src/commands/register.js
CHANGED
|
@@ -4,8 +4,14 @@
|
|
|
4
4
|
* registry as one names-only `.uwx` document (uwx-format.md §5).
|
|
5
5
|
*
|
|
6
6
|
* `uniweb login && uniweb register`. Distinct from `uniweb publish` (which
|
|
7
|
-
* targets the legacy
|
|
8
|
-
*
|
|
7
|
+
* targets the legacy platform) — `register` talks to the registry over HTTP at a
|
|
8
|
+
* configurable endpoint.
|
|
9
|
+
*
|
|
10
|
+
* If the foundation's `dist/` is missing or version-stale (the baked schema
|
|
11
|
+
* version differs from package.json), `register` builds it first — the same
|
|
12
|
+
* build-if-stale `uniweb publish` does, so `register` is a full drop-in.
|
|
13
|
+
* Preview paths (`--dry-run`, `-o`) never write to `dist/`; they require a
|
|
14
|
+
* pre-built foundation.
|
|
9
15
|
*
|
|
10
16
|
* Run from a foundation, or from a schemas-only package — a package that exports
|
|
11
17
|
* schemas (e.g. `@uniweb/schemas`, any `@org/schemas`) or a bare `schemas/*.yml`
|
|
@@ -13,10 +19,13 @@
|
|
|
13
19
|
* standalone (foundation-less); same flags, `--scope` names them.
|
|
14
20
|
*
|
|
15
21
|
* Usage:
|
|
16
|
-
* uniweb register Build the .uwx
|
|
22
|
+
* uniweb register Build the .uwx, submit it, then deliver
|
|
23
|
+
* the foundation's dist/ code (plan +
|
|
24
|
+
* upload — see utils/code-upload.js)
|
|
17
25
|
* uniweb register --scope @org Publish under @org (resolves @/x -> @org/x).
|
|
18
26
|
* Default: the package's package.json "uniweb.scope".
|
|
19
|
-
* uniweb register --
|
|
27
|
+
* uniweb register --schema-only Skip the code delivery (schemas land, no dist upload)
|
|
28
|
+
* uniweb register --dry-run Print the .uwx + the code file plan; submit nothing
|
|
20
29
|
* uniweb register -o foundation.uwx Write the .uwx to a file; submit nothing
|
|
21
30
|
* uniweb register --registry <url> Override the submit endpoint
|
|
22
31
|
* uniweb register --token <bearer> Submit with this bearer; skips `uniweb login`
|
|
@@ -25,20 +34,38 @@
|
|
|
25
34
|
* Auth (submit only): --token <bearer> > UNIWEB_TOKEN > `uniweb login` session.
|
|
26
35
|
*/
|
|
27
36
|
|
|
37
|
+
// DEFERRED foundation-registration capabilities (the legacy `uniweb publish` had
|
|
38
|
+
// these; the new backend doesn't yet — captured here so the design intent isn't
|
|
39
|
+
// lost, and the legacy code could be removed). Implement as `register` flags (or
|
|
40
|
+
// backend-side policy) when the need is real:
|
|
41
|
+
//
|
|
42
|
+
// • ACCESS POLICY — legacy `--edit-access open|restricted`. On the old platform
|
|
43
|
+
// this gated who could act on the foundation in the app. Its meaning is
|
|
44
|
+
// unclear for the new model: there is no editing of a foundation's code or
|
|
45
|
+
// schema, so it is most likely a LICENSING / access-control concern (who may
|
|
46
|
+
// use/reference the foundation), not "editing". Revisit as `--access` (or an
|
|
47
|
+
// org/licensing policy on the backend) when foundation licensing lands.
|
|
48
|
+
//
|
|
49
|
+
// • VERSION PROPAGATION — legacy `--propagate`. Opts a newly-registered version
|
|
50
|
+
// into the registry's version-update walk: trusting sites whose policy allows
|
|
51
|
+
// the jump (e.g. auto-patch) adopt it with no rebuild; default was "silent"
|
|
52
|
+
// (stored, nothing moves). The SAME concept applies to `runtime register`
|
|
53
|
+
// (legacy deploy-runtime had `--propagate` too). Implement once the backend
|
|
54
|
+
// has a version-update/propagation policy; until then every register is silent.
|
|
55
|
+
|
|
28
56
|
import { existsSync, readFileSync, writeFileSync } from 'node:fs'
|
|
57
|
+
import { execSync } from 'node:child_process'
|
|
29
58
|
import { resolve, join } from 'node:path'
|
|
30
59
|
import { buildRegistryPackage, buildSchemaOnlyPackage } from '@uniweb/build/uwx'
|
|
31
60
|
import { classifyPackage, isSchemasPackage, collectStandaloneSchemas } from '@uniweb/build'
|
|
32
|
-
import {
|
|
61
|
+
import { readRegistryAuth } from '../utils/registry-auth.js'
|
|
62
|
+
import { collectDistFiles } from '../utils/code-upload.js'
|
|
33
63
|
import { deriveScope } from '../utils/registry-orgs.js'
|
|
64
|
+
import { BackendClient } from '../backend/client.js'
|
|
34
65
|
import { writeJsonPreservingStyleAsync } from '../utils/json-file.js'
|
|
35
66
|
import { findWorkspaceRoot, findFoundations, promptSelect } from '../utils/workspace.js'
|
|
36
67
|
import { isNonInteractive, getCliPrefix } from '../utils/interactive.js'
|
|
37
68
|
|
|
38
|
-
// The backend route is `/dev/registry/register`; the host defaults to a local
|
|
39
|
-
// server and is overridable via --registry / UNIWEB_REGISTER_URL (full URL).
|
|
40
|
-
const DEFAULT_REGISTER_URL = 'http://localhost:8080/dev/registry/register'
|
|
41
|
-
|
|
42
69
|
const colors = {
|
|
43
70
|
reset: '\x1b[0m', bright: '\x1b[1m', dim: '\x1b[2m',
|
|
44
71
|
red: '\x1b[31m', green: '\x1b[32m', yellow: '\x1b[33m', blue: '\x1b[36m',
|
|
@@ -126,12 +153,53 @@ async function resolveFoundationDir(args) {
|
|
|
126
153
|
process.exit(1)
|
|
127
154
|
}
|
|
128
155
|
|
|
156
|
+
/**
|
|
157
|
+
* Does the foundation's `dist/` need a (re)build before we can register it?
|
|
158
|
+
*
|
|
159
|
+
* Mirrors `uniweb publish`'s build-if-stale so `register` is a full drop-in
|
|
160
|
+
* for the foundation-publish flow. Two staleness signals:
|
|
161
|
+
* - MISSING: no `dist/entry.js` (or the legacy `dist/foundation.js`), or no
|
|
162
|
+
* `dist/meta/schema.json` — nothing built yet.
|
|
163
|
+
* - STALE: the version baked into `dist/meta/schema.json::_self.version`
|
|
164
|
+
* differs from `package.json::version` — a version bump without a rebuild,
|
|
165
|
+
* so the artifact encodes the OLD version while the register intends the
|
|
166
|
+
* NEW one (we'd otherwise submit a schema whose version disagrees with the
|
|
167
|
+
* code we deliver).
|
|
168
|
+
*
|
|
169
|
+
* Returns `{ needs: false }` or `{ needs: true, reason }`.
|
|
170
|
+
*
|
|
171
|
+
* @param {string} targetDir the foundation directory
|
|
172
|
+
*/
|
|
173
|
+
export function foundationNeedsBuild(targetDir) {
|
|
174
|
+
const distDir = join(targetDir, 'dist')
|
|
175
|
+
const schemaPath = join(distDir, 'meta', 'schema.json')
|
|
176
|
+
// @uniweb/build emits dist/entry.js; older builds emitted dist/foundation.js.
|
|
177
|
+
const hasArtifact = existsSync(join(distDir, 'entry.js')) || existsSync(join(distDir, 'foundation.js'))
|
|
178
|
+
if (!hasArtifact || !existsSync(schemaPath)) return { needs: true, reason: 'no dist/ found' }
|
|
179
|
+
let pkgVersion = null
|
|
180
|
+
try {
|
|
181
|
+
pkgVersion = JSON.parse(readFileSync(join(targetDir, 'package.json'), 'utf8'))?.version || null
|
|
182
|
+
} catch {
|
|
183
|
+
// No readable package.json version — fall through; a present schema with no
|
|
184
|
+
// version to compare is treated as fresh (the submit path validates names).
|
|
185
|
+
}
|
|
186
|
+
try {
|
|
187
|
+
const peek = JSON.parse(readFileSync(schemaPath, 'utf8'))
|
|
188
|
+
if (peek?._self?.version && pkgVersion && peek._self.version !== pkgVersion) {
|
|
189
|
+
return { needs: true, reason: `package.json version (${pkgVersion}) differs from built schema (${peek._self.version})` }
|
|
190
|
+
}
|
|
191
|
+
} catch {
|
|
192
|
+
return { needs: true, reason: 'dist/meta/schema.json could not be parsed' }
|
|
193
|
+
}
|
|
194
|
+
return { needs: false }
|
|
195
|
+
}
|
|
196
|
+
|
|
129
197
|
export async function register(args = []) {
|
|
130
198
|
const dryRun = args.includes('--dry-run')
|
|
131
199
|
const output = flagValue(args, '-o') || flagValue(args, '--output')
|
|
132
200
|
const scopeFlag = flagValue(args, '--scope')
|
|
133
201
|
const tokenFlag = flagValue(args, '--token')
|
|
134
|
-
const
|
|
202
|
+
const client = new BackendClient({ originFlag: flagValue(args, '--registry'), token: tokenFlag, args, command: 'Registering' })
|
|
135
203
|
|
|
136
204
|
// Target: a schemas-only package (standalone data-schema register) or a
|
|
137
205
|
// foundation (foundation + the schemas it renders). A schemas package is only
|
|
@@ -146,8 +214,6 @@ export async function register(args = []) {
|
|
|
146
214
|
let scope = scopeFlag || pkgScope
|
|
147
215
|
let scopeSource = scopeFlag ? '--scope' : pkgScope ? 'package.json uniweb.scope' : null
|
|
148
216
|
const isPreview = !!output || dryRun
|
|
149
|
-
const apiBase = new URL(registryUrl).origin
|
|
150
|
-
let token = null
|
|
151
217
|
|
|
152
218
|
// Each path supplies a different schema source: the standalone path discovers
|
|
153
219
|
// the package's own schemas; the foundation path reads its built schema.json.
|
|
@@ -166,10 +232,29 @@ export async function register(args = []) {
|
|
|
166
232
|
return { exitCode: 2 }
|
|
167
233
|
}
|
|
168
234
|
} else {
|
|
235
|
+
// Build-if-stale (mirrors `uniweb publish`): a missing or version-stale
|
|
236
|
+
// dist/ gets (re)built before we read its schema. Preview paths
|
|
237
|
+
// (--dry-run / -o) must not write to dist/, so they require a pre-built
|
|
238
|
+
// foundation and say so instead of building.
|
|
239
|
+
const { needs, reason } = foundationNeedsBuild(targetDir)
|
|
240
|
+
if (needs) {
|
|
241
|
+
if (isPreview) {
|
|
242
|
+
error(`No usable build (${reason}).`)
|
|
243
|
+
log(` Build the foundation first: ${colors.bright}uniweb build${colors.reset}`)
|
|
244
|
+
return { exitCode: 2 }
|
|
245
|
+
}
|
|
246
|
+
info(`${reason} — building the foundation first …`)
|
|
247
|
+
try {
|
|
248
|
+
execSync('npx uniweb build --target foundation', { cwd: targetDir, stdio: 'inherit' })
|
|
249
|
+
} catch (err) {
|
|
250
|
+
error(`Build failed: ${err.message}`)
|
|
251
|
+
return { exitCode: 2 }
|
|
252
|
+
}
|
|
253
|
+
}
|
|
169
254
|
const schemaPath = join(targetDir, 'dist', 'meta', 'schema.json')
|
|
170
255
|
if (!existsSync(schemaPath)) {
|
|
171
|
-
error('No built schema found (dist/meta/schema.json).')
|
|
172
|
-
log(` Build the foundation
|
|
256
|
+
error('No built schema found (dist/meta/schema.json) after build.')
|
|
257
|
+
log(` Build the foundation: ${colors.bright}uniweb build${colors.reset}`)
|
|
173
258
|
return { exitCode: 2 }
|
|
174
259
|
}
|
|
175
260
|
try {
|
|
@@ -183,9 +268,9 @@ export async function register(args = []) {
|
|
|
183
268
|
// No scope for a real submit → derive it from login membership (list → 1 use /
|
|
184
269
|
// 0 create / N pick), persist to package.json, and reuse the session token.
|
|
185
270
|
if (!scope && !isPreview) {
|
|
186
|
-
token =
|
|
271
|
+
const token = await client.token()
|
|
187
272
|
const sess = await readRegistryAuth()
|
|
188
|
-
const derived = await deriveScope({ apiBase, token, accountHandle: sess?.handle || null, args })
|
|
273
|
+
const derived = await deriveScope({ apiBase: client.origin, token, accountHandle: sess?.handle || null, args })
|
|
189
274
|
if (!derived) return { exitCode: 0 }
|
|
190
275
|
scope = `@${derived}`
|
|
191
276
|
scopeSource = 'login'
|
|
@@ -229,7 +314,15 @@ export async function register(args = []) {
|
|
|
229
314
|
log('')
|
|
230
315
|
log(json)
|
|
231
316
|
log('')
|
|
232
|
-
info(`Dry run — would submit to ${
|
|
317
|
+
info(`Dry run — would submit to ${client.origin}`)
|
|
318
|
+
if (!standalone && !args.includes('--schema-only')) {
|
|
319
|
+
const distFiles = collectDistFiles(join(targetDir, 'dist'))
|
|
320
|
+
log('')
|
|
321
|
+
info(`Would then deliver ${distFiles.length} code file(s) (meta/ excluded):`)
|
|
322
|
+
for (const f of distFiles) {
|
|
323
|
+
log(` ${colors.dim}${f.path} ${f.size} bytes ${f.content_type}${colors.reset}`)
|
|
324
|
+
}
|
|
325
|
+
}
|
|
233
326
|
return { exitCode: 0 }
|
|
234
327
|
}
|
|
235
328
|
|
|
@@ -239,36 +332,88 @@ export async function register(args = []) {
|
|
|
239
332
|
log(` ${colors.dim}Without a scope, names stay @/… and the registry rejects them.${colors.reset}`)
|
|
240
333
|
return { exitCode: 2 }
|
|
241
334
|
}
|
|
242
|
-
// Submit
|
|
243
|
-
//
|
|
244
|
-
|
|
245
|
-
info(`Submitting to ${colors.dim}${registryUrl}${colors.reset} …`)
|
|
335
|
+
// Submit — the client carries the bearer (--token › UNIWEB_TOKEN › stored
|
|
336
|
+
// session › login), resolved lazily on this first authed call.
|
|
337
|
+
info(`Submitting to ${colors.dim}${client.origin}${colors.reset} …`)
|
|
246
338
|
let res
|
|
247
339
|
try {
|
|
248
|
-
res = await
|
|
249
|
-
method: 'POST',
|
|
250
|
-
headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${token}` },
|
|
251
|
-
body: json,
|
|
252
|
-
})
|
|
340
|
+
res = await client.register(json)
|
|
253
341
|
} catch (err) {
|
|
254
|
-
error(`Could not reach the registry at ${
|
|
342
|
+
error(`Could not reach the registry at ${client.origin}: ${err.message}`)
|
|
255
343
|
log(` ${colors.dim}Set the endpoint with --registry <url> or UNIWEB_REGISTER_URL.${colors.reset}`)
|
|
256
344
|
return { exitCode: 2 }
|
|
257
345
|
}
|
|
346
|
+
let alreadyRegistered = false
|
|
258
347
|
if (!res.ok) {
|
|
259
|
-
error(`Registry rejected the submission: HTTP ${res.status} ${res.statusText}`)
|
|
260
|
-
if (res.status === 401 || res.status === 403) {
|
|
261
|
-
log(` ${colors.dim}The registry didn't accept your credentials — it may use different ones than \`uniweb login\`.${colors.reset}`)
|
|
262
|
-
log(` ${colors.dim}Supply a registry bearer with --token <bearer> (or UNIWEB_TOKEN); an existing one may be wrong or expired.${colors.reset}`)
|
|
263
|
-
}
|
|
264
348
|
const body = await res.text().catch(() => '')
|
|
265
|
-
|
|
266
|
-
|
|
349
|
+
// Resume path: a registered version is immutable, so re-running after a
|
|
350
|
+
// partial code delivery hits the duplicate rejection here — a STRUCTURED
|
|
351
|
+
// 409 (problem+json, title "Conflict") — and proceeds to phase 2 (the
|
|
352
|
+
// code-uploads plan authorizes against the REGISTERED version; completed
|
|
353
|
+
// files are idempotent no-ops).
|
|
354
|
+
const isDuplicate = !standalone && res.status === 409
|
|
355
|
+
if (isDuplicate) {
|
|
356
|
+
alreadyRegistered = true
|
|
357
|
+
info(`${colors.dim}Schema for this version is already registered — resuming code delivery.${colors.reset}`)
|
|
358
|
+
} else {
|
|
359
|
+
error(`Registry rejected the submission: HTTP ${res.status} ${res.statusText}`)
|
|
360
|
+
if (res.status === 401 || res.status === 403) {
|
|
361
|
+
log(` ${colors.dim}The registry didn't accept your credentials — it may use different ones than \`uniweb login\`.${colors.reset}`)
|
|
362
|
+
log(` ${colors.dim}Supply a registry bearer with --token <bearer> (or UNIWEB_TOKEN); an existing one may be wrong or expired.${colors.reset}`)
|
|
363
|
+
}
|
|
364
|
+
if (body) log(` ${colors.dim}${body.slice(0, 500)}${colors.reset}`)
|
|
365
|
+
return { exitCode: 1 }
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
if (!alreadyRegistered) {
|
|
369
|
+
success(
|
|
370
|
+
standalone
|
|
371
|
+
? `Registered ${defined.length} data schema(s)${scope ? ` under ${scope}` : ''}`
|
|
372
|
+
: `Registered ${schema._self.name}@${schema._self.version}${defined.length ? ` + ${defined.length} data schema(s)` : ''}`
|
|
373
|
+
)
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
// Phase 2 — deliver the foundation's code (plan → PUT-per-file, entry
|
|
377
|
+
// last; contract: foundation-code-upload.md). Schemas-
|
|
378
|
+
// only packages have no dist; --schema-only skips deliberately.
|
|
379
|
+
if (!standalone && !args.includes('--schema-only')) {
|
|
380
|
+
const distDir = join(targetDir, 'dist')
|
|
381
|
+
// The registry's vocabulary is the SCOPED name (`@org/name`). A scoped
|
|
382
|
+
// package name passes through; a bare one gets the chosen scope — the
|
|
383
|
+
// same resolution the .uwx submission applied.
|
|
384
|
+
const bareName = schema._self.name
|
|
385
|
+
const name = bareName.startsWith('@') ? bareName : `${scope}/${bareName}`
|
|
386
|
+
const version = schema._self.version
|
|
387
|
+
info(`Delivering code for ${colors.bright}${name}@${version}${colors.reset} …`)
|
|
388
|
+
try {
|
|
389
|
+
const result = await client.uploadFoundationCode({
|
|
390
|
+
name,
|
|
391
|
+
version,
|
|
392
|
+
distDir,
|
|
393
|
+
onProgress: (m) => log(` ${colors.dim}${m}${colors.reset}`),
|
|
394
|
+
})
|
|
395
|
+
if (result.failed.length) {
|
|
396
|
+
error(`${result.failed.length} file(s) failed to upload:`)
|
|
397
|
+
for (const f of result.failed) {
|
|
398
|
+
log(` ${colors.red}${f.path}${colors.reset} ${colors.dim}HTTP ${f.status} ${f.detail}${colors.reset}`)
|
|
399
|
+
}
|
|
400
|
+
log(` ${colors.dim}Re-run \`uniweb register\` to resume — completed files are safe no-ops.${colors.reset}`)
|
|
401
|
+
return { exitCode: 1 }
|
|
402
|
+
}
|
|
403
|
+
const where = result.serveBase || 'the registry gateway'
|
|
404
|
+
if (result.verified === true) {
|
|
405
|
+
success(`Code delivered (${result.uploaded.length} files) — entry verified live at ${colors.dim}${where}${colors.reset}`)
|
|
406
|
+
} else if (result.verified === false) {
|
|
407
|
+
error('Code uploaded but the entry verification fetch did not match — investigate before using this version.')
|
|
408
|
+
return { exitCode: 1 }
|
|
409
|
+
} else {
|
|
410
|
+
success(`Code delivered (${result.uploaded.length} files, ${result.mode} mode)`)
|
|
411
|
+
}
|
|
412
|
+
} catch (err) {
|
|
413
|
+
error(`Code delivery failed: ${err.message}`)
|
|
414
|
+
log(` ${colors.dim}The schema registration above succeeded; re-run \`uniweb register\` to deliver the code.${colors.reset}`)
|
|
415
|
+
return { exitCode: 1 }
|
|
416
|
+
}
|
|
267
417
|
}
|
|
268
|
-
success(
|
|
269
|
-
standalone
|
|
270
|
-
? `Registered ${defined.length} data schema(s)${scope ? ` under ${scope}` : ''}`
|
|
271
|
-
: `Registered ${schema._self.name}@${schema._self.version}${defined.length ? ` + ${defined.length} data schema(s)` : ''}`
|
|
272
|
-
)
|
|
273
418
|
return { exitCode: 0 }
|
|
274
419
|
}
|
|
@@ -0,0 +1,141 @@
|
|
|
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). Foundations pin a runtime
|
|
5
|
+
* version (`dist/runtime-pin.json`); that version must be registered, or `uniweb
|
|
6
|
+
* register` of such a foundation fails.
|
|
7
|
+
*
|
|
8
|
+
* Contract AGREED with the backend (2026-06-14): `POST /dev/runtime`, @std-gated,
|
|
9
|
+
* manifest-last. Wire + the two-half artifact set (SPA + ssr-edge isolate, the
|
|
10
|
+
* orchestrator stays platform-owned): utils/runtime-upload.js.
|
|
11
|
+
*
|
|
12
|
+
* Usage:
|
|
13
|
+
* uniweb runtime register From framework/runtime (or --path <dir>)
|
|
14
|
+
* uniweb runtime register --path <dir> The @uniweb/runtime package dir
|
|
15
|
+
* uniweb runtime register --version <v> Override dist/app/manifest.json's version
|
|
16
|
+
* uniweb runtime register --backend <url> Override the backend origin
|
|
17
|
+
* uniweb runtime register --token <bearer> Auth bearer (skips `uniweb login`)
|
|
18
|
+
* uniweb runtime register --dry-run Print the version + file plan; upload nothing
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import { readFileSync } from 'node:fs'
|
|
22
|
+
import { resolve, join } from 'node:path'
|
|
23
|
+
|
|
24
|
+
import { BackendClient } from '../backend/client.js'
|
|
25
|
+
import { collectRuntimeFiles, hasWorkerRuntime, hasShims } from '../utils/runtime-upload.js'
|
|
26
|
+
import { readFlagValue } from '../utils/args.js'
|
|
27
|
+
|
|
28
|
+
const c = {
|
|
29
|
+
reset: '\x1b[0m', bold: '\x1b[1m', dim: '\x1b[2m',
|
|
30
|
+
cyan: '\x1b[36m', green: '\x1b[32m', yellow: '\x1b[33m', red: '\x1b[31m',
|
|
31
|
+
}
|
|
32
|
+
const say = {
|
|
33
|
+
ok: (m) => console.log(`${c.green}✓${c.reset} ${m}`),
|
|
34
|
+
info: (m) => console.log(`${c.cyan}→${c.reset} ${m}`),
|
|
35
|
+
warn: (m) => console.log(`${c.yellow}⚠${c.reset} ${m}`),
|
|
36
|
+
err: (m) => console.error(`${c.red}✗${c.reset} ${m}`),
|
|
37
|
+
dim: (m) => console.log(` ${c.dim}${m}${c.reset}`),
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// The runtime package dir: --path, else the cwd when it IS @uniweb/runtime.
|
|
41
|
+
function resolveRuntimeDir(args) {
|
|
42
|
+
const pathFlag = readFlagValue(args, '--path')
|
|
43
|
+
if (pathFlag) return resolve(pathFlag)
|
|
44
|
+
try {
|
|
45
|
+
if (JSON.parse(readFileSync(join(process.cwd(), 'package.json'), 'utf8')).name === '@uniweb/runtime') {
|
|
46
|
+
return process.cwd()
|
|
47
|
+
}
|
|
48
|
+
} catch {
|
|
49
|
+
// no readable package.json — fall through
|
|
50
|
+
}
|
|
51
|
+
return null
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export async function runtime(args = []) {
|
|
55
|
+
const sub = args[0]
|
|
56
|
+
if (sub !== 'register') {
|
|
57
|
+
say.err(sub ? `Unknown subcommand: runtime ${sub}` : 'Usage: uniweb runtime register')
|
|
58
|
+
say.dim('uniweb runtime register — upload the built @uniweb/runtime to the backend (@std only).')
|
|
59
|
+
return { exitCode: sub ? 1 : 0 }
|
|
60
|
+
}
|
|
61
|
+
const rest = args.slice(1)
|
|
62
|
+
const dryRun = rest.includes('--dry-run')
|
|
63
|
+
|
|
64
|
+
const runtimeDir = resolveRuntimeDir(rest)
|
|
65
|
+
if (!runtimeDir) {
|
|
66
|
+
say.err('Not an @uniweb/runtime package.')
|
|
67
|
+
say.dim('Run from framework/runtime, or pass --path <dir>.')
|
|
68
|
+
return { exitCode: 2 }
|
|
69
|
+
}
|
|
70
|
+
const distDir = join(runtimeDir, 'dist')
|
|
71
|
+
const files = collectRuntimeFiles(distDir)
|
|
72
|
+
if (!files.length) {
|
|
73
|
+
say.err('No built runtime found (dist/app/).')
|
|
74
|
+
say.dim('Build it first: `pnpm build` in framework/runtime.')
|
|
75
|
+
return { exitCode: 2 }
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Version from the SPA build's manifest (the backend keys the version on it);
|
|
79
|
+
// --version overrides, parity with `uniwebd runtime install --version`.
|
|
80
|
+
let version = readFlagValue(rest, '--version')
|
|
81
|
+
if (!version) {
|
|
82
|
+
try {
|
|
83
|
+
version = JSON.parse(readFileSync(join(distDir, 'app', 'manifest.json'), 'utf8')).version
|
|
84
|
+
} catch (err) {
|
|
85
|
+
say.err(`Could not read dist/app/manifest.json: ${err.message}`)
|
|
86
|
+
return { exitCode: 2 }
|
|
87
|
+
}
|
|
88
|
+
if (!version) {
|
|
89
|
+
say.err('dist/app/manifest.json has no "version" field — rebuild the runtime.')
|
|
90
|
+
return { exitCode: 2 }
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
// The ssr-edge artifact is a SET: worker-runtime.js + its shims/*.js. Warn when
|
|
94
|
+
// the set is absent or incomplete (a worker without shims can't resolve react).
|
|
95
|
+
if (!hasWorkerRuntime(files)) {
|
|
96
|
+
say.warn("dist/worker-runtime.js is missing — the SSR isolate bundle won't be uploaded.")
|
|
97
|
+
say.dim('Build it first: `pnpm build:worker` in @uniweb/runtime (after `pnpm build`).')
|
|
98
|
+
} else if (!hasShims(files)) {
|
|
99
|
+
say.warn('dist/worker-runtime.js is present but dist/shims/ is missing — the SSR isolate set is incomplete.')
|
|
100
|
+
say.dim('The isolate resolves react/jsx-runtime/@uniweb/core through those shims; re-run `pnpm build:worker`.')
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (dryRun) {
|
|
104
|
+
say.info(`Would register ${c.bold}@uniweb/runtime@${version}${c.reset} (${files.length} files):`)
|
|
105
|
+
for (const f of files) say.dim(`${f.path} ${f.size} bytes ${f.content_type}`)
|
|
106
|
+
return { exitCode: 0 }
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const client = new BackendClient({
|
|
110
|
+
originFlag: readFlagValue(rest, '--backend') || readFlagValue(rest, '--registry'),
|
|
111
|
+
token: readFlagValue(rest, '--token') || undefined,
|
|
112
|
+
args: rest,
|
|
113
|
+
command: 'Registering the runtime',
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
say.info(`Registering ${c.bold}@uniweb/runtime@${version}${c.reset} → ${c.dim}${client.origin}${c.reset} (${files.length} files)…`)
|
|
117
|
+
let result
|
|
118
|
+
try {
|
|
119
|
+
result = await client.uploadRuntime({ version, distDir, files, onProgress: (m) => say.dim(m) })
|
|
120
|
+
} catch (err) {
|
|
121
|
+
if (err.status === 403) {
|
|
122
|
+
say.err('Not authorized — registering a runtime version requires @std membership.')
|
|
123
|
+
return { exitCode: 1 }
|
|
124
|
+
}
|
|
125
|
+
say.err(`Runtime registration failed: ${err.message}`)
|
|
126
|
+
say.dim('Set the origin with --backend <url>; auth with `uniweb login` or --token <bearer>.')
|
|
127
|
+
return { exitCode: 1 }
|
|
128
|
+
}
|
|
129
|
+
if (result.failed.length) {
|
|
130
|
+
say.err(`${result.failed.length} file(s) failed to upload:`)
|
|
131
|
+
for (const f of result.failed) say.dim(`${f.path} — HTTP ${f.status} ${f.detail}`)
|
|
132
|
+
say.dim('Re-run `uniweb runtime register` to resume — completed files dedupe.')
|
|
133
|
+
return { exitCode: 1 }
|
|
134
|
+
}
|
|
135
|
+
console.log('')
|
|
136
|
+
say.ok(`Registered ${c.bold}@uniweb/runtime@${version}${c.reset} (${result.uploaded.length} files, ${result.mode} mode)`)
|
|
137
|
+
if (result.serveBase) say.dim(`served at ${result.serveBase}`)
|
|
138
|
+
return { exitCode: 0 }
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export default runtime
|