@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/check.mjs CHANGED
@@ -2,18 +2,25 @@
2
2
  // `check` hooks, the test T-codes, the config P-codes, the style D-codes (style.mjs) and the design D-codes (design-checks.mjs), and keep `.weft/styles.json`
3
3
  // current when the project has style groups.
4
4
 
5
- import { existsSync, readdirSync, readFileSync } from 'node:fs'
5
+ import { existsSync, readFileSync } from 'node:fs'
6
6
  import { createRequire } from 'node:module'
7
7
  import { isAbsolute, join, relative, resolve } from 'node:path'
8
8
  import { pathToFileURL } from 'node:url'
9
- import { diagnostic, locate, positions, sortDiagnostics, summarize } from './diag.mjs'
9
+ import { diagnostic, positions, sortDiagnostics, summarize } from './diag.mjs'
10
10
  import { discover } from './files.mjs'
11
11
  import { isDataConfig, loadConfig, writeConfigSchema } from './load-config.mjs'
12
12
  import { sync } from './plugins.mjs'
13
13
  import { writeMap } from './map.mjs'
14
14
  import { projectView } from './project.mjs'
15
15
  import { designChecks, hasTokens } from './design-checks.mjs'
16
- import { classSoup, matrixDiagnostics, styleFiles, styleModule, usesTailwind, writeMatrixFile } from './style.mjs'
16
+ import {
17
+ classSoup,
18
+ matrixDiagnostics,
19
+ styleFiles,
20
+ styleModule,
21
+ usesTailwind,
22
+ writeMatrixFile,
23
+ } from './style.mjs'
17
24
  import { checkTests, isTestFile, testGlobs } from './tests.mjs'
18
25
  import { pluginChecks } from './plugin-checks.mjs'
19
26
  import { propChecks } from './props-checks.mjs'
@@ -21,34 +28,60 @@ import { reactChecks } from './react-checks.mjs'
21
28
  import { trustDiagnostics } from './trust.mjs'
22
29
  import { shareChecks } from './share-checks.mjs'
23
30
  import { stateChecks } from './state-checks.mjs'
31
+ import { serverStateChecks } from './server-state-checks.mjs'
24
32
  import { surfaceChecks } from './surface-checks.mjs'
25
33
  import { textChecks } from './text-checks.mjs'
26
34
  import { loadCompiler, resolveWeft } from './weft.mjs'
27
35
  import { dbLayer } from './db-layer.mjs'
28
- import { routeDiagnostics, schemaHash, siteOf, start } from './start.mjs'
36
+ import { loadEnv, routeDiagnostics, schemaHash, siteOf, start } from './start.mjs'
37
+ import { deployChecks } from './deploy/index.mjs'
38
+ import { rustChecks, liveChecks, socketChecks, hygiene } from './check/runtime.mjs'
29
39
 
30
- const posix = p => p.split('\\').join('/')
40
+ const posix = (p) => p.split('\\').join('/')
31
41
 
32
42
  /** Q015: the schema's hash against the one `weft db generate` recorded (research 28 §2). */
33
43
  function schemaDrift(root, layer) {
34
44
  const schema = layer?.schema && join(root, layer.schema)
35
45
  if (!schema || !existsSync(schema)) return []
36
46
  const file = join(root, layer.migrations, '.schema-hash')
37
- if (existsSync(file) && readFileSync(file, 'utf8').trim() === schemaHash(readFileSync(schema))) return []
38
- return [diagnostic({ code: 'Q015', file: layer.schema, why: existsSync(file) ? 'The schema changed since the last `weft db generate`: the database will not match it.' : 'No migrations yet: the first request that reads a table fails with Q014.' })]
47
+ if (existsSync(file) && readFileSync(file, 'utf8').trim() === schemaHash(readFileSync(schema)))
48
+ return []
49
+ return [
50
+ diagnostic({
51
+ code: 'Q015',
52
+ file: layer.schema,
53
+ why: existsSync(file)
54
+ ? 'The schema changed since the last `weft db generate`: the database will not match it.'
55
+ : 'No migrations yet: the first request that reads a table fails with Q014.',
56
+ }),
57
+ ]
39
58
  }
40
59
 
41
60
  /** I001–I003: the walk a build does (@weft-js/warp-icons/compile), when the project has the package. */
