uniweb 0.14.28 → 0.14.30

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.14.28",
3
+ "version": "0.14.30",
4
4
  "description": "Create structured Vite + React sites with content/code separation",
5
5
  "type": "module",
6
6
  "bin": {
@@ -42,13 +42,13 @@
42
42
  "prompts": "^2.4.2",
43
43
  "tar": "^7.0.0",
44
44
  "@uniweb/core": "0.8.2",
45
- "@uniweb/runtime": "0.9.7",
46
- "@uniweb/kit": "0.10.20"
45
+ "@uniweb/runtime": "0.9.9",
46
+ "@uniweb/kit": "0.10.21"
47
47
  },
48
48
  "peerDependencies": {
49
- "@uniweb/semantic-parser": "1.2.1",
49
+ "@uniweb/build": "0.16.20",
50
50
  "@uniweb/content-reader": "1.2.2",
51
- "@uniweb/build": "0.16.18"
51
+ "@uniweb/semantic-parser": "1.2.1"
52
52
  },
53
53
  "peerDependenciesMeta": {
54
54
  "@uniweb/build": {
@@ -892,7 +892,7 @@ Nothing to install — the import brings the plugin with it. **Skip the import a
892
892
  **Layout helpers:** `useGridLayout(columns, { gap })`, `useAccordion({ multiple, defaultOpen })`
893
893
  **Theming data:** `useThemeData()`, `useColorContext(block)`
894
894
  **Data fetching:** `useFetched`, `useCacheEntry`, `useEntityDetail`
895
- **Forms:** `useFormSubmit`, `submitForm`, `resolveSubmitTarget` — see *Forms* below
895
+ **Forms:** `useFormValues` (an author-designed form's state), `useFormSubmit`, `submitForm`, `resolveSubmitTarget` — see *Forms* below
896
896
  **Utilities:** `cn()`, `SafeHtml`, `SocialIcon`, `filterSocialLinks(links)`, `getSocialPlatform(url)`, `getLocaleLabel(locale)`
897
897
  **Other styled:** `Code`, `Alert`, `Table`, `Details`, `Divider`, `Disclaimer`
898
898
 
@@ -1819,6 +1819,33 @@ fields, declare `fileSlots` and the endpoint replies with `uploadUrls` — the
1819
1819
  bytes never ride inside the JSON. `submitForm()` / `resolveSubmitTarget(website)`
1820
1820
  are the same behaviour without React.
1821
1821
 
1822
+ **Rendering a form the AUTHOR designed** — a `yaml:form` block at
1823
+ `content.data.form` — is the inverse of every other component: you don't declare
1824
+ the fields, you receive them and draw whatever you're given. `useFormValues` owns
1825
+ the part that's identical in every such component, so you write only the controls:
1826
+
1827
+ ```jsx
1828
+ import { useFormValues, useFormSubmit, valueAt } from '@uniweb/kit'
1829
+
1830
+ const { controls, values, setValue, missing, formData, files } =
1831
+ useFormValues(content.data.form)
1832
+ const { submit, canSubmit, status } = useFormSubmit({ block })
1833
+
1834
+ {controls.map((c) => (
1835
+ <MyControl key={c.path} control={c} // branch on c.type — your design
1836
+ value={valueAt(values, c.path)}
1837
+ onChange={(v) => setValue(c.path, v)} />
1838
+ ))}
1839
+ <button disabled={!canSubmit || missing.length > 0} onClick={() => submit(formData, { files })}>
1840
+ ```
1841
+
1842
+ It seeds each control's `default`, tracks edits, and reports `missing` — the
1843
+ `required` controls still empty — without enforcing anything, the same line
1844
+ `canSubmit` draws. **Submit `formData`, not `values`:** they differ because a
1845
+ `File` inside `formData` would be JSON-serialized to `{}`, so file controls are
1846
+ omitted from it and ride in `files` instead, each tagged with the control it came
1847
+ from. `values` keeps the `File` so your input can show its selection.
1848
+
1822
1849
  Full reference: `development/receiving-form-submissions.md`.
1823
1850
 
1824
1851
  <!-- template:loom -->
@@ -1951,13 +1978,17 @@ Platform-specific configuration that doesn't belong in npm-standard fields. All
1951
1978
  |---|---|---|---|
1952
1979
  | `id` | `uniweb register` | bare segment of a scoped `name` | The foundation's registered id — the bare name in `@org/<id>`. Decoupled from `package.json::name` (a workspace concern), so renaming on the registry doesn't ripple through site dependencies. |
1953
1980
  | `namespace` | `uniweb register` | none | Legacy explicit org-namespace override; equivalent to a scoped `package.json::name`. Rarely needed. |
1954
- | `runtimePolicy` | `dist/runtime-pin.json` | `"auto-minor"` | How sites using this foundation receive runtime updates. |
1981
+ | `runtimePolicy` | `dist/runtime-pin.json` | unset | Declares how far past the recorded runtime version a host may move a site. |
1982
+
1983
+ **Runtime policy** (foundation authors only — sites don't set this). At build time a foundation records the `@uniweb/runtime` version it linked against into `dist/runtime-pin.json`, and carries `uniweb.runtimePolicy` alongside it when you set one: `exact` (stay on the recorded version), `auto-patch` (within `MAJOR.MINOR.x`), `auto-minor` (within `MAJOR.x.y`). Leaving it unset is the normal case and the one to prefer — the runtime is backwards-compatible at the minor level by convention. Set `exact` only if you depend on undocumented runtime internals or have audited against one release. Site owners cannot override your choice.
1984
+
1985
+ ⚠️ **The pin is a compatibility FLOOR, not a selector.** It records what your build binds to; it does not choose the runtime a site runs, and it cannot: a site loads your foundation **plus any extensions**, each emitting its own pin, while a site has exactly **one** runtime. Pins are plural, the choice is singular. The runtime a site runs is selected by `site.yml::runtime`.
1955
1986
 
1956
- **Runtime policy** (foundation authors only sites don't set this). At build time a foundation pins the `@uniweb/runtime` version it built against into `dist/runtime-pin.json`, alongside a policy controlling how that version moves forward on already-published sites: `exact` (stay put), `auto-patch` (within `MAJOR.MINOR.x`), `auto-minor` (within `MAJOR.x.y`, the default). Most foundations should leave it unset — the runtime is backwards-compatible at the minor level by convention, so `auto-minor` lets sites pick up fixes without a foundation rebuild. Set `exact` only if you depend on undocumented runtime internals or have audited against one release. Site owners cannot override your choice.
1987
+ `uniweb register` reads the pin and sends it with your foundation, so the floor you built against travels with it. Its use is **validation** checking a site's chosen runtime against the floors of *every* foundation that site loads, which only something holding all of them can do. **That check is not implemented anywhere yet**, so today the floor is stated and carried but not enforced. Set `runtimePolicy` to declare your intent for when it is; don't design around it having an effect now.
1957
1988
 
1958
1989
  **Why this is yours and not the site's.** A published site ships **no JS** — content, config, data and assets only. Your foundation is the only part that contains code linking against the runtime, and that link is wider than it looks: a foundation externalizes `react`, `react-dom`, `react-dom/server`, both JSX runtimes and `@uniweb/core`, all supplied by the runtime at load time. So a foundation built against a runtime is bound to *that* React and *that* core API. The general rule, which also explains why the reverse holds: **each policy is declared by whoever's code binds to the thing being updated** — you declare the runtime policy because your code links against the runtime; the site declares its foundation policy because its content binds to your section types.
1959
1990
 
1960
- `@uniweb/runtime` arrives **transitively** through `@uniweb/build`, so your foundation pins a runtime version without declaring one — that's intentional. **Don't add `@uniweb/runtime` to your foundation's dependencies**; to bump the pinned version, bump `@uniweb/build`. If the pin is missing or malformed, the platform serves the foundation through its legacy compatibility path — sites still work, they just don't participate in runtime propagation.
1991
+ `@uniweb/runtime` arrives **transitively** through `@uniweb/build`, so your foundation pins a runtime version without declaring one — that's intentional. **Don't add `@uniweb/runtime` to your foundation's dependencies**; to bump the pinned version, bump `@uniweb/build`. If `@uniweb/runtime` isn't resolvable at build time the pin is simply not emitted, and the build still succeeds nothing in the toolchain requires it.
1961
1992
 
1962
1993
  ### Localization
1963
1994
 
@@ -69,7 +69,8 @@ import {
69
69
  import { readRegistryAuth } from '../utils/registry-auth.js'
70
70
  import {
71
71
  collectDistFiles,
72
- computeFoundationDigest
72
+ computeFoundationDigest,
73
+ readRuntimePin
73
74
  } from '../utils/code-upload.js'
74
75
  import { deriveScope } from '../utils/registry-orgs.js'
75
76
  import { BackendClient } from '../backend/client.js'
@@ -410,6 +411,17 @@ async function runRegister(args = []) {
410
411
  ? null
411
412
  : computeFoundationDigest(join(targetDir, 'dist'))
412
413
 
414
+ // Compatibility floor (foundation path only) — the @uniweb/runtime version
415
+ // this build linked against. Rides in info.runtime beside info.digest.
416
+ //
417
+ // Until this shipped, `dist/runtime-pin.json` was written by the build and
418
+ // read by NOTHING, so a floor a foundation stated could never reach anyone
419
+ // able to act on it. It cannot be acted on here either: a site's floor is
420
+ // max() over its primary foundation AND its extensions, and this command
421
+ // publishes one foundation without sight of the others. So the producer
422
+ // states its own floor and whoever resolves a whole site does the arithmetic.
423
+ const runtime = standalone ? null : readRuntimePin(join(targetDir, 'dist'))
424
+
413
425
  const exporter = { tool: 'uniweb', version: cliVersion(), instance: 'build' }
414
426
  let doc
415
427
  try {
@@ -420,7 +432,8 @@ async function runRegister(args = []) {
420
432
  foundationDir: targetDir,
421
433
  scope,
422
434
  exporter,
423
- digest
435
+ digest,
436
+ runtime
424
437
  })
425
438
  } catch (err) {
426
439
  error(`Could not assemble the .uwx: ${err.message}`)
@@ -447,6 +460,7 @@ async function runRegister(args = []) {
447
460
  if (scope)
448
461
  log(` ${colors.dim}scope: ${scope} (${scopeSource})${colors.reset}`)
449
462
  if (digest) log(` ${colors.dim}digest: ${digest}${colors.reset}`)
463
+ if (runtime) log(` ${colors.dim}runtime floor: ${runtime}${colors.reset}`)
450
464
 
451
465
  // Preview paths — no submit, no auth needed.
452
466
  if (output) {
@@ -3,20 +3,23 @@
3
3
  * can serve the runtime version. The runtime is a SYSTEM artifact: registering it
4
4
  * requires **@std membership** (a non-@std bearer 403s).
5
5
  *
6
- * A foundation emits `dist/runtime-pin.json`, but NOTHING READS IT not this CLI,
7
- * not the backend, not the edge (verified across all three lanes 2026-08-04,
8
- * channel `platform-backend-framework-8de5`). An earlier version of this comment
9
- * claimed `uniweb register` of a foundation fails when its pinned version isn't
10
- * registered. That was never true, and it propagated into a kb doc before anyone
11
- * checked it against `commands/register.js` three files away.
6
+ * A foundation emits `dist/runtime-pin.json`. `uniweb register` now reads it and
7
+ * ships it as the foundation's `info.runtime` but **NOTHING ENFORCES IT** in
8
+ * any lane. An earlier version of this comment claimed `uniweb register` of a
9
+ * foundation fails when its pinned version isn't registered. That was never
10
+ * true, and it propagated into internal notes AND into four public
11
+ * documentation surfaces before anyone checked it against `commands/register.js`
12
+ * three files away.
12
13
  *
13
14
  * The pin is a **compatibility floor**, not a selector: a site loads a primary
14
15
  * foundation plus N extensions, each emitting its own pin, and a site has exactly
15
16
  * one runtime — so pins are plural and the selector must be singular. The selector
16
17
  * is `site.yml::runtime` (see commands/publish.js), which the backend stamps into
17
- * the site's meta at publish. The pin's designed use is publish-time VALIDATION
18
- * (is the selected runtime inside every foundation's compatible interval?), which
19
- * is producer-side and not yet implemented.
18
+ * the site's meta at publish. The pin's use is VALIDATION — is the selected
19
+ * runtime at or above max() of every loaded foundation's floor? — and it belongs
20
+ * wherever all of a site's foundations are held. The producer publishes one
21
+ * foundation at a time and cannot see the others, so it STATES its floor and
22
+ * something holding the whole set does the arithmetic.
20
23
  *
21
24
  * Contract AGREED with the backend (2026-06-14): `POST /dev/runtime`, @std-gated,
22
25
  * manifest-last. Wire + the two-half artifact set (SPA + ssr-edge isolate, the
@@ -1,9 +1,9 @@
1
1
  {
2
2
  "schemaVersion": 1,
3
- "generatedAt": "2026-08-05T14:30:40.743Z",
3
+ "generatedAt": "2026-08-05T19:38:23.202Z",
4
4
  "packages": {
5
5
  "@uniweb/build": {
6
- "version": "0.16.18",
6
+ "version": "0.16.20",
7
7
  "path": "framework/build",
8
8
  "deps": [
9
9
  "@uniweb/content-reader",
@@ -46,7 +46,7 @@
46
46
  "deps": []
47
47
  },
48
48
  "@uniweb/kit": {
49
- "version": "0.10.20",
49
+ "version": "0.10.21",
50
50
  "path": "framework/kit",
51
51
  "deps": [
52
52
  "@uniweb/core",
@@ -73,7 +73,7 @@
73
73
  ]
74
74
  },
75
75
  "@uniweb/runtime": {
76
- "version": "0.9.7",
76
+ "version": "0.9.9",
77
77
  "path": "framework/runtime",
78
78
  "deps": [
79
79
  "@uniweb/core",
@@ -86,7 +86,7 @@
86
86
  "deps": []
87
87
  },
88
88
  "@uniweb/schemas": {
89
- "version": "0.2.6",
89
+ "version": "0.2.8",
90
90
  "path": "framework/schemas",
91
91
  "deps": []
92
92
  },
@@ -111,7 +111,7 @@
111
111
  "deps": []
112
112
  },
113
113
  "@uniweb/unipress": {
114
- "version": "0.6.16",
114
+ "version": "0.6.18",
115
115
  "path": "framework/unipress",
116
116
  "deps": [
117
117
  "@uniweb/build",
@@ -142,6 +142,40 @@ export function uploadOrder(files) {
142
142
  * @param {string} distDir - the built `dist/` directory
143
143
  * @returns {string|null} `sha256:<hex>` or null when there's nothing to hash
144
144
  */
145
+ /**
146
+ * The `@uniweb/runtime` version a built foundation linked against, from
147
+ * `dist/runtime-pin.json`.
148
+ *
149
+ * ⭐ **This is the ONLY reader of that file, and it exists so the pin can reach
150
+ * a party that can act on it.** The pin is a compatibility FLOOR — the lowest
151
+ * runtime this build is known to work against. A site loads a primary
152
+ * foundation plus N extensions, so a site's floor is `max()` over all of their
153
+ * pins, and the producer publishing one foundation cannot compute that: it does
154
+ * not hold the others. Whoever holds every foundation a site loads can.
155
+ *
156
+ * So the split is: the producer STATES its own floor, and a party that resolves
157
+ * a whole site CHECKS them. Riding it in the identity card next to `digest` is
158
+ * what makes the first half possible — and until it did, a consumer storing the
159
+ * value had no way to ever receive one.
160
+ *
161
+ * Returns null when the pin is absent (the build could not resolve
162
+ * `@uniweb/runtime`) or malformed. ⚠️ **null means UNKNOWN, not unconstrained** —
163
+ * a floor that is absent cannot be proven satisfied by anything, and a
164
+ * consumer's `max()` that skips nulls silently computes a floor over only the
165
+ * foundations that happened to declare one.
166
+ *
167
+ * @param {string} distDir - the built `dist/` directory
168
+ * @returns {string|null} e.g. `'0.9.8'`
169
+ */
170
+ export function readRuntimePin(distDir) {
171
+ try {
172
+ const pin = JSON.parse(readFileSync(join(distDir, 'runtime-pin.json'), 'utf8'))
173
+ return typeof pin?.runtime === 'string' && pin.runtime ? pin.runtime : null
174
+ } catch {
175
+ return null // absent or unparseable — both are "unknown"
176
+ }
177
+ }
178
+
145
179
  export function computeFoundationDigest(distDir) {
146
180
  const hashes = collectDistFiles(distDir).map((f) => f.sha256)
147
181
  // The schema rides in the register `.uwx`, not the code-upload set, so fold