uniweb 0.13.17 → 0.14.1
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/README.md +1 -1
- package/package.json +7 -7
- package/partials/agents.md +51 -0
- package/src/backend/client.js +173 -40
- package/src/backend/data-bundle.js +15 -3
- package/src/backend/foundation-bring-along.js +76 -21
- package/src/backend/payment-handoff.js +28 -9
- package/src/backend/site-media.js +147 -19
- package/src/backend/site-sync.js +362 -40
- package/src/commands/add.js +575 -273
- package/src/commands/build.js +160 -57
- package/src/commands/clone.js +79 -26
- package/src/commands/content.js +11 -7
- package/src/commands/deploy.js +80 -32
- package/src/commands/dev.js +39 -17
- package/src/commands/docs.js +44 -16
- package/src/commands/doctor.js +338 -82
- package/src/commands/export.js +15 -7
- package/src/commands/handoff.js +6 -2
- package/src/commands/i18n.js +248 -99
- package/src/commands/inspect.js +48 -19
- package/src/commands/invite.js +9 -3
- package/src/commands/org.js +19 -5
- package/src/commands/publish.js +229 -60
- package/src/commands/pull.js +157 -48
- package/src/commands/push.js +144 -42
- package/src/commands/refresh.js +37 -10
- package/src/commands/register.js +235 -64
- package/src/commands/rename.js +126 -46
- package/src/commands/runtime.js +74 -24
- package/src/commands/status.js +48 -15
- package/src/commands/sync.js +7 -2
- package/src/commands/template.js +12 -4
- package/src/commands/update.js +263 -87
- package/src/commands/validate.js +115 -32
- package/src/framework-index.json +15 -13
- package/src/index.js +298 -145
- package/src/templates/fetchers/github.js +7 -7
- package/src/templates/fetchers/npm.js +3 -3
- package/src/templates/fetchers/release.js +21 -18
- package/src/templates/index.js +34 -12
- package/src/templates/processor.js +44 -12
- package/src/templates/resolver.js +42 -15
- package/src/templates/validator.js +14 -7
- package/src/utils/asset-upload.js +90 -22
- package/src/utils/code-upload.js +62 -37
- package/src/utils/config.js +31 -10
- package/src/utils/dep-survey.js +33 -7
- package/src/utils/destination-prompt.js +27 -17
- package/src/utils/git.js +66 -22
- package/src/utils/host-prompt.js +4 -3
- package/src/utils/install-integrity.js +8 -4
- package/src/utils/interactive.js +3 -3
- package/src/utils/json-file.js +6 -2
- package/src/utils/names.js +15 -6
- package/src/utils/placement.js +16 -8
- package/src/utils/registry-auth.js +185 -57
- package/src/utils/registry-orgs.js +142 -53
- package/src/utils/runtime-upload.js +52 -14
- package/src/utils/scaffold.js +55 -14
- package/src/utils/site-content-refs.js +3 -1
- package/src/utils/update-check.js +43 -17
- package/src/utils/workspace.js +13 -7
- package/src/versions.js +18 -7
- package/templates/site/_gitignore +5 -0
package/README.md
CHANGED
|
@@ -360,7 +360,7 @@ Markdown in a git repo and content in the Uniweb apps can share the same site. D
|
|
|
360
360
|
|
|
361
361
|
---
|
|
362
362
|
|
|
363
|
-
Both paths use the same framework. The difference is who edits content, where it lives, and what gets deployed. For the deeper menu —
|
|
363
|
+
Both paths use the same framework. The difference is who edits content, where it lives, and what gets deployed. For the deeper menu — standalone vs cataloged foundations, S3+CloudFront, manual exports, per-host recipes, foundation propagation — see → **[Deploying](https://github.com/uniweb/docs/blob/main/development/deploying.md)**.
|
|
364
364
|
|
|
365
365
|
---
|
|
366
366
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "uniweb",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.14.1",
|
|
4
4
|
"description": "Create structured Vite + React sites with content/code separation",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -41,14 +41,14 @@
|
|
|
41
41
|
"js-yaml": "^4.1.0",
|
|
42
42
|
"prompts": "^2.4.2",
|
|
43
43
|
"tar": "^7.0.0",
|
|
44
|
-
"@uniweb/core": "0.
|
|
45
|
-
"@uniweb/kit": "0.
|
|
46
|
-
"@uniweb/runtime": "0.
|
|
44
|
+
"@uniweb/core": "0.8.0",
|
|
45
|
+
"@uniweb/kit": "0.10.1",
|
|
46
|
+
"@uniweb/runtime": "0.9.0"
|
|
47
47
|
},
|
|
48
48
|
"peerDependencies": {
|
|
49
|
-
"@uniweb/build": "0.
|
|
50
|
-
"@uniweb/content-reader": "1.
|
|
51
|
-
"@uniweb/semantic-parser": "1.
|
|
49
|
+
"@uniweb/build": "0.16.0",
|
|
50
|
+
"@uniweb/content-reader": "1.2.0",
|
|
51
|
+
"@uniweb/semantic-parser": "1.2.0"
|
|
52
52
|
},
|
|
53
53
|
"peerDependenciesMeta": {
|
|
54
54
|
"@uniweb/build": {
|
package/partials/agents.md
CHANGED
|
@@ -468,8 +468,12 @@ Optional attributes: `{size=20}`, `{color=red}`. Custom SVGs: `{target=_blank} <!-- Open in new tab -->
|
|
469
469
|
[text](./file.pdf){download} <!-- Download -->
|
|
470
470
|
{role=banner} <!-- Role determines array: images, icons, or videos -->
|
|
471
|
+
{role=video} <!-- → content.videos[], not content.images[] -->
|
|
472
|
+
{href=/products} <!-- Clickable media: href + target ride along -->
|
|
471
473
|
```
|
|
472
474
|
|
|
475
|
+
Sizing and loading ride on the image: `{width=800 height=600 loading=lazy fit=cover position=center}`. A video adds `{poster=./thumb.jpg autoplay muted loop controls}`; a `{role=pdf}` document adds `{preview=./cover.jpg author=… description=…}`.
|
|
476
|
+
|
|
473
477
|
**Quote values containing spaces:** `{note="Ready to go"}`, not `{note=Ready to go}` — unquoted values end at the first space.
|
|
474
478
|
|
|
475
479
|
**Separators are forgiving.** `:` works wherever `=` does, and pairs may be separated by whitespace, a comma, or both — `{role=banner width=1200}`, `{role:banner, width:1200}` and `{role:banner,width:1200}` are identical. `=` and spaces are canonical, and that is what gets written back on save. Two rules keep it unambiguous: the separator must **touch** the key (`{note:warning}` is one pair, `{note : warning}` is two flags), and a value containing a **comma** must be quoted (`{style="a, b"}`). A value containing a colon needs no quoting — only the first colon separates, so `{href:https://example.com}` is fine.
|
|
@@ -493,6 +497,10 @@ This is [less important]{muted} context.
|
|
|
493
497
|
|
|
494
498
|
Sites can adjust these or add named styles in `theme.yml`'s `inline:` section. Overrides merge **property by property**, so declare only what differs (`accent: { font-weight: inherit }` keeps the default color); to drop a default property rather than change it, give it a neutral value (`initial`, `inherit`, `unset`).
|
|
495
499
|
|
|
500
|
+
> **On a site synced with Uniweb Cloud, the built-in style names round-trip *editable*** — an author who opens a page containing `[Get started]{accent}` gets the editor's own styling control for it, and can change or remove it like any text they styled there.
|
|
501
|
+
>
|
|
502
|
+
> **A name the editor doesn't know is preserved exactly, but not editable** — a style you defined yourself in `inline:`, two names on one span, or a `class`/`id` set. Nothing is lost and nothing is approximated: it survives every edit and save untouched, the editor simply offers no control for it. That is a permanent property rather than a gap, because the name set is open by design — you can add styles the editor has no way to know about.
|
|
503
|
+
|
|
496
504
|
### Fenced code: data blocks vs snippets
|
|
497
505
|
|
|
498
506
|
Fenced code serves two purposes depending on whether it carries a tag.
|
|
@@ -630,6 +638,49 @@ seo:
|
|
|
630
638
|
|
|
631
639
|
**Localized URLs:** on a multilingual site (`languages:` in site.yml), `slug: { <lang>: <segment> }` gives a page a native URL segment per language; the folder name stays the canonical route. Nested folders compose automatically, and localized URLs flow through navigation, the language switcher, and the sitemap. See Part 5 for the translation workflow.
|
|
632
640
|
|
|
641
|
+
### Your site is readable by agents, automatically
|
|
642
|
+
|
|
643
|
+
Every build emits two things an AI agent can use directly, alongside the HTML. **Free, on by default, nothing to install.**
|
|
644
|
+
|
|
645
|
+
| Artifact | What it is |
|
|
646
|
+
|---|---|
|
|
647
|
+
| `/llms.txt` | An annotated index of the site — every page, with a one-line description, linking to the `.md` below |
|
|
648
|
+
| `/{route}.md` | Each page as clean markdown: the content, no navigation, no chrome |
|
|
649
|
+
|
|
650
|
+
The point is the pair. An agent fetches the index, *reads* what each page is about, and goes straight to the one it needs — two requests, no HTML stripping, no guessing at URLs. Descriptions come from `page.yml`'s `description:`, then `seo.ogDescription`, then the page's opening paragraph, so a site usually gets a complete index without writing any of them.
|
|
651
|
+
|
|
652
|
+
**Large sites also get per-branch indexes.** A branch with at least 5 pages gets its own — `/docs/llms.txt` lists just the docs, titled by that folder. The site index still lists everything; a branch index is an extra entry point, not a replacement, so an agent that starts at `/llms.txt` still reaches any page in two hops.
|
|
653
|
+
|
|
654
|
+
```yaml
|
|
655
|
+
# site.yml — all optional; these are the defaults
|
|
656
|
+
agents:
|
|
657
|
+
index: true # /llms.txt
|
|
658
|
+
markdown: true # /{route}.md
|
|
659
|
+
branchIndexes: true # /docs/llms.txt for branches big enough to want one
|
|
660
|
+
branchMinPages: 5 # how big is big enough
|
|
661
|
+
exclude: [/internal] # keep a branch out of both (cascades)
|
|
662
|
+
|
|
663
|
+
agents: false # or turn the whole thing off in one word
|
|
664
|
+
```
|
|
665
|
+
|
|
666
|
+
**Set `seo.baseUrl` if you want absolute links** in the index — without it the links are root-relative, which still works for an agent that arrived via the index. `uniweb doctor` warns when it's unset.
|
|
667
|
+
|
|
668
|
+
**What's excluded, and it's deliberate:** `seo.noindex` pages, `hidden` pages, `_`-prefixed drafts, and dynamic route templates. An index *describes* pages rather than merely listing them, so an unlinked page would become both discoverable and summarized — which is why these exclusions are load-bearing rather than tidy-up. `noindex` or `hidden` on a **folder** takes the whole branch with it.
|
|
669
|
+
|
|
670
|
+
**Declaring how your content may be used** is a separate axis from whether it may be fetched, and it goes in `seo.robots`:
|
|
671
|
+
|
|
672
|
+
```yaml
|
|
673
|
+
# site.yml
|
|
674
|
+
seo:
|
|
675
|
+
robots:
|
|
676
|
+
contentSignals:
|
|
677
|
+
search: true # may appear in search results
|
|
678
|
+
ai-input: true # may be retrieved at inference time
|
|
679
|
+
ai-train: false # may not be used to train a model
|
|
680
|
+
```
|
|
681
|
+
|
|
682
|
+
That emits a `Content-Signal:` line in `robots.txt`. Declare only what you mean — an omitted signal says nothing, which is not the same as saying no.
|
|
683
|
+
|
|
633
684
|
### Collections and dynamic routes
|
|
634
685
|
|
|
635
686
|
Most content lives in `pages/` — a fixed composition of sections on a fixed set of pages. **Collections are the other kind: repeating content managed as a set of files**, one item per file, that pages pull from. Blog posts, team members, products, case studies, bibliographies.
|
package/src/backend/client.js
CHANGED
|
@@ -28,8 +28,16 @@
|
|
|
28
28
|
*/
|
|
29
29
|
|
|
30
30
|
import { getRegistryApiBaseUrl } from '../utils/config.js'
|
|
31
|
-
import {
|
|
32
|
-
|
|
31
|
+
import {
|
|
32
|
+
ensureRegistryAuth,
|
|
33
|
+
fetchMe,
|
|
34
|
+
readRegistryAuth,
|
|
35
|
+
isExpired
|
|
36
|
+
} from '../utils/registry-auth.js'
|
|
37
|
+
import {
|
|
38
|
+
fetchOrgs as fetchOrgsImpl,
|
|
39
|
+
createOrg as createOrgImpl
|
|
40
|
+
} from '../utils/registry-orgs.js'
|
|
33
41
|
import { uploadFoundationCode } from '../utils/code-upload.js'
|
|
34
42
|
import { uploadSiteAssets } from '../utils/asset-upload.js'
|
|
35
43
|
import { uploadRuntime } from '../utils/runtime-upload.js'
|
|
@@ -51,11 +59,26 @@ import { uploadRuntime } from '../utils/runtime-upload.js'
|
|
|
51
59
|
* @returns {string} a bare origin with no trailing slash
|
|
52
60
|
*/
|
|
53
61
|
export function resolveBackendOrigin(flag, { siteBackend } = {}) {
|
|
54
|
-
const norm = (v) => {
|
|
55
|
-
|
|
62
|
+
const norm = (v) => {
|
|
63
|
+
try {
|
|
64
|
+
return new URL(v).origin
|
|
65
|
+
} catch {
|
|
66
|
+
return null
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
if (flag) {
|
|
70
|
+
const o = norm(flag)
|
|
71
|
+
if (o) return o
|
|
72
|
+
}
|
|
56
73
|
const env = process.env.UNIWEB_REGISTER_URL
|
|
57
|
-
if (env) {
|
|
58
|
-
|
|
74
|
+
if (env) {
|
|
75
|
+
const o = norm(env)
|
|
76
|
+
if (o) return o
|
|
77
|
+
}
|
|
78
|
+
if (siteBackend) {
|
|
79
|
+
const o = norm(siteBackend)
|
|
80
|
+
if (o) return o
|
|
81
|
+
}
|
|
59
82
|
return getRegistryApiBaseUrl()
|
|
60
83
|
}
|
|
61
84
|
|
|
@@ -67,12 +90,15 @@ export function resolveBackendOrigin(flag, { siteBackend } = {}) {
|
|
|
67
90
|
* `runtime.installed` is empty so runtime resolution requires an explicit pin.
|
|
68
91
|
*/
|
|
69
92
|
export const DISCOVERY_DEFAULTS = {
|
|
70
|
-
|
|
93
|
+
// No serve-root default. Serve locations are read from discovery or from an
|
|
94
|
+
// upload plan's `serve_base`; nothing here reconstructs one, so a default
|
|
95
|
+
// would be a route name with no consumer. (A `gatewayBase` entry lived here
|
|
96
|
+
// unread until 2026-07-29.)
|
|
71
97
|
assetBase: 'https://assets.uniweb.app/',
|
|
72
98
|
auth: { loginPath: '/dev/auth/login', required: true },
|
|
73
99
|
delivery: { deploy: true, publish: true, broker: 'self-serve' },
|
|
74
100
|
assets: { supported: false },
|
|
75
|
-
runtime: { installed: [] }
|
|
101
|
+
runtime: { installed: [] }
|
|
76
102
|
}
|
|
77
103
|
|
|
78
104
|
export class BackendClient {
|
|
@@ -88,8 +114,19 @@ export class BackendClient {
|
|
|
88
114
|
* @param {string} [opts.command] - label for the login prompt ('Pushing', 'Registering', …)
|
|
89
115
|
* @param {typeof fetch} [opts.fetchImpl] - injectable fetch (tests)
|
|
90
116
|
*/
|
|
91
|
-
constructor({
|
|
92
|
-
|
|
117
|
+
constructor({
|
|
118
|
+
origin,
|
|
119
|
+
originFlag,
|
|
120
|
+
siteBackend,
|
|
121
|
+
token,
|
|
122
|
+
getToken,
|
|
123
|
+
args = [],
|
|
124
|
+
command = 'This command',
|
|
125
|
+
fetchImpl
|
|
126
|
+
} = {}) {
|
|
127
|
+
this.origin = (
|
|
128
|
+
origin || resolveBackendOrigin(originFlag, { siteBackend })
|
|
129
|
+
).replace(/\/+$/, '')
|
|
93
130
|
this._token = token || process.env.UNIWEB_TOKEN || null
|
|
94
131
|
this._getToken = getToken || null
|
|
95
132
|
this._args = args
|
|
@@ -117,13 +154,25 @@ export class BackendClient {
|
|
|
117
154
|
this._warnedOriginMismatch = true
|
|
118
155
|
try {
|
|
119
156
|
const stored = await readRegistryAuth()
|
|
120
|
-
if (
|
|
121
|
-
|
|
122
|
-
|
|
157
|
+
if (
|
|
158
|
+
stored?.token &&
|
|
159
|
+
!isExpired(stored) &&
|
|
160
|
+
stored.origin &&
|
|
161
|
+
stored.origin.replace(/\/+$/, '') !== this.origin
|
|
162
|
+
) {
|
|
163
|
+
console.error(
|
|
164
|
+
`\x1b[33m⚠\x1b[0m Logged in to ${stored.origin}, but this command targets ${this.origin} — the session may be rejected. Run \`uniweb login --backend ${this.origin}\`, or pass --token.`
|
|
165
|
+
)
|
|
123
166
|
}
|
|
124
|
-
} catch {
|
|
167
|
+
} catch {
|
|
168
|
+
/* advisory only */
|
|
169
|
+
}
|
|
125
170
|
}
|
|
126
|
-
this._token = await ensureRegistryAuth({
|
|
171
|
+
this._token = await ensureRegistryAuth({
|
|
172
|
+
apiBase: this.origin,
|
|
173
|
+
command: this._command,
|
|
174
|
+
args: this._args
|
|
175
|
+
})
|
|
127
176
|
return this._token
|
|
128
177
|
}
|
|
129
178
|
|
|
@@ -143,7 +192,10 @@ export class BackendClient {
|
|
|
143
192
|
* @param {boolean} [opts.auth=true]
|
|
144
193
|
* @returns {Promise<Response>}
|
|
145
194
|
*/
|
|
146
|
-
async request(
|
|
195
|
+
async request(
|
|
196
|
+
path,
|
|
197
|
+
{ method = 'GET', body, headers = {}, query, auth = true } = {}
|
|
198
|
+
) {
|
|
147
199
|
const url = new URL(path, this.origin)
|
|
148
200
|
if (query) {
|
|
149
201
|
for (const [k, v] of Object.entries(query)) {
|
|
@@ -154,7 +206,8 @@ export class BackendClient {
|
|
|
154
206
|
if (auth) h.Authorization = `Bearer ${await this.token()}`
|
|
155
207
|
if (body != null && h['Content-Type'] == null) {
|
|
156
208
|
if (typeof body === 'string') h['Content-Type'] = 'application/json'
|
|
157
|
-
else if (body instanceof Uint8Array || Buffer.isBuffer(body))
|
|
209
|
+
else if (body instanceof Uint8Array || Buffer.isBuffer(body))
|
|
210
|
+
h['Content-Type'] = 'application/zip'
|
|
158
211
|
}
|
|
159
212
|
return this._fetch(url.href, { method, headers: h, body })
|
|
160
213
|
}
|
|
@@ -167,7 +220,7 @@ export class BackendClient {
|
|
|
167
220
|
* lifetime; a missing route or any transport/parse error falls back to
|
|
168
221
|
* DISCOVERY_DEFAULTS (non-breaking — an older backend still works). Lets the
|
|
169
222
|
* CLI hardcode nothing about a backend but its origin and discover the rest:
|
|
170
|
-
* `
|
|
223
|
+
* `assetBase` (the asset root — relative ⇒ relative-to-origin),
|
|
171
224
|
* `auth`, `delivery` (deploy/publish? broker), `assets` (lane built yet?),
|
|
172
225
|
* `runtime.installed` (the default-runtime source replacing the old /runtime/latest).
|
|
173
226
|
* @returns {Promise<object>}
|
|
@@ -201,7 +254,10 @@ export class BackendClient {
|
|
|
201
254
|
* @returns {Promise<Response>}
|
|
202
255
|
*/
|
|
203
256
|
async register(uwxJson) {
|
|
204
|
-
return this.request('/dev/registry/register', {
|
|
257
|
+
return this.request('/dev/registry/register', {
|
|
258
|
+
method: 'POST',
|
|
259
|
+
body: uwxJson
|
|
260
|
+
})
|
|
205
261
|
}
|
|
206
262
|
|
|
207
263
|
/**
|
|
@@ -210,7 +266,11 @@ export class BackendClient {
|
|
|
210
266
|
* @param {object} opts - { name, version, distDir, files?, onProgress? }
|
|
211
267
|
*/
|
|
212
268
|
async uploadFoundationCode(opts) {
|
|
213
|
-
return uploadFoundationCode({
|
|
269
|
+
return uploadFoundationCode({
|
|
270
|
+
apiBase: this.origin,
|
|
271
|
+
token: await this.token(),
|
|
272
|
+
...opts
|
|
273
|
+
})
|
|
214
274
|
}
|
|
215
275
|
|
|
216
276
|
/**
|
|
@@ -223,7 +283,10 @@ export class BackendClient {
|
|
|
223
283
|
async readDataSchema(modelName) {
|
|
224
284
|
const res = await this.request(dataSchemaPath(modelName))
|
|
225
285
|
if (res.status === 404) return null
|
|
226
|
-
if (!res.ok)
|
|
286
|
+
if (!res.ok)
|
|
287
|
+
throw new Error(
|
|
288
|
+
`Model-read ${modelName} failed: HTTP ${res.status} ${res.statusText}`
|
|
289
|
+
)
|
|
227
290
|
return res.json()
|
|
228
291
|
}
|
|
229
292
|
|
|
@@ -245,12 +308,17 @@ export class BackendClient {
|
|
|
245
308
|
const m = /^@([^/]+)\/([^@/]+)/.exec(String(scopedName || ''))
|
|
246
309
|
if (!m) return null
|
|
247
310
|
try {
|
|
248
|
-
const res = await this.request(
|
|
311
|
+
const res = await this.request(
|
|
312
|
+
`/dev/registry/${encodeURIComponent(m[1])}/${encodeURIComponent(m[2])}`
|
|
313
|
+
)
|
|
249
314
|
if (!res.ok) return null
|
|
250
315
|
const body = await res.json().catch(() => null)
|
|
251
316
|
if (!body) return null
|
|
252
317
|
// The read returns `version`; callers use `latest_version`. Tolerate both.
|
|
253
|
-
return {
|
|
318
|
+
return {
|
|
319
|
+
...body,
|
|
320
|
+
latest_version: body.latest_version ?? body.version ?? null
|
|
321
|
+
}
|
|
254
322
|
} catch {
|
|
255
323
|
return null
|
|
256
324
|
}
|
|
@@ -265,27 +333,74 @@ export class BackendClient {
|
|
|
265
333
|
|
|
266
334
|
/** POST /dev/orgs { handle } → { handle, uuid, is_primary }. Throws with the server's detail on 409/422. */
|
|
267
335
|
async createOrg(handle) {
|
|
268
|
-
return createOrgImpl({
|
|
336
|
+
return createOrgImpl({
|
|
337
|
+
apiBase: this.origin,
|
|
338
|
+
token: await this.token(),
|
|
339
|
+
handle
|
|
340
|
+
})
|
|
269
341
|
}
|
|
270
342
|
|
|
271
343
|
// ── Site sync (push / pull) ─────────────────────────────────────────────────────
|
|
272
344
|
|
|
345
|
+
/**
|
|
346
|
+
* POST /dev/site — CREATE an EMPTY site and return its site-content uuid.
|
|
347
|
+
*
|
|
348
|
+
* The uuid exists before any content or asset does, which is the whole point:
|
|
349
|
+
* uploaded bytes are metered against an owning entity and freed by deleting it,
|
|
350
|
+
* so an upload with no site to charge is billed and can never be reclaimed.
|
|
351
|
+
* Creating the site first is what makes a failed first publish leave a clearable
|
|
352
|
+
* empty site instead of unfreeable bytes.
|
|
353
|
+
*
|
|
354
|
+
* A thin re-projection of the same op the app's blank-site create uses, so the
|
|
355
|
+
* two lanes cannot drift. Adoption of `foundation` is best-effort — an
|
|
356
|
+
* unreleased ref leaves the resolved ref null and the site still exists — but
|
|
357
|
+
* the field itself is required and cannot be blank.
|
|
358
|
+
*
|
|
359
|
+
* NOT idempotent: two calls mint two sites (a name is not a unique key, by
|
|
360
|
+
* design). Callers must guard on `site.yml::$uuid` and write the result back
|
|
361
|
+
* immediately — see `ensureSiteExists`.
|
|
362
|
+
*
|
|
363
|
+
* @param {{ name: string, foundation: string, asOrg?: string|null }} opts
|
|
364
|
+
* @returns {Promise<Response>} `{ site_content_uuid }`
|
|
365
|
+
*/
|
|
366
|
+
async createSite({ name, foundation, asOrg } = {}) {
|
|
367
|
+
return this.request('/dev/site', {
|
|
368
|
+
method: 'POST',
|
|
369
|
+
// Both fields are REQUIRED: `info.name` is `required: true` on the model
|
|
370
|
+
// (and is a plain string — an identity label, never a `{lang: …}` map, since
|
|
371
|
+
// a localized value can vanish under locale projection and a name must always
|
|
372
|
+
// render), and a site must resolve to a foundation. Adoption of the ref is
|
|
373
|
+
// best-effort on the backend, but the field itself cannot be blank — so send
|
|
374
|
+
// whatever the site declares and let the backend judge it.
|
|
375
|
+
body: JSON.stringify({ name, foundation }),
|
|
376
|
+
query: { as_org: asOrg }
|
|
377
|
+
})
|
|
378
|
+
}
|
|
379
|
+
|
|
273
380
|
/** POST /dev/site/content — CREATE a site from its content lane (.uwx zip). */
|
|
274
381
|
async createSiteContent(buffer, { asOrg } = {}) {
|
|
275
|
-
return this.request('/dev/site/content', {
|
|
382
|
+
return this.request('/dev/site/content', {
|
|
383
|
+
method: 'POST',
|
|
384
|
+
body: buffer,
|
|
385
|
+
query: pushQuery(asOrg)
|
|
386
|
+
})
|
|
276
387
|
}
|
|
277
388
|
|
|
278
389
|
/** POST /dev/site/content/push/{uuid} — UPDATE the content lane by site uuid (.uwx zip). */
|
|
279
390
|
async updateSiteContent(uuid, buffer, { asOrg } = {}) {
|
|
280
391
|
return this.request(`/dev/site/content/push/${encodeURIComponent(uuid)}`, {
|
|
281
|
-
method: 'POST',
|
|
392
|
+
method: 'POST',
|
|
393
|
+
body: buffer,
|
|
394
|
+
query: pushQuery(asOrg)
|
|
282
395
|
})
|
|
283
396
|
}
|
|
284
397
|
|
|
285
398
|
/** POST /dev/site/folder/push/{uuid} — push the folder lane, keyed by the site uuid (.uwx zip). */
|
|
286
399
|
async pushFolder(uuid, buffer, { asOrg } = {}) {
|
|
287
400
|
return this.request(`/dev/site/folder/push/${encodeURIComponent(uuid)}`, {
|
|
288
|
-
method: 'POST',
|
|
401
|
+
method: 'POST',
|
|
402
|
+
body: buffer,
|
|
403
|
+
query: pushQuery(asOrg)
|
|
289
404
|
})
|
|
290
405
|
}
|
|
291
406
|
|
|
@@ -295,14 +410,14 @@ export class BackendClient {
|
|
|
295
410
|
*/
|
|
296
411
|
async pullSiteContent(uuid, { etag } = {}) {
|
|
297
412
|
return this.request(`/dev/site/content/pull/${encodeURIComponent(uuid)}`, {
|
|
298
|
-
headers: etag ? { 'If-None-Match': etag } : {}
|
|
413
|
+
headers: etag ? { 'If-None-Match': etag } : {}
|
|
299
414
|
})
|
|
300
415
|
}
|
|
301
416
|
|
|
302
417
|
/** GET /dev/site/folder/pull/{uuid} — the folder lane (folder + record documents). */
|
|
303
418
|
async pullFolder(uuid, { etag } = {}) {
|
|
304
419
|
return this.request(`/dev/site/folder/pull/${encodeURIComponent(uuid)}`, {
|
|
305
|
-
headers: etag ? { 'If-None-Match': etag } : {}
|
|
420
|
+
headers: etag ? { 'If-None-Match': etag } : {}
|
|
306
421
|
})
|
|
307
422
|
}
|
|
308
423
|
|
|
@@ -314,7 +429,7 @@ export class BackendClient {
|
|
|
314
429
|
* theme, languages, locales, optional dataFiles/searchFiles) plus an optional
|
|
315
430
|
* `site_uuid`. First deploy of a never-synced site omits it → the backend mints
|
|
316
431
|
* a uuid and returns it for write-back to deploy.yml; later deploys resend it so
|
|
317
|
-
*
|
|
432
|
+
* the site's published URL stays stable. Returns the raw Response so the caller
|
|
318
433
|
* messages its own errors and reads `{ site_uuid, url, locales }` on 200.
|
|
319
434
|
* @param {object} payload - the deploy payload (universal currency)
|
|
320
435
|
* @param {object} [opts]
|
|
@@ -323,7 +438,10 @@ export class BackendClient {
|
|
|
323
438
|
*/
|
|
324
439
|
async deploy(payload, { siteUuid } = {}) {
|
|
325
440
|
const body = siteUuid ? { ...payload, site_uuid: siteUuid } : payload
|
|
326
|
-
return this.request('/dev/deploy', {
|
|
441
|
+
return this.request('/dev/deploy', {
|
|
442
|
+
method: 'POST',
|
|
443
|
+
body: JSON.stringify(body)
|
|
444
|
+
})
|
|
327
445
|
}
|
|
328
446
|
|
|
329
447
|
/**
|
|
@@ -346,18 +464,20 @@ export class BackendClient {
|
|
|
346
464
|
return this.request(`/dev/site/publish/${encodeURIComponent(uuid)}`, {
|
|
347
465
|
method: 'POST',
|
|
348
466
|
query: { runtime: runtimeVersion },
|
|
349
|
-
...(languages ? { body: JSON.stringify({ languages }) } : {})
|
|
467
|
+
...(languages ? { body: JSON.stringify({ languages }) } : {})
|
|
350
468
|
})
|
|
351
469
|
}
|
|
352
470
|
|
|
353
471
|
/**
|
|
354
|
-
* POST /dev/site/unpublish/{uuid} — drop the published-folder gate so
|
|
472
|
+
* POST /dev/site/unpublish/{uuid} — drop the published-folder gate so the host
|
|
355
473
|
* stops serving the site's dynamic content. Returns the raw Response ({ was_published }).
|
|
356
474
|
* @param {string} uuid - the site-content uuid
|
|
357
475
|
* @returns {Promise<Response>}
|
|
358
476
|
*/
|
|
359
477
|
async unpublishSite(uuid) {
|
|
360
|
-
return this.request(`/dev/site/unpublish/${encodeURIComponent(uuid)}`, {
|
|
478
|
+
return this.request(`/dev/site/unpublish/${encodeURIComponent(uuid)}`, {
|
|
479
|
+
method: 'POST'
|
|
480
|
+
})
|
|
361
481
|
}
|
|
362
482
|
|
|
363
483
|
/**
|
|
@@ -374,7 +494,9 @@ export class BackendClient {
|
|
|
374
494
|
*/
|
|
375
495
|
async siteStatus(uuid) {
|
|
376
496
|
try {
|
|
377
|
-
const res = await this.request(
|
|
497
|
+
const res = await this.request(
|
|
498
|
+
`/dev/site/status/${encodeURIComponent(uuid)}`
|
|
499
|
+
)
|
|
378
500
|
return res.ok ? await res.json().catch(() => null) : null
|
|
379
501
|
} catch {
|
|
380
502
|
return null
|
|
@@ -394,7 +516,9 @@ export class BackendClient {
|
|
|
394
516
|
*/
|
|
395
517
|
async canGoLive(uuid) {
|
|
396
518
|
try {
|
|
397
|
-
const res = await this.request(
|
|
519
|
+
const res = await this.request(
|
|
520
|
+
`/dev/site/${encodeURIComponent(uuid)}/can-go-live`
|
|
521
|
+
)
|
|
398
522
|
return res.ok ? await res.json().catch(() => null) : null
|
|
399
523
|
} catch {
|
|
400
524
|
return null
|
|
@@ -409,24 +533,33 @@ export class BackendClient {
|
|
|
409
533
|
* @param {object} opts - { distDir, files?, onProgress? }
|
|
410
534
|
*/
|
|
411
535
|
async uploadSiteAssets(opts) {
|
|
412
|
-
return uploadSiteAssets({
|
|
536
|
+
return uploadSiteAssets({
|
|
537
|
+
apiBase: this.origin,
|
|
538
|
+
token: await this.token(),
|
|
539
|
+
...opts
|
|
540
|
+
})
|
|
413
541
|
}
|
|
414
542
|
|
|
415
543
|
/**
|
|
416
544
|
* Upload a built `@uniweb/runtime` to the runtime registry (plan → PUT-per-file),
|
|
417
|
-
* served at
|
|
545
|
+
* served by the backend at a location it reports. @std-gated on the backend. Thin
|
|
418
546
|
* pass-through to utils/runtime-upload.js with this client's origin + token.
|
|
419
547
|
* @param {object} opts - { version, distDir, files?, onProgress? }
|
|
420
548
|
*/
|
|
421
549
|
async uploadRuntime(opts) {
|
|
422
|
-
return uploadRuntime({
|
|
550
|
+
return uploadRuntime({
|
|
551
|
+
apiBase: this.origin,
|
|
552
|
+
token: await this.token(),
|
|
553
|
+
...opts
|
|
554
|
+
})
|
|
423
555
|
}
|
|
424
556
|
}
|
|
425
557
|
|
|
426
558
|
/** `@scope/name` → /dev/registry/data-schemas/{scope}/{name}; a bare name → …/{name}. */
|
|
427
559
|
export function dataSchemaPath(modelName) {
|
|
428
560
|
const m = /^@([^/]+)\/(.+)$/.exec(modelName)
|
|
429
|
-
if (m)
|
|
561
|
+
if (m)
|
|
562
|
+
return `/dev/registry/data-schemas/${encodeURIComponent(m[1])}/${encodeURIComponent(m[2])}`
|
|
430
563
|
return `/dev/registry/data-schemas/${encodeURIComponent(modelName)}`
|
|
431
564
|
}
|
|
432
565
|
|
|
@@ -20,16 +20,28 @@ import { buildAssetUrl } from '../utils/asset-upload.js'
|
|
|
20
20
|
* @param {{ onProgress?: (m: string) => void }} [opts]
|
|
21
21
|
* @returns {Promise<string>} the content-addressed serve URL (→ `info.data_bundle`)
|
|
22
22
|
*/
|
|
23
|
-
export async function uploadDataBundle(
|
|
23
|
+
export async function uploadDataBundle(
|
|
24
|
+
client,
|
|
25
|
+
ball,
|
|
26
|
+
{ siteUuid = null, onProgress } = {}
|
|
27
|
+
) {
|
|
24
28
|
const bytes = Buffer.from(JSON.stringify(ball))
|
|
25
29
|
const sha256 = createHash('sha256').update(bytes).digest('hex')
|
|
26
30
|
const localUrl = '/data-bundle/base.json' // bookkeeping key into assetsByLocalUrl
|
|
27
31
|
|
|
28
32
|
const result = await client.uploadSiteAssets({
|
|
29
33
|
files: [
|
|
30
|
-
{
|
|
34
|
+
{
|
|
35
|
+
path: 'data-bundle/base.json',
|
|
36
|
+
content_type: 'application/json',
|
|
37
|
+
size: bytes.length,
|
|
38
|
+
sha256,
|
|
39
|
+
localUrl,
|
|
40
|
+
bytes
|
|
41
|
+
}
|
|
31
42
|
],
|
|
32
|
-
|
|
43
|
+
siteUuid,
|
|
44
|
+
onProgress
|
|
33
45
|
})
|
|
34
46
|
if (result.failed?.length) {
|
|
35
47
|
const f = result.failed[0]
|