uniweb 0.89.0 → 0.89.2

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.0",
3
+ "version": "0.89.2",
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/runtime": "^0.29.5",
45
- "@uniweb/core": "^0.38.0",
46
- "@uniweb/kit": "^0.20.0",
44
+ "@uniweb/semantic-parser": "^1.5.1",
47
45
  "@uniweb/content-writer": "^0.3.4",
48
- "@uniweb/schemas": "^0.13.0",
49
- "@uniweb/semantic-parser": "^1.5.1"
46
+ "@uniweb/core": "^0.38.1",
47
+ "@uniweb/schemas": "^0.13.0"
50
48
  },
51
49
  "peerDependencies": {
50
+ "@uniweb/semantic-parser": "^1.5.1",
52
51
  "@uniweb/content-reader": "^1.2.8",
53
- "@uniweb/build": "^0.80.3",
54
- "@uniweb/semantic-parser": "^1.5.1"
52
+ "@uniweb/build": "^0.80.3"
55
53
  },
56
54
  "peerDependenciesMeta": {
57
55
  "@uniweb/build": {
@@ -2510,8 +2510,8 @@ const sessions = entity?.hydrated?.items.filter((item) => item.section === 'sess
2510
2510
  section's fields (`record.brief.name`, not `record.name`), and a record carries **no
2511
2511
  items**. Read one entity for its items. Each item carries `section`, its section's name
2512
2512
  (the word a write takes), and `id`, which is what the writer takes to update, move or
2513
- remove it. `scope: 'mine'` lists only the viewer's own records. Without it, the operator
2514
- also gets everyone else's.
2513
+ remove it. `scope: 'mine'` lists only the viewer's own records; without it, a list also
2514
+ holds what was shared with the viewer.
2515
2515
 
2516
2516
  ⭐ **`absent` and an empty `ready` are different answers, and confusing them is the
2517
2517
  mistake to avoid.** `absent` = there is no live source (no `backend` service, or nobody signed
@@ -2543,10 +2543,12 @@ await writer.remove(itemId)
2543
2543
 
2544
2544
  ### ⛔ Permissions are the SERVER'S, and your UI is only a courtesy
2545
2545
 
2546
- Gate your controls on what the viewer may do — `viewer.roles` (the site's operator
2547
- holds `system_admin`), or a record's own `can_edit` from a single-entity read — but
2548
- **never rely on that for safety**. A foundation runs with exactly the viewer's own
2549
- authority, so hiding a button hides a button.
2546
+ Gate your controls on what the viewer may do — a record's own `can_edit` from a
2547
+ single-entity read — but **never rely on that for safety**. A foundation runs with
2548
+ exactly the viewer's own authority, so hiding a button hides a button. ⛔ **Not on
2549
+ `viewer.roles`:** a session signed in on a site holds a member's rights — the operator's
2550
+ too — so its roles never name an administrator, and a gate on `system_admin` shows the
2551
+ operator nothing.
2550
2552
 
2551
2553
  What protects content is the **entry**: a member writes their own entries and not
2552
2554
  another's, and the store enforces it. **Models are open** — anyone signed in may
@@ -2565,6 +2567,42 @@ sections:
2565
2567
  `append_only` holds against the item's own author. That is the difference between a
2566
2568
  permission model and a CSS one.
2567
2569
 
2570
+ ### The Admin link
2571
+
2572
+ ```jsx
2573
+ const { adminHref } = useSession()
2574
+ {adminHref && <a href={adminHref}>Admin</a>}
2575
+ ```
2576
+
2577
+ `adminHref` is set only for a signed-in account the backend's administration admits —
2578
+ the operator, and whoever else administers it — and is `null` for everyone else. It
2579
+ takes them there already signed in. ⛔ **Navigate to it, never `fetch` it**: the answer is
2580
+ a redirect carrying a one-time credential no script on the site is to hold. It decides
2581
+ what to offer, not what is allowed — on the site the operator acts as a member.
2582
+
2583
+ ### The links the backend mails
2584
+
2585
+ A sign-up's confirmation, a password reset and an invitation to an address with no
2586
+ account all arrive as a link to the site's root, a token in the query — `?verify=`,
2587
+ `?reset=`, `?offer=` — and the page it lands on finishes the step:
2588
+
2589
+ ```jsx
2590
+ const { step, status, result, error, confirm } = useMailedLink()
2591
+ // step: null | 'verify' | 'reset' | 'offer' status: 'idle' | 'working' | 'done' | 'error'
2592
+ ```
2593
+
2594
+ - `verify` finishes by itself; then the member signs in.
2595
+ - `reset` waits for `confirm({ new_password })`, and the hook supplies the token. A
2596
+ password the rule refuses is `error.extensions.reason === 'password_too_short'`, and
2597
+ the same link works again.
2598
+ - `offer` waits for a sign-in, then claims by itself. `result` is counts,
2599
+ `{ invitations, grants }`, never names. ⛔ `error.kind === 'absent'` means unknown,
2600
+ revoked or already used, alike: one sentence, no cause.
2601
+
2602
+ Render it where the root renders it — a header or a layout is on every page. ⛔ Never
2603
+ copy the token anywhere: it is a credential, it stays in the URL until the backend has
2604
+ judged it, and then the hook takes it off.
2605
+
2568
2606
  ### A backend on your machine
2569
2607
 
2570
2608
  You do not need a live backend to build against one. In `site.yml`:
@@ -2586,7 +2624,9 @@ supplies on its own origin, so cookies behave exactly as they will in production
2586
2624
  no address of your own for it: under `services:` an address means a backend you run, and
2587
2625
  asks your host to leave its own off. It **enforces** who
2588
2626
  may edit an entry and `append_only`, so a permission you are relying on fails on your
2589
- machine rather than in front of a user. State is in memory; restart to reset.
2627
+ machine rather than in front of a user. The mail it would send is printed in the dev
2628
+ server's terminal, its link on your site's root — follow it to try your page; seed
2629
+ `offers: [{ email }]` to try an invitation. State is in memory; restart to reset.
2590
2630
 
2591
2631
  `uniweb create my-event --template conference` is a worked example of all of this.
2592
2632
 
@@ -1,9 +1,9 @@
1
1
  {
2
2
  "schemaVersion": 1,
3
- "generatedAt": "2026-10-09T22:44:36.037Z",
3
+ "generatedAt": "2026-10-11T16:47:25.397Z",
4
4
  "packages": {
5
5
  "@uniweb/api": {
6
- "version": "0.7.0"
6
+ "version": "0.9.0"
7
7
  },
8
8
  "@uniweb/build": {
9
9
  "version": "0.80.3"
@@ -15,7 +15,7 @@
15
15
  "version": "0.3.4"
16
16
  },
17
17
  "@uniweb/core": {
18
- "version": "0.38.0"
18
+ "version": "0.38.1"
19
19
  },
20
20
  "@uniweb/frame-bridge": {
21
21
  "version": "0.3.2"
@@ -36,7 +36,7 @@
36
36
  "version": "0.7.15"
37
37
  },
38
38
  "@uniweb/runtime": {
39
- "version": "0.29.5"
39
+ "version": "0.29.6"
40
40
  },
41
41
  "@uniweb/scene": {
42
42
  "version": "0.1.3"
@@ -54,7 +54,7 @@
54
54
  "version": "0.1.2"
55
55
  },
56
56
  "@uniweb/templates": {
57
- "version": "0.18.7"
57
+ "version": "0.18.8"
58
58
  },
59
59
  "@uniweb/theming": {
60
60
  "version": "0.2.0"
package/src/versions.js CHANGED
@@ -9,26 +9,27 @@
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
+ * ⭐ A version is not a reason to depend on a package. Steps 3 and 4 name
26
+ * every `@uniweb/*` package, so the CLI's `dependencies` list only what its
27
+ * code loads — each one is installed with every `npx uniweb`.
28
+ *
29
+ * The previous implementation shipped a hardcoded fallback table that
30
+ * nothing kept in sync, so every scaffold silently pinned years-old
31
+ * versions. That table is gone. A package none of these steps resolves gets
32
+ * the `{{version}}` helper's `^0.1.0` (templates/processor.js).
32
33
  */
33
34
 
34
35
  import { readFileSync, readdirSync, statSync } from 'node:fs'
@@ -198,16 +199,17 @@ function getFrameworkRoot() {
198
199
 
199
200
  /**
200
201
  * 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.
202
+ * looking up `framework/<last-segment>/package.json`, as a caret range
203
+ * (e.g. `^0.7.11`) — or null if the package isn't on disk (the CLI is
204
+ * running from npm, not from a workspace checkout).
205
+ *
206
+ * ⭐ A caret, because that is what a published CLI carries: pnpm rewrites
207
+ * its `workspace:^` deps to `^<version>` at publish. So `uniweb update` and
208
+ * the scaffolder write the same `package.json` whether the CLI ran from npm
209
+ * or from a workspace. ⛔ This returned the version exact until 2026-10-11:
210
+ * right while the deps were `workspace:*`, which pnpm publishes exact, and
211
+ * wrong once they became `workspace:^` — so a scaffold from a workspace
212
+ * checkout pinned `0.20.0` where a user's got `^0.20.0`.
211
213
  */
212
214
  function readWorkspaceVersion(packageName) {
213
215
  const root = getFrameworkRoot()
@@ -219,7 +221,7 @@ function readWorkspaceVersion(packageName) {
219
221
  try {
220
222
  const pkg = JSON.parse(readFileSync(pkgPath, 'utf8'))
221
223
  if (pkg.name === packageName && pkg.version) {
222
- return pkg.version
224
+ return `^${pkg.version}`
223
225
  }
224
226
  } catch {}
225
227
  return null
@@ -310,8 +312,8 @@ function discoverWorkspacePackages() {
310
312
 
311
313
  /**
312
314
  * 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
315
+ * spec like `^0.9.0` passes through untouched; a `workspace:` spec becomes
316
+ * `^<on-disk version>` of the named package; anything still unresolved
315
317
  * returns null so the caller can fall back.
316
318
  */
317
319
  function resolveVersionSpec(spec, packageName) {
@@ -328,8 +330,8 @@ function resolveVersionSpec(spec, packageName) {
328
330
  * Priority (highest first):
329
331
  *
330
332
  * 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).
333
+ * (the state after an npm publish: pnpm rewrites `workspace:^` to
334
+ * `^<version>`), or a `workspace:` spec read as `^<on-disk version>`.
333
335
  * 2. Live workspace walk — every `@uniweb/*` package found under
334
336
  * `framework/*` at CLI invocation time. This is the path that
335
337
  * matters for local dev: a freshly-added package becomes