uniweb 0.61.0 → 0.63.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.
@@ -3,15 +3,14 @@
3
3
  * standalone schemas package (the data schemas alone, no foundation), to the
4
4
  * registry as one names-only `.uwx` document (uwx-format.md §5).
5
5
  *
6
- * `uniweb login && uniweb register`. Distinct from `uniweb publish` (which
7
- * targets the legacy platform) — `register` talks to the registry over HTTP at a
8
- * configurable endpoint.
6
+ * `uniweb login && uniweb register`. Distinct from `uniweb publish`, which ships a
7
+ * SITE — and releases the site's local foundation by running this command
8
+ * (`backend/foundation-bring-along.js`).
9
9
  *
10
- * If the foundation's `dist/` is missing or version-stale (the baked schema
11
- * version differs from package.json), `register` builds it first — the same
12
- * build-if-stale `uniweb publish` does, so `register` is a full drop-in.
13
- * Preview paths (`--dry-run`, `-o`) never write to `dist/`; they require a
14
- * pre-built foundation.
10
+ * If the foundation's `dist/` is missing or stale — no build, a version bump
11
+ * since it, a source file newer than it, no SSR bundle (`foundationNeedsBuild`)
12
+ * — `register` builds it first. Preview paths (`--dry-run`, `-o`) never write to
13
+ * `dist/`; they require a pre-built foundation.
15
14
  *
16
15
  * Run from a foundation, or from a schemas-only package — a package that exports
17
16
  * schemas (e.g. `@uniweb/schemas`, any `@org/schemas`) or a bare `schemas/*.yml`
@@ -22,8 +21,13 @@
22
21
  * uniweb register Build the .uwx, submit it, then deliver
23
22
  * the foundation's dist/ code (plan +
24
23
  * upload — see utils/code-upload.js)
25
- * uniweb register --scope @org Publish under @org (resolves @/x -> @org/x).
26
- * Default: the package's package.json "uniweb.scope".
24
+ * uniweb register --scope @org A foundation whose name is bare: register it under
25
+ * @org (resolves @/x -> @org/x) and write the scope
26
+ * into its name in main.js. Refused when the name
27
+ * already carries another scope — a foundation's
28
+ * scope is the one in its name.
29
+ * A schemas-only package: publish under @org.
30
+ * Default: its package.json "uniweb.scope".
27
31
  * uniweb register --schema-only Skip the code delivery (schemas land, no dist upload)
28
32
  * uniweb register --dry-run Print the .uwx + the code file plan; submit nothing
29
33
  * uniweb register -o foundation.uwx Write the .uwx to a file; submit nothing
@@ -64,13 +68,21 @@ import {
64
68
  statSync
65
69
  } from 'node:fs'
66
70
  import { execSync } from 'node:child_process'
67
- import { resolve, join } from 'node:path'
71
+ import { resolve, join, relative } from 'node:path'
72
+ import prompts from 'prompts'
68
73
  import { buildRegistryPackage, buildSchemaOnlyPackage } from '@uniweb/build/uwx'
69
74
  import {
70
75
  classifyPackage,
71
76
  isSchemasPackage,
72
- collectStandaloneSchemas
77
+ collectStandaloneSchemas,
78
+ readFoundationName,
79
+ checkFoundationName,
80
+ splitFoundationName
73
81
  } from '@uniweb/build'
82
+ import {
83
+ suggestFoundationName,
84
+ writeFoundationName
85
+ } from '../utils/foundation-name.js'
74
86
  import { readRegistryAuth } from '../utils/registry-auth.js'
75
87
  import {
76
88
  collectDistFiles,
@@ -137,8 +149,10 @@ function cliVersion() {
137
149
  }
138
150
  }
139
151
 
