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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "uniweb",
3
- "version": "1.16.1",
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/schemas": "~1.6.5",
45
- "@uniweb/core": "~1.4.0",
44
+ "@uniweb/content-writer": "~1.3.0",
46
45
  "@uniweb/semantic-parser": "~1.12.2",
47
- "@uniweb/content-writer": "~1.3.0"
46
+ "@uniweb/core": "~1.4.0",
47
+ "@uniweb/schemas": "~1.6.5"
48
48
  },
49
49
  "peerDependencies": {
50
- "@uniweb/semantic-parser": "~1.12.2",
50
+ "@uniweb/build": "~1.7.5",
51
51
  "@uniweb/content-reader": "~1.5.4",
52
- "@uniweb/build": "~1.7.4"
52
+ "@uniweb/semantic-parser": "~1.12.2"
53
53
  },
54
54
  "peerDependenciesMeta": {
55
55
  "@uniweb/build": {
@@ -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.
@@ -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(() => '{}')
@@ -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
- const suggestion = suggestFoundationName(targetDir)
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
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "schemaVersion": 1,
3
- "generatedAt": "2026-10-22T17:15:55.292Z",
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.4"
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.0"
30
+ "version": "1.1.1"
31
31
  },
32
32
  "@uniweb/press": {
33
- "version": "1.1.3"
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.4"
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.11"
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 @uniweb/business-docs and demonstrates the same foundation that ships the unipress invoice template.",
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`, shared by the build, `register`
6
- * and `push`. This module only WRITES the name: the scaffold at `create` / `add`, and
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.'