uniweb 0.84.0 → 0.86.0
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 +6 -6
- package/partials/agents.md +126 -80
- package/src/backend/client.js +59 -6
- package/src/backend/foundation-bring-along.js +5 -3
- package/src/backend/service-request.js +91 -145
- package/src/backend/site-preview.js +1 -1
- package/src/backend/site-sync.js +161 -54
- package/src/commands/build.js +6 -2
- package/src/commands/clone.js +1 -1
- package/src/commands/deploy.js +3 -3
- package/src/commands/doctor.js +32 -36
- package/src/commands/forget.js +7 -7
- package/src/commands/publish.js +28 -30
- package/src/commands/pull.js +22 -11
- package/src/commands/push.js +19 -28
- package/src/commands/refresh.js +2 -1
- package/src/commands/register.js +6 -6
- package/src/commands/rename.js +6 -6
- package/src/commands/site.js +358 -0
- package/src/commands/snapshot.js +14 -5
- package/src/commands/status.js +1 -1
- package/src/framework-index.json +11 -11
- package/src/index.js +41 -11
- package/src/utils/config.js +13 -13
- package/src/utils/flag-guard.js +30 -16
- package/src/utils/site-identity.js +3 -3
- package/src/utils/yaml-edit.js +0 -115
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "uniweb",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.86.0",
|
|
4
4
|
"description": "Create structured Vite + React sites with content/code separation",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -42,15 +42,15 @@
|
|
|
42
42
|
"prompts": "^2.4.2",
|
|
43
43
|
"tar": "^7.0.0",
|
|
44
44
|
"@uniweb/content-writer": "^0.3.4",
|
|
45
|
-
"@uniweb/
|
|
46
|
-
"@uniweb/
|
|
45
|
+
"@uniweb/core": "^0.38.0",
|
|
46
|
+
"@uniweb/runtime": "^0.29.3",
|
|
47
47
|
"@uniweb/semantic-parser": "^1.5.1",
|
|
48
|
-
"@uniweb/
|
|
49
|
-
"@uniweb/
|
|
48
|
+
"@uniweb/schemas": "^0.13.0",
|
|
49
|
+
"@uniweb/kit": "^0.20.0"
|
|
50
50
|
},
|
|
51
51
|
"peerDependencies": {
|
|
52
|
-
"@uniweb/build": "^0.77.0",
|
|
53
52
|
"@uniweb/content-reader": "^1.2.8",
|
|
53
|
+
"@uniweb/build": "^0.79.0",
|
|
54
54
|
"@uniweb/semantic-parser": "^1.5.1"
|
|
55
55
|
},
|
|
56
56
|
"peerDependenciesMeta": {
|
package/partials/agents.md
CHANGED
|
@@ -899,7 +899,7 @@ brief:
|
|
|
899
899
|
The body — the value of the schema's content field, `body.content`.
|
|
900
900
|
```
|
|
901
901
|
|
|
902
|
-
**A component receives it in one of two shapes, and its `meta.js` says which**, per `data:` key — the same on a static site and a hosted one. `data: { articles: '@std/article' }` gets **briefs**: the brief's fields at the top (`article.title`), no other section. `data: { articles: '@std/article
|
|
902
|
+
**A component receives it in one of two shapes, and its `meta.js` says which**, per `data:` key — the same on a static site and a hosted one. `data: { articles: '@std/article' }` gets **briefs**: the brief's fields at the top (`article.title`), no other section. `data: { articles: { schema: '@std/article', whole: true } }` gets each record **whole**, as stored: every section under its own name, the brief's included (`article.brief.title`, `article.body.content`). Both carry `$name`, the handle. Lists carry briefs; a section that shows a record in full declares `whole: true`. A `fields:` schema's brief is the whole record, so its components need no `whole: true`. Full rules: `reference/entity-content.md`.
|
|
903
903
|
|
|
904
904
|
**A reference names the record it points at.** A `{ ref: '@/speaker' }` field is written as that record's name — `speaker: ada` for `records/speaker/ada.yml` (its file's name) — and a `many` one as a list of names. A component receives `{ entity, brief }`: the record reduced to its brief, so `talk.speaker.brief.name`. `uniweb validate` and a push report a name that matches no record; a push sends each reference to the record it names (in the same push when that record is new too), and a pull writes the name back.
|
|
905
905
|
|
|
@@ -962,15 +962,15 @@ pages/blog/
|
|
|
962
962
|
|
|
963
963
|
**Link a card with `item.$route` — never compose the URL.** Every record a query delivers carries `$route`, the URL of the parametric page whose route query is that query (`/blog/design-tips`), wherever the list appears — the query's own page, the homepage, a sidebar. No page for the query, or no value for the field its URL is built from, means no `$route`, never a broken one. `detailPage: page:<id>` on a fetch links the records to another page instead. `$` marks a field the framework fills, so a record's own `route` field is left alone. ⛔ `route:` on a query is retired and stops the build.
|
|
964
964
|
|
|
965
|
-
**Which query the URL narrows — the page's route query:** the `[slug]` page's own `query:`, else its parent page's (the usual shape, above), else `site.yml`'s (for a top-level `pages/[slug]/` only — the site's reaches no deeper page); if none declares one, the query its sections all declare. The first query of that level wins. Every section the route query reaches gets the one record; a section declaring a *different* query of its own gets that query as declared. The folder name says what the URL segment matches: `[slug]` the record's name (`$name`, a compiled record's file name) — or the field its route query binds `:slug` to (`where: { slug: :slug }`, how an external API's records, which have no `$name`, get pages), `[uuid]` its `$uuid`, any other `[name]` the record's own field of that name — and when that field holds several values, **any member** matches (the record's own link is its first value). Routing by a field that is not unique picks one record and which one is not guaranteed; the build warns. A folder inside `[slug]/` (`[slug]/cv/` → `/blog/:slug/cv`) is a parametric page too, about the same record: its route query is the `[slug]` page's, wherever that is declared, and a query `cv/` declares itself arrives under its own key. **A section chooses how it uses the page's record with `current:`** on its own `fetch:` — `only` (the default: the record
|
|
965
|
+
**Which query the URL narrows — the page's route query:** the `[slug]` page's own `query:`, else its parent page's (the usual shape, above), else `site.yml`'s (for a top-level `pages/[slug]/` only — the site's reaches no deeper page); if none declares one, the query its sections all declare. The first query of that level wins. Every section the route query reaches gets the one record; a section declaring a *different* query of its own gets that query as declared. The folder name says what the URL segment matches: `[slug]` the record's name (`$name`, a compiled record's file name) — or the field its route query binds `:slug` to (`where: { slug: :slug }`, how an external API's records, which have no `$name`, get pages), `[uuid]` its `$uuid`, any other `[name]` the record's own field of that name — and when that field holds several values, **any member** matches (the record's own link is its first value). Routing by a field that is not unique picks one record and which one is not guaranteed; the build warns. A folder inside `[slug]/` (`[slug]/cv/` → `/blog/:slug/cv`) is a parametric page too, about the same record: its route query is the `[slug]` page's, wherever that is declared, and a query `cv/` declares itself arrives under its own key. **A section chooses how it uses the page's record with `current:`** on its own `fetch:` — `only` (the default: the record), `exclude` (the others: "related"), `include` (all of them: a pager) — e.g. `fetch: { query: articles, as: related, current: exclude, limit: 3 }`, where `limit` counts the others, all from the query's set. **`current:` follows the query, not the key:** a fetch of the route query gets the record unless it says otherwise, whatever its `as`; a fetch of another query gets that query's records and reads `current:` only when written (`exclude` drops the page's record, `only` keeps just it). `refine: true` / `detail: false` are retired and stop the build. `[dir]` and `[path]` are refused as folder names, and so is any folder inside `[...path]/`.
|
|
966
966
|
|
|
967
|
-
> **The record arrives
|
|
967
|
+
> **The record arrives under the key the component declares, as that key says** — one record for a key declared `single: true` (`content.data.article` for `article: { schema: '@std/article', single: true }` over an `@std/article` query; `null` when the URL names none), a single-element array for any other key (`content.data.recent[0]`). It is the record's brief, or the record whole where the key declares `whole: true` — a section that shows an article's body declares `whole: true`. See *Data* in Part 4.
|
|
968
968
|
|
|
969
|
-
**Records with URLs of their own shape — `[...path]/`.** A folder named exactly `[...path]` (one fixed spelling) captures the rest of the URL: `/blog/my-post` and `/blog/rust/2025/my-post` both reach it. The capture yields three standard variables — `:path` (the whole capture), `:dir` (everything before the last segment), `:slug` (the last segment, the record's handle) — and the record is still delivered by its handle, so the section reads
|
|
969
|
+
**Records with URLs of their own shape — `[...path]/`.** A folder named exactly `[...path]` (one fixed spelling) captures the rest of the URL: `/blog/my-post` and `/blog/rust/2025/my-post` both reach it. The capture yields three standard variables — `:path` (the whole capture), `:dir` (everything before the last segment), `:slug` (the last segment, the record's handle) — and the record is still delivered by its handle, so the section reads it as before. The same three variables exist under every parametric page: under `[slug]`, `:slug` and `:path` are the segment and `:dir` is empty. A record's own URL (`$route`, and the page a static build writes) is its name, as under `[slug]`: `/blog/my-post` wherever `records/folder.yml` places it — a record does not carry its folder. A query may bind a part — `scope: :dir` exposes the folder branch, `where: { tag: :dir }` keeps it private — and an unbound or empty variable drops its clause, so one saved query serves the query's page and its parametric page, on a static site and a hosted one alike. Without `scope: :dir` the directory is decoration: the record is found by its handle wherever it sits. Reference: `reference/dynamic-routes.md`.
|
|
970
970
|
|
|
971
971
|
**Two options for bigger sets:**
|
|
972
972
|
|
|
973
|
-
A list carries briefs, so a schema that keeps its heavy parts out of the brief — `@std/article`'s body is its own section — keeps lists light with no configuration, while a section declaring
|
|
973
|
+
A list carries briefs, so a schema that keeps its heavy parts out of the brief — `@std/article`'s body is its own section — keeps lists light with no configuration, while a section declaring `whole: true` still receives the whole record on a `[slug]` page, and other components fetch it on demand with `useWholeRecord`. File-based records are written one file per record, whole, at `/data/<name>/<name of the record>.json`; an external query names its one-record request with `record:` instead (*Fetching from other sources* in Part 4). (`deferred:` is removed: a schema's brief decides what a list holds.)
|
|
974
974
|
|
|
975
975
|
`queryable:` declares which fields a reader may filter on, with enough metadata for the foundation to render controls:
|
|
976
976
|
|
|
@@ -1873,21 +1873,34 @@ Content-less containers appear as group nodes (`hasContent: false`) — use `nav
|
|
|
1873
1873
|
|
|
1874
1874
|
**A section receives the keys its component declares in `meta.js` `data:` — and nothing else** (plus any its foundation declares in `main.js` `data:`). A component that reads `content.data.articles` declares `articles`. Each declared key is filled by, in order: a tagged data block in the section; else the fetch that fills it, level by level from the section's own to the site's — a fetch whose `as` (its query's name by default) is the key, or else, **automatic `as`**, the first fetch under an undeclared key whose query's schema is the key's (`@/x` matches any scope's `x`); else `null`. So `data: { related: '@std/article' }` receives an `articles` query under `related`. `as:` on a fetch picks the key when two keys or two fetches share a schema. A fetch that fills none of a section's keys is not requested for it.
|
|
1875
1875
|
|
|
1876
|
-
**A
|
|
1876
|
+
**A key says how many records it holds, and how much of each — two flags, independent.** A ref alone (`'@std/article'`) is a **list of briefs**: on the query's own page every record the fetch takes, on a parametric page (`[slug]/`) a single-element array, `[]` when nothing matches. Write the key long to change either:
|
|
1877
1877
|
|
|
1878
|
-
|
|
1878
|
+
```js
|
|
1879
|
+
data: {
|
|
1880
|
+
articles: '@std/article', // a list of briefs
|
|
1881
|
+
article: { schema: '@std/article', single: true, whole: true }, // one record, whole
|
|
1882
|
+
card: { schema: '@std/article', single: true }, // one record's brief
|
|
1883
|
+
archive: { schema: '@std/article', whole: true }, // a list of whole records
|
|
1884
|
+
}
|
|
1885
|
+
```
|
|
1886
|
+
|
|
1887
|
+
- **`single: true`** — the key holds **one record**: the first its binding selects (on a `[slug]` page, the record the URL names), or `null` when there is none. The runtime asks for just that one. A binding over many gives the first: sort the query, or give the binding `limit: 1`.
|
|
1888
|
+
- **`whole: true`** — each record **whole**, as stored: every section under its own name, the brief's included — `article.brief.title`, `article.body.content`. Without it, each record is its **brief**: the brief section's fields at the top, the section not named. The runtime asks for what the key declares; a `whole` key its source cannot answer whole is `null`. A `fields:` schema's brief is the whole record, so `whole` is only needed for a schema with more than one section.
|
|
1889
|
+
- The long form takes `schema`, `single` and `whole`, nothing else — the build stops on any other property, and on `single` or `whole` over a schema whose root is a list (`@std/nav`, `@std/form`: the key holds that list). ⛔ `'@std/article/*'` is retired and stops the build: write `whole: true`.
|
|
1890
|
+
|
|
1891
|
+
When nothing fills a key it is `null`. An external query's records have no schema and arrive as its source answers them. Reference: `reference/component-metadata.md` § *One record, or a list* and § *Briefs or whole records*.
|
|
1879
1892
|
|
|
1880
1893
|
```jsx
|
|
1881
|
-
function Article({ content, block }) {
|
|
1894
|
+
function Article({ content, block }) { // data: { article: { schema: '@std/article', single: true } }
|
|
1882
1895
|
if (block.dataLoading) return <DataPlaceholder />
|
|
1883
|
-
if (block.dataError?.
|
|
1884
|
-
const article = content.data.
|
|
1896
|
+
if (block.dataError?.article) return <LoadFailed /> // the request failed — not "no records"
|
|
1897
|
+
const article = content.data.article // the record the [slug] page's URL names, or null
|
|
1885
1898
|
if (!article) return <NotFound />
|
|
1886
1899
|
return <ArticleView article={article} />
|
|
1887
1900
|
}
|
|
1888
1901
|
```
|
|
1889
1902
|
|
|
1890
|
-
|
|
1903
|
+
Any other reshaping is the foundation's job — once, with a `handlers.data` hook.
|
|
1891
1904
|
|
|
1892
1905
|
**Declaring keys and schemas.** `meta.js` declares each `content.data` key the component receives, with its schema, in a single `data:` field — there is no separate `schemas:` key. Each value is a **named ref**, an **inline field map**, an **inline rich-form** (`{ fields: [...] }`, an editor form), or `{}` for a key with no schema (an external API's records). Refs resolve on disk at build time, never fetched: `@/name` (this foundation's `schemas/`), `@std/name` (shared standards, from `@uniweb/schemas`), `@org/name` (an org's own `@org/schemas` package). A schema drives the editor, checks your data (`uniweb validate`), and lets a fetch of another name fill the key — it fills no field: **a record reaches the component as it is**, so a field it lacks is absent, and what that renders as is your component's choice (`record.status ?? 'published'`). **A named schema declares no `default:`** — the build refuses one; a `default:` belongs only in an inline field map or form, where an editor pre-fills from it. ⛔ **Delivery was default-on until this change** — every fetched key reached every component, and `data:` was a hint; a component that reads a key it does not declare now receives nothing under it. `data: false` declares nothing, like no `data:`. Keys a foundation's handlers (or a shared hook) read go in `main.js` `data:`, in the same form, and every section receives them.
|
|
1893
1906
|
|
|
@@ -1915,7 +1928,7 @@ fetch:
|
|
|
1915
1928
|
limit: 3
|
|
1916
1929
|
```
|
|
1917
1930
|
|
|
1918
|
-
**Lean lists.** A list carries each record's brief, so heavy parts belong in a section of their own — `@std/article`'s body is `body` — and never ride a list. A section that shows them declares
|
|
1931
|
+
**Lean lists.** A list carries each record's brief, so heavy parts belong in a section of their own — `@std/article`'s body is `body` — and never ride a list. A section that shows them declares its key `whole: true`; elsewhere components fetch the whole record on demand with `useWholeRecord(record, { query })`, which returns it as stored — `full.body.content`. Every file-based record gets its own file, `/data/<name>/<name of the record>.json`. An external query's whole record comes from its `record:` request, below. (`deferred:` is removed — the build stops on it.) The hook is safe to call on any query: when the query has no separate source for one record it returns the record you passed in.
|
|
1919
1932
|
|
|
1920
1933
|
**Component-side fetching.** When a component genuinely needs to fetch on its own (a search box, "load more", a lazy popover), use the kit hooks — `useFetched`, `useCacheEntry`, `useWholeRecord`. They share the framework's cache and dispatcher with declarative fetches; same-key requests dedupe automatically.
|
|
1921
1934
|
|
|
@@ -1962,15 +1975,15 @@ When an external query is enough and when a transport is the answer: `developmen
|
|
|
1962
1975
|
|
|
1963
1976
|
Full model: `reference/data-fetching.md`. Where-object format with examples: `authoring/predicates.md`.
|
|
1964
1977
|
|
|
1965
|
-
### Search (`search
|
|
1978
|
+
### Search (`services.search`)
|
|
1966
1979
|
|
|
1967
|
-
Search follows the same arrangement as `fetcher:` — the **site** declares where results come from, and a search UI reads them the same way regardless. Never hardcode a search endpoint in a component; that couples the foundation to one host.
|
|
1980
|
+
Search follows the same arrangement as `fetcher:` — the **site** declares where results come from, and a search UI reads them the same way regardless. Never hardcode a search endpoint in a component; that couples the foundation to one host. Search is on by default; its entry under `services:` (see *Services*, below) turns it off, sets options, or — on a site you publish — asks the host for it:
|
|
1968
1981
|
|
|
1969
1982
|
```yaml
|
|
1970
1983
|
# site.yml
|
|
1971
|
-
|
|
1972
|
-
|
|
1973
|
-
|
|
1984
|
+
services:
|
|
1985
|
+
search:
|
|
1986
|
+
provider: index # default — download an index, match in the browser
|
|
1974
1987
|
```
|
|
1975
1988
|
|
|
1976
1989
|
| Provider | Answers with | Trade-off |
|
|
@@ -1980,12 +1993,13 @@ search:
|
|
|
1980
1993
|
| *any other name* | A foundation-supplied search transport | Fully open — Typesense, Meilisearch, Pagefind, a vendor API |
|
|
1981
1994
|
|
|
1982
1995
|
```yaml
|
|
1983
|
-
|
|
1984
|
-
|
|
1985
|
-
|
|
1996
|
+
services:
|
|
1997
|
+
search:
|
|
1998
|
+
provider: endpoint
|
|
1999
|
+
endpoint: _search # REQUIRED — there is no default
|
|
1986
2000
|
```
|
|
1987
2001
|
|
|
1988
|
-
`endpoint:` is **required** with `provider: endpoint`; omit it and the provider refuses the query rather than guessing a path. It resolves against the site's base path — `/` → `/_search`, `base: /docs/` → `/docs/_search`, a subpath-served site follows its subpath. An absolute `https://…` URL points at another origin. A host that serves the site may offer search itself,
|
|
2002
|
+
`endpoint:` is **required** with `provider: endpoint`; omit it and the provider refuses the query rather than guessing a path. It resolves against the site's base path — `/` → `/_search`, `base: /docs/` → `/docs/_search`, a subpath-served site follows its subpath. An absolute `https://…` URL points at another origin. A host that serves the site may offer search itself: `search: true` asks for it, and the host supplies the address. An endpoint of your own asks the host to leave its search off, so yours answers.
|
|
1989
2003
|
|
|
1990
2004
|
**Results have one shape, whatever the provider.** Always present: `id`, `type`, `route`, `href`, `title`, `pageTitle`, `excerpt`, `snippetHtml`. Provider-optional (`null` when absent): `sectionId`, `anchor`, `description`, `component`, `snippetText`, `matches`, `group`, `item`. `type` is `page`, `section` or `record`; on a record hit, `item` holds the record's fields and `group` names the set it came from. Whether an optional field arrives is a deployment fact, not a content fact — the same site yields `item` from a server provider and `null` from the local index — so guard them: `result.item?.image`.
|
|
1991
2005
|
|
|
@@ -2002,29 +2016,30 @@ const { results, isLoading, query } = useSearch(website) // `query` is a funct
|
|
|
2002
2016
|
|
|
2003
2017
|
Full reference: `authoring/search.md`.
|
|
2004
2018
|
|
|
2005
|
-
### Forms (`submit
|
|
2019
|
+
### Forms (`services.submit`)
|
|
2006
2020
|
|
|
2007
2021
|
Drawing a form is a foundation's job; delivering what a visitor typed needs a
|
|
2008
2022
|
server. Where that server is comes from the **site or its host** — never from a
|
|
2009
|
-
section type, same arrangement as `fetcher:` and
|
|
2023
|
+
section type, same arrangement as `fetcher:` and search.
|
|
2010
2024
|
|
|
2011
2025
|
A form gets its destination from the first of these that applies:
|
|
2012
2026
|
|
|
2013
|
-
1. **One the host supplies** — `services.submit` in the
|
|
2014
|
-
host handles submissions, that is the destination
|
|
2015
|
-
|
|
2016
|
-
|
|
2017
|
-
|
|
2018
|
-
|
|
2027
|
+
1. **One the host supplies** — `config.services.submit` in the payload it serves. Where the
|
|
2028
|
+
host handles submissions, that is the destination. A site published to Uniweb
|
|
2029
|
+
Cloud asks for it with `submit: true` under `services:` and names no address.
|
|
2030
|
+
2. **An address of the site's own** — `submit: <url>` under `services:`, for a
|
|
2031
|
+
static site or a host that does not handle submissions. On a host that does, it
|
|
2032
|
+
asks the host to leave its own off, so this one answers.
|
|
2019
2033
|
3. **Neither** — there is no destination: render no form, or fall back to contact
|
|
2020
2034
|
details the site already carries.
|
|
2021
2035
|
|
|
2022
2036
|
That is the general arrangement, not a forms-only one. A host states everything
|
|
2023
|
-
it offers in the served payload's `services`, keyed by name, and every service
|
|
2024
|
-
resolves by the same rule — the host's offer, then
|
|
2037
|
+
it offers in the served payload's `config.services`, keyed by name, and every service
|
|
2038
|
+
resolves by the same rule — the host's offer, then the site's own address, then
|
|
2039
|
+
neither.
|
|
2025
2040
|
|
|
2026
2041
|
⭐ **Before you render UI for a service, ask whether the site has it** — one predicate per service,
|
|
2027
|
-
no arguments: `isSearchEnabled()`, `isSubmitEnabled()`, `
|
|
2042
|
+
no arguments: `isSearchEnabled()`, `isSubmitEnabled()`, `isBackendEnabled()`, `isAssistantEnabled()`,
|
|
2028
2043
|
`isTrackingEnabled()`.
|
|
2029
2044
|
|
|
2030
2045
|
```jsx
|
|
@@ -2116,10 +2131,15 @@ wherever tracking is configured, with no help from your components. List
|
|
|
2116
2131
|
leaving it out does not switch the baseline off.
|
|
2117
2132
|
|
|
2118
2133
|
```yaml
|
|
2119
|
-
# site.yml
|
|
2120
|
-
|
|
2121
|
-
submit:
|
|
2122
|
-
|
|
2134
|
+
# site.yml
|
|
2135
|
+
services:
|
|
2136
|
+
submit: true # your host's form handling (Uniweb Cloud)
|
|
2137
|
+
```
|
|
2138
|
+
|
|
2139
|
+
```yaml
|
|
2140
|
+
# site.yml — when YOU are providing the endpoint: `uniweb export`, most `deploy --host` targets
|
|
2141
|
+
services:
|
|
2142
|
+
submit: https://forms.example.com/intake # or /forms, base-relative like search's endpoint
|
|
2123
2143
|
```
|
|
2124
2144
|
|
|
2125
2145
|
```jsx
|
|
@@ -2138,12 +2158,12 @@ if (!canSubmit) return null // nowhere to send — render no form, or fall ba
|
|
|
2138
2158
|
```
|
|
2139
2159
|
|
|
2140
2160
|
> **The framework never invents an endpoint — but a host may supply one.** Don't
|
|
2141
|
-
>
|
|
2142
|
-
>
|
|
2143
|
-
>
|
|
2161
|
+
> write an address reflexively: on Uniweb Cloud an address asks the host to turn its
|
|
2162
|
+
> own form handling off — `submit: true` is the whole declaration there. Reach for an
|
|
2163
|
+
> address when you are the one hosting.
|
|
2144
2164
|
>
|
|
2145
|
-
> `canSubmit` is false only when neither
|
|
2146
|
-
> destination. **Check it when you render, not only on the button press** — a
|
|
2165
|
+
> `canSubmit` is false only when neither the host nor an address of the site's own
|
|
2166
|
+
> supplies a destination. **Check it when you render, not only on the button press** — a
|
|
2147
2167
|
> form nobody can send should not be on the page at all. A read that 404s
|
|
2148
2168
|
> degrades to `[]` and the page still renders; a write that 404s loses what a
|
|
2149
2169
|
> person typed, so it gets no silent fallback.
|
|
@@ -2197,7 +2217,7 @@ from. `values` keeps the `File` so your input can show its selection.
|
|
|
2197
2217
|
|
|
2198
2218
|
Full reference: `development/receiving-form-submissions.md`.
|
|
2199
2219
|
|
|
2200
|
-
### Tracking (`tracking
|
|
2220
|
+
### Tracking (`services.tracking`)
|
|
2201
2221
|
|
|
2202
2222
|
A site may declare one **tracking destination**, and everything worth counting
|
|
2203
2223
|
goes there as an event on a single stream. A page visit is just the event the
|
|
@@ -2205,23 +2225,28 @@ runtime emits by itself.
|
|
|
2205
2225
|
|
|
2206
2226
|
```yaml
|
|
2207
2227
|
# site.yml — your own collector, on any host
|
|
2208
|
-
|
|
2228
|
+
services:
|
|
2229
|
+
tracking: https://collector.example.com/events
|
|
2230
|
+
```
|
|
2209
2231
|
|
|
2232
|
+
```yaml
|
|
2210
2233
|
# or, when it needs more than an address
|
|
2211
|
-
|
|
2212
|
-
|
|
2213
|
-
|
|
2234
|
+
services:
|
|
2235
|
+
tracking:
|
|
2236
|
+
endpoint: /collect
|
|
2237
|
+
consent: required
|
|
2214
2238
|
```
|
|
2215
2239
|
|
|
2216
|
-
A host may also supply one
|
|
2217
|
-
applies: the host's, then yours, then neither.
|
|
2240
|
+
A host may also supply one — `tracking: true` asks for it on a site you publish —
|
|
2241
|
+
and the usual precedence applies: the host's, then yours, then neither. An
|
|
2242
|
+
endpoint of your own asks the host to leave its collector off, so yours answers.
|
|
2218
2243
|
|
|
2219
2244
|
⚠️ **The endpoint has to accept the framework's own format** — a batched
|
|
2220
2245
|
`{ "events": [ … ] }` POST, documented in `reference/site-configuration.md`. It
|
|
2221
2246
|
is not a third-party analytics product's public API, which expects that
|
|
2222
2247
|
product's own shape.
|
|
2223
2248
|
|
|
2224
|
-
A site may also name a vendor's own script under `tracking.scripts`, which the
|
|
2249
|
+
A site may also name a vendor's own script under `services.tracking.scripts`, which the
|
|
2225
2250
|
runtime loads once, after consent when a gate is declared, and never in a frame
|
|
2226
2251
|
or during prerender. That is a **separate path with no connection to the stream
|
|
2227
2252
|
below** — the vendor measures its own way, and nothing you `track()` reaches it.
|
|
@@ -2271,21 +2296,24 @@ Narrow or widen that with `emit`:
|
|
|
2271
2296
|
|
|
2272
2297
|
```yaml
|
|
2273
2298
|
# site.yml — your own collector
|
|
2274
|
-
|
|
2275
|
-
|
|
2276
|
-
|
|
2299
|
+
services:
|
|
2300
|
+
tracking:
|
|
2301
|
+
endpoint: https://collector.example.com/events
|
|
2302
|
+
emit: standard # minimal | standard | all — or a list of event names
|
|
2303
|
+
```
|
|
2277
2304
|
|
|
2305
|
+
```yaml
|
|
2278
2306
|
# site.yml — a host that supplies the collector: say what to send, not where
|
|
2279
|
-
|
|
2280
|
-
|
|
2307
|
+
services:
|
|
2308
|
+
tracking:
|
|
2309
|
+
emit: minimal
|
|
2281
2310
|
```
|
|
2282
2311
|
|
|
2283
2312
|
⭐ **`emit` needs no endpoint of its own.** Where a host provides one, the site
|
|
2284
2313
|
declares only what it wants sent and the address comes from the host. The two
|
|
2285
2314
|
are read key by key, so naming `emit` alone overrides nothing else the host
|
|
2286
|
-
declared. An `endpoint:` of your own
|
|
2287
|
-
|
|
2288
|
-
one, the host's is used.
|
|
2315
|
+
declared. An `endpoint:` of your own means you collect yourself, and asks the host
|
|
2316
|
+
to leave its collector off.
|
|
2289
2317
|
|
|
2290
2318
|
`minimal` is `page_view` alone. `standard` is the default when the collector is
|
|
2291
2319
|
your own. `all` is a standing yes, so an event added in a later framework
|
|
@@ -2442,7 +2470,7 @@ For cases the factory doesn't cover, write handlers directly using `Loom`, `inst
|
|
|
2442
2470
|
## Part 4b — When the site is also an app
|
|
2443
2471
|
|
|
2444
2472
|
Everything above is a site: content the author writes, built into pages. Some sites
|
|
2445
|
-
also have **
|
|
2473
|
+
also have **their own backend, the `backend` service** — accounts, per-visitor data, records their members
|
|
2446
2474
|
create and edit. `@uniweb/api` is its client.
|
|
2447
2475
|
|
|
2448
2476
|
⛔ **Only reach for this when the site actually has one.** A site without one is
|
|
@@ -2456,13 +2484,13 @@ npm install @uniweb/api # in the FOUNDATION, beside @uniweb/kit
|
|
|
2456
2484
|
### Ask before you draw
|
|
2457
2485
|
|
|
2458
2486
|
```jsx
|
|
2459
|
-
import {
|
|
2487
|
+
import { isBackendEnabled } from '@uniweb/kit'
|
|
2460
2488
|
import { useSession, SignedIn, SignedOut } from '@uniweb/api'
|
|
2461
2489
|
|
|
2462
|
-
if (!
|
|
2490
|
+
if (!isBackendEnabled()) return <StaticVersion /> // synchronous — nothing to await
|
|
2463
2491
|
```
|
|
2464
2492
|
|
|
2465
|
-
⛔ **When the site has no `
|
|
2493
|
+
⛔ **When the site has no `backend` service, draw nothing** — not a disabled control, and not an
|
|
2466
2494
|
explanation. Same rule as `services` in Part 4: which capabilities a site's operator
|
|
2467
2495
|
set up is none of a visitor's business, and "sign-in unavailable" reads as breakage
|
|
2468
2496
|
when it is simply a feature this site does not have. Render the version of your
|
|
@@ -2484,7 +2512,7 @@ remove it. `scope: 'mine'` lists only the viewer's own records. Without it, the
|
|
|
2484
2512
|
also gets everyone else's.
|
|
2485
2513
|
|
|
2486
2514
|
⭐ **`absent` and an empty `ready` are different answers, and confusing them is the
|
|
2487
|
-
mistake to avoid.** `absent` = there is no live source (no `
|
|
2515
|
+
mistake to avoid.** `absent` = there is no live source (no `backend` service, or nobody signed
|
|
2488
2516
|
in) → render the site's authored content. `ready` with `records: []` = the service
|
|
2489
2517
|
answered and there is nothing there → render your empty state. Showing "nothing yet"
|
|
2490
2518
|
for the first tells a visitor their content is gone when it was never requested.
|
|
@@ -2540,8 +2568,9 @@ permission model and a CSS one.
|
|
|
2540
2568
|
You do not need a live backend to build against one. In `site.yml`:
|
|
2541
2569
|
|
|
2542
2570
|
```yaml
|
|
2543
|
-
|
|
2544
|
-
|
|
2571
|
+
services:
|
|
2572
|
+
backend: true # ask your host for it when you publish
|
|
2573
|
+
$devBackend: ./mock/api.js # what answers it in `uniweb dev`; `$` keys are never published
|
|
2545
2574
|
```
|
|
2546
2575
|
|
|
2547
2576
|
```js
|
|
@@ -2550,8 +2579,10 @@ import { createMockBackend } from '@uniweb/api/mock'
|
|
|
2550
2579
|
export default createMockBackend({ seed }).fetch
|
|
2551
2580
|
```
|
|
2552
2581
|
|
|
2553
|
-
`uniweb dev`
|
|
2554
|
-
|
|
2582
|
+
`uniweb dev` answers your site's `backend` service with it, at an address the dev server
|
|
2583
|
+
supplies on its own origin, so cookies behave exactly as they will in production. Write
|
|
2584
|
+
no address of your own for it: under `services:` an address means a backend you run, and
|
|
2585
|
+
asks your host to leave its own off. It **enforces** who
|
|
2555
2586
|
may edit an entry and `append_only`, so a permission you are relying on fails on your
|
|
2556
2587
|
machine rather than in front of a user. State is in memory; restart to reset.
|
|
2557
2588
|
|
|
@@ -2578,7 +2609,7 @@ uniweb push / pull / clone / status # Git-style content sync with the Uniweb b
|
|
|
2578
2609
|
uniweb refresh / sync # Catch up (git + backend, never pushes) / catch up, then push
|
|
2579
2610
|
uniweb login --org @acme # The workspace you work in — new sites are created there (see below)
|
|
2580
2611
|
uniweb register [--scope @scope] # Register a foundation + its data schemas to the registry
|
|
2581
|
-
uniweb login / logout # One backend, one workspace at a time: log in (--
|
|
2612
|
+
uniweb login / logout # One backend, one workspace at a time: log in (--server, --org) or out
|
|
2582
2613
|
uniweb org list / create <handle> # Your personal scope, and the orgs you belong to
|
|
2583
2614
|
uniweb content export [dir] # Package a site (or a built foundation's schema) as .uwx
|
|
2584
2615
|
|
|
@@ -2655,31 +2686,46 @@ Foundations have their own free path too: `uniweb add ci --target foundation` pu
|
|
|
2655
2686
|
|
|
2656
2687
|
**Nobody's work is overwritten without asking.** `uniweb push` is refused if the site changed on the backend since your last pull — usually an author editing in the app — and reports what changed; nothing is written. Edits to different sections never collide. `uniweb pull` overwrites rather than merges, so it refuses while you have uncommitted changes. The recovery for both is `uniweb pull --merge` (or `uniweb refresh`, which runs `git pull` first): changes to different parts of a file combine silently, and a genuine overlap leaves conflict markers and a non-zero exit — so `uniweb pull --merge && uniweb push` never ships markers. `--force` is the deliberate overwrite (on `push` it replaces the backend's changes, on `pull` it discards yours); don't use it to get past a refusal. `uniweb sync` is `refresh` then `push`; neither publishes.
|
|
2657
2688
|
|
|
2658
|
-
**`sync.json` says which site this is, on each backend.** The first push to a backend records what that backend assigned — the site's id and owner, the ids of its records and uploaded files — in `sync.json` beside `site.yml`. Commit it; never edit it. **To make a new site from a copy of a project, run `uniweb forget --all` in the copy before its first push** — the copy carries the original's `sync.json`, so otherwise its push updates the original's site. When two projects in one workspace hold the same site, a push or publish from either is refused until that is done. `uniweb forget --
|
|
2689
|
+
**`sync.json` says which site this is, on each backend.** The first push to a backend records what that backend assigned — the site's id and owner, the ids of its records and uploaded files — in `sync.json` beside `site.yml`. Commit it; never edit it. **To make a new site from a copy of a project, run `uniweb forget --all` in the copy before its first push** — the copy carries the original's `sync.json`, so otherwise its push updates the original's site. When two projects in one workspace hold the same site, a push or publish from either is refused until that is done. `uniweb forget --server <url>` removes just one backend's records, such as a scratch server's. A record file's `$uuid` is its own id and stays in both cases. **`push`, `pull` and `publish` go to the backend you are logged in to** — the last `uniweb login --server <url>` — and never to one you are not logged in to. A bare `uniweb login` logs in to https://uniweb.app; for any other backend, name it — logging in is how you switch, and the backend commands take no `--server` of their own. When the project has no site on that backend but has one elsewhere, a push says so before creating a new one.
|
|
2659
2690
|
|
|
2660
|
-
**`services:` in `site.yml`
|
|
2691
|
+
**`services:` in `site.yml` is where a site says which services it uses** — site search, form
|
|
2692
|
+
handling, analytics, an assistant, accounts — one entry per service:
|
|
2661
2693
|
|
|
2662
2694
|
```yaml
|
|
2663
2695
|
services:
|
|
2664
|
-
search: true
|
|
2665
|
-
submit: false
|
|
2666
|
-
|
|
2696
|
+
search: true # on — your host's, or the built-in index
|
|
2697
|
+
submit: false # off
|
|
2698
|
+
tracking: https://collector.example.com/events # a provider you bring
|
|
2699
|
+
backend: # on, with the service's own settings
|
|
2667
2700
|
grade: pro
|
|
2668
2701
|
```
|
|
2669
2702
|
|
|
2670
|
-
|
|
2671
|
-
|
|
2672
|
-
|
|
2673
|
-
`
|
|
2674
|
-
|
|
2675
|
-
|
|
2676
|
-
|
|
2703
|
+
An entry is `true`, `false`, an address of your own (a string, or `endpoint:` in a map), or a map
|
|
2704
|
+
of options; anything else stops the build and the push. ⚠️ `yes`, `no`, `on` and `off` are text to
|
|
2705
|
+
YAML, not switches — write `true` or `false`. On a site you push or publish it is also **what you ask your host for**: `true` asks for
|
|
2706
|
+
its service, `false` asks it to turn its service off, and an address asks it to leave its own off so
|
|
2707
|
+
yours answers. **On a site your host publishes, a service is off unless `services:` asks for it** —
|
|
2708
|
+
list the ones the site uses (the templates list theirs). `records: true` asks for your records to be
|
|
2709
|
+
delivered live to the published pages: a query whose records have a data schema shows nothing there
|
|
2710
|
+
without it, and `publish` says so; a static build reads records from files and ignores it. An entry
|
|
2711
|
+
says everything about its service: a setting you remove from it is removed on the next push, and
|
|
2712
|
+
removing the entry switches the service off. `uniweb push` and `uniweb publish` send each service
|
|
2713
|
+
`site.yml` lists; a service the site has that this copy has never seen (turned on in the app since
|
|
2714
|
+
your last pull) is left as it is, and one changed both in the file and on the site since your last
|
|
2715
|
+
pull stops the push, naming it — `uniweb pull --merge`, then push. `uniweb pull` writes what the site
|
|
2716
|
+
has into `services:` (an off service with no settings only where the file already names it) — pull
|
|
2717
|
+
before you edit. A change that needs payment is settled in the app: `publish` opens it. **Everything in an entry but a
|
|
2718
|
+
credential is public** — it is built into the site, except `backend`'s settings, which only your host
|
|
2719
|
+
reads; a key or token is set in the app. ⛔ The top-level `search:` / `submit:` / `assistant:` /
|
|
2720
|
+
`tracking:` / `api:` keys are retired: the build stops on them and says where each one moves. So
|
|
2721
|
+
does `api` under `services:` — the site's own backend is the `backend` service — and `$devApi:`,
|
|
2722
|
+
which is `$devBackend:`.
|
|
2677
2723
|
|
|
2678
2724
|
**The Cloud also provides a real backend for structured data:** a database for every registered data schema, and a CMS that edits both static page content and dynamic data entities typed by those schemas. That's the piece that makes it viable for teams and client work — the client manages records, not markdown files.
|
|
2679
2725
|
|
|
2680
2726
|
Either side can publish. Nothing about this changes how you build: the same foundation and the same site run under `uniweb dev`, `uniweb export`, or a CI deploy with no account at all.
|
|
2681
2727
|
|
|
2682
|
-
**Publishing vs registering.** Foundations on Uniweb Cloud live in the catalog as `@org/name@version` — `name` from the foundation's `main.js`. When a foundation powers a single site, **don't run `uniweb register` yourself** — `uniweb publish` from the site directory releases the local foundation to the catalog (when its code changed) and goes live in one step. A registered version is immutable, so when the foundation's code changed under a version already registered, `push` and `publish` release the change under the next version and write it into the foundation's `package.json` — commit that file (`--no-release` sends the content without the code). The version it picks is always the next patch: for a change that is not backwards compatible, set a higher minor or major version in the foundation's `package.json` yourself before you push. They stop when the registry holds a version newer than yours, released from another copy: pull that change first, or pass `--bump` to release yours above it. Register deliberately only when the foundation is a product meant for multiple sites; consuming sites then pin `foundation: '@org/name@1.2.3'`. **The catalog is private and access-segregated, not a public package registry** — people see only the foundations licensed to sites they own or edit. The *site* carries the license, and it rides along with site ownership when a developer hands a site to a client. Don't describe publishing as making a foundation publicly discoverable. Schemas can also be registered on their own from a schemas-only package (`@uniweb/schemas`, any `@org/schemas`, or a bare folder of `schemas/*.{yml,json,js}`) — that's how `@std` schemas are published. Auth via `uniweb login` (`uniweb login --
|
|
2728
|
+
**Publishing vs registering.** Foundations on Uniweb Cloud live in the catalog as `@org/name@version` — `name` from the foundation's `main.js`. When a foundation powers a single site, **don't run `uniweb register` yourself** — `uniweb publish` from the site directory releases the local foundation to the catalog (when its code changed) and goes live in one step. A registered version is immutable, so when the foundation's code changed under a version already registered, `push` and `publish` release the change under the next version and write it into the foundation's `package.json` — commit that file (`--no-release` sends the content without the code). The version it picks is always the next patch: for a change that is not backwards compatible, set a higher minor or major version in the foundation's `package.json` yourself before you push. They stop when the registry holds a version newer than yours, released from another copy: pull that change first, or pass `--bump` to release yours above it. Register deliberately only when the foundation is a product meant for multiple sites; consuming sites then pin `foundation: '@org/name@1.2.3'`. **The catalog is private and access-segregated, not a public package registry** — people see only the foundations licensed to sites they own or edit. The *site* carries the license, and it rides along with site ownership when a developer hands a site to a client. Don't describe publishing as making a foundation publicly discoverable. Schemas can also be registered on their own from a schemas-only package (`@uniweb/schemas`, any `@org/schemas`, or a bare folder of `schemas/*.{yml,json,js}`) — that's how `@std` schemas are published. Auth via `uniweb login` (`uniweb login --server <url> --token <bearer>` without a terminal) or `UNIWEB_TOKEN`; preview with `--dry-run`.
|
|
2683
2729
|
|
|
2684
2730
|
### Staying current
|
|
2685
2731
|
|
package/src/backend/client.js
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* `fetch(… Authorization: Bearer …)` against a per-command default origin. It
|
|
8
8
|
* owns the three things that were previously scattered across a dozen files:
|
|
9
9
|
*
|
|
10
|
-
* 1. ORIGIN — where the backend is. resolveBackendOrigin():
|
|
10
|
+
* 1. ORIGIN — where the backend is. resolveBackendOrigin(): UNIWEB_SERVER >
|
|
11
11
|
* the backend you are logged in to > the default backend (below).
|
|
12
12
|
* Each is reduced to its origin, http(s) only. *(This named a
|
|
13
13
|
* `--backend` flag as the first tier until 2026-10-05; the verbs
|
|
@@ -46,7 +46,7 @@ import { uploadSiteAssets } from '../utils/asset-upload.js'
|
|
|
46
46
|
/**
|
|
47
47
|
* Resolve the backend a command talks to:
|
|
48
48
|
*
|
|
49
|
-
* 1.
|
|
49
|
+
* 1. UNIWEB_SERVER env — the override for automation (CI, scripts), one process
|
|
50
50
|
* 2. ⭐ the backend the user is LOGGED IN TO — the one session there is
|
|
51
51
|
* 3. the default backend — ~/.uniweb/config.json `registryApiUrl`, else uniweb.app —
|
|
52
52
|
* where the command's first request then asks the user to log in
|
|
@@ -60,9 +60,10 @@ import { uploadSiteAssets } from '../utils/asset-upload.js'
|
|
|
60
60
|
* ⛔ **No `--backend` tier, and no project tier.** The backend verbs had a per-command
|
|
61
61
|
* `--backend` until 2026-09-21 — it predates per-backend sessions, when one session
|
|
62
62
|
* slot made "aim this one command elsewhere" a flag's job; switching is a login now, and
|
|
63
|
-
* a script aims with
|
|
63
|
+
* a script aims with UNIWEB_SERVER without touching the machine's login. A
|
|
64
64
|
* project's sync.json and deploy.yml routed commands too, until the same day.
|
|
65
|
-
*
|
|
65
|
+
* A backend flag survives only where it SELECTS rather than routes — `--server`, `--backend`
|
|
66
|
+
* until 2026-10-07: `login` (where to log
|
|
66
67
|
* in) and `forget` (which backend's records to remove). `logout` needs none: there is
|
|
67
68
|
* one session.
|
|
68
69
|
*
|
|
@@ -227,6 +228,27 @@ export class WorkspaceMismatchError extends Error {
|
|
|
227
228
|
}
|
|
228
229
|
}
|
|
229
230
|
|
|
231
|
+
/**
|
|
232
|
+
* The backend could not be reached at all — the request never got an answer.
|
|
233
|
+
*
|
|
234
|
+
* ⛔ Not an answer, so it must not be read as one. A lookup that folds this into "not
|
|
235
|
+
* found" sends its caller down the not-found path: `readFoundationLatest` did, and a
|
|
236
|
+
* publish with the backend down announced "Releasing the foundation (not yet
|
|
237
|
+
* registered)…", failed inside `register`, and ended on "Fix the foundation".
|
|
238
|
+
*/
|
|
239
|
+
export class BackendUnreachableError extends Error {
|
|
240
|
+
/**
|
|
241
|
+
* @param {string} origin
|
|
242
|
+
* @param {unknown} cause - the transport error `fetch` threw
|
|
243
|
+
*/
|
|
244
|
+
constructor(origin, cause) {
|
|
245
|
+
super(`Could not reach the backend at ${origin}: ${cause?.message ?? cause}`)
|
|
246
|
+
this.name = 'BackendUnreachableError'
|
|
247
|
+
this.origin = origin
|
|
248
|
+
this.cause = cause
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
230
252
|
/**
|
|
231
253
|
* The line to print when a request THREW rather than answered.
|
|
232
254
|
*
|
|
@@ -562,7 +584,9 @@ export class BackendClient {
|
|
|
562
584
|
|
|
563
585
|
/**
|
|
564
586
|
* GET /dev/registry/{scope}/{name} → the latest registered foundation version
|
|
565
|
-
* + its content digest, or null
|
|
587
|
+
* + its content digest, or null when the backend answers with anything else (callers
|
|
588
|
+
* degrade). ⛔ A backend that cannot be reached is not an answer: that throws
|
|
589
|
+
* `BackendUnreachableError` rather than reading as "not registered".
|
|
566
590
|
*
|
|
567
591
|
* The bare `{scope}/{name}` path resolves the latest foundation version (the
|
|
568
592
|
* data-schema sibling is `/dev/registry/data-schemas/{scope}/{name}`). The
|
|
@@ -589,7 +613,11 @@ export class BackendClient {
|
|
|
589
613
|
...body,
|
|
590
614
|
latest_version: body.latest_version ?? body.version ?? null
|
|
591
615
|
}
|
|
592
|
-
} catch {
|
|
616
|
+
} catch (err) {
|
|
617
|
+
// Node's `fetch` throws `TypeError: fetch failed` when nothing answered.
|
|
618
|
+
if (err instanceof TypeError && /fetch failed/i.test(err.message)) {
|
|
619
|
+
throw new BackendUnreachableError(this.origin, err)
|
|
620
|
+
}
|
|
593
621
|
return null
|
|
594
622
|
}
|
|
595
623
|
}
|
|
@@ -785,6 +813,31 @@ export class BackendClient {
|
|
|
785
813
|
})
|
|
786
814
|
}
|
|
787
815
|
|
|
816
|
+
/**
|
|
817
|
+
* GET /dev/site — one page of the sites in the workspace the client names
|
|
818
|
+
* (`setWorkspace`; none ⇒ personal): `{ sites: [{ uuid, name?, updated_at,
|
|
819
|
+
* deployment?: { status, published_url, … } }] }`. A page shorter than `limit` is
|
|
820
|
+
* the last. `deployment` is absent for a site never published.
|
|
821
|
+
*
|
|
822
|
+
* @param {{ limit?: number, offset?: number }} [opts]
|
|
823
|
+
* @returns {Promise<Response>}
|
|
824
|
+
*/
|
|
825
|
+
async listSites({ limit = 100, offset = 0 } = {}) {
|
|
826
|
+
return this.request('/dev/site', { query: { limit, offset } })
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
/**
|
|
830
|
+
* DELETE /dev/site/{uuid} — delete a site, finally: no trash, no restore. Only an
|
|
831
|
+
* admin of the site may. Refused `409` with `blockers: [{ resource, detail }]` while
|
|
832
|
+
* anything is still active — `published` among them, which `unpublishSite` clears.
|
|
833
|
+
*
|
|
834
|
+
* @param {string} uuid - the site-content uuid
|
|
835
|
+
* @returns {Promise<Response>}
|
|
836
|
+
*/
|
|
837
|
+
async deleteSite(uuid) {
|
|
838
|
+
return this.request(`/dev/site/${encodeURIComponent(uuid)}`, { method: 'DELETE' })
|
|
839
|
+
}
|
|
840
|
+
|
|
788
841
|
/**
|
|
789
842
|
* GET /dev/site/status/{uuid} → the site's publish lifecycle (Contract 3,
|
|
790
843
|
* shipped backend-side — collab backend↔framework):
|
|
@@ -197,7 +197,7 @@ function writePkgVersion(dir, version) {
|
|
|
197
197
|
// and the WORKSPACE this command names (`--org` / `--personal`). The BACKEND travels in
|
|
198
198
|
// the child's environment (releaseFoundation), and the SESSION is the shared session
|
|
199
199
|
// file — or UNIWEB_TOKEN, which the child inherits. The backend commands take no
|
|
200
|
-
// `--
|
|
200
|
+
// backend flag (`--server` is the login's) and no `--token`.
|
|
201
201
|
//
|
|
202
202
|
// ⭐ The workspace travels because a bare name registers under the workspace the command
|
|
203
203
|
// works in *[Diego, 2026-10-06]*, and a workspace named on this command is this command's
|
|
@@ -577,7 +577,9 @@ function buildFoundation(local, cliBin) {
|
|
|
577
577
|
execFileSync('node', [cliBin, 'build', '--target', 'foundation'], {
|
|
578
578
|
cwd: local.dir,
|
|
579
579
|
stdio: 'inherit',
|
|
580
|
-
|
|
580
|
+
// A step of this command, so the build skips the next-step hints meant for
|
|
581
|
+
// someone who ran `uniweb build` themselves.
|
|
582
|
+
env: { ...process.env, UNIWEB_BUILD_STEP: '1' }
|
|
581
583
|
})
|
|
582
584
|
}
|
|
583
585
|
|
|
@@ -592,7 +594,7 @@ function releaseFoundation(local, args, cliBin, say, origin) {
|
|
|
592
594
|
execFileSync('node', [cliBin, 'register', ...forwardedFlags(args)], {
|
|
593
595
|
cwd: local.dir,
|
|
594
596
|
stdio: 'inherit',
|
|
595
|
-
env: origin ? { ...process.env,
|
|
597
|
+
env: origin ? { ...process.env, UNIWEB_SERVER: origin } : process.env
|
|
596
598
|
})
|
|
597
599
|
console.log('')
|
|
598
600
|
return true
|