uniweb 0.85.0 → 0.87.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -195,8 +195,8 @@ export async function deploy(args = []) {
195
195
  // uniweb target's `backend:` records where that target's publishes went; it routes
196
196
  // nothing (publish files its record under the target naming its backend).
197
197
  const goingTo = getRegistryApiBaseUrl()
198
- const aimedBy = process.env.UNIWEB_REGISTER_URL
199
- ? 'UNIWEB_REGISTER_URL'
198
+ const aimedBy = process.env.UNIWEB_SERVER
199
+ ? 'UNIWEB_SERVER'
200
200
  : loggedInOrigin()
201
201
  ? 'the backend you are logged in to'
202
202
  : 'the default backend — you are not logged in'
@@ -215,7 +215,7 @@ export async function deploy(args = []) {
215
215
  say.err(
216
216
  `Target '${resolved.targetName}' is on ${targetBackend}, but this would publish to ${goingTo} (${aimedBy}).`
217
217
  )
218
- say.dim(`To publish there, log in to it first: uniweb login --backend ${targetBackend}`)
218
+ say.dim(`To publish there, log in to it first: uniweb login --server ${targetBackend}`)
219
219
  process.exit(1)
220
220
  }
221
221
  say.dim(
@@ -2,10 +2,10 @@
2
2
  * `uniweb forget` — remove what this project recorded about where it has synced and
3
3
  * deployed. Local files only; nothing on any backend changes.
4
4
  *
5
- * uniweb forget --backend <url> one backend
5
+ * uniweb forget --server <url> one backend
6
6
  * uniweb forget --all everything — for a COPY that is to become a new project
7
7
  *
8
- * ## `--backend <url>`
8
+ * ## `--server <url>`
9
9
  *
10
10
  * - that backend's section of `sync.json` — the site uuid, the record map, asset
11
11
  * ids, provisioned services and the rest of what it minted;
@@ -43,12 +43,12 @@
43
43
  * ## Why it exists
44
44
  *
45
45
  * Two jobs. A script pushes a template site to a short-lived dev server and then
46
- * removes the traces of it without discarding the rest of the project (`--backend`)
46
+ * removes the traces of it without discarding the rest of the project (`--server`)
47
47
  * — which is also why traceless publish was dropped: push and pull leave traces too.
48
48
  * And someone duplicates a project to start a new site from it (`--all`).
49
49
  *
50
50
  * ⚠️ **No default target.** Forgetting a backend you still use means its next push
51
- * creates a second site there, so the verb makes you name it — `--backend` is
51
+ * creates a second site there, so the verb makes you name it — `--server` is
52
52
  * required even when the project has synced with only one.
53
53
  */
54
54
 
@@ -96,9 +96,9 @@ export async function forget(args = []) {
96
96
  }
97
97
 
98
98
  const all = args.includes('--all')
99
- const flag = readFlagValue(args, '--backend')
99
+ const flag = readFlagValue(args, '--server')
100
100
  if (all && flag) {
101
- say.err('Use --backend <url> or --all, not both.')
101
+ say.err('Use --server <url> or --all, not both.')
102
102
  return { exitCode: 2 }
103
103
  }
104
104
 
@@ -108,7 +108,7 @@ export async function forget(args = []) {
108
108
  if (all) return forgetAll(siteDir)
109
109
 
110
110
  if (!flag) {
111
- say.err('Name what to forget: uniweb forget --backend <url>')
111
+ say.err('Name what to forget: uniweb forget --server <url>')
112
112
  if (known.length) {
113
113
  say.dim(`This project has synced with: ${known.join(', ')}`)
114
114
  } else {
@@ -22,9 +22,9 @@
22
22
  * Distinct from `uniweb deploy` (third-party hosts) and `uniweb register`
23
23
  * (foundation code → catalog). For a self-contained artifact, see `uniweb export`.
24
24
  *
25
- * Backend: the one you are logged in to — UNIWEB_REGISTER_URL overrides it for a script
25
+ * Backend: the one you are logged in to — UNIWEB_SERVER overrides it for a script
26
26
  * (resolveBackendOrigin). Auth: UNIWEB_TOKEN > the stored session > `uniweb login`.
27
- * No `--backend` or `--token`: switching and signing in are `uniweb login`.
27
+ * No `--server` or `--token`: switching and signing in are `uniweb login`.
28
28
  *
29
29
  * Usage:
30
30
  * uniweb publish Bring the foundation along, sync, and go live
@@ -66,6 +66,7 @@ import { resolveDefaultLocale } from '@uniweb/core/locale-config'
66
66
 
67
67
  import {
68
68
  BackendClient,
69
+ BackendUnreachableError,
69
70
  describeRequestError,
70
71
  WorkspaceMismatchError
71
72
  } from '../backend/client.js'
@@ -270,10 +271,10 @@ export async function publish(args = []) {
270
271
  }
271
272
  const siteYml = readSiteYml(join(siteDir, 'site.yml'))
272
273
  // ⭐ THE BACKEND YOU ARE LOGGED IN TO is where this goes *[Diego, 2026-09-21]*, for every
273
- // backend verb (resolveBackendOrigin). Only UNIWEB_REGISTER_URL — the automation
274
+ // backend verb (resolveBackendOrigin). Only UNIWEB_SERVER — the automation
274
275
  // override — outranks it, and nothing talks to a backend the user is not logged in to:
275
276
  // logged in nowhere, the origin is the default backend and the first request asks for
276
- // that login. ⛔ There is no `--backend` here: switching is `uniweb login --backend`.
277
+ // that login. ⛔ There is no `--server` here: switching is `uniweb login --server`.
277
278
  // ⛔ The project's own record — its synced backend, deploy.yml's default target — routes
278
279
  // NOTHING. For part of 2026-09-21 it answered a "logged in nowhere" tier, which cannot
279
280
  // be reached: *"We do not allow any communication with backend if the user is not
@@ -303,8 +304,8 @@ export async function publish(args = []) {
303
304
  // ⚠️ Logged in to a backend where this project has no site, while it has one
304
305
  // elsewhere: following the login CREATES a second site. Say so before the owner
305
306
  // question, which otherwise arrives with no reason attached. Not when
306
- // UNIWEB_REGISTER_URL chose the backend — that is already a decision.
307
- if (!process.env.UNIWEB_REGISTER_URL) {
307
+ // UNIWEB_SERVER chose the backend — that is already a decision.
308
+ if (!process.env.UNIWEB_SERVER) {
308
309
  const known = syncedElsewhere(siteDir, client.origin)
309
310
  if (known) {
310
311
  const [headline, ...rest] = describeSyncedElsewhere(known, client.origin, 'publish')
@@ -480,6 +481,12 @@ export async function publish(args = []) {
480
481
  cliBin: process.argv[1]
481
482
  })
482
483
  } catch (err) {
484
+ // A backend that did not answer is said as that — not as a release that failed.
485
+ if (err instanceof BackendUnreachableError) {
486
+ say.err(err.message)
487
+ say.dim('Nothing was released or sent. Check that the backend is running, then re-run `uniweb publish`.')
488
+ return { exitCode: 1 }
489
+ }
483
490
  // A scope you are not a member of is said as that, with the ways on (`explainReleaseFailure`).
484
491
  say.err(err.notMember ? err.message : `Foundation release failed: ${err.message}`)
485
492
  for (const line of err.notMember ? err.ways : ['Fix the foundation, then re-run `uniweb publish`.']) say.dim(line)
@@ -933,7 +940,7 @@ export async function publish(args = []) {
933
940
  } catch (err) {
934
941
  say.err(describeRequestError(err, client.origin))
935
942
  if (!(err instanceof WorkspaceMismatchError))
936
- say.dim('Is that the backend you meant? Switch with: uniweb login --backend <url>')
943
+ say.dim('Is that the backend you meant? Switch with: uniweb login --server <url>')
937
944
  return { exitCode: 1 }
938
945
  }
939
946
  // The body of a publish that did not go through — read once, and kept for the report below.
@@ -44,9 +44,9 @@
44
44
  *
45
45
  * The content + folder pull lanes are both keyed by the site's uuid on that backend
46
46
  * (sync.json).
47
- * Backend: the one you are logged in to — UNIWEB_REGISTER_URL overrides it for a script
47
+ * Backend: the one you are logged in to — UNIWEB_SERVER overrides it for a script
48
48
  * (resolveBackendOrigin). Auth: UNIWEB_TOKEN > the stored session > `uniweb login`.
49
- * No `--backend` or `--token`: switching and signing in are `uniweb login`.
49
+ * No `--server` or `--token`: switching and signing in are `uniweb login`.
50
50
  *
51
51
  * A project that never pushed has no `$uuid` to pull by — pull is a no-op with a
52
52
  * clear message. The backend serves each lane as a `.uwx` (ZIP: `manifest.json` +
@@ -702,10 +702,10 @@ export async function pull(args = [], deps = {}) {
702
702
  if (blocked) return blocked
703
703
  }
704
704
  // ⭐ THE BACKEND YOU ARE LOGGED IN TO is where this goes *[Diego, 2026-09-21]*, for every
705
- // backend verb (resolveBackendOrigin). Only UNIWEB_REGISTER_URL — the automation
705
+ // backend verb (resolveBackendOrigin). Only UNIWEB_SERVER — the automation
706
706
  // override — outranks it, and nothing talks to a backend the user is not logged in to:
707
707
  // logged in nowhere, the origin is the default backend and the first request asks for
708
- // that login. ⛔ There is no `--backend` here: switching is `uniweb login --backend`.
708
+ // that login. ⛔ There is no `--server` here: switching is `uniweb login --server`.
709
709
  // ⛔ The project's own record — its synced backend, deploy.yml's default target — routes
710
710
  // NOTHING. For part of 2026-09-21 it answered a "logged in nowhere" tier, which cannot
711
711
  // be reached: *"We do not allow any communication with backend if the user is not
@@ -747,14 +747,14 @@ export async function pull(args = [], deps = {}) {
747
747
  if (!siteContentUuid) {
748
748
  // Following the login can land on a backend with no site for this project while it
749
749
  // has one elsewhere — say where, rather than only "push first", which would create a
750
- // second site. Not when UNIWEB_REGISTER_URL chose the backend: that is a decision.
751
- const known = !process.env.UNIWEB_REGISTER_URL
750
+ // second site. Not when UNIWEB_SERVER chose the backend: that is a decision.
751
+ const known = !process.env.UNIWEB_SERVER
752
752
  ? syncedElsewhere(siteDir, client.origin)
753
753
  : null
754
754
  if (known) {
755
755
  info(`Nothing to pull — this project has no site on ${client.origin}.`)
756
756
  note(
757
- `Its ${known.length === 1 ? `site is on ${known[0]}` : `sites are on ${known.join(', ')}`}. To pull from there: uniweb login --backend <url>`
757
+ `Its ${known.length === 1 ? `site is on ${known[0]}` : `sites are on ${known.join(', ')}`}. To pull from there: uniweb login --server <url>`
758
758
  )
759
759
  return { exitCode: 0 }
760
760
  }
@@ -799,7 +799,7 @@ export async function pull(args = [], deps = {}) {
799
799
  } catch (err) {
800
800
  error(describeRequestError(err, client.origin))
801
801
  if (err instanceof WorkspaceMismatchError) return { refused: true }
802
- note('Is that the backend you meant? Switch with: uniweb login --backend <url>')
802
+ note('Is that the backend you meant? Switch with: uniweb login --server <url>')
803
803
  return null
804
804
  }
805
805
  if (res.status === 404) {
@@ -816,7 +816,7 @@ export async function pull(args = [], deps = {}) {
816
816
  if (detail) note(detail)
817
817
  if (res.status === 401 || res.status === 403)
818
818
  note(
819
- "Credentials weren't accepted — log in again (`uniweb login --backend <url>`), or check UNIWEB_TOKEN."
819
+ "Credentials weren't accepted — log in again (`uniweb login --server <url>`), or check UNIWEB_TOKEN."
820
820
  )
821
821
  return null
822
822
  }
@@ -48,9 +48,9 @@
48
48
  * backend's reconcile deletes items absent from the package, so an author's NEW page
49
49
  * would be hard-deleted). `--force` omits the token and restores last-push-wins.
50
50
  *
51
- * Backend: the one you are logged in to — UNIWEB_REGISTER_URL overrides it for a script
51
+ * Backend: the one you are logged in to — UNIWEB_SERVER overrides it for a script
52
52
  * (resolveBackendOrigin). Auth: UNIWEB_TOKEN > the stored session > `uniweb login`.
53
- * No `--backend` or `--token`: switching and signing in are `uniweb login`.
53
+ * No `--server` or `--token`: switching and signing in are `uniweb login`.
54
54
  *
55
55
  * The two-lane SUBMISSION (POST both lanes, back-fill uuids, persist the
56
56
  * send-only-changed cache) lives in `../backend/site-sync.js` so `uniweb publish`
@@ -65,7 +65,7 @@ import { emitSyncPackages, readBackendState } from '@uniweb/build/uwx'
65
65
  import { findSiteCopies, describeSiteCopies } from '../utils/site-copies.js'
66
66
  import { uploadSiteMedia, describeAssetRefusal } from '../backend/site-media.js'
67
67
  import { updateBackendMap, carryServed, SYNC_STORE_FILE } from '@uniweb/build/uwx'
68
- import { BackendClient } from '../backend/client.js'
68
+ import { BackendClient, BackendUnreachableError } from '../backend/client.js'
69
69
  import { resolveSiteDir } from './deploy.js'
70
70
  import { refuseIfContentDoesNotConform } from '../utils/conformance.js'
71
71
  import { reportSchemalessQueries } from '../utils/schemaless-report.js'
@@ -162,10 +162,10 @@ export async function push(args = [], deps = {}) {
162
162
  // arrival or lose a value on the way. See utils/conformance.js.
163
163
  if (await refuseIfContentDoesNotConform(siteDir, { args })) return { exitCode: 1 }
164
164
  // ⭐ THE BACKEND YOU ARE LOGGED IN TO is where this goes *[Diego, 2026-09-21]*, for every
165
- // backend verb (resolveBackendOrigin). Only UNIWEB_REGISTER_URL — the automation
165
+ // backend verb (resolveBackendOrigin). Only UNIWEB_SERVER — the automation
166
166
  // override — outranks it, and nothing talks to a backend the user is not logged in to:
167
167
  // logged in nowhere, the origin is the default backend and the first request asks for
168
- // that login. ⛔ There is no `--backend` here: switching is `uniweb login --backend`.
168
+ // that login. ⛔ There is no `--server` here: switching is `uniweb login --server`.
169
169
  // ⛔ The project's own record — its synced backend, deploy.yml's default target — routes
170
170
  // NOTHING. For part of 2026-09-21 it answered a "logged in nowhere" tier, which cannot
171
171
  // be reached: *"We do not allow any communication with backend if the user is not
@@ -209,8 +209,8 @@ export async function push(args = [], deps = {}) {
209
209
  // ⚠️ Logged in to a backend where this project has no site, while it has one
210
210
  // elsewhere: following the login CREATES a second site. Say so before the owner
211
211
  // question, which otherwise arrives with no reason attached. Not for `-o` (nothing is
212
- // created) and not when UNIWEB_REGISTER_URL chose the backend — that is a decision.
213
- if (!output && !process.env.UNIWEB_REGISTER_URL) {
212
+ // created) and not when UNIWEB_SERVER chose the backend — that is a decision.
213
+ if (!output && !process.env.UNIWEB_SERVER) {
214
214
  const known = syncedElsewhere(siteDir, client.origin)
215
215
  if (known) {
216
216
  const [headline, ...rest] = describeSyncedElsewhere(known, client.origin, 'push')
@@ -278,6 +278,12 @@ export async function push(args = [], deps = {}) {
278
278
  verb: 'push'
279
279
  })
280
280
  } catch (err) {
281
+ // A backend that did not answer is said as that — not as a release that failed.
282
+ if (err instanceof BackendUnreachableError) {
283
+ error(err.message)
284
+ note('Nothing was released or sent. Check that the backend is running, then re-run `uniweb push`.')
285
+ return { exitCode: 1 }
286
+ }
281
287
  // A scope you are not a member of is said as that, with the ways on (`explainReleaseFailure`).
282
288
  error(err.notMember ? err.message : `Foundation release failed: ${err.message}`)
283
289
  for (const line of err.notMember ? err.ways : ['Fix the foundation, then re-run `uniweb push`.']) note(line)
@@ -31,7 +31,8 @@
31
31
  * uniweb refresh git pull, then merge the backend's content
32
32
  * uniweb refresh --no-git skip the git remote; backend only
33
33
  * uniweb refresh --no-backend skip the backend; git only
34
- * uniweb refresh --backend <url> Override the backend origin
34
+ *
35
+ * The backend is the one you are logged in to, as for every backend verb (no flag picks it).
35
36
  */
36
37
 
37
38
  import { readFileSync } from 'node:fs'
@@ -35,9 +35,9 @@
35
35
  * ({ok,scope,origin,entities:[{name,uuid,version,unchanged}]}),
36
36
  * all human output to stderr — for scripted callers
37
37
  *
38
- * Endpoint: UNIWEB_REGISTER_URL > the backend you are logged in to > the default
39
- * (~/.uniweb/config.json, else uniweb.app) — resolveBackendOrigin. No `--backend`:
40
- * switching is `uniweb login --backend <url>`.
38
+ * Endpoint: UNIWEB_SERVER > the backend you are logged in to > the default
39
+ * (~/.uniweb/config.json, else uniweb.app) — resolveBackendOrigin. No backend flag:
40
+ * switching is `uniweb login --server <url>`.
41
41
  * Auth (submit only): UNIWEB_TOKEN > the stored session > `uniweb login`.
42
42
  */
43
43
 
@@ -595,7 +595,7 @@ async function runRegister(args = []) {
595
595
  const dryRun = args.includes('--dry-run')
596
596
  const output = flagValue(args, '-o') || flagValue(args, '--output')
597
597
  const scopeFlag = flagValue(args, '--scope')
598
- // The backend you are logged in to (UNIWEB_REGISTER_URL overrides it for a script) —
598
+ // The backend you are logged in to (UNIWEB_SERVER overrides it for a script) —
599
599
  // resolveBackendOrigin, like every backend command.
600
600
  const client = new BackendClient({
601
601
  args,
@@ -698,7 +698,10 @@ async function runRegister(args = []) {
698
698
  // fd 2 is our stderr: progress stays visible, stdout stays parseable.
699
699
  // Found by a harness that had to work around it by taking the last
700
700
  // JSON line off stdout.
701
- stdio: jsonMode ? ['inherit', 2, 'inherit'] : 'inherit'
701
+ stdio: jsonMode ? ['inherit', 2, 'inherit'] : 'inherit',
702
+ // A step of register: the build's "Share with clients: uniweb register" hint
703
+ // would tell someone running register to run register.
704
+ env: { ...process.env, UNIWEB_BUILD_STEP: '1' }
702
705
  })
703
706
  } catch (err) {
704
707
  error(`Build failed: ${err.message}`)
@@ -845,7 +848,7 @@ async function runRegister(args = []) {
845
848
  } catch (err) {
846
849
  error(`Could not reach the registry at ${client.origin}: ${err.message}`)
847
850
  log(
848
- ` ${colors.dim}Is that the backend you meant? Switch with: uniweb login --backend <url>${colors.reset}`
851
+ ` ${colors.dim}Is that the backend you meant? Switch with: uniweb login --server <url>${colors.reset}`
849
852
  )
850
853
  return { exitCode: 2 }
851
854
  }
@@ -905,7 +908,7 @@ async function runRegister(args = []) {
905
908
  ` ${colors.dim}The registry didn't accept your credentials — it may use different ones than \`uniweb login\`.${colors.reset}`
906
909
  )
907
910
  log(
908
- ` ${colors.dim}Log in again (uniweb login --backend <url>), or check UNIWEB_TOKEN — an existing one may be wrong or expired.${colors.reset}`
911
+ ` ${colors.dim}Log in again (uniweb login --server <url>), or check UNIWEB_TOKEN — an existing one may be wrong or expired.${colors.reset}`
909
912
  )
910
913
  }
911
914
  // ⭐ **Surface `detail` as a sentence, not as a JSON dump.** The registry
@@ -62,7 +62,7 @@ import { writeJsonPreservingStyleAsync } from '../utils/json-file.js'
62
62
  import { getExistingPackageNames, validatePackageName } from '../utils/names.js'
63
63
  import { detectPackageManager, installCmd } from '../utils/pm.js'
64
64
  import { getCliPrefix } from '../utils/interactive.js'
65
- import { replaceInTopLevelList, setTopLevelScalar } from '../utils/yaml-edit.js'
65
+ import { replaceInYamlList, setYamlKey } from '@uniweb/build/uwx'
66
66
 
67
67
  const colors = {
68
68
  reset: '\x1b[0m',
@@ -307,9 +307,9 @@ async function renameFoundation(rootDir, oldName, newName, prefix) {
307
307
  pkgSrc,
308
308
  sitePkgPath,
309
309
  siteYmlPath,
310
- // Edited in place — see utils/yaml-edit.js — and computed HERE, before
310
+ // Edited in place — `setYamlKey`, @uniweb/build — and computed HERE, before
311
311
  // anything moves, so a file it cannot edit stops the rename cleanly.
312
- newYmlText: ymlMatches ? setTopLevelScalar(ymlText, 'foundation', newName) : null,
312
+ newYmlText: ymlMatches ? setYamlKey(ymlText, 'foundation', newName) : null,
313
313
  hasDep,
314
314
  ymlMatches
315
315
  })
@@ -318,7 +318,7 @@ async function renameFoundation(rootDir, oldName, newName, prefix) {
318
318
  refuseUneditableSiteYml(
319
319
  affectedSites.filter((s) => s.ymlMatches && s.newYmlText === null).map((s) => s.path),
320
320
  'foundation',
321
- `Write it on one line: \`foundation: ${oldName}\`.`
321
+ `Write \`foundation: ${oldName}\` as a plain value — not through a YAML alias (\`*name\`) or merge key (\`<<:\`).`
322
322
  )
323
323
 
324
324
  // ─── Print plan, then execute ────────────────────────────────
@@ -530,14 +530,14 @@ async function renameExtension(rootDir, oldName, newName, prefix) {
530
530
  siteYmlPath,
531
531
  hits,
532
532
  // Edited in place, and computed before anything moves — as for foundations.
533
- newYmlText: replaceInTopLevelList(ymlText, 'extensions', replacements)
533
+ newYmlText: replaceInYamlList(ymlText, 'extensions', replacements)
534
534
  })
535
535
  }
536
536
  }
537
537
  refuseUneditableSiteYml(
538
538
  affectedSites.filter((a) => a.newYmlText === null).map((a) => a.site.path),
539
539
  'extensions',
540
- `Write each entry on a line of its own, e.g. \`- ${oldUrlPrefix}dist/entry.js\`.`
540
+ 'Write `extensions:` as a plain list — not through a YAML alias (`*name`) or merge key (`<<:`).'
541
541
  )
542
542
 
543
543
  log('')