@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/docs.mjs CHANGED
@@ -11,31 +11,48 @@ import { probeTests } from './tests.mjs'
11
11
  import { exportsOf, loadCompiler, resolveWeft } from './weft.mjs'
12
12
 
13
13
  const docsDir = fileURLToPath(new URL('../docs/', import.meta.url))
14
- const fileTopics = Object.fromEntries(readdirSync(docsDir).filter(f => f.endsWith('.md')).map(f => [f.slice(0, -3), f]))
14
+ const fileTopics = Object.fromEntries(
15
+ readdirSync(docsDir)
16
+ .filter((f) => f.endsWith('.md'))
17
+ .map((f) => [f.slice(0, -3), f]),
18
+ )
15
19
 
16
20
  /** What `weft docs` can show, with the line that says what each topic covers. */
17
21
  export const topics = {
18
22
  rules: 'The nine rules: boxes, derived, live JSX and props, run-once bodies (start here)',
19
- state: 'Every reactive function (state, derived, watch, untrack, flush, batch, context) and defineState with persist, sync and history; S001, S002, S008, S009, S012, S013',
23
+ state:
24
+ 'Every reactive function (state, derived, watch, untrack, flush, batch, context) and defineState with persist, sync and history; S001, S002, S008, S009, S012, S013',
20
25
  props: 'How props stay live; W001, W005, W006',
21
26
  lists: 'Keyed lists, conditionals and stable items; W010, W011, W013, W014',
22
27
  async: 'resource, <Boundary pending error>, @weft-js/core/ssr and hydrate; W012',
23
- start: 'full stack: routes, load, query, mutation, the database, auth, realtime, runtimes; Q001–Q016, A001–A004, C001–C002, F001–F003, R001–R023',
28
+ start:
29
+ 'full stack: routes, load, query, mutation, the database, auth, realtime, runtimes; Q001–Q016, A001–A005, C001–C002, F001–F003, R001–R024',
24
30
  rust: 'Rust bodies: native() addons with a WASM fallback, remote() sidecars, weft add rust, impl, the weft crate, build.rs, Ctx, touch, weft dev reload, deployment; R101–R109',
25
- deploy: 'Deploy: which hosts can run the app, weft deploy hosts/init, the push workflow, Netlify, Vercel, Cloudflare, Fly, Render, CDN and Cloudinary; E001–E014',
26
- plugins: 'Build a plugin: slots, server extensions, check hooks, docs, `weft add`, conformance. Walks through how @weft-js/pay is built',
27
- testing: '@weft-js/core/test: render, screen, user, flush, clock, touches and toTouchAtMost(n, { runs }); T001–T009',
28
- browser: '@weft-js/core/browser: online, visible, lifecycle, media, on, interval; the net/page fakes; W050, W051, W054, W056, T012',
29
- 'ui-state': 'Hover, press and focus as state: interaction(), the ui prop, sharing across tabs and clients, realtime({ share }); U001–U005',
30
- surface: '@weft-js/core/surface: canvas and WebGL as layout children: defineSurface, <Surface>, s.draw, s.frame, s.text, the clock; X001–X004, X009–X012',
31
- verify: 'Check your work: drive the running app (weft browser / __weft__.agent: snapshot with the live graph, act, assert, clock, network, record) and the test gate; T010, T011, T020',
31
+ deploy:
32
+ 'Deploy: which hosts can run the app, weft deploy hosts/init, the push workflow, Netlify, Vercel, Cloudflare, Fly, Render, CDN and Cloudinary; E001–E014',
33
+ plugins:
34
+ 'Build a plugin: slots, server extensions, check hooks, docs, `weft add`, conformance. Walks through how @weft-js/pay is built',
35
+ theme: 'Change the look: warp() options, three worked looks, button tone channels, D010',
36
+ testing:
37
+ '@weft-js/core/test: render, screen, user, flush, clock, touches and toTouchAtMost(n, { runs }); T001–T009',
38
+ browser:
39
+ '@weft-js/core/browser: online, visible, lifecycle, media, on, interval; the net/page fakes; W050, W051, W054, W056, T012',
40
+ 'ui-state':
41
+ 'Hover, press and focus as state: interaction(), the ui prop, sharing across tabs and clients, realtime({ share }); U001–U005',
42
+ surface:
43
+ '@weft-js/core/surface: canvas and WebGL as layout children: defineSurface, <Surface>, s.draw, s.frame, s.text, the clock; X001–X004, X009–X012',
44
+ verify:
45
+ 'Check your work: drive the running app (weft browser / __weft__.agent: snapshot with the live graph, act, assert, clock, network, record) and the test gate; T010, T011, T020',
32
46
  icons: '@weft-js/warp-icons: per-icon exports, iconSet, the three tiers, icons({ tier }), sizes',
33
- fonts: 'Fonts with @weft-js/text: text({ fonts }), what the build makes, using a face in CSS, budgets; X020, X021, X022, X023',
34
- meta: 'Page metadata and share cards: route `meta`, what is automatic, `site.url`, `.og.tsx`, `weft routes --meta`; H001–H008, H011–H016',
47
+ fonts:
48
+ 'Fonts with @weft-js/text: text({ fonts }), what the build makes, using a face in CSS, budgets; X020, X021, X022, X023',
49
+ meta: 'Page metadata and share cards: route `meta`, what is automatic, `site.url`, `.og.tsx`, `weft routes --meta`; H001–H010, H011 (no description) to H017',
35
50
  config: 'weft.config.ts: scope, tasks, environments, reserved plugins/slots; P-codes',
36
- design: 'The design language: Stack, Row, Heading, Text, Link, the closed vocabulary, `weft fix`, `weft tokens`; D001–D007, D011, D030–D039',
37
- style: 'Style groups (@weft-js/core/style): group(), axes, parts, accents, weft style find/explain, .weft/styles.json; D020–D025, W060',
38
- diagnostics: 'Every W-, T-, P-, H- and D-code, and which ones the installed weft reports',
51
+ design:
52
+ 'The design language: Stack, Row, Heading, Text, Link, the closed vocabulary, `weft fix`, `weft tokens`; D001–D007, D011, D012, D030–D041',
53
+ style:
54
+ 'Style groups (@weft-js/core/style): group(), axes, parts, accents, weft style find/explain, .weft/styles.json; D020–D025, W060',
55
+ diagnostics: 'Every diagnostic code, from W to L, and which ones the installed weft reports',
39
56
  commands: "This project's commands and tasks (from `weft tasks`)",
40
57
  agents: 'The generated AGENTS.md',
41
58
  }
