@weft-js/cli 0.1.1 → 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.
- package/README.md +38 -14
- package/bin/weft.mjs +8 -1
- package/docs/async.md +16 -6
- package/docs/browser.md +22 -8
- package/docs/config.md +39 -9
- package/docs/core.md +30 -0
- package/docs/deploy.md +33 -8
- package/docs/design.md +12 -3
- package/docs/entrypoints.md +79 -0
- package/docs/fonts.md +12 -2
- package/docs/icons.md +13 -7
- package/docs/lists.md +3 -1
- package/docs/meta.md +13 -3
- package/docs/plugins.md +32 -9
- package/docs/props.md +13 -2
- package/docs/reactivity.md +26 -0
- package/docs/rules.md +19 -12
- package/docs/rust.md +47 -17
- package/docs/start-short.md +3 -2
- package/docs/start.md +226 -52
- package/docs/state.md +33 -7
- package/docs/style.md +31 -8
- package/docs/surface.md +61 -15
- package/docs/testing.md +15 -5
- package/docs/theme.md +182 -0
- package/docs/ui-state.md +19 -6
- package/docs/verify.md +13 -0
- package/package.json +13 -4
- package/src/add/config.mjs +93 -0
- package/src/add/errors.mjs +2 -0
- package/src/add/setup.mjs +88 -0
- package/src/add-rust.mjs +82 -27
- package/src/add.mjs +209 -181
- package/src/agents-md.mjs +144 -43
- package/src/alias.mjs +16 -5
- package/src/audit/page.mjs +313 -0
- package/src/audit/report.mjs +69 -0
- package/src/audit/utils.mjs +5 -0
- package/src/audit.mjs +159 -206
- package/src/browser/cli.mjs +24 -5
- package/src/browser/client.mjs +189 -25
- package/src/browser/commands.mjs +166 -51
- package/src/catalog/audit.mjs +129 -0
- package/src/catalog/auth.mjs +40 -0
- package/src/catalog/compiler.mjs +337 -0
- package/src/catalog/contracts.mjs +18 -0
- package/src/catalog/database.mjs +52 -0
- package/src/catalog/deploy.mjs +117 -0
- package/src/catalog/design.mjs +159 -0
- package/src/catalog/forms.mjs +25 -0
- package/src/catalog/hydration.mjs +29 -0
- package/src/catalog/icons.mjs +24 -0
- package/src/catalog/media.mjs +163 -0
- package/src/catalog/metadata.mjs +116 -0
- package/src/catalog/plugins.mjs +228 -0
- package/src/catalog/realtime.mjs +195 -0
- package/src/catalog/rust.mjs +75 -0
- package/src/catalog/server-functions.mjs +92 -0
- package/src/catalog/state.mjs +39 -0
- package/src/catalog/style-groups.mjs +54 -0
- package/src/catalog/tests.mjs +127 -0
- package/src/catalog/text.mjs +200 -0
- package/src/catalog/ui-state.mjs +42 -0
- package/src/catalog.mjs +67 -460
- package/src/check/runtime.mjs +280 -0
- package/src/check.mjs +198 -208
- package/src/cli/authoring.mjs +213 -0
- package/src/cli/project.mjs +302 -0
- package/src/cli.mjs +222 -250
- package/src/config.mjs +1 -1
- package/src/contributions.mjs +75 -25
- package/src/db-layer.mjs +13 -5
- package/src/deploy/cdn.mjs +94 -16
- package/src/deploy/files.mjs +100 -21
- package/src/deploy/fit.mjs +127 -23
- package/src/deploy/hosts.mjs +90 -20
- package/src/deploy/index/cli.mjs +20 -0
- package/src/deploy/index/environment.mjs +69 -0
- package/src/deploy/index/steps.mjs +87 -0
- package/src/deploy/index.mjs +228 -114
- package/src/deploy/needs.mjs +88 -31
- package/src/design-checks/accessible-names.mjs +62 -0
- package/src/design-checks/contrast.mjs +43 -0
- package/src/design-checks/csp.mjs +60 -0
- package/src/design-checks/files.mjs +59 -0
- package/src/design-checks/heading-links.mjs +61 -0
- package/src/design-checks/interaction.mjs +65 -0
- package/src/design-checks/jsx.mjs +67 -0
- package/src/design-checks/layout-values.mjs +68 -0
- package/src/design-checks/native-controls.mjs +48 -0
- package/src/design-checks/outline.mjs +39 -0
- package/src/design-checks/reporter.mjs +37 -0
- package/src/design-checks/rules.mjs +274 -0
- package/src/design-checks/values.mjs +262 -0
- package/src/design-checks.mjs +10 -400
- package/src/diag.mjs +32 -8
- package/src/docs.mjs +138 -43
- package/src/doctor.mjs +20 -8
- package/src/files.mjs +18 -5
- package/src/fix.mjs +10 -2
- package/src/gate/after.mjs +40 -0
- package/src/gate/before.mjs +54 -0
- package/src/gate/classify.mjs +72 -0
- package/src/gate/mutation.mjs +209 -0
- package/src/gate/resolve-cases.mjs +22 -0
- package/src/gate/result.mjs +63 -0
- package/src/gate/run.mjs +75 -0
- package/src/gate.mjs +104 -202
- package/src/infer/box-props.cjs +140 -0
- package/src/infer/core-boxes.cjs +46 -0
- package/src/infer/derived-writes.cjs +95 -0
- package/src/infer/engine/checks.cjs +193 -0
- package/src/infer/engine/flow.cjs +293 -0
- package/src/infer/engine/report.cjs +158 -0
- package/src/infer/engine/utils.cjs +15 -0
- package/src/infer/engine.cjs +108 -368
- package/src/infer/facts/sites.cjs +114 -0
- package/src/infer/facts.cjs +210 -117
- package/src/infer/hosts.cjs +69 -22
- package/src/infer/lsp.mjs +149 -40
- package/src/infer/session.cjs +73 -17
- package/src/infer/shadow.cjs +13 -5
- package/src/infer/worker.cjs +57 -9
- package/src/jsx-scan.mjs +63 -21
- package/src/load-config/schema.mjs +281 -0
- package/src/load-config.mjs +137 -138
- package/src/map.mjs +93 -26
- package/src/mcp/browser-tools.mjs +155 -0
- package/src/mcp.mjs +147 -69
- package/src/meta-table.mjs +43 -8
- package/src/mutate.mjs +120 -36
- package/src/plugin-checks.mjs +52 -23
- package/src/plugins.mjs +125 -39
- package/src/project.mjs +170 -79
- package/src/props-checks.mjs +24 -6
- package/src/props-listener.mjs +27 -9
- package/src/react-checks.mjs +35 -4
- package/src/server-state-checks.mjs +34 -0
- package/src/share-checks.mjs +121 -37
- package/src/site.mjs +33 -6
- package/src/slots.mjs +104 -19
- package/src/start/build.mjs +116 -0
- package/src/start/routes.mjs +38 -0
- package/src/start.mjs +193 -95
- package/src/state-checks/core-state.mjs +223 -0
- package/src/state-checks/modules.mjs +17 -0
- package/src/state-checks/scan.mjs +232 -0
- package/src/state-checks/server-graph.mjs +65 -0
- package/src/state-checks.mjs +109 -133
- package/src/style/find.mjs +121 -0
- package/src/style.mjs +133 -127
- package/src/surface-checks.mjs +212 -46
- package/src/tailwind.mjs +5 -4
- package/src/tasks.mjs +189 -27
- package/src/test-rules/context.mjs +109 -0
- package/src/test-rules/file-rules.mjs +279 -0
- package/src/test-rules/rule-helpers.mjs +244 -0
- package/src/test-rules/syntax.mjs +223 -0
- package/src/test-rules/test-rules.mjs +127 -0
- package/src/testdiff.mjs +94 -32
- package/src/tests.mjs +31 -483
- package/src/text-checks.mjs +229 -69
- package/src/tokens.mjs +12 -4
- package/src/trust/scan.mjs +95 -0
- package/src/trust.mjs +214 -102
- package/src/weft.mjs +41 -9
- package/src/workspace.mjs +96 -19
- package/ts-plugin/context.js +57 -0
- package/ts-plugin/index.js +4 -124
- package/ts-plugin/mapping.js +65 -0
- 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(
|
|
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:
|
|
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:
|
|
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:
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
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:
|
|
34
|
-
|
|
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:
|
|
37
|
-
|
|
38
|
-
|
|
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))
|
|
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')
|
|
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'))
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
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 = [
|
|
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'))
|
|
72
|
-
|
|
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
|
|
81
|
-
|
|
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 = [
|
|
85
|
-
|
|
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(
|
|
92
|
-
|
|
93
|
-
|
|
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 = {
|
|
106
|
-
|
|
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)
|
|
114
|
-
|
|
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')
|
|
118
|
-
|
|
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(
|
|
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')
|
|
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
|
|
23
|
-
|
|
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
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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([
|
|
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
|
|
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 (
|
|
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]))
|
|
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
|
+
}
|