uniweb 0.89.1 → 0.89.3

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.89.1",
3
+ "version": "0.89.3",
4
4
  "description": "Create structured Vite + React sites with content/code separation",
5
5
  "type": "module",
6
6
  "bin": {
@@ -41,17 +41,15 @@
41
41
  "js-yaml": "^4.1.0",
42
42
  "prompts": "^2.4.2",
43
43
  "tar": "^7.0.0",
44
- "@uniweb/core": "^0.38.1",
45
- "@uniweb/kit": "^0.20.0",
46
- "@uniweb/runtime": "^0.29.6",
47
- "@uniweb/semantic-parser": "^1.5.1",
48
- "@uniweb/content-writer": "^0.3.4",
49
- "@uniweb/schemas": "^0.13.0"
44
+ "@uniweb/content-writer": "~0.3.4",
45
+ "@uniweb/semantic-parser": "~1.5.1",
46
+ "@uniweb/schemas": "~0.13.0",
47
+ "@uniweb/core": "~0.38.2"
50
48
  },
51
49
  "peerDependencies": {
52
- "@uniweb/semantic-parser": "^1.5.1",
53
- "@uniweb/build": "^0.80.3",
54
- "@uniweb/content-reader": "^1.2.8"
50
+ "@uniweb/content-reader": "~1.2.8",
51
+ "@uniweb/build": "~0.80.4",
52
+ "@uniweb/semantic-parser": "~1.5.1"
55
53
  },
