@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.
Files changed (171) hide show
  1. package/README.md +534 -54
  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 +33 -8
  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 +159 -206
  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 +67 -460
  65. package/src/check/runtime.mjs +280 -0
  66. package/src/check.mjs +198 -208
  67. package/src/cli/authoring.mjs +213 -0
  68. package/src/cli/project.mjs +302 -0
  69. package/src/cli.mjs +222 -250
  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 +94 -16
  74. package/src/deploy/files.mjs +100 -21
  75. package/src/deploy/fit.mjs +127 -23
  76. package/src/deploy/hosts.mjs +90 -20
  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 +228 -114
  81. package/src/deploy/needs.mjs +88 -31
  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 -43
  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 +137 -138
  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 +193 -95
  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,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, 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
- import { host, presetFor } from './deploy/hosts.mjs'
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)) 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
+ })
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) sync(root, loaded)
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 { site: siteOf(loaded), hydration: loaded.config?.hydration, diagnostics: loaded.diagnostics }
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 = () => (process.versions.deno ? 'deno' : process.versions.bun ? 'bun' : 'node')
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)) 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
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) ? await devSidecars({ root, site: config.site, rust }) : undefined
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)].map(c => c.trim()).filter(Boolean)
92
- const allows = d => codes.some(c => c === 'all' || c === d.code || (/^[A-Z]$/.test(c) && d.code.startsWith(c)))
93
- const reason = d => reasons[d.code] ?? reasons[d.code[0]] ?? reasons.all ?? (flag ? '--unsafe-allow' : '')
94
- 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
+ }
95
122
  }
96
123
 
97
124
  export async function buildCommand(root, { preset, runtime, unsafeAllow } = {}) {
98
- requireTrust(root, await loadConfig(root))
99
- 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)
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
- const chosen = runtime ?? (await loadConfig(root)).config?.runtime ?? hostRuntime()
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(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
+ )
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({ root, site: config.site, preset, live: (await loadConfig(root)).config?.live, verify: false })
124
- 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')
125
163
  if (unlive) refuse(unlive)
126
- const { buildSidecars, shipSidecars, startSidecars, usesRemote } = await start(root, 'sidecar')
127
- const contracted = () => writeContract({ root, site: config.site, preset, rust })
128
- let warned = []
129
- // Sidecars first: prerendering a share card runs its route's `load`, which may read one.
130
- const running = usesRemote(root) ? await contracted().then(async c => {
131
- const stop = c.diagnostics.find(d => d.severity === 'error')
132
- if (stop) refuse(stop)
133
- const built = await buildSidecars({ root, contract: c.contract, rust, preset })
134
- const failed = built.diagnostics.find(d => d.severity === 'error')
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 ?? {})) 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])
147
182
  }
148
- const contract = await writeContract({ root, site: config.site, preset, rust, live: (await loadConfig(root)).config?.live, verify: true })
149
- for (const refused of contract.diagnostics.filter(d => d.severity === 'error')) refuse(refused)
150
- if (running) shipSidecars(root)
151
- await writeMap(root)
152
- // 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.
153
- const settings = (await loadConfig(root)).config?.build?.audit ?? true
154
- const audited = settings === false
155
- ? { diagnostics: [diagnostic({ code: 'L000', severity: 'warning', file: 'weft.config.ts', why: '`build.audit` is false: the built pages were not audited.' })] }
156
- : await audit(root, { dir: builtPublic(root), ...(typeof settings === 'object' && settings) })
157
- const pages = audited.diagnostics.map(d => (d.severity === 'error' && unsafe.allows(d) ? unsafe.let(d) : d))
158
- const kept = [...contract.diagnostics.filter(d => !(d.severity === 'error' && unsafe.allows(d))), ...warned].map(d => diagnostic({ file: '.weft/contract.json', ...d }))
159
- const diagnostics = [...kept, ...pages, ...forced.map(d => diagnostic({ file: '.weft/contract.json', ...d }))]
160
- 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
+ })
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 = { 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
+ }
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]) 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
+ )
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(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
+ )
181
231
  process.once('SIGINT', () => child.kill('SIGINT'))
182
232
  process.once('SIGTERM', () => child.kill('SIGTERM'))
183
- 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
+ })
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)) 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
+ )
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) 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
+ })
199
263
  const output = layer.generate(root, name)
200
264
  const schema = layer.schema && join(root, layer.schema)
201
- 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
+ )
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) 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
+ )
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') 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` }
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'], { cwd: root, encoding: 'utf8' })
243
- if (r.status !== 0) throw Object.assign(new Error(`drizzle-kit generate failed:\n${r.stdout}${r.stderr}`), { code: 'E_INTERNAL' })
244
- 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
+ )
245
311
  const file = join(layer.migrations, `${entries.at(-1).tag}.sql`)
246
312
  writeFileSync(join(root, file), sql)
247
- 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
+ }
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) 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
+ })
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 => { process.once('SIGINT', ok); process.once('SIGTERM', 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({ root, site: config.site, preset: preset ?? PRESETS[chosen], live: (await loadConfig(root)).config?.live, verify: false })
274
- 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
+ }
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 { ...result, diagnostics: [...config.diagnostics, ...routeDiagnostics(root, result.diagnostics)] }
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, ok: true, problems: manifest.problems,
293
- 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
+ })),
294
392
  diagnostics: config.diagnostics,
295
393
  }
296
394
  }