uniweb 0.62.0 → 0.63.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 +6 -6
- package/partials/agents.md +4 -3
- package/src/backend/client.js +231 -20
- package/src/backend/foundation-bring-along.js +12 -13
- package/src/backend/site-sync.js +235 -126
- package/src/commands/clone.js +35 -5
- package/src/commands/publish.js +33 -20
- package/src/commands/pull.js +55 -10
- package/src/commands/push.js +23 -17
- package/src/commands/register.js +136 -26
- package/src/commands/status.js +26 -5
- package/src/framework-index.json +6 -6
- package/src/index.js +14 -8
- package/src/utils/args.js +6 -14
- package/src/utils/flag-guard.js +17 -9
- package/src/utils/registry-orgs.js +3 -2
- package/src/utils/site-data-upload.js +13 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "uniweb",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.63.1",
|
|
4
4
|
"description": "Create structured Vite + React sites with content/code separation",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -41,17 +41,17 @@
|
|
|
41
41
|
"js-yaml": "^4.1.0",
|
|
42
42
|
"prompts": "^2.4.2",
|
|
43
43
|
"tar": "^7.0.0",
|
|
44
|
-
"@uniweb/core": "^0.29.8",
|
|
45
|
-
"@uniweb/schemas": "^0.3.3",
|
|
46
|
-
"@uniweb/semantic-parser": "^1.4.1",
|
|
47
44
|
"@uniweb/kit": "^0.19.4",
|
|
48
45
|
"@uniweb/runtime": "^0.27.4",
|
|
49
|
-
"@uniweb/
|
|
46
|
+
"@uniweb/schemas": "^0.3.4",
|
|
47
|
+
"@uniweb/content-writer": "^0.3.4",
|
|
48
|
+
"@uniweb/semantic-parser": "^1.4.1",
|
|
49
|
+
"@uniweb/core": "^0.29.8"
|
|
50
50
|
},
|
|
51
51
|
"peerDependencies": {
|
|
52
52
|
"@uniweb/content-reader": "^1.2.5",
|
|
53
53
|
"@uniweb/semantic-parser": "^1.4.1",
|
|
54
|
-
"@uniweb/build": "^0.
|
|
54
|
+
"@uniweb/build": "^0.59.0"
|
|
55
55
|
},
|
|
56
56
|
"peerDependenciesMeta": {
|
|
57
57
|
"@uniweb/build": {
|
package/partials/agents.md
CHANGED
|
@@ -2575,7 +2575,9 @@ Foundations have their own free path too: `uniweb add ci --target foundation` pu
|
|
|
2575
2575
|
> The answer is recorded in `sync.json` and committed, so it is a one-time choice per site,
|
|
2576
2576
|
> not per machine. **Ask the human which one to use** rather than picking for them — a site in
|
|
2577
2577
|
> the wrong org cannot be moved from here. Sites that already exist are unaffected: their
|
|
2578
|
-
> ownership is settled, so nothing is asked.
|
|
2578
|
+
> ownership is settled, so nothing is asked. After the create, every command names the
|
|
2579
|
+
> recorded org for you; an `--org` that disagrees with where the backend keeps the site
|
|
2580
|
+
> **stops the command** rather than acting in another org.
|
|
2579
2581
|
>
|
|
2580
2582
|
> Relatedly: these commands now **reject flags they do not recognize** instead of ignoring them.
|
|
2581
2583
|
> If you get `Unknown flag`, read the suggestion — it is usually a near miss.
|
|
@@ -2630,10 +2632,9 @@ Platform-specific configuration that doesn't belong in npm-standard fields. All
|
|
|
2630
2632
|
|
|
2631
2633
|
| Field | Where used | Default | Purpose |
|
|
2632
2634
|
|---|---|---|---|
|
|
2633
|
-
| `scope` | `uniweb register` | derived from your login, then saved here | The org the foundation registers under (`@acme`, or `acme`): a bare `main.js` name `marketing` registers as `@acme/marketing`. `--scope @org` overrides it for one run. |
|
|
2634
2635
|
| `runtimePolicy` | `dist/runtime-pin.json` | unset | Declares how far past the recorded runtime version a host may move a site. |
|
|
2635
2636
|
|
|
2636
|
-
**The foundation's name is not here — it is `name` in `main.js`** (else `package.json`'s `name`). ⛔ `uniweb.id`
|
|
2637
|
+
**The foundation's name is not here — it is `name` in `main.js`** (else `package.json`'s `name`), **and so is the org it registers under: the scope is part of the name**, `name: '@acme/marketing'`. A name with no scope has not been registered yet: the first `uniweb register` takes `--scope @org` (else one of your orgs) and writes it into the name, and a `--scope` naming another org than the name's is refused. The foundation's own data schemas register under the same scope (`@/article` → `@acme/article`). ⛔ `uniweb.id` and `uniweb.scope` are no longer read for a foundation: `uniweb register` refuses them and prints the `main.js` line to write instead. (A schemas-only package has no `main.js`, and still records its scope in `uniweb.scope`.)
|
|
2637
2638
|
|
|
2638
2639
|
**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.
|
|
2639
2640
|
|
package/src/backend/client.js
CHANGED
|
@@ -105,6 +105,141 @@ export function resolveBackendOrigin() {
|
|
|
105
105
|
*/
|
|
106
106
|
export const DISCOVERY_DEFAULTS = {}
|
|
107
107
|
|
|
108
|
+
/**
|
|
109
|
+
* The header a request names its WORKSPACE with — the unit it works in.
|
|
110
|
+
*
|
|
111
|
+
* ⭐ `@<handle>`, the `@` required: a bare value is read as a unit's uuid — the spelling
|
|
112
|
+
* for a workspace with no handle — so the two can never be mistaken for each other.
|
|
113
|
+
* Every `/dev` route accepts it; the client names it on the site routes, the ones that
|
|
114
|
+
* work in a workspace (`namesWorkspace`). ⛔ It replaced `?as_org=` (2026-09-23), which
|
|
115
|
+
* only routes that declared it accepted — the rest answered `400`.
|
|
116
|
+
*/
|
|
117
|
+
export const WORKSPACE_HEADER = 'x-uniweb-workspace'
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Whether a request works IN a workspace — a site route, `/dev/site…`.
|
|
121
|
+
*
|
|
122
|
+
* Only those name one. Every `/dev` route accepts the header, but registration reads
|
|
123
|
+
* none (the `@scope` in the name decides, membership-gated), `/dev/assets` reads none
|
|
124
|
+
* (a charge lands on the entity's own workspace), and auth, orgs and config are about
|
|
125
|
+
* the account. Naming the site's workspace there would be harmless and meaningless —
|
|
126
|
+
* and a workspace that is refused (`400`/`403`) would then fail a registration that
|
|
127
|
+
* never involved it.
|
|
128
|
+
*
|
|
129
|
+
* @param {string} path - leading-slash path
|
|
130
|
+
* @returns {boolean}
|
|
131
|
+
*/
|
|
132
|
+
export function namesWorkspace(path) {
|
|
133
|
+
return /^\/dev\/site(\/|$)/.test(String(path))
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* A workspace as it may be written — `@acme`, `acme`, `@acme/…` — as the header's
|
|
138
|
+
* `@acme`; null for none (the personal workspace, which a request names by omission).
|
|
139
|
+
*
|
|
140
|
+
* @param {unknown} value
|
|
141
|
+
* @returns {string|null}
|
|
142
|
+
*/
|
|
143
|
+
export function workspaceHandle(value) {
|
|
144
|
+
const h =
|
|
145
|
+
typeof value === 'string' ? value.trim().replace(/^@/, '').replace(/\/.*$/, '') : ''
|
|
146
|
+
return h ? `@${h}` : null
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const UNIT_UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* A workspace as the header names it — `@handle`, or a unit's bare uuid when the unit
|
|
153
|
+
* has no handle — or null for none.
|
|
154
|
+
*
|
|
155
|
+
* ⚠️ A bare value is taken for a uuid only when it is shaped like one, and our own
|
|
156
|
+
* handle grammar admits uuid-shaped handles (3–39 lowercase letters, digits, hyphens).
|
|
157
|
+
* So a HANDLE reaches this already dressed — `--org` through `workspaceHandle`, a
|
|
158
|
+
* record through `readSiteWorkspace` — and a bare uuid here is a unit.
|
|
159
|
+
*
|
|
160
|
+
* @param {unknown} value
|
|
161
|
+
* @returns {string|null}
|
|
162
|
+
*/
|
|
163
|
+
export function workspaceHeader(value) {
|
|
164
|
+
if (typeof value !== 'string' || !value.trim()) return null
|
|
165
|
+
const v = value.trim()
|
|
166
|
+
return !v.startsWith('@') && UNIT_UUID.test(v) ? v : workspaceHandle(v)
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* The request named its workspace EXPLICITLY — `--org`, `--personal`, a choice made at
|
|
171
|
+
* create — and the backend works on the site from another.
|
|
172
|
+
*
|
|
173
|
+
* ⛔ Stopping is the point. It is the mismatch check [Diego, 2026-09-22: "there is no org
|
|
174
|
+
* mismatch when the site is set for an org and the user tries a different one"], and it
|
|
175
|
+
* is the backend's `409` that decides it, never a handle comparison here: a site in
|
|
176
|
+
* `@acme/labs` is legitimately worked on from `@acme`. Adopting the answer would override
|
|
177
|
+
* a choice the user made.
|
|
178
|
+
*/
|
|
179
|
+
export class WorkspaceMismatchError extends Error {
|
|
180
|
+
/**
|
|
181
|
+
* @param {{ named: string|null, answer: string|null }} p - the workspace the request
|
|
182
|
+
* named, and the one the backend named (`@handle`, null for personal)
|
|
183
|
+
*/
|
|
184
|
+
constructor({ named, answer }) {
|
|
185
|
+
const where = (w) =>
|
|
186
|
+
!w ? 'your personal workspace' : w.startsWith('@') ? w : `the unit ${w}`
|
|
187
|
+
const flag = named ? '--org' : '--personal'
|
|
188
|
+
super(
|
|
189
|
+
`The backend works on this site from ${where(answer)}, not ${where(named)} ` +
|
|
190
|
+
`(${named ? `--org ${named}` : flag}). ` +
|
|
191
|
+
// `--org` takes a handle; a unit without one is reached by naming nothing.
|
|
192
|
+
(answer?.startsWith('@') ? `Pass --org ${answer}, or drop ${flag}.` : `Drop ${flag}.`)
|
|
193
|
+
)
|
|
194
|
+
this.name = 'WorkspaceMismatchError'
|
|
195
|
+
this.status = 409
|
|
196
|
+
this.named = named
|
|
197
|
+
this.answer = answer
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* The line to print when a request THREW rather than answered.
|
|
203
|
+
*
|
|
204
|
+
* A `WorkspaceMismatchError` is the backend's answer, not a transport failure, so it is
|
|
205
|
+
* printed as itself: "Could not reach the backend" in front of it would send the user
|
|
206
|
+
* to check their network over a choice they made on the command line.
|
|
207
|
+
*
|
|
208
|
+
* @param {unknown} err
|
|
209
|
+
* @param {string} origin
|
|
210
|
+
* @returns {string}
|
|
211
|
+
*/
|
|
212
|
+
export function describeRequestError(err, origin) {
|
|
213
|
+
if (err instanceof WorkspaceMismatchError) return err.message
|
|
214
|
+
return `Could not reach the backend at ${origin}: ${err?.message ?? err}`
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* The sentence a refusal carries — a problem document's `detail`, else its `title`, else
|
|
219
|
+
* the start of a prose body — or null. Consumes the body. For the surfaces that would
|
|
220
|
+
* otherwise print a status line alone, so a refusal the backend explains is shown as it
|
|
221
|
+
* is: a deployment with one workspace answers a request naming another with a plain
|
|
222
|
+
* `409 Conflict` whose `detail` names the one there is.
|
|
223
|
+
*
|
|
224
|
+
* @param {Response} res
|
|
225
|
+
* @returns {Promise<string|null>}
|
|
226
|
+
*/
|
|
227
|
+
export async function refusalDetail(res) {
|
|
228
|
+
let body = ''
|
|
229
|
+
try {
|
|
230
|
+
body = await res.text()
|
|
231
|
+
} catch {
|
|
232
|
+
return null
|
|
233
|
+
}
|
|
234
|
+
try {
|
|
235
|
+
const problem = JSON.parse(body)
|
|
236
|
+
const line = problem?.detail || problem?.title
|
|
237
|
+
return typeof line === 'string' && line ? line : null
|
|
238
|
+
} catch {
|
|
239
|
+
return body ? body.slice(0, 300) : null
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
108
243
|
export class BackendClient {
|
|
109
244
|
/**
|
|
110
245
|
* @param {object} [opts]
|
|
@@ -134,6 +269,35 @@ export class BackendClient {
|
|
|
134
269
|
this._command = command
|
|
135
270
|
this._fetch = fetchImpl || ((url, init) => globalThis.fetch(url, init))
|
|
136
271
|
this._discovery = null
|
|
272
|
+
this._workspace = null
|
|
273
|
+
this._workspaceExplicit = false
|
|
274
|
+
this._onWorkspaceAdopted = null
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/** The workspace this client's requests name — `@handle`, or null (none named). */
|
|
278
|
+
get workspace() {
|
|
279
|
+
return this._workspace
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* Name the workspace every request from here on works in (`x-uniweb-workspace`).
|
|
284
|
+
*
|
|
285
|
+
* @param {string|null} value - `@acme` / `acme`, a handle-less unit's uuid, or null to
|
|
286
|
+
* name none (`workspaceHeader`)
|
|
287
|
+
* @param {object} [opts]
|
|
288
|
+
* @param {boolean} [opts.explicit=false] - the user named it (`--org`, `--personal`,
|
|
289
|
+
* the create picker). A `409 wrong_workspace` then STOPS the command
|
|
290
|
+
* (`WorkspaceMismatchError`) instead of adopting the backend's answer.
|
|
291
|
+
* @param {(handle: string|null) => void|Promise<void>} [opts.onAdopted] - called when a
|
|
292
|
+
* request that named nothing, or a stale workspace, adopts the one the backend named;
|
|
293
|
+
* the caller records it (sync.json) and says so
|
|
294
|
+
* @returns {this}
|
|
295
|
+
*/
|
|
296
|
+
setWorkspace(value, { explicit = false, onAdopted = null } = {}) {
|
|
297
|
+
this._workspace = workspaceHeader(value)
|
|
298
|
+
this._workspaceExplicit = Boolean(explicit)
|
|
299
|
+
this._onWorkspaceAdopted = onAdopted || null
|
|
300
|
+
return this
|
|
137
301
|
}
|
|
138
302
|
|
|
139
303
|
/**
|
|
@@ -168,7 +332,9 @@ export class BackendClient {
|
|
|
168
332
|
* applies query params, and infers a content-type from the body when unset
|
|
169
333
|
* (string → application/json, Buffer/Uint8Array → application/zip). Returns
|
|
170
334
|
* the raw Response so callers branch on status themselves (409 resume,
|
|
171
|
-
* 404 → null, 401/403 messaging, …)
|
|
335
|
+
* 404 → null, 401/403 messaging, …) — except a `409 wrong_workspace`, which is
|
|
336
|
+
* answered here: adopted and retried once when the workspace was not named, and
|
|
337
|
+
* thrown as `WorkspaceMismatchError` when it was (see the body).
|
|
172
338
|
*
|
|
173
339
|
* @param {string} path - leading-slash path, e.g. '/dev/site/content'
|
|
174
340
|
* @param {object} [opts]
|
|
@@ -179,10 +345,41 @@ export class BackendClient {
|
|
|
179
345
|
* @param {boolean} [opts.auth=true]
|
|
180
346
|
* @returns {Promise<Response>}
|
|
181
347
|
*/
|
|
182
|
-
async request(
|
|
183
|
-
path,
|
|
184
|
-
|
|
185
|
-
|
|
348
|
+
async request(path, opts = {}) {
|
|
349
|
+
const res = await this._send(path, opts)
|
|
350
|
+
if (res?.status !== 409) return res
|
|
351
|
+
|
|
352
|
+
// ⭐ THE WORKSPACE A REQUEST NAMES IS CHECKED BY THE BACKEND, NOT HERE. A request that
|
|
353
|
+
// names the wrong one for an existing site is refused with `409 wrong_workspace`,
|
|
354
|
+
// naming a workspace the caller CAN open that contains the site — its own unit, or a
|
|
355
|
+
// parent the caller belongs to. Two answers, and which one depends on the user:
|
|
356
|
+
// - they named it (`--org`, `--personal`) → stop and say so: the mismatch check;
|
|
357
|
+
// - they named nothing, or a stale one → adopt the backend's answer, let the caller
|
|
358
|
+
// record it, and retry ONCE.
|
|
359
|
+
// ⛔ No handle comparison anywhere: a site in `@acme/labs` is worked on from `@acme`.
|
|
360
|
+
const problem =
|
|
361
|
+
typeof res.clone === 'function' ? await res.clone().json().catch(() => null) : null
|
|
362
|
+
if (problem?.reason !== 'wrong_workspace') return res
|
|
363
|
+
const where = problem.workspace || {}
|
|
364
|
+
// A unit with no handle is named by its uuid, bare — the header takes either.
|
|
365
|
+
const answer = where.handle
|
|
366
|
+
? workspaceHandle(where.handle)
|
|
367
|
+
: typeof where.unit_uuid === 'string' && where.unit_uuid
|
|
368
|
+
? where.unit_uuid
|
|
369
|
+
: null
|
|
370
|
+
// Naming what the backend names and still refused: nothing to adopt, or to report
|
|
371
|
+
// as a mismatch. The caller reads the 409 like any other.
|
|
372
|
+
if (answer === this._workspace) return res
|
|
373
|
+
if (this._workspaceExplicit) {
|
|
374
|
+
throw new WorkspaceMismatchError({ named: this._workspace, answer })
|
|
375
|
+
}
|
|
376
|
+
this._workspace = answer
|
|
377
|
+
if (this._onWorkspaceAdopted) await this._onWorkspaceAdopted(answer)
|
|
378
|
+
return this._send(path, opts)
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
/** One request, as `request` documents it — naming the client's workspace on a site route. */
|
|
382
|
+
async _send(path, { method = 'GET', body, headers = {}, query, auth = true } = {}) {
|
|
186
383
|
const url = new URL(path, this.origin)
|
|
187
384
|
if (query) {
|
|
188
385
|
for (const [k, v] of Object.entries(query)) {
|
|
@@ -191,6 +388,7 @@ export class BackendClient {
|
|
|
191
388
|
}
|
|
192
389
|
const h = { ...headers }
|
|
193
390
|
if (auth) h.Authorization = `Bearer ${await this.token()}`
|
|
391
|
+
if (this._workspace && namesWorkspace(path)) h[WORKSPACE_HEADER] = this._workspace
|
|
194
392
|
if (body != null && h['Content-Type'] == null) {
|
|
195
393
|
if (typeof body === 'string') h['Content-Type'] = 'application/json'
|
|
196
394
|
else if (body instanceof Uint8Array || Buffer.isBuffer(body))
|
|
@@ -405,10 +603,13 @@ export class BackendClient {
|
|
|
405
603
|
* design). Callers must guard on the site's uuid for this backend (sync.json) and
|
|
406
604
|
* write the result back immediately — see `ensureSiteExists`.
|
|
407
605
|
*
|
|
408
|
-
*
|
|
606
|
+
* The owner is the workspace the client names (`setWorkspace`) — the create is where
|
|
607
|
+
* that choice is made, once.
|
|
608
|
+
*
|
|
609
|
+
* @param {{ name: string, foundation: string }} opts
|
|
409
610
|
* @returns {Promise<Response>} `{ site_content_uuid }`
|
|
410
611
|
*/
|
|
411
|
-
async createSite({ name, foundation
|
|
612
|
+
async createSite({ name, foundation } = {}) {
|
|
412
613
|
return this.request('/dev/site', {
|
|
413
614
|
method: 'POST',
|
|
414
615
|
// Both fields are REQUIRED: `info.name` is `required: true` on the model
|
|
@@ -417,35 +618,34 @@ export class BackendClient {
|
|
|
417
618
|
// render), and a site must resolve to a foundation. Adoption of the ref is
|
|
418
619
|
// best-effort on the backend, but the field itself cannot be blank — so send
|
|
419
620
|
// whatever the site declares and let the backend judge it.
|
|
420
|
-
body: JSON.stringify({ name, foundation })
|
|
421
|
-
query: { as_org: asOrg }
|
|
621
|
+
body: JSON.stringify({ name, foundation })
|
|
422
622
|
})
|
|
423
623
|
}
|
|
424
624
|
|
|
425
625
|
/** POST /dev/site/content — CREATE a site from its content lane (.uwx zip). */
|
|
426
|
-
async createSiteContent(buffer
|
|
626
|
+
async createSiteContent(buffer) {
|
|
427
627
|
return this.request('/dev/site/content', {
|
|
428
628
|
method: 'POST',
|
|
429
629
|
body: buffer,
|
|
430
|
-
query:
|
|
630
|
+
query: PUSH_QUERY
|
|
431
631
|
})
|
|
432
632
|
}
|
|
433
633
|
|
|
434
634
|
/** POST /dev/site/content/push/{uuid} — UPDATE the content lane by site uuid (.uwx zip). */
|
|
435
|
-
async updateSiteContent(uuid, buffer
|
|
635
|
+
async updateSiteContent(uuid, buffer) {
|
|
436
636
|
return this.request(`/dev/site/content/push/${encodeURIComponent(uuid)}`, {
|
|
437
637
|
method: 'POST',
|
|
438
638
|
body: buffer,
|
|
439
|
-
query:
|
|
639
|
+
query: PUSH_QUERY
|
|
440
640
|
})
|
|
441
641
|
}
|
|
442
642
|
|
|
443
643
|
/** POST /dev/site/folder/push/{uuid} — push the folder lane, keyed by the site uuid (.uwx zip). */
|
|
444
|
-
async pushFolder(uuid, buffer
|
|
644
|
+
async pushFolder(uuid, buffer) {
|
|
445
645
|
return this.request(`/dev/site/folder/push/${encodeURIComponent(uuid)}`, {
|
|
446
646
|
method: 'POST',
|
|
447
647
|
body: buffer,
|
|
448
|
-
query:
|
|
648
|
+
query: PUSH_QUERY
|
|
449
649
|
})
|
|
450
650
|
}
|
|
451
651
|
|
|
@@ -565,8 +765,17 @@ export class BackendClient {
|
|
|
565
765
|
const res = await this.request(
|
|
566
766
|
`/dev/site/status/${encodeURIComponent(uuid)}`
|
|
567
767
|
)
|
|
768
|
+
if (res.status === 409) {
|
|
769
|
+
// A refusal about the workspace — e.g. a deployment with one workspace, naming
|
|
770
|
+
// it — is an answer to report, not an absent status.
|
|
771
|
+
const refused = new Error((await refusalDetail(res)) || 'HTTP 409 Conflict')
|
|
772
|
+
refused.status = 409
|
|
773
|
+
throw refused
|
|
774
|
+
}
|
|
568
775
|
return res.ok ? await res.json().catch(() => null) : null
|
|
569
|
-
} catch {
|
|
776
|
+
} catch (err) {
|
|
777
|
+
// A mismatch or a refusal is an answer, not an absent status.
|
|
778
|
+
if (err instanceof WorkspaceMismatchError || err?.status === 409) throw err
|
|
570
779
|
return null
|
|
571
780
|
}
|
|
572
781
|
}
|
|
@@ -596,7 +805,9 @@ export function dataSchemaPath(modelName) {
|
|
|
596
805
|
return `/dev/registry/data-schemas/${encodeURIComponent(modelName)}`
|
|
597
806
|
}
|
|
598
807
|
|
|
599
|
-
/**
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
808
|
+
/**
|
|
809
|
+
* The shared push query: last-push-wins. ⛔ No `as_org` — the workspace rides the
|
|
810
|
+
* `x-uniweb-workspace` header on every site request (`BackendClient.setWorkspace`),
|
|
811
|
+
* since 2026-09-23.
|
|
812
|
+
*/
|
|
813
|
+
const PUSH_QUERY = { collision: 'force' }
|
|
@@ -44,7 +44,6 @@ import {
|
|
|
44
44
|
} from '@uniweb/build'
|
|
45
45
|
import { computeFoundationDigest } from '../utils/code-upload.js'
|
|
46
46
|
import { isNonInteractive } from '../utils/interactive.js'
|
|
47
|
-
import { publishScope } from '../utils/registry-orgs.js'
|
|
48
47
|
import { compareSemverPrecedence } from '../utils/semver-precedence.js'
|
|
49
48
|
|
|
50
49
|
/**
|
|
@@ -119,18 +118,20 @@ export function resolveLocalExtensions(siteDir, siteYml) {
|
|
|
119
118
|
/**
|
|
120
119
|
* The foundation's scoped catalog name (`@org/name`): its name — `main.js`'s
|
|
121
120
|
* `name`, else package.json's, the one rule the build reads for the schema
|
|
122
|
-
* `register` submits (`readFoundationName`)
|
|
123
|
-
*
|
|
121
|
+
* `register` submits (`readFoundationName`). ⭐ The scope is part of that name
|
|
122
|
+
* (2026-09-22), so a scoped name IS the catalog name. Null otherwise; the caller
|
|
124
123
|
* then treats the foundation as unreleased, and `register` — which it runs to
|
|
125
|
-
* release — is where a missing scope is
|
|
124
|
+
* release — is where a missing scope is chosen and written into the name, and a
|
|
125
|
+
* missing name is asked for.
|
|
126
126
|
*
|
|
127
127
|
* ⛔ A NAME THAT CANNOT REGISTER IS NULL, not looked up. `src` and `foundation`
|
|
128
128
|
* name a folder (`checkFoundationName`); looking one up would find some other
|
|
129
129
|
* project's `@org/src` — the very collision refusing them prevents.
|
|
130
130
|
*
|
|
131
|
-
* ⛔
|
|
132
|
-
*
|
|
133
|
-
*
|
|
131
|
+
* ⛔ A BARE NAME IS NULL too: it has not registered, since `register` writes the scope
|
|
132
|
+
* it registers under into the name. Until 2026-09-22 this joined a bare name with
|
|
133
|
+
* `package.json::uniweb.scope`; that key is refused now (`readFoundationName` throws,
|
|
134
|
+
* so a leftover reads as unreleased here and `register` says what to write instead).
|
|
134
135
|
* *(This read package.json alone until 2026-09-21 — `uniweb.id`, else `name` — and
|
|
135
136
|
* so named a foundation differently from the build whenever `main.js` named it.)*
|
|
136
137
|
*
|
|
@@ -145,9 +146,7 @@ export async function foundationScopedName(dir) {
|
|
|
145
146
|
return null
|
|
146
147
|
}
|
|
147
148
|
if (checkFoundationName(name)) return null
|
|
148
|
-
|
|
149
|
-
const scope = publishScope(readPkgField(dir, 'uniweb')?.scope)
|
|
150
|
-
return scope ? `${scope}/${name}` : null
|
|
149
|
+
return name.startsWith('@') ? name : null
|
|
151
150
|
}
|
|
152
151
|
|
|
153
152
|
function readPkgField(dir, field) {
|
|
@@ -303,9 +302,9 @@ async function bringLocalCodeAlong({
|
|
|
303
302
|
// exists to avoid.
|
|
304
303
|
//
|
|
305
304
|
// ⚠️ It is null for a foundation that has NEVER been registered and carries no
|
|
306
|
-
// scope (a fresh scaffold), because the scope is what `register` writes
|
|
307
|
-
// (`
|
|
308
|
-
// `register` asks for. So the preview shows the authored value there, and the
|
|
305
|
+
// scope (a fresh scaffold), because the scope is what `register` writes into its
|
|
306
|
+
// name (`settleFoundationScope`) — and for one with no name of its own (`src`),
|
|
307
|
+
// which `register` asks for. So the preview shows the authored value there, and the
|
|
309
308
|
// first real push — which releases, and so acquires both — sends the pinned ref
|
|
310
309
|
// instead. That gap is unavoidable offline: before the first release there is
|
|
311
310
|
// no registered name to name.
|