@weft-js/cli 0.1.1 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +38 -14
- 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/start.mjs
CHANGED
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
// `weft dev`, `weft build` and `weft routes` for an app with src/routes: they run @weft-js/start (research 25 §6–7).
|
|
2
2
|
// @weft-js/start resolves from the app first (its own version), then from this workspace.
|
|
3
3
|
|
|
4
|
-
import { audit, builtPublic } from './audit.mjs'
|
|
5
|
-
import { writeMap } from './map.mjs'
|
|
6
4
|
import { spawn, spawnSync } from 'node:child_process'
|
|
7
5
|
import { createHash } from 'node:crypto'
|
|
8
6
|
import { existsSync, readFileSync, writeFileSync } from 'node:fs'
|
|
@@ -11,12 +9,17 @@ import { dirname, join } from 'node:path'
|
|
|
11
9
|
import { pathToFileURL } from 'node:url'
|
|
12
10
|
import { parseEnv } from 'node:util'
|
|
13
11
|
import { dbLayer } from './db-layer.mjs'
|
|
14
|
-
import { diagnostic
|
|
12
|
+
import { diagnostic } from './diag.mjs'
|
|
13
|
+
import { routeDiagnostics } from './start/routes.mjs'
|
|
14
|
+
import { finishBuild, startBuildSidecars, buildPreset } from './start/build.mjs'
|
|
15
|
+
import { writeMap } from './map.mjs'
|
|
16
|
+
|
|
15
17
|
import { loadConfig } from './load-config.mjs'
|
|
16
18
|
import { sync } from './plugins.mjs'
|
|
17
19
|
import { requireTrust } from './trust.mjs'
|
|
18
20
|
import { attachProps } from './props-listener.mjs'
|
|
19
|
-
|
|
21
|
+
|
|
22
|
+
export { routeDiagnostics } from './start/routes.mjs'
|
|
20
23
|
|
|
21
24
|
export async function start(root, sub) {
|
|
22
25
|
let file
|
|
@@ -24,7 +27,10 @@ export async function start(root, sub) {
|
|
|
24
27
|
file = createRequire(join(root, 'package.json')).resolve(`@weft-js/start/${sub}`)
|
|
25
28
|
} catch {
|
|
26
29
|
const sibling = new URL(`../../start/src/${sub}.mjs`, import.meta.url)
|
|
27
|
-
if (!existsSync(sibling))
|
|
30
|
+
if (!existsSync(sibling))
|
|
31
|
+
throw Object.assign(new Error('@weft-js/start is not installed in this project'), {
|
|
32
|
+
code: 'E_USAGE',
|
|
33
|
+
})
|
|
28
34
|
file = sibling
|
|
29
35
|
}
|
|
30
36
|
return import(typeof file === 'string' ? pathToFileURL(file).href : file.href)
|
|
@@ -34,17 +40,22 @@ export async function start(root, sub) {
|
|
|
34
40
|
* finds the build-side providers (`.weft/slots/<id>.build.json`, research 26 §3.1). */
|
|
35
41
|
async function site(root) {
|
|
36
42
|
const loaded = await loadConfig(root)
|
|
37
|
-
if (!loaded.diagnostics.some(d => d.code === 'P007') && loaded.config?.plugins?.length)
|
|
43
|
+
if (!loaded.diagnostics.some((d) => d.code === 'P007') && loaded.config?.plugins?.length)
|
|
44
|
+
sync(root, loaded)
|
|
38
45
|
// A build on a config that doesn't load would quietly drop every plugin: fonts, tokens and slots.
|
|
39
|
-
const bad = loaded.diagnostics.find(d => d.code === 'P007')
|
|
46
|
+
const bad = loaded.diagnostics.find((d) => d.code === 'P007')
|
|
40
47
|
if (bad) throw Object.assign(new Error(`P007: ${bad.message ?? bad.why}`), { code: 'E_REFUSED' }) // the config's fault, not the command's
|
|
41
|
-
return {
|
|
48
|
+
return {
|
|
49
|
+
site: siteOf(loaded),
|
|
50
|
+
hydration: loaded.config?.hydration,
|
|
51
|
+
diagnostics: loaded.diagnostics,
|
|
52
|
+
}
|
|
42
53
|
}
|
|
43
54
|
|
|
44
55
|
/** `site` with what plugins know about the app's look (`card` in a manifest): warp's colours, the text plugin's faces. */
|
|
45
56
|
export function siteOf(loaded) {
|
|
46
57
|
const base = loaded.config?.site ?? {}
|
|
47
|
-
const given = (loaded.contributions ?? []).map(c => c.manifest.card).filter(Boolean)
|
|
58
|
+
const given = (loaded.contributions ?? []).map((c) => c.manifest.card).filter(Boolean)
|
|
48
59
|
if (!given.length) return base
|
|
49
60
|
const card = Object.assign({}, ...given)
|
|
50
61
|
return { ...base, card: { ...card, ...base.card, fonts: { ...card.fonts, ...base.card?.fonts } } }
|
|
@@ -53,12 +64,15 @@ export function siteOf(loaded) {
|
|
|
53
64
|
/** `runtime` in weft.config.ts or `--runtime`, as a Nitro preset (research 28 §5). */
|
|
54
65
|
export const PRESETS = { node: 'node', bun: 'bun', deno: 'deno_server' }
|
|
55
66
|
/** With no `runtime` in the config, the one running this: a machine may have only that one. */
|
|
56
|
-
export const hostRuntime = () =>
|
|
67
|
+
export const hostRuntime = () =>
|
|
68
|
+
process.versions.deno ? 'deno' : process.versions.bun ? 'bun' : 'node'
|
|
57
69
|
|
|
58
70
|
/** `.env` under what's already set. Not `process.loadEnvFile`: on Deno the file wins. */
|
|
59
|
-
function loadEnv(root) {
|
|
71
|
+
export function loadEnv(root) {
|
|
60
72
|
const file = join(root, '.env')
|
|
61
|
-
if (existsSync(file))
|
|
73
|
+
if (existsSync(file))
|
|
74
|
+
for (const [key, value] of Object.entries(parseEnv(readFileSync(file, 'utf8'))))
|
|
75
|
+
process.env[key] ??= value
|
|
62
76
|
}
|
|
63
77
|
|
|
64
78
|
export async function devCommand(root, { port } = {}) {
|
|
@@ -71,7 +85,9 @@ export async function devCommand(root, { port } = {}) {
|
|
|
71
85
|
const rust = (await loadConfig(root)).config?.rust
|
|
72
86
|
const { devSidecars, usesRemote } = await start(root, 'sidecar')
|
|
73
87
|
// Before the dev server: its worker inherits the addresses, and a request can't reach a sidecar that's still building.
|
|
74
|
-
const sidecars = usesRemote(root)
|
|
88
|
+
const sidecars = usesRemote(root)
|
|
89
|
+
? await devSidecars({ root, site: config.site, rust })
|
|
90
|
+
: undefined
|
|
75
91
|
const server = await dev({ root, ...config, port })
|
|
76
92
|
if (server.watcher) server.weftProps = attachProps(server, root, (await loadConfig(root)).config)
|
|
77
93
|
if (sidecars) {
|
|
@@ -88,76 +104,98 @@ export async function devCommand(root, { port } = {}) {
|
|
|
88
104
|
*/
|
|
89
105
|
export function unsafeAllowed(flag, config) {
|
|
90
106
|
const reasons = config?.build?.unsafeAllow ?? {}
|
|
91
|
-
const codes = [...String(flag ?? '').split(','), ...Object.keys(reasons)]
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
107
|
+
const codes = [...String(flag ?? '').split(','), ...Object.keys(reasons)]
|
|
108
|
+
.map((c) => c.trim())
|
|
109
|
+
.filter(Boolean)
|
|
110
|
+
const allows = (d) =>
|
|
111
|
+
codes.some((c) => c === 'all' || c === d.code || (/^[A-Z]$/.test(c) && d.code.startsWith(c)))
|
|
112
|
+
const reason = (d) =>
|
|
113
|
+
reasons[d.code] ?? reasons[d.code[0]] ?? reasons.all ?? (flag ? '--unsafe-allow' : '')
|
|
114
|
+
return {
|
|
115
|
+
allows,
|
|
116
|
+
let: (d) => ({
|
|
117
|
+
...d,
|
|
118
|
+
severity: 'warning',
|
|
119
|
+
why: `UNSAFE, let through (${reason(d)}): ${d.why}`,
|
|
120
|
+
}),
|
|
121
|
+
}
|
|
95
122
|
}
|
|
96
123
|
|
|
97
124
|
export async function buildCommand(root, { preset, runtime, unsafeAllow } = {}) {
|
|
98
|
-
|
|
99
|
-
|
|
125
|
+
const loaded = await loadConfig(root)
|
|
126
|
+
requireTrust(root, loaded)
|
|
127
|
+
// A plugin that refused its options contributed nothing: its look or data would go missing from a build that succeeds.
|
|
128
|
+
const declined = loaded.diagnostics.find((d) => d.severity === 'error' && d.owner !== 'core')
|
|
129
|
+
if (declined)
|
|
130
|
+
throw Object.assign(new Error(`${declined.code}: ${declined.why}`), { code: 'E_USAGE' })
|
|
131
|
+
const unsafe = unsafeAllowed(unsafeAllow, loaded.config)
|
|
100
132
|
const forced = []
|
|
101
133
|
const { build } = await start(root, 'app')
|
|
102
134
|
const { writeContract } = await start(root, 'contract')
|
|
103
135
|
const t = performance.now()
|
|
104
136
|
const config = await site(root)
|
|
105
|
-
|
|
106
|
-
// `deploy.target` picks the preset when nothing else does: a build for Fly is the runtime's server, for Netlify `netlify`
|
|
107
|
-
const target = !preset && !runtime && (await loadConfig(root)).config?.deploy?.target
|
|
108
|
-
if (target) preset = presetFor(host(target), chosen)
|
|
109
|
-
if (!preset && !PRESETS[chosen]) throw Object.assign(new Error(`unknown runtime "${chosen}": use ${Object.keys(PRESETS).join(', ')}`), { code: 'E_USAGE' })
|
|
110
|
-
preset ??= PRESETS[chosen]
|
|
137
|
+
preset = await buildPreset(root, { preset, runtime, hostRuntime, PRESETS })
|
|
111
138
|
// A build holds no secrets: an app's env check stands down, and server modules load without a database.
|
|
112
139
|
const held = process.env.WEFT_BUILD
|
|
113
140
|
process.env.WEFT_BUILD = '1'
|
|
114
141
|
try {
|
|
115
142
|
const rust = (await loadConfig(root)).config?.rust
|
|
116
|
-
const refuse = d => {
|
|
143
|
+
const refuse = (d) => {
|
|
117
144
|
if (unsafe.allows(d)) return void forced.push(unsafe.let(d))
|
|
118
145
|
// a finding, not a misuse: exit 1 with the fix and the way past it, never the usage text
|
|
119
|
-
throw Object.assign(
|
|
146
|
+
throw Object.assign(
|
|
147
|
+
new Error(
|
|
148
|
+
`${d.code}: ${d.why}${d.rendered ? `\n${d.rendered}` : ''}\nTo build anyway: --unsafe-allow ${d.code}, or build.unsafeAllow in weft.config.ts with why.`,
|
|
149
|
+
),
|
|
150
|
+
{ code: 'E_REFUSED' },
|
|
151
|
+
)
|
|
120
152
|
}
|
|
121
153
|
// What the preset can't do fails now, not after a build: a verify runs at the end, against a target that's up.
|
|
122
154
|
const { readLivePlan } = await start(root, 'contract')
|
|
123
|
-
const plan = await readLivePlan({
|
|
124
|
-
|
|
155
|
+
const plan = await readLivePlan({
|
|
156
|
+
root,
|
|
157
|
+
site: config.site,
|
|
158
|
+
preset,
|
|
159
|
+
live: (await loadConfig(root)).config?.live,
|
|
160
|
+
verify: false,
|
|
161
|
+
})
|
|
162
|
+
const unlive = plan.diagnostics.find((d) => d.severity === 'error')
|
|
125
163
|
if (unlive) refuse(unlive)
|
|
126
|
-
const {
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
if (failed) refuse(failed)
|
|
136
|
-
warned = built.diagnostics
|
|
137
|
-
return startSidecars({ root, built: true, log: () => {} })
|
|
138
|
-
}) : undefined
|
|
164
|
+
const { running, warned, shipSidecars } = await startBuildSidecars({
|
|
165
|
+
start,
|
|
166
|
+
root,
|
|
167
|
+
config,
|
|
168
|
+
preset,
|
|
169
|
+
rust,
|
|
170
|
+
refuse,
|
|
171
|
+
writeContract,
|
|
172
|
+
})
|
|
139
173
|
const before = { ...process.env }
|
|
140
174
|
Object.assign(process.env, running?.env)
|
|
141
175
|
let output
|
|
142
176
|
try {
|
|
143
|
-
({ output } = await build({ root, ...config, preset, logLevel: 'warn' }))
|
|
177
|
+
;({ output } = await build({ root, ...config, preset, logLevel: 'warn' }))
|
|
144
178
|
} finally {
|
|
145
179
|
running?.close()
|
|
146
|
-
for (const k of Object.keys(running?.env ?? {}))
|
|
180
|
+
for (const k of Object.keys(running?.env ?? {}))
|
|
181
|
+
before[k] === undefined ? delete process.env[k] : (process.env[k] = before[k])
|
|
147
182
|
}
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
183
|
+
return await finishBuild({
|
|
184
|
+
root,
|
|
185
|
+
config,
|
|
186
|
+
preset,
|
|
187
|
+
rust,
|
|
188
|
+
refuse,
|
|
189
|
+
running,
|
|
190
|
+
shipSidecars,
|
|
191
|
+
output,
|
|
192
|
+
warned,
|
|
193
|
+
unsafe,
|
|
194
|
+
forced,
|
|
195
|
+
loaded,
|
|
196
|
+
t,
|
|
197
|
+
writeContract,
|
|
198
|
+
})
|
|
161
199
|
} finally {
|
|
162
200
|
if (held === undefined) delete process.env.WEFT_BUILD
|
|
163
201
|
else process.env.WEFT_BUILD = held
|
|
@@ -165,58 +203,73 @@ export async function buildCommand(root, { preset, runtime, unsafeAllow } = {})
|
|
|
165
203
|
}
|
|
166
204
|
|
|
167
205
|
/** The built server's command for each runtime: `.env` for a local run, the host's environment otherwise. */
|
|
168
|
-
const SERVERS = {
|
|
206
|
+
const SERVERS = {
|
|
207
|
+
node: ['node', ['--disable-warning=ExperimentalWarning']],
|
|
208
|
+
bun: ['bun', ['run']],
|
|
209
|
+
deno: ['deno', ['run', '-A']],
|
|
210
|
+
}
|
|
169
211
|
|
|
170
212
|
/** `weft start`: run `.output/` on the runtime it was built for. Resolves with the exit code. */
|
|
171
213
|
export async function startCommand(root, { runtime } = {}) {
|
|
172
214
|
const chosen = runtime ?? (await loadConfig(root)).config?.runtime ?? hostRuntime()
|
|
173
|
-
if (!SERVERS[chosen])
|
|
215
|
+
if (!SERVERS[chosen])
|
|
216
|
+
throw Object.assign(
|
|
217
|
+
new Error(`unknown runtime "${chosen}": use ${Object.keys(SERVERS).join(', ')}`),
|
|
218
|
+
{ code: 'E_USAGE' },
|
|
219
|
+
)
|
|
174
220
|
loadEnv(root)
|
|
175
221
|
const [cmd, flags] = SERVERS[chosen]
|
|
176
222
|
// Sidecars first: the server's environment has their addresses, and they outlive a crash of either side.
|
|
177
223
|
const { startSidecars } = await start(root, 'sidecar')
|
|
178
224
|
const sidecars = await startSidecars({ root })
|
|
179
225
|
// The running binary when it can be: Deno on Windows doesn't find itself on PATH.
|
|
180
|
-
const child = spawn(
|
|
226
|
+
const child = spawn(
|
|
227
|
+
chosen === hostRuntime() ? process.execPath : cmd,
|
|
228
|
+
[...flags, join('.output', 'server', 'index.mjs')],
|
|
229
|
+
{ cwd: root, stdio: 'inherit', env: { ...process.env, ...sidecars.env } },
|
|
230
|
+
)
|
|
181
231
|
process.once('SIGINT', () => child.kill('SIGINT'))
|
|
182
232
|
process.once('SIGTERM', () => child.kill('SIGTERM'))
|
|
183
|
-
return new Promise(ok => {
|
|
233
|
+
return new Promise((ok) => {
|
|
234
|
+
child.on('close', (code) => {
|
|
235
|
+
sidecars.close()
|
|
236
|
+
ok(code ?? 0)
|
|
237
|
+
})
|
|
238
|
+
})
|
|
184
239
|
}
|
|
185
240
|
|
|
186
241
|
/** The hash `weft db generate` records and Q015 compares. */
|
|
187
|
-
export const schemaHash = text => createHash('sha256').update(text).digest('hex').slice(0, 16)
|
|
242
|
+
export const schemaHash = (text) => createHash('sha256').update(text).digest('hex').slice(0, 16)
|
|
188
243
|
|
|
189
244
|
/** What `weft db` takes. There's no `migrate`: new migrations apply on the next request. */
|
|
190
245
|
export const DB_COMMANDS = ['generate', 'live', 'listen']
|
|
191
246
|
|
|
192
247
|
/** `weft db generate [name]`: the data layer's migration, on Node like all of dev (research 28 §2, 34 §4). */
|
|
193
248
|
export async function dbCommand(root, sub, name) {
|
|
194
|
-
if (!DB_COMMANDS.includes(sub))
|
|
249
|
+
if (!DB_COMMANDS.includes(sub))
|
|
250
|
+
throw Object.assign(
|
|
251
|
+
new Error(
|
|
252
|
+
`weft db ${sub ?? ''}: use generate (new migrations apply on the next request), live or listen`,
|
|
253
|
+
),
|
|
254
|
+
{ code: 'E_USAGE' },
|
|
255
|
+
)
|
|
195
256
|
if (sub === 'live') return dbLive(root)
|
|
196
257
|
if (sub === 'listen') return dbListen(root)
|
|
197
258
|
const layer = await dbLayer(root)
|
|
198
|
-
if (!layer)
|
|
259
|
+
if (!layer)
|
|
260
|
+
throw Object.assign(new Error('this app has no database: `weft add db` adds one'), {
|
|
261
|
+
code: 'E_USAGE',
|
|
262
|
+
})
|
|
199
263
|
const output = layer.generate(root, name)
|
|
200
264
|
const schema = layer.schema && join(root, layer.schema)
|
|
201
|
-
if (schema && existsSync(schema))
|
|
265
|
+
if (schema && existsSync(schema))
|
|
266
|
+
writeFileSync(
|
|
267
|
+
join(root, layer.migrations, '.schema-hash'),
|
|
268
|
+
schemaHash(readFileSync(schema)) + '\n',
|
|
269
|
+
)
|
|
202
270
|
return { ok: true, output }
|
|
203
271
|
}
|
|
204
272
|
|
|
205
|
-
/**
|
|
206
|
-
* Route findings as diagnostics, pointing at the `meta` export, or at where one goes. H011's fix is that export, as an
|
|
207
|
-
* edit an agent can apply: not safe, since the words are a start and not the page's best.
|
|
208
|
-
*/
|
|
209
|
-
export function routeDiagnostics(root, found) {
|
|
210
|
-
const text = f => { try { return readFileSync(join(root, f), 'utf8') } catch { return '' } }
|
|
211
|
-
return found.map(({ code, severity, file, route, why, help }) => {
|
|
212
|
-
const source = text(file)
|
|
213
|
-
const at = code === 'H011' && help?.startsWith('export const') && !/\bexport\s+(?:const|function|async function)\s+meta\b/.test(source) ? source.indexOf('export default') : -1
|
|
214
|
-
const place = positions(source)
|
|
215
|
-
const fix = at >= 0 && { fix: { safe: false, description: 'Add `meta`', edits: [{ file, span: place.span(at, at), offset: [at, at], text: `${help}\n\n` }] } }
|
|
216
|
-
return { ...diagnostic({ code, severity, file, why, help, ...locate(source, 'meta'), ...fix }), route }
|
|
217
|
-
})
|
|
218
|
-
}
|
|
219
|
-
|
|
220
273
|
/** `weft db live`: a migration that adds change triggers to the tables in `changes` (Postgres, SQLite, MySQL). */
|
|
221
274
|
async function dbLive(root) {
|
|
222
275
|
const held = process.env.WEFT_BUILD
|
|
@@ -226,25 +279,41 @@ async function dbLive(root) {
|
|
|
226
279
|
const { changeSql } = await start(root, 'changes-sql')
|
|
227
280
|
let sql, tables
|
|
228
281
|
try {
|
|
229
|
-
({ sql, tables } = await withRealtime({ root, site: config.site }, async ({ changes }) => {
|
|
230
|
-
if (!changes?.defs)
|
|
282
|
+
;({ sql, tables } = await withRealtime({ root, site: config.site }, async ({ changes }) => {
|
|
283
|
+
if (!changes?.defs)
|
|
284
|
+
throw Object.assign(
|
|
285
|
+
new Error('`src/realtime.server.ts` has no `changes: dbChanges(db, [tables])`'),
|
|
286
|
+
{ code: 'E_USAGE' },
|
|
287
|
+
)
|
|
231
288
|
const defs = await changes.defs()
|
|
232
|
-
return { sql: changeSql(changes.dialect, defs), tables: defs.map(t => t.name) }
|
|
289
|
+
return { sql: changeSql(changes.dialect, defs), tables: defs.map((t) => t.name) }
|
|
233
290
|
}))
|
|
234
291
|
} finally {
|
|
235
292
|
if (held === undefined) delete process.env.WEFT_BUILD
|
|
236
293
|
else process.env.WEFT_BUILD = held
|
|
237
294
|
}
|
|
238
295
|
const layer = await dbLayer(root)
|
|
239
|
-
if (layer?.layer !== 'drizzle')
|
|
296
|
+
if (layer?.layer !== 'drizzle')
|
|
297
|
+
return { ok: true, output: `${sql}\n-- add this to a migration of your data layer` }
|
|
240
298
|
const req = createRequire(join(root, 'package.json'))
|
|
241
299
|
const bin = join(dirname(req.resolve('drizzle-kit')), 'bin.cjs')
|
|
242
|
-
const r = spawnSync(process.execPath, [bin, 'generate', '--custom', '--name=weft_live'], {
|
|
243
|
-
|
|
244
|
-
|
|
300
|
+
const r = spawnSync(process.execPath, [bin, 'generate', '--custom', '--name=weft_live'], {
|
|
301
|
+
cwd: root,
|
|
302
|
+
encoding: 'utf8',
|
|
303
|
+
})
|
|
304
|
+
if (r.status !== 0)
|
|
305
|
+
throw Object.assign(new Error(`drizzle-kit generate failed:\n${r.stdout}${r.stderr}`), {
|
|
306
|
+
code: 'E_INTERNAL',
|
|
307
|
+
})
|
|
308
|
+
const { entries } = JSON.parse(
|
|
309
|
+
readFileSync(join(root, layer.migrations, 'meta', '_journal.json'), 'utf8'),
|
|
310
|
+
)
|
|
245
311
|
const file = join(layer.migrations, `${entries.at(-1).tag}.sql`)
|
|
246
312
|
writeFileSync(join(root, file), sql)
|
|
247
|
-
return {
|
|
313
|
+
return {
|
|
314
|
+
ok: true,
|
|
315
|
+
output: `${file}: change triggers for ${tables.join(', ')}. They apply on the next request.`,
|
|
316
|
+
}
|
|
248
317
|
}
|
|
249
318
|
|
|
250
319
|
/** `weft db listen`: the change feed as its own process, for an app on functions. Runs until SIGINT or SIGTERM. */
|
|
@@ -253,10 +322,16 @@ async function dbListen(root) {
|
|
|
253
322
|
const config = await site(root)
|
|
254
323
|
const { withRealtime } = await start(root, 'contract')
|
|
255
324
|
await withRealtime({ root, site: config.site }, async ({ changes, bus }, server) => {
|
|
256
|
-
if (!changes)
|
|
325
|
+
if (!changes)
|
|
326
|
+
throw Object.assign(new Error('`src/realtime.server.ts` has no `changes`'), {
|
|
327
|
+
code: 'E_USAGE',
|
|
328
|
+
})
|
|
257
329
|
const { listen } = await server.ssrLoadModule('@weft-js/start/listen')
|
|
258
330
|
const stop = await listen({ changes, bus: bus ?? { name: 'memory' } })
|
|
259
|
-
await new Promise(ok => {
|
|
331
|
+
await new Promise((ok) => {
|
|
332
|
+
process.once('SIGINT', ok)
|
|
333
|
+
process.once('SIGTERM', ok)
|
|
334
|
+
})
|
|
260
335
|
await stop()
|
|
261
336
|
})
|
|
262
337
|
return { ok: true, output: 'weft db listen: stopped' }
|
|
@@ -270,8 +345,19 @@ export async function doctorCommand(root, { preset, runtime } = {}) {
|
|
|
270
345
|
process.env.WEFT_BUILD = '1'
|
|
271
346
|
try {
|
|
272
347
|
const { readLivePlan } = await start(root, 'contract')
|
|
273
|
-
const plan = await readLivePlan({
|
|
274
|
-
|
|
348
|
+
const plan = await readLivePlan({
|
|
349
|
+
root,
|
|
350
|
+
site: config.site,
|
|
351
|
+
preset: preset ?? PRESETS[chosen],
|
|
352
|
+
live: (await loadConfig(root)).config?.live,
|
|
353
|
+
verify: false,
|
|
354
|
+
})
|
|
355
|
+
return {
|
|
356
|
+
...plan,
|
|
357
|
+
diagnostics: plan.diagnostics.map((d) =>
|
|
358
|
+
diagnostic({ file: 'src/realtime.server.ts', ...d }),
|
|
359
|
+
),
|
|
360
|
+
}
|
|
275
361
|
} finally {
|
|
276
362
|
if (held === undefined) delete process.env.WEFT_BUILD
|
|
277
363
|
else process.env.WEFT_BUILD = held
|
|
@@ -284,13 +370,25 @@ export async function routesCommand(root, { meta, preset } = {}) {
|
|
|
284
370
|
if (meta) {
|
|
285
371
|
const { routesMeta } = await start(root, 'inspect')
|
|
286
372
|
const result = await routesMeta({ root, site: config.site, preset })
|
|
287
|
-
return {
|
|
373
|
+
return {
|
|
374
|
+
...result,
|
|
375
|
+
diagnostics: [...config.diagnostics, ...routeDiagnostics(root, result.diagnostics)],
|
|
376
|
+
}
|
|
288
377
|
}
|
|
289
378
|
const { scanRoutes, filePath } = await start(root, 'routes')
|
|
290
379
|
const manifest = scanRoutes(root)
|
|
291
380
|
return {
|
|
292
|
-
version: 1,
|
|
293
|
-
|
|
381
|
+
version: 1,
|
|
382
|
+
ok: true,
|
|
383
|
+
problems: manifest.problems,
|
|
384
|
+
routes: manifest.routes.map((r) => ({
|
|
385
|
+
path: r.kind === 'page' ? filePath(manifest, r) : r.kind,
|
|
386
|
+
kind: r.kind,
|
|
387
|
+
file: r.file,
|
|
388
|
+
layouts: r.layouts,
|
|
389
|
+
params: r.params,
|
|
390
|
+
card: r.card ?? null,
|
|
391
|
+
})),
|
|
294
392
|
diagnostics: config.diagnostics,
|
|
295
393
|
}
|
|
296
394
|
}
|