uniweb 0.25.5 → 0.26.1
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": "0.
|
|
3
|
+
"version": "0.26.1",
|
|
4
4
|
"description": "Create structured Vite + React sites with content/code separation",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -43,8 +43,8 @@
|
|
|
43
43
|
"tar": "^7.0.0",
|
|
44
44
|
"@uniweb/core": "^0.10.1",
|
|
45
45
|
"@uniweb/kit": "^0.12.3",
|
|
46
|
-
"@uniweb/
|
|
47
|
-
"@uniweb/
|
|
46
|
+
"@uniweb/semantic-parser": "^1.2.3",
|
|
47
|
+
"@uniweb/runtime": "^0.12.2"
|
|
48
48
|
},
|
|
49
49
|
"peerDependencies": {
|
|
50
50
|
"@uniweb/build": "^0.24.5",
|
|
@@ -338,23 +338,58 @@ async function bringLocalCodeAlong({
|
|
|
338
338
|
// Case 3 (§4): the code was edited but the version wasn't bumped. The
|
|
339
339
|
// registered version is immutable, so we never silently ship the old code —
|
|
340
340
|
// the deliberate release gate is a version bump (§3.1).
|
|
341
|
+
// ⭐ THREE OUTCOMES, NOT TWO — `--yes` and "no TTY" are not the same answer.
|
|
342
|
+
//
|
|
343
|
+
// They were one condition until 2026-08-19, and conflating them meant the only
|
|
344
|
+
// party that never got asked was the one that most needed asking. A `--yes` is a
|
|
345
|
+
// decision someone made in advance; an absent TTY is the ABSENCE of a decision,
|
|
346
|
+
// and absence is not consent.
|
|
347
|
+
//
|
|
348
|
+
// ⚖️ Which flips the safe default by caller, for a reason particular to this
|
|
349
|
+
// situation: proceeding here ships a site bound to code that is NOT the code in
|
|
350
|
+
// the working tree. A human is at a terminal, reads the warning, and decides. An
|
|
351
|
+
// agent re-runs commands for free and reports "pushed" on exit 0 — so for it the
|
|
352
|
+
// cheap outcome is one more cycle and the expensive one is a silent wrong success
|
|
353
|
+
// it will not look at again. Refusing costs an agent a retry; proceeding costs it
|
|
354
|
+
// a false completion report and a live site nobody notices is stale.
|
|
355
|
+
|
|
356
|
+
// 1. Explicit consent — proceed, but at WARN. `dim` is what we print for things
|
|
357
|
+
// nobody needs to read, and "your changes are not live" is not that.
|
|
358
|
+
if (skipPrompts) {
|
|
359
|
+
say.warn(
|
|
360
|
+
`Local ${label} differs from the registered ${reg.latest_version} and the version wasn't bumped — ` +
|
|
361
|
+
`shipping against the registered code. Your local changes will NOT be live.`
|
|
362
|
+
)
|
|
363
|
+
return { released: false, proceed: true, ref: pinnedRef() }
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
// 2. Nobody to ask. Refuse, and name both real options as runnable commands so
|
|
367
|
+
// the caller's next step needs no interpretation. `refused` marks this as a
|
|
368
|
+
// hard stop rather than a human's "no", which callers map to different exits.
|
|
369
|
+
if (isNonInteractive(args)) {
|
|
370
|
+
say.err(
|
|
371
|
+
`Local ${kind} ${label} differs from the registered ${reg.latest_version}, and the version wasn't bumped.`
|
|
372
|
+
)
|
|
373
|
+
say.dim(`Nothing was sent. Either release the change, or ship without it:`)
|
|
374
|
+
say.dim(` • bump the ${kind}'s version in package.json, then re-run \`uniweb ${verb}\``)
|
|
375
|
+
say.dim(` • \`uniweb ${verb} --yes\` — sends content bound to the registered ${reg.latest_version}`)
|
|
376
|
+
return { released: false, proceed: false, refused: true, ref: null }
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
// 3. A human is here. State it, then ask.
|
|
341
380
|
say.warn(
|
|
342
381
|
`Your local ${label} differs from the registered version ${reg.latest_version}, but the version wasn't bumped.`
|
|
343
382
|
)
|
|
344
383
|
say.dim(
|
|
345
|
-
`A registered version is immutable
|
|
384
|
+
`A registered version is immutable — bump the ${kind}'s version to release the change.`
|
|
346
385
|
)
|
|
347
|
-
if (skipPrompts || isNonInteractive(args)) {
|
|
348
|
-
say.dim(`Proceeding with the already-registered ${reg.latest_version}.`)
|
|
349
|
-
return { released: false, proceed: true, ref: pinnedRef() }
|
|
350
|
-
}
|
|
351
386
|
const proceed = await confirm(
|
|
352
387
|
`Continue with the already-registered ${reg.latest_version} anyway?`,
|
|
353
388
|
false
|
|
354
389
|
)
|
|
355
390
|
if (!proceed) {
|
|
356
391
|
say.info(
|
|
357
|
-
`Aborted —
|
|
392
|
+
`Aborted — nothing was sent. Bump the ${kind} version, then re-run \`uniweb ${verb}\`.`
|
|
358
393
|
)
|
|
359
394
|
return { released: false, proceed: false, ref: null }
|
|
360
395
|
}
|
package/src/commands/publish.js
CHANGED
|
@@ -428,7 +428,11 @@ export async function publish(args = []) {
|
|
|
428
428
|
return { exitCode: 1 }
|
|
429
429
|
}
|
|
430
430
|
if (!ext.proceed) return { exitCode: 1 }
|
|
431
|
-
|
|
431
|
+
// A human who answered "no" chose this, so it is not a failure — exit 0. A
|
|
432
|
+
// REFUSAL is different: nobody was asked, nothing shipped, and the caller is
|
|
433
|
+
// usually an agent that reads the exit code and reports done. Exit 0 there would
|
|
434
|
+
// be the silent-wrong-success this whole branch exists to prevent.
|
|
435
|
+
if (!fnd.proceed) return { exitCode: fnd.refused ? 1 : 0 }
|
|
432
436
|
|
|
433
437
|
// 2. Build the site data (link mode): dist/site-content.json (+ per-locale),
|
|
434
438
|
// dist/data/*, dist/_search/*, dist/assets/*. Spawn the SAME CLI binary so
|
package/src/commands/push.js
CHANGED
|
@@ -241,6 +241,33 @@ export async function push(args = [], deps = {}) {
|
|
|
241
241
|
// `index` — the per-entity source-file map for back-fill, correlated by submission
|
|
242
242
|
// position. Non-local Models are fetched from the registry on demand. `priorHashes`
|
|
243
243
|
// (the .uniweb push-cache) drives "send only changed" across both lanes; --all bypasses.
|
|
244
|
+
// ⛔ DROP STALE REMOTE STATE FIRST — this must precede the hash read below, and it
|
|
245
|
+
// did not until 2026-08-19.
|
|
246
|
+
//
|
|
247
|
+
// The guard itself is right and its docblock names this exact outcome: on a clone
|
|
248
|
+
// whose `$uuid` was cleared to re-publish as a new site — WHICH IS OUR OWN
|
|
249
|
+
// DOCUMENTED RECOVERY — send-only-changed would "skip every entity whose content
|
|
250
|
+
// had not changed since the OLD site's last push, so the NEW site would come up
|
|
251
|
+
// missing exactly the content that did not change." It was defeated twice over:
|
|
252
|
+
//
|
|
253
|
+
// · it ran AFTER `readSyncCache`, so the emit still diffed against the old
|
|
254
|
+
// site's hashes even though the file on disk had just been emptied;
|
|
255
|
+
// · it sat inside `if (mediaRefs.length)`, so a site with no local images never
|
|
256
|
+
// reached it at all.
|
|
257
|
+
//
|
|
258
|
+
// Measured 2026-08-19 against a live uniwebd: clearing `$uuid` and pushing created
|
|
259
|
+
// the new site and pulled back **0 pages, 0 sections**. Successful exit, empty site,
|
|
260
|
+
// nothing to indicate it — the failure the guard was written to prevent.
|
|
261
|
+
//
|
|
262
|
+
// Still skipped for an offline emit: `-o` / `--dry-run` must not mutate project state.
|
|
263
|
+
if (!output && !dryRun) {
|
|
264
|
+
const dropped = clearRemoteSyncStateIfUnbound(siteDir)
|
|
265
|
+
if (dropped.length) {
|
|
266
|
+
note(
|
|
267
|
+
`Cleared stale sync state from a previous site (${dropped.join(', ')}).`
|
|
268
|
+
)
|
|
269
|
+
}
|
|
270
|
+
}
|
|
244
271
|
const priorHashes = readSyncCache(siteDir)
|
|
245
272
|
// Per-item identity, without which the backend reads every record as new and
|
|
246
273
|
// recreates every page and section row.
|
|
@@ -324,13 +351,23 @@ export async function push(args = [], deps = {}) {
|
|
|
324
351
|
// The site has to exist before its bytes do — an upload with no owning
|
|
325
352
|
// entity is charged and cannot be freed, because freeing means deleting the
|
|
326
353
|
// owner. A no-op once `$uuid` is set, so only a never-synced site pays.
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
354
|
+
// ⛔ The PINNED ref, not site.yml's. The create is a THIRD writer of this
|
|
355
|
+
// value — the emit below stamps it via `injectInfo`, publish passes it here,
|
|
356
|
+
// and push did neither until 2026-08-19: it sent the authored alias (`src`),
|
|
357
|
+
// which names a foundation no deployment can resolve. A site created that way
|
|
358
|
+
// keeps the bad ref, and the backend's create guard now refuses it outright
|
|
359
|
+
// (channel backend-framework-787e, their measurement).
|
|
360
|
+
//
|
|
361
|
+
// ⭐ Same shape as the send-only-changed defect fixed this morning, and missed
|
|
362
|
+
// for the same reason: the rule was applied at the writers under discussion and
|
|
363
|
+
// a third caller of the same value was never enumerated.
|
|
364
|
+
const site = await ensureSiteExists({
|
|
365
|
+
client,
|
|
366
|
+
siteDir,
|
|
367
|
+
asOrg,
|
|
368
|
+
note,
|
|
369
|
+
...(fnd.ref ? { foundation: fnd.ref } : {})
|
|
370
|
+
})
|
|
334
371
|
if (!site.uuid) {
|
|
335
372
|
error(`Could not create the site on the backend: ${site.reason}`)
|
|
336
373
|
note('Nothing was uploaded and nothing was charged.')
|