uniweb 0.12.9 → 0.12.11
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 +22 -5
- package/src/commands/add.js +2 -2
- package/src/commands/build.js +22 -7
- package/src/commands/deploy.js +150 -30
- package/src/commands/dev.js +111 -0
- package/src/commands/export.js +19 -6
- package/src/commands/handoff.js +1 -1
- package/src/commands/invite.js +1 -1
- package/src/commands/publish.js +1 -1
- package/src/commands/template.js +1 -1
- package/src/framework-index.json +6 -6
- package/src/index.js +362 -12
- package/src/utils/args.js +37 -0
- package/src/utils/auth.js +29 -1
- package/src/utils/config.js +15 -6
- package/src/utils/host-prompt.js +50 -0
- package/src/utils/update-check.js +34 -3
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.11",
|
|
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/
|
|
44
|
+
"@uniweb/runtime": "0.8.13",
|
|
45
|
+
"@uniweb/core": "0.7.11",
|
|
46
|
+
"@uniweb/kit": "0.9.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
|
@@ -136,12 +136,29 @@ Creates `sections/Hero/index.jsx` and `meta.js` with a minimal CCA-proper starte
|
|
|
136
136
|
## Commands
|
|
137
137
|
|
|
138
138
|
```bash
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
pnpm
|
|
142
|
-
pnpm
|
|
139
|
+
# Local development
|
|
140
|
+
uniweb dev # Start dev server (picks the site for you)
|
|
141
|
+
pnpm install # Install dependencies
|
|
142
|
+
pnpm build # Build for production
|
|
143
|
+
pnpm preview # Preview production build (SSG + SPA)
|
|
144
|
+
|
|
145
|
+
# Ship the site (uniweb verbs)
|
|
146
|
+
uniweb deploy # Deploy to Uniweb hosting (default; needs `uniweb login` first)
|
|
147
|
+
uniweb deploy --host=<adapter> # Deploy to a static host: cloudflare-pages, netlify,
|
|
148
|
+
# vercel, github-pages, s3-cloudfront, generic-static
|
|
149
|
+
uniweb deploy --dry-run # Resolve foundation/runtime + print summary; no writes
|
|
150
|
+
uniweb export # Build dist/ for any static host (no Uniweb account)
|
|
151
|
+
uniweb publish # Publish a foundation as a catalog product (deliberate;
|
|
152
|
+
# for site-bound foundations use `uniweb deploy` instead)
|
|
153
|
+
uniweb doctor # Diagnose project configuration issues (--fix to auto-repair)
|
|
154
|
+
|
|
155
|
+
# Help
|
|
156
|
+
uniweb --help # Top-level help
|
|
157
|
+
uniweb <command> --help # Per-command help (no side effects)
|
|
143
158
|
```
|
|
144
159
|
|
|
160
|
+
`uniweb deploy` auto-publishes a workspace-local foundation as part of the deploy under a site-scoped slot — no separate `uniweb publish` step needed for site-bound foundations.
|
|
161
|
+
|
|
145
162
|
---
|
|
146
163
|
|
|
147
164
|
## `package.json` `uniweb` configuration
|
|
@@ -172,7 +189,7 @@ The `uniweb` block in `package.json` carries platform-specific configuration tha
|
|
|
172
189
|
**Catalog vs site-bound foundations.** Two distribution intents share the same `dist/foundation.js` artifact:
|
|
173
190
|
|
|
174
191
|
- 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,
|
|
192
|
+
- 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
193
|
|
|
177
194
|
**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
195
|
|
package/src/commands/add.js
CHANGED
|
@@ -389,7 +389,7 @@ async function addSite(rootDir, projectName, opts, pm = 'pnpm') {
|
|
|
389
389
|
success(`Created site ${colors.bright}${siteName}${colors.reset} at ${relativePath}/`)
|
|
390
390
|
}
|
|
391
391
|
log('')
|
|
392
|
-
log(`Next: ${colors.cyan}${installCmd(pm)} && ${
|
|
392
|
+
log(`Next: ${colors.cyan}${installCmd(pm)} && uniweb dev ${siteName}${colors.reset}`)
|
|
393
393
|
if (!opts.from) {
|
|
394
394
|
log('')
|
|
395
395
|
log(`${colors.dim}To add your first page, create ${relativePath}/pages/home/page.yml and a .md file.${colors.reset}`)
|
|
@@ -631,7 +631,7 @@ async function addProject(rootDir, projectName, opts, pm = 'pnpm') {
|
|
|
631
631
|
log(` ${colors.dim}Foundation: ${name}/src/ (${foundationPkgName})${colors.reset}`)
|
|
632
632
|
log(` ${colors.dim}Site: ${name}/site/ (${sitePkgName})${colors.reset}`)
|
|
633
633
|
log('')
|
|
634
|
-
log(`Next: ${colors.cyan}${installCmd(pm)} && ${
|
|
634
|
+
log(`Next: ${colors.cyan}${installCmd(pm)} && uniweb dev ${sitePkgName}${colors.reset}`)
|
|
635
635
|
}
|
|
636
636
|
|
|
637
637
|
/**
|
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,
|
|
@@ -395,6 +401,21 @@ export async function deploy(args = []) {
|
|
|
395
401
|
// of the current source's git sha. This flag opts out.
|
|
396
402
|
const autoPublishFoundation = !args.includes('--no-auto-publish')
|
|
397
403
|
|
|
404
|
+
// --local: redirect platform URLs to the unicloud mock (localhost:4001)
|
|
405
|
+
// for internal end-to-end testing. Documented in the workspace root
|
|
406
|
+
// CLAUDE.md ("The --local Flag" section). NOT a public user-facing
|
|
407
|
+
// feature — a real user has no unicloud server running. The flag is
|
|
408
|
+
// intentionally absent from the global help to avoid leaking it into
|
|
409
|
+
// user docs; per-command help (uniweb deploy --help) lists it under
|
|
410
|
+
// an "Internal" caveat for the eval / test team.
|
|
411
|
+
//
|
|
412
|
+
// The override unconditionally pins both backend and worker to
|
|
413
|
+
// http://localhost:4001 (unicloud's default port) regardless of any
|
|
414
|
+
// env vars set in the calling shell. Auth is NOT skipped — the runbook
|
|
415
|
+
// expects mock-login.js to seed ~/.uniweb/auth.json with a JWT
|
|
416
|
+
// unicloud's verifyToken accepts.
|
|
417
|
+
const isLocal = args.includes('--local')
|
|
418
|
+
|
|
398
419
|
// Internal escape hatches — see framework/cli/docs/env-vars.md. These
|
|
399
420
|
// are not user-facing flags; they exist for the platform test team,
|
|
400
421
|
// CI scripts, and dev-loop unblockers. The bare `deploy` command should
|
|
@@ -409,26 +430,70 @@ export async function deploy(args = []) {
|
|
|
409
430
|
const treatDirtyAsStale = !parseBoolEnv('UNIWEB_ALLOW_DIRTY_FOUNDATION')
|
|
410
431
|
|
|
411
432
|
const siteDir = await resolveSiteDir(args)
|
|
412
|
-
const backendUrl = getBackendUrl()
|
|
413
|
-
const workerUrl = getRegistryUrl()
|
|
433
|
+
const backendUrl = isLocal ? 'http://localhost:4001' : getBackendUrl()
|
|
434
|
+
const workerUrl = isLocal ? 'http://localhost:4001' : getRegistryUrl()
|
|
435
|
+
if (isLocal) {
|
|
436
|
+
console.log(` \x1b[2m→ Local mock mode (unicloud at ${backendUrl}; see workspace root CLAUDE.md)\x1b[0m`)
|
|
437
|
+
}
|
|
414
438
|
|
|
415
439
|
// Read site.yml — declares the foundation (required) and optionally the
|
|
416
440
|
// site.id / site.handle from prior deploys.
|
|
417
441
|
const siteYmlPath = join(siteDir, 'site.yml')
|
|
418
442
|
const siteYml = await readSiteYml(siteYmlPath)
|
|
419
443
|
|
|
420
|
-
// Host dispatch.
|
|
421
|
-
//
|
|
422
|
-
//
|
|
423
|
-
//
|
|
424
|
-
//
|
|
425
|
-
//
|
|
426
|
-
//
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
444
|
+
// Host dispatch.
|
|
445
|
+
//
|
|
446
|
+
// Resolution order:
|
|
447
|
+
// 1. --target <name> picks a target from deploy.yml (full config:
|
|
448
|
+
// host + adapter-specific fields)
|
|
449
|
+
// 2. deploy.yml's `default:` target is used when no flag is given
|
|
450
|
+
// 3. With no deploy.yml at all, the implicit default is host: 'uniweb'
|
|
451
|
+
// 4. --host <name> is a one-off override of the resolved target's host
|
|
452
|
+
// and does NOT persist on success (see saveDeployTarget below).
|
|
453
|
+
//
|
|
454
|
+
// The default flow (`uniweb`) requires a `foundation:` declaration;
|
|
455
|
+
// static-host deploys don't, so this branch comes BEFORE the foundation
|
|
456
|
+
// check. See kb/framework/plans/static-host-deploy-adapters.md.
|
|
457
|
+
const targetFromFlag = readFlagValue(args, '--target')
|
|
458
|
+
let hostFromFlag = readFlagValue(args, '--host')
|
|
459
|
+
const noSave = args.includes('--no-save')
|
|
460
|
+
|
|
461
|
+
let deployYml
|
|
462
|
+
try {
|
|
463
|
+
deployYml = await loadDeployYml(siteDir)
|
|
464
|
+
} catch (err) {
|
|
465
|
+
say.err(err.message)
|
|
466
|
+
process.exit(1)
|
|
467
|
+
}
|
|
468
|
+
let resolved
|
|
469
|
+
try {
|
|
470
|
+
resolved = resolveTarget(deployYml, targetFromFlag || null)
|
|
471
|
+
} catch (err) {
|
|
472
|
+
say.err(err.message)
|
|
473
|
+
process.exit(1)
|
|
474
|
+
}
|
|
475
|
+
// --host with no value → interactive picker. Pre-selects the resolved
|
|
476
|
+
// target's host so Enter does the obvious thing.
|
|
477
|
+
if (hostFromFlag === null) {
|
|
478
|
+
try {
|
|
479
|
+
hostFromFlag = await promptForHost({ args, preselect: resolved.host })
|
|
480
|
+
} catch (err) {
|
|
481
|
+
say.err(err.message)
|
|
482
|
+
process.exit(1)
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
const host = hostFromFlag || resolved.host
|
|
486
|
+
const hostOverridden = !!hostFromFlag && hostFromFlag !== resolved.host
|
|
487
|
+
// Auto-save scope: 'off' from --no-save OR an ad-hoc --host override
|
|
488
|
+
// (we don't want a one-off experiment to rewrite the file).
|
|
489
|
+
const autoSave = noSave || hostOverridden ? 'off' : resolved.autoSave
|
|
490
|
+
|
|
430
491
|
if (host !== 'uniweb') {
|
|
431
|
-
await deployStaticHost(siteDir,
|
|
492
|
+
await deployStaticHost(siteDir, host, resolved, {
|
|
493
|
+
dryRun,
|
|
494
|
+
autoSave,
|
|
495
|
+
hostOverridden,
|
|
496
|
+
})
|
|
432
497
|
return
|
|
433
498
|
}
|
|
434
499
|
|
|
@@ -552,7 +617,7 @@ export async function deploy(args = []) {
|
|
|
552
617
|
// doesn't fail the whole deploy.
|
|
553
618
|
const desiredFeatures = readFeaturesFromYaml(siteYml)
|
|
554
619
|
|
|
555
|
-
const cliToken = await ensureAuth({ command: 'Deploying' })
|
|
620
|
+
const cliToken = await ensureAuth({ command: 'Deploying', args })
|
|
556
621
|
|
|
557
622
|
// Always rebuild unless the user explicitly opts out with --skip-build.
|
|
558
623
|
// A stale dist/ from a previous build + edited content on disk would
|
|
@@ -944,19 +1009,40 @@ export async function deploy(args = []) {
|
|
|
944
1009
|
if (handleResolved) {
|
|
945
1010
|
console.log(` ${c.cyan}https://${handleResolved}.uniweb.website/${c.reset}`)
|
|
946
1011
|
}
|
|
1012
|
+
|
|
1013
|
+
// Record a fresh lastDeploy.<target> entry. Skipped on --no-save (and
|
|
1014
|
+
// on --host overrides, but uniweb-host can't be reached via override
|
|
1015
|
+
// since the override branches into deployStaticHost above).
|
|
1016
|
+
await persistLastDeploy(siteDir, {
|
|
1017
|
+
targetName: resolved.targetName,
|
|
1018
|
+
targetConfig: resolved.fromFile ? null : { host: 'uniweb' },
|
|
1019
|
+
autoSave,
|
|
1020
|
+
lastDeploy: {
|
|
1021
|
+
at: deployReceipt.deployedAt,
|
|
1022
|
+
host: 'uniweb',
|
|
1023
|
+
url: deployReceipt.url,
|
|
1024
|
+
siteId: siteIdResolved,
|
|
1025
|
+
handle: handleResolved,
|
|
1026
|
+
foundation: {
|
|
1027
|
+
shape: 'linked',
|
|
1028
|
+
ref: foundationRef,
|
|
1029
|
+
},
|
|
1030
|
+
runtime: runtimeVersion,
|
|
1031
|
+
},
|
|
1032
|
+
})
|
|
947
1033
|
}
|
|
948
1034
|
|
|
949
1035
|
// ─── Static-host deploy (S3+CloudFront, etc.) ─────────────────
|
|
950
1036
|
//
|
|
951
|
-
// Distinct from the uniweb-edge flow above. Picked when
|
|
952
|
-
//
|
|
953
|
-
// registered in @uniweb/build/hosts. Always runs `uniweb build`
|
|
954
|
-
//
|
|
955
|
-
//
|
|
1037
|
+
// Distinct from the uniweb-edge flow above. Picked when the resolved
|
|
1038
|
+
// deploy.yml target (or --host override) names a static-host adapter
|
|
1039
|
+
// registered in @uniweb/build/hosts. Always runs `uniweb build` (bundle
|
|
1040
|
+
// mode + prerender) first, then hands dist/ to the adapter's deploy hook
|
|
1041
|
+
// for upload + invalidation.
|
|
956
1042
|
//
|
|
957
1043
|
// See kb/framework/plans/static-host-deploy-adapters.md.
|
|
958
1044
|
|
|
959
|
-
async function deployStaticHost(siteDir,
|
|
1045
|
+
async function deployStaticHost(siteDir, hostName, resolved, { dryRun, autoSave, hostOverridden }) {
|
|
960
1046
|
let getAdapter
|
|
961
1047
|
try {
|
|
962
1048
|
({ getAdapter } = await import('@uniweb/build/hosts'))
|
|
@@ -971,7 +1057,7 @@ async function deployStaticHost(siteDir, siteYml, hostName, { dryRun }) {
|
|
|
971
1057
|
adapter = getAdapter(hostName)
|
|
972
1058
|
} catch (err) {
|
|
973
1059
|
say.err(err.message)
|
|
974
|
-
say.dim(
|
|
1060
|
+
say.dim('Set the host in deploy.yml or pass --host=<name>. See `uniweb deploy --help`.')
|
|
975
1061
|
process.exit(1)
|
|
976
1062
|
}
|
|
977
1063
|
|
|
@@ -982,17 +1068,18 @@ async function deployStaticHost(siteDir, siteYml, hostName, { dryRun }) {
|
|
|
982
1068
|
process.exit(1)
|
|
983
1069
|
}
|
|
984
1070
|
|
|
985
|
-
const deployConfig =
|
|
1071
|
+
const deployConfig = resolved.config || {}
|
|
986
1072
|
const distDir = join(siteDir, 'dist')
|
|
987
1073
|
|
|
988
1074
|
if (dryRun) {
|
|
989
1075
|
say.info(`Dry run — would deploy via host adapter: ${c.bold}${adapter.name}${c.reset}`)
|
|
990
1076
|
say.dim(`Site dir : ${siteDir}`)
|
|
991
1077
|
say.dim(`dist/ : ${existsSync(distDir) ? 'exists (would not rebuild)' : 'missing (would build)'}`)
|
|
992
|
-
say.dim(`
|
|
993
|
-
say.dim(`
|
|
994
|
-
say.dim(`
|
|
995
|
-
say.dim(`
|
|
1078
|
+
say.dim(`Target : ${resolved.targetName}`)
|
|
1079
|
+
say.dim(`bucket : ${deployConfig.bucket || '(unset)'}`)
|
|
1080
|
+
say.dim(`distributionId : ${deployConfig.distributionId || '(unset)'}`)
|
|
1081
|
+
say.dim(`region : ${deployConfig.region || '(unset)'}`)
|
|
1082
|
+
say.dim(`profile : ${deployConfig.profile || '(default AWS chain)'}`)
|
|
996
1083
|
return
|
|
997
1084
|
}
|
|
998
1085
|
|
|
@@ -1045,6 +1132,39 @@ async function deployStaticHost(siteDir, siteYml, hostName, { dryRun }) {
|
|
|
1045
1132
|
}
|
|
1046
1133
|
throw err
|
|
1047
1134
|
}
|
|
1135
|
+
|
|
1136
|
+
// Record a fresh lastDeploy.<target> entry. Skipped on --no-save and
|
|
1137
|
+
// on ad-hoc --host overrides — see autoSave gating in deploy().
|
|
1138
|
+
await persistLastDeploy(siteDir, {
|
|
1139
|
+
targetName: resolved.targetName,
|
|
1140
|
+
targetConfig: resolved.fromFile ? null : { host: hostName, ...deployConfig },
|
|
1141
|
+
autoSave,
|
|
1142
|
+
lastDeploy: {
|
|
1143
|
+
at: new Date().toISOString(),
|
|
1144
|
+
host: hostName,
|
|
1145
|
+
// Static hosts know their public URL only via the user's CDN config;
|
|
1146
|
+
// we don't have it on hand. Future: pull from a known field.
|
|
1147
|
+
},
|
|
1148
|
+
})
|
|
1149
|
+
if (hostOverridden && !dryRun) {
|
|
1150
|
+
say.dim('--host override active — did not write to deploy.yml. Edit deploy.yml to make this permanent.')
|
|
1151
|
+
}
|
|
1152
|
+
}
|
|
1153
|
+
|
|
1154
|
+
// ─── deploy.yml lastDeploy persistence ──────────────────────────
|
|
1155
|
+
|
|
1156
|
+
async function persistLastDeploy(siteDir, opts) {
|
|
1157
|
+
if (opts.autoSave === 'off') return
|
|
1158
|
+
try {
|
|
1159
|
+
const result = await recordLastDeploy(siteDir, opts)
|
|
1160
|
+
if (result?.created) {
|
|
1161
|
+
say.dim(`Wrote deploy.yml (target: ${opts.targetName})`)
|
|
1162
|
+
}
|
|
1163
|
+
} catch (err) {
|
|
1164
|
+
// The deploy itself succeeded — never fail the whole command on a
|
|
1165
|
+
// memo-write error. Surface it so the user can fix the file.
|
|
1166
|
+
say.dim(`Could not update deploy.yml: ${err.message}`)
|
|
1167
|
+
}
|
|
1048
1168
|
}
|
|
1049
1169
|
|
|
1050
1170
|
// ─── site.yml ──────────────────────────────────────────────
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dev Command
|
|
3
|
+
*
|
|
4
|
+
* Starts a dev server for a site in the current workspace. Wraps the
|
|
5
|
+
* project's `dev` script (set up by `uniweb create` to filter to the
|
|
6
|
+
* appropriate site package). Provides discoverability and consistency
|
|
7
|
+
* with `uniweb build` / `uniweb deploy` — users shouldn't have to know
|
|
8
|
+
* whether to type `pnpm dev` or `npm run dev` when the rest of the CLI
|
|
9
|
+
* is verb-shaped.
|
|
10
|
+
*
|
|
11
|
+
* Usage:
|
|
12
|
+
* uniweb dev Start dev server for the (single) site
|
|
13
|
+
* uniweb dev <site> Start dev server for a specific site
|
|
14
|
+
* uniweb dev --site <name> Same, with explicit flag form
|
|
15
|
+
*
|
|
16
|
+
* Resolution order for which site to launch:
|
|
17
|
+
* 1. --site <name> (if passed)
|
|
18
|
+
* 2. Positional <site> arg
|
|
19
|
+
* 3. The single site in the workspace (if exactly one)
|
|
20
|
+
* 4. The first site in the workspace, with a "multiple sites" notice
|
|
21
|
+
* pointing at --site for explicit selection
|
|
22
|
+
*
|
|
23
|
+
* Multi-site workspaces with no positional / flag will run the first
|
|
24
|
+
* site by default (mirrors the `pnpm dev` shortcut `uniweb create` writes).
|
|
25
|
+
* Use `--site` to pick a different one without editing the root scripts.
|
|
26
|
+
*
|
|
27
|
+
* Implementation: shells out to the package manager that invoked the CLI
|
|
28
|
+
* (detected via npm_config_user_agent), running the workspace-filtered
|
|
29
|
+
* dev command (`pnpm --filter <name> dev` or `npm -w <name> run dev`).
|
|
30
|
+
* No special handling of vite directly — the site package already owns
|
|
31
|
+
* its dev script, and shelling through pnpm/npm respects whatever the
|
|
32
|
+
* site has configured (Vite plugins, env vars, port overrides, etc.).
|
|
33
|
+
*/
|
|
34
|
+
|
|
35
|
+
import { spawn } from 'node:child_process'
|
|
36
|
+
import { join } from 'node:path'
|
|
37
|
+
|
|
38
|
+
import { detectPackageManager, filterCmd } from '../utils/pm.js'
|
|
39
|
+
import { discoverSites, readWorkspaceConfig } from '../utils/config.js'
|
|
40
|
+
import { findWorkspaceRoot } from '../utils/workspace.js'
|
|
41
|
+
import { readFlagValue } from '../utils/args.js'
|
|
42
|
+
|
|
43
|
+
const RED = '\x1b[31m'
|
|
44
|
+
const YELLOW = '\x1b[33m'
|
|
45
|
+
const DIM = '\x1b[2m'
|
|
46
|
+
const CYAN = '\x1b[36m'
|
|
47
|
+
const RESET = '\x1b[0m'
|
|
48
|
+
|
|
49
|
+
export async function dev(args = []) {
|
|
50
|
+
const cwd = process.cwd()
|
|
51
|
+
const rootDir = findWorkspaceRoot(cwd) || cwd
|
|
52
|
+
|
|
53
|
+
// Verify we're in a Uniweb workspace (has pnpm-workspace.yaml or
|
|
54
|
+
// package.json::workspaces). discoverSites already handles both.
|
|
55
|
+
let workspaceConfig
|
|
56
|
+
try {
|
|
57
|
+
workspaceConfig = await readWorkspaceConfig(rootDir)
|
|
58
|
+
} catch {
|
|
59
|
+
workspaceConfig = { packages: [] }
|
|
60
|
+
}
|
|
61
|
+
if (workspaceConfig.packages.length === 0) {
|
|
62
|
+
console.error(`${RED}✗${RESET} Not in a Uniweb workspace (no pnpm-workspace.yaml or package.json::workspaces).`)
|
|
63
|
+
console.error(` Run \`uniweb create <name>\` to scaffold a project, or cd into an existing one.`)
|
|
64
|
+
process.exit(1)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const sites = await discoverSites(rootDir)
|
|
68
|
+
if (sites.length === 0) {
|
|
69
|
+
console.error(`${RED}✗${RESET} No sites found in this workspace.`)
|
|
70
|
+
console.error(` Add one with \`uniweb add site <name>\`.`)
|
|
71
|
+
process.exit(1)
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Pick the site
|
|
75
|
+
const siteFlag = readFlagValue(args, '--site')
|
|
76
|
+
const positional = args.find(a => !a.startsWith('-'))
|
|
77
|
+
const requested = (typeof siteFlag === 'string' ? siteFlag : null) || positional || null
|
|
78
|
+
|
|
79
|
+
let site
|
|
80
|
+
if (requested) {
|
|
81
|
+
site = sites.find(s => s.name === requested) || sites.find(s => s.path === requested)
|
|
82
|
+
if (!site) {
|
|
83
|
+
console.error(`${RED}✗${RESET} Site "${requested}" not found.`)
|
|
84
|
+
console.error(` Available: ${sites.map(s => s.name).join(', ')}`)
|
|
85
|
+
process.exit(1)
|
|
86
|
+
}
|
|
87
|
+
} else if (sites.length === 1) {
|
|
88
|
+
site = sites[0]
|
|
89
|
+
} else {
|
|
90
|
+
site = sites[0]
|
|
91
|
+
console.error(`${YELLOW}⚠${RESET} Multiple sites found; using ${CYAN}${site.name}${RESET}.`)
|
|
92
|
+
console.error(` Pick a different one with \`uniweb dev --site <name>\`.`)
|
|
93
|
+
console.error(` Available: ${sites.map(s => s.name).join(', ')}`)
|
|
94
|
+
console.error('')
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const pm = detectPackageManager()
|
|
98
|
+
const command = filterCmd(pm, site.name, 'dev')
|
|
99
|
+
const [bin, ...rest] = command.split(' ')
|
|
100
|
+
const sitePath = join(rootDir, site.path)
|
|
101
|
+
|
|
102
|
+
console.error(`${DIM}→ ${command}${RESET} ${DIM}(site: ${site.name}, dir: ${sitePath})${RESET}`)
|
|
103
|
+
console.error('')
|
|
104
|
+
|
|
105
|
+
const child = spawn(bin, rest, { cwd: rootDir, stdio: 'inherit' })
|
|
106
|
+
child.on('close', code => process.exit(code ?? 0))
|
|
107
|
+
child.on('error', err => {
|
|
108
|
+
console.error(`${RED}✗${RESET} Failed to start dev server: ${err.message}`)
|
|
109
|
+
process.exit(1)
|
|
110
|
+
})
|
|
111
|
+
}
|
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/commands/handoff.js
CHANGED
|
@@ -145,7 +145,7 @@ async function readSchema(foundationDir) {
|
|
|
145
145
|
* Create a RemoteRegistry instance with auth.
|
|
146
146
|
*/
|
|
147
147
|
async function createRegistry(args) {
|
|
148
|
-
const token = await ensureAuth({ command: 'Handing off' })
|
|
148
|
+
const token = await ensureAuth({ command: 'Handing off', args })
|
|
149
149
|
|
|
150
150
|
const registryUrl = parseFlag(args, '--registry')
|
|
151
151
|
const url = registryUrl || process.env.UNIWEB_REGISTRY_URL || 'http://localhost:4001'
|
package/src/commands/invite.js
CHANGED
|
@@ -146,7 +146,7 @@ async function readSchema(foundationDir) {
|
|
|
146
146
|
* Create a RemoteRegistry instance with auth.
|
|
147
147
|
*/
|
|
148
148
|
async function createRegistry(args) {
|
|
149
|
-
const token = await ensureAuth({ command: 'Creating invite' })
|
|
149
|
+
const token = await ensureAuth({ command: 'Creating invite', args })
|
|
150
150
|
|
|
151
151
|
const registryUrl = parseFlag(args, '--registry')
|
|
152
152
|
const url = registryUrl || process.env.UNIWEB_REGISTRY_URL || 'http://localhost:4001'
|
package/src/commands/publish.js
CHANGED
|
@@ -794,7 +794,7 @@ export async function publish(args = []) {
|
|
|
794
794
|
registry = createLocalRegistry(foundationDir)
|
|
795
795
|
} else {
|
|
796
796
|
// Remote publish — ensure authenticated (inline login if needed)
|
|
797
|
-
const token = await ensureAuth({ command: 'Publishing' })
|
|
797
|
+
const token = await ensureAuth({ command: 'Publishing', args })
|
|
798
798
|
|
|
799
799
|
const url = registryUrl || getRegistryUrl()
|
|
800
800
|
registry = new RemoteRegistry(url, token)
|
package/src/commands/template.js
CHANGED
|
@@ -191,7 +191,7 @@ async function templatePublish(args) {
|
|
|
191
191
|
console.log(` ${colors.dim}${fileCount} files${colors.reset}`)
|
|
192
192
|
|
|
193
193
|
// 5. Authenticate
|
|
194
|
-
const token = await ensureAuth({ command: 'Publishing template' })
|
|
194
|
+
const token = await ensureAuth({ command: 'Publishing template', args })
|
|
195
195
|
|
|
196
196
|
// 6. Build payload
|
|
197
197
|
const url = registryUrl || process.env.UNIWEB_REGISTRY_URL || 'http://localhost:4001'
|