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 +5 -5
- package/partials/agents.md +35 -4
- package/src/commands/register.js +16 -2
- package/src/commands/runtime.js +12 -9
- package/src/framework-index.json +6 -6
- package/src/utils/code-upload.js +34 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "uniweb",
|
|
3
|
-
"version": "0.14.
|
|
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.
|
|
46
|
-
"@uniweb/kit": "0.10.
|
|
45
|
+
"@uniweb/runtime": "0.9.9",
|
|
46
|
+
"@uniweb/kit": "0.10.21"
|
|
47
47
|
},
|
|
48
48
|
"peerDependencies": {
|
|
49
|
-
"@uniweb/
|
|
49
|
+
"@uniweb/build": "0.16.20",
|
|
50
50
|
"@uniweb/content-reader": "1.2.2",
|
|
51
|
-
"@uniweb/
|
|
51
|
+
"@uniweb/semantic-parser": "1.2.1"
|
|
52
52
|
},
|
|
53
53
|
"peerDependenciesMeta": {
|
|
54
54
|
"@uniweb/build": {
|
package/partials/agents.md
CHANGED
|
@@ -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` |
|
|
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
|
-
|
|
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
|
|
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
|
|
package/src/commands/register.js
CHANGED
|
@@ -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) {
|
package/src/commands/runtime.js
CHANGED
|
@@ -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
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
* checked it against `commands/register.js`
|
|
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
|
|
18
|
-
*
|
|
19
|
-
*
|
|
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
|
package/src/framework-index.json
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schemaVersion": 1,
|
|
3
|
-
"generatedAt": "2026-08-
|
|
3
|
+
"generatedAt": "2026-08-05T19:38:23.202Z",
|
|
4
4
|
"packages": {
|
|
5
5
|
"@uniweb/build": {
|
|
6
|
-
"version": "0.16.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
114
|
+
"version": "0.6.18",
|
|
115
115
|
"path": "framework/unipress",
|
|
116
116
|
"deps": [
|
|
117
117
|
"@uniweb/build",
|
package/src/utils/code-upload.js
CHANGED
|
@@ -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
|