140
- // The foundation's recorded publish org, from its package.json `uniweb.scope`
141
- // (`{ "uniweb": { "scope": "@acme" } }`) — the default when `--scope` is absent.
152
+ // A SCHEMAS-ONLY package's recorded publish org, from its package.json `uniweb.scope`
153
+ // (`{ "uniweb": { "scope": "@acme" } }`) — the default when `--scope` is absent. It
154
+ // has no main.js whose name could carry it. ⛔ A foundation's scope is part of its name
155
+ // (2026-09-22); its `uniweb.scope` is refused by `readFoundationName`.
142
156
  function readPkgScope(foundationDir) {
143
157
  try {
144
158
  const pkg = JSON.parse(
@@ -150,9 +164,10 @@ function readPkgScope(foundationDir) {
150
164
  }
151
165
  }
152
166
 
153
- // Record the chosen publish scope in the foundation's package.json so it travels
154
- // with the foundation (read back by readPkgScope on the next register). Preserves
155
- // the file's existing JSON style.
167
+ // Record the chosen publish scope in a schemas-only package's package.json so it
168
+ // travels with the package (read back by readPkgScope on the next register). Preserves
169
+ // the file's existing JSON style. A foundation's goes into its name instead
170
+ // (`settleFoundationScope`).
156
171
  async function writePkgScope(foundationDir, scope) {
157
172
  const path = join(foundationDir, 'package.json')
158
173
  const pkg = JSON.parse(readFileSync(path, 'utf8'))
@@ -209,6 +224,173 @@ async function resolveFoundationDir(args) {
209
224
  process.exit(1)
210
225
  }
211
226
 
227
+ /**
228
+ * The foundation's name, settled before anything is built or sent.
229
+ *
230
+ * A foundation registers as `@org/<name>`, and sites pin it by that name. The name
231
+ * is `main.js`'s `name`, else package.json's — `readFoundationName`, the rule the
232
+ * build reads for the schema this command submits. ⛔ `src` and `foundation` are
233
+ * the folder the code sits in, not the code: a project scaffolded before the name
234
+ * moved to main.js has package name `src`, and every such project in an org would
235
+ * register the same `@org/src`. So a name that cannot register is ASKED FOR here,
236
+ * once, and kept in main.js — or, with nobody to ask, refused with the line to add.
237
+ * Before the build, so a written name is built into the schema like any edit.
238
+ *
239
+ * @param {string} targetDir
240
+ * @param {{ args: string[], isPreview: boolean }} o
241
+ * @returns {Promise<boolean>} false → stop; the reason is printed
242
+ */
243
+ export async function settleFoundationName(targetDir, { args, isPreview }) {
244
+ let read
245
+ try {
246
+ read = await readFoundationName(targetDir)
247
+ } catch (err) {
248
+ error(err.message)
249
+ return false
250
+ }
251
+ const problem = checkFoundationName(read.name)
252
+ if (!problem) return true
253
+
254
+ const where = relative(process.cwd(), read.mainFile) || read.mainFile
255
+ const suggestion = suggestFoundationName(targetDir)
256
+ const reason = `${problem[0].toUpperCase()}${problem.slice(1)}.`
257
+
258
+ // Nobody to ask — a script, a preview (which writes nothing), or --json (whose
259
+ // stdout is a data channel a prompt would corrupt). Name the fix exactly.
260
+ if (isPreview || jsonMode || isNonInteractive(args)) {
261
+ error(`This foundation has no name it can register under. ${reason}`)
262
+ log(` Name it in ${colors.bright}${where}${colors.reset}, in the default export:`)
263
+ log(` ${colors.bright}name: '${suggestion || '<name>'}',${colors.reset}`)
264
+ log(` ${colors.dim}It registers as @<org>/<name>, and sites pin that name.${colors.reset}`)
265
+ return false
266
+ }
267
+
268
+ info(`This foundation needs a name to register under. ${reason}`)
269
+ const { name } = await prompts(
270
+ {
271
+ type: 'text',
272
+ name: 'name',
273
+ message: 'Foundation name (it registers as @<org>/<name>):',
274
+ initial: suggestion || '',
275
+ validate: (v) => checkFoundationName(String(v).trim()) || true
276
+ },
277
+ {
278
+ onCancel: () => {
279
+ log('\nRegister cancelled.')
280
+ process.exit(0)
281
+ }
282
+ }
283
+ )
284
+ const chosen = String(name || '').trim()
285
+ if (!chosen) return false
286
+
287
+ const written = writeFoundationName(read.mainFile, chosen, {
288
+ replace: read.source === 'main.js' ? read.name : null
289
+ })
290
+ if (!written.ok) {
291
+ error(`Could not write the name: ${written.reason}.`)
292
+ log(` Add it to the default export in ${colors.bright}${where}${colors.reset}: name: '${chosen}',`)
293
+ return false
294
+ }
295
+ // Read it back through the build's own rule — the text edit is only a means.
296
+ const again = await readFoundationName(targetDir).catch(() => null)
297
+ if (again?.name !== chosen) {
298
+ error(`${where} now names the foundation '${chosen}', but it still reads as "${again?.name}".`)
299
+ return false
300
+ }
301
+ success(`Named the foundation ${colors.bright}${chosen}${colors.reset} — kept in ${where}.`)
302
+ return true
303
+ }
304
+
305
+ /**
306
+ * Settle the scope a foundation registers under — the one in its name.
307
+ *
308
+ * ⭐ THE SCOPE IS PART OF THE NAME (2026-09-22): `name: '@acme/marketing'` in main.js.
309
+ * A scoped name decides it, and a `--scope` naming another org is refused — which org
310
+ * was meant is not ours to guess, and today's alternative was a split identity (the
311
+ * foundation under one org, its data schemas under the other). A bare name has not
312
+ * registered yet: it takes `--scope`, else one derived from your orgs, and the chosen
313
+ * scope is WRITTEN INTO THE NAME, so it is chosen once and travels with the code.
314
+ * ⛔ Until then it was written to `package.json::uniweb.scope`, which is refused now
315
+ * (`readFoundationName`, naming the line to write).
316
+ *
317
+ * A preview (`--dry-run`, `-o`) writes nothing: a bare name previews under `--scope`,
318
+ * or unscoped.
319
+ *
320
+ * @returns {Promise<{ scope: string|null, source: string|null } | { cancelled: true } | null>}
321
+ * null when refused (the reason is printed); `cancelled` when no org was chosen
322
+ */
323
+ export async function settleFoundationScope(targetDir, { args, isPreview, flagScope, client }) {
324
+ let read
325
+ try {
326
+ read = await readFoundationName(targetDir)
327
+ } catch (err) {
328
+ error(err.message)
329
+ return null
330
+ }
331
+ const where = relative(process.cwd(), read.mainFile) || read.mainFile
332
+ const { scope: nameScope, bare } = splitFoundationName(read.name)
333
+
334
+ if (nameScope) {
335
+ if (flagScope && flagScope !== nameScope) {
336
+ error(
337
+ `${where} names this foundation ${read.name}, so it registers under ${nameScope} — ` +
338
+ `--scope ${flagScope} names another org.`
339
+ )
340
+ log(
341
+ ` ${colors.dim}To register it under ${flagScope}, change its name in ${where}: ` +
342
+ `name: '${flagScope}/${bare}'${colors.reset}`
343
+ )
344
+ return null
345
+ }
346
+ return { scope: nameScope, source: 'main.js' }
347
+ }
348
+
349
+ if (isPreview) return { scope: flagScope, source: flagScope ? '--scope' : null }
350
+
351
+ let scope = flagScope
352
+ let source = flagScope ? '--scope' : null
353
+ if (!scope) {
354
+ scope = await deriveScopeFromLogin(client, args)
355
+ if (!scope) return { cancelled: true }
356
+ source = 'login'
357
+ }
358
+
359
+ const scoped = `${scope}/${bare}`
360
+ const written = writeFoundationName(read.mainFile, scoped, {
361
+ replace: read.source === 'main.js' ? read.name : null
362
+ })
363
+ if (!written.ok) {
364
+ error(`Could not write the scope into the foundation's name: ${written.reason}.`)
365
+ log(` Name it in the default export in ${colors.bright}${where}${colors.reset}: name: '${scoped}',`)
366
+ return null
367
+ }
368
+ // Read it back through the build's own rule — the text edit is only a means.
369
+ const again = await readFoundationName(targetDir).catch(() => null)
370
+ if (again?.name !== scoped) {
371
+ error(`${where} now names the foundation '${scoped}', but it still reads as "${again?.name}".`)
372
+ return null
373
+ }
374
+ success(`This foundation registers as ${colors.bright}${scoped}${colors.reset} — kept in ${where}.`)
375
+ return { scope, source }
376
+ }
377
+
378
+ /**
379
+ * A publish scope derived from the login's org memberships (list → 1 use / 0 create /
380
+ * N pick), as `@handle` — or null when none was chosen.
381
+ */
382
+ async function deriveScopeFromLogin(client, args) {
383
+ const token = await client.token()
384
+ const sess = await readRegistryAuth(client.origin)
385
+ const derived = await deriveScope({
386
+ apiBase: client.origin,
387
+ token,
388
+ accountHandle: sess?.handle || null,
389
+ args
390
+ })
391
+ return derived ? `@${derived}` : null
392
+ }
393
+
212
394
  // Directories that are never foundation source. `dist` is the output we compare
213
395
  // against; the rest are tooling. Dotted entries are skipped wholesale.
214
396
  const NON_SOURCE_DIRS = new Set(['dist', 'node_modules'])
@@ -403,10 +585,13 @@ async function runRegister(args = []) {
403
585
  ? process.cwd()
404
586
  : await resolveFoundationDir(args)
405
587
 
406
- // Scope: --scope flag, else package.json `uniweb.scope`, else (real submit
407
- // only) derived from login membership in the bootstrap below. Either spelling,
408
- // `@acme` or `acme`, and from here on the one form: `@acme` (publishScope).
409
- const pkgScope = readPkgScope(targetDir)
588
+ // Scope. ⭐ A FOUNDATION's is the one in its name (`name: '@acme/marketing'` in
589
+ // main.js), settled below before the build; a bare name takes --scope, else one
590
+ // derived from your orgs, and register writes it into the name (2026-09-22).
591
+ // A SCHEMAS-ONLY package has no name to carry one: --scope, else package.json
592
+ // `uniweb.scope`, else (real submit only) derived from login membership. Either
593
+ // spelling, `@acme` or `acme`, and from here on the one form: `@acme` (publishScope).
594
+ const pkgScope = standalone ? readPkgScope(targetDir) : null
410
595
  const givenScope = scopeFlag || pkgScope
411
596
  let scope = publishScope(givenScope)
412
597
  let scopeSource = scopeFlag
@@ -443,8 +628,24 @@ async function runRegister(args = []) {
443
628
  return { exitCode: 2 }
444
629
  }
445
630
  } else {
446
- // Build-if-stale (mirrors `uniweb publish`): a missing or version-stale
447
- // dist/ gets (re)built before we read its schema. Preview paths
631
+ if (!(await settleFoundationName(targetDir, { args, isPreview }))) {
632
+ return { exitCode: 2 }
633
+ }
634
+ // ⭐ BEFORE THE BUILD, so the build embeds the name the foundation registers as.
635
+ // Settled after it, a scope written into main.js would leave dist/ describing the
636
+ // bare name — stale the moment it was built.
637
+ const settled = await settleFoundationScope(targetDir, {
638
+ args,
639
+ isPreview,
640
+ flagScope: scope,
641
+ client
642
+ })
643
+ if (!settled) return { exitCode: 2 }
644
+ if (settled.cancelled) return { exitCode: 0 }
645
+ scope = settled.scope
646
+ scopeSource = settled.source
647
+ // Build-if-stale (`foundationNeedsBuild`): a missing or stale dist/ gets
648
+ // (re)built before we read its schema. Preview paths
448
649
  // (--dry-run / -o) must not write to dist/, so they require a pre-built
449
650
  // foundation and say so instead of building.
450
651
  const { needs, reason } = foundationNeedsBuild(targetDir)
@@ -491,24 +692,18 @@ async function runRegister(args = []) {
491
692
  }
492
693
  }
493
694
 
494
- // No scope for a real submit → derive it from login membership (list → 1 use /
495
- // 0 create / N pick), persist to package.json, and reuse the session token.
496
- if (!scope && !isPreview) {
497
- const token = await client.token()
498
- const sess = await readRegistryAuth(client.origin)
499
- const derived = await deriveScope({
500
- apiBase: client.origin,
501
- token,
502
- accountHandle: sess?.handle || null,
503
- args
504
- })
695
+ // A schemas-only package with no scope, on a real submit → derive it from login
696
+ // membership (list → 1 use / 0 create / N pick) and persist it to package.json.
697
+ // (A foundation's was settled before its build — `settleFoundationScope`.)
698
+ if (standalone && !scope && !isPreview) {
699
+ const derived = await deriveScopeFromLogin(client, args)
505
700
  if (!derived) return { exitCode: 0 }
506
- scope = `@${derived}`
701
+ scope = derived
507
702
  scopeSource = 'login'
508
703
  try {
509
704
  await writePkgScope(targetDir, scope)
510
705
  info(
511
- `Saved ${colors.bright}${scope}${colors.reset} as this ${standalone ? 'package' : 'foundation'}'s publish scope (package.json).`
706
+ `Saved ${colors.bright}${scope}${colors.reset} as this package's publish scope (package.json).`
512
707
  )
513
708
  } catch {
514
709
  log(
@@ -605,7 +800,9 @@ async function runRegister(args = []) {
605
800
  // Submit requires a concrete scope — the registry rejects @/… fail-closed.
606
801
  if (!scope) {
607
802
  error(
608
- 'No publish scope — set "uniweb.scope" in package.json, or pass --scope @org.'
803
+ standalone
804
+ ? 'No publish scope — set "uniweb.scope" in package.json, or pass --scope @org.'
805
+ : "No publish scope — name the foundation '@org/<name>' in main.js, or pass --scope @org."
609
806
  )
610
807
  log(
611
808
  ` ${colors.dim}Without a scope, names stay @/… and the registry rejects them.${colors.reset}`
@@ -20,7 +20,7 @@
20
20
  *
21
21
  * Extensions are technically a flavor of foundation (same build, same
22
22
  * package.json shape, distinguished only by `extension: true` in
23
- * src/foundation.js or `role: 'extension'` in the built schema). The
23
+ * main.js or `role: 'extension'` in the built schema). The
24
24
  * rename verb still keeps them on a separate subcommand because the
25
25
  * touch-point sets differ — `rename foundation` against an extension
26
26
  * would update the wrong things. Each subcommand guards its target
@@ -30,11 +30,13 @@
30
30
  * (target name already taken, target not found, folder collision,
31
31
  * type mismatch) we bail with a clear message and no partial state.
32
32
  *
33
- * Out of scope: registry side. The registered id (the scoped
34
- * `package.json::name` / `uniweb.id`) is independent of the workspace name and
35
- * stays untouched. There is no registry-rename flag — a registered version is
36
- * immutable; to change a foundation's registered identity, register it under
37
- * the new name (consuming sites repoint their `foundation:` ref).
33
+ * Out of scope: registry side. What a foundation registers as is `name` in its
34
+ * main.js, which is independent of the workspace name and stays untouched —
35
+ * except for a foundation with no main.js `name`, whose package name IS its name
36
+ * (`readFoundationName`), so renaming the package renames what it would register
37
+ * as. There is no registry-rename flag — a registered version is immutable; to
38
+ * change a foundation's registered name, change main.js's `name` and register
39
+ * again (consuming sites repoint their `foundation:` ref).
38
40
  *
39
41
  * Usage:
40
42
  * uniweb rename foundation <old> <new>
@@ -632,9 +634,10 @@ ${colors.bright}What rename extension does:${colors.reset}
632
634
  • Updates pnpm-workspace.yaml + package.json::workspaces.
633
635
 
634
636
  ${colors.bright}What rename does NOT do (any subcommand):${colors.reset}
635
- • Push to the registry. The registered id (package.json::uniweb.id) is
636
- independent. To register under a new name, set it and run \`${prefix} register\`
637
- (alias \`${prefix} release\`) — there is no rename flag.
637
+ • Push to the registry, or change what a foundation registers as — that is
638
+ \`name\` in its main.js, independent of the package name. To register under
639
+ a new name, change it there and run \`${prefix} register\` (alias
640
+ \`${prefix} release\`) — there is no rename flag.
638
641
 
639
642
  ${colors.bright}Examples:${colors.reset}
640
643
  ${prefix} rename foundation src marketing-src
@@ -25,10 +25,19 @@ import { join } from 'node:path'
25
25
  import yaml from 'js-yaml'
26
26
 
27
27
  import { resolveSiteDir } from './deploy.js'
28
- import { probeUnpushed } from '../backend/site-sync.js'
29
- import { BackendClient, resolveBackendOrigin } from '../backend/client.js'
28
+ import { probeUnpushed, nameSiteWorkspace, readSiteWorkspace } from '../backend/site-sync.js'
29
+ import { readOrgFlag } from '../utils/args.js'
30
+ import {
31
+ BackendClient,
32
+ resolveBackendOrigin,
33
+ workspaceHandle,
34
+ WorkspaceMismatchError
35
+ } from '../backend/client.js'
30
36
  import { readBackendState } from '@uniweb/build/uwx'
31
- import { resolveLocalFoundation } from '../backend/foundation-bring-along.js'
37
+ import {
38
+ resolveLocalFoundation,
39
+ foundationScopedName
40
+ } from '../backend/foundation-bring-along.js'
32
41
  import { computeFoundationDigest } from '../utils/code-upload.js'
33
42
  import { checkFlags } from '../utils/flag-guard.js'
34
43
 
@@ -112,26 +121,40 @@ export async function status(args = []) {
112
121
  let site = null
113
122
  let fdnLatest = null
114
123
  let foundationFresh = null // true/false when both digests are known; else null
124
+ let remoteError = null // a refusal about the workspace — the one remote failure worth saying
115
125
  if (remote) {
116
126
  try {
117
127
  const client = new BackendClient({
118
128
  args,
119
129
  command: 'Status'
120
130
  })
131
+ // The workspace the site's requests name — as pull names it. A stale record is
132
+ // re-adopted from the backend's answer; `--json` keeps stdout to the document.
133
+ const orgFlag = readOrgFlag(args)
134
+ nameSiteWorkspace(client, {
135
+ siteDir,
136
+ workspace: orgFlag ? workspaceHandle(orgFlag) : readSiteWorkspace(siteDir, client.origin),
137
+ explicit: Boolean(orgFlag),
138
+ note: jsonMode ? undefined : say.dim
139
+ })
121
140
  if (uuid) site = await client.siteStatus(uuid)
122
141
  // Foundation freshness: prefer the LOCAL foundation's scoped name (so a
123
142
  // local-foundation site can be checked too); fall back to a scoped
124
143
  // site.yml ref. The digest compare is read-only — it never builds, so it
125
144
  // only fires when the local foundation is already built (dist present).
126
145
  const local = resolveLocalFoundation(siteDir, siteYml)
127
- const lookupName = local?.scopedName || fndScope
146
+ const lookupName =
147
+ (local && (await foundationScopedName(local.dir))) || fndScope
128
148
  if (lookupName) fdnLatest = await client.readFoundationLatest(lookupName)
129
149
  if (local?.dir && fdnLatest?.digest) {
130
150
  const localDigest = computeFoundationDigest(join(local.dir, 'dist'))
131
151
  if (localDigest) foundationFresh = localDigest === fdnLatest.digest
132
152
  }
133
- } catch {
134
- // degrade silently
153
+ } catch (err) {
154
+ // Degrade silently — except a refusal about the workspace: `--org` named one the
155
+ // backend does not work on this site from, or the deployment has another. Saying
156
+ // nothing would read as "fine".
157
+ if (err instanceof WorkspaceMismatchError || err?.status === 409) remoteError = err.message
135
158
  }
136
159
  }
137
160
 
@@ -149,7 +172,8 @@ export async function status(args = []) {
149
172
  remote: {
150
173
  site,
151
174
  foundation_latest: fdnLatest?.latest_version ?? null,
152
- foundation_fresh: foundationFresh
175
+ foundation_fresh: foundationFresh,
176
+ ...(remoteError ? { error: remoteError } : {})
153
177
  }
154
178
  }
155
179
  : {})
@@ -198,6 +222,7 @@ export async function status(args = []) {
198
222
 
199
223
  // Remote signals
200
224
  if (remote) {
225
+ if (remoteError) say.warn(remoteError)
201
226
  if (site) {
202
227
  if (site.draft_dirty) {
203
228
  say.info(
@@ -1,16 +1,16 @@
1
1
  {
2
2
  "schemaVersion": 1,
3
- "generatedAt": "2026-09-21T21:47:05.925Z",
3
+ "generatedAt": "2026-09-23T12:12:24.996Z",
4
4
  "packages": {
5
5
  "@uniweb/api": {
6
- "version": "0.4.1",
6
+ "version": "0.5.0",
7
7
  "path": "framework/api",
8
8
  "deps": [
9
9
  "@uniweb/core"
10
10
  ]
11
11
  },
12
12
  "@uniweb/build": {
13
- "version": "0.57.0",
13
+ "version": "0.59.0",
14
14
  "path": "framework/build",
15
15
  "deps": [
16
16
  "@uniweb/content-reader",
@@ -35,7 +35,7 @@
35
35
  "deps": []
36
36
  },
37
37
  "@uniweb/core": {
38
- "version": "0.29.7",
38
+ "version": "0.29.8",
39
39
  "path": "framework/core",
40
40
  "deps": [
41
41
  "@uniweb/semantic-parser",
@@ -83,7 +83,7 @@
83
83
  ]
84
84
  },
85
85
  "@uniweb/runtime": {
86
- "version": "0.27.3",
86
+ "version": "0.27.4",
87
87
  "path": "framework/runtime",
88
88
  "deps": [
89
89
  "@uniweb/core",
@@ -96,7 +96,7 @@
96
96
  "deps": []
97
97
  },
98
98
  "@uniweb/schemas": {
99
- "version": "0.3.3",
99
+ "version": "0.3.4",
100
100
  "path": "framework/schemas",
101
101
  "deps": []
102
102
  },
@@ -116,7 +116,7 @@
116
116
  "deps": []
117
117
  },
118
118
  "@uniweb/templates": {
119
- "version": "0.15.0",
119
+ "version": "0.16.0",
120
120
  "path": "framework/templates",
121
121
  "deps": []
122
122
  },
@@ -126,7 +126,7 @@
126
126
  "deps": []
127
127
  },
128
128
  "@uniweb/unipress": {
129
- "version": "0.10.0",
129
+ "version": "0.10.2",
130
130
  "path": "framework/unipress",
131
131
  "deps": [
132
132
  "@uniweb/build",
package/src/index.js CHANGED
@@ -53,6 +53,10 @@ import {
53
53
  mergeTemplateDependencies,
54
54
  getWorkspaceTemplateOutputs
55
55
  } from './utils/scaffold.js'
56
+ import {
57
+ normalizeFoundationName,
58
+ ensureFoundationName
59
+ } from './utils/foundation-name.js'
56
60
  import {
57
61
  detectPackageManager,
58
62
  detectWorkspacePm,
@@ -354,12 +358,16 @@ async function createFromPackageTemplates(
354
358
  // 2. Scaffold foundation (folder: src/, package name: src)
355
359
  // The folder name 'src' carries the meaning — a foundation is the site's
356
360
  // source code. The package name 'src' keeps it unique within the
357
- // workspace, since 'site' is taken by the site package.
361
+ // workspace, since 'site' is taken by the site package. Neither is the
362
+ // foundation's NAME — what it registers as — which is main.js's `name`:
363
+ // the project's, since a project's code is its foundation.
358
364
  onProgress?.('Creating foundation...')
365
+ const registryName = normalizeFoundationName(projectName)
359
366
  await scaffoldFoundation(
360
367
  join(projectDir, 'src'),
361
368
  {
362
369
  name: 'src',
370
+ registryName,
363
371
  projectName,
364
372
  isExtension: false
365
373
  },
@@ -380,10 +388,12 @@ async function createFromPackageTemplates(
380
388
  { onProgress, onWarning }
381
389
  )
382
390
 
383
- // 4. Apply starter content (unless creating a "none" project)
391
+ // 4. Apply starter content (unless creating a "none" project). Its main.js
392
+ // replaces the scaffolded one, so the name is given again.
384
393
  if (includeStarter) {
385
394
  onProgress?.('Adding starter content...')
386
395
  await applyStarter(projectDir, { projectName }, { onProgress, onWarning })
396
+ ensureFoundationName(join(projectDir, 'src'), registryName)
387
397
  }
388
398
 
389
399
  success(`Created project: ${projectName}`)
@@ -475,15 +485,31 @@ async function createFromContentTemplate(
475
485
  )
476
486
 
477
487
  // 2. Scaffold and apply content for each package
488
+ const claimed = new Set() // names this project's foundations register as
478
489
  for (const pkg of placed) {
479
490
  const fullPath = join(projectDir, pkg.relativePath)
480
491
 
492
+ // A foundation's name — what it registers as — is the template's name for
493
+ // the package when that is one, else the project's (`src` and `foundation`
494
+ // name a folder); `<project>-<package>` if another package here took it. A
495
+ // template's own main.js `name` outranks all of them.
496
+ let registryName = null
497
+ if (pkg.type === 'foundation' || pkg.type === 'extension') {
498
+ registryName =
499
+ [
500
+ normalizeFoundationName(pkg.name) || normalizeFoundationName(projectName),
501
+ normalizeFoundationName(`${projectName}-${pkg.name}`)
502
+ ].find((n) => n && !claimed.has(n)) || null
503
+ if (registryName) claimed.add(registryName)
504
+ }
505
+
481
506
  if (pkg.type === 'foundation' || pkg.type === 'extension') {
482
507
  onProgress?.(`Creating ${pkg.type}: ${pkg.name}...`)
483
508
  await scaffoldFoundation(
484
509
  fullPath,
485
510
  {
486
511
  name: pkg.name,
512
+ registryName,
487
513
  projectName,
488
514
  isExtension: pkg.type === 'extension'
489
515
  },
@@ -534,6 +560,7 @@ async function createFromContentTemplate(
534
560
  renames: contentDir.renames
535
561
  }
536
562
  )
563
+ ensureFoundationName(fullPath, registryName)
537
564
  }
538
565
 
539
566
  // Merge template dependencies into package.json
@@ -1429,11 +1456,12 @@ ${colors.bright}Options:${colors.reset}
1429
1456
  bind to, and the app cannot open such a site.
1430
1457
  --no-save Do not record this deploy in deploy.yml
1431
1458
  --no-validate Skip the content-conformance check (it only warns)
1432
- --org @org Publish under @org (membership-gated; alias: --as-org). Read
1433
- only on a site's FIRST publish — that create decides which org
1434
- owns the site, and whose storage its assets are charged to.
1435
- Recorded in sync.json and replayed, so it is never
1436
- re-typed. Without it, you are asked once.
1459
+ --org @org Publish under @org (membership-gated). On a site's FIRST
1460
+ publish it decides which org owns the site, and whose storage
1461
+ its assets are charged to. Recorded in sync.json and named on
1462
+ every request after, so it is never re-typed. Without it, you
1463
+ are asked once. Later, it must match the workspace the backend
1464
+ works on the site from, or publish stops.
1437
1465
  --personal Create the site under your personal account, deliberately.
1438
1466
  Only needed on a first publish, and only to answer the owner
1439
1467
  question without a prompt (CI, agents, scripts).
@@ -1698,13 +1726,18 @@ Auto-detects what you run it in:
1698
1726
  • a schemas-only pkg just its data schemas, no foundation — e.g. @uniweb/schemas,
1699
1727
  any @org/schemas package, or a bare schemas/*.yml folder
1700
1728
 
1701
- Schema scopes (set the org with --scope, or package.json uniweb.scope):
1702
- @/name your own schema, scoped to the publish org (@/x -> @org/x)
1729
+ A foundation's scope is part of its name — name: '@org/<name>' in main.js. A bare
1730
+ name takes --scope (or one of your orgs), and register writes it into the name.
1731
+
1732
+ Schema scopes:
1733
+ @/name your own schema, in the foundation's scope (@/x -> @org/x)
1703
1734
  @std/name a shared standard schema (from @uniweb/schemas)
1704
1735
  @org/name another org's published schema, referenced by name
1705
1736
 
1706
1737
  ${colors.bright}Options:${colors.reset}
1707
- --scope @org Publish under @org (resolves @/x -> @org/x); default: package.json uniweb.scope
1738
+ --scope @org A foundation with a bare name: register under @org, and write it into
1739
+ the name (refused when the name has another scope). A schemas-only
1740
+ package: publish under @org; default: package.json uniweb.scope
1708
1741
  --dry-run Print the .uwx; submit nothing
1709
1742
  -o, --output <f> Write the .uwx to a file; submit nothing
1710
1743
  --non-interactive Fail with usage info instead of prompting