uniweb 0.60.0 → 0.62.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.
- package/package.json +8 -8
- package/partials/agents.md +27 -36
- package/src/backend/asset-download.js +5 -5
- package/src/backend/foundation-bring-along.js +72 -60
- package/src/backend/service-request.js +2 -1
- package/src/backend/site-media.js +6 -6
- package/src/backend/site-sync.js +106 -64
- package/src/commands/add.js +62 -4
- package/src/commands/clone.js +1 -1
- package/src/commands/doctor.js +6 -7
- package/src/commands/i18n.js +19 -10
- package/src/commands/publish.js +10 -4
- package/src/commands/pull.js +16 -6
- package/src/commands/push.js +7 -7
- package/src/commands/register.js +126 -21
- package/src/commands/rename.js +12 -9
- package/src/commands/status.js +6 -2
- package/src/framework-index.json +7 -7
- package/src/index.js +30 -3
- package/src/utils/foundation-name.js +179 -0
- package/src/utils/git.js +5 -2
- package/src/utils/records-guard.js +35 -28
- package/src/utils/registry-orgs.js +19 -0
- package/src/utils/scaffold.js +7 -5
- package/starter/foundation/main.js +2 -1
- package/templates/foundation/main.js.hbs +9 -0
- package/templates/site/site.yml.hbs +12 -9
package/src/commands/publish.js
CHANGED
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
* FIRST publish of a site reads it — that create is
|
|
34
34
|
* what decides which org owns the site and whose
|
|
35
35
|
* storage its assets are charged to. It is then
|
|
36
|
-
* recorded
|
|
36
|
+
* recorded in `sync.json` and replayed, so it
|
|
37
37
|
* never has to be re-typed.
|
|
38
38
|
* uniweb publish --personal Own the new site personally, deliberately. Sends
|
|
39
39
|
* NO `as_org` — byte-identical to the wire before
|
|
@@ -535,12 +535,18 @@ export async function publish(args = []) {
|
|
|
535
535
|
// Non-local @std/registry Model schemas resolve through the backend (same as push).
|
|
536
536
|
const resolveModel = makeModelResolver({ client, offline: false })
|
|
537
537
|
|
|
538
|
-
// ⛔ AN EMPTY
|
|
539
|
-
// destructive — a
|
|
540
|
-
// is reported and confirmed before anything is sent.
|
|
538
|
+
// ⛔ AN EMPTY RECORDS DIRECTORY REMOVES. It is the one path where an ordinary act
|
|
539
|
+
// is destructive — a directory emptied by accident, or kept with only a
|
|
540
|
+
// placeholder — so the count is reported and confirmed before anything is sent.
|
|
541
|
+
//
|
|
542
|
+
// ⛔ `backend` WAS MISSING HERE until 2026-09-21, and it is what the count is read
|
|
543
|
+
// under: placements are banked per backend, so with none the guard counted zero
|
|
544
|
+
// and never asked — `publish` sent an empty folder without a word while `push`
|
|
545
|
+
// stopped for the same one.
|
|
541
546
|
{
|
|
542
547
|
const guard = await guardEmptyRecords({
|
|
543
548
|
siteDir,
|
|
549
|
+
backend: client.origin,
|
|
544
550
|
args,
|
|
545
551
|
warn: say.warn,
|
|
546
552
|
note: say.dim
|
package/src/commands/pull.js
CHANGED
|
@@ -65,7 +65,7 @@ import {
|
|
|
65
65
|
import { tmpdir } from 'node:os'
|
|
66
66
|
import { createHash } from 'node:crypto'
|
|
67
67
|
import { createInterface } from 'node:readline/promises'
|
|
68
|
-
import { join, dirname, relative } from 'node:path'
|
|
68
|
+
import { join, dirname, relative, resolve } from 'node:path'
|
|
69
69
|
import yaml from 'js-yaml'
|
|
70
70
|
import { downloadMissingAssets } from '../backend/asset-download.js'
|
|
71
71
|
import {
|
|
@@ -843,9 +843,9 @@ export async function pull(args = [], deps = {}) {
|
|
|
843
843
|
}
|
|
844
844
|
// ⛔ NO QUERY CONFIG. A record's home is decided by what it IS — its
|
|
845
845
|
// `$model` names the pool folder — not by any query that happens to select
|
|
846
|
-
// it. `recordsToProject` reads
|
|
847
|
-
// model the producer resolved to `@org/x` is placed
|
|
848
|
-
// wrote it.
|
|
846
|
+
// it. `recordsToProject` reads the site's org itself (this backend's, from
|
|
847
|
+
// `sync.json`), so a `@/x` model the producer resolved to `@org/x` is placed
|
|
848
|
+
// back where the author wrote it.
|
|
849
849
|
const report = recordsToProject({
|
|
850
850
|
folderDoc,
|
|
851
851
|
recordDocs,
|
|
@@ -856,7 +856,16 @@ export async function pull(args = [], deps = {}) {
|
|
|
856
856
|
resolveDeclaration: (name) => declByModel.get(name) || null
|
|
857
857
|
}
|
|
858
858
|
})
|
|
859
|
-
|
|
859
|
+
// The folder's organization, `folder.yml` in the records directory — named as
|
|
860
|
+
// the author would write it (`records/folder.yml`, or under `paths.records`).
|
|
861
|
+
if (report.records === 'updated') {
|
|
862
|
+
info(`Wrote ${report.recordsFile}`)
|
|
863
|
+
wrote.push(resolve(siteDir, report.recordsFile))
|
|
864
|
+
} else if (report.records === 'removed') {
|
|
865
|
+
// The backend's folder has no sub-folders, and the file holds nothing else.
|
|
866
|
+
info(`Removed ${report.recordsFile} — the folder has no sub-folders`)
|
|
867
|
+
removed.push(resolve(siteDir, report.recordsFile))
|
|
868
|
+
}
|
|
860
869
|
records += report.placed.length + report.updated.length
|
|
861
870
|
for (const s of report.skipped)
|
|
862
871
|
note(`↷ ${s.slug ?? s.uuid ?? '(record)'}: ${s.reason}`)
|
|
@@ -890,7 +899,8 @@ export async function pull(args = [], deps = {}) {
|
|
|
890
899
|
siteDir,
|
|
891
900
|
[
|
|
892
901
|
...wrote,
|
|
893
|
-
|
|
902
|
+
// The records folder's `folder.yml` is in `wrote` when this pull wrote it.
|
|
903
|
+
...['site.yml', 'theme.yml', 'head.html', 'queries.yml'].map((f) =>
|
|
894
904
|
join(siteDir, f)
|
|
895
905
|
)
|
|
896
906
|
],
|
package/src/commands/push.js
CHANGED
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
* the backend owns the site's `@uniweb/folder`, so the framework never holds a
|
|
15
15
|
* folder uuid. Records still round-trip their own `$uuid`
|
|
16
16
|
* (back-filled into their source files). site-content items carry a per-item `$uuid`
|
|
17
|
-
* too, stamped at emit from
|
|
17
|
+
* too, stamped at emit from `sync.json` rather than from author files — without
|
|
18
18
|
* it the backend reads every record as new and recreates every page and section row.
|
|
19
19
|
* Push-only, and gated on the backend's per-entity `version`
|
|
20
20
|
* (see "Pushes are GATED by default" below); `--force` restores last-push-wins.
|
|
@@ -30,8 +30,8 @@
|
|
|
30
30
|
* uniweb push --org @org Own the new site under @org (alias: --as-org).
|
|
31
31
|
* Read only on the FIRST push of a site — it
|
|
32
32
|
* decides which org owns it, and whose storage
|
|
33
|
-
* its assets are charged to. Recorded
|
|
34
|
-
* `
|
|
33
|
+
* its assets are charged to. Recorded in
|
|
34
|
+
* `sync.json` and replayed after that.
|
|
35
35
|
* Without it, you are asked once.
|
|
36
36
|
* uniweb push --personal Own the new site personally, deliberately.
|
|
37
37
|
* Sends NO `as_org` — the same wire as before
|
|
@@ -388,10 +388,10 @@ export async function push(args = [], deps = {}) {
|
|
|
388
388
|
// It also has to be this emit that carries `assetRewrite` below: the push cache
|
|
389
389
|
// stores hashes of the REWRITTEN content, so the emit compared against it must
|
|
390
390
|
// rewrite too, or every entity reads as changed forever.
|
|
391
|
-
// ⛔ AN EMPTY
|
|
392
|
-
// destructive — a
|
|
393
|
-
// is reported and confirmed before anything is sent.
|
|
394
|
-
// the asking happens here.
|
|
391
|
+
// ⛔ AN EMPTY RECORDS DIRECTORY REMOVES. It is the one path where an ordinary act
|
|
392
|
+
// is destructive — a directory emptied by accident, or kept with only a
|
|
393
|
+
// placeholder — so the count is reported and confirmed before anything is sent.
|
|
394
|
+
// The format stays honest; the asking happens here.
|
|
395
395
|
if (!dryRun) {
|
|
396
396
|
const guard = await guardEmptyRecords({ siteDir, backend: client.origin, args, warn, note })
|
|
397
397
|
if (!guard.ok) return { exitCode: 1 }
|
package/src/commands/register.js
CHANGED
|
@@ -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
|
|
7
|
-
*
|
|
8
|
-
*
|
|
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
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
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`
|
|
@@ -64,20 +63,27 @@ import {
|
|
|
64
63
|
statSync
|
|
65
64
|
} from 'node:fs'
|
|
66
65
|
import { execSync } from 'node:child_process'
|
|
67
|
-
import { resolve, join } from 'node:path'
|
|
66
|
+
import { resolve, join, relative } from 'node:path'
|
|
67
|
+
import prompts from 'prompts'
|
|
68
68
|
import { buildRegistryPackage, buildSchemaOnlyPackage } from '@uniweb/build/uwx'
|
|
69
69
|
import {
|
|
70
70
|
classifyPackage,
|
|
71
71
|
isSchemasPackage,
|
|
72
|
-
collectStandaloneSchemas
|
|
72
|
+
collectStandaloneSchemas,
|
|
73
|
+
readFoundationName,
|
|
74
|
+
checkFoundationName
|
|
73
75
|
} from '@uniweb/build'
|
|
76
|
+
import {
|
|
77
|
+
suggestFoundationName,
|
|
78
|
+
writeFoundationName
|
|
79
|
+
} from '../utils/foundation-name.js'
|
|
74
80
|
import { readRegistryAuth } from '../utils/registry-auth.js'
|
|
75
81
|
import {
|
|
76
82
|
collectDistFiles,
|
|
77
83
|
computeFoundationDigest,
|
|
78
84
|
readRuntimePin
|
|
79
85
|
} from '../utils/code-upload.js'
|
|
80
|
-
import { deriveScope } from '../utils/registry-orgs.js'
|
|
86
|
+
import { deriveScope, publishScope } from '../utils/registry-orgs.js'
|
|
81
87
|
import { BackendClient } from '../backend/client.js'
|
|
82
88
|
import { writeJsonPreservingStyleAsync } from '../utils/json-file.js'
|
|
83
89
|
import {
|
|
@@ -209,6 +215,84 @@ async function resolveFoundationDir(args) {
|
|
|
209
215
|
process.exit(1)
|
|
210
216
|
}
|
|
211
217
|
|
|
218
|
+
/**
|
|
219
|
+
* The foundation's name, settled before anything is built or sent.
|
|
220
|
+
*
|
|
221
|
+
* A foundation registers as `@org/<name>`, and sites pin it by that name. The name
|
|
222
|
+
* is `main.js`'s `name`, else package.json's — `readFoundationName`, the rule the
|
|
223
|
+
* build reads for the schema this command submits. ⛔ `src` and `foundation` are
|
|
224
|
+
* the folder the code sits in, not the code: a project scaffolded before the name
|
|
225
|
+
* moved to main.js has package name `src`, and every such project in an org would
|
|
226
|
+
* register the same `@org/src`. So a name that cannot register is ASKED FOR here,
|
|
227
|
+
* once, and kept in main.js — or, with nobody to ask, refused with the line to add.
|
|
228
|
+
* Before the build, so a written name is built into the schema like any edit.
|
|
229
|
+
*
|
|
230
|
+
* @param {string} targetDir
|
|
231
|
+
* @param {{ args: string[], isPreview: boolean }} o
|
|
232
|
+
* @returns {Promise<boolean>} false → stop; the reason is printed
|
|
233
|
+
*/
|
|
234
|
+
export async function settleFoundationName(targetDir, { args, isPreview }) {
|
|
235
|
+
let read
|
|
236
|
+
try {
|
|
237
|
+
read = await readFoundationName(targetDir)
|
|
238
|
+
} catch (err) {
|
|
239
|
+
error(err.message)
|
|
240
|
+
return false
|
|
241
|
+
}
|
|
242
|
+
const problem = checkFoundationName(read.name)
|
|
243
|
+
if (!problem) return true
|
|
244
|
+
|
|
245
|
+
const where = relative(process.cwd(), read.mainFile) || read.mainFile
|
|
246
|
+
const suggestion = suggestFoundationName(targetDir)
|
|
247
|
+
const reason = `${problem[0].toUpperCase()}${problem.slice(1)}.`
|
|
248
|
+
|
|
249
|
+
// Nobody to ask — a script, a preview (which writes nothing), or --json (whose
|
|
250
|
+
// stdout is a data channel a prompt would corrupt). Name the fix exactly.
|
|
251
|
+
if (isPreview || jsonMode || isNonInteractive(args)) {
|
|
252
|
+
error(`This foundation has no name it can register under. ${reason}`)
|
|
253
|
+
log(` Name it in ${colors.bright}${where}${colors.reset}, in the default export:`)
|
|
254
|
+
log(` ${colors.bright}name: '${suggestion || '<name>'}',${colors.reset}`)
|
|
255
|
+
log(` ${colors.dim}It registers as @<org>/<name>, and sites pin that name.${colors.reset}`)
|
|
256
|
+
return false
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
info(`This foundation needs a name to register under. ${reason}`)
|
|
260
|
+
const { name } = await prompts(
|
|
261
|
+
{
|
|
262
|
+
type: 'text',
|
|
263
|
+
name: 'name',
|
|
264
|
+
message: 'Foundation name (it registers as @<org>/<name>):',
|
|
265
|
+
initial: suggestion || '',
|
|
266
|
+
validate: (v) => checkFoundationName(String(v).trim()) || true
|
|
267
|
+
},
|
|
268
|
+
{
|
|
269
|
+
onCancel: () => {
|
|
270
|
+
log('\nRegister cancelled.')
|
|
271
|
+
process.exit(0)
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
)
|
|
275
|
+
const chosen = String(name || '').trim()
|
|
276
|
+
if (!chosen) return false
|
|
277
|
+
|
|
278
|
+
const written = writeFoundationName(read.mainFile, chosen, {
|
|
279
|
+
replace: read.source === 'main.js' ? read.name : null
|
|
280
|
+
})
|
|
281
|
+
if (!written.ok) {
|
|
282
|
+
error(`Could not write the name: ${written.reason}.`)
|
|
283
|
+
log(` Add it to the default export in ${colors.bright}${where}${colors.reset}: name: '${chosen}',`)
|
|
284
|
+
return false
|
|
285
|
+
}
|
|
286
|
+
// Read it back through the build's own rule — the text edit is only a means.
|
|
287
|
+
const again = await readFoundationName(targetDir).catch(() => null)
|
|
288
|
+
if (again?.name !== chosen) {
|
|
289
|
+
error(`${where} now names the foundation '${chosen}', but it still reads as "${again?.name}".`)
|
|
290
|
+
return false
|
|
291
|
+
}
|
|
292
|
+
success(`Named the foundation ${colors.bright}${chosen}${colors.reset} — kept in ${where}.`)
|
|
293
|
+
return true
|
|
294
|
+
}
|
|
295
|
+
|
|
212
296
|
// Directories that are never foundation source. `dist` is the output we compare
|
|
213
297
|
// against; the rest are tooling. Dotted entries are skipped wholesale.
|
|
214
298
|
const NON_SOURCE_DIRS = new Set(['dist', 'node_modules'])
|
|
@@ -404,14 +488,24 @@ async function runRegister(args = []) {
|
|
|
404
488
|
: await resolveFoundationDir(args)
|
|
405
489
|
|
|
406
490
|
// Scope: --scope flag, else package.json `uniweb.scope`, else (real submit
|
|
407
|
-
// only) derived from login membership in the bootstrap below.
|
|
491
|
+
// only) derived from login membership in the bootstrap below. Either spelling,
|
|
492
|
+
// `@acme` or `acme`, and from here on the one form: `@acme` (publishScope).
|
|
408
493
|
const pkgScope = readPkgScope(targetDir)
|
|
409
|
-
|
|
494
|
+
const givenScope = scopeFlag || pkgScope
|
|
495
|
+
let scope = publishScope(givenScope)
|
|
410
496
|
let scopeSource = scopeFlag
|
|
411
497
|
? '--scope'
|
|
412
498
|
: pkgScope
|
|
413
499
|
? 'package.json uniweb.scope'
|
|
414
500
|
: null
|
|
501
|
+
// A scope that was GIVEN but names no org (`--scope @`) is refused, never read as
|
|
502
|
+
// absent: absent derives one from the login, which would register under an org the
|
|
503
|
+
// caller did not name.
|
|
504
|
+
if (givenScope && !scope) {
|
|
505
|
+
error(`Not an org scope: ${givenScope} (from ${scopeSource})`)
|
|
506
|
+
log(` ${colors.dim}Pass --scope @org.${colors.reset}`)
|
|
507
|
+
return { exitCode: 2 }
|
|
508
|
+
}
|
|
415
509
|
const isPreview = !!output || dryRun
|
|
416
510
|
|
|
417
511
|
// Each path supplies a different schema source: the standalone path discovers
|
|
@@ -433,8 +527,11 @@ async function runRegister(args = []) {
|
|
|
433
527
|
return { exitCode: 2 }
|
|
434
528
|
}
|
|
435
529
|
} else {
|
|
436
|
-
|
|
437
|
-
|
|
530
|
+
if (!(await settleFoundationName(targetDir, { args, isPreview }))) {
|
|
531
|
+
return { exitCode: 2 }
|
|
532
|
+
}
|
|
533
|
+
// Build-if-stale (`foundationNeedsBuild`): a missing or stale dist/ gets
|
|
534
|
+
// (re)built before we read its schema. Preview paths
|
|
438
535
|
// (--dry-run / -o) must not write to dist/, so they require a pre-built
|
|
439
536
|
// foundation and say so instead of building.
|
|
440
537
|
const { needs, reason } = foundationNeedsBuild(targetDir)
|
|
@@ -671,11 +768,17 @@ async function runRegister(args = []) {
|
|
|
671
768
|
return { exitCode: 1 }
|
|
672
769
|
}
|
|
673
770
|
}
|
|
771
|
+
// The foundation's name as the registry holds it (`@org/name`) — read from the
|
|
772
|
+
// document just submitted, never composed again. See phase 2.
|
|
773
|
+
const registeredName = standalone
|
|
774
|
+
? null
|
|
775
|
+
: doc.entities.find((e) => e.model === '@uniweb/foundation-schema')?.info
|
|
776
|
+
?.name
|
|
674
777
|
if (!alreadyRegistered) {
|
|
675
778
|
success(
|
|
676
779
|
standalone
|
|
677
780
|
? `Registered ${defined.length} data schema(s)${scope ? ` under ${scope}` : ''}`
|
|
678
|
-
: `Registered ${
|
|
781
|
+
: `Registered ${registeredName}@${schema._self.version}${defined.length ? ` + ${defined.length} data schema(s)` : ''}`
|
|
679
782
|
)
|
|
680
783
|
}
|
|
681
784
|
|
|
@@ -684,11 +787,13 @@ async function runRegister(args = []) {
|
|
|
684
787
|
// only packages have no dist; --schema-only skips deliberately.
|
|
685
788
|
if (!standalone && !args.includes('--schema-only')) {
|
|
686
789
|
const distDir = join(targetDir, 'dist')
|
|
687
|
-
// The
|
|
688
|
-
//
|
|
689
|
-
//
|
|
690
|
-
|
|
691
|
-
|
|
790
|
+
// ⭐ The plan authorizes against the REGISTERED (name, version), so the name here
|
|
791
|
+
// is the one phase 1 registered — the `.uwx`'s own `info.name`, where a scoped
|
|
792
|
+
// package name passes through and a bare one gets the scope. ⛔ Not composed a
|
|
793
|
+
// second time: this line composed it from the raw scope until 2026-09-21, so
|
|
794
|
+
// `--scope std` registered `@std/src` and then asked to deliver `std/src`, which
|
|
795
|
+
// the registry refuses. One name, read from the one place it was decided.
|
|
796
|
+
const name = registeredName
|
|
692
797
|
const version = schema._self.version
|
|
693
798
|
info(
|
|
694
799
|
`Delivering code for ${colors.bright}${name}@${version}${colors.reset} …`
|
package/src/commands/rename.js
CHANGED
|
@@ -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
|
-
*
|
|
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.
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
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
|
|
636
|
-
|
|
637
|
-
|
|
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
|
package/src/commands/status.js
CHANGED
|
@@ -28,7 +28,10 @@ import { resolveSiteDir } from './deploy.js'
|
|
|
28
28
|
import { probeUnpushed } from '../backend/site-sync.js'
|
|
29
29
|
import { BackendClient, resolveBackendOrigin } from '../backend/client.js'
|
|
30
30
|
import { readBackendState } from '@uniweb/build/uwx'
|
|
31
|
-
import {
|
|
31
|
+
import {
|
|
32
|
+
resolveLocalFoundation,
|
|
33
|
+
foundationScopedName
|
|
34
|
+
} from '../backend/foundation-bring-along.js'
|
|
32
35
|
import { computeFoundationDigest } from '../utils/code-upload.js'
|
|
33
36
|
import { checkFlags } from '../utils/flag-guard.js'
|
|
34
37
|
|
|
@@ -124,7 +127,8 @@ export async function status(args = []) {
|
|
|
124
127
|
// site.yml ref. The digest compare is read-only — it never builds, so it
|
|
125
128
|
// only fires when the local foundation is already built (dist present).
|
|
126
129
|
const local = resolveLocalFoundation(siteDir, siteYml)
|
|
127
|
-
const lookupName =
|
|
130
|
+
const lookupName =
|
|
131
|
+
(local && (await foundationScopedName(local.dir))) || fndScope
|
|
128
132
|
if (lookupName) fdnLatest = await client.readFoundationLatest(lookupName)
|
|
129
133
|
if (local?.dir && fdnLatest?.digest) {
|
|
130
134
|
const localDigest = computeFoundationDigest(join(local.dir, 'dist'))
|
package/src/framework-index.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schemaVersion": 1,
|
|
3
|
-
"generatedAt": "2026-09-
|
|
3
|
+
"generatedAt": "2026-09-21T23:16:47.962Z",
|
|
4
4
|
"packages": {
|
|
5
5
|
"@uniweb/api": {
|
|
6
6
|
"version": "0.4.1",
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
]
|
|
11
11
|
},
|
|
12
12
|
"@uniweb/build": {
|
|
13
|
-
"version": "0.
|
|
13
|
+
"version": "0.58.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.
|
|
38
|
+
"version": "0.29.8",
|
|
39
39
|
"path": "framework/core",
|
|
40
40
|
"deps": [
|
|
41
41
|
"@uniweb/semantic-parser",
|
|
@@ -55,7 +55,7 @@
|
|
|
55
55
|
]
|
|
56
56
|
},
|
|
57
57
|
"@uniweb/kit": {
|
|
58
|
-
"version": "0.19.
|
|
58
|
+
"version": "0.19.4",
|
|
59
59
|
"path": "framework/kit",
|
|
60
60
|
"deps": [
|
|
61
61
|
"@uniweb/core",
|
|
@@ -83,7 +83,7 @@
|
|
|
83
83
|
]
|
|
84
84
|
},
|
|
85
85
|
"@uniweb/runtime": {
|
|
86
|
-
"version": "0.27.
|
|
86
|
+
"version": "0.27.4",
|
|
87
87
|
"path": "framework/runtime",
|
|
88
88
|
"deps": [
|
|
89
89
|
"@uniweb/core",
|
|
@@ -116,7 +116,7 @@
|
|
|
116
116
|
"deps": []
|
|
117
117
|
},
|
|
118
118
|
"@uniweb/templates": {
|
|
119
|
-
"version": "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.
|
|
129
|
+
"version": "0.10.1",
|
|
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
|
|
@@ -1432,7 +1459,7 @@ ${colors.bright}Options:${colors.reset}
|
|
|
1432
1459
|
--org @org Publish under @org (membership-gated; alias: --as-org). Read
|
|
1433
1460
|
only on a site's FIRST publish — that create decides which org
|
|
1434
1461
|
owns the site, and whose storage its assets are charged to.
|
|
1435
|
-
Recorded
|
|
1462
|
+
Recorded in sync.json and replayed, so it is never
|
|
1436
1463
|
re-typed. Without it, you are asked once.
|
|
1437
1464
|
--personal Create the site under your personal account, deliberately.
|
|
1438
1465
|
Only needed on a first publish, and only to answer the owner
|