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.
- package/package.json +7 -7
- package/partials/agents.md +17 -15
- package/src/backend/client.js +231 -20
- package/src/backend/foundation-bring-along.js +71 -65
- package/src/backend/site-sync.js +255 -127
- package/src/commands/add.js +62 -4
- package/src/commands/clone.js +36 -6
- package/src/commands/publish.js +33 -20
- package/src/commands/pull.js +68 -15
- package/src/commands/push.js +23 -17
- package/src/commands/register.js +234 -37
- package/src/commands/rename.js +12 -9
- package/src/commands/status.js +32 -7
- package/src/framework-index.json +8 -8
- package/src/index.js +43 -10
- package/src/utils/args.js +6 -14
- package/src/utils/flag-guard.js +17 -9
- package/src/utils/foundation-name.js +179 -0
- package/src/utils/git.js +4 -1
- package/src/utils/registry-orgs.js +3 -2
- package/src/utils/scaffold.js +7 -5
- package/src/utils/site-data-upload.js +13 -2
- package/starter/foundation/main.js +2 -1
- package/templates/foundation/main.js.hbs +9 -0
- package/templates/site/site.yml.hbs +3 -2
|
@@ -36,10 +36,14 @@ import { readFileSync } from 'node:fs'
|
|
|
36
36
|
import { join } from 'node:path'
|
|
37
37
|
import { execFileSync } from 'node:child_process'
|
|
38
38
|
|
|
39
|
-
import {
|
|
39
|
+
import {
|
|
40
|
+
detectFoundationType,
|
|
41
|
+
isExtensionUrl,
|
|
42
|
+
readFoundationName,
|
|
43
|
+
checkFoundationName
|
|
44
|
+
} from '@uniweb/build'
|
|
40
45
|
import { computeFoundationDigest } from '../utils/code-upload.js'
|
|
41
46
|
import { isNonInteractive } from '../utils/interactive.js'
|
|
42
|
-
import { publishScope } from '../utils/registry-orgs.js'
|
|
43
47
|
import { compareSemverPrecedence } from '../utils/semver-precedence.js'
|
|
44
48
|
|
|
45
49
|
/**
|
|
@@ -48,9 +52,12 @@ import { compareSemverPrecedence } from '../utils/semver-precedence.js'
|
|
|
48
52
|
* already has it; nothing to do). Uses the SAME resolver the build uses
|
|
49
53
|
* (`detectFoundationType`), so "which foundation" never drifts between them.
|
|
50
54
|
*
|
|
55
|
+
* Its catalog name is `foundationScopedName(dir)` — a separate, async read, because
|
|
56
|
+
* the name lives in `main.js`, which is loaded rather than parsed.
|
|
57
|
+
*
|
|
51
58
|
* @param {string} siteDir
|
|
52
59
|
* @param {object} siteYml - parsed site.yml
|
|
53
|
-
* @returns {{ dir: string,
|
|
60
|
+
* @returns {{ dir: string, version: string|null }|null}
|
|
54
61
|
*/
|
|
55
62
|
export function resolveLocalFoundation(siteDir, siteYml) {
|
|
56
63
|
const decl = siteYml?.foundation
|
|
@@ -64,11 +71,7 @@ export function resolveLocalFoundation(siteDir, siteYml) {
|
|
|
64
71
|
return null
|
|
65
72
|
}
|
|
66
73
|
if (!info || info.type !== 'local' || !info.path) return null
|
|
67
|
-
return {
|
|
68
|
-
dir: info.path,
|
|
69
|
-
scopedName: foundationScopedName(info.path),
|
|
70
|
-
version: readPkgField(info.path, 'version')
|
|
71
|
-
}
|
|
74
|
+
return { dir: info.path, version: readPkgField(info.path, 'version') }
|
|
72
75
|
}
|
|
73
76
|
|
|
74
77
|
/**
|
|
@@ -82,7 +85,7 @@ export function resolveLocalFoundation(siteDir, siteYml) {
|
|
|
82
85
|
*
|
|
83
86
|
* @param {string} siteDir
|
|
84
87
|
* @param {object} siteYml - parsed site.yml
|
|
85
|
-
* @returns {Array<{ decl: string, dir: string,
|
|
88
|
+
* @returns {Array<{ decl: string, dir: string, version: string|null }>}
|
|
86
89
|
* `decl` is the authored declaration, which is the wire entry's `$id` — the key
|
|
87
90
|
* publish stamps the pinned ref back onto.
|
|
88
91
|
*/
|
|
@@ -107,41 +110,43 @@ export function resolveLocalExtensions(siteDir, siteYml) {
|
|
|
107
110
|
continue
|
|
108
111
|
}
|
|
109
112
|
if (!info || info.type !== 'local' || !info.path) continue
|
|
110
|
-
out.push({
|
|
111
|
-
decl,
|
|
112
|
-
dir: info.path,
|
|
113
|
-
scopedName: foundationScopedName(info.path),
|
|
114
|
-
version: readPkgField(info.path, 'version')
|
|
115
|
-
})
|
|
113
|
+
out.push({ decl, dir: info.path, version: readPkgField(info.path, 'version') })
|
|
116
114
|
}
|
|
117
115
|
return out
|
|
118
116
|
}
|
|
119
117
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
118
|
+
/**
|
|
119
|
+
* The foundation's scoped catalog name (`@org/name`): its name — `main.js`'s
|
|
120
|
+
* `name`, else package.json's, the one rule the build reads for the schema
|
|
121
|
+
* `register` submits (`readFoundationName`). ⭐ The scope is part of that name
|
|
122
|
+
* (2026-09-22), so a scoped name IS the catalog name. Null otherwise; the caller
|
|
123
|
+
* then treats the foundation as unreleased, and `register` — which it runs to
|
|
124
|
+
* release — is where a missing scope is chosen and written into the name, and a
|
|
125
|
+
* missing name is asked for.
|
|
126
|
+
*
|
|
127
|
+
* ⛔ A NAME THAT CANNOT REGISTER IS NULL, not looked up. `src` and `foundation`
|
|
128
|
+
* name a folder (`checkFoundationName`); looking one up would find some other
|
|
129
|
+
* project's `@org/src` — the very collision refusing them prevents.
|
|
130
|
+
*
|
|
131
|
+
* ⛔ A BARE NAME IS NULL too: it has not registered, since `register` writes the scope
|
|
132
|
+
* it registers under into the name. Until 2026-09-22 this joined a bare name with
|
|
133
|
+
* `package.json::uniweb.scope`; that key is refused now (`readFoundationName` throws,
|
|
134
|
+
* so a leftover reads as unreleased here and `register` says what to write instead).
|
|
135
|
+
* *(This read package.json alone until 2026-09-21 — `uniweb.id`, else `name` — and
|
|
136
|
+
* so named a foundation differently from the build whenever `main.js` named it.)*
|
|
137
|
+
*
|
|
138
|
+
* @param {string} dir - the foundation package
|
|
139
|
+
* @returns {Promise<string|null>}
|
|
140
|
+
*/
|
|
141
|
+
export async function foundationScopedName(dir) {
|
|
142
|
+
let name
|
|
135
143
|
try {
|
|
136
|
-
|
|
137
|
-
const name = pkg?.uniweb?.id || pkg?.name
|
|
138
|
-
if (typeof name === 'string' && name.startsWith('@')) return name
|
|
139
|
-
const scope = publishScope(pkg?.uniweb?.scope)
|
|
140
|
-
if (scope && name) return `${scope}/${name}`
|
|
141
|
-
return null
|
|
144
|
+
;({ name } = await readFoundationName(dir))
|
|
142
145
|
} catch {
|
|
143
146
|
return null
|
|
144
147
|
}
|
|
148
|
+
if (checkFoundationName(name)) return null
|
|
149
|
+
return name.startsWith('@') ? name : null
|
|
145
150
|
}
|
|
146
151
|
|
|
147
152
|
function readPkgField(dir, field) {
|
|
@@ -225,7 +230,7 @@ export async function bringFoundationAlong({
|
|
|
225
230
|
* it in the messages.
|
|
226
231
|
*
|
|
227
232
|
* @param {object} o
|
|
228
|
-
* @param {{dir: string,
|
|
233
|
+
* @param {{dir: string, version: string|null}} o.local
|
|
229
234
|
* @param {'foundation'|'extension'} o.kind
|
|
230
235
|
* @returns {Promise<{ released: boolean, proceed: boolean, ref: string|null }>}
|
|
231
236
|
*/
|
|
@@ -241,9 +246,10 @@ async function bringLocalCodeAlong({
|
|
|
241
246
|
verb = 'publish'
|
|
242
247
|
}) {
|
|
243
248
|
const Kind = kind === 'extension' ? 'Extension ' : 'Foundation'
|
|
249
|
+
const scopedName = await foundationScopedName(local.dir)
|
|
244
250
|
const label =
|
|
245
|
-
|
|
246
|
-
? `${
|
|
251
|
+
scopedName || local.version
|
|
252
|
+
? `${scopedName || kind}${local.version ? `@${local.version}` : ''}`
|
|
247
253
|
: `the local ${kind}`
|
|
248
254
|
const skipPrompts =
|
|
249
255
|
args.includes('--yes') ||
|
|
@@ -266,10 +272,11 @@ async function bringLocalCodeAlong({
|
|
|
266
272
|
const noRelease = args.includes('--no-release')
|
|
267
273
|
|
|
268
274
|
// The pinned ref to stamp on the pushed site — read at RETURN time (after any
|
|
269
|
-
// release), so it reflects the released version
|
|
270
|
-
// null when no scoped ref can be formed (then the site.yml
|
|
271
|
-
|
|
272
|
-
|
|
275
|
+
// release), so it reflects the released version, the scope register derived and
|
|
276
|
+
// the name it asked for. null when no scoped ref can be formed (then the site.yml
|
|
277
|
+
// ref is forwarded).
|
|
278
|
+
const pinnedRef = async () => {
|
|
279
|
+
const s = await foundationScopedName(local.dir)
|
|
273
280
|
const v = readPkgField(local.dir, 'version')
|
|
274
281
|
return s && v ? `${s}@${v}` : null
|
|
275
282
|
}
|
|
@@ -279,8 +286,8 @@ async function bringLocalCodeAlong({
|
|
|
279
286
|
// so on a bumped-but-unreleased foundation it names a version nobody can serve. Any
|
|
280
287
|
// branch that skips a release must bind to this instead.
|
|
281
288
|
const registeredRef = (reg) =>
|
|
282
|
-
|
|
283
|
-
? `${
|
|
289
|
+
scopedName && reg?.latest_version
|
|
290
|
+
? `${scopedName}@${reg.latest_version}`
|
|
284
291
|
: null
|
|
285
292
|
|
|
286
293
|
// Dry-run reports the intent WITHOUT touching the network — it must not force
|
|
@@ -290,24 +297,23 @@ async function bringLocalCodeAlong({
|
|
|
290
297
|
`${Kind} : ${label} — local; would release if changed or not yet registered`
|
|
291
298
|
)
|
|
292
299
|
// The ref still comes back where one can be formed: it is read from the
|
|
293
|
-
// foundation's own
|
|
294
|
-
//
|
|
295
|
-
//
|
|
300
|
+
// foundation's own files, so it costs no network, and an offline preview that
|
|
301
|
+
// omitted it would emit a document the real run would not — the one thing `-o`
|
|
302
|
+
// exists to avoid.
|
|
296
303
|
//
|
|
297
304
|
// ⚠️ It is null for a foundation that has NEVER been registered and carries no
|
|
298
|
-
// scope (a
|
|
299
|
-
//
|
|
300
|
-
//
|
|
301
|
-
//
|
|
302
|
-
// the first release there is
|
|
303
|
-
|
|
305
|
+
// scope (a fresh scaffold), because the scope is what `register` writes into its
|
|
306
|
+
// name (`settleFoundationScope`) — and for one with no name of its own (`src`),
|
|
307
|
+
// which `register` asks for. So the preview shows the authored value there, and the
|
|
308
|
+
// first real push — which releases, and so acquires both — sends the pinned ref
|
|
309
|
+
// instead. That gap is unavoidable offline: before the first release there is
|
|
310
|
+
// no registered name to name.
|
|
311
|
+
return { released: false, proceed: true, ref: await pinnedRef() }
|
|
304
312
|
}
|
|
305
313
|
|
|
306
314
|
// Ask the catalog what it has. Null → not registered (or the backend can't
|
|
307
315
|
// answer / no scoped name to look up) → release.
|
|
308
|
-
const reg =
|
|
309
|
-
? await client.readFoundationLatest(local.scopedName)
|
|
310
|
-
: null
|
|
316
|
+
const reg = scopedName ? await client.readFoundationLatest(scopedName) : null
|
|
311
317
|
|
|
312
318
|
if (!reg) {
|
|
313
319
|
// ⛔ Nothing to bind to. Releasing anyway would be the opposite of what was asked,
|
|
@@ -326,7 +332,7 @@ async function bringLocalCodeAlong({
|
|
|
326
332
|
return {
|
|
327
333
|
released: releaseFoundation(local, args, cliBin, say, client?.origin),
|
|
328
334
|
proceed: true,
|
|
329
|
-
ref: pinnedRef()
|
|
335
|
+
ref: await pinnedRef()
|
|
330
336
|
}
|
|
331
337
|
}
|
|
332
338
|
|
|
@@ -339,7 +345,7 @@ async function bringLocalCodeAlong({
|
|
|
339
345
|
say.dim(
|
|
340
346
|
`${Kind} : ${label} — unchanged since release (digest matches); nothing to release.`
|
|
341
347
|
)
|
|
342
|
-
return { released: false, proceed: true, ref: pinnedRef() }
|
|
348
|
+
return { released: false, proceed: true, ref: await pinnedRef() }
|
|
343
349
|
}
|
|
344
350
|
|
|
345
351
|
// A different version locally → a new version to release.
|
|
@@ -369,7 +375,7 @@ async function bringLocalCodeAlong({
|
|
|
369
375
|
return {
|
|
370
376
|
released: releaseFoundation(local, args, cliBin, say, client?.origin),
|
|
371
377
|
proceed: true,
|
|
372
|
-
ref: pinnedRef()
|
|
378
|
+
ref: await pinnedRef()
|
|
373
379
|
}
|
|
374
380
|
}
|
|
375
381
|
|
|
@@ -390,7 +396,7 @@ async function bringLocalCodeAlong({
|
|
|
390
396
|
say.dim(
|
|
391
397
|
'Proceeding without re-releasing — pass nothing to re-deliver, or bump the version to release a change.'
|
|
392
398
|
)
|
|
393
|
-
return { released: false, proceed: true, ref: pinnedRef() }
|
|
399
|
+
return { released: false, proceed: true, ref: await pinnedRef() }
|
|
394
400
|
}
|
|
395
401
|
const reRelease = await confirm(
|
|
396
402
|
`Re-release ${label} to be sure its code is current?`,
|
|
@@ -400,9 +406,9 @@ async function bringLocalCodeAlong({
|
|
|
400
406
|
return {
|
|
401
407
|
released: releaseFoundation(local, args, cliBin, say, client?.origin),
|
|
402
408
|
proceed: true,
|
|
403
|
-
ref: pinnedRef()
|
|
409
|
+
ref: await pinnedRef()
|
|
404
410
|
}
|
|
405
|
-
return { released: false, proceed: true, ref: pinnedRef() }
|
|
411
|
+
return { released: false, proceed: true, ref: await pinnedRef() }
|
|
406
412
|
}
|
|
407
413
|
|
|
408
414
|
// Case 3 (§4): the code was edited but the version wasn't bumped. The
|
|
@@ -438,7 +444,7 @@ async function bringLocalCodeAlong({
|
|
|
438
444
|
`Local ${label} differs from the registered ${reg.latest_version} and the version wasn't bumped — ` +
|
|
439
445
|
`shipping against the registered code. Your local changes will NOT be live.`
|
|
440
446
|
)
|
|
441
|
-
return { released: false, proceed: true, ref: pinnedRef() }
|
|
447
|
+
return { released: false, proceed: true, ref: await pinnedRef() }
|
|
442
448
|
}
|
|
443
449
|
|
|
444
450
|
// 2. Nobody to ask. Refuse, and name both real options as runnable commands so
|
|
@@ -476,7 +482,7 @@ async function bringLocalCodeAlong({
|
|
|
476
482
|
)
|
|
477
483
|
return { released: false, proceed: false, ref: null }
|
|
478
484
|
}
|
|
479
|
-
return { released: false, proceed: true, ref: pinnedRef() }
|
|
485
|
+
return { released: false, proceed: true, ref: await pinnedRef() }
|
|
480
486
|
}
|
|
481
487
|
|
|
482
488
|
// Build the foundation so its dist/ can be fingerprinted. Idempotent — the
|