uniweb 1.16.0 → 1.16.2
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 +1 -1
- package/src/backend/workspace.js +48 -5
- package/src/commands/add.js +17 -1
- package/src/commands/register.js +44 -10
- package/src/framework-index.json +7 -7
- package/src/index.js +20 -2
- package/src/utils/foundation-name.js +33 -3
- package/src/utils/registry-auth.js +21 -4
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "uniweb",
|
|
3
|
-
"version": "1.16.
|
|
3
|
+
"version": "1.16.2",
|
|
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/
|
|
45
|
-
"@uniweb/schemas": "~1.6.5",
|
|
44
|
+
"@uniweb/content-writer": "~1.3.0",
|
|
46
45
|
"@uniweb/semantic-parser": "~1.12.2",
|
|
47
|
-
"@uniweb/
|
|
46
|
+
"@uniweb/core": "~1.4.0",
|
|
47
|
+
"@uniweb/schemas": "~1.6.5"
|
|
48
48
|
},
|
|
49
49
|
"peerDependencies": {
|
|
50
|
-
"@uniweb/build": "~1.7.
|
|
51
|
-
"@uniweb/
|
|
52
|
-
"@uniweb/
|
|
50
|
+
"@uniweb/build": "~1.7.5",
|
|
51
|
+
"@uniweb/content-reader": "~1.5.4",
|
|
52
|
+
"@uniweb/semantic-parser": "~1.12.2"
|
|
53
53
|
},
|
|
54
54
|
"peerDependenciesMeta": {
|
|
55
55
|
"@uniweb/build": {
|
package/partials/agents.md
CHANGED
|
@@ -131,7 +131,7 @@ my-project/
|
|
|
131
131
|
└── pnpm-workspace.yaml
|
|
132
132
|
```
|
|
133
133
|
|
|
134
|
-
A site is pure content. A foundation is the site's source code — that's why it lives in `src/`. The foundation's `package.json::name` is `src`, symmetric with `site` — a **workspace** name, how the site depends on it. What the foundation **registers as** (`@org/<name>`, the name sites pin) is `name` in its `main.js`, which `uniweb create` sets to the project's name. **`src` and `foundation` are never a foundation's name** — `uniweb register` refuses them, because every project in an org would register the same one.
|
|
134
|
+
A site is pure content. A foundation is the site's source code — that's why it lives in `src/`. The foundation's `package.json::name` is `src`, symmetric with `site` — a **workspace** name, how the site depends on it. What the foundation **registers as** (`@org/<name>`, the name sites pin) is `name` in its `main.js`, which `uniweb create` sets to the project's name. **`src` and `foundation` are never a foundation's name** — `uniweb register` refuses them, because every project in an org would register the same one. **Nor is any name under `@uniweb`**: that scope is the Uniweb framework's own, and `uniweb create`, `uniweb add` and `uniweb register` refuse it.
|
|
135
135
|
|
|
136
136
|
- **Foundation** (`src/`): React components. Those in `sections/` and `layouts/` are *section types* — selectable by authors via `type:`, or used for layout areas. Everything in `components/` and `utils/` is ordinary React and JS: the developer's workbench, not visible to authors.
|
|
137
137
|
- **Site** (`site/`): markdown content and configuration, plus optional records of structured content and the queries that reach them or an external data source.
|
package/src/backend/workspace.js
CHANGED
|
@@ -91,6 +91,33 @@ export function listedWorkspaces(envelope) {
|
|
|
91
91
|
.filter((w) => w.name)
|
|
92
92
|
}
|
|
93
93
|
|
|
94
|
+
/**
|
|
95
|
+
* The top-level organization of a unit named by its child handle — `@studio` for `@studio:acme`,
|
|
96
|
+
* the part before the colon (backend, 2026-10-22).
|
|
97
|
+
*
|
|
98
|
+
* @param {string} handle - `@top:part`
|
|
99
|
+
* @returns {string} `@top`
|
|
100
|
+
*/
|
|
101
|
+
export function topOfChildHandle(handle) {
|
|
102
|
+
return `@${String(handle).replace(/^@/, '').split(':')[0]}`
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* The account's own handle, as the header would name it (`@jane`), from the orgs read — or null.
|
|
107
|
+
*
|
|
108
|
+
* ⛔ **An account's handle names no workspace** (backend, 2026-10-22, pinned by its tests). A
|
|
109
|
+
* handle names a person or an org, and only an org is a workspace: the header answers
|
|
110
|
+
* `@<account handle>` with `400`, never as the personal workspace, which a request names by
|
|
111
|
+
* naming none. So `--org @<your handle>` is refused, saying what names your own.
|
|
112
|
+
*/
|
|
113
|
+
export function ownHandle(envelope) {
|
|
114
|
+
return envelope?.account_handle ? workspaceHandle(envelope.account_handle) : null
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** What naming your own handle as a workspace is told — `--personal`, or `UNIWEB_WORKSPACE=personal`. */
|
|
118
|
+
export const ownHandleNamesNone = (named, personal) =>
|
|
119
|
+
`${named} is your own handle, which names no workspace — your personal workspace is ${personal}.`
|
|
120
|
+
|
|
94
121
|
/**
|
|
95
122
|
* What a backend whose orgs read lists no workspaces is told. ⛔ The CLI does not check
|
|
96
123
|
* against `orgs` in its place: that list leaves out every unit below an org.
|
|
@@ -185,22 +212,36 @@ export async function resolveWorkspace({ client, args = [], offline = false }) {
|
|
|
185
212
|
* decides which SITE a command works on; here it only answers the question a bare name
|
|
186
213
|
* asks the first time it registers.
|
|
187
214
|
*
|
|
215
|
+
* ⭐ **A unit below an organization — a sub-org, a project — takes its organization's scope**
|
|
216
|
+
* *[Diego, 2026-10-22]*: its child handle (`@studio:acme`) names no scope (backend, the same day),
|
|
217
|
+
* and a bare name registered from it defaults to the top-level org's handle, `@studio`, when that
|
|
218
|
+
* org is among the account's organizations — the orgs read's `orgs`, the scopes. Not otherwise: a
|
|
219
|
+
* project's guest belongs to the project alone and has only their own scope, and so do the people
|
|
220
|
+
* of a sub-org whose belonging stops there — which the orgs read cannot tell apart, so membership is
|
|
221
|
+
* read from `orgs` and nothing else.
|
|
222
|
+
*
|
|
188
223
|
* Null when the workspace names no scope, and the caller derives one as before
|
|
189
224
|
* (`deriveScope`): none is chosen (you belong to organizations and none is named), the
|
|
190
225
|
* command is a preview, the workspace is a unit without a handle, the workspace is a unit
|
|
191
|
-
* below an organization
|
|
192
|
-
*
|
|
226
|
+
* below an organization the account is not a member of, or the account has no handle (a
|
|
227
|
+
* service account).
|
|
193
228
|
*
|
|
194
229
|
* @param {{ workspace?: string|null, source?: string, refused?: boolean }} ws -
|
|
195
230
|
* `resolveWorkspace`'s answer
|
|
196
231
|
* @param {string|null} accountHandle - the account's own handle (`GET /dev/orgs`), read
|
|
197
232
|
* only for the personal workspace
|
|
233
|
+
* @param {Array<{ handle?: string }>} [orgs] - the orgs read's `orgs`, read only for a unit
|
|
234
|
+
* below an organization
|
|
198
235
|
* @returns {string|null} `@handle`
|
|
199
236
|
*/
|
|
200
|
-
export function scopeOfWorkspace(ws, accountHandle) {
|
|
237
|
+
export function scopeOfWorkspace(ws, accountHandle, orgs = []) {
|
|
201
238
|
if (!ws || ws.refused || ws.source === 'offline') return null
|
|
202
239
|
if (typeof ws.workspace === 'string') {
|
|
203
|
-
|
|
240
|
+
if (!ws.workspace.startsWith('@')) return null
|
|
241
|
+
if (!ws.workspace.includes(':')) return ws.workspace
|
|
242
|
+
const top = topOfChildHandle(ws.workspace)
|
|
243
|
+
const member = (Array.isArray(orgs) ? orgs : []).some((o) => `@${bareHandle(o?.handle)}` === top)
|
|
244
|
+
return member ? top : null
|
|
204
245
|
}
|
|
205
246
|
const h = bareHandle(accountHandle || '')
|
|
206
247
|
return h && !validateHandle(h) ? `@${h}` : null
|
|
@@ -228,12 +269,14 @@ export async function chooseWorkspace({ apiBase, token, args = [] }) {
|
|
|
228
269
|
const flag = readOrgFlag(args)
|
|
229
270
|
if (!flag && args.includes('--personal')) return { choice: PERSONAL }
|
|
230
271
|
|
|
231
|
-
const
|
|
272
|
+
const envelope = await fetchOrgs({ apiBase, token })
|
|
273
|
+
const listed = listedWorkspaces(envelope)
|
|
232
274
|
if (!listed) return { refused: true, reason: noWorkspaceList(apiBase) }
|
|
233
275
|
const mine = listed.map((w) => w.name)
|
|
234
276
|
if (flag) {
|
|
235
277
|
const named = namedWorkspace(flag)
|
|
236
278
|
if (!named) return { refused: true, reason: `--org ${flag} names no workspace — give @org, or --personal.` }
|
|
279
|
+
if (named === ownHandle(envelope)) return { refused: true, reason: ownHandleNamesNone(named, '--personal') }
|
|
237
280
|
if (!mine.includes(named)) {
|
|
238
281
|
return {
|
|
239
282
|
refused: true,
|
package/src/commands/add.js
CHANGED
|
@@ -46,7 +46,8 @@ import {
|
|
|
46
46
|
import { findWorkspaceRoot } from '../utils/workspace.js'
|
|
47
47
|
import {
|
|
48
48
|
normalizeFoundationName,
|
|
49
|
-
ensureFoundationName
|
|
49
|
+
ensureFoundationName,
|
|
50
|
+
reservedScopeIn
|
|
50
51
|
} from '../utils/foundation-name.js'
|
|
51
52
|
import {
|
|
52
53
|
detectPackageManager,
|
|
@@ -305,6 +306,21 @@ export async function add(rawArgs) {
|
|
|
305
306
|
parsed = parseArgs(args)
|
|
306
307
|
}
|
|
307
308
|
|
|
309
|
+
// ⛔ A FOUNDATION NAMED UNDER THE RESERVED SCOPE IS REFUSED before anything is placed
|
|
310
|
+
// [Diego, 2026-10-23]. The name read as `@scope/name` would otherwise be a folder
|
|
311
|
+
// path, and the foundation named `name` — the scope dropped in silence.
|
|
312
|
+
const reserved = ['project', 'foundation', 'extension'].includes(parsed.subcommand)
|
|
313
|
+
? reservedScopeIn(parsed.name)
|
|
314
|
+
: null
|
|
315
|
+
if (reserved) {
|
|
316
|
+
error(`${reserved.problem}.`)
|
|
317
|
+
log(
|
|
318
|
+
`Add it without the scope — ${colors.cyan}${prefix} add ${parsed.subcommand} ${reserved.bare}${colors.reset} — and ` +
|
|
319
|
+
`${colors.cyan}${prefix} register${colors.reset} gives it the scope of the workspace you work in.`
|
|
320
|
+
)
|
|
321
|
+
process.exit(1)
|
|
322
|
+
}
|
|
323
|
+
|
|
308
324
|
// Read root package.json for project name
|
|
309
325
|
const rootPkg = JSON.parse(
|
|
310
326
|
await readFile(join(rootDir, 'package.json'), 'utf-8').catch(() => '{}')
|
package/src/commands/register.js
CHANGED
|
@@ -76,6 +76,7 @@ import {
|
|
|
76
76
|
collectStandaloneSchemas,
|
|
77
77
|
readFoundationName,
|
|
78
78
|
checkFoundationName,
|
|
79
|
+
checkRegistrationScope,
|
|
79
80
|
splitFoundationName
|
|
80
81
|
} from '@uniweb/build'
|
|
81
82
|
import {
|
|
@@ -90,7 +91,7 @@ import {
|
|
|
90
91
|
} from '../utils/code-upload.js'
|
|
91
92
|
import { deriveScope, publishScope, belongsToScope } from '../utils/registry-orgs.js'
|
|
92
93
|
import { BackendClient } from '../backend/client.js'
|
|
93
|
-
import { resolveWorkspace, scopeOfWorkspace, SOURCE_LABEL } from '../backend/workspace.js'
|
|
94
|
+
import { resolveWorkspace, scopeOfWorkspace, topOfChildHandle, SOURCE_LABEL } from '../backend/workspace.js'
|
|
94
95
|
import { writeJsonPreservingStyleAsync } from '../utils/json-file.js'
|
|
95
96
|
import {
|
|
96
97
|
findWorkspaceRoot,
|
|
@@ -252,7 +253,12 @@ export async function settleFoundationName(targetDir, { args, isPreview }) {
|
|
|
252
253
|
if (!problem) return true
|
|
253
254
|
|
|
254
255
|
const where = relative(process.cwd(), read.mainFile) || read.mainFile
|
|
255
|
-
|
|
256
|
+
// A name refused for its reserved scope keeps the rest of itself: `@uniweb/marketing`
|
|
257
|
+
// suggests `marketing`, which registers under the scope of the workspace you work in.
|
|
258
|
+
const { scope: nameScope, bare } = splitFoundationName(read.name)
|
|
259
|
+
const suggestion =
|
|
260
|
+
(nameScope && checkRegistrationScope(nameScope) && !checkFoundationName(bare) ? bare : null) ||
|
|
261
|
+
suggestFoundationName(targetDir)
|
|
256
262
|
const reason = `${problem[0].toUpperCase()}${problem.slice(1)}.`
|
|
257
263
|
|
|
258
264
|
// Nobody to ask — a script, a preview (which writes nothing), or --json (whose
|
|
@@ -334,6 +340,7 @@ export async function settleFoundationScope(targetDir, { args, isPreview, flagSc
|
|
|
334
340
|
const { scope: nameScope, bare } = splitFoundationName(read.name)
|
|
335
341
|
|
|
336
342
|
if (nameScope) {
|
|
343
|
+
if (refusedReservedScope(nameScope, `${where} names this foundation ${read.name}, under`)) return null
|
|
337
344
|
if (flagScope && flagScope !== nameScope) {
|
|
338
345
|
error(
|
|
339
346
|
`${where} names this foundation ${read.name}, so it registers under ${nameScope} — ` +
|
|
@@ -352,6 +359,7 @@ export async function settleFoundationScope(targetDir, { args, isPreview, flagSc
|
|
|
352
359
|
|
|
353
360
|
let scope = flagScope
|
|
354
361
|
let source = flagScope ? '--scope' : null
|
|
362
|
+
if (refusedReservedScope(scope, '--scope names')) return null
|
|
355
363
|
if (!scope) {
|
|
356
364
|
scope = await deriveScopeFromLogin(client, args)
|
|
357
365
|
if (!scope) return null
|
|
@@ -392,26 +400,50 @@ export async function settleFoundationScope(targetDir, { args, isPreview, flagSc
|
|
|
392
400
|
* the scope is derived from your orgs as before (`deriveScope`): your personal scope when
|
|
393
401
|
* you belong to none, else a pick at a terminal, your personal scope without one.
|
|
394
402
|
*/
|
|
403
|
+
/**
|
|
404
|
+
* Refuse a reserved scope wherever it came from, before anything is written or sent —
|
|
405
|
+
* `@uniweb` names the framework's own packages and a system account on every Uniweb
|
|
406
|
+
* backend [Diego, 2026-10-23] (`checkRegistrationScope`).
|
|
407
|
+
*
|
|
408
|
+
* @param {string|null} scope
|
|
409
|
+
* @param {string} lead - where the scope came from, said before it: `--scope names`
|
|
410
|
+
* @returns {boolean} true when refused — the reason printed
|
|
411
|
+
*/
|
|
412
|
+
function refusedReservedScope(scope, lead) {
|
|
413
|
+
const problem = scope ? checkRegistrationScope(scope) : null
|
|
414
|
+
if (!problem) return false
|
|
415
|
+
error(`${lead} ${scope}. ${problem}.`)
|
|
416
|
+
log(` ${colors.dim}Register under your own scope or an organization's: --scope @org.${colors.reset}`)
|
|
417
|
+
return true
|
|
418
|
+
}
|
|
419
|
+
|
|
395
420
|
async function deriveScopeFromLogin(client, args) {
|
|
396
421
|
const token = await client.token()
|
|
397
422
|
const ws = await resolveWorkspace({ client, args })
|
|
398
|
-
const
|
|
399
|
-
const
|
|
400
|
-
|
|
423
|
+
const live = !ws.refused && ws.source !== 'offline'
|
|
424
|
+
const personal = live && ws.workspace === null
|
|
425
|
+
// A unit below an organization — a sub-org, a project — names no scope; its organization's is
|
|
426
|
+
// the default for the organization's members (`scopeOfWorkspace`), read from the same answer.
|
|
427
|
+
const child = live && typeof ws.workspace === 'string' && ws.workspace.includes(':')
|
|
428
|
+
const envelope = personal || child ? await client.fetchOrgs() : null
|
|
429
|
+
const fromWorkspace = scopeOfWorkspace(ws, envelope?.account_handle ?? null, envelope?.orgs)
|
|
401
430
|
if (fromWorkspace) {
|
|
431
|
+
if (refusedReservedScope(fromWorkspace, 'The workspace you work in is')) return null
|
|
402
432
|
const named = `${colors.bright}${fromWorkspace}${colors.reset}`
|
|
403
433
|
console.error(
|
|
404
434
|
personal
|
|
405
435
|
? `Registering under your personal scope ${named} — you work in your personal workspace (${SOURCE_LABEL[ws.source]}). For an org: --scope @org.`
|
|
406
|
-
:
|
|
436
|
+
: child
|
|
437
|
+
? `Registering under ${named} — the organization of ${ws.workspace}, the workspace you work in (${SOURCE_LABEL[ws.source]}). For another scope: --scope @org.`
|
|
438
|
+
: `Registering under ${named} — the workspace you work in (${SOURCE_LABEL[ws.source]}). For another scope: --scope @org.`
|
|
407
439
|
)
|
|
408
440
|
return fromWorkspace
|
|
409
441
|
}
|
|
410
|
-
//
|
|
411
|
-
//
|
|
412
|
-
if (
|
|
442
|
+
// Not a member of the unit's organization — a project's guest — so it gives no scope, and a bare
|
|
443
|
+
// name is asked for one as when no workspace names any.
|
|
444
|
+
if (child) {
|
|
413
445
|
console.error(
|
|
414
|
-
`${colors.dim}${ws.workspace}, the workspace you work in,
|
|
446
|
+
`${colors.dim}${ws.workspace}, the workspace you work in, names no scope, and ${topOfChildHandle(ws.workspace)} is not among your organizations.${colors.reset}`
|
|
415
447
|
)
|
|
416
448
|
}
|
|
417
449
|
const sess = await readRegistryAuth(client.origin)
|
|
@@ -421,6 +453,7 @@ async function deriveScopeFromLogin(client, args) {
|
|
|
421
453
|
accountHandle: sess?.handle || null,
|
|
422
454
|
args
|
|
423
455
|
})
|
|
456
|
+
if (derived && refusedReservedScope(`@${derived}`, 'The scope chosen is')) return null
|
|
424
457
|
return derived ? `@${derived}` : null
|
|
425
458
|
}
|
|
426
459
|
|
|
@@ -642,6 +675,7 @@ async function runRegister(args = []) {
|
|
|
642
675
|
log(` ${colors.dim}Pass --scope @org.${colors.reset}`)
|
|
643
676
|
return { exitCode: 2 }
|
|
644
677
|
}
|
|
678
|
+
if (refusedReservedScope(scope, `${scopeSource} names`)) return { exitCode: 2 }
|
|
645
679
|
const isPreview = !!output || dryRun
|
|
646
680
|
|
|
647
681
|
// Each path supplies a different schema source: the standalone path discovers
|
package/src/framework-index.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schemaVersion": 1,
|
|
3
|
-
"generatedAt": "2026-10-
|
|
3
|
+
"generatedAt": "2026-10-23T16:20:14.706Z",
|
|
4
4
|
"packages": {
|
|
5
5
|
"@uniweb/api": {
|
|
6
6
|
"version": "1.6.4"
|
|
7
7
|
},
|
|
8
8
|
"@uniweb/build": {
|
|
9
|
-
"version": "1.7.
|
|
9
|
+
"version": "1.7.5"
|
|
10
10
|
},
|
|
11
11
|
"@uniweb/content-reader": {
|
|
12
12
|
"version": "1.5.4"
|
|
@@ -27,10 +27,10 @@
|
|
|
27
27
|
"version": "1.6.0"
|
|
28
28
|
},
|
|
29
29
|
"@uniweb/loom": {
|
|
30
|
-
"version": "1.1.
|
|
30
|
+
"version": "1.1.1"
|
|
31
31
|
},
|
|
32
32
|
"@uniweb/press": {
|
|
33
|
-
"version": "1.1.
|
|
33
|
+
"version": "1.1.4"
|
|
34
34
|
},
|
|
35
35
|
"@uniweb/projections": {
|
|
36
36
|
"version": "1.1.6"
|
|
@@ -54,13 +54,13 @@
|
|
|
54
54
|
"version": "1.1.0"
|
|
55
55
|
},
|
|
56
56
|
"@uniweb/templates": {
|
|
57
|
-
"version": "1.1.
|
|
57
|
+
"version": "1.1.5"
|
|
58
58
|
},
|
|
59
59
|
"@uniweb/theming": {
|
|
60
60
|
"version": "1.2.1"
|
|
61
61
|
},
|
|
62
62
|
"@uniweb/unipress": {
|
|
63
|
-
"version": "1.1.
|
|
63
|
+
"version": "1.1.12"
|
|
64
64
|
}
|
|
65
65
|
},
|
|
66
66
|
"templates": {
|
|
@@ -198,7 +198,7 @@
|
|
|
198
198
|
},
|
|
199
199
|
"business-docs": {
|
|
200
200
|
"name": "Business Documents",
|
|
201
|
-
"description": "A complete business-document site: invoice and SOW records, pages listing them, and a filtered engagement report with XLSX export. Built on @
|
|
201
|
+
"description": "A complete business-document site: invoice and SOW records, pages listing them, and a filtered engagement report with XLSX export. Built on @unipress/business-docs and demonstrates the same foundation that ships the unipress invoice template.",
|
|
202
202
|
"tags": [
|
|
203
203
|
"business-docs",
|
|
204
204
|
"invoice",
|
package/src/index.js
CHANGED
|
@@ -54,7 +54,8 @@ import {
|
|
|
54
54
|
} from './utils/scaffold.js'
|
|
55
55
|
import {
|
|
56
56
|
normalizeFoundationName,
|
|
57
|
-
ensureFoundationName
|
|
57
|
+
ensureFoundationName,
|
|
58
|
+
reservedScopeIn
|
|
58
59
|
} from './utils/foundation-name.js'
|
|
59
60
|
import {
|
|
60
61
|
detectPackageManager,
|
|
@@ -1171,6 +1172,21 @@ async function main() {
|
|
|
1171
1172
|
await exitWhenDrained(1)
|
|
1172
1173
|
}
|
|
1173
1174
|
|
|
1175
|
+
// ⛔ A NAME UNDER THE RESERVED SCOPE IS REFUSED before anything is asked or written
|
|
1176
|
+
// [Diego, 2026-10-23]. The foundation's name is the project's without its scope, so
|
|
1177
|
+
// `@uniweb/marketing` would be dropped to `marketing` in silence — read as granted
|
|
1178
|
+
// until `register` refused it.
|
|
1179
|
+
const reserved = reservedScopeIn(projectName)
|
|
1180
|
+
if (reserved) {
|
|
1181
|
+
error(`${reserved.problem}.`)
|
|
1182
|
+
const again = inPlace ? `create . --name ${reserved.bare}` : `create ${reserved.bare}`
|
|
1183
|
+
log(
|
|
1184
|
+
`Create it without the scope — ${colors.cyan}${prefix} ${again}${colors.reset} — and ` +
|
|
1185
|
+
`${colors.cyan}${prefix} register${colors.reset} gives its foundation the scope of the workspace you work in.`
|
|
1186
|
+
)
|
|
1187
|
+
await exitWhenDrained(1)
|
|
1188
|
+
}
|
|
1189
|
+
|
|
1174
1190
|
// Prompt for template if not specified via --template or --blank
|
|
1175
1191
|
if (!templateType && !isBlank) {
|
|
1176
1192
|
const templateResponse = await prompts(
|
|
@@ -1759,7 +1775,9 @@ ${colors.bright}Options:${colors.reset}
|
|
|
1759
1775
|
org's — and write it into the name (refused when the name has another
|
|
1760
1776
|
scope). A schemas-only package: publish under @scope; default:
|
|
1761
1777
|
package.json uniweb.scope
|
|
1762
|
-
--org @org Work in @org for this command: a bare name registers under it
|
|
1778
|
+
--org @org Work in @org for this command: a bare name registers under it —
|
|
1779
|
+
a sub-org's or project's (@studio:acme): under its organization's,
|
|
1780
|
+
when you are a member of it
|
|
1763
1781
|
--personal Work in your personal workspace: a bare name registers under your
|
|
1764
1782
|
personal scope
|
|
1765
1783
|
--dry-run Print the .uwx; submit nothing
|
|
@@ -2,9 +2,10 @@
|
|
|
2
2
|
* Giving a foundation its name — `name` in `main.js`'s default export.
|
|
3
3
|
*
|
|
4
4
|
* The rule itself (main.js `name`, else package.json `name`; `src` and `foundation`
|
|
5
|
-
* refused) is `@uniweb/build`'s `foundation-name.js`,
|
|
6
|
-
* and `push`. This module only WRITES the name: the
|
|
7
|
-
* `register` when it asks for one
|
|
5
|
+
* refused, and so is the `@uniweb` scope) is `@uniweb/build`'s `foundation-name.js`,
|
|
6
|
+
* shared by the build, `register` and `push`. This module only WRITES the name: the
|
|
7
|
+
* scaffold at `create` / `add`, and `register` when it asks for one — and refuses, at
|
|
8
|
+
* `create` / `add`, a name that asks for the reserved scope (`reservedScopeIn`).
|
|
8
9
|
*
|
|
9
10
|
* ⛔ NO `@uniweb/build` IMPORT, AND NO IMPORT OF main.js. `uniweb create` runs before
|
|
10
11
|
* any project exists: `@uniweb/build` is an optional peer that may not be installed,
|
|
@@ -21,6 +22,35 @@ import { join, resolve, basename, dirname } from 'node:path'
|
|
|
21
22
|
// (`@uniweb/build`'s FORBIDDEN_FOUNDATION_NAMES; the test keeps them equal).
|
|
22
23
|
export const SCAFFOLD_FOLDER_NAMES = new Set(['src', 'foundation'])
|
|
23
24
|
|
|
25
|
+
// Scopes no foundation is named under — `@uniweb`, the framework's own (`@uniweb/build`'s
|
|
26
|
+
// RESERVED_SCOPES and its reason, which the test keeps equal).
|
|
27
|
+
export const RESERVED_SCOPES = new Set(['uniweb'])
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* The reserved scope a name asks for — `@uniweb/marketing` asks for `@uniweb` — with why
|
|
31
|
+
* it is refused and the name without it. Null when the name asks for no reserved scope.
|
|
32
|
+
*
|
|
33
|
+
* `create` and `add` drop any scope from the name they write, so this is for refusing
|
|
34
|
+
* what was ASKED for, before anything is written: a scope dropped in silence would read
|
|
35
|
+
* as granted until `register` refused it.
|
|
36
|
+
*
|
|
37
|
+
* @param {unknown} raw - a name as typed
|
|
38
|
+
* @returns {{ scope: string, bare: string, problem: string } | null}
|
|
39
|
+
*/
|
|
40
|
+
export function reservedScopeIn(raw) {
|
|
41
|
+
const text = String(raw ?? '').trim()
|
|
42
|
+
const scoped = /^@([^/]+)\/(.+)$/.exec(text)
|
|
43
|
+
if (!scoped || !RESERVED_SCOPES.has(scoped[1].toLowerCase())) return null
|
|
44
|
+
const scope = `@${scoped[1].toLowerCase()}`
|
|
45
|
+
return {
|
|
46
|
+
scope,
|
|
47
|
+
bare: scoped[2],
|
|
48
|
+
problem:
|
|
49
|
+
`"${text}" is named under ${scope}, which is reserved: it names the Uniweb framework's ` +
|
|
50
|
+
'own packages and the system account of every Uniweb backend, so nothing registers under it'
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
24
54
|
// Written above the name wherever the CLI adds one — the scaffold's main.js
|
|
25
55
|
// template says the same.
|
|
26
56
|
const NAME_COMMENT = '// What this foundation registers as — @<org>/<name> — and what sites pin.'
|
|
@@ -952,8 +952,16 @@ async function reportTokenProcess({ named, token }) {
|
|
|
952
952
|
// Their workspace is UNIWEB_WORKSPACE's — never the stored session's — and, with none
|
|
953
953
|
// named, personal for an account in no organization or project (resolveWorkspace,
|
|
954
954
|
// workspace.js). Checked, as a login's `--org` is, against the workspaces the orgs read lists.
|
|
955
|
-
const {
|
|
956
|
-
|
|
955
|
+
const {
|
|
956
|
+
WORKSPACE_ENV,
|
|
957
|
+
PERSONAL,
|
|
958
|
+
parseWorkspaceChoice,
|
|
959
|
+
headerOf,
|
|
960
|
+
describeWorkspace,
|
|
961
|
+
listedWorkspaces,
|
|
962
|
+
noWorkspaceList,
|
|
963
|
+
ownHandle
|
|
964
|
+
} = await import('../backend/workspace.js')
|
|
957
965
|
const unsettled = (reason, { commandsRefuse }) => {
|
|
958
966
|
console.error(`\x1b[31m✗\x1b[0m ${reason}`)
|
|
959
967
|
if (commandsRefuse) {
|
|
@@ -968,18 +976,27 @@ async function reportTokenProcess({ named, token }) {
|
|
|
968
976
|
commandsRefuse: true
|
|
969
977
|
})
|
|
970
978
|
}
|
|
971
|
-
let
|
|
979
|
+
let envelope
|
|
972
980
|
try {
|
|
973
|
-
|
|
981
|
+
envelope = await (await import('./registry-orgs.js')).fetchOrgs({ apiBase: origin, token })
|
|
974
982
|
} catch (err) {
|
|
975
983
|
console.error(`\x1b[33m⚠\x1b[0m ${err.message} — the workspace is not checked.`)
|
|
976
984
|
return
|
|
977
985
|
}
|
|
986
|
+
const listed = listedWorkspaces(envelope)
|
|
978
987
|
if (!listed) {
|
|
979
988
|
console.error(`\x1b[33m⚠\x1b[0m ${noWorkspaceList(origin)}`)
|
|
980
989
|
return
|
|
981
990
|
}
|
|
982
991
|
const mine = listed.map((w) => w.name)
|
|
992
|
+
if (choice && choice === ownHandle(envelope)) {
|
|
993
|
+
// As a login refuses `--org @<own handle>` (chooseWorkspace): a handle names a person or an org,
|
|
994
|
+
// and only an org is a workspace.
|
|
995
|
+
unsettled(
|
|
996
|
+
`${WORKSPACE_ENV}=${raw} names the account's own handle, which names no workspace — its personal workspace is ${WORKSPACE_ENV}=personal.`,
|
|
997
|
+
{ commandsRefuse: false }
|
|
998
|
+
)
|
|
999
|
+
}
|
|
983
1000
|
if (choice && choice !== PERSONAL && !mine.includes(choice)) {
|
|
984
1001
|
// As a login refuses `--org` for one (chooseWorkspace).
|
|
985
1002
|
unsettled(`${WORKSPACE_ENV}=${raw} is not one of this account's workspaces on ${origin}.`, {
|