uniweb 1.16.1 → 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 +6 -6
- package/partials/agents.md +1 -1
- package/src/commands/add.js +17 -1
- package/src/commands/register.js +29 -1
- package/src/framework-index.json +7 -7
- package/src/index.js +17 -1
- package/src/utils/foundation-name.js +33 -3
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/core": "~1.4.0",
|
|
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/
|
|
50
|
+
"@uniweb/build": "~1.7.5",
|
|
51
51
|
"@uniweb/content-reader": "~1.5.4",
|
|
52
|
-
"@uniweb/
|
|
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/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 {
|
|
@@ -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,6 +400,23 @@ 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 })
|
|
@@ -403,6 +428,7 @@ async function deriveScopeFromLogin(client, args) {
|
|
|
403
428
|
const envelope = personal || child ? await client.fetchOrgs() : null
|
|
404
429
|
const fromWorkspace = scopeOfWorkspace(ws, envelope?.account_handle ?? null, envelope?.orgs)
|
|
405
430
|
if (fromWorkspace) {
|
|
431
|
+
if (refusedReservedScope(fromWorkspace, 'The workspace you work in is')) return null
|
|
406
432
|
const named = `${colors.bright}${fromWorkspace}${colors.reset}`
|
|
407
433
|
console.error(
|
|
408
434
|
personal
|
|
@@ -427,6 +453,7 @@ async function deriveScopeFromLogin(client, args) {
|
|
|
427
453
|
accountHandle: sess?.handle || null,
|
|
428
454
|
args
|
|
429
455
|
})
|
|
456
|
+
if (derived && refusedReservedScope(`@${derived}`, 'The scope chosen is')) return null
|
|
430
457
|
return derived ? `@${derived}` : null
|
|
431
458
|
}
|
|
432
459
|
|
|
@@ -648,6 +675,7 @@ async function runRegister(args = []) {
|
|
|
648
675
|
log(` ${colors.dim}Pass --scope @org.${colors.reset}`)
|
|
649
676
|
return { exitCode: 2 }
|
|
650
677
|
}
|
|
678
|
+
if (refusedReservedScope(scope, `${scopeSource} names`)) return { exitCode: 2 }
|
|
651
679
|
const isPreview = !!output || dryRun
|
|
652
680
|
|
|
653
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(
|
|
@@ -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.'
|