uniweb 1.0.0 → 1.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +201 -0
- package/README.md +532 -3
- package/package.json +60 -11
- package/partials/adding-pages.hbs +77 -0
- package/partials/agents.md +3000 -0
- package/partials/ai-assistance.hbs +28 -0
- package/partials/components-docs.hbs +13 -0
- package/partials/config-reference.hbs +66 -0
- package/partials/deployment.hbs +13 -0
- package/partials/learn-more.hbs +6 -0
- package/partials/quick-start.hbs +8 -0
- package/partials/search-docs.hbs +110 -0
- package/src/backend/asset-download.js +222 -0
- package/src/backend/client.js +986 -0
- package/src/backend/foundation-bring-along.js +692 -0
- package/src/backend/payment-handoff.js +362 -0
- package/src/backend/service-request.js +210 -0
- package/src/backend/site-media.js +255 -0
- package/src/backend/site-preview.js +77 -0
- package/src/backend/site-sync.js +2515 -0
- package/src/backend/workspace.js +234 -0
- package/src/commands/add.js +1900 -0
- package/src/commands/build.js +1214 -0
- package/src/commands/clone.js +466 -0
- package/src/commands/content.js +202 -0
- package/src/commands/deploy.js +537 -0
- package/src/commands/dev.js +202 -0
- package/src/commands/docs.js +530 -0
- package/src/commands/doctor.js +1982 -0
- package/src/commands/export.js +116 -0
- package/src/commands/families.js +82 -0
- package/src/commands/forget.js +197 -0
- package/src/commands/handoff.js +21 -0
- package/src/commands/i18n.js +1707 -0
- package/src/commands/inspect.js +375 -0
- package/src/commands/invite.js +24 -0
- package/src/commands/org.js +96 -0
- package/src/commands/publish.js +1087 -0
- package/src/commands/pull.js +1257 -0
- package/src/commands/push.js +663 -0
- package/src/commands/refresh.js +212 -0
- package/src/commands/register.js +1092 -0
- package/src/commands/rename.js +647 -0
- package/src/commands/site.js +358 -0
- package/src/commands/snapshot.js +461 -0
- package/src/commands/starter.js +206 -0
- package/src/commands/status.js +302 -0
- package/src/commands/sync.js +93 -0
- package/src/commands/template.js +107 -0
- package/src/commands/update.js +887 -0
- package/src/commands/validate.js +376 -0
- package/src/framework-index.json +248 -0
- package/src/index.js +2241 -0
- package/src/templates/fetchers/github.js +113 -0
- package/src/templates/fetchers/npm.js +85 -0
- package/src/templates/fetchers/release.js +92 -0
- package/src/templates/index.js +190 -0
- package/src/templates/processor.js +333 -0
- package/src/templates/resolver.js +232 -0
- package/src/templates/validator.js +155 -0
- package/src/utils/agents-stamp.js +110 -0
- package/src/utils/args.js +126 -0
- package/src/utils/asset-upload.js +270 -0
- package/src/utils/bytes.js +30 -0
- package/src/utils/code-upload.js +394 -0
- package/src/utils/config.js +417 -0
- package/src/utils/conformance.js +274 -0
- package/src/utils/dep-survey.js +176 -0
- package/src/utils/destination-prompt.js +146 -0
- package/src/utils/discover.js +75 -0
- package/src/utils/env.js +22 -0
- package/src/utils/exit.js +30 -0
- package/src/utils/fetch-retry.js +109 -0
- package/src/utils/flag-guard.js +246 -0
- package/src/utils/foundation-name.js +179 -0
- package/src/utils/git.js +342 -0
- package/src/utils/host-prompt.js +51 -0
- package/src/utils/install-integrity.js +210 -0
- package/src/utils/interactive.js +118 -0
- package/src/utils/json-file.js +72 -0
- package/src/utils/names.js +91 -0
- package/src/utils/placement.js +151 -0
- package/src/utils/pm.js +165 -0
- package/src/utils/preview.js +20 -0
- package/src/utils/pull-written.js +132 -0
- package/src/utils/records-guard.js +88 -0
- package/src/utils/registry-auth.js +994 -0
- package/src/utils/registry-orgs.js +290 -0
- package/src/utils/scaffold.js +483 -0
- package/src/utils/schemaless-report.js +30 -0
- package/src/utils/semver-precedence.js +80 -0
- package/src/utils/session-file.js +95 -0
- package/src/utils/site-content-refs.js +23 -0
- package/src/utils/site-copies.js +123 -0
- package/src/utils/site-data-upload.js +302 -0
- package/src/utils/site-identity.js +137 -0
- package/src/utils/snapshot-settings.js +112 -0
- package/src/utils/update-check.js +302 -0
- package/src/utils/uwx-read.js +157 -0
- package/src/utils/workspace.js +223 -0
- package/src/versions.js +467 -0
- package/starter/foundation/main.js +50 -0
- package/starter/foundation/sections/Section/index.jsx +95 -0
- package/starter/foundation/sections/Section/meta.js +44 -0
- package/starter/site/pages/about/1-about.md.hbs +13 -0
- package/starter/site/pages/about/page.yml +3 -0
- package/starter/site/pages/home/1-welcome.md.hbs +14 -0
- package/starter/site/pages/home/page.yml +3 -0
- package/templates/foundation/_gitignore +5 -0
- package/templates/foundation/components/.gitkeep +0 -0
- package/templates/foundation/main.js.hbs +33 -0
- package/templates/foundation/package.json.hbs +51 -0
- package/templates/foundation/sections/.gitkeep +0 -0
- package/templates/foundation/styles.css +6 -0
- package/templates/foundation/vite.config.js +3 -0
- package/templates/site/_gitignore +12 -0
- package/templates/site/entry.js +7 -0
- package/templates/site/index.html.hbs +13 -0
- package/templates/site/package.json.hbs +27 -0
- package/templates/site/public/favicon.svg +7 -0
- package/templates/site/site.yml.hbs +91 -0
- package/templates/site/theme.yml +99 -0
- package/templates/site/vite.config.js +7 -0
- package/templates/workspace/AGENTS.md.hbs +2 -0
- package/templates/workspace/README.md.hbs +87 -0
- package/templates/workspace/_gitignore +29 -0
- package/templates/workspace/_vscode/settings.json +6 -0
- package/templates/workspace/package.json.hbs +20 -0
- package/templates/workspace/pnpm-workspace.yaml.hbs +26 -0
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Upload a site's local media to the backend's content-addressed asset store via the
|
|
3
|
+
* SAME asset lane the data bundle rides, and return a `{ ref → serveUrl }` map for the
|
|
4
|
+
* deploy's second emit (`assetRewrite`) to swap the entity content refs for.
|
|
5
|
+
*
|
|
6
|
+
* Input is the site-root asset refs the producer surfaced in
|
|
7
|
+
* `emitSyncPackages().localAssets` (`/images/hero.png`); `resolveAssetPath` finds the
|
|
8
|
+
* file under the site's `public/` (or `assets/`). A ref whose file is missing is
|
|
9
|
+
* skipped (warned), never a broken serve URL. The serve URL is the backend's canonical
|
|
10
|
+
* `serve_url`, read verbatim — origin-relative forms included; nothing here composes one.
|
|
11
|
+
* Content-addressed like every asset: identical bytes → same id → a re-deploy of
|
|
12
|
+
* unchanged media is a cheap no-op PUT (the lane's `present` skip-list).
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { createHash } from 'node:crypto'
|
|
16
|
+
import { existsSync, readFileSync } from 'node:fs'
|
|
17
|
+
import { basename } from 'node:path'
|
|
18
|
+
import { resolveAssetPath } from '@uniweb/build/site'
|
|
19
|
+
import { servedFingerprint } from '@uniweb/build/uwx'
|
|
20
|
+
import { contentTypeFor } from '../utils/code-upload.js'
|
|
21
|
+
import { humanBytes } from '../utils/bytes.js'
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* @param {object} client - BackendClient (uploadSiteAssets). No `discover` — this
|
|
25
|
+
* lane stopped consulting the capability doc when `assetBase` was removed.
|
|
26
|
+
* @param {string} siteDir - the site root (site-root refs resolve under public/)
|
|
27
|
+
* @param {Array<string|{ ref: string, path: string, contentType?: string }>} refs - site-root local
|
|
28
|
+
* asset refs (`/images/x.png`), or files already located (a file record's)
|
|
29
|
+
* @param {{ siteUuid?: string|null, onProgress?: (m: string) => void, warn?: (m: string) => void }} [opts]
|
|
30
|
+
* `siteUuid` is the owner the uploaded bytes are charged to. Callers create the
|
|
31
|
+
* site before uploading precisely so this is set — an unowned upload is charged
|
|
32
|
+
* and cannot be freed, because freeing means deleting the owning entity.
|
|
33
|
+
* @returns {Promise<{ map: Record<string,string>, ids: Record<string,{id:string,ext:string}>, missing: string[], failed: Array<{path:string,status:number,detail?:string}> }>}
|
|
34
|
+
* `map` is ref → serve URL for refs that resolved AND uploaded. The two failure
|
|
35
|
+
* kinds are reported SEPARATELY because callers must treat them differently:
|
|
36
|
+
* `missing` is a ref with no file under the site — an authoring mistake, already
|
|
37
|
+
* broken before us, worth a warning; `failed` is a ref whose bytes we could not
|
|
38
|
+
* store — a transport or QUOTA refusal, and shipping content that still points at
|
|
39
|
+
* the local path would publish a broken image while only warning about it.
|
|
40
|
+
*/
|
|
41
|
+
export async function uploadSiteMedia(
|
|
42
|
+
client,
|
|
43
|
+
siteDir,
|
|
44
|
+
refs,
|
|
45
|
+
{ siteUuid = null, onProgress, warn } = {}
|
|
46
|
+
) {
|
|
47
|
+
if (!refs?.length) return { map: {}, ids: {}, missing: [], failed: [] }
|
|
48
|
+
|
|
49
|
+
const files = []
|
|
50
|
+
const missing = []
|
|
51
|
+
// ⭐ A ref is a site-root media path (`/images/x.png`), resolved under the site — or a file the
|
|
52
|
+
// caller already located, `{ ref, path, contentType }`: a file record's file and its preview, which
|
|
53
|
+
// live in the records directory and declare their own type (`@uniweb/file`, 2026-09-28).
|
|
54
|
+
const keys = []
|
|
55
|
+
for (const item of refs) {
|
|
56
|
+
const ref = typeof item === 'string' ? item : item?.ref
|
|
57
|
+
if (typeof ref !== 'string' || !ref) continue
|
|
58
|
+
keys.push(ref)
|
|
59
|
+
const resolved = typeof item === 'string' ? resolveAssetPath(ref, siteDir, siteDir).resolved : item.path
|
|
60
|
+
if (!resolved || !existsSync(resolved)) {
|
|
61
|
+
warn?.(`local-media: ${ref} not found under the site (skipped)`)
|
|
62
|
+
missing.push(ref)
|
|
63
|
+
continue
|
|
64
|
+
}
|
|
65
|
+
const bytes = readFileSync(resolved)
|
|
66
|
+
files.push({
|
|
67
|
+
path: ref.replace(/^\/+/, ''), // bookkeeping key into the plan (must be unique)
|
|
68
|
+
content_type: (typeof item === 'object' && item.contentType) || contentTypeFor(basename(resolved)),
|
|
69
|
+
size: bytes.length,
|
|
70
|
+
sha256: createHash('sha256').update(bytes).digest('hex'),
|
|
71
|
+
localUrl: ref, // the rewrite key — the original content ref
|
|
72
|
+
diskPath: resolved
|
|
73
|
+
})
|
|
74
|
+
}
|
|
75
|
+
if (!files.length) return { map: {}, ids: {}, missing, failed: [] }
|
|
76
|
+
|
|
77
|
+
const result = await client.uploadSiteAssets({ files, siteUuid, onProgress })
|
|
78
|
+
const failed = [...(result.failed || [])]
|
|
79
|
+
for (const f of failed)
|
|
80
|
+
warn?.(`local-media: upload failed for ${f.path} (HTTP ${f.status})`)
|
|
81
|
+
|
|
82
|
+
const map = {}
|
|
83
|
+
// The identity half, for this backend's asset map in `sync.json`. The plan
|
|
84
|
+
// returns an authoritative `id`+`ext` for every entry INCLUDING a `present: true`
|
|
85
|
+
// dedup skip, so a re-push of unchanged media still records identity without
|
|
86
|
+
// moving bytes — which is what makes the committed map cheap to keep accurate.
|
|
87
|
+
const ids = {}
|
|
88
|
+
for (const ref of keys) {
|
|
89
|
+
const entry = result.assetsByLocalUrl[ref]
|
|
90
|
+
if (!entry) continue
|
|
91
|
+
// `served` — a fingerprint of the address, never the address. It is how a pull
|
|
92
|
+
// recognizes this asset where the reference is a bare string with no room for an
|
|
93
|
+
// id beside it (`info.preview`, `seo.image`, a section param); the header of
|
|
94
|
+
// `@uniweb/build`'s asset-map.js says why a hash and not the URL.
|
|
95
|
+
if (entry.id) {
|
|
96
|
+
ids[ref] = {
|
|
97
|
+
id: entry.id,
|
|
98
|
+
ext: entry.ext || '',
|
|
99
|
+
...(entry.serveUrl ? { served: servedFingerprint(entry.serveUrl) } : {})
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
// The backend's canonical serve URL, READ — never composed. An entry without
|
|
103
|
+
// one is an asset we cannot address, and inventing a location for it is the
|
|
104
|
+
// exact failure this lane exists to avoid: a guessed host is SILENTLY wrong,
|
|
105
|
+
// where a missing one is visibly missing. So it joins `failed` and publish
|
|
106
|
+
// refuses, rather than shipping content pointing somewhere nobody claimed.
|
|
107
|
+
//
|
|
108
|
+
// `serve_url` is part of the asset-plan contract: on EVERY entry, both
|
|
109
|
+
// lanes, including entries reported as already `present`. Reconstruction
|
|
110
|
+
// from a discovered `assetBase` was removed once the backend covered that
|
|
111
|
+
// with a test of its own; it was the last hardcoded cross-deployment
|
|
112
|
+
// constant in the CLI. (Confirmed 2026-08-17.)
|
|
113
|
+
if (!entry.serveUrl) {
|
|
114
|
+
warn?.(`local-media: ${ref} — the asset plan returned no serve_url`)
|
|
115
|
+
failed.push({
|
|
116
|
+
path: ref,
|
|
117
|
+
status: 0,
|
|
118
|
+
detail: 'asset plan entry carried no serve_url'
|
|
119
|
+
})
|
|
120
|
+
continue
|
|
121
|
+
}
|
|
122
|
+
map[ref] = entry.serveUrl
|
|
123
|
+
}
|
|
124
|
+
return { map, ids, missing, failed }
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// ─── Asset-plan refusals ──────────────────────────────────────────────────────
|
|
128
|
+
//
|
|
129
|
+
// There is deliberately NO status-based refusal predicate here, and re-adding
|
|
130
|
+
// one would be a mistake. None of the statuses such a heuristic would match
|
|
131
|
+
// means what it looks like —
|
|
132
|
+
// 507 reserved for `storage_quota_exceeded`, but status alone cannot tell it
|
|
133
|
+
// from any other 507, and carries none of the numbers a user needs;
|
|
134
|
+
// 402 never from assets — it is the publish billing-consent gate, so matching it
|
|
135
|
+
// labels a billing refusal a storage problem and tells the user to free space
|
|
136
|
+
// they do not need;
|
|
137
|
+
// 413 comes from the upload PUT, which collects into `failed[]` and never throws,
|
|
138
|
+
// so it cannot reach a catch around the plan.
|
|
139
|
+
// The plan's caps (64 MiB/file, 512 MiB/plan, 1024 files) refuse with `400`, which
|
|
140
|
+
// a 402/413/507 predicate never matched at all.
|
|
141
|
+
//
|
|
142
|
+
// Branch on `reason` instead — never on status, never on `detail` (prose, subject to
|
|
143
|
+
// rewording). Same house style as the push-staleness `reason: "stale_base"` in
|
|
144
|
+
// site-sync.js.
|
|
145
|
+
|
|
146
|
+
// Append `label: <bytes>` when the value is a usable number. A refusal that omits an
|
|
147
|
+
// extra still produces a useful message — never print `undefined` at a user.
|
|
148
|
+
function pushBytes(lines, label, n) {
|
|
149
|
+
const h = humanBytes(n)
|
|
150
|
+
if (h) lines.push(` ${label}: ${h}`)
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Turn an asset-plan refusal into user-facing lines, or null when this is not a
|
|
155
|
+
* refusal we recognise. Null means "fall through to the generic error", so this
|
|
156
|
+
* degrades rather than swallowing.
|
|
157
|
+
*
|
|
158
|
+
* ⛔ This comment used to assert that the typed `reason` values were "agreed but
|
|
159
|
+
* NOT YET EMITTED" — an unverified claim about ANOTHER LANE's deployed state,
|
|
160
|
+
* sitting in our source where it read as fact and nobody would think to re-check
|
|
161
|
+
* it. That is the precise shape that cost the backend lane a metering hole (a
|
|
162
|
+
* comment claiming what the app sent, wrong the whole time). What we can say is
|
|
163
|
+
* ours: we branch on `reason` and fall through cleanly when there is none, so
|
|
164
|
+
* this function is correct whether or not any given backend emits one, and no
|
|
165
|
+
* sentence here needs to track what a deployment is running.
|
|
166
|
+
*
|
|
167
|
+
* Two wording rules are load-bearing and come from the ratified accounting model,
|
|
168
|
+
* not from taste:
|
|
169
|
+
*
|
|
170
|
+
* 1. **Never size a storage refusal from what was transferred.** `needed_bytes` is
|
|
171
|
+
* "assets not yet on this workspace's books", which can be non-zero with ZERO
|
|
172
|
+
* transfers — another workspace already uploaded the same bytes, so they come
|
|
173
|
+
* back `present: true`, never PUT, and are charged here anyway. A push can print
|
|
174
|
+
* no `↑` line at all and still be refused, so the message must talk about assets
|
|
175
|
+
* new to the workspace, never "the files being uploaded".
|
|
176
|
+
* 2. **Never advise removing an image to free space.** Freeing is
|
|
177
|
+
* entity-deletion-granular: editing an image out of live content frees nothing.
|
|
178
|
+
* Only deleting the entity (or the site) it was uploaded for returns quota.
|
|
179
|
+
* The predicate this replaced said "remove or shrink assets, then re-run" —
|
|
180
|
+
* advice that is now simply wrong.
|
|
181
|
+
*
|
|
182
|
+
* @param {Error & { problem?: object|null }} err
|
|
183
|
+
* @returns {{ headline: string, notes: string[] } | null}
|
|
184
|
+
*/
|
|
185
|
+
export function describeAssetRefusal(err) {
|
|
186
|
+
const reason = err?.problem?.reason
|
|
187
|
+
if (typeof reason !== 'string') return null
|
|
188
|
+
const p = err.problem
|
|
189
|
+
const notes = []
|
|
190
|
+
|
|
191
|
+
switch (reason) {
|
|
192
|
+
case 'storage_quota_exceeded': {
|
|
193
|
+
pushBytes(notes, 'Used', p.used_bytes)
|
|
194
|
+
pushBytes(notes, 'Limit', p.limit_bytes)
|
|
195
|
+
// Rule 1: this is what the workspace must take on, not what would transfer.
|
|
196
|
+
pushBytes(notes, 'This publish adds', p.needed_bytes)
|
|
197
|
+
notes.push(
|
|
198
|
+
'Assets already on its books cost nothing to re-present, so a change'
|
|
199
|
+
)
|
|
200
|
+
notes.push('that touches only content is never refused here.')
|
|
201
|
+
// Rule 2.
|
|
202
|
+
notes.push(
|
|
203
|
+
'Quota is returned by deleting the site or entity an asset was uploaded'
|
|
204
|
+
)
|
|
205
|
+
notes.push('for — removing an image from content does not free it.')
|
|
206
|
+
// Rule 3: the allowance belongs to whoever OWNS the site, not to whoever is
|
|
207
|
+
// pushing — an asset is charged to the workspace of the entity it is for, so
|
|
208
|
+
// a push to an org site spends the org's allowance whatever context the
|
|
209
|
+
// pusher acts in. "Your storage" would be wrong for every contributor who is
|
|
210
|
+
// not the owner, and wrong in the direction that sends them looking through
|
|
211
|
+
// their own files for space that was never theirs.
|
|
212
|
+
return {
|
|
213
|
+
headline:
|
|
214
|
+
"Storage quota reached — the site owner's workspace cannot take on more assets.",
|
|
215
|
+
notes
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
case 'asset_file_too_large': {
|
|
219
|
+
const path = typeof p.path === 'string' ? p.path : null
|
|
220
|
+
pushBytes(notes, 'Size', p.size_bytes)
|
|
221
|
+
pushBytes(notes, 'Per-file limit', p.limit_bytes)
|
|
222
|
+
return {
|
|
223
|
+
headline: `Asset too large${path ? `: ${path}` : ''}`,
|
|
224
|
+
notes: [...notes, 'Shrink or re-encode the file, then re-run.']
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
case 'asset_plan_too_large': {
|
|
228
|
+
pushBytes(notes, 'This publish', p.requested_bytes)
|
|
229
|
+
pushBytes(notes, 'Per-publish limit', p.limit_bytes)
|
|
230
|
+
return {
|
|
231
|
+
headline: 'Too many bytes in one publish.',
|
|
232
|
+
notes: [
|
|
233
|
+
...notes,
|
|
234
|
+
'Split the media across publishes, or shrink the largest files.'
|
|
235
|
+
]
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
case 'asset_plan_too_many_files': {
|
|
239
|
+
if (Number.isFinite(p.files)) notes.push(` Files: ${p.files}`)
|
|
240
|
+
if (Number.isFinite(p.limit))
|
|
241
|
+
notes.push(` Per-publish limit: ${p.limit}`)
|
|
242
|
+
return {
|
|
243
|
+
headline: 'Too many asset files in one publish.',
|
|
244
|
+
notes: [...notes, 'Split the media across publishes.']
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
default:
|
|
248
|
+
// An unrecognised reason is still more useful than nothing, but we do not
|
|
249
|
+
// invent advice for it — surface it and let the generic detail follow.
|
|
250
|
+
return {
|
|
251
|
+
headline: `Asset upload refused by the backend (${reason}).`,
|
|
252
|
+
notes: []
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What `uniweb dev` asks the backend before previewing a site whose foundation is a catalog ref —
|
|
3
|
+
* every clone.
|
|
4
|
+
*
|
|
5
|
+
* Such a site has no build of its foundation, and a build does not turn a ref into a URL: where a
|
|
6
|
+
* version is served is the backend's to say. So the dev server is handed the backend's answer, read
|
|
7
|
+
* for it here through the site — the route push and pull read the version's section types from
|
|
8
|
+
* (`client.readRegisteredFoundation`) — and passed on in `UNIWEB_PREVIEW` (`@uniweb/build/site`'s
|
|
9
|
+
* `preview.js` has the contract). The backend it names is also where the site's kept media URLs are
|
|
10
|
+
* fetched from.
|
|
11
|
+
*
|
|
12
|
+
* ⛔ Nothing here is kept. The answer lives as long as the dev server: `site.yml` keeps the ref, and
|
|
13
|
+
* no file a push or pull reads gets the URL — the round trip never sees it.
|
|
14
|
+
*
|
|
15
|
+
* ⚠️ It may not be known. Whether a backend tells the CLI where a version is served is the backend's
|
|
16
|
+
* to decide, and may change, so an answer without a location is an ordinary outcome: the preview
|
|
17
|
+
* says so and does not start. Nothing else reads it — push, pull and publish work without it.
|
|
18
|
+
*/
|
|
19
|
+
import { readBackendState, parseCatalogRef } from '@uniweb/build/uwx'
|
|
20
|
+
import { BackendClient } from './client.js'
|
|
21
|
+
import { resolveWorkspace } from './workspace.js'
|
|
22
|
+
import { readDeclaredFoundation } from '../utils/install-integrity.js'
|
|
23
|
+
import { syncedBackends } from '../utils/site-identity.js'
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* What the dev server of `siteDir` is handed, when its foundation is a catalog ref.
|
|
27
|
+
*
|
|
28
|
+
* @param {object} p
|
|
29
|
+
* @param {string} p.siteDir
|
|
30
|
+
* @param {string[]} [p.args]
|
|
31
|
+
* @param {BackendClient} [p.client] - injected in tests
|
|
32
|
+
* @returns {Promise<null
|
|
33
|
+
* | { preview: { backend: string, foundation: { ref: string, url: string, cssUrl: string|null } } }
|
|
34
|
+
* | { refused: string[] }>}
|
|
35
|
+
* null when there is nothing to ask — the foundation is not a catalog ref
|
|
36
|
+
*/
|
|
37
|
+
export async function readSitePreview({ siteDir, args = [], client = null }) {
|
|
38
|
+
const ref = readDeclaredFoundation(siteDir)
|
|
39
|
+
if (!parseCatalogRef(ref)) return null
|
|
40
|
+
|
|
41
|
+
client = client || new BackendClient({ args, command: 'Previewing' })
|
|
42
|
+
const uuid = readBackendState(siteDir, client.origin).site?.uuid
|
|
43
|
+
if (!uuid) {
|
|
44
|
+
const known = syncedBackends(siteDir)
|
|
45
|
+
return {
|
|
46
|
+
refused: [
|
|
47
|
+
`This site's foundation is ${ref}, and the backend the site is on says where that version is served.`,
|
|
48
|
+
known.length
|
|
49
|
+
? `The site is on ${known.join(', ')} — not on ${client.origin}, the backend you are logged in to. To preview it: uniweb login --server <url>`
|
|
50
|
+
: `The site is on no backend yet: \`uniweb push\` puts it on ${client.origin}.`
|
|
51
|
+
]
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const ws = await resolveWorkspace({ client, args })
|
|
56
|
+
if (ws.refused) return { refused: ['A preview reads the site in one workspace, and none is chosen.', ws.reason] }
|
|
57
|
+
client.setWorkspace(ws.workspace, { source: ws.source })
|
|
58
|
+
|
|
59
|
+
const reply = await client.readRegisteredFoundation(uuid, ref)
|
|
60
|
+
const url = typeof reply?.module_url === 'string' && reply.module_url ? reply.module_url : null
|
|
61
|
+
if (!url) {
|
|
62
|
+
return {
|
|
63
|
+
refused: [
|
|
64
|
+
reply
|
|
65
|
+
? `${client.origin} did not say where ${ref} is served, so this site cannot be previewed here.`
|
|
66
|
+
: `Could not read ${ref} through this site on ${client.origin}, so this site cannot be previewed here.`,
|
|
67
|
+
'Push, pull and publish do not need it.'
|
|
68
|
+
]
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return {
|
|
72
|
+
preview: {
|
|
73
|
+
backend: client.origin,
|
|
74
|
+
foundation: { ref, url, cssUrl: typeof reply.css_url === 'string' && reply.css_url ? reply.css_url : null }
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|