@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/project.mjs CHANGED
@@ -9,15 +9,30 @@ import { relative } from 'node:path'
9
9
  import ts from 'typescript'
10
10
  import { local } from './alias.mjs'
11
11
 
12
- const exts = ['', '.tsx', '.ts', '.jsx', '.js', '/index.tsx', '/index.ts', '/index.jsx', '/index.js']
13
- const isComponentName = name => /^[A-Z]/.test(name)
12
+ const exts = [
13
+ '',
14
+ '.tsx',
15
+ '.ts',
16
+ '.jsx',
17
+ '.js',
18
+ '/index.tsx',
19
+ '/index.ts',
20
+ '/index.jsx',
21
+ '/index.js',
22
+ ]
23
+ const isComponentName = (name) => /^[A-Z]/.test(name)
14
24
 
15
25
  /** `a.b.c(…).d` → ['a', 'b']: the root identifier and the first member after it. */
16
26
  function chainRoot(expr) {
17
27
  let node = expr
18
28
  let member
19
29
  for (;;) {
20
- if (ts.isParenthesizedExpression(node) || ts.isNonNullExpression(node) || ts.isAsExpression(node)) node = node.expression
30
+ if (
31
+ ts.isParenthesizedExpression(node) ||
32
+ ts.isNonNullExpression(node) ||
33
+ ts.isAsExpression(node)
34
+ )
35
+ node = node.expression
21
36
  else if (ts.isPropertyAccessExpression(node)) {
22
37
  member = node.name.text
23
38
  node = node.expression
@@ -37,9 +52,11 @@ function isInJsx(node, stop) {
37
52
  /** Identifiers an expression reads (not property names); `props.x` is kept as 'props.x'. */
38
53
  function identifiersIn(node) {
39
54
  const names = new Set()
40
- const visit = n => {
41
- if (ts.isPropertyAccessExpression(n) && ts.isIdentifier(n.expression)) names.add(`${n.expression.text}.${n.name.text}`)
42
- if (ts.isIdentifier(n) && !(ts.isPropertyAccessExpression(n.parent) && n.parent.name === n)) names.add(n.text)
55
+ const visit = (n) => {
56
+ if (ts.isPropertyAccessExpression(n) && ts.isIdentifier(n.expression))
57
+ names.add(`${n.expression.text}.${n.name.text}`)
58
+ if (ts.isIdentifier(n) && !(ts.isPropertyAccessExpression(n.parent) && n.parent.name === n))
59
+ names.add(n.text)
43
60
  ts.forEachChild(n, visit)
44
61
  }
45
62
  visit(node)
@@ -55,44 +72,87 @@ function componentsOf(sf) {
55
72
  if (param && ts.isObjectBindingPattern(param.name)) {
56
73
  for (const el of param.name.elements) {
57
74
  if (el.dotDotDotToken || !ts.isIdentifier(el.name)) continue
58
- const key = el.propertyName && ts.isIdentifier(el.propertyName) ? el.propertyName.text : el.name.text
75
+ const key =
76
+ el.propertyName && ts.isIdentifier(el.propertyName) ? el.propertyName.text : el.name.text
59
77
  props.set(el.name.text, key)
60
78
  }
61
79
  } else if (param && ts.isIdentifier(param.name)) propsIdent = param.name.text
62
80
  comps.push({ name, fn, props, propsIdent, derives: new Map(), lists: [], edges: [] })
63
81
  }
64
82
  for (const st of sf.statements) {
65
- if (ts.isFunctionDeclaration(st) && st.name && isComponentName(st.name.text)) add(st.name.text, st)
66
- if (ts.isFunctionDeclaration(st) && !st.name && st.modifiers?.some(m => m.kind === ts.SyntaxKind.DefaultKeyword)) add('default', st)
83
+ if (ts.isFunctionDeclaration(st) && st.name && isComponentName(st.name.text))
84
+ add(st.name.text, st)
85
+ if (
86
+ ts.isFunctionDeclaration(st) &&
87
+ !st.name &&
88
+ st.modifiers?.some((m) => m.kind === ts.SyntaxKind.DefaultKeyword)
89
+ )
90
+ add('default', st)
67
91
  if (ts.isVariableStatement(st)) {
68
92
  for (const d of st.declarationList.declarations) {
69
- if (ts.isIdentifier(d.name) && isComponentName(d.name.text) && d.initializer && (ts.isArrowFunction(d.initializer) || ts.isFunctionExpression(d.initializer))) add(d.name.text, d.initializer)
93
+ if (
94
+ ts.isIdentifier(d.name) &&
95
+ isComponentName(d.name.text) &&
96
+ d.initializer &&
97
+ (ts.isArrowFunction(d.initializer) || ts.isFunctionExpression(d.initializer))
98
+ )
99
+ add(d.name.text, d.initializer)
70
100
  }
71
101
  }
72
102
  }
73
- for (const c of comps) {
74
- const visit = n => {
75
- if (ts.isVariableDeclaration(n) && ts.isIdentifier(n.name) && n.initializer && ts.isCallExpression(n.initializer)) {
76
- const callee = n.initializer.expression.getText()
77
- if (callee === 'derived' || callee === 'state.linked') c.derives.set(n.name.text, identifiersIn(n.initializer))
78
- }
79
- if (ts.isCallExpression(n) && ts.isPropertyAccessExpression(n.expression) && n.expression.name.text === 'map' && isInJsx(n, c.fn)) {
80
- const cb = n.arguments[0]
81
- const root = chainRoot(n.expression.expression)
82
- if (root && cb && (ts.isArrowFunction(cb) || ts.isFunctionExpression(cb))) c.lists.push({ root, at: n.getStart() })
83
- }
84
- if ((ts.isJsxSelfClosingElement(n) || ts.isJsxOpeningElement(n)) && ts.isIdentifier(n.tagName) && isComponentName(n.tagName.text)) {
85
- for (const attr of n.attributes.properties) {
86
- if (!ts.isJsxAttribute(attr) || !attr.initializer || !ts.isJsxExpression(attr.initializer) || !attr.initializer.expression) continue
87
- const root = chainRoot(attr.initializer.expression)
88
- if (root) c.edges.push({ tag: n.tagName.text, prop: attr.name.getText(), root, at: attr.getStart() })
89
- }
103
+ for (const c of comps) analyzeComponent(c)
104
+ return comps
105
+ }
106
+
107
+ function analyzeComponent(c) {
108
+ const visit = (n) => {
109
+ if (
110
+ ts.isVariableDeclaration(n) &&
111
+ ts.isIdentifier(n.name) &&
112
+ n.initializer &&
113
+ ts.isCallExpression(n.initializer)
114
+ ) {
115
+ const callee = n.initializer.expression.getText()
116
+ if (callee === 'derived' || callee === 'state.linked')
117
+ c.derives.set(n.name.text, identifiersIn(n.initializer))
118
+ }
119
+ if (
120
+ ts.isCallExpression(n) &&
121
+ ts.isPropertyAccessExpression(n.expression) &&
122
+ n.expression.name.text === 'map' &&
123
+ isInJsx(n, c.fn)
124
+ ) {
125
+ const cb = n.arguments[0]
126
+ const root = chainRoot(n.expression.expression)
127
+ if (root && cb && (ts.isArrowFunction(cb) || ts.isFunctionExpression(cb)))
128
+ c.lists.push({ root, at: n.getStart() })
129
+ }
130
+ if (
131
+ (ts.isJsxSelfClosingElement(n) || ts.isJsxOpeningElement(n)) &&
132
+ ts.isIdentifier(n.tagName) &&
133
+ isComponentName(n.tagName.text)
134
+ ) {
135
+ for (const attr of n.attributes.properties) {
136
+ if (
137
+ !ts.isJsxAttribute(attr) ||
138
+ !attr.initializer ||
139
+ !ts.isJsxExpression(attr.initializer) ||
140
+ !attr.initializer.expression
141
+ )
142
+ continue
143
+ const root = chainRoot(attr.initializer.expression)
144
+ if (root)
145
+ c.edges.push({
146
+ tag: n.tagName.text,
147
+ prop: attr.name.getText(),
148
+ root,
149
+ at: attr.getStart(),
150
+ })
90
151
  }
91
- ts.forEachChild(n, visit)
92
152
  }
93
- visit(c.fn.body ?? c.fn)
153
+ ts.forEachChild(n, visit)
94
154
  }
95
- return comps
155
+ visit(c.fn.body ?? c.fn)
96
156
  }
97
157
 
98
158
  /**
@@ -101,31 +161,17 @@ function componentsOf(sf) {
101
161
  */
102
162
  export function projectView(files, pos) {
103
163
  // Only a W014 warning needs the project view, so most runs parse nothing here.
104
- if (!files.some(f => f.diagnostics.some(d => d.code === 'W014' && d.severity === 'warning'))) return
105
- const byAbs = new Map(files.map(f => [f.abs, f]))
106
- for (const f of files) {
107
- f.sf = ts.createSourceFile(f.abs, f.text, ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX)
108
- f.comps = componentsOf(f.sf)
109
- f.imports = new Map()
110
- for (const st of f.sf.statements) {
111
- if (!ts.isImportDeclaration(st) || !st.importClause || !ts.isStringLiteral(st.moduleSpecifier)) continue
112
- const spec = st.moduleSpecifier.text
113
- const base = local(f.abs, spec)
114
- if (!base) continue
115
- const target = exts.map(e => base + e).find(p => byAbs.has(p))
116
- if (!target) continue
117
- if (st.importClause.name) f.imports.set(st.importClause.name.text, { file: target, name: 'default' })
118
- const named = st.importClause.namedBindings
119
- if (named && ts.isNamedImports(named)) for (const el of named.elements) f.imports.set(el.name.text, { file: target, name: (el.propertyName ?? el.name).text })
120
- }
121
- }
164
+ if (!files.some((f) => f.diagnostics.some((d) => d.code === 'W014' && d.severity === 'warning')))
165
+ return
166
+ const byAbs = new Map(files.map((f) => [f.abs, f]))
167
+ parseProject(files, byAbs)
122
168
 
123
169
  const lookup = (file, tag) => {
124
- const own = file.comps.find(c => c.name === tag)
170
+ const own = file.comps.find((c) => c.name === tag)
125
171
  if (own) return { file, comp: own }
126
172
  const imp = file.imports.get(tag)
127
173
  const target = imp && byAbs.get(imp.file)
128
- const comp = target?.comps.find(c => c.name === imp.name)
174
+ const comp = target?.comps.find((c) => c.name === imp.name)
129
175
  return comp ? { file: target, comp } : null
130
176
  }
131
177
 
@@ -136,7 +182,8 @@ export function projectView(files, pos) {
136
182
  if (comp.props.has(name)) return new Set([comp.props.get(name)])
137
183
  if (name === comp.propsIdent && member) return new Set([member])
138
184
  const out = new Set()
139
- for (const dep of comp.derives.get(name) ?? []) for (const p of propsRead(comp, dep.split('.'), seen)) out.add(p)
185
+ for (const dep of comp.derives.get(name) ?? [])
186
+ for (const p of propsRead(comp, dep.split('.'), seen)) out.add(p)
140
187
  return out
141
188
  }
142
189
 
@@ -145,12 +192,14 @@ export function projectView(files, pos) {
145
192
  const id = `${file.abs}#${comp.name}#${prop}`
146
193
  if (seen.has(id)) return null
147
194
  seen.add(id)
148
- for (const list of comp.lists) if (propsRead(comp, list.root).has(prop)) return { file, at: list.at, via: [] }
195
+ for (const list of comp.lists)
196
+ if (propsRead(comp, list.root).has(prop)) return { file, at: list.at, via: [] }
149
197
  for (const edge of comp.edges) {
150
198
  if (!propsRead(comp, edge.root).has(prop)) continue
151
199
  const next = lookup(file, edge.tag)
152
200
  const hit = next && reachesList(next.file, next.comp, edge.prop, seen)
153
- if (hit) return { ...hit, via: [{ file, at: edge.at, tag: edge.tag, prop: edge.prop }, ...hit.via] }
201
+ if (hit)
202
+ return { ...hit, via: [{ file, at: edge.at, tag: edge.tag, prop: edge.prop }, ...hit.via] }
154
203
  }
155
204
  return null
156
205
  }
@@ -160,15 +209,75 @@ export function projectView(files, pos) {
160
209
  if (name === box) return true
161
210
  if (seen.has(name)) return false
162
211
  seen.add(name)
163
- return [...(comp?.derives.get(name) ?? [])].some(dep => readsBox(comp, dep.split('.'), box, seen))
212
+ return [...(comp?.derives.get(name) ?? [])].some((dep) =>
213
+ readsBox(comp, dep.split('.'), box, seen),
214
+ )
164
215
  }
165
216
 
217
+ promoteFindings(files, pos, readsBox, lookup, reachesList)
218
+ }
219
+
220
+ /** The box a flagged `.map` call writes: `box.value = …map(…)` or `const box = …map(…)`. */
221
+ function boxOf(sf, offset) {
222
+ let found
223
+ const visit = (n) => {
224
+ if (found) return
225
+ if (ts.isCallExpression(n) && n.getStart() === offset) found = n
226
+ else if (n.pos <= offset && offset < n.end) ts.forEachChild(n, visit)
227
+ }
228
+ visit(sf)
229
+ for (let n = found; n; n = n.parent) {
230
+ if (ts.isBinaryExpression(n) && n.operatorToken.kind === ts.SyntaxKind.EqualsToken) {
231
+ const left = n.left
232
+ if (
233
+ ts.isPropertyAccessExpression(left) &&
234
+ left.name.text === 'value' &&
235
+ ts.isIdentifier(left.expression)
236
+ )
237
+ return left.expression.text
238
+ return null
239
+ }
240
+ if (ts.isVariableDeclaration(n)) return ts.isIdentifier(n.name) ? n.name.text : null
241
+ }
242
+ return null
243
+ }
244
+
245
+ export const relPath = (root, abs) => relative(root, abs).split('\\').join('/')
246
+
247
+ function parseProject(files, byAbs) {
248
+ for (const f of files) {
249
+ f.sf = ts.createSourceFile(f.abs, f.text, ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX)
250
+ f.comps = componentsOf(f.sf)
251
+ f.imports = new Map()
252
+ for (const st of f.sf.statements) {
253
+ if (
254
+ !ts.isImportDeclaration(st) ||
255
+ !st.importClause ||
256
+ !ts.isStringLiteral(st.moduleSpecifier)
257
+ )
258
+ continue
259
+ const spec = st.moduleSpecifier.text
260
+ const base = local(f.abs, spec)
261
+ if (!base) continue
262
+ const target = exts.map((e) => base + e).find((p) => byAbs.has(p))
263
+ if (!target) continue
264
+ if (st.importClause.name)
265
+ f.imports.set(st.importClause.name.text, { file: target, name: 'default' })
266
+ const named = st.importClause.namedBindings
267
+ if (named && ts.isNamedImports(named))
268
+ for (const el of named.elements)
269
+ f.imports.set(el.name.text, { file: target, name: (el.propertyName ?? el.name).text })
270
+ }
271
+ }
272
+ }
273
+
274
+ function promoteFindings(files, pos, readsBox, lookup, reachesList) {
166
275
  for (const f of files) {
167
276
  for (const d of f.diagnostics) {
168
277
  if (d.code !== 'W014' || d.severity !== 'warning' || !d.offset) continue
169
278
  const box = boxOf(f.sf, d.offset[0])
170
279
  if (!box) continue
171
- const owners = f.comps.filter(c => c.fn.pos <= d.offset[0] && d.offset[0] < c.fn.end)
280
+ const owners = f.comps.filter((c) => c.fn.pos <= d.offset[0] && d.offset[0] < c.fn.end)
172
281
  for (const comp of owners.length ? owners : f.comps) {
173
282
  let hit = null
174
283
  let edge
@@ -185,9 +294,13 @@ export function projectView(files, pos) {
185
294
  d.project = true
186
295
  d.why =
187
296
  `\`.map(p => ({ ...p, … }))\` gives every item a new identity on each update, and \`${box}\` reaches a keyed list in ` +
188
- `${hit.file.rel}:${listPos[0]} through ${path.map(p => `<${p.tag} ${p.prop}>`).join(' → ')}: every row re-runs on every change.`
297
+ `${hit.file.rel}:${listPos[0]} through ${path.map((p) => `<${p.tag} ${p.prop}>`).join(' → ')}: every row re-runs on every change.`
189
298
  d.related = [
190
- ...path.map(p => ({ file: p.file.rel, span: pos(p.file).span(p.at, p.at), why: `passed as \`<${p.tag} ${p.prop}>\`` })),
299
+ ...path.map((p) => ({
300
+ file: p.file.rel,
301
+ span: pos(p.file).span(p.at, p.at),
302
+ why: `passed as \`<${p.tag} ${p.prop}>\``,
303
+ })),
191
304
  { file: hit.file.rel, span: listPos, why: 'rendered here as a keyed list' },
192
305
  ]
193
306
  break
@@ -195,25 +308,3 @@ export function projectView(files, pos) {
195
308
  }
196
309
  }
197
310
  }
198
-
199
- /** The box a flagged `.map` call writes: `box.value = …map(…)` or `const box = …map(…)`. */
200
- function boxOf(sf, offset) {
201
- let found
202
- const visit = n => {
203
- if (found) return
204
- if (ts.isCallExpression(n) && n.getStart() === offset) found = n
205
- else if (n.pos <= offset && offset < n.end) ts.forEachChild(n, visit)
206
- }
207
- visit(sf)
208
- for (let n = found; n; n = n.parent) {
209
- if (ts.isBinaryExpression(n) && n.operatorToken.kind === ts.SyntaxKind.EqualsToken) {
210
- const left = n.left
211
- if (ts.isPropertyAccessExpression(left) && left.name.text === 'value' && ts.isIdentifier(left.expression)) return left.expression.text
212
- return null
213
- }
214
- if (ts.isVariableDeclaration(n)) return ts.isIdentifier(n.name) ? n.name.text : null
215
- }
216
- return null
217
- }
218
-
219
- export const relPath = (root, abs) => relative(root, abs).split('\\').join('/')
@@ -1,5 +1,6 @@
1
1
  // Prop inference in `weft check` and `weft tsc` (research 44). `W080`–`W085` come from the engine; a `TS` code is the
2
2
  // compiler's own error, reported at the source position with the inferred types applied.
3
+ // W007/W008 map proven core box errors.
3
4
 
4
5
  import { existsSync, readFileSync } from 'node:fs'
5
6
  import { createRequire } from 'node:module'
@@ -10,19 +11,36 @@ import { diagnostic, positions } from './diag.mjs'
10
11
  const require = createRequire(import.meta.url)
11
12
  const { Session } = require('./infer/session.cjs')
12
13
 
13
- const posix = p => p.split('\\').join('/')
14
+ const posix = (p) => p.split('\\').join('/')
14
15
 
15
16
  export function toDiagnostics(root, list, file) {
16
17
  const rel = posix(relative(root, file))
17
18
  const text = readFileSync(file, 'utf8')
18
19
  const pos = positions(text)
19
- return list.map(f => diagnostic({
20
- code: f.code, severity: f.severity, file: rel, span: pos.span(f.start, f.end), offset: [f.start, f.end], why: f.why, help: f.help,
21
- fix: f.edits && { description: f.help, safe: f.safe, edits: f.edits.map(e => ({ file: rel, span: pos.span(e.start, e.end), offset: [e.start, e.end], text: e.text })) },
22
- }))
20
+ return list.map((f) =>
21
+ diagnostic({
22
+ code: f.code,
23
+ severity: f.severity,
24
+ file: rel,
25
+ span: pos.span(f.start, f.end),
26
+ offset: [f.start, f.end],
27
+ why: f.why,
28
+ help: f.help,
29
+ fix: f.edits && {
30
+ description: f.help,
31
+ safe: f.safe,
32
+ edits: f.edits.map((e) => ({
33
+ file: rel,
34
+ span: pos.span(e.start, e.end),
35
+ offset: [e.start, e.end],
36
+ text: e.text,
37
+ })),
38
+ },
39
+ }),
40
+ )
23
41
  }
24
42
 
25
- /** The checks `weft check` adds. A project with nothing to infer parses its `.tsx` files once and starts nothing. */
43
+ /** Prop inference and core box diagnostics; a project with no relevant source starts no checker. */
26
44
  export async function propChecks(root, config) {
27
45
  if (config.check?.props === false || !existsSync(join(root, 'tsconfig.json'))) return []
28
46
  const session = new Session(ts, root)
@@ -10,29 +10,47 @@ const SKIP = /[\\/](node_modules|\.weft|\.output|dist)[\\/]/
10
10
  export function attachProps(server, root, config) {
11
11
  const setting = config?.check?.props
12
12
  if (setting === false || process.env.WEFT_PROPS === 'off') return null
13
- const worker = new Worker(new URL('./infer/worker.cjs', import.meta.url), { workerData: { root, write: setting !== 'manual' }, stdout: false })
13
+ const worker = new Worker(new URL('./infer/worker.cjs', import.meta.url), {
14
+ workerData: { root, write: setting !== 'manual' },
15
+ stdout: false,
16
+ })
14
17
  worker.unref()
15
18
  const stats = { saves: 0, ms: [] }
16
- const post = file => { if (SOURCE.test(file) && !SKIP.test(file)) worker.postMessage(file) }
19
+ const post = (file) => {
20
+ if (SOURCE.test(file) && !SKIP.test(file)) worker.postMessage(file)
21
+ }
17
22
  for (const event of ['change', 'add', 'unlink']) server.watcher.on(event, post)
18
23
 
19
24
  const logger = server.config?.logger ?? console
20
- worker.on('message', msg => {
25
+ worker.on('message', (msg) => {
21
26
  if (msg.type === 'failed') return logger.warn?.(`weft props: ${msg.message}`)
22
- if (msg.type === 'wrote') return logger.info?.(`weft props: wrote the inferred type in ${msg.file}`)
27
+ if (msg.type === 'wrote')
28
+ return logger.info?.(`weft props: wrote the inferred type in ${msg.file}`)
23
29
  if (msg.type !== 'done') return
24
30
  stats.saves++
25
31
  stats.ms.push(msg.ms)
26
32
  if (!msg.changed) return
27
- const errors = msg.diagnostics.filter(d => d.severity === 'error')
28
- for (const d of msg.diagnostics) logger.warn?.(`${d.file}:${d.line} ${d.code} ${d.severity} ${d.why}\n fix: ${d.help}`)
33
+ const errors = msg.diagnostics.filter((d) => d.severity === 'error')
34
+ for (const d of msg.diagnostics)
35
+ logger.warn?.(`${d.file}:${d.line} ${d.code} ${d.severity} ${d.why}\n fix: ${d.help}`)
29
36
  server.ws.send({ type: 'custom', event: 'weft:props', data: msg.diagnostics })
30
37
  // A save's HMR update has cleared any earlier overlay already, so this only ever shows the current errors.
31
- if (errors.length) server.ws.send({ type: 'error', err: { message: errors.map(d => `${d.file}:${d.line} ${d.code}: ${d.why}`).join('\n'), stack: '', plugin: 'weft:props' } })
38
+ if (errors.length)
39
+ server.ws.send({
40
+ type: 'error',
41
+ err: {
42
+ message: errors.map((d) => `${d.file}:${d.line} ${d.code}: ${d.why}`).join('\n'),
43
+ stack: '',
44
+ plugin: 'weft:props',
45
+ },
46
+ })
32
47
  })
33
- worker.on('error', e => logger.warn?.(`weft props stopped: ${e.message}`))
48
+ worker.on('error', (e) => logger.warn?.(`weft props stopped: ${e.message}`))
34
49
 
35
50
  const close = server.close.bind(server)
36
- server.close = async () => { worker.postMessage('close'); return close() }
51
+ server.close = async () => {
52
+ worker.postMessage('close')
53
+ return close()
54
+ }
37
55
  return { worker, stats }
38
56
  }
@@ -16,19 +16,50 @@ export function reactChecks({ files }) {
16
16
  const pos = positions(text)
17
17
  for (const el of scanJsx(text, rel).elements) {
18
18
  for (const a of el.attrs) {
19
- if (a.name === 'key' && a.kind === 'expr' && RANDOM.test(a.expr)) out.push(diagnostic({ code: 'W044', file: rel, span: pos.span(a.start, a.end), offset: [a.start, a.end], why: `\`key={${a.expr}}\` differs between the server and the client, so this row can't hydrate and is built again, and it differs on every render.` }))
19
+ if (a.name === 'key' && a.kind === 'expr' && RANDOM.test(a.expr))
20
+ out.push(
21
+ diagnostic({
22
+ code: 'W044',
23
+ file: rel,
24
+ span: pos.span(a.start, a.end),
25
+ offset: [a.start, a.end],
26
+ why: `\`key={${a.expr}}\` differs between the server and the client, so this row can't hydrate and is built again, and it differs on every render.`,
27
+ }),
28
+ )
20
29
  }
21
30
  if (!NATIVE.test(el.name)) continue
22
31
  for (const a of el.attrs) {
23
- if (!a.name.startsWith('data-') || a.kind !== 'expr' || (a.expr !== 'true' && a.expr !== 'false')) continue
32
+ if (
33
+ !a.name.startsWith('data-') ||
34
+ a.kind !== 'expr' ||
35
+ (a.expr !== 'true' && a.expr !== 'false')
36
+ )
37
+ continue
24
38
  const on = a.expr === 'true'
25
39
  // Removing the attribute takes the space before it too.
26
40
  const [from, to] = on ? [a.start, a.end] : [text.slice(0, a.start).trimEnd().length, a.end]
27
41
  const why = on
28
42
  ? `\`${a.name}={true}\` writes \`${a.name}=""\`, not React's \`"true"\`, so \`[${a.name}="true"]\` won't match. Write \`${a.name}\` for a presence flag, or \`${a.name}="true"\` for the word.`
29
43
  : `\`${a.name}={false}\` writes no attribute, not React's \`"false"\`. Drop it, or write \`${a.name}="false"\` for the word.`
30
- const fix = { description: on ? `Write \`${a.name}\`, which renders the same` : `Remove \`${a.name}\`, which renders the same`, safe: true, edits: [{ file: rel, span: pos.span(from, to), offset: [from, to], text: on ? a.name : '' }] }
31
- out.push(diagnostic({ code: 'W043', file: rel, span: pos.span(a.start, a.end), offset: [a.start, a.end], why, fix }))
44
+ const fix = {
45
+ description: on
46
+ ? `Write \`${a.name}\`, which renders the same`
47
+ : `Remove \`${a.name}\`, which renders the same`,
48
+ safe: true,
49
+ edits: [
50
+ { file: rel, span: pos.span(from, to), offset: [from, to], text: on ? a.name : '' },
51
+ ],
52
+ }
53
+ out.push(
54
+ diagnostic({
55
+ code: 'W043',
56
+ file: rel,
57
+ span: pos.span(a.start, a.end),
58
+ offset: [a.start, a.end],
59
+ why,
60
+ fix,
61
+ }),
62
+ )
32
63
  }
33
64
  }
34
65
  }
@@ -0,0 +1,34 @@
1
+ // W030 joins server reachability with proven core state calls at module evaluation.
2
+ import { diagnostic, positions } from './diag.mjs'
3
+ import { serverGraph } from './state-checks/server-graph.mjs'
4
+ import { coreState, moduleCalls } from './state-checks/core-state.mjs'
5
+
6
+ /** State created by a server-reachable module is shared across requests unless explicitly global. */
7
+ export function serverStateChecks(root, options) {
8
+ const graph = serverGraph(root, options)
9
+ if (!graph.size) return []
10
+ const resolve = coreState(root, graph)
11
+ const out = []
12
+ for (const [file, { sf, entry }] of graph) {
13
+ const pos = positions(sf.text)
14
+ moduleCalls(sf, (call) => {
15
+ if (!['state', 'linked'].includes(resolve(call.expression))) return
16
+ const start = call.getStart(sf),
17
+ end = call.end
18
+ out.push(
19
+ diagnostic({
20
+ code: 'W030',
21
+ severity: 'error',
22
+ file,
23
+ span: pos.span(start, end),
24
+ offset: [start, end],
25
+ why: `This module creates state reachable from server entry \`${entry}\`; requests share it.`,
26
+ help:
27
+ 'Create state inside a request/component factory or a context owned by the request. ' +
28
+ 'Use `state.global` only when sharing across requests is intended.',
29
+ }),
30
+ )
31
+ })
32
+ }
33
+ return out.sort((a, b) => a.file.localeCompare(b.file) || a.offset[0] - b.offset[0])
34
+ }