56
54
  "peerDependenciesMeta": {
57
55
  "@uniweb/build": {
@@ -1016,6 +1016,8 @@ src/ # the foundation package (folder name is `src`)
1016
1016
 
1017
1017
  **Discovery:** PascalCase files and folders at the root of `sections/` are auto-discovered. Nested levels require a `meta.js`. Lowercase directories are organizational only. `hidden: true` excludes a component entirely. Everything outside `sections/` and `layouts/` is ordinary React.
1018
1018
 
1019
+ **JSX goes in `.jsx` files** (or `.tsx`). Discovery also accepts a `.js` component, but only one without JSX: JSX compiles in no `.js` file, and the build stops on one, naming the file to rename.
1020
+
1019
1021
  **Source root.** The foundation package's source lives at the package root — the `src/` folder *is* the foundation. The build reads `package.json::main` (for new scaffolds, `main: "./_entry.generated.js"`). Older foundations nest source in `foundation/src/` with `main` pointing at `./src/_entry.generated.js`; both work through the same code path.
1020
1022
 
1021
1023
  **Import aliases.** Foundations include subpath imports for shared internals — use them instead of brittle relative paths:
@@ -1032,7 +1034,7 @@ import LessonHeader from '../../components/LessonHeader' // ❌ breaks if you re
1032
1034
 
1033
1035
  Within the same directory, use normal relative imports (`./AIFeedbackCard`).
1034
1036
 
1035
- **Foundation entry (`main.js`).** A single `export default { … }` whose top-level keys are the foundation's identity — `name` (what it registers as, `@org/<name>`) and `description` — and the capabilities it provides — `defaultLayout`, `defaultSection`, `viewTransitions`, `defaultInsets`, `xref`, `outputs`, `handlers` — plus an optional named `vars` export. The capabilities are read at render; the one thing a foundation declares that *isn't* — which host services it supports — lives in `package.json` instead (see [Declaring what your foundation supports](#declaring-what-your-foundation-supports)). Section types and layouts are auto-discovered and merged in by `@uniweb/build`. The build carries into `default.capabilities` in `dist/entry.js` only what a published page reads of your default export — the code it calls (`handlers`, `defaultInsets`, `xref`, `transports`, `outputs`) and its data, lean (`defaultLayout`, `defaultSection`, `viewTransitions`, `scroll`, each var's `default`, `type` and `applyTo`, each `data:` key's schema ref). `name`, `description`, a var's label or description and any key the framework does not read stay out of the published bundle: they are for the editor, which reads them from the foundation's schema. You never write that wrapper. The file is `main.js` — never `main.jsx`, which the build refuses; `main.js` may hold JSX. The one place it matters: when you import your **own** `main.js` from a component (e.g. a download button calling `compileDocument(website, { foundation })`), you get the bare default object — pass it through directly, Press handles both shapes.
1037
+ **Foundation entry (`main.js`).** A single `export default { … }` whose top-level keys are the foundation's identity — `name` (what it registers as, `@org/<name>`) and `description` — and the capabilities it provides — `defaultLayout`, `defaultSection`, `viewTransitions`, `defaultInsets`, `xref`, `outputs`, `handlers` — plus an optional named `vars` export. The capabilities are read at render; the one thing a foundation declares that *isn't* — which host services it supports — lives in `package.json` instead (see [Declaring what your foundation supports](#declaring-what-your-foundation-supports)). Section types and layouts are auto-discovered and merged in by `@uniweb/build`. The build carries into `default.capabilities` in `dist/entry.js` only what a published page reads of your default export — the code it calls (`handlers`, `defaultInsets`, `xref`, `transports`, `outputs`) and its data, lean (`defaultLayout`, `defaultSection`, `viewTransitions`, `scroll`, each var's `default`, `type` and `applyTo`, each `data:` key's schema ref). `name`, `description`, a var's label or description and any key the framework does not read stay out of the published bundle: they are for the editor, which reads them from the foundation's schema. You never write that wrapper. The file is `main.js` — never `main.jsx`, which the build refuses — and it is plain JavaScript: a component it declares (in `defaultInsets`, `xref`) lives in a `.jsx` file it imports. The one place it matters: when you import your **own** `main.js` from a component (e.g. a download button calling `compileDocument(website, { foundation })`), you get the bare default object — pass it through directly, Press handles both shapes.
1036
1038
 
1037
1039
  ### Props interface
1038
1040
 
@@ -2580,6 +2582,29 @@ takes them there already signed in. ⛔ **Navigate to it, never `fetch` it**: th
2580
2582
  a redirect carrying a one-time credential no script on the site is to hold. It decides
2581
2583
  what to offer, not what is allowed — on the site the operator acts as a member.
2582
2584
 
2585
+ ### The links the backend mails
2586
+
2587
+ A sign-up's confirmation, a password reset and an invitation to an address with no
2588
+ account all arrive as a link to the site's root, a token in the query — `?verify=`,
2589
+ `?reset=`, `?offer=` — and the page it lands on finishes the step:
2590
+
2591
+ ```jsx
2592
+ const { step, status, result, error, confirm } = useMailedLink()
2593
+ // step: null | 'verify' | 'reset' | 'offer' status: 'idle' | 'working' | 'done' | 'error'
2594
+ ```
2595
+
2596
+ - `verify` finishes by itself; then the member signs in.
2597
+ - `reset` waits for `confirm({ new_password })`, and the hook supplies the token. A
2598
+ password the rule refuses is `error.extensions.reason === 'password_too_short'`, and
2599
+ the same link works again.
2600
+ - `offer` waits for a sign-in, then claims by itself. `result` is counts,
2601
+ `{ invitations, grants }`, never names. ⛔ `error.kind === 'absent'` means unknown,
2602
+ revoked or already used, alike: one sentence, no cause.
2603
+
2604
+ Render it where the root renders it — a header or a layout is on every page. ⛔ Never
2605
+ copy the token anywhere: it is a credential, it stays in the URL until the backend has
2606
+ judged it, and then the hook takes it off.
2607
+
2583
2608
  ### A backend on your machine
2584
2609
 
2585
2610
  You do not need a live backend to build against one. In `site.yml`:
@@ -2601,7 +2626,9 @@ supplies on its own origin, so cookies behave exactly as they will in production
2601
2626
  no address of your own for it: under `services:` an address means a backend you run, and
2602
2627
  asks your host to leave its own off. It **enforces** who
2603
2628
  may edit an entry and `append_only`, so a permission you are relying on fails on your
2604
- machine rather than in front of a user. State is in memory; restart to reset.
2629
+ machine rather than in front of a user. The mail it would send is printed in the dev
2630
+ server's terminal, its link on your site's root — follow it to try your page; seed
2631
+ `offers: [{ email }]` to try an invitation. State is in memory; restart to reset.
2605
2632
 
2606
2633
  `uniweb create my-event --template conference` is a worked example of all of this.
2607
2634
 
@@ -260,8 +260,8 @@ export function printSurvey(
260
260
  } else if (row.status === 'behind') {
261
261
  icon = `${colors.yellow}✗${colors.reset}`
262
262
  // ⛔ `behind` alone was the whole message, in one colour, for every
263
- // distance. In 0.x the minor slot is where our breaking changes live
264
- // (`publish.js` derives it from a breaking marker and nothing else), so
263
+ // distance. The minor slot is where our breaking changes live, in 1.x as in
264
+ // 0.x (`publish.js` derives it from a breaking marker and nothing else), so
265
265
  // `^0.14.1 → ^0.16.0` is two releases a consumer must act on and
266
266
  // `^0.15.0 → ^0.15.2` is not — and the table said the same thing about
267
267
  // both. Naming the class is the difference between a list and a signal.
@@ -289,7 +289,7 @@ export function printSurvey(
289
289
  // artifact is a log. This block names the packages, so "what did that
290
290
  // upgrade cross?" is answerable afterwards from the log alone.
291
291
  //
292
- // ⚖️ It reports; it does not gate. Crossing a 0.x minor IS the ordinary way
292
+ // ⚖️ It reports; it does not gate. Crossing a minor IS the ordinary way
293
293
  // to take a Uniweb update — gating it would gate nearly every real upgrade
294
294
  // and make the verb useless. The decision stays the operator's; what changed
295
295
  // is that they can now make it knowingly.
@@ -305,7 +305,7 @@ export function printSurvey(
305
305
  log(` ${colors.red}${name}${colors.reset} ${r.current} → ${r.target} ${colors.dim}(${r.bump})${colors.reset}`)
306
306
  }
307
307
  log(
308
- ` ${colors.dim}In 0.x the minor slot is where breaking changes go, so these are releases${colors.reset}`
308
+ ` ${colors.dim}A minor version is where Uniweb's breaking changes go, so these are releases${colors.reset}`
309
309
  )
310
310
  log(
311
311
  ` ${colors.dim}you may need to act on. Read their changelogs before shipping.${colors.reset}`
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "schemaVersion": 1,
3
- "generatedAt": "2026-10-11T15:34:07.453Z",
3
+ "generatedAt": "2026-10-11T19:38:43.125Z",
4
4
  "packages": {
5
5
  "@uniweb/api": {
6
- "version": "0.8.0"
6
+ "version": "0.9.1"
7
7
  },
8
8
  "@uniweb/build": {
9
- "version": "0.80.3"
9
+ "version": "0.80.4"
10
10
  },
11
11
  "@uniweb/content-reader": {
12
12
  "version": "1.2.8"
@@ -15,16 +15,16 @@
15
15
  "version": "0.3.4"
16
16
  },
17
17
  "@uniweb/core": {
18
- "version": "0.38.1"
18
+ "version": "0.38.2"
19
19
  },
20
20
  "@uniweb/frame-bridge": {
21
21
  "version": "0.3.2"
22
22
  },
23
23
  "@uniweb/icons": {
24
- "version": "0.4.29"
24
+ "version": "0.4.30"
25
25
  },
26
26
  "@uniweb/kit": {
27
- "version": "0.20.0"
27
+ "version": "0.20.1"
28
28
  },
29
29
  "@uniweb/loom": {
30
30
  "version": "0.2.5"
@@ -33,10 +33,10 @@
33
33
  "version": "0.4.18"
34
34
  },
35
35
  "@uniweb/projections": {
36
- "version": "0.7.15"
36
+ "version": "0.7.16"
37
37
  },
38
38
  "@uniweb/runtime": {
39
- "version": "0.29.6"
39
+ "version": "0.29.7"
40
40
  },
41
41
  "@uniweb/scene": {
42
42
  "version": "0.1.3"
@@ -60,7 +60,7 @@
60
60
  "version": "0.2.0"
61
61
  },
62
62
  "@uniweb/unipress": {
63
- "version": "0.10.24"
63
+ "version": "0.10.25"
64
64
  }
65
65
  },
66
66
  "templates": {
@@ -36,8 +36,8 @@ export function stripVersionRange(spec) {
36
36
  /**
37
37
  * How far a bump moves, in the terms our versioning actually uses.
38
38
  *
39
- * ⭐ **In 0.x the MINOR slot is the breaking one, and it is the only channel we
40
- * have.** `scripts/framework/publish.js` derives a package's bump from its own
39
+ * ⭐ **The MINOR slot is the breaking one — in 1.x as in 0.x** *[ruled
40
+ * 2026-10-11]*. `scripts/framework/publish.js` derives a package's bump from its own
41
41
  * commits: a breaking marker means minor, everything else patch. So
42
42
  * `^0.14.1 → ^0.16.0` is two breaking releases and `^0.15.0 → ^0.15.2` is not —
43
43
  * and until 2026-09-02 `update` printed both as `behind`, in the same colour,
@@ -64,10 +64,11 @@ export function bumpClass(from, to) {
64
64
  /**
65
65
  * Is this crossing one a consumer must act on?
66
66
  *
67
- * A major always is. A minor is **when the major is 0**, because that is where
68
- * our scheme puts breaking changes — and npm agrees, which is the check that
69
- * makes this more than our own convention: `^0.14.1` admits `0.14.x` and
70
- * refuses `0.15.0`, so the range itself already treats the slot as a wall.
67
+ * A major, or a minor — at every major. Our scheme ships a breaking change as a minor
68
+ * bump, in 1.x as in 0.x *[ruled 2026-10-11]*, and the ranges a project carries (`~`)
69
+ * treat the minor slot as the wall: `~1.4.0` admits 1.4.x and refuses 1.5.0, as
70
+ * `^0.14.1` refuses 0.15.0. ⛔ Until then a minor counted only below 1.0, where a caret
71
+ * refuses it — so this would have stopped warning at the first 1.x minor.
71
72
  *
72
73
  * @param {string} from
73
74
  * @param {string} to
@@ -75,9 +76,7 @@ export function bumpClass(from, to) {
75
76
  */
76
77
  export function isBreakingBump(from, to) {
77
78
  const cls = bumpClass(from, to)
78
- if (cls === 'major') return true
79
- if (cls !== 'minor') return false
80
- return Number(stripVersionRange(to).split('.')[0]) === 0
79
+ return cls === 'major' || cls === 'minor'
81
80
  }
82
81
 
83
82
  /**
package/src/versions.js CHANGED
@@ -9,26 +9,33 @@
9
9
  *
10
10
  * ## How versions are resolved
11
11
  *
12
- * 1. Start from the CLI's own `package.json` dependencies. When the CLI
13
- * was installed via npm, pnpm has already resolved every `workspace:*`
14
- * spec into a concrete version like `^0.9.1`, so this step is
15
- * usually enough.
16
- * 2. For any dep that is still `workspace:*` (i.e. the CLI is running
17
- * *from* the pnpm workspace — local dev, sandbox script, tests), fall
18
- * back to reading the real `package.json` of each sibling package
19
- * under `framework/<name>/` and using `^<that version>`. This is the
20
- * path that keeps local sandboxes and templates aligned with whatever
21
- * was last bumped in the monorepo.
22
- * 3. Scan `framework/*` to catch any additional `@uniweb/*` package that
23
- * wasn't explicitly listed as a CLI dep (press, loom, scholar, etc.).
24
- * New packages get picked up by the scaffolder automatically.
25
- *
26
- * The previous implementation shipped a hardcoded fallback table and
27
- * nothing kept it in sync with reality — see the audit note in
28
- * `framework/CLAUDE.md` under "Publishing". Every workspace-based scaffold
29
- * silently pinned against years-old versions. That table is gone. If this
30
- * function ever fails to resolve a package, it returns `^0.0.0` rather
31
- * than a stale best-guess, so the breakage is loud.
12
+ * 1. Start from the CLI's own `package.json` dependencies. A published CLI
13
+ * carries concrete ranges like `~0.9.1` there — pnpm rewrites each
14
+ * `workspace:~` spec at publish.
15
+ * 2. For any dep that is still a `workspace:` spec (the CLI is running
16
+ * *from* a workspace checkout — local dev, sandboxes, tests), read the
17
+ * `package.json` of the sibling package under `framework/<name>/` and
18
+ * use `~<that version>`: the range a published CLI would carry.
19
+ * 3. Scan `framework/*` for every other `@uniweb/*` package (press, loom,
20
+ * scholar, etc.), so a new package reaches the scaffolder automatically.
21
+ * 4. A published CLI has no `framework/` beside it: it reads every other
22
+ * package's version from its bundled `framework-index.json`, refreshed
23
+ * when the CLI is published.
24
+ *
25
+ * ⭐ EVERY RANGE IS A TILDE. A breaking change ships as a minor bump, in 1.x as in
26
+ * 0.x *[ruled 2026-10-11]*, and `~1.4.0` admits 1.4.x alone, where `^1.4.0` would
27
+ * admit 1.5.0. So a project receives fixes and features (patches) by itself, and a
28
+ * breaking release through `uniweb update`, which says so. Below 1.0 a caret meant
29
+ * the same, which is why the table wrote one until then.
30
+ *
31
+ * ⭐ A version is not a reason to depend on a package. Steps 3 and 4 name
32
+ * every `@uniweb/*` package, so the CLI's `dependencies` list only what its
33
+ * code loads — each one is installed with every `npx uniweb`.
34
+ *
35
+ * The previous implementation shipped a hardcoded fallback table that
36
+ * nothing kept in sync, so every scaffold silently pinned years-old
37
+ * versions. That table is gone. A package none of these steps resolves gets
38
+ * the `{{version}}` helper's `^0.1.0` (templates/processor.js).
32
39
  */
33
40
 
34
41
  import { readFileSync, readdirSync, statSync } from 'node:fs'
@@ -198,16 +205,17 @@ function getFrameworkRoot() {
198
205
 
199
206
  /**
200
207
  * Read the current on-disk version of a specific `@uniweb/*` package by
201
- * looking up `framework/<last-segment>/package.json`. Returns the version
202
- * string verbatim (e.g. `0.7.11`), or null if the package isn't present
203
- * on disk (i.e. the CLI is running from npm, not from the workspace).
204
- *
205
- * Returns the version *exact*, not as a caret range, to match the shape
206
- * published-CLI direct deps take after pnpm resolves `workspace:*` at
207
- * publish time. Both modes converge on identical specs in
208
- * `getResolvedVersions()`, so `uniweb update` and the scaffolder produce
209
- * the same `package.json` whether the CLI ran from npm or from this
210
- * monorepo.
208
+ * looking up `framework/<last-segment>/package.json`, as a tilde range
209
+ * (e.g. `~0.7.11`) — or null if the package isn't on disk (the CLI is
210
+ * running from npm, not from a workspace checkout).
211
+ *
212
+ * ⭐ A tilde, because that is what a published CLI carries: pnpm rewrites
213
+ * its `workspace:~` deps to `~<version>` at publish. So `uniweb update` and
214
+ * the scaffolder write the same `package.json` whether the CLI ran from npm
215
+ * or from a workspace. ⛔ This returned the version exact until 2026-10-11:
216
+ * right while the deps were `workspace:*`, which pnpm publishes exact, and
217
+ * wrong once they became a range — a scaffold from a workspace checkout
218
+ * pinned `0.20.0` where a user's got `^0.20.0`.
211
219
  */
212
220
  function readWorkspaceVersion(packageName) {
213
221
  const root = getFrameworkRoot()
@@ -219,7 +227,7 @@ function readWorkspaceVersion(packageName) {
219
227
  try {
220
228
  const pkg = JSON.parse(readFileSync(pkgPath, 'utf8'))
221
229
  if (pkg.name === packageName && pkg.version) {
222
- return pkg.version
230
+ return `~${pkg.version}`
223
231
  }
224
232
  } catch {}
225
233
  return null
@@ -258,7 +266,7 @@ export function loadFrameworkIndex() {
258
266
 
259
267
  /**
260
268
  * Pull `@uniweb/*` package versions out of the framework index's
261
- * `packages` field and format them as caret ranges. Used as the
269
+ * `packages` field and format them as tilde ranges. Used as the
262
270
  * published-CLI fallback source after the CLI's own deps and the live
263
271
  * workspace walk both come up empty.
264
272
  */
@@ -268,7 +276,7 @@ function readIndexPackages() {
268
276
  const result = {}
269
277
  for (const [name, entry] of Object.entries(index.packages)) {
270
278
  if (entry && entry.version) {
271
- result[name] = `^${entry.version}`
279
+ result[name] = `~${entry.version}`
272
280
  }
273
281
  }
274
282
  return result
@@ -301,7 +309,7 @@ function discoverWorkspacePackages() {
301
309
  try {
302
310
  const pkg = JSON.parse(readFileSync(pkgPath, 'utf8'))
303
311
  if (pkg.name && pkg.version && pkg.name.startsWith('@uniweb/')) {
304
- found[pkg.name] = `^${pkg.version}`
312
+ found[pkg.name] = `~${pkg.version}`
305
313
  }
306
314
  } catch {}
307
315
  }
@@ -310,8 +318,8 @@ function discoverWorkspacePackages() {
310
318
 
311
319
  /**
312
320
  * Resolve a single version spec to something npm can install. A concrete
313
- * spec like `^0.9.0` passes through untouched; `workspace:*` is replaced
314
- * by the on-disk version of the named package; anything still unresolved
321
+ * spec like `~0.9.0` passes through untouched; a `workspace:` spec becomes
322
+ * `~<on-disk version>` of the named package; anything still unresolved
315
323
  * returns null so the caller can fall back.
316
324
  */
317
325
  function resolveVersionSpec(spec, packageName) {
@@ -328,8 +336,8 @@ function resolveVersionSpec(spec, packageName) {
328
336
  * Priority (highest first):
329
337
  *
330
338
  * 1. A concrete version spec already in the CLI's own `package.json`
331
- * (the state after an npm publish: `workspace:*` is resolved by
332
- * pnpm to a real version).
339
+ * (the state after an npm publish: pnpm rewrites `workspace:~` to
340
+ * `~<version>`), or a `workspace:` spec read as `~<on-disk version>`.
333
341
  * 2. Live workspace walk — every `@uniweb/*` package found under
334
342
  * `framework/*` at CLI invocation time. This is the path that
335
343
  * matters for local dev: a freshly-added package becomes
@@ -382,10 +390,10 @@ export function getResolvedVersions() {
382
390
  if (!result[name]) result[name] = version
383
391
  }
384
392
 
385
- // CLI itself. Caret on the current version — templates referencing
386
- // `{{version "uniweb"}}` pick up whatever patch/minor ships in the
387
- // same publish cycle as the template.
388
- result['uniweb'] = `^${pkg.version}`
393
+ // CLI itself. Tilde on the current version, like every other entry: a
394
+ // project takes the CLI's patches by itself, and a minor — breaking —
395
+ // through `uniweb update`.
396
+ result['uniweb'] = `~${pkg.version}`
389
397
 
390
398
  resolvedVersions = result
391
399
  return resolvedVersions
@@ -395,7 +403,7 @@ export function getResolvedVersions() {
395
403
  * Get a specific package version
396
404
  *
397
405
  * @param {string} packageName - Package name (e.g., "@uniweb/build")
398
- * @returns {string} Version spec (e.g., "^0.1.4")
406
+ * @returns {string} Version spec (e.g., "~0.1.4")
399
407
  */
400
408
  export function getVersion(packageName) {
401
409
  const versions = getResolvedVersions()