@weft-js/cli 0.0.0-stage → 0.1.1
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.
- package/LICENSE +16 -0
- package/README.md +74 -2
- package/bin/weft.mjs +8 -0
- package/docs/async.md +40 -0
- package/docs/browser.md +39 -0
- package/docs/config.md +93 -0
- package/docs/deploy.md +83 -0
- package/docs/design.md +97 -0
- package/docs/fonts.md +70 -0
- package/docs/icons.md +102 -0
- package/docs/lists.md +23 -0
- package/docs/meta.md +82 -0
- package/docs/plugins.md +143 -0
- package/docs/props.md +77 -0
- package/docs/rules.md +32 -0
- package/docs/rust.md +223 -0
- package/docs/start-short.md +8 -0
- package/docs/start.md +422 -0
- package/docs/state.md +96 -0
- package/docs/style.md +59 -0
- package/docs/surface.md +111 -0
- package/docs/testing.md +71 -0
- package/docs/ui-state.md +92 -0
- package/docs/verify.md +48 -0
- package/package.json +39 -4
- package/src/add-rust.mjs +123 -0
- package/src/add.mjs +358 -0
- package/src/agents-md.mjs +134 -0
- package/src/alias.mjs +59 -0
- package/src/audit.mjs +366 -0
- package/src/browser/cli.mjs +34 -0
- package/src/browser/client.mjs +132 -0
- package/src/browser/commands.mjs +143 -0
- package/src/catalog.mjs +496 -0
- package/src/check.mjs +330 -0
- package/src/cli.mjs +364 -0
- package/src/config.d.ts +1 -0
- package/src/config.mjs +5 -0
- package/src/contributions.mjs +69 -0
- package/src/db-layer.mjs +33 -0
- package/src/deploy/cdn.mjs +64 -0
- package/src/deploy/files.mjs +187 -0
- package/src/deploy/fit.mjs +62 -0
- package/src/deploy/hosts.mjs +56 -0
- package/src/deploy/index.mjs +273 -0
- package/src/deploy/needs.mjs +92 -0
- package/src/design-checks.mjs +439 -0
- package/src/diag.mjs +74 -0
- package/src/docs.mjs +132 -0
- package/src/doctor.mjs +30 -0
- package/src/files.mjs +47 -0
- package/src/fix.mjs +40 -0
- package/src/gate.mjs +276 -0
- package/src/index.mjs +9 -0
- package/src/infer/engine.cjs +525 -0
- package/src/infer/facts.cjs +252 -0
- package/src/infer/hosts.cjs +114 -0
- package/src/infer/lsp.mjs +131 -0
- package/src/infer/session.cjs +105 -0
- package/src/infer/shadow.cjs +46 -0
- package/src/infer/worker.cjs +97 -0
- package/src/jsx-scan.mjs +86 -0
- package/src/load-config.mjs +303 -0
- package/src/map.mjs +110 -0
- package/src/mcp.mjs +195 -0
- package/src/meta-table.mjs +28 -0
- package/src/mutate.mjs +140 -0
- package/src/plugin-checks.mjs +89 -0
- package/src/plugins.mjs +172 -0
- package/src/project.mjs +219 -0
- package/src/props-checks.mjs +46 -0
- package/src/props-listener.mjs +38 -0
- package/src/react-checks.mjs +36 -0
- package/src/share-checks.mjs +74 -0
- package/src/site.mjs +19 -0
- package/src/slots.mjs +96 -0
- package/src/start.mjs +296 -0
- package/src/state-checks.mjs +213 -0
- package/src/style.mjs +320 -0
- package/src/surface-checks.mjs +218 -0
- package/src/tailwind.mjs +22 -0
- package/src/tasks.mjs +99 -0
- package/src/testdiff.mjs +157 -0
- package/src/tests.mjs +572 -0
- package/src/text-checks.mjs +172 -0
- package/src/tokens.mjs +24 -0
- package/src/trust.mjs +282 -0
- package/src/weft.mjs +66 -0
- package/src/workspace.mjs +105 -0
- package/ts-plugin/index.js +137 -0
- package/ts-plugin/package.json +1 -0
package/src/testdiff.mjs
ADDED
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
// Test cases in a test file, and what changed between two versions of one: which tests are new or edited (the ones
|
|
2
|
+
// the gate must prove), and T020, an existing test deleted, skipped or loosened without a written reason.
|
|
3
|
+
//
|
|
4
|
+
// // weft-allow T020: the old assertion pinned the bug this change fixes
|
|
5
|
+
|
|
6
|
+
import ts from 'typescript'
|
|
7
|
+
import { diagnostic, positions } from './diag.mjs'
|
|
8
|
+
import { allowances, isSubject, matcherOf, norm, parse, testCall } from './tests.mjs'
|
|
9
|
+
|
|
10
|
+
const skipMods = new Set(['skip', 'todo'])
|
|
11
|
+
const skipRoots = new Set(['xit', 'xtest', 'xdescribe'])
|
|
12
|
+
|
|
13
|
+
/** A case's name as vitest reports it: suite titles and the test title joined by ' > '. */
|
|
14
|
+
const join = names => names.join(' > ')
|
|
15
|
+
|
|
16
|
+
function nameOf(arg) {
|
|
17
|
+
if (!arg) return { name: '<anonymous>', dynamic: true }
|
|
18
|
+
if (ts.isStringLiteral(arg) || ts.isNoSubstitutionTemplateLiteral(arg)) return { name: arg.text, dynamic: false }
|
|
19
|
+
return { name: arg.getText(), dynamic: true }
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Every test case in one file, in order.
|
|
24
|
+
* @returns {{ name: string, line: number, end: number, body: string, skipped: boolean, fails: boolean, dynamic: boolean, asserts: { key: string, subject: string, matcher: string, not: boolean, args: string, line: number }[], expected: number | null }[]}
|
|
25
|
+
*/
|
|
26
|
+
export function testCases(text, rel) {
|
|
27
|
+
const sf = parse(text, rel)
|
|
28
|
+
const pos = positions(text)
|
|
29
|
+
const out = []
|
|
30
|
+
const visit = (node, suites, skipped, dynamic) => {
|
|
31
|
+
if (ts.isCallExpression(node)) {
|
|
32
|
+
const t = testCall(node)
|
|
33
|
+
if (t) {
|
|
34
|
+
const each = t.mods.some(m => m.name === 'each' || m.name === 'for')
|
|
35
|
+
const { name, dynamic: dyn } = nameOf(node.arguments[0])
|
|
36
|
+
const skip = skipped || skipRoots.has(t.root) || t.mods.some(m => skipMods.has(m.name))
|
|
37
|
+
const isDynamic = dynamic || dyn || each
|
|
38
|
+
if (t.suite) {
|
|
39
|
+
if (t.fn?.body) ts.forEachChild(t.fn.body, c => visit(c, [...suites, name], skip, isDynamic))
|
|
40
|
+
return
|
|
41
|
+
}
|
|
42
|
+
const asserts = []
|
|
43
|
+
let expected = null
|
|
44
|
+
if (t.fn?.body)
|
|
45
|
+
walk(t.fn.body, n => {
|
|
46
|
+
if (!ts.isCallExpression(n)) return
|
|
47
|
+
if (isSubject(n)) {
|
|
48
|
+
const m = matcherOf(n)
|
|
49
|
+
if (!m) return
|
|
50
|
+
const subject = n.arguments[0] ? norm(n.arguments[0]) : ''
|
|
51
|
+
const args = m.call.arguments.map(norm).join(', ')
|
|
52
|
+
asserts.push({ key: `${subject}|${m.not ? 'not.' : ''}${m.name}|${args}`, subject, matcher: m.name, not: m.not, args, line: pos.lineCol(n.getStart(sf))[0] })
|
|
53
|
+
} else if (ts.isPropertyAccessExpression(n.expression) && ts.isIdentifier(n.expression.expression) && n.expression.expression.text === 'expect' && n.expression.name.text === 'assertions') {
|
|
54
|
+
expected = Number(n.arguments[0]?.getText()) || null
|
|
55
|
+
}
|
|
56
|
+
})
|
|
57
|
+
out.push({
|
|
58
|
+
name: join([...suites, name]), line: pos.lineCol(node.getStart(sf))[0], end: pos.lineCol(node.getEnd())[0], body: t.fn?.body ? norm(t.fn.body) : '',
|
|
59
|
+
skipped: skip, fails: t.mods.some(m => m.name === 'fails'), dynamic: isDynamic, asserts, expected,
|
|
60
|
+
})
|
|
61
|
+
return
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
ts.forEachChild(node, c => visit(c, suites, skipped, dynamic))
|
|
65
|
+
}
|
|
66
|
+
visit(sf, [], false, false)
|
|
67
|
+
return out
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function walk(node, fn) {
|
|
71
|
+
const go = n => { fn(n); ts.forEachChild(n, go) }
|
|
72
|
+
go(node)
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** The test names in `after` that are new or whose body changed since `before` (null: the file is new). */
|
|
76
|
+
export function changedCases(before, after, rel) {
|
|
77
|
+
const old = new Map((before == null ? [] : testCases(before, rel)).map(c => [c.name, c]))
|
|
78
|
+
return testCases(after, rel).filter(c => !c.skipped && old.get(c.name)?.body !== c.body)
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Loosening a matcher: the new one accepts everything the old one did, and more. Keyed by the old matcher.
|
|
83
|
+
* Changing an expected value (`toBe(3)` → `toBe(4)`) is not loosening: a fix often has to.
|
|
84
|
+
*/
|
|
85
|
+
const looser = {
|
|
86
|
+
toBe: ['toBeTruthy', 'toBeFalsy', 'toBeDefined', 'toEqual', 'toBeTypeOf', 'toBeInstanceOf'],
|
|
87
|
+
toEqual: ['toBeTruthy', 'toBeDefined', 'toMatchObject', 'toBeTypeOf', 'toBeInstanceOf'],
|
|
88
|
+
toStrictEqual: ['toEqual', 'toBeTruthy', 'toBeDefined', 'toMatchObject'],
|
|
89
|
+
toHaveLength: ['toBeTruthy', 'toBeDefined'],
|
|
90
|
+
toHaveBeenCalledWith: ['toHaveBeenCalled'],
|
|
91
|
+
toHaveBeenCalledTimes: ['toHaveBeenCalled'],
|
|
92
|
+
toHaveBeenLastCalledWith: ['toHaveBeenCalled'],
|
|
93
|
+
toContain: ['toBeTruthy', 'toBeDefined'],
|
|
94
|
+
toMatch: ['toBeTruthy', 'toBeDefined'],
|
|
95
|
+
toThrow: ['toThrow'], // with an argument → without one (checked below)
|
|
96
|
+
toBeNull: ['toBeFalsy'],
|
|
97
|
+
toBeUndefined: ['toBeFalsy'],
|
|
98
|
+
toBeGreaterThan: ['toBeGreaterThanOrEqual'],
|
|
99
|
+
toBeLessThan: ['toBeLessThanOrEqual'],
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function loosened(a, b) {
|
|
103
|
+
if (a.subject !== b.subject || a.not !== b.not) return false
|
|
104
|
+
if (a.matcher === 'toThrow' || a.matcher === 'toThrowError') return b.matcher.startsWith('toThrow') && a.args && !b.args
|
|
105
|
+
return looser[a.matcher]?.includes(b.matcher) ?? false
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* T020 for one test file: an existing test deleted, skipped, marked `.fails`, or with an assertion removed or loosened.
|
|
110
|
+
* `after` null: the whole file was deleted (reported on `before`'s path, line 1).
|
|
111
|
+
*/
|
|
112
|
+
export function weakened(before, after, rel) {
|
|
113
|
+
if (before == null) return []
|
|
114
|
+
const old = testCases(before, rel)
|
|
115
|
+
if (after == null) {
|
|
116
|
+
const live = old.filter(c => !c.skipped)
|
|
117
|
+
return live.length ? [t020(rel, null, 1, `${rel} was deleted with ${live.length} test${live.length > 1 ? 's' : ''} in it (${live.slice(0, 3).map(c => `"${c.name}"`).join(', ')}${live.length > 3 ? ', …' : ''})`)] : []
|
|
118
|
+
}
|
|
119
|
+
const sf = parse(after, rel)
|
|
120
|
+
const allowed = allowances(after, sf)
|
|
121
|
+
const now = testCases(after, rel)
|
|
122
|
+
const byName = new Map(now.map(c => [c.name, c]))
|
|
123
|
+
const byBody = new Set(now.map(c => c.body))
|
|
124
|
+
const out = []
|
|
125
|
+
const report = (line, why) => { if (!allowed('T020', line)) out.push(t020(rel, after, line, why)) }
|
|
126
|
+
for (const o of old) {
|
|
127
|
+
if (o.skipped || o.dynamic) continue
|
|
128
|
+
const n = byName.get(o.name)
|
|
129
|
+
if (!n) {
|
|
130
|
+
if (!byBody.has(o.body)) report(1, `test "${o.name}" was deleted`)
|
|
131
|
+
continue
|
|
132
|
+
}
|
|
133
|
+
if (n.skipped) { report(n.line, `test "${o.name}" is now skipped`); continue }
|
|
134
|
+
if (n.fails && !o.fails) { report(n.line, `test "${o.name}" is now marked \`.fails\``); continue }
|
|
135
|
+
if (o.expected && (n.expected ?? 0) < o.expected) report(n.line, `test "${o.name}" now expects ${n.expected ?? 'no'} assertions instead of ${o.expected}`)
|
|
136
|
+
const nowKeys = new Set(n.asserts.map(a => a.key))
|
|
137
|
+
const oldKeys = new Set(o.asserts.map(a => a.key))
|
|
138
|
+
const gone = o.asserts.filter(a => !nowKeys.has(a.key))
|
|
139
|
+
const added = n.asserts.filter(a => !oldKeys.has(a.key))
|
|
140
|
+
for (const a of gone) {
|
|
141
|
+
const loose = added.find(b => loosened(a, b))
|
|
142
|
+
if (loose) report(loose.line, `in "${o.name}", \`expect(${a.subject}).${a.not ? 'not.' : ''}${a.matcher}(${a.args})\` became \`${loose.not ? 'not.' : ''}${loose.matcher}(${loose.args})\`, which accepts more`)
|
|
143
|
+
}
|
|
144
|
+
const replaced = gone.filter(a => !added.some(b => loosened(a, b)))
|
|
145
|
+
if (n.asserts.length < o.asserts.length && replaced.length)
|
|
146
|
+
report(n.line, `"${o.name}" lost ${o.asserts.length - n.asserts.length} assertion${o.asserts.length - n.asserts.length > 1 ? 's' : ''}: ${replaced.slice(0, 2).map(a => `\`expect(${a.subject}).${a.matcher}(…)\``).join(', ')}`)
|
|
147
|
+
}
|
|
148
|
+
return out
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function t020(rel, _text, line, why) {
|
|
152
|
+
const span = [line, 1, line, 1]
|
|
153
|
+
return diagnostic({
|
|
154
|
+
code: 'T020', file: rel, span, why: `${why}. An existing test got weaker, so it may no longer catch what it used to.`,
|
|
155
|
+
help: 'Restore it; or, when the change really is intended (the old test pinned a bug), keep it with `// weft-allow T020: <reason>` on that test, where the reviewer sees it',
|
|
156
|
+
})
|
|
157
|
+
}
|