@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.
Files changed (171) hide show
  1. package/README.md +38 -14
  2. package/bin/weft.mjs +8 -1
  3. package/docs/async.md +16 -6
  4. package/docs/browser.md +22 -8
  5. package/docs/config.md +39 -9
  6. package/docs/core.md +30 -0
  7. package/docs/deploy.md +108 -0
  8. package/docs/design.md +12 -3
  9. package/docs/entrypoints.md +79 -0
  10. package/docs/fonts.md +12 -2
  11. package/docs/icons.md +13 -7
  12. package/docs/lists.md +3 -1
  13. package/docs/meta.md +13 -3
  14. package/docs/plugins.md +32 -9
  15. package/docs/props.md +13 -2
  16. package/docs/reactivity.md +26 -0
  17. package/docs/rules.md +19 -12
  18. package/docs/rust.md +47 -17
  19. package/docs/start-short.md +3 -2
  20. package/docs/start.md +226 -52
  21. package/docs/state.md +33 -7
  22. package/docs/style.md +31 -8
  23. package/docs/surface.md +61 -15
  24. package/docs/testing.md +15 -5
  25. package/docs/theme.md +182 -0
  26. package/docs/ui-state.md +19 -6
  27. package/docs/verify.md +13 -0
  28. package/package.json +13 -4
  29. package/src/add/config.mjs +93 -0
  30. package/src/add/errors.mjs +2 -0
  31. package/src/add/setup.mjs +88 -0
  32. package/src/add-rust.mjs +82 -27
  33. package/src/add.mjs +209 -181
  34. package/src/agents-md.mjs +144 -43
  35. package/src/alias.mjs +16 -5
  36. package/src/audit/page.mjs +313 -0
  37. package/src/audit/report.mjs +69 -0
  38. package/src/audit/utils.mjs +5 -0
  39. package/src/audit.mjs +174 -188
  40. package/src/browser/cli.mjs +24 -5
  41. package/src/browser/client.mjs +189 -25
  42. package/src/browser/commands.mjs +166 -51
  43. package/src/catalog/audit.mjs +129 -0
  44. package/src/catalog/auth.mjs +40 -0
  45. package/src/catalog/compiler.mjs +337 -0
  46. package/src/catalog/contracts.mjs +18 -0
  47. package/src/catalog/database.mjs +52 -0
  48. package/src/catalog/deploy.mjs +117 -0
  49. package/src/catalog/design.mjs +159 -0
  50. package/src/catalog/forms.mjs +25 -0
  51. package/src/catalog/hydration.mjs +29 -0
  52. package/src/catalog/icons.mjs +24 -0
  53. package/src/catalog/media.mjs +163 -0
  54. package/src/catalog/metadata.mjs +116 -0
  55. package/src/catalog/plugins.mjs +228 -0
  56. package/src/catalog/realtime.mjs +195 -0
  57. package/src/catalog/rust.mjs +75 -0
  58. package/src/catalog/server-functions.mjs +92 -0
  59. package/src/catalog/state.mjs +39 -0
  60. package/src/catalog/style-groups.mjs +54 -0
  61. package/src/catalog/tests.mjs +127 -0
  62. package/src/catalog/text.mjs +200 -0
  63. package/src/catalog/ui-state.mjs +42 -0
  64. package/src/catalog.mjs +68 -444
  65. package/src/check/runtime.mjs +280 -0
  66. package/src/check.mjs +199 -207
  67. package/src/cli/authoring.mjs +213 -0
  68. package/src/cli/project.mjs +302 -0
  69. package/src/cli.mjs +228 -224
  70. package/src/config.mjs +1 -1
  71. package/src/contributions.mjs +75 -25
  72. package/src/db-layer.mjs +13 -5
  73. package/src/deploy/cdn.mjs +142 -0
  74. package/src/deploy/files.mjs +266 -0
  75. package/src/deploy/fit.mjs +166 -0
  76. package/src/deploy/hosts.mjs +126 -0
  77. package/src/deploy/index/cli.mjs +20 -0
  78. package/src/deploy/index/environment.mjs +69 -0
  79. package/src/deploy/index/steps.mjs +87 -0
  80. package/src/deploy/index.mjs +387 -0
  81. package/src/deploy/needs.mjs +149 -0
  82. package/src/design-checks/accessible-names.mjs +62 -0
  83. package/src/design-checks/contrast.mjs +43 -0
  84. package/src/design-checks/csp.mjs +60 -0
  85. package/src/design-checks/files.mjs +59 -0
  86. package/src/design-checks/heading-links.mjs +61 -0
  87. package/src/design-checks/interaction.mjs +65 -0
  88. package/src/design-checks/jsx.mjs +67 -0
  89. package/src/design-checks/layout-values.mjs +68 -0
  90. package/src/design-checks/native-controls.mjs +48 -0
  91. package/src/design-checks/outline.mjs +39 -0
  92. package/src/design-checks/reporter.mjs +37 -0
  93. package/src/design-checks/rules.mjs +274 -0
  94. package/src/design-checks/values.mjs +262 -0
  95. package/src/design-checks.mjs +10 -400
  96. package/src/diag.mjs +32 -8
  97. package/src/docs.mjs +138 -42
  98. package/src/doctor.mjs +20 -8
  99. package/src/files.mjs +18 -5
  100. package/src/fix.mjs +10 -2
  101. package/src/gate/after.mjs +40 -0
  102. package/src/gate/before.mjs +54 -0
  103. package/src/gate/classify.mjs +72 -0
  104. package/src/gate/mutation.mjs +209 -0
  105. package/src/gate/resolve-cases.mjs +22 -0
  106. package/src/gate/result.mjs +63 -0
  107. package/src/gate/run.mjs +75 -0
  108. package/src/gate.mjs +104 -202
  109. package/src/infer/box-props.cjs +140 -0
  110. package/src/infer/core-boxes.cjs +46 -0
  111. package/src/infer/derived-writes.cjs +95 -0
  112. package/src/infer/engine/checks.cjs +193 -0
  113. package/src/infer/engine/flow.cjs +293 -0
  114. package/src/infer/engine/report.cjs +158 -0
  115. package/src/infer/engine/utils.cjs +15 -0
  116. package/src/infer/engine.cjs +108 -368
  117. package/src/infer/facts/sites.cjs +114 -0
  118. package/src/infer/facts.cjs +210 -117
  119. package/src/infer/hosts.cjs +69 -22
  120. package/src/infer/lsp.mjs +149 -40
  121. package/src/infer/session.cjs +73 -17
  122. package/src/infer/shadow.cjs +13 -5
  123. package/src/infer/worker.cjs +57 -9
  124. package/src/jsx-scan.mjs +63 -21
  125. package/src/load-config/schema.mjs +281 -0
  126. package/src/load-config.mjs +144 -116
  127. package/src/map.mjs +93 -26
  128. package/src/mcp/browser-tools.mjs +155 -0
  129. package/src/mcp.mjs +147 -69
  130. package/src/meta-table.mjs +43 -8
  131. package/src/mutate.mjs +120 -36
  132. package/src/plugin-checks.mjs +52 -23
  133. package/src/plugins.mjs +125 -39
  134. package/src/project.mjs +170 -79
  135. package/src/props-checks.mjs +24 -6
  136. package/src/props-listener.mjs +27 -9
  137. package/src/react-checks.mjs +35 -4
  138. package/src/server-state-checks.mjs +34 -0
  139. package/src/share-checks.mjs +121 -37
  140. package/src/site.mjs +33 -6
  141. package/src/slots.mjs +104 -19
  142. package/src/start/build.mjs +116 -0
  143. package/src/start/routes.mjs +38 -0
  144. package/src/start.mjs +194 -92
  145. package/src/state-checks/core-state.mjs +223 -0
  146. package/src/state-checks/modules.mjs +17 -0
  147. package/src/state-checks/scan.mjs +232 -0
  148. package/src/state-checks/server-graph.mjs +65 -0
  149. package/src/state-checks.mjs +109 -133
  150. package/src/style/find.mjs +121 -0
  151. package/src/style.mjs +133 -127
  152. package/src/surface-checks.mjs +212 -46
  153. package/src/tailwind.mjs +5 -4
  154. package/src/tasks.mjs +189 -27
  155. package/src/test-rules/context.mjs +109 -0
  156. package/src/test-rules/file-rules.mjs +279 -0
  157. package/src/test-rules/rule-helpers.mjs +244 -0
  158. package/src/test-rules/syntax.mjs +223 -0
  159. package/src/test-rules/test-rules.mjs +127 -0
  160. package/src/testdiff.mjs +94 -32
  161. package/src/tests.mjs +31 -483
  162. package/src/text-checks.mjs +229 -69
  163. package/src/tokens.mjs +12 -4
  164. package/src/trust/scan.mjs +95 -0
  165. package/src/trust.mjs +214 -102
  166. package/src/weft.mjs +41 -9
  167. package/src/workspace.mjs +96 -19
  168. package/ts-plugin/context.js +57 -0
  169. package/ts-plugin/index.js +4 -124
  170. package/ts-plugin/mapping.js +65 -0
  171. 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, locate, positions } from './diag.mjs'
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)) throw Object.assign(new Error('@weft-js/start is not installed in this project'), { code: 'E_USAGE' })
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) sync(root, loaded)
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: 'E_USAGE' })
40
- return { site: siteOf(loaded), hydration: loaded.config?.hydration, diagnostics: loaded.diagnostics }
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 = () => (process.versions.deno ? 'deno' : process.versions.bun ? 'bun' : 'node')
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)) for (const [key, value] of Object.entries(parseEnv(readFileSync(file, 'utf8')))) process.env[key] ??= value
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) ? await devSidecars({ root, site: config.site, rust }) : undefined
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)].map(c => c.trim()).filter(Boolean)
91
- const allows = d => codes.some(c => c === 'all' || c === d.code || (/^[A-Z]$/.test(c) && d.code.startsWith(c)))
92
- const reason = d => reasons[d.code] ?? reasons[d.code[0]] ?? reasons.all ?? (flag ? '--unsafe-allow' : '')
93
- return { allows, let: d => ({ ...d, severity: 'warning', why: `UNSAFE, let through (${reason(d)}): ${d.why}` }) }
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
- requireTrust(root, await loadConfig(root))
98
- const unsafe = unsafeAllowed(unsafeAllow, (await loadConfig(root)).config)
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
- const chosen = runtime ?? (await loadConfig(root)).config?.runtime ?? hostRuntime()
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(new Error(`${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.`), { code: 'E_REFUSED' })
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({ root, site: config.site, preset, live: (await loadConfig(root)).config?.live, verify: false })
120
- const unlive = plan.diagnostics.find(d => d.severity === 'error')
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 { buildSidecars, shipSidecars, startSidecars, usesRemote } = await start(root, 'sidecar')
123
- const contracted = () => writeContract({ root, site: config.site, preset, rust })
124
- let warned = []
125
- // Sidecars first: prerendering a share card runs its route's `load`, which may read one.
126
- const running = usesRemote(root) ? await contracted().then(async c => {
127
- const stop = c.diagnostics.find(d => d.severity === 'error')
128
- if (stop) refuse(stop)
129
- const built = await buildSidecars({ root, contract: c.contract, rust, preset })
130
- const failed = built.diagnostics.find(d => d.severity === 'error')
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 ?? {})) before[k] === undefined ? delete process.env[k] : (process.env[k] = before[k])
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
- const contract = await writeContract({ root, site: config.site, preset, rust, live: (await loadConfig(root)).config?.live, verify: true })
145
- for (const refused of contract.diagnostics.filter(d => d.severity === 'error')) refuse(refused)
146
- if (running) shipSidecars(root)
147
- await writeMap(root)
148
- // Last, the pages as built, in a clean Chrome at a phone's width and a desktop's (audit.mjs): an error fails the build.
149
- const settings = (await loadConfig(root)).config?.build?.audit ?? true
150
- const audited = settings === false
151
- ? { diagnostics: [diagnostic({ code: 'L000', severity: 'warning', file: 'weft.config.ts', why: '`build.audit` is false: the built pages were not audited.' })] }
152
- : await audit(root, { dir: builtPublic(root), ...(typeof settings === 'object' && settings) })
153
- const pages = audited.diagnostics.map(d => (d.severity === 'error' && unsafe.allows(d) ? unsafe.let(d) : d))
154
- const kept = [...contract.diagnostics.filter(d => !(d.severity === 'error' && unsafe.allows(d))), ...warned].map(d => diagnostic({ file: '.weft/contract.json', ...d }))
155
- const diagnostics = [...kept, ...pages, ...forced.map(d => diagnostic({ file: '.weft/contract.json', ...d }))]
156
- return { ok: !diagnostics.some(d => d.severity === 'error'), preset, output, contract: contract.path, functions: contract.functions, audited: audited.pages?.length ?? 0, diagnostics, ms: Math.round(performance.now() - t) }
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 = { node: ['node', ['--disable-warning=ExperimentalWarning']], bun: ['bun', ['run']], deno: ['deno', ['run', '-A']] }
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]) throw Object.assign(new Error(`unknown runtime "${chosen}": use ${Object.keys(SERVERS).join(', ')}`), { code: 'E_USAGE' })
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(chosen === hostRuntime() ? process.execPath : cmd, [...flags, join('.output', 'server', 'index.mjs')], { cwd: root, stdio: 'inherit', env: { ...process.env, ...sidecars.env } })
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 => { child.on('close', code => { sidecars.close(); ok(code ?? 0) }) })
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)) throw Object.assign(new Error(`weft db ${sub ?? ''}: use generate (new migrations apply on the next request), live or listen`), { code: 'E_USAGE' })
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) throw Object.assign(new Error('this app has no database: `weft add db` adds one'), { code: 'E_USAGE' })
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)) writeFileSync(join(root, layer.migrations, '.schema-hash'), schemaHash(readFileSync(schema)) + '\n')
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) throw Object.assign(new Error('`src/realtime.server.ts` has no `changes: dbChanges(db, [tables])`'), { code: 'E_USAGE' })
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') return { ok: true, output: `${sql}\n-- add this to a migration of your data layer` }
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'], { cwd: root, encoding: 'utf8' })
239
- if (r.status !== 0) throw Object.assign(new Error(`drizzle-kit generate failed:\n${r.stdout}${r.stderr}`), { code: 'E_INTERNAL' })
240
- const { entries } = JSON.parse(readFileSync(join(root, layer.migrations, 'meta', '_journal.json'), 'utf8'))
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 { ok: true, output: `${file}: change triggers for ${tables.join(', ')}. They apply on the next request.` }
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) throw Object.assign(new Error('`src/realtime.server.ts` has no `changes`'), { code: 'E_USAGE' })
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 => { process.once('SIGINT', ok); process.once('SIGTERM', 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({ root, site: config.site, preset: preset ?? PRESETS[chosen], live: (await loadConfig(root)).config?.live, verify: false })
270
- return { ...plan, diagnostics: plan.diagnostics.map(d => diagnostic({ file: 'src/realtime.server.ts', ...d })) }
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 { ...result, diagnostics: [...config.diagnostics, ...routeDiagnostics(root, result.diagnostics)] }
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, ok: true, problems: manifest.problems,
289
- routes: manifest.routes.map(r => ({ path: r.kind === 'page' ? filePath(manifest, r) : r.kind, kind: r.kind, file: r.file, layouts: r.layouts, params: r.params, card: r.card ?? null })),
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
  }