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,537 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Deploy Command — ship a site to its resolved target.
|
|
3
|
+
*
|
|
4
|
+
* With a destination already known (a `--host` flag, or a target in
|
|
5
|
+
* deploy.yml) `uniweb deploy` acts on it directly. With NO destination
|
|
6
|
+
* known it runs the WIZARD — "Where should this site go?" — rather than
|
|
7
|
+
* assuming one. Assuming a host was the old behavior and it was wrong;
|
|
8
|
+
* so was the picker that replaced it, which listed every registered
|
|
9
|
+
* adapter when only one could actually be deployed to. The wizard offers
|
|
10
|
+
* only destinations that can be acted on, and says what acting will do.
|
|
11
|
+
*
|
|
12
|
+
* Four outcomes, from the wizard or resolved directly:
|
|
13
|
+
* - THIRD-PARTY host, upload now: build `dist/` in bundle mode and hand
|
|
14
|
+
* it to the adapter's deploy hook (`github-pages`, `cloudflare-pages`,
|
|
15
|
+
* `netlify`, `vercel`, `s3-cloudfront`).
|
|
16
|
+
* - THIRD-PARTY host, set up CI: delegate to `uniweb add ci --host=…`,
|
|
17
|
+
* which scaffolds a workflow so every push deploys. For most free
|
|
18
|
+
* hosts this is the better answer, and the wizard says so.
|
|
19
|
+
* - UNIWEB hosting (`--host=uniweb`, or a `uniweb` target in deploy.yml,
|
|
20
|
+
* or "Uniweb Cloud" in the wizard): DELEGATE to `uniweb publish` — the
|
|
21
|
+
* smart path (sync + dynamic hosting, brings the foundation along). So
|
|
22
|
+
* deploy.yml stays one actionable "where this site deploys" record.
|
|
23
|
+
* - SOMEWHERE ELSE: delegate to `uniweb export` for a self-contained
|
|
24
|
+
* artifact you upload yourself.
|
|
25
|
+
*
|
|
26
|
+
* `uniweb publish` remains the canonical direct verb for Uniweb hosting.
|
|
27
|
+
*
|
|
28
|
+
* Host resolution:
|
|
29
|
+
* 1. --target <name> picks a target from deploy.yml (full config)
|
|
30
|
+
* 2. deploy.yml's `default:` target when no flag is given
|
|
31
|
+
* 3. with no deploy.yml at all, NO host is chosen → the wizard runs
|
|
32
|
+
* (interactive); non-interactive → an actionable error listing the
|
|
33
|
+
* real options
|
|
34
|
+
* 4. --host <name> is a one-off override (does NOT persist to deploy.yml)
|
|
35
|
+
* 5. --host with no value jumps straight to the wizard
|
|
36
|
+
*
|
|
37
|
+
* Usage:
|
|
38
|
+
* uniweb deploy Wizard: pick a destination
|
|
39
|
+
* uniweb deploy --host <name> Build bundle-mode dist/ + hand to the host adapter
|
|
40
|
+
* uniweb deploy --host=uniweb Delegate to `uniweb publish` (Uniweb hosting)
|
|
41
|
+
* uniweb deploy --target <name> Pick a target from deploy.yml
|
|
42
|
+
* uniweb deploy --dry-run Resolve everything; upload nothing
|
|
43
|
+
* uniweb deploy --no-save Do not record this deploy in deploy.yml
|
|
44
|
+
*
|
|
45
|
+
* Escape hatch: UNIWEB_SKIP_BUILD=1 reuses an existing dist/.
|
|
46
|
+
*/
|
|
47
|
+
|
|
48
|
+
import { existsSync, readFileSync } from 'node:fs'
|
|
49
|
+
import { resolve, join } from 'node:path'
|
|
50
|
+
import { execSync } from 'node:child_process'
|
|
51
|
+
|
|
52
|
+
import {
|
|
53
|
+
loadDeployYml,
|
|
54
|
+
resolveTarget,
|
|
55
|
+
recordLastDeploy
|
|
56
|
+
} from '@uniweb/build/site'
|
|
57
|
+
import { promptForDestination } from '../utils/destination-prompt.js'
|
|
58
|
+
import { readFlagValue } from '../utils/args.js'
|
|
59
|
+
import { DEFAULT_BACKEND_ORIGIN, getRegistryApiBaseUrl, loggedInOrigin } from '../utils/config.js'
|
|
60
|
+
import { normalizeOrigin } from '../utils/site-identity.js'
|
|
61
|
+
import { parseBoolEnv } from '../utils/env.js'
|
|
62
|
+
import { headProvenance } from '../utils/git.js'
|
|
63
|
+
import { warnIfContentDoesNotConform } from '../utils/conformance.js'
|
|
64
|
+
|
|
65
|
+
import {
|
|
66
|
+
findWorkspaceRoot,
|
|
67
|
+
findSites,
|
|
68
|
+
classifyPackage,
|
|
69
|
+
promptSelect
|
|
70
|
+
} from '../utils/workspace.js'
|
|
71
|
+
import { isNonInteractive, getCliPrefix } from '../utils/interactive.js'
|
|
72
|
+
|
|
73
|
+
const c = {
|
|
74
|
+
reset: '\x1b[0m',
|
|
75
|
+
bold: '\x1b[1m',
|
|
76
|
+
dim: '\x1b[2m',
|
|
77
|
+
cyan: '\x1b[36m',
|
|
78
|
+
green: '\x1b[32m',
|
|
79
|
+
yellow: '\x1b[33m',
|
|
80
|
+
red: '\x1b[31m'
|
|
81
|
+
}
|
|
82
|
+
const say = {
|
|
83
|
+
ok: (m) => console.log(`${c.green}✓${c.reset} ${m}`),
|
|
84
|
+
info: (m) => console.log(`${c.cyan}→${c.reset} ${m}`),
|
|
85
|
+
warn: (m) => console.log(`${c.yellow}⚠${c.reset} ${m}`),
|
|
86
|
+
err: (m) => console.error(`${c.red}✗${c.reset} ${m}`),
|
|
87
|
+
dim: (m) => console.log(` ${c.dim}${m}${c.reset}`)
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// ─── Main ───────────────────────────────────────────────────
|
|
91
|
+
|
|
92
|
+
export async function deploy(args = []) {
|
|
93
|
+
const dryRun = args.includes('--dry-run')
|
|
94
|
+
const siteDir = await resolveSiteDir(args)
|
|
95
|
+
|
|
96
|
+
// Host dispatch. Resolution order:
|
|
97
|
+
// 1. --target <name> picks a target from deploy.yml
|
|
98
|
+
// 2. deploy.yml's `default:` target when no flag is given
|
|
99
|
+
// 3. with no deploy.yml, NO host is chosen → the wizard decides
|
|
100
|
+
// 4. --host <name> is a one-off override (does not persist on success)
|
|
101
|
+
const targetFromFlag = readFlagValue(args, '--target')
|
|
102
|
+
const hostFromFlag = readFlagValue(args, '--host')
|
|
103
|
+
const noSave = args.includes('--no-save')
|
|
104
|
+
|
|
105
|
+
let deployYml
|
|
106
|
+
try {
|
|
107
|
+
deployYml = await loadDeployYml(siteDir)
|
|
108
|
+
} catch (err) {
|
|
109
|
+
say.err(err.message)
|
|
110
|
+
process.exit(1)
|
|
111
|
+
}
|
|
112
|
+
let resolved
|
|
113
|
+
try {
|
|
114
|
+
resolved = resolveTarget(deployYml, targetFromFlag || null)
|
|
115
|
+
} catch (err) {
|
|
116
|
+
say.err(err.message)
|
|
117
|
+
process.exit(1)
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Resolve a PLAN — {kind, host?, action?} — before doing anything. A
|
|
121
|
+
// destination is "known" when the user named it (--host) or deploy.yml
|
|
122
|
+
// records one. Otherwise the wizard asks; we never assume.
|
|
123
|
+
//
|
|
124
|
+
// resolveTarget returns host:'uniweb' with fromFile:false as its
|
|
125
|
+
// no-deploy.yml fallback. That is NOT a chosen destination — treat it
|
|
126
|
+
// as "unknown" so a bare `deploy` in a fresh project opens the wizard
|
|
127
|
+
// instead of silently heading for the paid product.
|
|
128
|
+
let plan
|
|
129
|
+
// `--host` with NO value (readFlagValue → null) is an explicit "ask me",
|
|
130
|
+
// and it must outrank deploy.yml: the user typed the flag precisely to
|
|
131
|
+
// choose something other than what's recorded. Omitting the flag
|
|
132
|
+
// entirely (→ undefined) is the different case where deploy.yml wins.
|
|
133
|
+
const wantsWizard = hostFromFlag === null
|
|
134
|
+
const knownHost = wantsWizard
|
|
135
|
+
? null
|
|
136
|
+
: hostFromFlag || (resolved.fromFile ? resolved.host : null)
|
|
137
|
+
|
|
138
|
+
if (knownHost === 'uniweb') {
|
|
139
|
+
plan = { kind: 'uniweb' }
|
|
140
|
+
} else if (knownHost) {
|
|
141
|
+
plan = { kind: 'adapter', host: knownHost, action: 'deploy' }
|
|
142
|
+
} else {
|
|
143
|
+
if (isNonInteractive(args)) {
|
|
144
|
+
if (wantsWizard) {
|
|
145
|
+
// They asked to pick, but there's no way to ask. Never silently
|
|
146
|
+
// fall back to deploy.yml — that would deploy somewhere the user
|
|
147
|
+
// was in the middle of overriding.
|
|
148
|
+
const { listAdapters } = await import('@uniweb/build/hosts')
|
|
149
|
+
say.err('`--host` requires a value when running non-interactively.')
|
|
150
|
+
say.dim(`Known adapters: ${listAdapters().join(', ')}.`)
|
|
151
|
+
say.dim('For Uniweb Cloud use `uniweb publish` (or --host=uniweb).')
|
|
152
|
+
process.exit(1)
|
|
153
|
+
}
|
|
154
|
+
say.err('`uniweb deploy` needs a destination.')
|
|
155
|
+
console.log('')
|
|
156
|
+
say.dim(
|
|
157
|
+
'`uniweb publish` Uniweb Cloud (sync + dynamic hosting; brings the foundation along)'
|
|
158
|
+
)
|
|
159
|
+
say.dim(
|
|
160
|
+
'`uniweb deploy --host=…` A third-party host — run `uniweb deploy --help` for the list'
|
|
161
|
+
)
|
|
162
|
+
say.dim('`uniweb add ci --host=…` Set up CI so every push deploys')
|
|
163
|
+
say.dim(
|
|
164
|
+
'`uniweb export` Self-contained dist/ artifact you upload anywhere'
|
|
165
|
+
)
|
|
166
|
+
process.exit(1)
|
|
167
|
+
}
|
|
168
|
+
try {
|
|
169
|
+
plan = await promptForDestination({
|
|
170
|
+
args,
|
|
171
|
+
preselect: resolved.fromFile ? resolved.host : null
|
|
172
|
+
})
|
|
173
|
+
} catch (err) {
|
|
174
|
+
say.err(err.message)
|
|
175
|
+
process.exit(1)
|
|
176
|
+
}
|
|
177
|
+
if (!plan) {
|
|
178
|
+
console.log('\nDeploy cancelled.')
|
|
179
|
+
process.exit(0)
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// A static host: advisory — warns and ships (utils/conformance.js). Uniweb Cloud is
|
|
184
|
+
// `publish`, below, which refuses content that does not conform: there the backend
|
|
185
|
+
// enforces the schemas, so the check is a gate, not advice (2026-09-24).
|
|
186
|
+
if (plan.kind !== 'uniweb') {
|
|
187
|
+
await warnIfContentDoesNotConform(siteDir, { args, warn: say.warn, dim: say.dim })
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// Uniweb Cloud is `publish`'s flow — delegate so deploy.yml stays one
|
|
191
|
+
// actionable record and there's a single implementation of go-live.
|
|
192
|
+
if (plan.kind === 'uniweb') {
|
|
193
|
+
// ⭐ `--host=uniweb` IS `publish` *[Diego, 2026-09-21]*: it goes to the backend you are
|
|
194
|
+
// logged in to — deploy follows the login like every other command. A deploy.yml
|
|
195
|
+
// uniweb target's `backend:` records where that target's publishes went; it routes
|
|
196
|
+
// nothing (publish files its record under the target naming its backend).
|
|
197
|
+
const goingTo = getRegistryApiBaseUrl()
|
|
198
|
+
const aimedBy = process.env.UNIWEB_SERVER
|
|
199
|
+
? 'UNIWEB_SERVER'
|
|
200
|
+
: loggedInOrigin()
|
|
201
|
+
? 'the backend you are logged in to'
|
|
202
|
+
: 'the default backend — you are not logged in'
|
|
203
|
+
const fromTarget =
|
|
204
|
+
resolved.fromFile &&
|
|
205
|
+
resolved.host === 'uniweb' &&
|
|
206
|
+
// not after the wizard (`--host` with no value): a pick there names no target
|
|
207
|
+
(hostFromFlag === undefined || hostFromFlag === 'uniweb')
|
|
208
|
+
const targetBackend = fromTarget
|
|
209
|
+
? normalizeOrigin(resolved.config?.backend || DEFAULT_BACKEND_ORIGIN)
|
|
210
|
+
: null
|
|
211
|
+
if (targetBackend && targetBackend !== goingTo) {
|
|
212
|
+
if (targetFromFlag) {
|
|
213
|
+
// ⛔ A refusal, not a route: the user TYPED this destination, and going live
|
|
214
|
+
// somewhere else would contradict it with nothing said. Switching is a login.
|
|
215
|
+
say.err(
|
|
216
|
+
`Target '${resolved.targetName}' is on ${targetBackend}, but this would publish to ${goingTo} (${aimedBy}).`
|
|
217
|
+
)
|
|
218
|
+
say.dim(`To publish there, log in to it first: uniweb login --server ${targetBackend}`)
|
|
219
|
+
process.exit(1)
|
|
220
|
+
}
|
|
221
|
+
say.dim(
|
|
222
|
+
`deploy.yml's default target '${resolved.targetName}' is on ${targetBackend}; publishing to ${goingTo} (${aimedBy}).`
|
|
223
|
+
)
|
|
224
|
+
}
|
|
225
|
+
say.info('Uniweb Cloud → running `uniweb publish`.')
|
|
226
|
+
console.log('')
|
|
227
|
+
// publish ignores deploy's --host/--target; --dry-run/--no-save pass straight
|
|
228
|
+
// through.
|
|
229
|
+
const { publish } = await import('./publish.js')
|
|
230
|
+
// publish runs the conformance gate itself; deploy checked nothing for this host.
|
|
231
|
+
const result = await publish(args)
|
|
232
|
+
process.exit(result?.exitCode ?? 0)
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
if (plan.kind === 'export') {
|
|
236
|
+
// `export` has no --dry-run of its own, so handle it here rather than
|
|
237
|
+
// passing a flag it would ignore — a dry run that actually ran a full
|
|
238
|
+
// build would be a lie.
|
|
239
|
+
if (dryRun) {
|
|
240
|
+
say.info(
|
|
241
|
+
'Dry run — would run `uniweb export` to build a self-contained dist/.'
|
|
242
|
+
)
|
|
243
|
+
return
|
|
244
|
+
}
|
|
245
|
+
say.info('Building a self-contained artifact → running `uniweb export`.')
|
|
246
|
+
console.log('')
|
|
247
|
+
const { exportSite } = await import('./export.js')
|
|
248
|
+
await exportSite([])
|
|
249
|
+
return
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// CI setup is `add ci`'s job. Shell out rather than re-implement: add
|
|
253
|
+
// owns site discovery, package-manager detection, the --force guard,
|
|
254
|
+
// and the deploy.yml target write.
|
|
255
|
+
if (plan.action === 'ci') {
|
|
256
|
+
await delegateToAddCi(siteDir, plan.host, dryRun)
|
|
257
|
+
return
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// Auto-save scope: 'off' from --no-save OR an ad-hoc --host override (we don't
|
|
261
|
+
// want a one-off experiment to rewrite the file). A host picked interactively
|
|
262
|
+
// for a bare `deploy` is NOT an override — we DO want to remember it.
|
|
263
|
+
const hostOverridden = !!hostFromFlag && hostFromFlag !== resolved.host
|
|
264
|
+
// An ad-hoc --host override describes no target, so there is nothing to file a
|
|
265
|
+
// deploy under — the switch goes off without anyone typing it.
|
|
266
|
+
const saveDeploys = !(noSave || hostOverridden) && resolved.saveDeploys !== false
|
|
267
|
+
|
|
268
|
+
await deployStaticHost(siteDir, plan.host, resolved, {
|
|
269
|
+
dryRun,
|
|
270
|
+
saveDeploys,
|
|
271
|
+
hostOverridden
|
|
272
|
+
})
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// ─── CI delegation ──────────────────────────────────────────
|
|
276
|
+
|
|
277
|
+
async function delegateToAddCi(siteDir, host, dryRun) {
|
|
278
|
+
const workspaceRoot = findWorkspaceRoot(siteDir) || siteDir
|
|
279
|
+
|
|
280
|
+
// Forward the site we already resolved. Without this, `add ci` re-runs
|
|
281
|
+
// its own discovery and asks "which site?" a second time in a
|
|
282
|
+
// multi-site workspace — right after the user answered that in the
|
|
283
|
+
// wizard. `add ci --site` matches on the package name.
|
|
284
|
+
let siteFlag = ''
|
|
285
|
+
try {
|
|
286
|
+
const pkg = JSON.parse(readFileSync(join(siteDir, 'package.json'), 'utf8'))
|
|
287
|
+
if (pkg.name) siteFlag = ` --site ${JSON.stringify(pkg.name)}`
|
|
288
|
+
} catch {
|
|
289
|
+
// No readable package.json — let `add ci` resolve the site itself.
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
const cmd = `add ci --host ${JSON.stringify(host)}${siteFlag}`
|
|
293
|
+
if (dryRun) {
|
|
294
|
+
say.info(`Dry run — would run \`uniweb ${cmd}\` in ${workspaceRoot}`)
|
|
295
|
+
return
|
|
296
|
+
}
|
|
297
|
+
say.info(`Setting up CI → running \`uniweb ${cmd}\`.`)
|
|
298
|
+
console.log('')
|
|
299
|
+
try {
|
|
300
|
+
execSync(`node ${JSON.stringify(process.argv[1])} ${cmd}`, {
|
|
301
|
+
cwd: workspaceRoot,
|
|
302
|
+
stdio: 'inherit'
|
|
303
|
+
})
|
|
304
|
+
} catch {
|
|
305
|
+
// add ci already printed the reason and set the exit code.
|
|
306
|
+
process.exit(1)
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// ─── Static-host deploy (S3+CloudFront, etc.) ─────────────────
|
|
311
|
+
//
|
|
312
|
+
// Picked when the resolved deploy.yml target (or --host override) names a
|
|
313
|
+
// static-host adapter registered in @uniweb/build/hosts. Always runs
|
|
314
|
+
// `uniweb build` (bundle mode + prerender) first, then hands dist/ to the
|
|
315
|
+
// adapter's deploy hook for upload + invalidation.
|
|
316
|
+
|
|
317
|
+
async function deployStaticHost(
|
|
318
|
+
siteDir,
|
|
319
|
+
hostName,
|
|
320
|
+
resolved,
|
|
321
|
+
{ dryRun, saveDeploys, hostOverridden }
|
|
322
|
+
) {
|
|
323
|
+
let getAdapter
|
|
324
|
+
try {
|
|
325
|
+
;({ getAdapter } = await import('@uniweb/build/hosts'))
|
|
326
|
+
} catch (err) {
|
|
327
|
+
say.err('Failed to load host adapter registry from @uniweb/build/hosts.')
|
|
328
|
+
say.dim(err.message)
|
|
329
|
+
process.exit(1)
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
let adapter
|
|
333
|
+
try {
|
|
334
|
+
adapter = getAdapter(hostName)
|
|
335
|
+
} catch (err) {
|
|
336
|
+
say.err(err.message)
|
|
337
|
+
say.dim(
|
|
338
|
+
'Set the host in deploy.yml or pass --host=<name>. See `uniweb deploy --help`.'
|
|
339
|
+
)
|
|
340
|
+
process.exit(1)
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
if (typeof adapter.deploy !== 'function') {
|
|
344
|
+
say.err(`Host adapter '${hostName}' does not implement a deploy step.`)
|
|
345
|
+
say.dim(
|
|
346
|
+
`Build with \`uniweb build --host=${hostName}\` and upload \`dist/\` manually,`
|
|
347
|
+
)
|
|
348
|
+
say.dim(
|
|
349
|
+
`or use a host whose adapter ships a deploy hook (e.g., s3-cloudfront).`
|
|
350
|
+
)
|
|
351
|
+
process.exit(1)
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
const deployConfig = resolved.config || {}
|
|
355
|
+
const distDir = join(siteDir, 'dist')
|
|
356
|
+
|
|
357
|
+
if (dryRun) {
|
|
358
|
+
say.info(
|
|
359
|
+
`Dry run — would deploy via host adapter: ${c.bold}${adapter.name}${c.reset}`
|
|
360
|
+
)
|
|
361
|
+
say.dim(`Site dir : ${siteDir}`)
|
|
362
|
+
say.dim(
|
|
363
|
+
`dist/ : ${existsSync(distDir) ? 'exists (would not rebuild)' : 'missing (would build)'}`
|
|
364
|
+
)
|
|
365
|
+
say.dim(`Target : ${resolved.targetName}`)
|
|
366
|
+
if (adapter.display?.pushWith)
|
|
367
|
+
say.dim(`Uploads by: ${adapter.display.pushWith}`)
|
|
368
|
+
// Echo the resolved target's config as-is. Each adapter reads
|
|
369
|
+
// different keys (bucket/distributionId for s3, projectName for
|
|
370
|
+
// Cloudflare, siteId for Netlify), so listing a fixed set would be
|
|
371
|
+
// wrong for four of the five.
|
|
372
|
+
const configKeys = Object.keys(deployConfig)
|
|
373
|
+
if (configKeys.length) {
|
|
374
|
+
say.dim('Config :')
|
|
375
|
+
for (const key of configKeys.sort()) {
|
|
376
|
+
say.dim(
|
|
377
|
+
` ${key}: ${typeof deployConfig[key] === 'object' ? JSON.stringify(deployConfig[key]) : deployConfig[key]}`
|
|
378
|
+
)
|
|
379
|
+
}
|
|
380
|
+
} else {
|
|
381
|
+
say.dim(
|
|
382
|
+
'Config : (none in deploy.yml — the adapter will say what it needs)'
|
|
383
|
+
)
|
|
384
|
+
}
|
|
385
|
+
return
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
// Always rebuild — the static-host flow expects fresh dist/ on every
|
|
389
|
+
// deploy. UNIWEB_SKIP_BUILD env var lets CI / dev loops reuse an
|
|
390
|
+
// existing build.
|
|
391
|
+
const skipBuild = parseBoolEnv('UNIWEB_SKIP_BUILD')
|
|
392
|
+
if (skipBuild) {
|
|
393
|
+
if (!existsSync(distDir)) {
|
|
394
|
+
say.err('UNIWEB_SKIP_BUILD is set but dist/ does not exist.')
|
|
395
|
+
process.exit(1)
|
|
396
|
+
}
|
|
397
|
+
say.info('UNIWEB_SKIP_BUILD set — reusing existing dist/.')
|
|
398
|
+
} else {
|
|
399
|
+
say.info(`Building site (host: ${adapter.name})…`)
|
|
400
|
+
console.log('')
|
|
401
|
+
try {
|
|
402
|
+
execSync(
|
|
403
|
+
`node ${JSON.stringify(process.argv[1])} build --bundle --host ${JSON.stringify(adapter.name)}`,
|
|
404
|
+
{ cwd: siteDir, stdio: 'inherit' }
|
|
405
|
+
)
|
|
406
|
+
} catch {
|
|
407
|
+
say.err('Build failed. See output above.')
|
|
408
|
+
process.exit(1)
|
|
409
|
+
}
|
|
410
|
+
if (!existsSync(distDir)) {
|
|
411
|
+
say.err('Build did not produce dist/.')
|
|
412
|
+
process.exit(1)
|
|
413
|
+
}
|
|
414
|
+
console.log('')
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
// Hand off to the adapter. DeployError is the structured shape from
|
|
418
|
+
// @uniweb/build/hosts/s3-cloudfront — translate to user-facing output.
|
|
419
|
+
let deployResult
|
|
420
|
+
try {
|
|
421
|
+
deployResult = await adapter.deploy({
|
|
422
|
+
distDir,
|
|
423
|
+
deployConfig,
|
|
424
|
+
env: process.env,
|
|
425
|
+
log: (m) => console.log(m)
|
|
426
|
+
})
|
|
427
|
+
} catch (err) {
|
|
428
|
+
if (err && err.name === 'DeployError') {
|
|
429
|
+
say.err(err.message)
|
|
430
|
+
if (err.hint) {
|
|
431
|
+
console.log('')
|
|
432
|
+
console.log(err.hint)
|
|
433
|
+
}
|
|
434
|
+
process.exit(1)
|
|
435
|
+
}
|
|
436
|
+
throw err
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
// Record a fresh lastDeploy.<target> entry. Skipped on --no-save and
|
|
440
|
+
// on ad-hoc --host overrides — see saveDeploys gating in deploy().
|
|
441
|
+
const gitStamp = {
|
|
442
|
+
at: new Date().toISOString(),
|
|
443
|
+
git: headProvenance(siteDir)
|
|
444
|
+
}
|
|
445
|
+
await persistLastDeploy(siteDir, {
|
|
446
|
+
targetName: resolved.targetName,
|
|
447
|
+
targetConfig: resolved.fromFile
|
|
448
|
+
? null
|
|
449
|
+
: { host: hostName, ...deployConfig },
|
|
450
|
+
saveDeploys,
|
|
451
|
+
lastDeploy: {
|
|
452
|
+
at: gitStamp.at,
|
|
453
|
+
host: hostName,
|
|
454
|
+
// Adapters that drive a host CLI get the public URL back from it
|
|
455
|
+
// (wrangler/netlify/vercel print it; github-pages derives it from
|
|
456
|
+
// the git remote). s3-cloudfront can't — the public URL lives in
|
|
457
|
+
// the user's CloudFront/DNS config, which we never see — so the
|
|
458
|
+
// field is simply omitted there rather than guessed.
|
|
459
|
+
...(deployResult?.url ? { url: deployResult.url } : {}),
|
|
460
|
+
// Same provenance the Uniweb path records: which commit this artifact came
|
|
461
|
+
// from, and whether the tree was clean when it was built.
|
|
462
|
+
...(gitStamp.git ? { git: gitStamp.git } : {})
|
|
463
|
+
}
|
|
464
|
+
})
|
|
465
|
+
if (hostOverridden && !dryRun) {
|
|
466
|
+
say.dim(
|
|
467
|
+
'--host override active — did not write to deploy.yml. Edit deploy.yml to make this permanent.'
|
|
468
|
+
)
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
// ─── deploy.yml lastDeploy persistence ──────────────────────────
|
|
473
|
+
|
|
474
|
+
async function persistLastDeploy(siteDir, opts) {
|
|
475
|
+
if (opts.saveDeploys === false) return
|
|
476
|
+
try {
|
|
477
|
+
const result = await recordLastDeploy(siteDir, opts)
|
|
478
|
+
if (result?.created) {
|
|
479
|
+
say.dim(`Wrote deploy.yml (target: ${opts.targetName})`)
|
|
480
|
+
}
|
|
481
|
+
} catch (err) {
|
|
482
|
+
// The deploy itself succeeded — never fail the whole command on a
|
|
483
|
+
// memo-write error. Surface it so the user can fix the file.
|
|
484
|
+
say.dim(`Could not update deploy.yml: ${err.message}`)
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
// ─── Resolve site dir ──────────────────────────────────────
|
|
489
|
+
|
|
490
|
+
// Exported so `uniweb export` / `uniweb publish` / `uniweb status` reuse the
|
|
491
|
+
// same site-discovery logic. `verb` is the command being run; it appears in
|
|
492
|
+
// the error messages so the user gets accurate guidance.
|
|
493
|
+
export async function resolveSiteDir(args, verb = 'deploy') {
|
|
494
|
+
const cwd = process.cwd()
|
|
495
|
+
const prefix = getCliPrefix()
|
|
496
|
+
|
|
497
|
+
const type = await classifyPackage(cwd)
|
|
498
|
+
if (type === 'site') return cwd
|
|
499
|
+
|
|
500
|
+
const workspaceRoot = findWorkspaceRoot(cwd)
|
|
501
|
+
if (workspaceRoot) {
|
|
502
|
+
const sites = await findSites(workspaceRoot)
|
|
503
|
+
if (sites.length === 1) return resolve(workspaceRoot, sites[0])
|
|
504
|
+
if (sites.length > 1) {
|
|
505
|
+
if (isNonInteractive(args)) {
|
|
506
|
+
say.err(`Multiple sites found. Specify which one to ${verb}.`)
|
|
507
|
+
console.log('')
|
|
508
|
+
for (const s of sites) {
|
|
509
|
+
console.log(` ${c.cyan}cd ${s} && ${prefix} ${verb}${c.reset}`)
|
|
510
|
+
}
|
|
511
|
+
process.exit(1)
|
|
512
|
+
}
|
|
513
|
+
const choice = await promptSelect('Which site?', sites)
|
|
514
|
+
if (!choice) {
|
|
515
|
+
console.log(
|
|
516
|
+
`\n${verb.charAt(0).toUpperCase() + verb.slice(1)} cancelled.`
|
|
517
|
+
)
|
|
518
|
+
process.exit(0)
|
|
519
|
+
}
|
|
520
|
+
return resolve(workspaceRoot, choice)
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
say.err('No site found in this workspace.')
|
|
525
|
+
if (verb === 'export') {
|
|
526
|
+
say.dim(
|
|
527
|
+
'`export` produces a self-contained dist/ artifact for third-party hosting.'
|
|
528
|
+
)
|
|
529
|
+
} else if (verb === 'deploy') {
|
|
530
|
+
say.dim(
|
|
531
|
+
'`deploy` ships a built site to a third-party host (use `uniweb publish` for Uniweb hosting).'
|
|
532
|
+
)
|
|
533
|
+
} else {
|
|
534
|
+
say.dim(`\`${verb}\` operates on a site.`)
|
|
535
|
+
}
|
|
536
|
+
process.exit(1)
|
|
537
|
+
}
|