uniweb 0.12.34 → 0.12.36
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 +7 -7
- package/package.json +2 -2
- package/partials/agents.md +28 -17
- package/src/backend/client.js +103 -13
- package/src/backend/foundation-bring-along.js +229 -0
- package/src/backend/payment-handoff.js +105 -0
- package/src/backend/site-sync.js +24 -3
- package/src/commands/build.js +39 -35
- package/src/commands/clone.js +3 -3
- package/src/commands/deploy.js +95 -424
- package/src/commands/export.js +5 -3
- package/src/commands/publish.js +304 -76
- package/src/commands/pull.js +7 -5
- package/src/commands/push.js +8 -6
- package/src/commands/register.js +13 -5
- package/src/commands/rename.js +8 -5
- package/src/commands/runtime.js +1 -1
- package/src/commands/status.js +193 -0
- package/src/framework-index.json +4 -4
- package/src/index.js +71 -48
- package/src/utils/asset-upload.js +3 -3
- package/src/utils/code-upload.js +43 -3
- package/src/utils/config.js +30 -5
- package/src/utils/registry-auth.js +84 -33
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Payment handoff — the one piece of `uniweb publish` that's three-way
|
|
3
|
+
* (framework + backend + the `uniweb.app` web app). Design: payment-handoff-plan.md.
|
|
4
|
+
*
|
|
5
|
+
* The intent: `uniweb publish` on an unpaid (or newly-charged) site should just
|
|
6
|
+
* handle it — open a browser to `uniweb.app`, let the user pay, and continue.
|
|
7
|
+
* An already-paid site never opens a browser.
|
|
8
|
+
*
|
|
9
|
+
* The framework's ENTIRE payment knowledge is "open this URL, wait for done" —
|
|
10
|
+
* PROVIDER-AGNOSTIC. The CLI opens whatever `checkout_url` the backend hands it;
|
|
11
|
+
* the app drives the provider (Stripe or anything else) and settles with the
|
|
12
|
+
* backend. We reuse `awaitBrowserCallback` (the same loopback `uniweb login`
|
|
13
|
+
* uses) for the open + wait.
|
|
14
|
+
*
|
|
15
|
+
* DEGRADES: when the backend exposes no can-go-live route (404 / any failure),
|
|
16
|
+
* `canGoLive` returns null and we PROCEED — so publish ships before the payment
|
|
17
|
+
* route lands (same posture as `status --remote` on a missing endpoint). Live
|
|
18
|
+
* acceptance is the three-way test.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import { randomBytes } from 'node:crypto'
|
|
22
|
+
|
|
23
|
+
import { awaitBrowserCallback } from '../utils/registry-auth.js'
|
|
24
|
+
import { isNonInteractive } from '../utils/interactive.js'
|
|
25
|
+
|
|
26
|
+
// Append query params to a backend-supplied URL without disturbing its own.
|
|
27
|
+
function withParams(url, params) {
|
|
28
|
+
const u = new URL(url)
|
|
29
|
+
for (const [k, v] of Object.entries(params)) {
|
|
30
|
+
if (v != null) u.searchParams.set(k, String(v))
|
|
31
|
+
}
|
|
32
|
+
return u.toString()
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Settle payment for a site before go-live, if the backend says it's needed.
|
|
37
|
+
*
|
|
38
|
+
* @param {object} o
|
|
39
|
+
* @param {import('./client.js').BackendClient} o.client
|
|
40
|
+
* @param {string|null} o.uuid - the site-content uuid (null before the first push mints it)
|
|
41
|
+
* @param {string[]} o.args
|
|
42
|
+
* @param {object} o.say - { ok, info, warn, err, dim } reporters
|
|
43
|
+
* @param {boolean} [o.dryRun]
|
|
44
|
+
* @returns {Promise<{ proceed: boolean }>} proceed:false → the caller aborts go-live.
|
|
45
|
+
*/
|
|
46
|
+
export async function settlePaymentIfNeeded({ client, uuid, args, say, dryRun = false }) {
|
|
47
|
+
// No uuid yet (a first publish mints it on push) → nothing to check here; the
|
|
48
|
+
// post-push go-live is the moment the backend gates on payment.
|
|
49
|
+
if (!uuid) return { proceed: true }
|
|
50
|
+
|
|
51
|
+
// Dry-run reports the intent WITHOUT touching the network — the can-go-live
|
|
52
|
+
// read is auth-gated and must not force a login on a dry-run.
|
|
53
|
+
if (dryRun) {
|
|
54
|
+
say.dim(`Payment : would check whether go-live needs payment for ${uuid}`)
|
|
55
|
+
return { proceed: true }
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const verdict = await client.canGoLive(uuid)
|
|
59
|
+
// Degrade (no route) or already-paid → proceed.
|
|
60
|
+
if (!verdict || verdict.ok || !verdict.payment_required) return { proceed: true }
|
|
61
|
+
|
|
62
|
+
const checkoutUrl = verdict.checkout_url
|
|
63
|
+
if (!checkoutUrl) {
|
|
64
|
+
say.warn('The backend reports payment is required but returned no checkout URL — proceeding.')
|
|
65
|
+
return { proceed: true }
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (dryRun) {
|
|
69
|
+
say.dim(`Payment : required — would open ${checkoutUrl}`)
|
|
70
|
+
return { proceed: true }
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (isNonInteractive(args)) {
|
|
74
|
+
say.err('Payment is required to publish this site, and the CLI is non-interactive.')
|
|
75
|
+
say.dim(`Complete it in a browser, then re-run: ${checkoutUrl}`)
|
|
76
|
+
return { proceed: false }
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// The CSRF nonce the app echoes back on the done-signal redirect. The
|
|
80
|
+
// wait_token (when present) lets the app correlate the session backend-side.
|
|
81
|
+
const state = randomBytes(16).toString('hex')
|
|
82
|
+
say.info('Payment required — completing it in your browser…')
|
|
83
|
+
try {
|
|
84
|
+
await awaitBrowserCallback({
|
|
85
|
+
buildUrl: (redirectUri) =>
|
|
86
|
+
withParams(checkoutUrl, { redirect_uri: redirectUri, state, wait_token: verdict.wait_token }),
|
|
87
|
+
validate: (params) => {
|
|
88
|
+
if (params.get('error')) return { error: params.get('error') }
|
|
89
|
+
if (params.get('state') !== state) return { error: 'state mismatch — please retry.' }
|
|
90
|
+
return { value: true } // ok=1 / any non-error return = the app settled with the backend
|
|
91
|
+
},
|
|
92
|
+
openingLabel: 'Opening uniweb.app to complete payment…',
|
|
93
|
+
waitingLabel: 'Waiting for payment to complete (5 min)…',
|
|
94
|
+
timeoutMs: 5 * 60 * 1000,
|
|
95
|
+
okTitle: 'Payment complete',
|
|
96
|
+
errTitle: 'Payment failed',
|
|
97
|
+
})
|
|
98
|
+
} catch (err) {
|
|
99
|
+
say.err(`Payment was not completed: ${err.message}`)
|
|
100
|
+
say.dim('Re-run `uniweb publish` once payment is done.')
|
|
101
|
+
return { proceed: false }
|
|
102
|
+
}
|
|
103
|
+
say.ok('Payment complete.')
|
|
104
|
+
return { proceed: true }
|
|
105
|
+
}
|
package/src/backend/site-sync.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* packages, submit them over the two directional lanes (site-content first, then the
|
|
4
4
|
* folder keyed by the site's uuid), back-fill the minted uuids into the source files,
|
|
5
5
|
* and persist the send-only-changed cache. Extracted from the push command so
|
|
6
|
-
* `uniweb
|
|
6
|
+
* `uniweb publish` (the composite path) reuses the exact same lane submission.
|
|
7
7
|
*
|
|
8
8
|
* The command keeps flag parsing, the emit, and the `-o`/`--dry-run` preview;
|
|
9
9
|
* everything from "the packages are built, now POST them" lives here. Logging is
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
|
|
14
14
|
import { writeFileSync, readFileSync, mkdirSync } from 'node:fs'
|
|
15
15
|
import { join, dirname } from 'node:path'
|
|
16
|
-
import { backfillEntityUuids, writeSiteEntityUuid } from '@uniweb/build/uwx'
|
|
16
|
+
import { backfillEntityUuids, writeSiteEntityUuid, emitSyncPackages } from '@uniweb/build/uwx'
|
|
17
17
|
|
|
18
18
|
// Pull the finalized entities out of the restore response. The backend returns
|
|
19
19
|
// `{ report: { finalized: [ { index, uuid, changed, document }, … ] } }` — each entry
|
|
@@ -106,6 +106,27 @@ export function writeSyncCache(siteDir, hashes) {
|
|
|
106
106
|
writeFileSync(p, JSON.stringify({ version: 1, hashes }, null, 2) + '\n')
|
|
107
107
|
}
|
|
108
108
|
|
|
109
|
+
/**
|
|
110
|
+
* Offline-probe how many of a site's entities differ from the last successful push.
|
|
111
|
+
* Runs the SAME emit + send-only-changed diff `uniweb push` runs, but with an
|
|
112
|
+
* OFFLINE Model resolver — no auth, no submit, no backend round-trip. Used by
|
|
113
|
+
* `uniweb status` and the `uniweb publish` pre-flight. Throws if the producer
|
|
114
|
+
* can't build the sync packages (e.g. an unresolved data Model); callers report it.
|
|
115
|
+
*
|
|
116
|
+
* @param {string} siteDir
|
|
117
|
+
* @returns {Promise<{ changed: number, unchanged: number, warnings: string[] }>}
|
|
118
|
+
*/
|
|
119
|
+
export async function probeUnpushed(siteDir, { sendAll = false } = {}) {
|
|
120
|
+
const priorHashes = readSyncCache(siteDir)
|
|
121
|
+
const pkg = await emitSyncPackages(siteDir, {
|
|
122
|
+
resolveModel: makeModelResolver({ client: null, offline: true }),
|
|
123
|
+
priorHashes,
|
|
124
|
+
sendAll,
|
|
125
|
+
})
|
|
126
|
+
const changed = (pkg.siteContent?.entityCount || 0) + (pkg.collections?.entityCount || 0)
|
|
127
|
+
return { changed, unchanged: pkg.skipped || 0, warnings: pkg.warnings || [] }
|
|
128
|
+
}
|
|
129
|
+
|
|
109
130
|
/**
|
|
110
131
|
* Submit a site's emitted sync packages over both directional lanes, back-fill the
|
|
111
132
|
* minted uuids, and persist the send-only-changed cache. The HTTP + file-write-back
|
|
@@ -141,7 +162,7 @@ export async function pushSyncPackages({ client, siteDir, pkg, asOrg, report })
|
|
|
141
162
|
res = await doRequest()
|
|
142
163
|
} catch (err) {
|
|
143
164
|
error(`Could not reach the backend at ${client.origin}: ${err.message}`)
|
|
144
|
-
note('Set the origin with --
|
|
165
|
+
note('Set the origin with --backend <url> or UNIWEB_REGISTER_URL.')
|
|
145
166
|
return null
|
|
146
167
|
}
|
|
147
168
|
if (!res.ok) {
|
package/src/commands/build.js
CHANGED
|
@@ -4,30 +4,31 @@
|
|
|
4
4
|
* Builds foundations with schema generation, or sites.
|
|
5
5
|
*
|
|
6
6
|
* Two site build pipelines are available, but they're INTERNAL vocabulary
|
|
7
|
-
* after Phase 2 of the CLI ergonomics overhaul. Users see `uniweb
|
|
8
|
-
* (
|
|
9
|
-
*
|
|
10
|
-
* pipeline the caller requested.
|
|
7
|
+
* after Phase 2 of the CLI ergonomics overhaul. Users see `uniweb publish`
|
|
8
|
+
* (Uniweb hosting — runtime-linked), `uniweb deploy --host` (third-party host)
|
|
9
|
+
* and `uniweb export` (self-contained bundle); the build command itself just
|
|
10
|
+
* dispatches to whichever pipeline the caller requested.
|
|
11
11
|
*
|
|
12
|
-
* --bundle (internal; called by `uniweb export`)
|
|
12
|
+
* --bundle (internal; called by `uniweb deploy --host` / `uniweb export`)
|
|
13
13
|
* Full vite + post-vite pipeline. Produces a static-host JS bundle
|
|
14
14
|
* (`dist/index.html`, `dist/entry.js`, `_importmap/*`, `_pages/*` for
|
|
15
15
|
* split mode, sitemap/robots/search-index, prerendered HTML when
|
|
16
16
|
* configured). Foundation is loaded by URL when site.yml's foundation
|
|
17
17
|
* is a registry ref; statically inlined when it's a workspace-local
|
|
18
|
-
* ref
|
|
18
|
+
* ref (the self-contained case).
|
|
19
19
|
*
|
|
20
|
-
* --link (internal; called by `uniweb
|
|
21
|
-
* Data-only pipeline. No vite. Emits ONLY what
|
|
22
|
-
*
|
|
20
|
+
* --link (internal; called by `uniweb publish`)
|
|
21
|
+
* Data-only pipeline. No vite. Emits ONLY what Uniweb hosting
|
|
22
|
+
* needs: `dist/site-content.json` (with full sections),
|
|
23
23
|
* `dist/<lang>/site-content.json` per non-default locale,
|
|
24
24
|
* `dist/data/*.json` (collections), and `dist/assets/<media>` (images,
|
|
25
|
-
* fonts, video posters).
|
|
25
|
+
* fonts, video posters). The backend stitches runtime + foundation per
|
|
26
26
|
* request — the site's JS bundle would be dead weight.
|
|
27
27
|
*
|
|
28
28
|
* Bare `uniweb build` for a site defaults to --bundle (the historical
|
|
29
29
|
* behavior). This is mostly useful for inspecting the build output during
|
|
30
|
-
* development; for shipping, use `uniweb
|
|
30
|
+
* development; for shipping, use `uniweb publish` (Uniweb hosting) or
|
|
31
|
+
* `uniweb deploy --host` / `uniweb export` (third-party / self-contained).
|
|
31
32
|
*
|
|
32
33
|
* Usage:
|
|
33
34
|
* uniweb build # Build current directory (sites default to --bundle)
|
|
@@ -40,9 +41,9 @@
|
|
|
40
41
|
* s3-cloudfront, github-pages,
|
|
41
42
|
* generic-static). Default: cloudflare-pages.
|
|
42
43
|
*
|
|
43
|
-
* Internal flags
|
|
44
|
-
* --link # Data-only pipeline (Uniweb
|
|
45
|
-
* --bundle # Full vite pipeline (third-party
|
|
44
|
+
* Internal flags:
|
|
45
|
+
* --link # Data-only pipeline (Uniweb hosting; called by `uniweb publish`)
|
|
46
|
+
* --bundle # Full vite pipeline (third-party / self-contained; deploy --host / export)
|
|
46
47
|
*/
|
|
47
48
|
|
|
48
49
|
import { existsSync, readFileSync, readdirSync, statSync } from 'node:fs'
|
|
@@ -246,11 +247,12 @@ async function buildFoundation(projectDir, options = {}) {
|
|
|
246
247
|
/**
|
|
247
248
|
* Ensure a local foundation's `dist/entry.js` is current.
|
|
248
249
|
*
|
|
249
|
-
*
|
|
250
|
-
*
|
|
251
|
-
* the
|
|
252
|
-
*
|
|
253
|
-
*
|
|
250
|
+
* Bundle mode reads a local foundation from disk (vite imports it, prerender
|
|
251
|
+
* loads dist/entry.js for SSG), so the foundation must be built and current.
|
|
252
|
+
* Otherwise the verb fails with "Foundation not found at .../dist/entry.js" or
|
|
253
|
+
* silently ships stale artifacts. (Link mode does NOT read the built foundation
|
|
254
|
+
* — it forwards only the `foundation:` ref to the backend and ships no
|
|
255
|
+
* foundation code — so buildSiteLink does not call this.)
|
|
254
256
|
*
|
|
255
257
|
* `buildWorkspace()` already cascades when invoked from a workspace root,
|
|
256
258
|
* but verbs invoked from a site directory (`uniweb build` in `sites/x/`,
|
|
@@ -260,8 +262,8 @@ async function buildFoundation(projectDir, options = {}) {
|
|
|
260
262
|
*
|
|
261
263
|
* This helper is idempotent: when the workspace-root cascade has already
|
|
262
264
|
* run, the freshness check sees a current `dist/entry.js` and returns
|
|
263
|
-
* without rebuilding. So
|
|
264
|
-
*
|
|
265
|
+
* without rebuilding. So calling it inside `buildSite()` does not
|
|
266
|
+
* double-build under `buildWorkspace()`.
|
|
265
267
|
*
|
|
266
268
|
* Freshness rule: a built artifact (`dist/entry.js` or the legacy
|
|
267
269
|
* `dist/foundation.js`) exists AND its mtime is >= the newest mtime of
|
|
@@ -525,7 +527,7 @@ function resolveFoundationDir(projectDir, siteConfig) {
|
|
|
525
527
|
/**
|
|
526
528
|
* Build a site in link mode — data only, no vite.
|
|
527
529
|
*
|
|
528
|
-
* Emits exactly what `uniweb
|
|
530
|
+
* Emits exactly what `uniweb publish` ships to Uniweb hosting:
|
|
529
531
|
* dist/site-content.json (full sections inlined)
|
|
530
532
|
* dist/<lang>/site-content.json per non-default locale
|
|
531
533
|
* dist/data/<collection>.json (+ per-record files for `deferred:`)
|
|
@@ -564,10 +566,12 @@ async function buildSiteLink(projectDir, options = {}) {
|
|
|
564
566
|
// null and theme defaults come from theme.yml only.
|
|
565
567
|
const foundationDir = await resolveFoundationDirForSite(projectDir, siteConfig).catch(() => null)
|
|
566
568
|
|
|
567
|
-
//
|
|
568
|
-
//
|
|
569
|
-
//
|
|
570
|
-
|
|
569
|
+
// Link mode does NOT (re)build the foundation. It reads only the
|
|
570
|
+
// foundation's SOURCE config (foundation.js::theme.vars, passed as
|
|
571
|
+
// foundationPath below) for theme defaults, and ships NO foundation code —
|
|
572
|
+
// the backend serves the foundation from the registry by the `foundation:`
|
|
573
|
+
// ref. (Bundle mode is the one that needs a current dist/entry.js — see
|
|
574
|
+
// buildSite — so the ensureFoundationFresh cascade lives there, not here.)
|
|
571
575
|
|
|
572
576
|
await buildSiteData({
|
|
573
577
|
siteRoot: projectDir,
|
|
@@ -637,7 +641,6 @@ async function resolveFoundationDirForSite(siteDir, siteConfig) {
|
|
|
637
641
|
if (!foundation || typeof foundation !== 'string') return null
|
|
638
642
|
// Registry ref or URL — no local foundation.
|
|
639
643
|
if (/^@[a-z0-9_-]+\/[a-z0-9_-]+@/.test(foundation)) return null
|
|
640
|
-
if (/^~[A-Za-z0-9_-]+\/[a-z0-9_-]+@/.test(foundation)) return null
|
|
641
644
|
if (foundation.startsWith('http://') || foundation.startsWith('https://')) return null
|
|
642
645
|
|
|
643
646
|
// Workspace sibling.
|
|
@@ -910,14 +913,15 @@ function showNextSteps(hasFoundations, hasSites) {
|
|
|
910
913
|
if (hasFoundations) {
|
|
911
914
|
log('')
|
|
912
915
|
log(`${colors.bright}Share with clients:${colors.reset}`)
|
|
913
|
-
log(` ${colors.bright}uniweb
|
|
916
|
+
log(` ${colors.bright}uniweb register${colors.reset} Release your foundation to the catalog (alias: uniweb release)`)
|
|
914
917
|
log(` ${colors.bright}uniweb handoff <email>${colors.reset} Hand off a site to a client`)
|
|
915
918
|
}
|
|
916
919
|
if (hasSites) {
|
|
917
920
|
log('')
|
|
918
|
-
log(`${colors.bright}
|
|
919
|
-
log(` ${colors.bright}uniweb
|
|
920
|
-
log(`
|
|
921
|
+
log(`${colors.bright}Ship a site:${colors.reset}`)
|
|
922
|
+
log(` ${colors.bright}uniweb publish${colors.reset} Uniweb hosting (brings the foundation along)`)
|
|
923
|
+
log(` ${colors.bright}uniweb deploy --host${colors.reset}=… Third-party host`)
|
|
924
|
+
log(` Or upload ${colors.cyan}dist/${colors.reset} (\`uniweb export\`) to any static host`)
|
|
921
925
|
}
|
|
922
926
|
}
|
|
923
927
|
|
|
@@ -943,10 +947,10 @@ export async function build(args = []) {
|
|
|
943
947
|
const prerenderFlag = args.includes('--prerender')
|
|
944
948
|
const noPrerenderFlag = args.includes('--no-prerender')
|
|
945
949
|
|
|
946
|
-
// Internal flags — called by `uniweb
|
|
947
|
-
// `uniweb export` (always --bundle). After Phase 2
|
|
948
|
-
// ergonomics overhaul, users don't see these flags directly; they
|
|
949
|
-
// pick the
|
|
950
|
+
// Internal flags — called by `uniweb publish` (always --link) and
|
|
951
|
+
// `uniweb deploy --host` / `uniweb export` (always --bundle). After Phase 2
|
|
952
|
+
// of the CLI ergonomics overhaul, users don't see these flags directly; they
|
|
953
|
+
// pick the verb (publish vs deploy vs export) and the corresponding
|
|
950
954
|
// pipeline runs. Bare `uniweb build` for a site defaults to --bundle
|
|
951
955
|
// (mostly used during development to inspect the vite output).
|
|
952
956
|
const linkFlag = args.includes('--link')
|
package/src/commands/clone.js
CHANGED
|
@@ -163,9 +163,9 @@ export async function clone(args = [], deps = {}) {
|
|
|
163
163
|
const pathFlag = flagValue(args, '--path')
|
|
164
164
|
const projectFlag = flagValue(args, '--project')
|
|
165
165
|
const tokenFlag = flagValue(args, '--token')
|
|
166
|
-
const
|
|
166
|
+
const explicitBackend = flagValue(args, '--backend') || flagValue(args, '--registry')
|
|
167
167
|
const client = new BackendClient({
|
|
168
|
-
originFlag:
|
|
168
|
+
originFlag: explicitBackend,
|
|
169
169
|
token: tokenFlag,
|
|
170
170
|
getToken: deps.getToken,
|
|
171
171
|
fetchImpl: deps.fetch,
|
|
@@ -295,7 +295,7 @@ export async function clone(args = [], deps = {}) {
|
|
|
295
295
|
}
|
|
296
296
|
|
|
297
297
|
const pullExtra = []
|
|
298
|
-
if (
|
|
298
|
+
if (explicitBackend) pullExtra.push('--backend', explicitBackend)
|
|
299
299
|
if (tokenFlag) pullExtra.push('--token', tokenFlag)
|
|
300
300
|
if (noCollections) pullExtra.push('--no-collections')
|
|
301
301
|
|