uniweb 0.12.9 → 0.12.10
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 +13 -5
- package/package.json +5 -5
- package/partials/agents.md +1 -1
- package/src/commands/build.js +22 -7
- package/src/commands/deploy.js +129 -27
- package/src/commands/export.js +19 -6
- package/src/framework-index.json +6 -6
- package/src/utils/args.js +37 -0
- package/src/utils/host-prompt.js +50 -0
package/README.md
CHANGED
|
@@ -310,14 +310,22 @@ Start with local files deployed anywhere. The same foundation works across all t
|
|
|
310
310
|
|
|
311
311
|
## Deployment
|
|
312
312
|
|
|
313
|
-
A Uniweb project produces two artifacts — a **site** (content) and a **foundation** (code) — and they don't have to ship together.
|
|
313
|
+
A Uniweb project produces two artifacts — a **site** (content) and a **foundation** (code) — and they don't have to ship together. Two top-level modes:
|
|
314
314
|
|
|
315
|
-
- **
|
|
316
|
-
- **Linked mode** — the foundation
|
|
315
|
+
- **Standalone mode** — site and foundation built into one self-contained `dist/`, deployed to any static host.
|
|
316
|
+
- **Linked mode** — the foundation is a separate file the site loads at runtime, with two flavours:
|
|
317
|
+
- **Site-bound** — the foundation belongs to one site and rides with it (`foundation: ~self/<name>@<version>` in `site.yml`).
|
|
318
|
+
- **Cataloged** — the foundation is a catalog product, published once and licensed to consuming sites (`foundation: '@<org>/<name>@<version>'`).
|
|
317
319
|
|
|
318
|
-
|
|
320
|
+
`uniweb publish` ships a cataloged foundation; `uniweb deploy` ships a site (and, for site-bound, the foundation along with it). Most projects start standalone or site-bound and grow into cataloged when a foundation needs to serve more than one site.
|
|
319
321
|
|
|
320
|
-
|
|
322
|
+
Where can you deploy?
|
|
323
|
+
|
|
324
|
+
- **Free static hosts** — Vercel, Cloudflare Pages, Netlify, GitHub Pages — work great when you have a site to publish. Built-in adapters: `vercel`, `cloudflare-pages`, `netlify`, `github-pages`. Lifecycle is Git-driven: connect your repo, the host runs `uniweb build` on each push, serves `dist/`. The framework auto-detects the CI host and emits the right helper files.
|
|
325
|
+
- **AWS S3 + CloudFront** — `uniweb deploy --host=s3-cloudfront` builds, syncs, and invalidates in one command.
|
|
326
|
+
- **Uniweb hosting** — paid (starts at $14/month per site). Always serves linked sites with JIT prerender, edge SSR, locale-aware routing, foundation/runtime version propagation, and the multi-tenant CMS for non-technical content authors via the visual editor. The right choice when foundation developers or agencies build for clients who manage their own content. The catalog is private and access-segregated — foundations are commercial products licensed by site, not packages on a public registry.
|
|
327
|
+
|
|
328
|
+
→ **[Deploying](https://github.com/uniweb/docs/blob/main/development/deploying.md)** — the full menu: picking a deploy path (free vs paid), standalone vs linked, site-bound vs cataloged, the two-verb model, CI-detection, and per-host recipes.
|
|
321
329
|
|
|
322
330
|
---
|
|
323
331
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "uniweb",
|
|
3
|
-
"version": "0.12.
|
|
3
|
+
"version": "0.12.10",
|
|
4
4
|
"description": "Create structured Vite + React sites with content/code separation",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -41,12 +41,12 @@
|
|
|
41
41
|
"js-yaml": "^4.1.0",
|
|
42
42
|
"prompts": "^2.4.2",
|
|
43
43
|
"tar": "^7.0.0",
|
|
44
|
-
"@uniweb/
|
|
45
|
-
"@uniweb/
|
|
46
|
-
"@uniweb/core": "0.7.
|
|
44
|
+
"@uniweb/runtime": "0.8.13",
|
|
45
|
+
"@uniweb/kit": "0.9.11",
|
|
46
|
+
"@uniweb/core": "0.7.11"
|
|
47
47
|
},
|
|
48
48
|
"peerDependencies": {
|
|
49
|
-
"@uniweb/build": "0.14.
|
|
49
|
+
"@uniweb/build": "0.14.2",
|
|
50
50
|
"@uniweb/semantic-parser": "1.1.17",
|
|
51
51
|
"@uniweb/content-reader": "1.1.10"
|
|
52
52
|
},
|
package/partials/agents.md
CHANGED
|
@@ -172,7 +172,7 @@ The `uniweb` block in `package.json` carries platform-specific configuration tha
|
|
|
172
172
|
**Catalog vs site-bound foundations.** Two distribution intents share the same `dist/foundation.js` artifact:
|
|
173
173
|
|
|
174
174
|
- A **catalog foundation** is a deliberate product — named, versioned, listed in the catalog, consumable by other developers' sites. Use `uniweb publish @org/name` for these. The CLI requires an explicit name argument so you don't accidentally catalog a foundation that was meant to be site-bound.
|
|
175
|
-
- A **site-bound foundation** powers exactly one site. Don't run `uniweb publish` for it. Just run `uniweb deploy` from the site directory — the CLI auto-publishes your local foundation as part of the deploy,
|
|
175
|
+
- A **site-bound foundation** powers exactly one site. Don't run `uniweb publish` for it. Just run `uniweb deploy` from the site directory — the CLI auto-publishes your local foundation as part of the deploy, **uploaded with the site's other published assets** (per-site storage, never to the catalog). With no naming ceremony, no catalog visibility, and no developer-vs-site ownership confusion. To later promote the foundation to a catalog product, run `uniweb publish @org/name` from the foundation directory and update the site's `site.yml` to a versioned ref (`foundation: '@org/name@1.2.3'`).
|
|
176
176
|
|
|
177
177
|
**On the split between `package.json::name` and `uniweb.id`:** the workspace name is what pnpm uses for `file:` linking and what `site.yml::foundation` references. The published id is what the registry stores. Keeping them separate means renaming on the registry (e.g. `marketing` → `marketing-pro`) is a one-shot `uniweb publish --name marketing-pro` — it persists to `uniweb.id` without touching the workspace.
|
|
178
178
|
|
package/src/commands/build.js
CHANGED
|
@@ -35,8 +35,10 @@
|
|
|
35
35
|
* uniweb build --target site # Explicitly build as site
|
|
36
36
|
* uniweb build --prerender # Force pre-rendering
|
|
37
37
|
* uniweb build --no-prerender # Skip pre-rendering
|
|
38
|
-
* uniweb build --host <name> #
|
|
39
|
-
*
|
|
38
|
+
* uniweb build --host <name> # Pick the host adapter for this build's
|
|
39
|
+
* postBuild step (e.g. cloudflare-pages,
|
|
40
|
+
* s3-cloudfront, github-pages,
|
|
41
|
+
* generic-static). Default: cloudflare-pages.
|
|
40
42
|
*
|
|
41
43
|
* Internal flags (called by `uniweb deploy` / `uniweb export`):
|
|
42
44
|
* --link # Data-only pipeline (Uniweb-edge)
|
|
@@ -838,12 +840,25 @@ export async function build(args = []) {
|
|
|
838
840
|
foundationDir = resolve(args[foundationDirIndex + 1])
|
|
839
841
|
}
|
|
840
842
|
|
|
841
|
-
// --host
|
|
842
|
-
//
|
|
843
|
+
// --host names the host adapter for this build's prerender step.
|
|
844
|
+
// Default = 'cloudflare-pages' (resolved inside prerender.js, via the
|
|
845
|
+
// registry). Build does not read deploy.yml; that is the deploy
|
|
846
|
+
// orchestrator's job. See kb/framework/plans/static-host-deploy-adapters.md.
|
|
847
|
+
//
|
|
848
|
+
// `--host` with no value → interactive picker (errors in CI / non-TTY).
|
|
849
|
+
const { readFlagValue } = await import('../utils/args.js')
|
|
850
|
+
const hostFlag = readFlagValue(args, '--host')
|
|
843
851
|
let host = null
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
852
|
+
if (hostFlag === null) {
|
|
853
|
+
const { promptForHost } = await import('../utils/host-prompt.js')
|
|
854
|
+
try {
|
|
855
|
+
host = await promptForHost({ args })
|
|
856
|
+
} catch (err) {
|
|
857
|
+
error(err.message)
|
|
858
|
+
process.exit(1)
|
|
859
|
+
}
|
|
860
|
+
} else if (typeof hostFlag === 'string') {
|
|
861
|
+
host = hostFlag
|
|
847
862
|
}
|
|
848
863
|
|
|
849
864
|
// Auto-detect project type if not specified
|
package/src/commands/deploy.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Deploy Command
|
|
3
3
|
*
|
|
4
|
-
* Deploys a site. Host is determined by
|
|
5
|
-
* `--host <name>`
|
|
4
|
+
* Deploys a site. Host is determined by the resolved deploy.yml target
|
|
5
|
+
* (or `--target <name>` / `--host <name>` flags). The default is `uniweb`:
|
|
6
6
|
*
|
|
7
7
|
* - `uniweb` (default): Uniweb hosting — link-mode + edge JIT prerender.
|
|
8
8
|
* Foundation loaded by URL from the registry. Requires `uniweb login`
|
|
@@ -39,8 +39,10 @@
|
|
|
39
39
|
* uniweb deploy Normal deploy (browser may open on first deploy)
|
|
40
40
|
* uniweb deploy --dry-run Resolve everything but skip the Worker POST
|
|
41
41
|
* uniweb deploy --no-auto-publish Don't auto-publish workspace-local foundation
|
|
42
|
-
* uniweb deploy --
|
|
43
|
-
*
|
|
42
|
+
* uniweb deploy --target <name> Pick a target from deploy.yml (default: deploy.yml's `default:`)
|
|
43
|
+
* uniweb deploy --host <name> Override the resolved target's host adapter
|
|
44
|
+
* (does not write to deploy.yml on success)
|
|
45
|
+
* uniweb deploy --no-save Skip the auto-save of lastDeploy in deploy.yml
|
|
44
46
|
*
|
|
45
47
|
* Internal escape hatches (UNIWEB_* env vars — see framework/cli/docs/env-vars.md):
|
|
46
48
|
* UNIWEB_SKIP_BUILD=1 Reuse existing dist/ instead of rebuilding
|
|
@@ -60,6 +62,9 @@ import { execSync } from 'node:child_process'
|
|
|
60
62
|
import yaml from 'js-yaml'
|
|
61
63
|
|
|
62
64
|
import { detectFoundationType } from '@uniweb/build'
|
|
65
|
+
import { loadDeployYml, resolveTarget, recordLastDeploy } from '@uniweb/build/site'
|
|
66
|
+
import { promptForHost } from '../utils/host-prompt.js'
|
|
67
|
+
import { readFlagValue } from '../utils/args.js'
|
|
63
68
|
|
|
64
69
|
import { ensureAuth, readAuth, decodeJwtPayload } from '../utils/auth.js'
|
|
65
70
|
import { getBackendUrl, getRegistryUrl } from '../utils/config.js'
|
|
@@ -77,6 +82,7 @@ function splitRegistryRef(ref) {
|
|
|
77
82
|
const m = /^(@[^/]+\/[^@]+|~[^/]+\/[^@]+|[^@]+)@(.+)$/.exec(ref)
|
|
78
83
|
return m ? { name: m[1], version: m[2] } : null
|
|
79
84
|
}
|
|
85
|
+
|
|
80
86
|
import {
|
|
81
87
|
findWorkspaceRoot,
|
|
82
88
|
findSites,
|
|
@@ -417,18 +423,59 @@ export async function deploy(args = []) {
|
|
|
417
423
|
const siteYmlPath = join(siteDir, 'site.yml')
|
|
418
424
|
const siteYml = await readSiteYml(siteYmlPath)
|
|
419
425
|
|
|
420
|
-
// Host dispatch.
|
|
421
|
-
//
|
|
422
|
-
//
|
|
423
|
-
//
|
|
424
|
-
//
|
|
425
|
-
//
|
|
426
|
-
//
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
426
|
+
// Host dispatch.
|
|
427
|
+
//
|
|
428
|
+
// Resolution order:
|
|
429
|
+
// 1. --target <name> picks a target from deploy.yml (full config:
|
|
430
|
+
// host + adapter-specific fields)
|
|
431
|
+
// 2. deploy.yml's `default:` target is used when no flag is given
|
|
432
|
+
// 3. With no deploy.yml at all, the implicit default is host: 'uniweb'
|
|
433
|
+
// 4. --host <name> is a one-off override of the resolved target's host
|
|
434
|
+
// and does NOT persist on success (see saveDeployTarget below).
|
|
435
|
+
//
|
|
436
|
+
// The default flow (`uniweb`) requires a `foundation:` declaration;
|
|
437
|
+
// static-host deploys don't, so this branch comes BEFORE the foundation
|
|
438
|
+
// check. See kb/framework/plans/static-host-deploy-adapters.md.
|
|
439
|
+
const targetFromFlag = readFlagValue(args, '--target')
|
|
440
|
+
let hostFromFlag = readFlagValue(args, '--host')
|
|
441
|
+
const noSave = args.includes('--no-save')
|
|
442
|
+
|
|
443
|
+
let deployYml
|
|
444
|
+
try {
|
|
445
|
+
deployYml = await loadDeployYml(siteDir)
|
|
446
|
+
} catch (err) {
|
|
447
|
+
say.err(err.message)
|
|
448
|
+
process.exit(1)
|
|
449
|
+
}
|
|
450
|
+
let resolved
|
|
451
|
+
try {
|
|
452
|
+
resolved = resolveTarget(deployYml, targetFromFlag || null)
|
|
453
|
+
} catch (err) {
|
|
454
|
+
say.err(err.message)
|
|
455
|
+
process.exit(1)
|
|
456
|
+
}
|
|
457
|
+
// --host with no value → interactive picker. Pre-selects the resolved
|
|
458
|
+
// target's host so Enter does the obvious thing.
|
|
459
|
+
if (hostFromFlag === null) {
|
|
460
|
+
try {
|
|
461
|
+
hostFromFlag = await promptForHost({ args, preselect: resolved.host })
|
|
462
|
+
} catch (err) {
|
|
463
|
+
say.err(err.message)
|
|
464
|
+
process.exit(1)
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
const host = hostFromFlag || resolved.host
|
|
468
|
+
const hostOverridden = !!hostFromFlag && hostFromFlag !== resolved.host
|
|
469
|
+
// Auto-save scope: 'off' from --no-save OR an ad-hoc --host override
|
|
470
|
+
// (we don't want a one-off experiment to rewrite the file).
|
|
471
|
+
const autoSave = noSave || hostOverridden ? 'off' : resolved.autoSave
|
|
472
|
+
|
|
430
473
|
if (host !== 'uniweb') {
|
|
431
|
-
await deployStaticHost(siteDir,
|
|
474
|
+
await deployStaticHost(siteDir, host, resolved, {
|
|
475
|
+
dryRun,
|
|
476
|
+
autoSave,
|
|
477
|
+
hostOverridden,
|
|
478
|
+
})
|
|
432
479
|
return
|
|
433
480
|
}
|
|
434
481
|
|
|
@@ -944,19 +991,40 @@ export async function deploy(args = []) {
|
|
|
944
991
|
if (handleResolved) {
|
|
945
992
|
console.log(` ${c.cyan}https://${handleResolved}.uniweb.website/${c.reset}`)
|
|
946
993
|
}
|
|
994
|
+
|
|
995
|
+
// Record a fresh lastDeploy.<target> entry. Skipped on --no-save (and
|
|
996
|
+
// on --host overrides, but uniweb-host can't be reached via override
|
|
997
|
+
// since the override branches into deployStaticHost above).
|
|
998
|
+
await persistLastDeploy(siteDir, {
|
|
999
|
+
targetName: resolved.targetName,
|
|
1000
|
+
targetConfig: resolved.fromFile ? null : { host: 'uniweb' },
|
|
1001
|
+
autoSave,
|
|
1002
|
+
lastDeploy: {
|
|
1003
|
+
at: deployReceipt.deployedAt,
|
|
1004
|
+
host: 'uniweb',
|
|
1005
|
+
url: deployReceipt.url,
|
|
1006
|
+
siteId: siteIdResolved,
|
|
1007
|
+
handle: handleResolved,
|
|
1008
|
+
foundation: {
|
|
1009
|
+
shape: 'linked',
|
|
1010
|
+
ref: foundationRef,
|
|
1011
|
+
},
|
|
1012
|
+
runtime: runtimeVersion,
|
|
1013
|
+
},
|
|
1014
|
+
})
|
|
947
1015
|
}
|
|
948
1016
|
|
|
949
1017
|
// ─── Static-host deploy (S3+CloudFront, etc.) ─────────────────
|
|
950
1018
|
//
|
|
951
|
-
// Distinct from the uniweb-edge flow above. Picked when
|
|
952
|
-
//
|
|
953
|
-
// registered in @uniweb/build/hosts. Always runs `uniweb build`
|
|
954
|
-
//
|
|
955
|
-
//
|
|
1019
|
+
// Distinct from the uniweb-edge flow above. Picked when the resolved
|
|
1020
|
+
// deploy.yml target (or --host override) names a static-host adapter
|
|
1021
|
+
// registered in @uniweb/build/hosts. Always runs `uniweb build` (bundle
|
|
1022
|
+
// mode + prerender) first, then hands dist/ to the adapter's deploy hook
|
|
1023
|
+
// for upload + invalidation.
|
|
956
1024
|
//
|
|
957
1025
|
// See kb/framework/plans/static-host-deploy-adapters.md.
|
|
958
1026
|
|
|
959
|
-
async function deployStaticHost(siteDir,
|
|
1027
|
+
async function deployStaticHost(siteDir, hostName, resolved, { dryRun, autoSave, hostOverridden }) {
|
|
960
1028
|
let getAdapter
|
|
961
1029
|
try {
|
|
962
1030
|
({ getAdapter } = await import('@uniweb/build/hosts'))
|
|
@@ -971,7 +1039,7 @@ async function deployStaticHost(siteDir, siteYml, hostName, { dryRun }) {
|
|
|
971
1039
|
adapter = getAdapter(hostName)
|
|
972
1040
|
} catch (err) {
|
|
973
1041
|
say.err(err.message)
|
|
974
|
-
say.dim(
|
|
1042
|
+
say.dim('Set the host in deploy.yml or pass --host=<name>. See `uniweb deploy --help`.')
|
|
975
1043
|
process.exit(1)
|
|
976
1044
|
}
|
|
977
1045
|
|
|
@@ -982,17 +1050,18 @@ async function deployStaticHost(siteDir, siteYml, hostName, { dryRun }) {
|
|
|
982
1050
|
process.exit(1)
|
|
983
1051
|
}
|
|
984
1052
|
|
|
985
|
-
const deployConfig =
|
|
1053
|
+
const deployConfig = resolved.config || {}
|
|
986
1054
|
const distDir = join(siteDir, 'dist')
|
|
987
1055
|
|
|
988
1056
|
if (dryRun) {
|
|
989
1057
|
say.info(`Dry run — would deploy via host adapter: ${c.bold}${adapter.name}${c.reset}`)
|
|
990
1058
|
say.dim(`Site dir : ${siteDir}`)
|
|
991
1059
|
say.dim(`dist/ : ${existsSync(distDir) ? 'exists (would not rebuild)' : 'missing (would build)'}`)
|
|
992
|
-
say.dim(`
|
|
993
|
-
say.dim(`
|
|
994
|
-
say.dim(`
|
|
995
|
-
say.dim(`
|
|
1060
|
+
say.dim(`Target : ${resolved.targetName}`)
|
|
1061
|
+
say.dim(`bucket : ${deployConfig.bucket || '(unset)'}`)
|
|
1062
|
+
say.dim(`distributionId : ${deployConfig.distributionId || '(unset)'}`)
|
|
1063
|
+
say.dim(`region : ${deployConfig.region || '(unset)'}`)
|
|
1064
|
+
say.dim(`profile : ${deployConfig.profile || '(default AWS chain)'}`)
|
|
996
1065
|
return
|
|
997
1066
|
}
|
|
998
1067
|
|
|
@@ -1045,6 +1114,39 @@ async function deployStaticHost(siteDir, siteYml, hostName, { dryRun }) {
|
|
|
1045
1114
|
}
|
|
1046
1115
|
throw err
|
|
1047
1116
|
}
|
|
1117
|
+
|
|
1118
|
+
// Record a fresh lastDeploy.<target> entry. Skipped on --no-save and
|
|
1119
|
+
// on ad-hoc --host overrides — see autoSave gating in deploy().
|
|
1120
|
+
await persistLastDeploy(siteDir, {
|
|
1121
|
+
targetName: resolved.targetName,
|
|
1122
|
+
targetConfig: resolved.fromFile ? null : { host: hostName, ...deployConfig },
|
|
1123
|
+
autoSave,
|
|
1124
|
+
lastDeploy: {
|
|
1125
|
+
at: new Date().toISOString(),
|
|
1126
|
+
host: hostName,
|
|
1127
|
+
// Static hosts know their public URL only via the user's CDN config;
|
|
1128
|
+
// we don't have it on hand. Future: pull from a known field.
|
|
1129
|
+
},
|
|
1130
|
+
})
|
|
1131
|
+
if (hostOverridden && !dryRun) {
|
|
1132
|
+
say.dim('--host override active — did not write to deploy.yml. Edit deploy.yml to make this permanent.')
|
|
1133
|
+
}
|
|
1134
|
+
}
|
|
1135
|
+
|
|
1136
|
+
// ─── deploy.yml lastDeploy persistence ──────────────────────────
|
|
1137
|
+
|
|
1138
|
+
async function persistLastDeploy(siteDir, opts) {
|
|
1139
|
+
if (opts.autoSave === 'off') return
|
|
1140
|
+
try {
|
|
1141
|
+
const result = await recordLastDeploy(siteDir, opts)
|
|
1142
|
+
if (result?.created) {
|
|
1143
|
+
say.dim(`Wrote deploy.yml (target: ${opts.targetName})`)
|
|
1144
|
+
}
|
|
1145
|
+
} catch (err) {
|
|
1146
|
+
// The deploy itself succeeded — never fail the whole command on a
|
|
1147
|
+
// memo-write error. Surface it so the user can fix the file.
|
|
1148
|
+
say.dim(`Could not update deploy.yml: ${err.message}`)
|
|
1149
|
+
}
|
|
1048
1150
|
}
|
|
1049
1151
|
|
|
1050
1152
|
// ─── site.yml ──────────────────────────────────────────────
|
package/src/commands/export.js
CHANGED
|
@@ -17,9 +17,10 @@
|
|
|
17
17
|
* Usage:
|
|
18
18
|
* uniweb export Produce dist/ for static hosting
|
|
19
19
|
* uniweb export --no-prerender Skip per-page prerendered HTML
|
|
20
|
-
* uniweb export --host <name>
|
|
21
|
-
*
|
|
22
|
-
*
|
|
20
|
+
* uniweb export --host <name> Pick a host adapter for postBuild
|
|
21
|
+
* (e.g. cloudflare-pages, s3-cloudfront,
|
|
22
|
+
* github-pages, generic-static).
|
|
23
|
+
* Default: cloudflare-pages.
|
|
23
24
|
*/
|
|
24
25
|
|
|
25
26
|
import { execSync } from 'node:child_process'
|
|
@@ -53,9 +54,21 @@ export async function exportSite(args = []) {
|
|
|
53
54
|
const buildArgs = ['build', '--bundle']
|
|
54
55
|
if (noPrerender) buildArgs.push('--no-prerender')
|
|
55
56
|
|
|
56
|
-
const
|
|
57
|
-
|
|
58
|
-
|
|
57
|
+
const { readFlagValue } = await import('../utils/args.js')
|
|
58
|
+
const hostFlag = readFlagValue(args, '--host')
|
|
59
|
+
if (hostFlag === null) {
|
|
60
|
+
// --host with no value → prompt here so the build subprocess gets
|
|
61
|
+
// a concrete value (and doesn't re-prompt against its own argv).
|
|
62
|
+
const { promptForHost } = await import('../utils/host-prompt.js')
|
|
63
|
+
try {
|
|
64
|
+
const chosen = await promptForHost({ args })
|
|
65
|
+
buildArgs.push('--host', chosen)
|
|
66
|
+
} catch (err) {
|
|
67
|
+
say.err(err.message)
|
|
68
|
+
process.exit(1)
|
|
69
|
+
}
|
|
70
|
+
} else if (typeof hostFlag === 'string') {
|
|
71
|
+
buildArgs.push('--host', hostFlag)
|
|
59
72
|
}
|
|
60
73
|
|
|
61
74
|
say.info('Exporting site (vite build → dist/)…')
|
package/src/framework-index.json
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schemaVersion": 1,
|
|
3
|
-
"generatedAt": "2026-05-
|
|
3
|
+
"generatedAt": "2026-05-05T18:36:54.411Z",
|
|
4
4
|
"packages": {
|
|
5
5
|
"@uniweb/build": {
|
|
6
|
-
"version": "0.14.
|
|
6
|
+
"version": "0.14.2",
|
|
7
7
|
"path": "framework/build",
|
|
8
8
|
"deps": [
|
|
9
9
|
"@uniweb/content-reader",
|
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
"deps": []
|
|
25
25
|
},
|
|
26
26
|
"@uniweb/core": {
|
|
27
|
-
"version": "0.7.
|
|
27
|
+
"version": "0.7.11",
|
|
28
28
|
"path": "framework/core",
|
|
29
29
|
"deps": [
|
|
30
30
|
"@uniweb/semantic-parser",
|
|
@@ -42,7 +42,7 @@
|
|
|
42
42
|
"deps": []
|
|
43
43
|
},
|
|
44
44
|
"@uniweb/kit": {
|
|
45
|
-
"version": "0.9.
|
|
45
|
+
"version": "0.9.11",
|
|
46
46
|
"path": "framework/kit",
|
|
47
47
|
"deps": [
|
|
48
48
|
"@uniweb/core"
|
|
@@ -59,7 +59,7 @@
|
|
|
59
59
|
"deps": []
|
|
60
60
|
},
|
|
61
61
|
"@uniweb/runtime": {
|
|
62
|
-
"version": "0.8.
|
|
62
|
+
"version": "0.8.13",
|
|
63
63
|
"path": "framework/runtime",
|
|
64
64
|
"deps": [
|
|
65
65
|
"@uniweb/core",
|
|
@@ -92,7 +92,7 @@
|
|
|
92
92
|
"deps": []
|
|
93
93
|
},
|
|
94
94
|
"@uniweb/unipress": {
|
|
95
|
-
"version": "0.4.
|
|
95
|
+
"version": "0.4.6",
|
|
96
96
|
"path": "framework/unipress",
|
|
97
97
|
"deps": [
|
|
98
98
|
"@uniweb/build",
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* argv parsing helpers shared across CLI commands.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Read `--flag value` from argv. Accepts both `--flag value` and
|
|
7
|
+
* `--flag=value`.
|
|
8
|
+
*
|
|
9
|
+
* Returns:
|
|
10
|
+
* - undefined when the flag is absent
|
|
11
|
+
* - null when the flag is present without a value (last arg, next is
|
|
12
|
+
* another flag, or `--flag=` empty form)
|
|
13
|
+
* - string when the flag carries a value
|
|
14
|
+
*
|
|
15
|
+
* The three-state return lets callers distinguish "not given" (e.g.,
|
|
16
|
+
* fall back to a default) from "given but empty" (e.g., trigger an
|
|
17
|
+
* interactive prompt).
|
|
18
|
+
*
|
|
19
|
+
* @param {string[]} args
|
|
20
|
+
* @param {string} name — Including the leading dashes, e.g. '--host'.
|
|
21
|
+
* @returns {string | null | undefined}
|
|
22
|
+
*/
|
|
23
|
+
export function readFlagValue(args, name) {
|
|
24
|
+
const eqPrefix = name + '='
|
|
25
|
+
for (let i = 0; i < args.length; i++) {
|
|
26
|
+
if (args[i] === name) {
|
|
27
|
+
const next = args[i + 1]
|
|
28
|
+
if (next === undefined || next.startsWith('--')) return null
|
|
29
|
+
return next
|
|
30
|
+
}
|
|
31
|
+
if (args[i].startsWith(eqPrefix)) {
|
|
32
|
+
const v = args[i].slice(eqPrefix.length)
|
|
33
|
+
return v === '' ? null : v
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return undefined
|
|
37
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Interactive host adapter selection
|
|
3
|
+
*
|
|
4
|
+
* Prompts the user to pick a host adapter from the registry. Used when
|
|
5
|
+
* `--host` is passed without a value to `uniweb deploy / build / export`.
|
|
6
|
+
*
|
|
7
|
+
* Non-interactive contexts (CI, piped input, --non-interactive) get a
|
|
8
|
+
* structured error instead of a prompt — never silently default to
|
|
9
|
+
* something the user didn't pick.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { promptSelect } from './workspace.js'
|
|
13
|
+
import { isNonInteractive } from './interactive.js'
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Pick a host adapter, optionally with a pre-selection.
|
|
17
|
+
*
|
|
18
|
+
* @param {object} opts
|
|
19
|
+
* @param {string[]} opts.args — Argv, used only to gate non-interactive mode.
|
|
20
|
+
* @param {string|null} [opts.preselect] — Suggested adapter name; the prompt
|
|
21
|
+
* highlights this so Enter accepts it without arrow-key navigation.
|
|
22
|
+
* @returns {Promise<string>} The chosen adapter name.
|
|
23
|
+
* @throws {Error} When non-interactive (with the registry list in the message),
|
|
24
|
+
* or when the user aborts the prompt.
|
|
25
|
+
*/
|
|
26
|
+
export async function promptForHost({ args, preselect = null } = {}) {
|
|
27
|
+
// Lazy-load so this module doesn't pull @uniweb/build at import time
|
|
28
|
+
// for callers that never reach the prompt path.
|
|
29
|
+
const { listAdapters } = await import('@uniweb/build/hosts')
|
|
30
|
+
const adapters = listAdapters()
|
|
31
|
+
|
|
32
|
+
if (isNonInteractive(args || [])) {
|
|
33
|
+
const list = adapters.join(', ')
|
|
34
|
+
throw new Error(
|
|
35
|
+
`--host requires a value when running non-interactively. Known adapters: ${list}.`
|
|
36
|
+
)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// promptSelect doesn't expose initial-index, so move the preselect to
|
|
40
|
+
// the top of the list — the menu still highlights index 0 by default.
|
|
41
|
+
const ordered = preselect && adapters.includes(preselect)
|
|
42
|
+
? [preselect, ...adapters.filter(a => a !== preselect)]
|
|
43
|
+
: adapters
|
|
44
|
+
|
|
45
|
+
const choice = await promptSelect('Pick a host adapter:', ordered)
|
|
46
|
+
if (!choice) {
|
|
47
|
+
throw new Error('Host selection cancelled.')
|
|
48
|
+
}
|
|
49
|
+
return choice
|
|
50
|
+
}
|