@weft-js/cli 0.1.1 → 0.1.3
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 +534 -54
- package/bin/weft.mjs +8 -1
- package/docs/async.md +16 -6
- package/docs/browser.md +22 -8
- package/docs/config.md +39 -9
- package/docs/core.md +30 -0
- package/docs/deploy.md +33 -8
- package/docs/design.md +12 -3
- package/docs/entrypoints.md +79 -0
- package/docs/fonts.md +12 -2
- package/docs/icons.md +13 -7
- package/docs/lists.md +3 -1
- package/docs/meta.md +13 -3
- package/docs/plugins.md +32 -9
- package/docs/props.md +13 -2
- package/docs/reactivity.md +26 -0
- package/docs/rules.md +19 -12
- package/docs/rust.md +47 -17
- package/docs/start-short.md +3 -2
- package/docs/start.md +226 -52
- package/docs/state.md +33 -7
- package/docs/style.md +31 -8
- package/docs/surface.md +61 -15
- package/docs/testing.md +15 -5
- package/docs/theme.md +182 -0
- package/docs/ui-state.md +19 -6
- package/docs/verify.md +13 -0
- package/package.json +13 -4
- package/src/add/config.mjs +93 -0
- package/src/add/errors.mjs +2 -0
- package/src/add/setup.mjs +88 -0
- package/src/add-rust.mjs +82 -27
- package/src/add.mjs +209 -181
- package/src/agents-md.mjs +144 -43
- package/src/alias.mjs +16 -5
- package/src/audit/page.mjs +313 -0
- package/src/audit/report.mjs +69 -0
- package/src/audit/utils.mjs +5 -0
- package/src/audit.mjs +159 -206
- package/src/browser/cli.mjs +24 -5
- package/src/browser/client.mjs +189 -25
- package/src/browser/commands.mjs +166 -51
- package/src/catalog/audit.mjs +129 -0
- package/src/catalog/auth.mjs +40 -0
- package/src/catalog/compiler.mjs +337 -0
- package/src/catalog/contracts.mjs +18 -0
- package/src/catalog/database.mjs +52 -0
- package/src/catalog/deploy.mjs +117 -0
- package/src/catalog/design.mjs +159 -0
- package/src/catalog/forms.mjs +25 -0
- package/src/catalog/hydration.mjs +29 -0
- package/src/catalog/icons.mjs +24 -0
- package/src/catalog/media.mjs +163 -0
- package/src/catalog/metadata.mjs +116 -0
- package/src/catalog/plugins.mjs +228 -0
- package/src/catalog/realtime.mjs +195 -0
- package/src/catalog/rust.mjs +75 -0
- package/src/catalog/server-functions.mjs +92 -0
- package/src/catalog/state.mjs +39 -0
- package/src/catalog/style-groups.mjs +54 -0
- package/src/catalog/tests.mjs +127 -0
- package/src/catalog/text.mjs +200 -0
- package/src/catalog/ui-state.mjs +42 -0
- package/src/catalog.mjs +67 -460
- package/src/check/runtime.mjs +280 -0
- package/src/check.mjs +198 -208
- package/src/cli/authoring.mjs +213 -0
- package/src/cli/project.mjs +302 -0
- package/src/cli.mjs +222 -250
- package/src/config.mjs +1 -1
- package/src/contributions.mjs +75 -25
- package/src/db-layer.mjs +13 -5
- package/src/deploy/cdn.mjs +94 -16
- package/src/deploy/files.mjs +100 -21
- package/src/deploy/fit.mjs +127 -23
- package/src/deploy/hosts.mjs +90 -20
- package/src/deploy/index/cli.mjs +20 -0
- package/src/deploy/index/environment.mjs +69 -0
- package/src/deploy/index/steps.mjs +87 -0
- package/src/deploy/index.mjs +228 -114
- package/src/deploy/needs.mjs +88 -31
- package/src/design-checks/accessible-names.mjs +62 -0
- package/src/design-checks/contrast.mjs +43 -0
- package/src/design-checks/csp.mjs +60 -0
- package/src/design-checks/files.mjs +59 -0
- package/src/design-checks/heading-links.mjs +61 -0
- package/src/design-checks/interaction.mjs +65 -0
- package/src/design-checks/jsx.mjs +67 -0
- package/src/design-checks/layout-values.mjs +68 -0
- package/src/design-checks/native-controls.mjs +48 -0
- package/src/design-checks/outline.mjs +39 -0
- package/src/design-checks/reporter.mjs +37 -0
- package/src/design-checks/rules.mjs +274 -0
- package/src/design-checks/values.mjs +262 -0
- package/src/design-checks.mjs +10 -400
- package/src/diag.mjs +32 -8
- package/src/docs.mjs +138 -43
- package/src/doctor.mjs +20 -8
- package/src/files.mjs +18 -5
- package/src/fix.mjs +10 -2
- package/src/gate/after.mjs +40 -0
- package/src/gate/before.mjs +54 -0
- package/src/gate/classify.mjs +72 -0
- package/src/gate/mutation.mjs +209 -0
- package/src/gate/resolve-cases.mjs +22 -0
- package/src/gate/result.mjs +63 -0
- package/src/gate/run.mjs +75 -0
- package/src/gate.mjs +104 -202
- package/src/infer/box-props.cjs +140 -0
- package/src/infer/core-boxes.cjs +46 -0
- package/src/infer/derived-writes.cjs +95 -0
- package/src/infer/engine/checks.cjs +193 -0
- package/src/infer/engine/flow.cjs +293 -0
- package/src/infer/engine/report.cjs +158 -0
- package/src/infer/engine/utils.cjs +15 -0
- package/src/infer/engine.cjs +108 -368
- package/src/infer/facts/sites.cjs +114 -0
- package/src/infer/facts.cjs +210 -117
- package/src/infer/hosts.cjs +69 -22
- package/src/infer/lsp.mjs +149 -40
- package/src/infer/session.cjs +73 -17
- package/src/infer/shadow.cjs +13 -5
- package/src/infer/worker.cjs +57 -9
- package/src/jsx-scan.mjs +63 -21
- package/src/load-config/schema.mjs +281 -0
- package/src/load-config.mjs +137 -138
- package/src/map.mjs +93 -26
- package/src/mcp/browser-tools.mjs +155 -0
- package/src/mcp.mjs +147 -69
- package/src/meta-table.mjs +43 -8
- package/src/mutate.mjs +120 -36
- package/src/plugin-checks.mjs +52 -23
- package/src/plugins.mjs +125 -39
- package/src/project.mjs +170 -79
- package/src/props-checks.mjs +24 -6
- package/src/props-listener.mjs +27 -9
- package/src/react-checks.mjs +35 -4
- package/src/server-state-checks.mjs +34 -0
- package/src/share-checks.mjs +121 -37
- package/src/site.mjs +33 -6
- package/src/slots.mjs +104 -19
- package/src/start/build.mjs +116 -0
- package/src/start/routes.mjs +38 -0
- package/src/start.mjs +193 -95
- package/src/state-checks/core-state.mjs +223 -0
- package/src/state-checks/modules.mjs +17 -0
- package/src/state-checks/scan.mjs +232 -0
- package/src/state-checks/server-graph.mjs +65 -0
- package/src/state-checks.mjs +109 -133
- package/src/style/find.mjs +121 -0
- package/src/style.mjs +133 -127
- package/src/surface-checks.mjs +212 -46
- package/src/tailwind.mjs +5 -4
- package/src/tasks.mjs +189 -27
- package/src/test-rules/context.mjs +109 -0
- package/src/test-rules/file-rules.mjs +279 -0
- package/src/test-rules/rule-helpers.mjs +244 -0
- package/src/test-rules/syntax.mjs +223 -0
- package/src/test-rules/test-rules.mjs +127 -0
- package/src/testdiff.mjs +94 -32
- package/src/tests.mjs +31 -483
- package/src/text-checks.mjs +229 -69
- package/src/tokens.mjs +12 -4
- package/src/trust/scan.mjs +95 -0
- package/src/trust.mjs +214 -102
- package/src/weft.mjs +41 -9
- package/src/workspace.mjs +96 -19
- package/ts-plugin/context.js +57 -0
- package/ts-plugin/index.js +4 -124
- package/ts-plugin/mapping.js +65 -0
- package/ts-plugin/proxy.js +144 -0
package/src/deploy/fit.mjs
CHANGED
|
@@ -4,15 +4,29 @@
|
|
|
4
4
|
import { HOSTS } from './hosts.mjs'
|
|
5
5
|
|
|
6
6
|
const SERVERS = "`deploy: { target: 'fly' }` or `'render'`: a server that stays up"
|
|
7
|
-
const SPLIT =
|
|
7
|
+
const SPLIT =
|
|
8
|
+
'or keep the pages here and run realtime on a small server (the split, `weft docs deploy`)'
|
|
8
9
|
|
|
9
10
|
/** What a disk need asks of a host, with the config that gives it one. */
|
|
10
11
|
function disk(h, need, cfg = {}, assume) {
|
|
11
|
-
if (h.disk === 'none')
|
|
12
|
+
if (h.disk === 'none')
|
|
13
|
+
return {
|
|
14
|
+
severity: 'error',
|
|
15
|
+
fix:
|
|
16
|
+
need.what === 'a SQLite file'
|
|
17
|
+
? `${SERVERS}, with a volume or disk, or a hosted database here (Postgres on Neon or Supabase, Turso for SQLite)`
|
|
18
|
+
: `${SERVERS}, with a volume or disk: this weft has no object store for uploads yet (research 46 §9)`,
|
|
19
|
+
}
|
|
12
20
|
const has = h.id === 'fly' ? cfg.fly?.volume : cfg.render?.disk
|
|
13
21
|
if (has) return
|
|
14
|
-
const add =
|
|
15
|
-
|
|
22
|
+
const add =
|
|
23
|
+
h.id === 'fly'
|
|
24
|
+
? "`deploy.fly.volume: { name: 'data', sizeGb: 1 }` (billed)"
|
|
25
|
+
: '`deploy.render.disk: { sizeGb: 1 }` (a paid plan)'
|
|
26
|
+
return {
|
|
27
|
+
severity: assume ? 'warning' : 'error',
|
|
28
|
+
fix: `add ${add}: it mounts at \`/app/.data\`, where SQLite's default \`file:.data/app.db\` already is, and uploads go to \`.data/media\``,
|
|
29
|
+
}
|
|
16
30
|
}
|
|
17
31
|
|
|
18
32
|
/**
|
|
@@ -24,39 +38,129 @@ function disk(h, need, cfg = {}, assume) {
|
|
|
24
38
|
*/
|
|
25
39
|
export function fit(needs, h, { deploy = {}, assume = false, local = false } = {}) {
|
|
26
40
|
const findings = []
|
|
27
|
-
const add = (code, severity, need, why, fix) =>
|
|
28
|
-
|
|
29
|
-
if (needs.
|
|
30
|
-
|
|
31
|
-
|
|
41
|
+
const add = (code, severity, need, why, fix) =>
|
|
42
|
+
findings.push({ code, severity, file: need.file, why, fix })
|
|
43
|
+
if (needs.sockets && !h.sockets)
|
|
44
|
+
add(
|
|
45
|
+
'E001',
|
|
46
|
+
'error',
|
|
47
|
+
needs.sockets,
|
|
48
|
+
`${needs.sockets.what} holds a WebSocket, and ${h.name} ends each process with its request: the page would load and never connect (R024)`,
|
|
49
|
+
`${SERVERS}, or \`'cloudflare'\` (Durable Objects), ${SPLIT}`,
|
|
50
|
+
)
|
|
51
|
+
if (needs.required && !h.held)
|
|
52
|
+
add(
|
|
53
|
+
'E002',
|
|
54
|
+
'error',
|
|
55
|
+
needs.required,
|
|
56
|
+
`${needs.required.what} needs a change feed that stays connected, and ${h.name} keeps no process between requests (R013)`,
|
|
57
|
+
`${SERVERS}, run \`weft db listen\` on one with \`listener: 'external'\`, or drop \`required\``,
|
|
58
|
+
)
|
|
59
|
+
if (needs.sidecar && !h.sidecars)
|
|
60
|
+
add(
|
|
61
|
+
'E002',
|
|
62
|
+
'error',
|
|
63
|
+
needs.sidecar,
|
|
64
|
+
`${needs.sidecar.what} is a second process next to the server, and ${h.name} runs only the server`,
|
|
65
|
+
`${SERVERS} (the image runs both), or deploy the sidecar on its own and set \`rust.services.<name>.external: true\` with its \`WEFT_RUST_<NAME>_URL\``,
|
|
66
|
+
)
|
|
67
|
+
if (needs.native && h.kind === 'workers')
|
|
68
|
+
add(
|
|
69
|
+
'R104',
|
|
70
|
+
'error',
|
|
71
|
+
needs.native,
|
|
72
|
+
`${needs.native.what} loads an addon or a \`.wasm\` file, and ${h.name} has no file system`,
|
|
73
|
+
'use `remote()`, or a server host',
|
|
74
|
+
)
|
|
32
75
|
for (const key of ['sqlite', 'uploads']) {
|
|
33
76
|
const need = needs[key]
|
|
34
77
|
const d = need && disk(h, need, deploy, assume)
|
|
35
|
-
if (d)
|
|
78
|
+
if (d)
|
|
79
|
+
add(
|
|
80
|
+
'E003',
|
|
81
|
+
d.severity,
|
|
82
|
+
need,
|
|
83
|
+
`${need.what} is kept on local disk, and ${h.disk === 'none' ? `${h.name}'s disk is gone after each request` : `${h.name} wipes it on each deploy without one`}`,
|
|
84
|
+
d.fix,
|
|
85
|
+
)
|
|
36
86
|
}
|
|
37
|
-
if (needs.feed && !h.held)
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
87
|
+
if (needs.feed && !h.held)
|
|
88
|
+
add(
|
|
89
|
+
'R014',
|
|
90
|
+
'warning',
|
|
91
|
+
needs.feed,
|
|
92
|
+
`${needs.feed.what} needs a process that stays up: on ${h.name}, writes from outside the app won't reach \`.live()\``,
|
|
93
|
+
`${SERVERS}, or \`weft db listen\` on one with \`listener: 'external'\``,
|
|
94
|
+
)
|
|
95
|
+
if (needs.memoryBus && h.many)
|
|
96
|
+
add(
|
|
97
|
+
'E009',
|
|
98
|
+
'warning',
|
|
99
|
+
needs.memoryBus,
|
|
100
|
+
`${needs.memoryBus.what} reaches the clients of one instance, and ${h.name} runs many`,
|
|
101
|
+
h.id === 'cloudflare'
|
|
102
|
+
? '`durableObjectBus` in `src/realtime.server.ts`'
|
|
103
|
+
: '`weft add redis` or `weft add nats`, or `postgresBus({ url })` in `src/realtime.server.ts`',
|
|
104
|
+
)
|
|
105
|
+
if (needs.addons && local && h.prebuilt && process.platform !== 'linux')
|
|
106
|
+
add(
|
|
107
|
+
'E004',
|
|
108
|
+
'warning',
|
|
109
|
+
needs.addons,
|
|
110
|
+
`${needs.addons.what} has a compiled addon, built here for ${process.platform}, and ${h.name} runs Linux`,
|
|
111
|
+
"deploy from CI (`deploy.ci: 'github'` builds on Linux), or drop the addon",
|
|
112
|
+
)
|
|
113
|
+
return { host: h.id, fits: !findings.some((f) => f.severity === 'error'), findings }
|
|
41
114
|
}
|
|
42
115
|
|
|
43
116
|
/** Every host, fitted, and the one to suggest (research 46 §4), with why. */
|
|
44
117
|
export function hostsReport(needs, { deploy = {} } = {}) {
|
|
45
|
-
const fits = HOSTS.map(h => ({
|
|
118
|
+
const fits = HOSTS.map((h) => ({
|
|
119
|
+
...fit(needs, h, { deploy, assume: true }),
|
|
120
|
+
name: h.name,
|
|
121
|
+
kind: h.kind,
|
|
122
|
+
cost: h.cost,
|
|
123
|
+
pricing: h.pricing,
|
|
124
|
+
deploys: h.deploys,
|
|
125
|
+
}))
|
|
46
126
|
return { fits, suggestion: suggest(needs, fits) }
|
|
47
127
|
}
|
|
48
128
|
|
|
49
129
|
/** @returns {{ host: string, why: string, alternative?: string }} */
|
|
50
|
-
export function suggest(needs, fits = HOSTS.map(h => fit(needs, h, { assume: true }))) {
|
|
51
|
-
const ok = id => fits.find(f => f.host === id)?.fits
|
|
52
|
-
const clean = id => ok(id) && !fits.find(f => f.host === id).findings.length
|
|
130
|
+
export function suggest(needs, fits = HOSTS.map((h) => fit(needs, h, { assume: true }))) {
|
|
131
|
+
const ok = (id) => fits.find((f) => f.host === id)?.fits
|
|
132
|
+
const clean = (id) => ok(id) && !fits.find((f) => f.host === id).findings.length
|
|
53
133
|
const held = needs.required || needs.sidecar || needs.feed
|
|
54
134
|
const kept = needs.sqlite || needs.uploads
|
|
55
135
|
if (!needs.sockets && !held && !kept) {
|
|
56
|
-
if (clean('netlify'))
|
|
57
|
-
|
|
136
|
+
if (clean('netlify'))
|
|
137
|
+
return {
|
|
138
|
+
host: 'netlify',
|
|
139
|
+
why: 'nothing here needs a server that stays up: a CDN with functions behind it is the cheapest and fastest host',
|
|
140
|
+
alternative: 'vercel',
|
|
141
|
+
}
|
|
142
|
+
if (ok('netlify'))
|
|
143
|
+
return {
|
|
144
|
+
host: 'netlify',
|
|
145
|
+
why: 'nothing here needs a server that stays up, once the warnings are fixed',
|
|
146
|
+
alternative: 'vercel',
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
if (needs.sockets && !held && !kept && !needs.native && clean('cloudflare'))
|
|
150
|
+
return {
|
|
151
|
+
host: 'cloudflare',
|
|
152
|
+
why: 'channels need a socket, and a Durable Object holds one for each room without a server to run',
|
|
153
|
+
alternative: 'fly',
|
|
154
|
+
}
|
|
155
|
+
if (kept)
|
|
156
|
+
return {
|
|
157
|
+
host: 'fly',
|
|
158
|
+
why: `${kept.what} needs a disk that lasts: a Fly volume is per machine and cheap`,
|
|
159
|
+
alternative: 'render',
|
|
160
|
+
}
|
|
161
|
+
return {
|
|
162
|
+
host: 'fly',
|
|
163
|
+
why: `${(needs.sockets ?? held).what} needs a server that stays up, and Fly runs the app as it is. Render's free instance does too, but sleeps after 15 idle minutes`,
|
|
164
|
+
alternative: 'render',
|
|
58
165
|
}
|
|
59
|
-
if (needs.sockets && !held && !kept && !needs.native && clean('cloudflare')) return { host: 'cloudflare', why: 'channels need a socket, and a Durable Object holds one for each room without a server to run', alternative: 'fly' }
|
|
60
|
-
if (kept) return { host: 'fly', why: `${kept.what} needs a disk that lasts: a Fly volume is per machine and cheap`, alternative: 'render' }
|
|
61
|
-
return { host: 'fly', why: `${(needs.sockets ?? held).what} needs a server that stays up, and Fly runs the app as it is. Render's free instance does too, but sleeps after 15 idle minutes`, alternative: 'render' }
|
|
62
166
|
}
|
package/src/deploy/hosts.mjs
CHANGED
|
@@ -14,40 +14,110 @@
|
|
|
14
14
|
/** @type {Host[]} */
|
|
15
15
|
export const HOSTS = [
|
|
16
16
|
{
|
|
17
|
-
id: 'netlify',
|
|
18
|
-
|
|
19
|
-
|
|
17
|
+
id: 'netlify',
|
|
18
|
+
name: 'Netlify',
|
|
19
|
+
kind: 'functions',
|
|
20
|
+
preset: 'netlify',
|
|
21
|
+
sockets: false,
|
|
22
|
+
held: false,
|
|
23
|
+
disk: 'none',
|
|
24
|
+
sidecars: false,
|
|
25
|
+
many: true,
|
|
26
|
+
prebuilt: true,
|
|
27
|
+
env: ['NETLIFY_AUTH_TOKEN', 'NETLIFY_SITE_ID'],
|
|
28
|
+
deploys: true,
|
|
29
|
+
cost: 'free tier; usage past it is billed in credits',
|
|
30
|
+
pricing: 'https://www.netlify.com/pricing/',
|
|
31
|
+
docs: 'https://docs.netlify.com/cli/get-started/',
|
|
20
32
|
},
|
|
21
33
|
{
|
|
22
|
-
id: 'vercel',
|
|
23
|
-
|
|
24
|
-
|
|
34
|
+
id: 'vercel',
|
|
35
|
+
name: 'Vercel',
|
|
36
|
+
kind: 'functions',
|
|
37
|
+
preset: 'vercel',
|
|
38
|
+
sockets: false,
|
|
39
|
+
held: false,
|
|
40
|
+
disk: 'none',
|
|
41
|
+
sidecars: false,
|
|
42
|
+
many: true,
|
|
43
|
+
prebuilt: true,
|
|
44
|
+
env: ['VERCEL_TOKEN', 'VERCEL_ORG_ID', 'VERCEL_PROJECT_ID'],
|
|
45
|
+
deploys: true,
|
|
46
|
+
cost: 'Hobby is free for non-commercial use; Pro is per seat',
|
|
47
|
+
pricing: 'https://vercel.com/pricing',
|
|
48
|
+
docs: 'https://vercel.com/docs/cli',
|
|
25
49
|
},
|
|
26
50
|
{
|
|
27
|
-
id: 'cloudflare',
|
|
28
|
-
|
|
29
|
-
|
|
51
|
+
id: 'cloudflare',
|
|
52
|
+
name: 'Cloudflare Workers',
|
|
53
|
+
kind: 'workers',
|
|
54
|
+
preset: 'cloudflare_durable',
|
|
55
|
+
sockets: true,
|
|
56
|
+
held: false,
|
|
57
|
+
disk: 'none',
|
|
58
|
+
sidecars: false,
|
|
59
|
+
many: true,
|
|
60
|
+
prebuilt: true,
|
|
61
|
+
env: ['CLOUDFLARE_API_TOKEN', 'CLOUDFLARE_ACCOUNT_ID'],
|
|
62
|
+
deploys: true,
|
|
63
|
+
cost: 'free tier includes Workers and SQLite-backed Durable Objects',
|
|
64
|
+
pricing: 'https://developers.cloudflare.com/workers/platform/pricing/',
|
|
65
|
+
docs: 'https://developers.cloudflare.com/workers/wrangler/',
|
|
30
66
|
},
|
|
31
67
|
{
|
|
32
|
-
id: 'fly',
|
|
33
|
-
|
|
34
|
-
|
|
68
|
+
id: 'fly',
|
|
69
|
+
name: 'Fly.io',
|
|
70
|
+
kind: 'server',
|
|
71
|
+
sockets: true,
|
|
72
|
+
held: true,
|
|
73
|
+
disk: 'volume',
|
|
74
|
+
sidecars: true,
|
|
75
|
+
many: false,
|
|
76
|
+
prebuilt: false,
|
|
77
|
+
env: ['FLY_API_TOKEN'],
|
|
78
|
+
deploys: true,
|
|
79
|
+
cost: 'pay as you go, no free tier for new organizations',
|
|
80
|
+
pricing: 'https://fly.io/docs/about/pricing/',
|
|
81
|
+
docs: 'https://fly.io/docs/flyctl/',
|
|
35
82
|
},
|
|
36
83
|
{
|
|
37
|
-
id: 'render',
|
|
38
|
-
|
|
39
|
-
|
|
84
|
+
id: 'render',
|
|
85
|
+
name: 'Render',
|
|
86
|
+
kind: 'server',
|
|
87
|
+
sockets: true,
|
|
88
|
+
held: true,
|
|
89
|
+
disk: 'paid-disk',
|
|
90
|
+
sidecars: true,
|
|
91
|
+
many: false,
|
|
92
|
+
prebuilt: false,
|
|
93
|
+
env: ['RENDER_DEPLOY_HOOK_URL'],
|
|
94
|
+
deploys: true,
|
|
95
|
+
cost: 'free instance sleeps after 15 idle minutes and has no disk; paid plans add a disk',
|
|
96
|
+
pricing: 'https://render.com/pricing',
|
|
97
|
+
docs: 'https://render.com/docs/blueprint-spec',
|
|
40
98
|
},
|
|
41
99
|
{
|
|
42
|
-
id: 'aws',
|
|
43
|
-
|
|
44
|
-
|
|
100
|
+
id: 'aws',
|
|
101
|
+
name: 'AWS Lambda',
|
|
102
|
+
kind: 'functions',
|
|
103
|
+
preset: 'aws_lambda',
|
|
104
|
+
sockets: false,
|
|
105
|
+
held: false,
|
|
106
|
+
disk: 'none',
|
|
107
|
+
sidecars: false,
|
|
108
|
+
many: true,
|
|
109
|
+
prebuilt: true,
|
|
110
|
+
env: [],
|
|
111
|
+
deploys: false,
|
|
112
|
+
cost: 'Lambda has a free tier; a server would go on App Runner, billed per running instance',
|
|
113
|
+
pricing: 'https://aws.amazon.com/pricing/',
|
|
114
|
+
docs: 'https://nitro.build/deploy/providers/aws',
|
|
45
115
|
},
|
|
46
116
|
]
|
|
47
117
|
|
|
48
|
-
export const TARGETS = HOSTS.filter(h => h.deploys).map(h => h.id)
|
|
118
|
+
export const TARGETS = HOSTS.filter((h) => h.deploys).map((h) => h.id)
|
|
49
119
|
|
|
50
|
-
export const host = id => HOSTS.find(h => h.id === id)
|
|
120
|
+
export const host = (id) => HOSTS.find((h) => h.id === id)
|
|
51
121
|
|
|
52
122
|
/** `runtime` in the config as the server preset a container runs. */
|
|
53
123
|
const SERVER_PRESETS = { node: 'node', bun: 'bun', deno: 'deno_server' }
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { spawnSync } from 'node:child_process'
|
|
2
|
+
|
|
3
|
+
const refused = (message) => Object.assign(new Error(message), { code: 'E_REFUSED' })
|
|
4
|
+
|
|
5
|
+
/** The host's CLI: on PATH, else through npx for the ones on npm. */
|
|
6
|
+
export function cliFor(
|
|
7
|
+
name,
|
|
8
|
+
{
|
|
9
|
+
which = (cmd) =>
|
|
10
|
+
spawnSync(process.platform === 'win32' ? 'where' : 'which', [cmd], { encoding: 'utf8' })
|
|
11
|
+
.status === 0,
|
|
12
|
+
} = {},
|
|
13
|
+
) {
|
|
14
|
+
const npm = { netlify: 'netlify-cli', vercel: 'vercel', wrangler: 'wrangler' }
|
|
15
|
+
for (const cmd of name === 'flyctl' ? ['flyctl', 'fly'] : [name]) if (which(cmd)) return [cmd]
|
|
16
|
+
if (npm[name]) return ['npx', '--yes', npm[name]]
|
|
17
|
+
throw refused(
|
|
18
|
+
`\`${name}\` isn't installed: ${name === 'flyctl' ? 'https://fly.io/docs/flyctl/install/' : `npm install -g ${npm[name] ?? name}`}`,
|
|
19
|
+
)
|
|
20
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs'
|
|
2
|
+
import { join } from 'node:path'
|
|
3
|
+
|
|
4
|
+
/** A variable a step reads, and whether its absence stops the deploy (E007). */
|
|
5
|
+
export function missingEnv(root, target, config, env) {
|
|
6
|
+
const ci = !!env.CI
|
|
7
|
+
const linked = {
|
|
8
|
+
netlify: existsSync(join(root, '.netlify/state.json')) || !!config.deploy?.netlify?.site,
|
|
9
|
+
vercel: existsSync(join(root, '.vercel/project.json')),
|
|
10
|
+
}
|
|
11
|
+
// a token: the CLI's own login stands in, except in CI. An id: needed unless the project is linked.
|
|
12
|
+
const token = (k, how) => [k, ci ? 'error' : 'warning', how, ": the CLI's own login is used"]
|
|
13
|
+
const id = (k, known, how) => [k, known ? null : 'error', how, '']
|
|
14
|
+
const need = {
|
|
15
|
+
netlify: [
|
|
16
|
+
token(
|
|
17
|
+
'NETLIFY_AUTH_TOKEN',
|
|
18
|
+
'`netlify login` here, or a personal access token from User settings → Applications',
|
|
19
|
+
),
|
|
20
|
+
id(
|
|
21
|
+
'NETLIFY_SITE_ID',
|
|
22
|
+
linked.netlify,
|
|
23
|
+
'`netlify link`, `deploy.netlify.site`, or the site id from Site configuration',
|
|
24
|
+
),
|
|
25
|
+
],
|
|
26
|
+
vercel: [
|
|
27
|
+
token('VERCEL_TOKEN', '`vercel login` here, or a token from Account Settings → Tokens'),
|
|
28
|
+
id('VERCEL_ORG_ID', linked.vercel, '`vercel link` writes it to .vercel/project.json'),
|
|
29
|
+
id('VERCEL_PROJECT_ID', linked.vercel, '`vercel link` writes it to .vercel/project.json'),
|
|
30
|
+
],
|
|
31
|
+
cloudflare: [
|
|
32
|
+
token('CLOUDFLARE_API_TOKEN', '`wrangler login` here, or a token with Workers Scripts: Edit'),
|
|
33
|
+
id('CLOUDFLARE_ACCOUNT_ID', !ci, 'the account id on the Workers overview page'),
|
|
34
|
+
],
|
|
35
|
+
fly: [token('FLY_API_TOKEN', '`flyctl auth login` here, or `flyctl tokens create deploy`')],
|
|
36
|
+
render: [id('RENDER_DEPLOY_HOOK_URL', false, "the service's Settings → Deploy Hook")],
|
|
37
|
+
}[target]
|
|
38
|
+
return need
|
|
39
|
+
.filter(([k, severity]) => severity && !env[k])
|
|
40
|
+
.map(([k, severity, how, local]) => ({
|
|
41
|
+
code: 'E007',
|
|
42
|
+
severity,
|
|
43
|
+
file: 'weft.config.ts',
|
|
44
|
+
why: `\`${k}\` is unset${severity === 'warning' ? local : ''}`,
|
|
45
|
+
fix: `${how}. Set it in the environment (never a file)`,
|
|
46
|
+
}))
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** The URL a host's CLI printed. */
|
|
50
|
+
export function urlFrom(step, stdout) {
|
|
51
|
+
if (step.url === 'json') {
|
|
52
|
+
try {
|
|
53
|
+
const j = JSON.parse(stdout.slice(stdout.indexOf('{')))
|
|
54
|
+
return j.url ?? j.deploy_url
|
|
55
|
+
} catch {
|
|
56
|
+
return
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
if (step.url === 'last-line')
|
|
60
|
+
return stdout
|
|
61
|
+
.trim()
|
|
62
|
+
.split('\n')
|
|
63
|
+
.reverse()
|
|
64
|
+
.find((l) => l.trim().startsWith('https://'))
|
|
65
|
+
?.trim()
|
|
66
|
+
if (step.url === 'workers') return /https:\/\/[\w.-]+\.workers\.dev/.exec(stdout)?.[0]
|
|
67
|
+
if (step.url === 'fly') return /https:\/\/[\w.-]+\.fly\.dev/.exec(stdout)?.[0]
|
|
68
|
+
return step.url
|
|
69
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { host, presetFor } from '../hosts.mjs'
|
|
2
|
+
import { needsOf } from '../needs.mjs'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* The steps `weft deploy` takes for a target, as data: a dry run prints them, `--yes` runs them. A step reads env vars
|
|
6
|
+
* by name; their values are filled in only as it runs.
|
|
7
|
+
*/
|
|
8
|
+
export function deploySteps(root, config, { target, prod = false } = {}) {
|
|
9
|
+
const h = host(target)
|
|
10
|
+
const deploy = config.deploy ?? {}
|
|
11
|
+
const runtime = config.runtime ?? 'node'
|
|
12
|
+
const needs = needsOf(root, config)
|
|
13
|
+
const steps = []
|
|
14
|
+
const build = {
|
|
15
|
+
kind: 'build',
|
|
16
|
+
what: `weft build --preset ${presetFor(h, runtime)}`,
|
|
17
|
+
preset: presetFor(h, runtime),
|
|
18
|
+
}
|
|
19
|
+
if (target === 'netlify') {
|
|
20
|
+
const site = process.env.NETLIFY_SITE_ID
|
|
21
|
+
? []
|
|
22
|
+
: deploy.netlify?.site
|
|
23
|
+
? ['--site', deploy.netlify.site]
|
|
24
|
+
: []
|
|
25
|
+
steps.push(build, {
|
|
26
|
+
kind: 'run',
|
|
27
|
+
cli: 'netlify',
|
|
28
|
+
args: [
|
|
29
|
+
'deploy',
|
|
30
|
+
'--dir',
|
|
31
|
+
'dist',
|
|
32
|
+
'--functions',
|
|
33
|
+
'.netlify/functions-internal',
|
|
34
|
+
'--json',
|
|
35
|
+
...site,
|
|
36
|
+
...(prod ? ['--prod'] : []),
|
|
37
|
+
],
|
|
38
|
+
reads: ['NETLIFY_AUTH_TOKEN', 'NETLIFY_SITE_ID'],
|
|
39
|
+
url: 'json',
|
|
40
|
+
})
|
|
41
|
+
}
|
|
42
|
+
if (target === 'vercel')
|
|
43
|
+
steps.push(build, {
|
|
44
|
+
kind: 'run',
|
|
45
|
+
cli: 'vercel',
|
|
46
|
+
args: ['deploy', '--prebuilt', '--yes', ...(prod ? ['--prod'] : [])],
|
|
47
|
+
token: 'VERCEL_TOKEN',
|
|
48
|
+
reads: ['VERCEL_TOKEN', 'VERCEL_ORG_ID', 'VERCEL_PROJECT_ID'],
|
|
49
|
+
url: 'last-line',
|
|
50
|
+
})
|
|
51
|
+
if (target === 'cloudflare')
|
|
52
|
+
steps.push(build, {
|
|
53
|
+
kind: 'run',
|
|
54
|
+
cli: 'wrangler',
|
|
55
|
+
args: ['deploy'],
|
|
56
|
+
cwd: '.output',
|
|
57
|
+
reads: ['CLOUDFLARE_API_TOKEN', 'CLOUDFLARE_ACCOUNT_ID'],
|
|
58
|
+
url: 'workers',
|
|
59
|
+
})
|
|
60
|
+
if (target === 'fly') {
|
|
61
|
+
// one machine unless a bus reaches the others: a second machine would split the rooms, and a volume is per machine
|
|
62
|
+
const one = needs.memoryBus || deploy.fly?.volume
|
|
63
|
+
steps.push({
|
|
64
|
+
kind: 'run',
|
|
65
|
+
cli: 'flyctl',
|
|
66
|
+
args: [
|
|
67
|
+
'deploy',
|
|
68
|
+
'--remote-only',
|
|
69
|
+
...(deploy.fly?.app ? ['--app', deploy.fly.app] : []),
|
|
70
|
+
...(one ? ['--ha=false'] : []),
|
|
71
|
+
],
|
|
72
|
+
reads: ['FLY_API_TOKEN'],
|
|
73
|
+
url: deploy.fly?.app ? `https://${deploy.fly.app}.fly.dev` : 'fly',
|
|
74
|
+
})
|
|
75
|
+
}
|
|
76
|
+
if (target === 'render')
|
|
77
|
+
steps.push({
|
|
78
|
+
kind: 'hook',
|
|
79
|
+
what: 'POST $RENDER_DEPLOY_HOOK_URL (Render builds the pushed commit)',
|
|
80
|
+
reads: ['RENDER_DEPLOY_HOOK_URL'],
|
|
81
|
+
url: deploy.render?.service ? `https://${deploy.render.service}.onrender.com` : undefined,
|
|
82
|
+
})
|
|
83
|
+
for (const s of steps)
|
|
84
|
+
if (s.cli)
|
|
85
|
+
s.what = `${s.cli} ${s.args.join(' ')}${s.token ? ` --token $${s.token}` : ''}${s.cwd ? ` (in ${s.cwd}/)` : ''}`
|
|
86
|
+
return steps
|
|
87
|
+
}
|