42
61
  async function iconChecks(root) {
43
62
  const require = createRequire(join(root, 'package.json'))
44
63
  let compile
45
- for (const from of [() => require.resolve('@weft-js/warp-icons/compile'), () => createRequire(require.resolve('@weft-js/warp'))
46
- .resolve('@weft-js/warp-icons/compile')]) {
47
- try { compile = from(); break } catch { /* not this way */ }
64
+ for (const from of [
65
+ () => require.resolve('@weft-js/warp-icons/compile'),
66
+ () => createRequire(require.resolve('@weft-js/warp')).resolve('@weft-js/warp-icons/compile'),
67
+ ]) {
68
+ try {
69
+ compile = from()
70
+ break
71
+ } catch {
72
+ /* not this way */
73
+ }
48
74
  }
49
75
  if (!compile) return []
50
76
  const { findings } = await import(pathToFileURL(compile).href)
51
- return findings(root).map(f => diagnostic({ code: f.code, file: f.file, span: [f.line, f.column, f.line, f.column], why: f.why }))
77
+ return findings(root).map((f) =>
78
+ diagnostic({
79
+ code: f.code,
80
+ file: f.file,
81
+ span: [f.line, f.column, f.line, f.column],
82
+ why: f.why,
83
+ }),
84
+ )
52
85
  }
53
86
 
54
87
  /** H010: what the last build or `weft dev` found keeps an island from waiting (`.weft/hydration.json`, @weft-js/core/hydration). */
@@ -56,7 +89,17 @@ function hydrationChecks(root) {
56
89
  const file = join(root, '.weft', 'hydration.json')
57
90
  if (!existsSync(file)) return []
58
91
  try {
59
- return JSON.parse(readFileSync(file, 'utf8')).diagnostics.filter(d => existsSync(join(root, d.file))).map(d => diagnostic({ code: d.code, file: d.file, span: [d.line, 1, d.line, 1], why: d.why, help: d.fix }))
92
+ return JSON.parse(readFileSync(file, 'utf8'))
93
+ .diagnostics.filter((d) => existsSync(join(root, d.file)))
94
+ .map((d) =>
95
+ diagnostic({
96
+ code: d.code,
97
+ file: d.file,
98
+ span: [d.line, 1, d.line, 1],
99
+ why: d.why,
100
+ help: d.fix,
101
+ }),
102
+ )
60
103
  } catch {
61
104
  return []
62
105
  }
@@ -67,46 +110,40 @@ function hydrationChecks(root) {
67
110
  * when a route is among them. An app that can't run here is still read: a check never fails for what it couldn't start.
68
111
  */
69
112
  async function routeChecks(root, loaded, given, reported) {
70
- if (!existsSync(join(root, 'src', 'routes')) || loaded.config?.check?.routes === false || loaded.diagnostics.some(d => d.code === 'P007')) return []
71
- if (given?.length && !given.some(f => f.startsWith('src/routes/'))) return []
113
+ if (
114
+ !existsSync(join(root, 'src', 'routes')) ||
115
+ loaded.config?.check?.routes === false ||
116
+ loaded.diagnostics.some((d) => d.code === 'P007')
117
+ )
118
+ return []
119
+ if (given?.length && !given.some((f) => f.startsWith('src/routes/'))) return []
72
120
  let inspect
73
- try { inspect = await start(root, 'inspect') } catch { return [] }
74
- const keep = list => routeDiagnostics(root, list).filter(d => !(d.code === 'H001' && reported.has('H001')))
121
+ // The routes run as they do under `weft dev`, so they get the same `.env` (an env schema throws without it).
122
+ loadEnv(root)
123
+ try {
124
+ inspect = await start(root, 'inspect')
125
+ } catch {
126
+ return []
127
+ }
128
+ const keep = (list) =>
129
+ routeDiagnostics(root, list).filter((d) => !(d.code === 'H001' && reported.has('H001')))
75
130
  try {
76
131
  return keep((await inspect.routesMeta({ root, site: siteOf(loaded) })).diagnostics)
77
132
  } catch (error) {
78
- return keep([...inspect.staticRouteChecks(root), { code: 'H016', severity: 'info', file: 'src/routes', route: '*', why: `The routes didn't load here, so only their source was checked: ${String(error?.message ?? error).split('\n')[0]}` }])
133
+ return keep([
134
+ ...inspect.staticRouteChecks(root),
135
+ {
136
+ code: 'H016',
137
+ severity: 'info',
138
+ file: 'src/routes',
139
+ route: '*',
140
+ why: `The routes didn't load here, so only their source was checked: ${String(error?.message ?? error).split('\n')[0]}`,
141
+ },
142
+ ])
79
143
  }
80
144
  }
81
145
 
82
- /**
83
- * @param {string} root
84
- * @param {{ files?: string[] }} [options] files relative to `root` (or absolute); default: discovered
85
- */
86
- export async function check(root, options = {}) {
87
- const started = performance.now()
88
- root = resolve(root)
89
- const weft = resolveWeft(root)
90
- const compiler = loadCompiler(weft)
91
- const loaded = await loadConfig(root)
92
- const { config } = loaded
93
- // Slot modules and plugin token files stay current with the config (research 13 §2.6).
94
- if (!loaded.diagnostics.some(d => d.code === 'P007') && (config.plugins?.length || existsSync(join(root, '.weft', 'slots')))) sync(root, loaded)
95
-
96
- if (loaded.file && isDataConfig(loaded.file)) writeConfigSchema(root)
97
- if (existsSync(join(root, '.weft'))) await writeMap(root)
98
-
99
- const exclude = [...(config.check?.exclude ?? []), ...(config.scope?.forbid ?? [])]
100
- const given = options.files?.map(f => posix(relative(root, isAbsolute(f) ? f : join(root, f))))
101
- const include = config.check?.include ?? ['**/*.{tsx,jsx}']
102
- const rels = given?.length ? given.filter(f => /\.[jt]sx$/.test(f) || !isTestFile(f)) : discover(root, { include, exclude })
103
- const testRels = given?.length ? given.filter(isTestFile) : discover(root, { include: config.test?.include ?? testGlobs, exclude })
104
-
105
- // Style groups (research 23): the project's definitions first, so each file compiles with the groups it imports.
106
- const style = await styleModule(weft)
107
- const sfiles = style ? styleFiles(root, config) : []
108
- const scan = sfiles.some(f => readFileSync(join(root, f), 'utf8').includes('@weft-js/core/style')) ? style.scanProject(root, sfiles, (...a) => compiler.transform(...a)) : null
109
-
146
+ function compileFiles(root, rels, compiler, scan, style) {
110
147
  const files = []
111
148
  const posByAbs = new Map()
112
149
  for (const rel of rels) {
@@ -114,40 +151,69 @@ export async function check(root, options = {}) {
114
151
  const text = readFileSync(abs, 'utf8')
115
152
  const pos = positions(text)
116
153
  posByAbs.set(abs, pos)
117
- const groups = scan ? style.importedGroups(abs, text, f => scan.groups.filter(g => join(root, g.file) === f)) : {}
118
- const out = compiler.transform(text, Object.keys(groups).length ? { filename: abs, checkOnly: true, groups } : { filename: rel, checkOnly: true })
119
- const diagnostics = out.diagnostics.map(d => {
154
+ const groups = scan
155
+ ? style.importedGroups(abs, text, (f) => scan.groups.filter((g) => join(root, g.file) === f))
156
+ : {}
157
+ const out = compiler.transform(
158
+ text,
159
+ Object.keys(groups).length
160
+ ? { filename: abs, checkOnly: true, groups }
161
+ : { filename: rel, checkOnly: true },
162
+ )
163
+ const diagnostics = out.diagnostics.map((d) => {
120
164
  const from = pos.fromByte(d.start)
121
165
  const end = pos.fromByte(d.end)
122
166
  const fix = d.fix && {
123
167
  description: d.fix.description,
124
168
  safe: d.fix.safe,
125
- edits: d.fix.edits.map(e => {
169
+ edits: d.fix.edits.map((e) => {
126
170
  const a = pos.fromByte(e.start)
127
171
  const b = pos.fromByte(e.end)
128
172
  return { file: rel, span: pos.span(a, b), offset: [a, b], text: e.text }
129
173
  }),
130
174
  }
131
- return diagnostic({ code: d.code, severity: d.severity, file: rel, span: pos.span(from, end), offset: [from, end], why: d.message, help: d.help, fix })
175
+ return diagnostic({
176
+ code: d.code,
177
+ severity: d.severity,
178
+ file: rel,
179
+ span: pos.span(from, end),
180
+ offset: [from, end],
181
+ why: d.message,
182
+ help: d.help,
183
+ fix,
184
+ })
132
185
  })
133
186
  files.push({ abs, rel, text, diagnostics })
134
187
  }
135
188
 
136
- projectView(files, file => posByAbs.get(file.abs))
137
-
138
- const matrix = scan && style.buildMatrix(scan.groups, scan.uses)
139
- if (matrix && scan.groups.length) writeMatrixFile(root, matrix)
140
- const styles = [...(usesTailwind(root) ? [] : classSoup(files.filter(f => /\.[jt]sx$/.test(f.rel)))), ...(matrix ? matrixDiagnostics(root, matrix) : [])]
189
+ return { files, posByAbs }
190
+ }
141
191
 
142
- const fontBuild = typeof loaded.slots['font.build'] === 'string' && !loaded.slots['font.build'].startsWith('weft:default')
192
+ async function collectDiagnostics({
193
+ root,
194
+ loaded,
195
+ config,
196
+ files,
197
+ exclude,
198
+ styles,
199
+ testRels,
200
+ options,
201
+ given,
202
+ }) {
203
+ const fontBuild =
204
+ typeof loaded.slots['font.build'] === 'string' &&
205
+ !loaded.slots['font.build'].startsWith('weft:default')
143
206
  const diagnostics = [
144
207
  ...loaded.diagnostics,
145
- ...(loaded.diagnostics.some(d => d.code === 'P007') ? [] : trustDiagnostics(root, loaded)),
146
- ...files.flatMap(f => f.diagnostics),
208
+ ...(loaded.diagnostics.some((d) => d.code === 'P007') ? [] : trustDiagnostics(root, loaded)),
209
+ ...files.flatMap((f) => f.diagnostics),
147
210
  ...styles,
148
- ...(config.check?.design === false ? [] : designChecks(root, { files, exclude, tokens: hasTokens(root) })),
211
+ ...(config.check?.design === false
212
+ ? []
213
+ : designChecks(root, { files, exclude, tokens: hasTokens(root) })),
149
214
  ...textChecks(root, { files, exclude, fontBuild }),
150
215
  ...stateChecks(root, { files, exclude }),
216
+ ...serverStateChecks(root, { files, exclude }),
151
217
  ...shareChecks(root, { files }),
152
218
  ...reactChecks({ files }),
153
219
  ...surfaceChecks(root, { files, exclude, slots: loaded.slots }),
@@ -161,9 +227,79 @@ export async function check(root, options = {}) {
161
227
  ...schemaDrift(root, await dbLayer(root, loaded)),
162
228
  ...(await rustChecks(root, loaded)),
163
229
  ...(await liveChecks(root, loaded, options.preset)),
230
+ ...(loaded.diagnostics.some((d) => d.code === 'P007')
231
+ ? []
232
+ : deployChecks(root, config, { preset: options.preset })),
164
233
  ]
165
- diagnostics.push(...await routeChecks(root, loaded, given, new Set(loaded.diagnostics.map(d => d.code))))
234
+ diagnostics.push(
235
+ ...(await routeChecks(root, loaded, given, new Set(loaded.diagnostics.map((d) => d.code)))),
236
+ )
166
237
  sortDiagnostics(diagnostics)
238
+ return diagnostics
239
+ }
240
+
241
+ /**
242
+ * @param {string} root
243
+ * @param {{ files?: string[] }} [options] files relative to `root` (or absolute); default: discovered
244
+ */
245
+ export async function check(root, options = {}) {
246
+ const started = performance.now()
247
+ root = resolve(root)
248
+ const weft = resolveWeft(root)
249
+ const compiler = loadCompiler(weft)
250
+ const loaded = await loadConfig(root)
251
+ const { config } = loaded
252
+ // Slot modules and plugin token files stay current with the config (research 13 §2.6).
253
+ if (
254
+ !loaded.diagnostics.some((d) => d.code === 'P007') &&
255
+ (config.plugins?.length || existsSync(join(root, '.weft', 'slots')))
256
+ )
257
+ sync(root, loaded)
258
+
259
+ if (loaded.file && isDataConfig(loaded.file)) writeConfigSchema(root)
260
+ if (existsSync(join(root, '.weft'))) await writeMap(root)
261
+
262
+ const exclude = [...(config.check?.exclude ?? []), ...(config.scope?.forbid ?? [])]
263
+ const given = options.files?.map((f) => posix(relative(root, isAbsolute(f) ? f : join(root, f))))
264
+ const include = config.check?.include ?? ['**/*.{tsx,jsx}']
265
+ const rels = given?.length
266
+ ? given.filter((f) => /\.[jt]sx$/.test(f) || !isTestFile(f))
267
+ : discover(root, { include, exclude })
268
+ const testRels = given?.length
269
+ ? given.filter(isTestFile)
270
+ : discover(root, { include: config.test?.include ?? testGlobs, exclude })
271
+
272
+ // Style groups (research 23): the project's definitions first, so each file compiles with the groups it imports.
273
+ const style = await styleModule(weft)
274
+ const sfiles = style ? styleFiles(root, config) : []
275
+ const scan = sfiles.some((f) =>
276
+ readFileSync(join(root, f), 'utf8').includes('@weft-js/core/style'),
277
+ )
278
+ ? style.scanProject(root, sfiles, (...a) => compiler.transform(...a))
279
+ : null
280
+
281
+ const { files, posByAbs } = compileFiles(root, rels, compiler, scan, style)
282
+
283
+ projectView(files, (file) => posByAbs.get(file.abs))
284
+
285
+ const matrix = scan && style.buildMatrix(scan.groups, scan.uses)
286
+ if (matrix && scan.groups.length) writeMatrixFile(root, matrix)
287
+ const styles = [
288
+ ...(usesTailwind(root) ? [] : classSoup(files.filter((f) => /\.[jt]sx$/.test(f.rel)))),
289
+ ...(matrix ? matrixDiagnostics(root, matrix) : []),
290
+ ]
291
+
292
+ const diagnostics = await collectDiagnostics({
293
+ root,
294
+ loaded,
295
+ config,
296
+ files,
297
+ exclude,
298
+ styles,
299
+ testRels,
300
+ options,
301
+ given,
302
+ })
167
303
  const summary = { files: files.length, tests: testRels.length, ...summarize(diagnostics) }
168
304
  return {
169
305
  version: 1,
@@ -174,155 +310,11 @@ export async function check(root, options = {}) {
174
310
  summary,
175
311
  diagnostics,
176
312
  slots: loaded.slots,
177
- styles: matrix ? { groups: Object.keys(matrix.groups).length, matrix: '.weft/styles.json' } : null,
313
+ styles: matrix
314
+ ? { groups: Object.keys(matrix.groups).length, matrix: '.weft/styles.json' }
315
+ : null,
178
316
  files: rels,
179
317
  tests: testRels,
180
318
  ms: Math.round(performance.now() - started),
181
319
  }
182
320
  }
183
-
184
- /**
185
- * R101, R102 from the contract, and R103, R109 from `cargo check`, for an app with a sidecar: an agent gets the same
186
- * answer here as from `weft build`, with the function's id on it.
187
- */
188
- async function rustChecks(root, loaded) {
189
- let sidecar
190
- try { sidecar = await start(root, 'sidecar') } catch { return [] }
191
- if (!sidecar.usesRemote(root)) return []
192
- const rust = loaded.config?.rust
193
- const held = process.env.WEFT_BUILD
194
- process.env.WEFT_BUILD = '1'
195
- try {
196
- const { writeContract } = await start(root, 'contract')
197
- const preset = { node: 'node', bun: 'bun', deno: 'deno_server' }[loaded.config?.runtime ?? 'node']
198
- const { contract, diagnostics } = await writeContract({ root, site: loaded.config?.site, rust, preset })
199
- const out = diagnostics.filter(d => d.code.startsWith('R1')).map(d => diagnostic({ ...d, file: '.weft/contract.json' }))
200
- if (out.some(d => d.severity === 'error')) return out
201
- for (const service of Object.keys(contract.services ?? {})) {
202
- if (rust?.services?.[service]?.external) continue
203
- const dir = sidecar.crateDir(root, rust, service)
204
- if (!existsSync(dir)) {
205
- out.push(diagnostic({ code: 'R109', file: '.weft/contract.json', why: `\`rust://${service}\` has no crate at \`${posix(relative(root, dir))}\`.` }))
206
- continue
207
- }
208
- const checked = await sidecar.cargo('check', { root, dir, service, contract })
209
- out.push(...checked.diagnostics.map(({ rendered, ...d }) => diagnostic({ ...d, help: rendered })))
210
- out.push(...untouchedQueries(root, dir, contract, service))
211
- if (contract.services[service].kind !== 'native' || !checked.ok) continue
212
- const wasm = await sidecar.checkWasm({ root, dir, service, contract, preset, rust })
213
- out.push(...wasm.diagnostics.map(d => diagnostic(d)))
214
- }
215
- return out
216
- } catch (e) {
217
- return [diagnostic({ code: 'R109', severity: 'warning', file: '.weft/contract.json', why: `the contract couldn't be written, so the sidecars weren't checked: ${e.message.split('\n')[0]}` })]
218
- } finally {
219
- if (held === undefined) delete process.env.WEFT_BUILD
220
- else process.env.WEFT_BUILD = held
221
- }
222
- }
223
-
224
- /** R105 before anything runs: a cached query with tags whose Rust never mentions `touch`, or hands `ctx` to anything. */
225
- function untouchedQueries(root, dir, contract, service) {
226
- const sources = readdirSync(join(dir, 'src'), { recursive: true, withFileTypes: true }).filter(f => f.isFile() && f.name.endsWith('.rs')).map(f => join(f.parentPath, f.name))
227
- const out = []
228
- for (const f of contract.functions) {
229
- if (f.impl?.service !== service || !f.cache || !(f.tags === 'dynamic' || f.tags?.length)) continue
230
- const method = f.name.replace(/[A-Z]/g, (c, i) => `${i ? '_' : ''}${c.toLowerCase()}`)
231
- for (const file of sources) {
232
- const text = readFileSync(file, 'utf8')
233
- const at = text.search(new RegExp(`\\bfn\\s+${method}\\s*\\(`))
234
- if (at < 0) continue
235
- const open = text.indexOf('{', at)
236
- let depth = 0, end = open
237
- for (; end < text.length; end++) {
238
- if (text[end] === '{') depth++
239
- if (text[end] === '}' && --depth === 0) break
240
- }
241
- const body = text.slice(open, end)
242
- const line = text.slice(0, at).split('\n').length
243
- if (!/\.touch\(|\(\s*&?ctx\b|,\s*&?ctx\b/.test(body)) out.push(diagnostic({ code: 'R105', severity: 'warning', file: posix(relative(root, file)), span: [line, 1, line, 1], why: `\`${f.id}\` is cached under tags and reports no table: nothing can tell whether they cover what it reads. Call \`ctx.touch("table")\`, or drop \`tags\` if it reads none.` }))
244
- break
245
- }
246
- }
247
- return out
248
- }
249
-
250
- /** R013, R014 and R018: what the deploy target does to live data. Only an app with `changes` or `required` loads anything. */
251
- async function liveChecks(root, loaded, preset) {
252
- const file = join(root, 'src/realtime.server.ts')
253
- let target
254
- try { target = await start(root, 'live-target') } catch { return [] }
255
- const required = target.scanRequired(root)
256
- if (!required.length && !target.literalSecrets(root).length && !(existsSync(file) && /\bchanges\s*[:,}]/.test(readFileSync(file, 'utf8')))) return []
257
- const held = process.env.WEFT_BUILD
258
- process.env.WEFT_BUILD = '1'
259
- try {
260
- const { readLivePlan } = await start(root, 'contract')
261
- const plan = await readLivePlan({ root, site: loaded.config?.site, live: loaded.config?.live, preset: preset ?? { node: 'node', bun: 'bun', deno: 'deno_server' }[loaded.config?.runtime ?? 'node'] })
262
- return plan.diagnostics.map(d => diagnostic({ ...d, file: d.file ?? required[0]?.file ?? 'src/realtime.server.ts' }))
263
- } catch (e) {
264
- return [diagnostic({ code: 'R014', severity: 'warning', file: 'src/realtime.server.ts', why: `the deploy target couldn't be checked: ${e.message.split('\n')[0]}` })]
265
- } finally {
266
- if (held === undefined) delete process.env.WEFT_BUILD
267
- else process.env.WEFT_BUILD = held
268
- }
269
- }
270
-
271
- /** R001 for a server module's `channel`, and R004 for a socket handler of the app's own (research 32 §1). */
272
- async function socketChecks(root) {
273
- const src = join(root, 'src')
274
- if (!existsSync(src)) return []
275
- const out = []
276
- let withIds
277
- for (const f of readdirSync(src, { recursive: true, withFileTypes: true })) {
278
- if (!f.isFile() || !/\.[cm]?[jt]sx?$/.test(f.name) || f.name.endsWith('.d.ts')) continue
279
- const abs = join(f.parentPath, f.name), rel = relative(root, abs).replaceAll('\\', '/'), text = readFileSync(abs, 'utf8')
280
- const hand = /from\s+['"]crossws|\bdefineWebSocketHandler\b/.exec(text)
281
- if (hand) out.push(diagnostic({ code: 'R004', file: rel, ...locate(text, hand[0]), why: 'A WebSocket handler of its own skips the Origin check, `authorize`, the limits and resume. Use `channel`.' }))
282
- if (!/\b(channel|sharedState)\s*[(<]/.test(text) || !/^src\/server\/|\.server\.[cm]?[jt]sx?$/.test(rel)) continue
283
- if (withIds === undefined) {
284
- try { withIds = (await import(pathToFileURL(createRequire(join(root, 'package.json')).resolve('@weft-js/start/vite')).href)).withIds } catch { withIds = null }
285
- }
286
- try { withIds?.(text, abs, rel) } catch (e) {
287
- if (e.code !== 'R001') continue
288
- const line = +/:(\d+) /.exec(e.message)?.[1]
289
- out.push(diagnostic({ code: 'R001', file: rel, span: [line, 1, line, 1], why: e.message.split('\n')[0].replace(/^R001: \S+ /, '') }))
290
- }
291
- }
292
- return out
293
- }
294
-
295
- /** weft owns the connection: these bypass reconnects, resume and the fake transport (research 30 §8). */
296
- const SOCKET_PACKAGES = ['ws', 'socket.io-client', 'reconnecting-websocket', '@microsoft/fetch-event-source']
297
-
298
- /** P002 (`package.json` scripts that aren't `weft …`), P120 (weft plugins wired straight into Vite), R007 (socket libraries). */
299
- function hygiene(root) {
300
- const out = []
301
- const pkgFile = join(root, 'package.json')
302
- if (existsSync(pkgFile)) {
303
- const text = readFileSync(pkgFile, 'utf8')
304
- let scripts = {}, deps = {}
305
- try {
306
- const pkg = JSON.parse(text)
307
- scripts = pkg.scripts ?? {}
308
- deps = pkg.dependencies ?? {}
309
- } catch {}
310
- for (const name of SOCKET_PACKAGES) {
311
- if (deps[name]) out.push(diagnostic({ code: 'R007', file: 'package.json', ...locate(text, `"${name}"`), why: `\`${name}\` is a dependency: its connection has no resume, no cleanup and no fake in @weft-js/core/test. Use \`stream\`, \`.live()\` or \`channel\`.` }))
312
- }
313
- for (const [name, cmd] of Object.entries(scripts)) {
314
- if (!/^weft(\s|$)/.test(cmd.trim())) {
315
- out.push(diagnostic({ code: 'P002', file: 'package.json', ...locate(text, `"${name}"`), why: `Script \`${name}\` runs \`${cmd}\`, not \`weft …\`: agents can't see what it does or what it touches.` }))
316
- }
317
- }
318
- }
319
- for (const name of ['vite.config.ts', 'vite.config.mts', 'vite.config.js', 'vite.config.mjs']) {
320
- const file = join(root, name)
321
- if (!existsSync(file)) continue
322
- const text = readFileSync(file, 'utf8')
323
- for (const m of text.matchAll(/from\s+['"](@weft-js\/(?:plugin-[\w-]+|text))/g)) {
324
- out.push(diagnostic({ code: 'P120', file: name, ...locate(text, m[1]), why: `\`${m[1]}\` is registered in ${name}, which bypasses weft.config.ts.` }))
325
- }
326
- }
327
- return out
328
- }