@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
@@ -0,0 +1,209 @@
1
+ import { existsSync, readFileSync } from 'node:fs'
2
+ import { join, relative } from 'node:path'
3
+ import { diagnostic } from '../diag.mjs'
4
+ import { apply, changedLines, mutants as makeMutants, sample, suggest } from '../mutate.mjs'
5
+ import { isTestFile } from '../tests.mjs'
6
+ import { copyProject, removeCopy, runVitest, writeFiles } from '../workspace.mjs'
7
+
8
+ export async function mutate({
9
+ options,
10
+ root,
11
+ source,
12
+ testFiles,
13
+ importsOf,
14
+ code,
15
+ posix,
16
+ isSupport,
17
+ maxMutants,
18
+ byKey,
19
+ result,
20
+ log,
21
+ concurrency,
22
+ budgetSec,
23
+ started,
24
+ after,
25
+ bin,
26
+ compileError,
27
+ allowed,
28
+ diagnostics,
29
+ }) {
30
+ // Mutation: the changed source lines (or, with no source change, the source files the tests import).
31
+ if (options.mutation !== false) {
32
+ const targets = mutationTargets({
33
+ root,
34
+ source,
35
+ testFiles,
36
+ importsOf,
37
+ code,
38
+ posix,
39
+ isSupport,
40
+ result,
41
+ })
42
+ const all = targets.flatMap((t) => makeMutants(t.text, t.rel, t.lines))
43
+ const list = sample(all, maxMutants)
44
+ result.mutants.generated = all.length
45
+ result.mutants.sampled = list.length < all.length
46
+ const passing = [...byKey.values()].filter((t) => t.after === 'passed')
47
+ const files = [...new Set(passing.map((t) => t.file))]
48
+ if (list.length && files.length) {
49
+ log(
50
+ `gate: ${list.length}${result.mutants.sampled ? ` of ${all.length}` : ''} mutants on ${targets.length} files, ${concurrency} at a time, ${budgetSec}s budget`,
51
+ )
52
+ const outcomes = await runMutants({
53
+ root,
54
+ options,
55
+ targets,
56
+ started,
57
+ budgetSec,
58
+ after,
59
+ list,
60
+ result,
61
+ files,
62
+ bin,
63
+ concurrency,
64
+ })
65
+ recordMutants({ outcomes, result, compileError, passing, allowed, diagnostics })
66
+ } else if (!list.length) result.notes.push('no mutants on the changed lines')
67
+ }
68
+ }
69
+
70
+ function mutationTargets({ root, source, testFiles, importsOf, code, posix, isSupport, result }) {
71
+ // Only code the changed tests can reach (runtime imports): a mutant nothing runs survives by definition.
72
+ const reach = (limit) => {
73
+ const seen = new Set()
74
+ const walk = (abs, depth) => {
75
+ if (seen.has(abs) || depth > limit || !existsSync(abs)) return
76
+ seen.add(abs)
77
+ for (const p of importsOf(abs, readFileSync(abs, 'utf8'))) walk(p, depth + 1)
78
+ }
79
+ for (const f of testFiles) walk(join(root, f), 0)
80
+ return seen
81
+ }
82
+ const reachable = reach(12)
83
+ const unreached = source
84
+ .filter((c) => c.after != null && code(c.rel) && !reachable.has(join(root, c.rel)))
85
+ .map((c) => c.rel)
86
+ if (unreached.length)
87
+ result.notes.push(`not mutated (no changed test imports them): ${unreached.join(', ')}`)
88
+ let targets = source
89
+ .filter((c) => c.after != null && reachable.has(join(root, c.rel)))
90
+ .map((c) => ({ rel: c.rel, text: c.after, lines: changedLines(c.before, c.after) }))
91
+ if (!source.length) {
92
+ targets = [...reach(3)]
93
+ .map((abs) => posix(relative(root, abs)))
94
+ .filter(
95
+ (rel) =>
96
+ !isTestFile(rel) &&
97
+ code(rel) &&
98
+ !isSupport(rel) &&
99
+ !rel.startsWith('..') &&
100
+ !rel.includes('node_modules'),
101
+ )
102
+ .map((rel) => {
103
+ const text = readFileSync(join(root, rel), 'utf8')
104
+ return { rel, text, lines: changedLines(null, text) }
105
+ })
106
+ }
107
+ return targets
108
+ }
109
+
110
+ async function runMutants({
111
+ root,
112
+ options,
113
+ targets,
114
+ started,
115
+ budgetSec,
116
+ after,
117
+ list,
118
+ result,
119
+ files,
120
+ bin,
121
+ concurrency,
122
+ }) {
123
+ const texts = Object.fromEntries(targets.map((t) => [t.rel, t.text]))
124
+ const deadline = started + budgetSec * 1000 + after.ms
125
+ const timeoutMs = Math.max(15_000, after.ms * 3)
126
+ const queue = [...list]
127
+ const outcomes = []
128
+ const worker = async (i) => {
129
+ const dir = copyProject(root, { exclude: options.exclude, id: `mutant-${i}` })
130
+ try {
131
+ for (let m; (m = queue.shift());) {
132
+ if (performance.now() > deadline) {
133
+ result.mutants.sampled = true
134
+ queue.length = 0
135
+ break
136
+ }
137
+ writeFiles(dir, { [m.file]: apply(texts[m.file], m) })
138
+ const res = await runVitest(dir, files, { bin, config: options.config, timeoutMs })
139
+ writeFiles(dir, { [m.file]: texts[m.file] })
140
+ outcomes.push({ m, res })
141
+ }
142
+ } finally {
143
+ removeCopy(dir)
144
+ }
145
+ }
146
+ await Promise.all(Array.from({ length: Math.min(concurrency, list.length) }, (_, i) => worker(i)))
147
+ return outcomes
148
+ }
149
+
150
+ function recordMutants({ outcomes, result, compileError, passing, allowed, diagnostics }) {
151
+ for (const { m, res } of outcomes) {
152
+ result.mutants.run++
153
+ if (res.timedOut) {
154
+ result.mutants.timedOut++
155
+ result.mutants.killed++
156
+ continue
157
+ }
158
+ const loadOnly = res.tests.size === 0 && res.loadErrors.size > 0
159
+ if (loadOnly && [...res.loadErrors.values()].some(compileError)) {
160
+ result.mutants.invalid++
161
+ continue
162
+ }
163
+ let killed = false
164
+ for (const t of passing) {
165
+ const st = res.loadErrors.has(t.file)
166
+ ? 'failed'
167
+ : res.tests.get(`${t.file}::${t.name}`)?.status
168
+ if (st && st !== 'passed') {
169
+ t.kills++
170
+ killed = true
171
+ }
172
+ }
173
+ if (killed) result.mutants.killed++
174
+ else {
175
+ result.mutants.survived++
176
+ result.survivors.push({
177
+ file: m.file,
178
+ line: m.line,
179
+ column: m.column,
180
+ op: m.op,
181
+ original: m.original.slice(0, 80),
182
+ replacement: m.replacement,
183
+ suggestion: suggest(m),
184
+ })
185
+ }
186
+ }
187
+ const valid = result.mutants.run - result.mutants.invalid
188
+ if (valid > 0)
189
+ for (const t of passing) {
190
+ if (t.kills > 0 || t.before === 'passed') continue
191
+ if (allowed('T011', t.file, t.line)) {
192
+ result.notes.push(`T011 allowed on "${t.name}"`)
193
+ continue
194
+ }
195
+ const partial = result.mutants.sampled
196
+ diagnostics.push(
197
+ diagnostic({
198
+ code: 'T011',
199
+ severity: partial ? 'warning' : 'error',
200
+ file: t.file,
201
+ span: [t.line, 1, t.line, 1],
202
+ why: `"${t.name}" killed none of the ${valid} mutants of the changed lines${partial ? ` (a sample of ${result.mutants.generated})` : ''}: it would still pass if that code were wrong`,
203
+ help: result.survivors[0]
204
+ ? `Assert what the changed code computes. For example: ${result.survivors[0].suggestion}`
205
+ : 'Assert what the changed code computes',
206
+ }),
207
+ )
208
+ }
209
+ }
@@ -0,0 +1,22 @@
1
+ export function resolver(testFiles, cases) {
2
+ const resolveCases = (res) => {
3
+ // Static names map 1:1; results nobody claimed belong to the file's dynamic (`.each`) cases.
4
+ const out = new Map()
5
+ for (const f of testFiles) {
6
+ const mine = cases.filter((c) => c.file === f)
7
+ const keys = [...res.tests.keys()].filter((k) => k.startsWith(`${f}::`))
8
+ for (const c of mine.filter((x) => !x.dynamic))
9
+ out.set(
10
+ `${f}::${c.name}`,
11
+ res.loadErrors.has(f)
12
+ ? { status: 'failed', message: res.loadErrors.get(f), load: true }
13
+ : (res.tests.get(`${f}::${c.name}`) ?? { status: 'missing' }),
14
+ )
15
+ if (mine.some((c) => c.dynamic))
16
+ for (const k of keys) if (!out.has(k)) out.set(k, res.tests.get(k))
17
+ }
18
+ return out
19
+ }
20
+
21
+ return resolveCases
22
+ }
@@ -0,0 +1,63 @@
1
+ import { sortDiagnostics, summarize } from '../diag.mjs'
2
+
3
+ export function makeResult({
4
+ root,
5
+ options,
6
+ tests,
7
+ support,
8
+ source,
9
+ diagnostics,
10
+ budgetSec,
11
+ started,
12
+ }) {
13
+ const result = {
14
+ version: 1,
15
+ ok: true,
16
+ root,
17
+ base: options.changes ? null : (options.base ?? 'HEAD'),
18
+ files: {
19
+ tests: tests.map((c) => c.rel),
20
+ support: support.map((c) => c.rel),
21
+ source: source.map((c) => c.rel),
22
+ },
23
+ tests: [],
24
+ mutants: {
25
+ generated: 0,
26
+ run: 0,
27
+ killed: 0,
28
+ survived: 0,
29
+ timedOut: 0,
30
+ invalid: 0,
31
+ sampled: false,
32
+ budgetSec,
33
+ },
34
+ survivors: [],
35
+ diagnostics,
36
+ summary: null,
37
+ notes: [],
38
+ ms: 0,
39
+ }
40
+ const finish = () => {
41
+ sortDiagnostics(diagnostics)
42
+ // Each test's verdict: it ran green on the change, failed before it (when there was a before), and no error
43
+ // diagnostic falls inside it.
44
+ for (const t of result.tests) {
45
+ t.codes = diagnostics
46
+ .filter(
47
+ (d) =>
48
+ d.severity === 'error' &&
49
+ d.file === t.file &&
50
+ d.span[0] >= t.line &&
51
+ d.span[0] <= t.end,
52
+ )
53
+ .map((d) => d.code)
54
+ t.accepted = t.after === 'passed' && t.before !== 'passed' && !t.codes.length
55
+ }
56
+ result.summary = { changedTests: result.tests.length, ...summarize(diagnostics) }
57
+ result.ok = result.summary.errors === 0 && result.tests.every((t) => t.accepted)
58
+ result.ms = Math.round(performance.now() - started)
59
+ return result
60
+ }
61
+
62
+ return { result, finish }
63
+ }
@@ -0,0 +1,75 @@
1
+ import { runVitest, vitestBin } from '../workspace.mjs'
2
+ import { afterRun } from './after.mjs'
3
+ import { beforeRun } from './before.mjs'
4
+ import { mutate } from './mutation.mjs'
5
+
6
+ export async function runGate({
7
+ root,
8
+ options,
9
+ cases,
10
+ testFiles,
11
+ log,
12
+ result,
13
+ source,
14
+ allowed,
15
+ diagnostics,
16
+ importsOf,
17
+ code,
18
+ posix,
19
+ isSupport,
20
+ maxMutants,
21
+ concurrency,
22
+ budgetSec,
23
+ started,
24
+ compileError,
25
+ }) {
26
+ const bin = vitestBin(root)
27
+ const run = (dir, files) =>
28
+ runVitest(dir, files, { bin, config: options.config, timeoutMs: 180_000 })
29
+
30
+ const { after, resolveCases, byKey } = await afterRun({
31
+ cases,
32
+ testFiles,
33
+ log,
34
+ root,
35
+ options,
36
+ run,
37
+ result,
38
+ })
39
+ await beforeRun({
40
+ source,
41
+ options,
42
+ log,
43
+ root,
44
+ run,
45
+ testFiles,
46
+ resolveCases,
47
+ byKey,
48
+ allowed,
49
+ result,
50
+ diagnostics,
51
+ })
52
+
53
+ await mutate({
54
+ options,
55
+ root,
56
+ source,
57
+ testFiles,
58
+ importsOf,
59
+ code,
60
+ posix,
61
+ isSupport,
62
+ maxMutants,
63
+ byKey,
64
+ result,
65
+ log,
66
+ concurrency,
67
+ budgetSec,
68
+ started,
69
+ after,
70
+ bin,
71
+ compileError,
72
+ allowed,
73
+ diagnostics,
74
+ })
75
+ }