uniweb 1.22.4 → 1.23.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": "1.22.4",
3
+ "version": "1.23.1",
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.3.2",
42
42
  "prompts": "^2.4.2",
43
43
  "tar": "^7.5.21",
44
- "@uniweb/core": "~1.4.12",
45
44
  "@uniweb/schemas": "~1.6.7",
46
- "@uniweb/content-writer": "~1.3.1",
47
- "@uniweb/semantic-parser": "~1.13.0"
45
+ "@uniweb/core": "~1.5.0",
46
+ "@uniweb/semantic-parser": "~1.13.0",
47
+ "@uniweb/content-writer": "~1.3.1"
48
48
  },
49
49
  "peerDependencies": {
50
50
  "@uniweb/semantic-parser": "~1.13.0",
51
51
  "@uniweb/content-reader": "~1.5.7",
52
- "@uniweb/build": "~1.11.4"
52
+ "@uniweb/build": "~1.12.1"
53
53
  },
54
54
  "peerDependenciesMeta": {
55
55
  "@uniweb/build": {
@@ -1155,7 +1155,7 @@ Four things you won't discover by reading exports:
1155
1155
 
1156
1156
  > **Content fields are HTML strings.** Covered above — it's the single most common rendering bug.
1157
1157
 
1158
- > **`<Link>` takes `to` or `href`.** `to="page:about"` resolves a page ID (survives moves), external URLs get `target="_blank"` automatically, and `reload` forces a full page load.
1158
+ > **`<Link>` takes `to` or `href`.** `to="page:about"` resolves a page ID (survives moves) or a route, never a folder: the homepage is `page:/`, or `page:home` only when its `page.yml` says `id: home`. External URLs get `target="_blank"` automatically, and `reload` forces a full page load.
1159
1159
 
1160
1160
  > **`cn()` gotcha — a later `text-<size>` silently drops an earlier `leading-*`.** Tailwind's size classes set line height too, so `cn()` treats the size as replacing the leading: `cn('leading-[1.1] text-4xl')` → `text-4xl`. Put the size first, or fold the leading into it (`text-[clamp(2rem,5vw,4rem)]/[1.1]`). Most likely to bite when the size comes from a lookup and the leading sits in a shared base string.
1161
1161
 
@@ -2267,6 +2267,22 @@ export async function pushSyncPackages({
2267
2267
  // which is the case this message now names. Suggesting it there sends the
2268
2268
  // user to re-fetch a map they already have.
2269
2269
  if (problem.detail) note(String(problem.detail))
2270
+ // ⭐ THE SITE'S FOLDER: one placement per record, keyed by the record it references, so
2271
+ // its placements are all new when its records are — every record moved to another data
2272
+ // schema, where a record keeps its type and each becomes a new one. The backend cannot
2273
+ // tell that from identity lost, so it refuses, and the route is an empty folder first.
2274
+ // ⛔ Until 2026-10-29 this said nothing more, and the type-change advice below led here:
2275
+ // thirteen gallery records moving from @std/project to their foundation's own schema.
2276
+ if (label === 'records' && where === 'contents') {
2277
+ note(
2278
+ "That section is the site's folder, one placement per record, and a placement is new when its " +
2279
+ 'record is — as when every record has moved to another data schema.'
2280
+ )
2281
+ note(
2282
+ 'To replace every record in the folder: push once with the records directory there and holding ' +
2283
+ 'nothing at all (the push asks, then removes them), then put the records back and push again.'
2284
+ )
2285
+ }
2270
2286
  return null
2271
2287
  }
2272
2288
  // ⭐ A RECORD KEEPS ITS TYPE. A record file moved to another type's folder keeps its `$uuid`, and
@@ -2279,6 +2295,13 @@ export async function pushSyncPackages({
2279
2295
  'A record keeps its type. To store it as the new type, make it a new record: delete the ' +
2280
2296
  '`$uuid` line from its file, then push again.'
2281
2297
  )
2298
+ // ⚠️ Not the whole route when every record of the folder moves at once: each is then new,
2299
+ // and so is every placement, which the folder refuses (`identity_required` above).
2300
+ note(
2301
+ 'Moving every record in the folder to a new type? Then the folder refuses that push too: empty it ' +
2302
+ 'first — push once with the records directory there and holding nothing at all — then put the ' +
2303
+ 'records back without `$uuid` and push again.'
2304
+ )
2282
2305
  return null
2283
2306
  }
2284
2307
  if (problem?.reason === 'stale_base') {
@@ -89,7 +89,7 @@ import {
89
89
  describeSyncedElsewhere
90
90
  } from '../utils/site-identity.js'
91
91
  import { isNonInteractive, confirm } from '../utils/interactive.js'
92
- import { guardEmptyRecords } from '../utils/records-guard.js'
92
+ import { guardEmptyRecords, noteFolderLeftAlone } from '../utils/records-guard.js'
93
93
  import { findSiteCopies, describeSiteCopies } from '../utils/site-copies.js'
94
94
  import { headProvenance } from '../utils/git.js'
95
95
  import {
@@ -964,6 +964,8 @@ export async function publish(args = []) {
964
964
  return { exitCode: 1 }
965
965
  }
966
966
  for (const w of pkg.warnings) say.dim(`! ${w}`)
967
+ // A folder this publish leaves alone while the backend's still holds records — see `uniweb push`.
968
+ await noteFolderLeftAlone({ pkg, siteDir, backend: client.origin, note: (m) => say.dim(m) })
967
969
  if (refuseUnsendableRecords(pkg.refusals, { error: say.err, note: say.dim })) {
968
970
  return { exitCode: 1 }
969
971
  }
@@ -79,7 +79,7 @@ import {
79
79
  } from '../utils/site-identity.js'
80
80
  import { confirm, isNonInteractive } from '../utils/interactive.js'
81
81
  import { announceServices, foundationSupports } from '../backend/service-request.js'
82
- import { guardEmptyRecords } from '../utils/records-guard.js'
82
+ import { guardEmptyRecords, noteFolderLeftAlone } from '../utils/records-guard.js'
83
83
  import { bringFoundationAlong } from '../backend/foundation-bring-along.js'
84
84
  import { repoOf } from '../utils/sync-point.js'
85
85
  import {
@@ -627,6 +627,9 @@ export async function push(args = [], deps = {}) {
627
627
  const { siteContent, records, siteContentUuid, warnings, skipped } = pkg
628
628
  log('')
629
629
  for (const w of warnings) note(`! ${w}`)
630
+ // A folder this push leaves alone while the backend's still holds records — said, since an author
631
+ // who took the records out expects them gone (`noteFolderLeftAlone`).
632
+ await noteFolderLeftAlone({ pkg, siteDir, backend: client.origin, note })
630
633
  // The same stop for the paths the probe above does not run on — `-o` and
631
634
  // `--dry-run` say what a push would do, and a push would stop here.
632
635
  if (refuseUnsendableRecords(pkg.refusals, { error, note })) return { exitCode: 1 }
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "schemaVersion": 1,
3
- "generatedAt": "2026-10-29T18:30:31.506Z",
3
+ "generatedAt": "2026-10-30T01:27:00.910Z",
4
4
  "packages": {
5
5
  "@uniweb/api": {
6
- "version": "1.6.6"
6
+ "version": "1.6.7"
7
7
  },
8
8
  "@uniweb/build": {
9
- "version": "1.11.4"
9
+ "version": "1.12.1"
10
10
  },
11
11
  "@uniweb/content-reader": {
12
12
  "version": "1.5.7"
@@ -15,16 +15,16 @@
15
15
  "version": "1.3.1"
16
16
  },
17
17
  "@uniweb/core": {
18
- "version": "1.4.12"
18
+ "version": "1.5.0"
19
19
  },
20
20
  "@uniweb/frame-bridge": {
21
21
  "version": "1.0.2"
22
22
  },
23
23
  "@uniweb/icons": {
24
- "version": "1.0.6"
24
+ "version": "1.0.7"
25
25
  },
26
26
  "@uniweb/kit": {
27
- "version": "1.6.7"
27
+ "version": "1.6.8"
28
28
  },
29
29
  "@uniweb/loom": {
30
30
  "version": "1.1.2"
@@ -33,10 +33,10 @@
33
33
  "version": "1.1.4"
34
34
  },
35
35
  "@uniweb/projections": {
36
- "version": "1.2.7"
36
+ "version": "1.2.8"
37
37
  },
38
38
  "@uniweb/runtime": {
39
- "version": "1.2.13"
39
+ "version": "1.2.14"
40
40
  },
41
41
  "@uniweb/scene": {
42
42
  "version": "1.0.1"
@@ -60,7 +60,7 @@
60
60
  "version": "1.2.1"
61
61
  },
62
62
  "@uniweb/unipress": {
63
- "version": "1.1.17"
63
+ "version": "1.1.18"
64
64
  }
65
65
  },
66
66
  "templates": {
@@ -86,3 +86,37 @@ export async function guardEmptyRecords({ siteDir, backend, args = [], warn, not
86
86
  const yes = await confirm(`Remove ${count} record${count === 1 ? '' : 's'} from the site's folder?`, false)
87
87
  return { ok: yes, count }
88
88
  }
89
+
90
+ /**
91
+ * Say when a push leaves the backend's folder as it is although it holds records.
92
+ *
93
+ * A records directory that holds files but no records — data with no schema, say — states no
94
+ * folder, by design (`sendsFolder` in `@uniweb/build`): that is not an author emptying it. But an
95
+ * author who has just taken the records out and kept such files expects them gone from the
96
+ * backend, and the push said nothing (2026-10-29: a gallery's records moving to another schema,
97
+ * its schema-less articles left beside them). So where this copy holds placements from an earlier
98
+ * push or pull, say they stay, and how they would go.
99
+ *
100
+ * @param {object} params
101
+ * @param {object} params.pkg - what `emitSyncPackages` returned; `sendsFolder: false` is the case
102
+ * @param {string} params.siteDir
103
+ * @param {string} params.backend - whose placements to count
104
+ * @param {(m: string) => void} params.note - the caller's reporter
105
+ * @returns {Promise<number>} how many records the backend's folder keeps, as this copy last saw it
106
+ */
107
+ export async function noteFolderLeftAlone({ pkg, siteDir, backend, note }) {
108
+ if (pkg?.sendsFolder !== false) return 0
109
+ const count = countPlacedRecords(readFolderItemUuids(siteDir, backend))
110
+ if (count === 0) return 0
111
+ const pool = await readEntityPool(siteDir)
112
+ const records = count === 1 ? '1 record' : `${count} records`
113
+ note(
114
+ `! The site's folder on the backend keeps its ${records}: this push leaves it as it is, because ` +
115
+ (pool.exists ? `${pool.dir}/ holds files but no records of a data schema.` : `there is no ${pool.dir}/ here.`)
116
+ )
117
+ note(
118
+ ` To remove ${count === 1 ? 'it' : 'them'} from the backend, push once with ${pool.dir}/ there and holding nothing at all ` +
119
+ `— the push asks first.`
120
+ )
121
+ return count
122
+ }