@@ -45,20 +62,32 @@ const probes = new Map()
45
62
  /** What the installed weft offers: version, exports, and which catalog codes its compiler reports. */
46
63
  export function installed(root) {
47
64
  const weft = resolveWeft(root)
48
- if (!probes.has(weft.compilerPath)) probes.set(weft.compilerPath, { ...probe(loadCompiler(weft)), ...probeTests(catalog) })
65
+ if (!probes.has(weft.compilerPath))
66
+ probes.set(weft.compilerPath, { ...probe(loadCompiler(weft)), ...probeTests(catalog) })
49
67
  return { weft, exports: exportsOf(weft), status: probes.get(weft.compilerPath) }
50
68
  }
51
69
 
52
70
  function status(code, info) {
53
71
  const e = entry(code)
54
72
  const st = info.status[e.code]
55
- if (e.by === 'compiler') return st?.implemented ? `reported${st.autofix ? ', safe autofix' : st.suggestion ? ', suggested edit' : ''}` : 'not reported by this weft yet'
73
+ if (e.by === 'compiler')
74
+ return st?.implemented
75
+ ? `reported${st.autofix ? ', safe autofix' : st.suggestion ? ', suggested edit' : ''}`
76
+ : 'not reported by this weft yet'
56
77
  if (e.by === 'test') return 'reported by @weft-js/core/test after each test'
57
78
  if (e.by === 'gate') return 'reported by `weft test --gate` on a diff'
58
- if (e.code.startsWith('T')) return st?.implemented ? `reported by weft check on test files${st.autofix ? ', safe autofix' : st.suggestion ? ', suggested edit' : ''}` : 'not reported by this weft yet'
59
- if (e.by === 'start') return 'reported by @weft-js/start when it serves or renders the route, and by weft check and `weft routes --meta`'
60
- if (e.by === 'cli' && e.code.startsWith('H')) return 'reported by weft check and by `weft routes --meta`, per route, on what the routes produce'
61
- if (e.by === 'cli') return e.proposed ? 'reported by weft check (proposed code, research 17)' : 'reported by weft check'
79
+ if (e.code.startsWith('T'))
80
+ return st?.implemented
81
+ ? `reported by weft check on test files${st.autofix ? ', safe autofix' : st.suggestion ? ', suggested edit' : ''}`
82
+ : 'not reported by this weft yet'
83
+ if (e.by === 'start')
84
+ return 'reported by @weft-js/start when it serves or renders the route, and by weft check and `weft routes --meta`'
85
+ if (e.by === 'cli' && e.code.startsWith('H'))
86
+ return 'reported by weft check and by `weft routes --meta`, per route, on what the routes produce'
87
+ if (e.by === 'cli')
88
+ return e.proposed
89
+ ? 'reported by weft check (proposed code, research 17)'
90
+ : 'reported by weft check'
62
91
  if (e.by === 'runtime') return 'reported in dev by the runtime (a console warning)'
63
92
  if (e.by === 'plugin') return `reported by weft check when \`${e.owner}\` is in \`plugins\``
64
93
  return 'reserved'
@@ -66,31 +95,76 @@ function status(code, info) {
66
95
 
67
96
  function codePage(code, info) {
68
97
  const e = entry(code)
69
- const lines = [`# ${code} (${e.severity})`, '', e.catches + '.', '', `**Fix:** ${e.fix}.`, '', `**Status in weft@${info.weft.version}:** ${status(code, info)}.`]
98
+ const lines = [
99
+ `# ${code} (${e.severity})`,
100
+ '',
101
+ e.catches + '.',
102
+ '',
103
+ `**Fix:** ${e.fix}.`,
104
+ '',
105
+ `**Status in weft@${info.weft.version}:** ${status(code, info)}.`,
106
+ ]
70
107
  if (e.example) lines.push('', 'Triggers it:', '', '```tsx', e.example, '```')
71
- if (code.startsWith('T')) lines.push('', 'A test that needs it on purpose keeps it with a reason: `// weft-allow ' + code + ': <why>` in the file header (whole file) or on the line above.')
72
- const topic = Object.entries(topics).find(([, d]) => d.includes(code)) ?? (code.startsWith('T') ? ['testing'] : undefined)
108
+ if (code.startsWith('T'))
109
+ lines.push(
110
+ '',
111
+ 'A test that needs it on purpose keeps it with a reason: `// weft-allow ' +
112
+ code +
113
+ ': <why>` in the file header (whole file) or on the line above.',
114
+ )
115
+ const topic =
116
+ Object.entries(topics).find(([, d]) => d.includes(code)) ??
117
+ (code.startsWith('T') ? ['testing'] : undefined)
73
118
  if (topic) lines.push('', `More: \`weft docs ${topic[0]}\`.`)
74
119
  return lines.join('\n')
75
120
  }
76
121
 
77
- const contributed = async root => (await loadConfig(root)).contributions
122
+ const contributed = async (root) => (await loadConfig(root)).contributions
78
123
 
79
124
  /** What this app's plugins add to the docs (`docs` and `diagnostics` in their manifests). */
80
- const pluginDocs = async root => (await contributed(root)).flatMap(c => (c.manifest.docs ? [c.manifest.docs] : []))
81
- const pluginCodes = async root => (await contributed(root)).flatMap(c => (c.manifest.diagnostics ?? []).map(d => ({ ...d, plugin: c.name })))
125
+ const pluginDocs = async (root) =>
126
+ (await contributed(root)).flatMap((c) => (c.manifest.docs ? [c.manifest.docs] : []))
127
+ const pluginCodes = async (root) =>
128
+ (await contributed(root)).flatMap((c) =>
129
+ (c.manifest.diagnostics ?? []).map((d) => ({ ...d, plugin: c.name })),
130
+ )
82
131
 
83
132
  function diagnosticsPage(info, own = []) {
84
- const rows = [...catalog.map(e => `| ${e.code} | ${e.severity} | ${e.catches} | ${e.fix} | ${status(e.code, info)} |`), ...own.map(d => `| ${d.code} | ${d.severity ?? 'error'} | ${d.catches} | ${d.fix} | from \`${d.plugin}\` |`)]
85
- return [`# Diagnostics (weft@${info.weft.version})`, '', '| Code | Severity | Catches | Fix | Status |', '|---|---|---|---|---|', ...rows].join('\n')
133
+ const rows = [
134
+ ...catalog.map(
135
+ (e) => `| ${e.code} | ${e.severity} | ${e.catches} | ${e.fix} | ${status(e.code, info)} |`,
136
+ ),
137
+ ...own.map(
138
+ (d) =>
139
+ `| ${d.code} | ${d.severity ?? 'error'} | ${d.catches} | ${d.fix} | from \`${d.plugin}\` |`,
140
+ ),
141
+ ]
142
+ return [
143
+ `# Diagnostics (weft@${info.weft.version})`,
144
+ '',
145
+ '| Code | Severity | Catches | Fix | Status |',
146
+ '|---|---|---|---|---|',
147
+ ...rows,
148
+ ].join('\n')
86
149
  }
87
150
 
88
151
  export function commandsPage(list, { planned: showPlanned = true } = {}) {
89
152
  const rows = list.tasks
90
- .filter(t => t.available)
91
- .map(t => `| \`${t.command}\` | ${t.description ?? '(no description: P004)'} | ${t.sideEffects?.length ? t.sideEffects.join(', ') : 'none'} |`)
92
- const planned = list.tasks.filter(t => !t.available).map(t => `\`${t.name}\``)
93
- return ['| Command | Does | Side effects |', '|---|---|---|', ...rows, '', showPlanned && planned.length ? `Planned, not shipped yet: ${planned.join(', ')}.` : ''].join('\n').trim()
153
+ .filter((t) => t.available)
154
+ .map(
155
+ (t) =>
156
+ `| \`${t.command}\` | ${t.description ?? '(no description: P004)'} | ${t.sideEffects?.length ? t.sideEffects.join(', ') : 'none'} |`,
157
+ )
158
+ const planned = list.tasks.filter((t) => !t.available).map((t) => `\`${t.name}\``)
159
+ return [
160
+ '| Command | Does | Side effects |',
161
+ '|---|---|---|',
162
+ ...rows,
163
+ '',
164
+ showPlanned && planned.length ? `Planned, not shipped yet: ${planned.join(', ')}.` : '',
165
+ ]
166
+ .join('\n')
167
+ .trim()
94
168
  }
95
169
 
96
170
  /**
@@ -102,31 +176,52 @@ export async function docs(root, topic) {
102
176
  root = resolve(root)
103
177
  const key = (topic ?? 'index').trim()
104
178
  if (key === 'index') {
105
- const all = { ...topics, ...Object.fromEntries((await pluginDocs(root)).map(d => [d.topic, d.summary])) }
106
- const text = ['# weft docs', '', ...Object.entries(all).map(([t, d]) => `- \`${t}\`: ${d}`), '', 'Any diagnostic code works too: `W014`, `T003`, `P004`, `D025`, `Q001`.'].join('\n')
179
+ const all = {
180
+ ...topics,
181
+ ...Object.fromEntries((await pluginDocs(root)).map((d) => [d.topic, d.summary])),
182
+ }
183
+ const text = [
184
+ '# weft docs',
185
+ '',
186
+ ...Object.entries(all).map(([t, d]) => `- \`${t}\`: ${d}`),
187
+ '',
188
+ 'Any diagnostic code works too: `W014`, `T003`, `P004`, `D025`, `Q001`.',
189
+ ].join('\n')
107
190
  return { topic: 'index', text, topics: all }
108
191
  }
109
192
  const info = installed(root)
110
193
  if (/^[A-Z]{1,5}\d{3}$/i.test(key)) {
111
194
  const code = key.toUpperCase()
112
- const own = !entry(code) && (await pluginCodes(root)).find(d => d.code === code)
113
- if (own) return { topic: code, text: `# ${code} (${own.severity ?? 'error'})\n\n${own.catches}.\n\n**Fix:** ${own.fix}.\n\nReported by weft check, from \`${own.plugin}\`.` }
114
- if (!entry(code)) throw Object.assign(new Error(`unknown diagnostic code ${code}`), { code: 'E_UNKNOWN_TOPIC' })
195
+ const own = !entry(code) && (await pluginCodes(root)).find((d) => d.code === code)
196
+ if (own)
197
+ return {
198
+ topic: code,
199
+ text: `# ${code} (${own.severity ?? 'error'})\n\n${own.catches}.\n\n**Fix:** ${own.fix}.\n\nReported by weft check, from \`${own.plugin}\`.`,
200
+ }
201
+ if (!entry(code))
202
+ throw Object.assign(new Error(`unknown diagnostic code ${code}`), { code: 'E_UNKNOWN_TOPIC' })
115
203
  return { topic: code, text: codePage(code, info) }
116
204
  }
117
- if (key === 'diagnostics') return { topic: key, text: diagnosticsPage(info, await pluginCodes(root)) }
118
- if (key === 'commands') return { topic: key, text: `# Commands\n\n${commandsPage(await tasks(root))}` }
205
+ if (key === 'diagnostics')
206
+ return { topic: key, text: diagnosticsPage(info, await pluginCodes(root)) }
207
+ if (key === 'commands')
208
+ return { topic: key, text: `# Commands\n\n${commandsPage(await tasks(root))}` }
119
209
  if (key === 'agents') {
120
210
  const { agentsMd } = await import('./agents-md.mjs')
121
211
  return { topic: key, text: (await agentsMd(root)).text }
122
212
  }
123
- const page = (await pluginDocs(root)).find(d => d.topic === key)
213
+ const page = (await pluginDocs(root)).find((d) => d.topic === key)
124
214
  if (page) return { topic: key, text: readFileSync(page.file, 'utf8').trim() }
125
215
  if (fileTopics[key]) {
126
216
  const text = readFileSync(resolve(docsDir, fileTopics[key]), 'utf8').trim()
127
217
  return { topic: key, text: key === 'rules' ? `# The rules\n\n${text}` : text }
128
218
  }
129
- throw Object.assign(new Error(`unknown docs topic "${key}"; topics: ${Object.keys(topics).join(', ')}, or a code like W014`), { code: 'E_UNKNOWN_TOPIC' })
219
+ throw Object.assign(
220
+ new Error(
221
+ `unknown docs topic "${key}"; topics: ${Object.keys(topics).join(', ')}, or a code like W014`,
222
+ ),
223
+ { code: 'E_UNKNOWN_TOPIC' },
224
+ )
130
225
  }
131
226
 
132
- export const readDoc = name => readFileSync(resolve(docsDir, `${name}.md`), 'utf8').trim()
227
+ export const readDoc = (name) => readFileSync(resolve(docsDir, `${name}.md`), 'utf8').trim()
package/src/doctor.mjs CHANGED
@@ -7,7 +7,8 @@ import { loadCompiler, resolveWeft } from './weft.mjs'
7
7
  /** One line for `weft check --verbose` and `weft doctor`. */
8
8
  export function compilerLine(backend) {
9
9
  if (!backend) return 'compiler: this weft does not say which build it uses'
10
- if (backend.kind === 'native') return `compiler: native addon${backend.package ? ` (${backend.package})` : ` (${backend.file})`}`
10
+ if (backend.kind === 'native')
11
+ return `compiler: native addon${backend.package ? ` (${backend.package})` : ` (${backend.file})`}`
11
12
  const offer = backend.wanted?.length
12
13
  ? `. Optional, and faster: ${backend.wanted.join(' or ')}, which a normal install adds by itself`
13
14
  : '. No prebuilt addon for this platform, and none is needed'
@@ -19,12 +20,23 @@ export function doctor(root) {
19
20
  const { backend } = loadCompiler(weft)
20
21
  const cargo = spawnSync('cargo', ['--version'], { encoding: 'utf8' })
21
22
  const version = cargo.status === 0 ? cargo.stdout.trim() : null
22
- const runtime = globalThis.Deno ? `Deno ${Deno.version.deno}` : globalThis.Bun ? `Bun ${Bun.version}` : `Node ${process.version}`
23
- return { ok: true, weft: weft.version, runtime, compiler: backend ?? null, rust: { cargo: version, required: false } }
23
+ const runtime = globalThis.Deno
24
+ ? `Deno ${Deno.version.deno}`
25
+ : globalThis.Bun
26
+ ? `Bun ${Bun.version}`
27
+ : `Node ${process.version}`
28
+ return {
29
+ ok: true,
30
+ weft: weft.version,
31
+ runtime,
32
+ compiler: backend ?? null,
33
+ rust: { cargo: version, required: false },
34
+ }
24
35
  }
25
36
 
26
- export const formatDoctor = r => [
27
- `weft ${r.weft} on ${r.runtime}`,
28
- compilerLine(r.compiler),
29
- `rust: ${r.rust.cargo ?? 'no cargo on PATH'}. Not needed for weft; only \`weft add rust\` sidecars and native() use it`,
30
- ].join('\n')
37
+ export const formatDoctor = (r) =>
38
+ [
39
+ `weft ${r.weft} on ${r.runtime}`,
40
+ compilerLine(r.compiler),
41
+ `rust: ${r.rust.cargo ?? 'no cargo on PATH'}. Not needed for weft; only \`weft add rust\` sidecars and native() use it`,
42
+ ].join('\n')
package/src/files.mjs CHANGED
@@ -3,7 +3,16 @@
3
3
  import { readdirSync } from 'node:fs'
4
4
  import { join } from 'node:path'
5
5
 
6
- const skipDirs = new Set(['public', 'node_modules', '.git', '.weft', '.output', 'dist', 'coverage', 'target'])
6
+ const skipDirs = new Set([
7
+ 'public',
8
+ 'node_modules',
9
+ '.git',
10
+ '.weft',
11
+ '.output',
12
+ 'dist',
13
+ 'coverage',
14
+ 'target',
15
+ ])
7
16
 
8
17
  export function globToRegExp(glob) {
9
18
  let re = ''
@@ -16,16 +25,20 @@ export function globToRegExp(glob) {
16
25
  else if (c === '?') re += '[^/]'
17
26
  else if (c === '{') {
18
27
  const end = glob.indexOf('}', i)
19
- re += `(?:${glob.slice(i + 1, end).split(',').map(s => s.replace(/[.+^$()|[\]\\]/g, '\\$&')).join('|')})`
28
+ re += `(?:${glob
29
+ .slice(i + 1, end)
30
+ .split(',')
31
+ .map((s) => s.replace(/[.+^$()|[\]\\]/g, '\\$&'))
32
+ .join('|')})`
20
33
  i = end
21
34
  } else re += c.replace(/[.+^$()|[\]\\]/g, '\\$&')
22
35
  }
23
36
  return new RegExp(`^${re}$`)
24
37
  }
25
38
 
26
- export const matcher = globs => {
39
+ export const matcher = (globs) => {
27
40
  const res = (globs ?? []).map(globToRegExp)
28
- return path => res.some(re => re.test(path))
41
+ return (path) => res.some((re) => re.test(path))
29
42
  }
30
43
 
31
44
  /** Relative posix paths under `root` matching `include` and none of `exclude`. Sorted, so output is stable. */
@@ -34,7 +47,7 @@ export function discover(root, { include = ['**/*.{tsx,jsx}'], exclude = [] } =
34
47
  const exc = matcher(exclude)
35
48
  /** @type {string[]} */
36
49
  const out = []
37
- const walk = rel => {
50
+ const walk = (rel) => {
38
51
  for (const ent of readdirSync(join(root, rel), { withFileTypes: true })) {
39
52
  const path = rel ? `${rel}/${ent.name}` : ent.name
40
53
  if (ent.isDirectory()) {
package/src/fix.mjs CHANGED
@@ -24,14 +24,22 @@ export async function fix(root, options = {}) {
24
24
  for (const [file, list] of byFile) {
25
25
  const edits = []
26
26
  for (const d of list) {
27
- if (d.fix.edits.some(e => e.file !== file || edits.some(x => e.offset[0] < x.offset[1] && x.offset[0] < e.offset[1]))) continue
27
+ if (
28
+ d.fix.edits.some(
29
+ (e) =>
30
+ e.file !== file ||
31
+ edits.some((x) => e.offset[0] < x.offset[1] && x.offset[0] < e.offset[1]),
32
+ )
33
+ )
34
+ continue
28
35
  edits.push(...d.fix.edits)
29
36
  applied.push({ code: d.code, file, span: d.span })
30
37
  changed++
31
38
  }
32
39
  const abs = join(root, file)
33
40
  let text = readFileSync(abs, 'utf8')
34
- for (const e of edits.sort((a, b) => b.offset[0] - a.offset[0])) text = text.slice(0, e.offset[0]) + e.text + text.slice(e.offset[1])
41
+ for (const e of edits.sort((a, b) => b.offset[0] - a.offset[0]))
42
+ text = text.slice(0, e.offset[0]) + e.text + text.slice(e.offset[1])
35
43
  writeFileSync(abs, text)
36
44
  }
37
45
  if (!changed) break
@@ -0,0 +1,40 @@
1
+ import { copyProject, removeCopy } from '../workspace.mjs'
2
+ import { resolver } from './resolve-cases.mjs'
3
+
4
+ export async function afterRun({ cases, testFiles, log, root, options, run, result }) {
5
+ // After: the change as it stands. Every changed test must pass.
6
+ log(
7
+ `gate: ${cases.length} new or changed tests in ${testFiles.length} files; running them on the change`,
8
+ )
9
+ const afterDir = copyProject(root, { exclude: options.exclude, id: 'after' })
10
+ let after
11
+ try {
12
+ after = await run(afterDir, testFiles)
13
+ } finally {
14
+ removeCopy(afterDir)
15
+ }
16
+ const resolveCases = resolver(testFiles, cases)
17
+ const afterStatus = resolveCases(after)
18
+ const byKey = new Map()
19
+ for (const [key, st] of afterStatus) {
20
+ const [file, name] = key.split('::')
21
+ const c =
22
+ cases.find((x) => x.file === file && x.name === name) ??
23
+ cases.find((x) => x.file === file && x.dynamic)
24
+ const t = {
25
+ file,
26
+ name,
27
+ line: c?.line ?? 1,
28
+ end: c?.end ?? c?.line ?? 1,
29
+ after: st.status,
30
+ afterMessage: st.status === 'passed' ? undefined : st.message,
31
+ before: null,
32
+ kills: 0,
33
+ }
34
+ byKey.set(key, t)
35
+ result.tests.push(t)
36
+ }
37
+ if (after.timedOut) result.notes.push('the run on the change timed out')
38
+
39
+ return { after, resolveCases, byKey }
40
+ }
@@ -0,0 +1,54 @@
1
+ import { diagnostic } from '../diag.mjs'
2
+ import { copyProject, removeCopy, writeFiles } from '../workspace.mjs'
3
+
4
+ export async function beforeRun({
5
+ source,
6
+ options,
7
+ log,
8
+ root,
9
+ run,
10
+ testFiles,
11
+ resolveCases,
12
+ byKey,
13
+ allowed,
14
+ result,
15
+ diagnostics,
16
+ }) {
17
+ // Before: source reverted, tests and their helpers as they are now. Every changed test must fail.
18
+ if (source.length && !options.refactor) {
19
+ log(`gate: reverting ${source.length} source files; the changed tests must fail`)
20
+ const beforeDir = copyProject(root, { exclude: options.exclude, id: 'before' })
21
+ let before
22
+ try {
23
+ writeFiles(beforeDir, Object.fromEntries(source.map((c) => [c.rel, c.before])))
24
+ before = await run(beforeDir, testFiles)
25
+ } finally {
26
+ removeCopy(beforeDir)
27
+ }
28
+ const beforeStatus = resolveCases(before)
29
+ for (const [key, t] of byKey) {
30
+ const st =
31
+ beforeStatus.get(key) ??
32
+ (before.loadErrors.has(t.file)
33
+ ? { status: 'failed', message: before.loadErrors.get(t.file) }
34
+ : { status: 'missing' })
35
+ t.before = st.status
36
+ t.beforeMessage = st.message
37
+ if (t.after === 'passed' && st.status === 'passed' && allowed('T010', t.file, t.line))
38
+ result.notes.push(`T010 allowed on "${t.name}"`)
39
+ else if (t.after === 'passed' && st.status === 'passed')
40
+ diagnostics.push(
41
+ diagnostic({
42
+ code: 'T010',
43
+ file: t.file,
44
+ span: [t.line, 1, t.line, 1],
45
+ why: `"${t.name}" passes on the code before this change too, so it doesn't test the change`,
46
+ help: "Assert what the change makes different (what the user sees now and didn't before); or, if this is a test for existing behavior, add it in its own change with no source edits",
47
+ }),
48
+ )
49
+ }
50
+ } else if (!options.refactor)
51
+ result.notes.push(
52
+ 'no source change: fail-before skipped; mutants come from the source the new tests import',
53
+ )
54
+ }
@@ -0,0 +1,72 @@
1
+ import { readFileSync } from 'node:fs'
2
+ import { join } from 'node:path'
3
+ import { discover } from '../files.mjs'
4
+ import { changedCases, weakened } from '../testdiff.mjs'
5
+ import { allowances, checkTestFile, isTestFile, parse } from '../tests.mjs'
6
+
7
+ export function classify(root, changes, code, configFile, importsOf) {
8
+ // Tests, test support (helpers only tests import) and source.
9
+ const tests = changes.filter((c) => isTestFile(c.rel))
10
+ const others = changes.filter((c) => !isTestFile(c.rel) && code(c.rel) && !configFile(c.rel))
11
+ const testAbs = new Set(
12
+ discover(root, {
13
+ include: ['**/*.{test,spec}.{ts,tsx,mts,cts,js,jsx,mjs,cjs}'],
14
+ exclude: ['.weft/**'],
15
+ }).map((r) => join(root, r)),
16
+ )
17
+ const importedBySource = new Set()
18
+ for (const rel of discover(root, {
19
+ include: ['**/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}'],
20
+ exclude: ['.weft/**'],
21
+ })) {
22
+ const abs = join(root, rel)
23
+ if (testAbs.has(abs)) continue
24
+ for (const p of importsOf(abs, readFileSync(abs, 'utf8'))) importedBySource.add(p)
25
+ }
26
+ const importedByTests = new Set()
27
+ for (const abs of testAbs)
28
+ for (const p of importsOf(abs, readFileSync(abs, 'utf8'))) importedByTests.add(p)
29
+ const isSupport = (rel) =>
30
+ importedByTests.has(join(root, rel)) &&
31
+ !importedBySource.has(join(root, rel)) &&
32
+ !rel.match(/(^|\/)src\//)
33
+ const support = others.filter((c) => isSupport(c.rel))
34
+ const source = others.filter((c) => !isSupport(c.rel))
35
+
36
+ // Static: T020 on every changed test file, T001–T009 on the ones that still exist (T005 blocks here).
37
+ const diagnostics = []
38
+ for (const c of tests) diagnostics.push(...weakened(c.before, c.after, c.rel))
39
+ for (const c of tests) {
40
+ if (c.after == null) continue
41
+ for (const d of checkTestFile(c.after, c.rel, { abs: join(root, c.rel), root })) {
42
+ if (d.code === 'T005' && d.severity === 'warning') {
43
+ d.severity = 'error'
44
+ d.why += ' The gate blocks implementation queries in new and changed tests.'
45
+ }
46
+ diagnostics.push(d)
47
+ }
48
+ }
49
+
50
+ // The tests this change has to prove.
51
+ // `// weft-allow T010: <reason>` / T011 on a test keeps it (shown in the result, for the reviewer).
52
+ const allowFns = new Map(
53
+ tests
54
+ .filter((c) => c.after != null)
55
+ .map((c) => [c.rel, allowances(c.after, parse(c.after, c.rel))]),
56
+ )
57
+ const allowed = (rule, file, line) => allowFns.get(file)?.(rule, line) ?? false
58
+ const cases = tests
59
+ .filter((c) => c.after != null)
60
+ .flatMap((c) =>
61
+ changedCases(c.before, c.after, c.rel).map((t) => ({
62
+ file: c.rel,
63
+ name: t.name,
64
+ line: t.line,
65
+ end: t.end,
66
+ dynamic: t.dynamic,
67
+ })),
68
+ )
69
+ const testFiles = [...new Set(cases.map((c) => c.file))]
70
+
71
+ return { tests, support, source, isSupport, diagnostics, allowed, cases, testFiles }
72
+ }