@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/docs.mjs CHANGED
@@ -11,30 +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
- plugins: 'Build a plugin: slots, server extensions, check hooks, docs, `weft add`, conformance. Walks through how @weft-js/pay is built',
26
- testing: '@weft-js/core/test: render, screen, user, flush, clock, touches and toTouchAtMost(n, { runs }); T001–T009',
27
- browser: '@weft-js/core/browser: online, visible, lifecycle, media, on, interval; the net/page fakes; W050, W051, W054, W056, T012',
28
- 'ui-state': 'Hover, press and focus as state: interaction(), the ui prop, sharing across tabs and clients, realtime({ share }); U001–U005',
29
- 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',
30
- 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',
31
46
  icons: '@weft-js/warp-icons: per-icon exports, iconSet, the three tiers, icons({ tier }), sizes',
32
- fonts: 'Fonts with @weft-js/text: text({ fonts }), what the build makes, using a face in CSS, budgets; X020, X021, X022, X023',
33
- 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',
34
50
  config: 'weft.config.ts: scope, tasks, environments, reserved plugins/slots; P-codes',
35
- design: 'The design language: Stack, Row, Heading, Text, Link, the closed vocabulary, `weft fix`, `weft tokens`; D001–D007, D011, D030–D039',
36
- style: 'Style groups (@weft-js/core/style): group(), axes, parts, accents, weft style find/explain, .weft/styles.json; D020–D025, W060',
37
- 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',
38
56
  commands: "This project's commands and tasks (from `weft tasks`)",
39
57
  agents: 'The generated AGENTS.md',
40
58
  }
@@ -44,20 +62,32 @@ const probes = new Map()
44
62
  /** What the installed weft offers: version, exports, and which catalog codes its compiler reports. */
45
63
  export function installed(root) {
46
64
  const weft = resolveWeft(root)
47
- 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) })
48
67
  return { weft, exports: exportsOf(weft), status: probes.get(weft.compilerPath) }
49
68
  }
50
69
 
51
70
  function status(code, info) {
52
71
  const e = entry(code)
53
72
  const st = info.status[e.code]
54
- 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'
55
77
  if (e.by === 'test') return 'reported by @weft-js/core/test after each test'
56
78
  if (e.by === 'gate') return 'reported by `weft test --gate` on a diff'
57
- 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'
58
- 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`'
59
- 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'
60
- 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'
61
91
  if (e.by === 'runtime') return 'reported in dev by the runtime (a console warning)'
62
92
  if (e.by === 'plugin') return `reported by weft check when \`${e.owner}\` is in \`plugins\``
63
93
  return 'reserved'
@@ -65,31 +95,76 @@ function status(code, info) {
65
95
 
66
96
  function codePage(code, info) {
67
97
  const e = entry(code)
68
- 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
+ ]
69
107
  if (e.example) lines.push('', 'Triggers it:', '', '```tsx', e.example, '```')
70
- 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.')
71
- 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)
72
118
  if (topic) lines.push('', `More: \`weft docs ${topic[0]}\`.`)
73
119
  return lines.join('\n')
74
120
  }
75
121
 
76
- const contributed = async root => (await loadConfig(root)).contributions
122
+ const contributed = async (root) => (await loadConfig(root)).contributions
77
123
 
78
124
  /** What this app's plugins add to the docs (`docs` and `diagnostics` in their manifests). */
79
- const pluginDocs = async root => (await contributed(root)).flatMap(c => (c.manifest.docs ? [c.manifest.docs] : []))
80
- 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
+ )
81
131
 
82
132
  function diagnosticsPage(info, own = []) {
83
- 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}\` |`)]
84
- 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')
85
149
  }
86
150
 
87
151
  export function commandsPage(list, { planned: showPlanned = true } = {}) {
88
152
  const rows = list.tasks
89
- .filter(t => t.available)
90
- .map(t => `| \`${t.command}\` | ${t.description ?? '(no description: P004)'} | ${t.sideEffects?.length ? t.sideEffects.join(', ') : 'none'} |`)
91
- const planned = list.tasks.filter(t => !t.available).map(t => `\`${t.name}\``)
92
- 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()
93
168
  }
94
169
 
95
170
  /**
@@ -101,31 +176,52 @@ export async function docs(root, topic) {
101
176
  root = resolve(root)
102
177
  const key = (topic ?? 'index').trim()
103
178
  if (key === 'index') {
104
- const all = { ...topics, ...Object.fromEntries((await pluginDocs(root)).map(d => [d.topic, d.summary])) }
105
- 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')
106
190
  return { topic: 'index', text, topics: all }
107
191
  }
108
192
  const info = installed(root)
109
193
  if (/^[A-Z]{1,5}\d{3}$/i.test(key)) {
110
194
  const code = key.toUpperCase()
111
- const own = !entry(code) && (await pluginCodes(root)).find(d => d.code === code)
112
- 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}\`.` }
113
- 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' })
114
203
  return { topic: code, text: codePage(code, info) }
115
204
  }
116
- if (key === 'diagnostics') return { topic: key, text: diagnosticsPage(info, await pluginCodes(root)) }
117
- 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))}` }
118
209
  if (key === 'agents') {
119
210
  const { agentsMd } = await import('./agents-md.mjs')
120
211
  return { topic: key, text: (await agentsMd(root)).text }
121
212
  }
122
- const page = (await pluginDocs(root)).find(d => d.topic === key)
213
+ const page = (await pluginDocs(root)).find((d) => d.topic === key)
123
214
  if (page) return { topic: key, text: readFileSync(page.file, 'utf8').trim() }
124
215
  if (fileTopics[key]) {
125
216
  const text = readFileSync(resolve(docsDir, fileTopics[key]), 'utf8').trim()
126
217
  return { topic: key, text: key === 'rules' ? `# The rules\n\n${text}` : text }
127
218
  }
128
- 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
+ )
129
225
  }
130
226
 
131
- 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
+ }