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