@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/catalog.mjs
ADDED
|
@@ -0,0 +1,496 @@
|
|
|
1
|
+
// The diagnostic catalog the agent layer knows about: PLAN §3.5 (W), §4.2 (W050–W056, T012), §7b (T001–T009,
|
|
2
|
+
// P001–P006), research 13 §4.4 (P100–P120), research 20 §6, §10 (H001–H008, H011–H016), PLAN §12 (H009, H010, partial hydration), research 23 §3 (D020–D025, style groups), research 37 (D001–D039, the design language),
|
|
3
|
+
// research 11 §8.5 and 15 §7.5 as built in research 26 §7 (Phase 2's X-codes, M001–M013), research 27 §6 (Q001–Q010,
|
|
4
|
+
// server functions, and S011), research 28 §7 (Q011–Q015, the database; A001–A004, auth; C001–C002, the contract),
|
|
5
|
+
// research 29 §7 (S001, S002, S012, S013, state on the server; F001–F003, forms; M016–M018, the media route), research
|
|
6
|
+
// 30 §8 (W052, W053, T013, R001–R023, realtime), research 36 §8 and 39 (R101–R109, Rust), research 46 §12 (E001–E014, deploy), plus the codes proposed in research 17 (marked `proposed`).
|
|
7
|
+
//
|
|
8
|
+
// W-codes come from the compiler. Each has an `example` that should trigger it; `probe()` runs the installed
|
|
9
|
+
// compiler over the examples, so AGENTS.md and `docs` only claim what this weft version actually reports. T-codes
|
|
10
|
+
// come from `weft check`'s test pass (tests.mjs); their examples are test files, checked the same way.
|
|
11
|
+
|
|
12
|
+
/** @typedef {{ code: string, severity: 'error' | 'warning' | 'info', catches: string, fix: string, by: 'compiler' | 'cli' | 'test' | 'gate' | 'start' | 'runtime' | 'plugin' | 'reserved', owner?: string, example?: string, proposed?: boolean }} Entry */
|
|
13
|
+
|
|
14
|
+
/** @type {Entry[]} */
|
|
15
|
+
export const catalog = [
|
|
16
|
+
{ code: 'W000', severity: 'error', by: 'compiler', catches: 'Syntax error; nothing else in the file is checked', fix: 'Fix the syntax error first' },
|
|
17
|
+
{ code: 'W080', severity: 'error', by: 'cli', catches: 'A component with an untyped parameter that nothing renders: its props have nothing to be inferred from', fix: 'Render it, delete it, or annotate the parameter' },
|
|
18
|
+
{ code: 'W081', severity: 'error', by: 'cli', catches: 'An exported component with no written prop type', fix: '`weft fix` writes the inferred type (`weft dev` does this on save)' },
|
|
19
|
+
{ code: 'W082', severity: 'error', by: 'cli', catches: 'A generated `/*inferred*/` prop type no longer matches the callers', fix: '`weft fix` rewrites it, or delete the comment to keep the type you have' },
|
|
20
|
+
{ code: 'W083', severity: 'warning', by: 'cli', catches: 'An `any` weakens an inferred prop; names where the `any` came from', fix: 'Give the source a type, or annotate the prop' },
|
|
21
|
+
{ code: 'W084', severity: 'error', by: 'cli', catches: 'One caller passes a type the component body can\'t handle, and the others agree', fix: 'Change the caller, or make the component handle both' },
|
|
22
|
+
{ code: 'W085', severity: 'warning', by: 'cli', catches: 'An inferred prop names a type the component\'s scope can\'t see', fix: 'Move the type to module scope or export it, or annotate the parameter' },
|
|
23
|
+
{
|
|
24
|
+
code: 'W001', severity: 'error', by: 'compiler',
|
|
25
|
+
catches: 'A prop or `.value` used to compute something in the component body outside `derived`/`watch`/JSX/handlers (the body runs once). `const x = props.y` and destructuring only name the prop and stay live',
|
|
26
|
+
fix: 'Wrap in `derived`, move into `watch`, or `untrack()` if a snapshot is intended',
|
|
27
|
+
example: 'export function A({ items }: any) { const n = items.length + 1; return <p>{n}</p> }',
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
code: 'W002', severity: 'error', by: 'compiler', catches: 'Writing state inside `derived`', fix: 'Move the write to a handler, or make the target `derived`',
|
|
31
|
+
example: "import { state, derived } from '@weft-js/core'\nexport function A() { const a = state(0); const b = state(0); const d = derived(() => { b.value = a.value; return a.value }); return <p>{d.value}</p> }",
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
code: 'W003', severity: 'error', by: 'compiler', catches: '`watch` writes state that its own source reads (cycle)', fix: 'Use `derived`',
|
|
35
|
+
example: "import { state, watch } from '@weft-js/core'\nexport function A() { const a = state(0); watch(() => a.value, v => { a.value = v + 1 }); return <p>{a.value}</p> }",
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
code: 'W004', severity: 'warning', by: 'compiler', catches: '`watch` that only copies one value into another', fix: 'Replace with `derived`',
|
|
39
|
+
example: "import { state, watch } from '@weft-js/core'\nexport function A() { const a = state(0); const b = state(0); watch(() => a.value, v => { b.value = v }); return <p>{b.value}</p> }",
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
code: 'W005', severity: 'error', by: 'compiler', catches: "`state(prop)`: React's `useState(prop)` bug, the box never follows the prop", fix: '`state.linked(() => prop)`, or `untrack` for a one-time copy',
|
|
43
|
+
example: "import { state } from '@weft-js/core'\nexport function A({ v }: any) { const s = state(v); return <p>{s.value}</p> }",
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
code: 'W006', severity: 'error', by: 'compiler', catches: "Nested prop destructuring (`({ user: { name } })`) can't stay live", fix: 'Read at the use site: `user.name`',
|
|
47
|
+
example: 'export function A({ user: { name } }: any) { return <p>{name}</p> }',
|
|
48
|
+
},
|
|
49
|
+
{ code: 'W007', severity: 'error', by: 'compiler', catches: 'Assigning to a `Derived` (maps TS2540)', fix: 'Make it `state` or `state.linked`' },
|
|
50
|
+
{ code: 'W008', severity: 'error', by: 'compiler', catches: 'Box/value mismatch at a prop (a `State<T>` where `T` is expected, or the reverse)', fix: 'Pass `count.value` / pass `count`' },
|
|
51
|
+
{
|
|
52
|
+
code: 'W010', severity: 'error', by: 'compiler', catches: '`.map` over objects without `key`', fix: '`key={item.id}` on the row root',
|
|
53
|
+
example: 'export function A({ xs }: any) { return <ul>{xs.map((x: any) => <li>{x.name}</li>)}</ul> }',
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
code: 'W011', severity: 'error', by: 'compiler', catches: 'A number on the left of `&&` (renders `0`)', fix: '`cond > 0 &&`',
|
|
57
|
+
example: 'export function A({ xs }: any) { return <div>{xs.length && <p>has</p>}</div> }',
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
code: 'W012', severity: 'error', by: 'compiler', catches: '`derived(async …)`, or reads after `await` in `resource` (untracked)', fix: 'Use `resource`; read before the `await`',
|
|
61
|
+
example: "import { derived } from '@weft-js/core'\nexport function A() { const d = derived(async () => 1); return <p>{d.value}</p> }",
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
code: 'W013', severity: 'error', by: 'compiler', catches: 'Early `return` inside a JSX `.map` callback', fix: 'Filter first: `xs.filter(…).map(…)`',
|
|
65
|
+
example: 'export function A({ xs }: any) { return <ul>{xs.map((x: any) => { if (!x.ok) return null; return <li key={x.id}>{x.name}</li> })}</ul> }',
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
code: 'W014', severity: 'error', by: 'compiler',
|
|
69
|
+
catches: 'Identity-breaking `.map(p => ({ ...p, … }))` feeding a keyed list: every row re-runs on every change (in-file, and across files via props in `weft check`)',
|
|
70
|
+
fix: 'Replace only the changed item (`p.id === id ? { ...p, price } : p`), or read the changing field live',
|
|
71
|
+
example: "import { state } from '@weft-js/core'\nexport function A() { const xs = state([{ id: 'a', price: 1 }]); const push = (price: number) => { xs.value = xs.value.map(p => ({ ...p, price })) }; return <ul onClick={() => push(2)}>{xs.value.map(p => <li key={p.id}>{p.price}</li>)}</ul> }",
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
code: 'W015', severity: 'error', by: 'compiler', catches: 'A box rendered without `.value` in JSX (a child, or a native attribute)', fix: 'Add `.value`',
|
|
75
|
+
example: "import { state } from '@weft-js/core'\nexport function A() { const n = state(0); return <p>{n}</p> }",
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
code: 'W017', severity: 'warning', by: 'compiler',
|
|
79
|
+
catches: 'One entry of a whole object read per row (`cart[p.id]`, `cart.value[p.id]` in a keyed-list row or its props, `box.value[prop]` in a row component): every change re-runs every row',
|
|
80
|
+
fix: 'Read the entry from the box: `cart.get(p.id)`; to a row component pass the box (`State<Cart>`) and read `.get(id)` there',
|
|
81
|
+
example: "export function A({ xs, cart }: any) { return <ul>{xs.map((x: any) => <li key={x.id}>{cart[x.id]}</li>)}</ul> }",
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
code: 'W018', severity: 'warning', by: 'compiler',
|
|
85
|
+
catches: '`const x = untrack(() => …reactive…)` in a component body that only the JSX reads: it renders the setup-time value and never updates',
|
|
86
|
+
fix: '`derived(() => …)` and render `x.value`; `untrack` only for a value meant to stay fixed',
|
|
87
|
+
example: "import { untrack, type State } from '@weft-js/core'\nexport function A({ id, live }: { id: string; live: State<Record<string, number>> }) { const n = untrack(() => live.get(id)); return <p>{n}</p> }",
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
code: 'W019', severity: 'error', by: 'compiler', catches: 'Unsupported row pattern', fix: 'Read from one item param',
|
|
91
|
+
example: "export function L({ xs }) { return <ul>{xs.map(({ id, ...more }) => <li key={id}>{id}</li>)}</ul> }",
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
code: 'W020', severity: 'error', by: 'compiler', catches: 'React-isms (W020–W029): `useState`, `useEffect`, `useMemo`, `useRef`, `useCallback`, custom `useX()` hooks, dependency arrays',
|
|
95
|
+
fix: '`state`, `watch`, `derived`, `ref={el}`, plain functions',
|
|
96
|
+
example: "import { useState } from 'react'\nexport function A() { const [n] = useState(0); return <p>{n}</p> }",
|
|
97
|
+
},
|
|
98
|
+
{ code: 'W030', severity: 'error', by: 'compiler', catches: 'Module-level `state` reachable from the server entry graph (leaks across requests)', fix: '`context`, or mark `state.global`' },
|
|
99
|
+
{
|
|
100
|
+
code: 'W040', severity: 'error', by: 'compiler', catches: 'Early `return` in a component body that depends on state (runs once, never re-checked)', fix: 'Branch in JSX',
|
|
101
|
+
example: "import { state } from '@weft-js/core'\nexport function A() { const n = state(0); if (n.value > 1) return null; return <p>{n.value}</p> }",
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
code: 'W041', severity: 'error', by: 'compiler', catches: '`let` variable mutated in a handler and read in JSX (never updates)', fix: 'Make it `state`',
|
|
105
|
+
example: 'export function A() { let n = 0; return <button onClick={() => { n++ }}>{n}</button> }',
|
|
106
|
+
},
|
|
107
|
+
{ code: 'W044', severity: 'warning', by: 'cli', catches: 'A `key` read from `crypto.randomUUID()`, `Date.now()`, `Math.random()` or `performance.now()`: it differs between server and client, so the row is rebuilt on hydration and on every render. At runtime the same miss logs `a key differs between server and client`', fix: 'Key on a stable id from the data' },
|
|
108
|
+
{
|
|
109
|
+
code: 'W043', severity: 'warning', by: 'cli', catches: '`data-x={true}` or `data-x={false}` on a native element: `data-*` booleans are presence flags, so these write `data-x=""` or nothing, not React\'s `"true"` and `"false"`',
|
|
110
|
+
fix: '`data-x` for a flag (`weft fix`), or `data-x="true"` for the word. CSS matches `[data-x]`',
|
|
111
|
+
},
|
|
112
|
+
{
|
|
113
|
+
code: 'W050', severity: 'error', by: 'compiler',
|
|
114
|
+
catches: 'Raw `addEventListener`, `setInterval`, `new WebSocket` or `new EventSource` in a component with no cleanup (it outlives the component)',
|
|
115
|
+
fix: '`on(target, type, fn)` / `interval(ms, fn)` from @weft-js/core/browser, or clean it up in `onCleanup`',
|
|
116
|
+
example: "export function A() { window.addEventListener('keydown', () => {}); return <p /> }",
|
|
117
|
+
},
|
|
118
|
+
{
|
|
119
|
+
code: 'W051', severity: 'error', by: 'compiler',
|
|
120
|
+
catches: 'A @weft-js/core/browser value, `on` or `interval` created outside an owner: module scope, a timer or promise callback, an event handler, after `await`',
|
|
121
|
+
fix: "Create it in a component's (or `defineState`'s) setup, or inside `createRoot(dispose => …)`",
|
|
122
|
+
example: "import { online } from '@weft-js/core/browser'\nexport const up = online()",
|
|
123
|
+
},
|
|
124
|
+
{
|
|
125
|
+
code: 'W052', severity: 'error', by: 'compiler',
|
|
126
|
+
catches: '`watch` folding a stream by hand',
|
|
127
|
+
fix: '`fold` + `initial` where it is read: kept in the cache',
|
|
128
|
+
example: "import { state, watch } from '@weft-js/core'\nimport { prices } from './server/prices'\nexport function A() { const tick = prices({}); const list = state([]); watch(() => tick.value, m => { list.value = [...list.value, m] }); return <p>{list.value.length}</p> }",
|
|
129
|
+
},
|
|
130
|
+
{ code: 'W053', severity: 'error', by: 'start', catches: '`.live()` on a query with no `tags` (dev): no mutation can ever poke it', fix: 'Add `tags` to the query, and the same tag to the mutations that change it' },
|
|
131
|
+
{
|
|
132
|
+
code: 'W054', severity: 'error', by: 'compiler',
|
|
133
|
+
catches: '`media(query)` without `ssr` in a server-rendered component (hydration mismatch); a warning when `weft check` cannot tell',
|
|
134
|
+
fix: "Pass the server value: `media(query, { ssr: false })`",
|
|
135
|
+
example: "import { media } from '@weft-js/core/browser'\nexport function A() { const dark = media('(prefers-color-scheme: dark)'); return <p>{dark.value ? 'dark' : 'light'}</p> }",
|
|
136
|
+
},
|
|
137
|
+
{
|
|
138
|
+
code: 'W056', severity: 'warning', by: 'compiler', catches: '`watch` that copies a live value (a @weft-js/core/browser value, `.live()`) into `state`',
|
|
139
|
+
fix: 'Read `live.value` directly, or `derived(() => live.value)`',
|
|
140
|
+
example: "import { state, watch } from '@weft-js/core'\nimport { online } from '@weft-js/core/browser'\nexport function A() { const isOnline = online(); const up = state(true); watch(() => isOnline.value, v => { up.value = v }); return <p>{up.value}</p> }",
|
|
141
|
+
},
|
|
142
|
+
{
|
|
143
|
+
code: 'W060', severity: 'error', by: 'compiler', catches: "A `group()` from @weft-js/core/style that can't be evaluated at build: a value that isn't a literal (named with its path), a `group()` outside a top-level `const`, or an accent that isn't literal tokens",
|
|
144
|
+
fix: 'Inline the token name or literal; define groups as `export const x = group(\'x\', { … })`; raw CSS goes in `css: { … }`',
|
|
145
|
+
example: "import { group } from '@weft-js/core/style'\nconst px = 13\nexport const g = group('g', { base: { pad: px } })",
|
|
146
|
+
},
|
|
147
|
+
{
|
|
148
|
+
code: 'W061', severity: 'error', by: 'compiler', catches: '`bind:x` on a native element, or `bind:x`/`bind(box)` with something that isn\'t a box or that isn\'t a component prop',
|
|
149
|
+
fix: 'Write `<Card bind:person={name} />`. A form element takes `value` and `onChange`',
|
|
150
|
+
example: "import { bind, state } from '@weft-js/core'\nexport function A() { const n = state(1); return <input value={bind(n)} /> }",
|
|
151
|
+
},
|
|
152
|
+
|
|
153
|
+
// The design language (PLAN §6, research 37): tokens and layout where the app has warp's tokens, contrast and accessibility everywhere.
|
|
154
|
+
{ code: 'D001', severity: 'error', by: 'cli', catches: 'A raw colour in `style`, app CSS, or a Tailwind arbitrary or palette class, in an app with warp\'s tokens: it won\'t follow the theme, the dials or dark mode', fix: 'Use a colour token or a `tone`. No autofix: a token follows the theme, the raw value didn\'t' },
|
|
155
|
+
{ code: 'D002', severity: 'error', by: 'cli', catches: 'Spacing off the scale, or a scale step written by hand (`padding: 16px`, `p-[13px]`, `gap-3`)', fix: 'A step of `hair tight related group section page`; autofix when the value is exactly one' },
|
|
156
|
+
{ code: 'D003', severity: 'warning', by: 'cli', catches: 'Raw type, radius or shadow (`font-size`, `text-lg`, `rounded-[7px]`, `box-shadow`)', fix: '`<Heading role>` / `<Text role>`, the radius tokens, `--warp-elevation-*`; autofix for an exact radius' },
|
|
157
|
+
{ code: 'D004', severity: 'warning', by: 'cli', catches: 'Layout spelled out on an element (`display: flex`, `flex flex-col gap-4`) where a primitive says it', fix: '`<Stack>` or `<Row>`; a suggested edit when nothing else is on the element' },
|
|
158
|
+
{ code: 'D040', severity: 'warning', by: 'cli', catches: 'The style gate (`data-weft="styled"`) on the main content or a page heading: the first paint hides what it should show', fix: 'Mark the secondary part that needs its late styles, or drop the marker' },
|
|
159
|
+
{ code: 'D041', severity: 'error', by: 'cli', catches: '`aria-label` or `aria-labelledby` on a `<div>`, `<span>` or other element whose role may not be named: screen readers drop it (axe aria-prohibited-attr)', fix: 'Put the words in the element (a visually hidden `<span data-part="sr-only">` keeps them off screen), or give it a role that takes a name: `role="img"` for a mark or icon that means something' },
|
|
160
|
+
{ code: 'D005', severity: 'warning', by: 'cli', catches: 'A browser-painted control in a warp app: `<button>`, `<select>`, `<textarea>`, an input type like `date`, `number` or `range`, or a `title` tooltip', fix: 'The warp component: focus ring, sweep, tones and touch target come with it' },
|
|
161
|
+
{ code: 'D006', severity: 'warning', by: 'cli', catches: 'One heading level at two roles across an app (`h1` as both `title` and `display`), or a bare `<hN>` beside `<Heading>`', fix: 'Drop `role`, or match the rest: `Heading` picks the role from `level`. A suggested edit at each site' },
|
|
162
|
+
{ code: 'D007', severity: 'warning', by: 'cli', catches: 'A bare `<a href>` in a warp app', fix: '`<Link>`, or `Shell`\'s `nav` for navigation (suggested edit)' },
|
|
163
|
+
{ code: 'D011', severity: 'error', by: 'cli', catches: 'A literal text and background colour pair on one element below 4.5:1', fix: 'Use tokens: they pass at every dial stop' },
|
|
164
|
+
{ code: 'D030', severity: 'error', by: 'cli', catches: 'An `<input>`, `<select>` or `<textarea>` with no label', fix: 'A wrapping `<label>`, `<label for>`, `aria-label`, or warp\'s `<Input label>`' },
|
|
165
|
+
{ code: 'D031', severity: 'error', by: 'cli', catches: 'A button or link with no accessible name (icon only, or empty)', fix: 'Text, or `aria-label`; warp\'s Button takes `label`' },
|
|
166
|
+
{ code: 'D032', severity: 'error', by: 'cli', catches: '`<img>` with no `alt`', fix: 'Describe the image; `alt=""` when it is decoration' },
|
|
167
|
+
{ code: 'D033', severity: 'error', by: 'cli', catches: '`onClick` on a non-interactive element with no role: mouse only', fix: 'A real `<button type="button">` (suggested edit)' },
|
|
168
|
+
{ code: 'D034', severity: 'error', by: 'cli', catches: 'A control inside a control (`<a><button>`)', fix: 'One control: move the other beside it' },
|
|
169
|
+
{ code: 'D035', severity: 'warning', by: 'cli', catches: 'A heading level that jumps (h1 to h3)', fix: 'Use the next level; `<Heading level role>` keeps the look apart from the outline' },
|
|
170
|
+
{ code: 'D036', severity: 'error', by: 'cli', catches: 'The focus ring removed with nothing in its place (`outline: none`, `outline-none`)', fix: 'Remove it: warp\'s `:focus-visible` is the ring. Or add a `focus-visible:` style' },
|
|
171
|
+
{ code: 'D037', severity: 'error', by: 'cli', catches: 'A positive `tabindex`', fix: '`0`, and the order follows the DOM (autofix)' },
|
|
172
|
+
{ code: 'D038', severity: 'warning', by: 'cli', catches: 'Two solid Buttons side by side', fix: 'One primary action per region: make the others `soft` or `ghost`' },
|
|
173
|
+
{ code: 'D039', severity: 'warning', by: 'cli', catches: '`<html>` with no `lang`', fix: 'Set `lang`' },
|
|
174
|
+
|
|
175
|
+
// Style groups (research 23 §3). D010 is warp's contrast gate (its own plugin); D040 and D081 are reserved by warp's docs.
|
|
176
|
+
{
|
|
177
|
+
code: 'D020', severity: 'warning', by: 'cli', catches: 'Class soup: a static `class`/`className` with more than 8 classes, or the same class list (4+ classes) in two or more places',
|
|
178
|
+
fix: "Extract a style group (`group('order-card', …)`, name suggested from the component) and spread it; `weft style find` first, one may already fit",
|
|
179
|
+
},
|
|
180
|
+
{ code: 'D021', severity: 'warning', by: 'cli', catches: 'Two style groups resolve to 90% or more of the same CSS (`similar` in .weft/styles.json)', fix: 'Merge them into one group with an axis for what differs' },
|
|
181
|
+
{ code: 'D022', severity: 'info', by: 'cli', catches: 'The same accent (`accent: { … }` at a call site) on one group at 3 or more sites', fix: 'Promote it to an axis value and pass that value at those sites' },
|
|
182
|
+
{ code: 'D023', severity: 'warning', by: 'cli', catches: 'A `css: { … }` escape (raw CSS) in a style group', fix: 'Use the token the fix names; keep the escape only where no intent sets the property' },
|
|
183
|
+
{
|
|
184
|
+
code: 'D024', severity: 'error', by: 'compiler',
|
|
185
|
+
catches: 'Styles that fight with no order: two group roots spread on one element; two compounds that can match at once and set one property differently, or an accent that sets what the site\'s own axis value sets (`weft check`)',
|
|
186
|
+
fix: 'One group per element (nest an element, or make it an axis); exclusive compound `when`s; drop the accent or make it an axis value',
|
|
187
|
+
example: "import { group } from '@weft-js/core/style'\nconst a = group('a', { base: { pad: 'group' } })\nconst b = group('b', { base: { gap: 'related' } })\nexport function X() { return <li {...a()} {...b()} /> }",
|
|
188
|
+
},
|
|
189
|
+
{
|
|
190
|
+
code: 'D025', severity: 'error', by: 'compiler',
|
|
191
|
+
catches: "Unknown name in a style group: an axis value, axis, state or part a call site uses that the group doesn't have, or an intent or token that isn't in the vocabulary (also a type error)",
|
|
192
|
+
fix: 'Use one of the names listed (the closest come first); `weft style explain <group>` shows them all',
|
|
193
|
+
example: "import { group } from '@weft-js/core/style'\nconst card = group('card', { axes: { status: { open: {}, late: {} } } })\nexport function X() { return <article {...card({ status: 'lost' })} /> }",
|
|
194
|
+
},
|
|
195
|
+
|
|
196
|
+
{
|
|
197
|
+
code: 'T001', severity: 'error', by: 'cli', catches: 'A test with no assertion (or an `expect(x)` with no matcher): it passes as long as nothing throws. `@weft-js/core/test` also fails such a test at runtime',
|
|
198
|
+
fix: 'Assert what the user sees: `expect(screen.getByRole(…)).toHaveTextContent(…)`',
|
|
199
|
+
example: "import { render } from '@weft-js/core/test'\nimport { Cart } from './Cart'\ntest('renders', () => { render(Cart) })",
|
|
200
|
+
},
|
|
201
|
+
{
|
|
202
|
+
code: 'T002', severity: 'error', by: 'cli', catches: 'Tautology: `expect` on a literal, or a value compared with itself', fix: 'Assert on the output of the code under test',
|
|
203
|
+
example: "test('works', () => { expect(true).toBe(true) })",
|
|
204
|
+
},
|
|
205
|
+
{
|
|
206
|
+
code: 'T003', severity: 'error', by: 'cli', catches: 'Asserting a value the test just set (`n.value = 5; expect(n.value).toBe(5)`) without going through the UI or the graph',
|
|
207
|
+
fix: 'Act through the UI (`await user.click(…)`) or assert on what depends on the value',
|
|
208
|
+
example: "import { state } from '@weft-js/core'\ntest('count', () => { const n = state(0); n.value = 5; expect(n.value).toBe(5) })",
|
|
209
|
+
},
|
|
210
|
+
{
|
|
211
|
+
code: 'T004', severity: 'error', by: 'cli', catches: 'Sleeps, real timers or `waitFor` polling in a test', fix: '`await flush()`; for time, `clock.install()` and `await clock.advance(ms)` from @weft-js/core/test',
|
|
212
|
+
example: "test('later', async () => { await new Promise(r => setTimeout(r, 100)); expect(1 + 1).toBe(2) })",
|
|
213
|
+
},
|
|
214
|
+
{
|
|
215
|
+
code: 'T005', severity: 'warning', by: 'cli', catches: 'Implementation query: `container`, CSS selectors, classes, `innerHTML`, test ids, or app state read directly in a component test',
|
|
216
|
+
fix: 'Query by role, label or text (`screen.getByRole`, `getByLabelText`, `getByText`)',
|
|
217
|
+
example: "import { render } from '@weft-js/core/test'\nimport { Cart } from './Cart'\ntest('total', () => { const { container } = render(Cart); expect(container.querySelector('.total')!.textContent).toBe('$0') })",
|
|
218
|
+
},
|
|
219
|
+
{
|
|
220
|
+
code: 'T006', severity: 'error', by: 'cli', catches: 'Mocking weft (`vi.mock(\'@weft-js/core\')`) or the unit under test, so the test checks the mock', fix: 'Fake the edges (network, storage) and render the real component',
|
|
221
|
+
example: "import { render } from '@weft-js/core/test'\nimport { Cart } from './Cart'\nvi.mock('./Cart')\ntest('cart', () => { render(Cart); expect(document.body.textContent).toBe('') })",
|
|
222
|
+
},
|
|
223
|
+
{
|
|
224
|
+
code: 'T007', severity: 'error', by: 'cli', catches: 'Snapshot without approval: it records today\'s output and passes against it', fix: 'Assert the behavior with role/label/text queries, or approve it with `// weft-allow T007: <reason>`',
|
|
225
|
+
example: "import { render } from '@weft-js/core/test'\nimport { Cart } from './Cart'\ntest('cart', () => { render(Cart); expect(document.body.innerHTML).toMatchSnapshot() })",
|
|
226
|
+
},
|
|
227
|
+
{
|
|
228
|
+
code: 'T008', severity: 'error', by: 'cli', catches: 'Conditional `expect` (`if`, `?:`, `&&`, `catch`), or a test whose only `expect`s are in a loop', fix: 'Make the condition its own assertion, then assert unconditionally; `expect(fn).toThrow()` for errors',
|
|
229
|
+
example: "test('maybe', () => { const xs = [1, 2]; if (xs.length > 5) expect(xs[0]).toBe(1) })",
|
|
230
|
+
},
|
|
231
|
+
{
|
|
232
|
+
code: 'T009', severity: 'error', by: 'cli', catches: '`.only` or `.skip` committed (also `fit`, `xit`, `xdescribe`)', fix: 'Remove `.only`; fix or delete a skipped test (`test.fails` for one meant to fail)',
|
|
233
|
+
example: "test.only('focused', () => { expect(Math.max(1, 2)).toBe(2) })",
|
|
234
|
+
},
|
|
235
|
+
{
|
|
236
|
+
code: 'T010', severity: 'error', by: 'gate', catches: 'A new or changed test passes on the code before the change (fail-before), so it does not test the change',
|
|
237
|
+
fix: 'Assert what the change makes different; a test for existing behavior goes in its own change with no source edits',
|
|
238
|
+
},
|
|
239
|
+
{
|
|
240
|
+
code: 'T011', severity: 'error', by: 'gate', catches: 'A new test kills none of the mutants of the changed lines: it would still pass if that code were wrong (a warning when the mutants were sampled)',
|
|
241
|
+
fix: "Assert what the changed code computes; the gate's surviving mutants say what to check",
|
|
242
|
+
},
|
|
243
|
+
{
|
|
244
|
+
code: 'T012', severity: 'error', by: 'test', catches: 'A test ends with @weft-js/core/browser listeners, intervals or subscribed values still open (checked by @weft-js/core/test after each test)',
|
|
245
|
+
fix: 'Unmount what the test created (dispose any `createRoot`), or fix the leak',
|
|
246
|
+
},
|
|
247
|
+
|
|
248
|
+
{
|
|
249
|
+
code: 'T013', severity: 'error', by: 'cli', catches: 'A real `WebSocket` or `EventSource` in a fast test',
|
|
250
|
+
fix: 'Use the fake transport: `net.push`, `net.poke`, `net.drop`',
|
|
251
|
+
example: "test('feed', () => { const socket = new WebSocket('ws://localhost/feed'); expect(socket).toBeTruthy() })",
|
|
252
|
+
},
|
|
253
|
+
{
|
|
254
|
+
code: 'T020', severity: 'error', by: 'gate', catches: 'An existing test deleted, skipped, marked `.fails`, or with an assertion removed or loosened (`toBe(3)` → `toBeTruthy()`)',
|
|
255
|
+
fix: 'Restore it, or keep the change with `// weft-allow T020: <reason>` on that test',
|
|
256
|
+
},
|
|
257
|
+
|
|
258
|
+
{ code: 'P001', severity: 'warning', by: 'reserved', catches: 'Stray top-level script file outside a declared task', fix: 'Declare it in `tasks`, or delete it' },
|
|
259
|
+
{ code: 'P002', severity: 'warning', by: 'cli', catches: '`package.json` script that is not a `weft …` alias', fix: 'Move it to `tasks` in weft.config.ts and alias `weft run <name>`' },
|
|
260
|
+
{ code: 'P003', severity: 'error', by: 'cli', catches: 'Task duplicates a built-in command or another task (same name or same command)', fix: 'Rename it or remove the duplicate' },
|
|
261
|
+
{ code: 'P004', severity: 'error', by: 'cli', catches: 'Task without `description` or `sideEffects`', fix: 'Add both (`sideEffects: []` when it has none)' },
|
|
262
|
+
{ code: 'P005', severity: 'error', by: 'reserved', catches: 'Write outside `scope.write`', fix: 'Revert the write, or widen `scope.write` deliberately' },
|
|
263
|
+
{ code: 'P006', severity: 'warning', by: 'reserved', catches: 'Unreferenced one-off script', fix: 'Delete it, or declare it as a task' },
|
|
264
|
+
{ code: 'P007', severity: 'error', by: 'cli', proposed: true, catches: 'weft.config.ts fails to load or fails the schema (the Zod path is given)', fix: 'Fix the key at the path shown' },
|
|
265
|
+
{ code: 'P008', severity: 'error', by: 'cli', proposed: true, catches: '`slots` names a slot id weft does not define', fix: 'Use an id from `weft check --json` `slots`' },
|
|
266
|
+
{ code: 'P009', severity: 'error', by: 'cli', proposed: true, catches: '`slots` picks a winner that is not in `plugins` (or does not provide that slot)', fix: 'Add the plugin, or pick one that provides the slot' },
|
|
267
|
+
|
|
268
|
+
// Metadata and share cards (research 20 §6; H for head, M is media). `start`: reported by @weft-js/start at request or
|
|
269
|
+
// render time, and by `weft routes --meta` for every route.
|
|
270
|
+
{ code: 'H009', severity: 'error', by: 'runtime', catches: 'A prop of an island is something JSON can\'t carry (a function, a Date, a class instance), found as the server renders', fix: 'Pass plain data, or put `/* @weft client */` on the component that renders it' },
|
|
271
|
+
{ code: 'H010', severity: 'info', by: 'cli', catches: 'An island has to load with the page for a reason its code could avoid: work that reaches past it, a module that runs as it loads, a box at module level, code the compiler can\'t see into. `.weft/hydration.json` says why for every island', fix: '`/* @weft visible */` above the component when its work only matters on screen; otherwise the fix the diagnostic names' },
|
|
272
|
+
{ code: 'H001', severity: 'warning', by: 'cli', catches: 'No `site.url`: a build has no absolute canonical, og:url or og:image, and a server takes them from the request\'s Host header', fix: 'Set `site.url`, or `WEFT_SITE_URL` in the deploy environment' },
|
|
273
|
+
{ code: 'H002', severity: 'error', by: 'cli', catches: 'Param route with an `auto` image on a static-only preset and no `prerender` (`weft routes --meta --preset static`)', fix: 'Export `prerender`, or set `og.image`' },
|
|
274
|
+
{ code: 'H003', severity: 'warning', by: 'cli', catches: 'No description after merging the config, layouts and route (`weft check`, per route and on `site`)', fix: 'Set `site.description` (or the route\'s `meta.description`)' },
|
|
275
|
+
{ code: 'H004', severity: 'warning', by: 'cli', catches: 'Title over 60 characters or description over 160 (search results and cards cut them)', fix: 'Shorten it' },
|
|
276
|
+
{ code: 'H011', severity: 'warning', by: 'cli', catches: 'A page sets no `meta`, or leaves out its title or description: they come from what it renders (its `h1`, its first paragraph) or the site default', fix: 'Add `meta` when you build the page, in words written for it; the diagnostic carries a starting point' },
|
|
277
|
+
{ code: 'H012', severity: 'warning', by: 'cli', catches: 'Two routes have the same title', fix: 'Give each page a title from what is on it' },
|
|
278
|
+
{ code: 'H013', severity: 'warning', by: 'cli', catches: 'Two routes have the same description, usually the site default', fix: 'Give each page a description from what is on it' },
|
|
279
|
+
{ code: 'H014', severity: 'warning', by: 'cli', catches: 'A param route whose `meta` ignores `data` and `params`, or whose real pages all have the same title or description', fix: 'Build `meta` from `data` and `params`: `({ data }) => ({ title: data.name, description: data.summary })`' },
|
|
280
|
+
{ code: 'H015', severity: 'warning', by: 'cli', catches: 'A written description that reads like code, a markdown fragment or a cut-off sentence', fix: 'Write one plain sentence. Never slice raw markdown or code into `description`' },
|
|
281
|
+
{ code: 'H016', severity: 'info', by: 'cli', catches: 'A route `weft check` couldn\'t fully check: it failed here, or a param route with no page to sample', fix: 'Export `prerender`, or link to a page of it; `check: { routes: false }` turns the route pass off' },
|
|
282
|
+
// The rendered page (`weft audit`, and the end of `weft build`): what Lighthouse, a crawler and a screen reader see
|
|
283
|
+
{ code: 'L000', severity: 'warning', by: 'cli', catches: 'The built pages weren\'t audited: no Chrome or Chromium on this machine, or no prerendered pages', fix: 'Install Chrome, or set `WEFT_CHROME`; `weft audit --url <deploy>` audits a running site' },
|
|
284
|
+
{ code: 'L001', severity: 'error', by: 'cli', catches: 'A control (button, link, field, frame) with no accessible name in the rendered page, at a phone\'s width or a desktop\'s: often CSS that hid its only text', fix: 'Give it a name: `label` on a warp `Button` or `aria-label` on an element. For a button that shows its text on wide screens, `collapse="phone"` (or `"tablet"`) hides the text there and keeps it as the name; `display: none` takes the name with it.' },
|
|
285
|
+
{ code: 'L002', severity: 'error', by: 'cli', catches: 'An image with no text alternative in the rendered page', fix: '`alt` on an `<img>` (`alt=""` if it shows nothing a reader needs), `label` on warp\'s `Icon` and `Image`.' },
|
|
286
|
+
{ code: 'L003', severity: 'error', by: 'cli', catches: 'Text under WCAG contrast (4.5:1, or 3:1 when large) against what is actually behind it in the rendered page', fix: 'Text needs 4.5:1 against what is behind it (3:1 at 24px, or 18.7px bold). Use warp\'s text roles (`--warp-color-text`, `--warp-color-text-subtle`) on warp surfaces instead of a raw or faded colour.' },
|
|
287
|
+
{ code: 'L004', severity: 'error', by: 'cli', catches: 'Another axe-core WCAG A/AA rule the rendered page breaks (the rule Lighthouse runs for Accessibility and Agentic Browsing)', fix: 'Each finding names the axe rule, its first remedy and the rule\'s page.' },
|
|
288
|
+
{ code: 'L010', severity: 'error', by: 'cli', catches: 'An uncaught exception or a `console.error` while the page loads', fix: 'Fix the error the page throws or logs; `weft browser open <route>` then `weft browser snapshot` shows it with the page.' },
|
|
289
|
+
{ code: 'L011', severity: 'error', by: 'cli', catches: 'The browser logged an error loading the page: usually a missing file (404)', fix: 'Usually a file the page asks for that is missing (a 404): fix the URL or ship the file.' },
|
|
290
|
+
{ code: 'L012', severity: 'error', by: 'cli', catches: 'A deprecated browser API in use (Lighthouse Best Practices)', fix: 'Replace the deprecated API; Chrome\'s message names what replaces it.' },
|
|
291
|
+
{ code: 'L013', severity: 'error', by: 'cli', catches: 'An issue Chrome logs and Lighthouse counts: mixed content, a blocked cookie, a CSP or CORS failure', fix: 'Open the page in Chrome and read the Issues panel for the request or element it names.' },
|
|
292
|
+
{ code: 'L020', severity: 'error', by: 'cli', catches: 'A built page with no `<title>`', fix: '`meta: { title }` on the route, or `site.title` in weft.config.ts.' },
|
|
293
|
+
{ code: 'L021', severity: 'warning', by: 'cli', catches: 'A built page with no meta description', fix: '`meta: { description }` on the route, or `site.description` in weft.config.ts.' },
|
|
294
|
+
{ code: 'L022', severity: 'error', by: 'cli', catches: '`<html>` with no `lang`', fix: '`site.locale` in weft.config.ts sets it.' },
|
|
295
|
+
{ code: 'L023', severity: 'error', by: 'cli', catches: 'No `width=device-width` viewport meta: phones render at 980px and shrink the page', fix: 'weft writes it; a custom `head` or document must keep it.' },
|
|
296
|
+
{ code: 'L024', severity: 'warning', by: 'cli', catches: 'A page told search engines not to index it (meta robots, or the host\'s `x-robots-tag` on a preview URL)', fix: 'Remove the `noindex` if the page should be found. On a host\'s preview URL, audit the production URL instead: `weft audit --url <it>`.' },
|
|
297
|
+
{ code: 'L025', severity: 'error', by: 'cli', catches: 'A page answered with a 4xx or 5xx status', fix: 'The route errors or is missing: run it with `weft dev` and read the server log.' },
|
|
298
|
+
{ code: 'L030', severity: 'warning', by: 'cli', catches: 'The page scrolls sideways at a phone\'s width', fix: 'Let the named element shrink: `max-inline-size: 100%`, `min-inline-size: 0` on a flex or grid child, or `overflow-x: auto` on wide content, with `tabindex="0"`, a role and a name so a keyboard can scroll it (warp\'s `Table` does this).' },
|
|
299
|
+
{ code: 'L031', severity: 'warning', by: 'cli', catches: 'The layout moved while the page loaded (CLS over 0.05; an error over 0.1)', fix: 'Reserve the space before it fills: `width` and `height` on images and video, a fixed `block-size` for what loads late, and fonts through weft\'s text() plugin (it matches fallback metrics).' },
|
|
300
|
+
{ code: 'H005', severity: 'error', by: 'start', catches: '`.og.tsx` uses a style or element the card renderer does not support (an error in dev; production drops the declaration)', fix: 'Use flexbox, text, images, borders, radius, gradients, shadows' },
|
|
301
|
+
{ code: 'H006', severity: 'error', by: 'cli', catches: '`meta` reads a box or calls a hook (metadata is plain data); found statically by `weft check`', fix: 'Compute it from `params` and `data`' },
|
|
302
|
+
{ code: 'H007', severity: 'error', by: 'start', catches: '`meta` (or a card) reads a deferred (streamed) field of the loader\'s data: the request fails with 500 instead of a page with missing tags', fix: 'Await the field in `load`, or leave it out of `meta`' },
|
|
303
|
+
{ code: 'H008', severity: 'warning', by: 'start', catches: 'A card\'s `<img>` didn\'t load (timeout, HTTP error, not an image, too large, host not allowed); the card renders without it', fix: 'Fix the URL, or embed the image with an import' },
|
|
304
|
+
|
|
305
|
+
// Server functions and the query cache (research 27 §6; Q for query). `start`: reported by @weft-js/start while serving
|
|
306
|
+
// (the server log, and the error the call rejects with); `runtime`: thrown or warned by the client runtime.
|
|
307
|
+
{ code: 'Q001', severity: 'error', by: 'runtime', catches: '`q()` (a live query, a Resource) called with no owner: in `load`, a handler, a timer or module scope', fix: '`await q.fetch(input)` there; `q()` in components and defineState scopes' },
|
|
308
|
+
{ code: 'Q002', severity: 'error', by: 'start', catches: 'A query\'s or mutation\'s input failed its schema (400, with the issues and their paths)', fix: 'Pass what the schema accepts' },
|
|
309
|
+
{ code: 'Q003', severity: 'error', by: 'start', catches: '`query`/`mutation` defined outside a server module: its `run` and imports would ship to the client (the build fails)', fix: 'Move it to `src/server/**` or a `*.server.ts` file' },
|
|
310
|
+
{ code: 'Q004', severity: 'error', by: 'start', catches: 'A mutation called while the server handles a GET (`load`, a page render, a query\'s `run`)', fix: 'Call it from a handler or another mutation; reads go in queries' },
|
|
311
|
+
{ code: 'Q005', severity: 'warning', by: 'start', catches: 'A server function\'s result doesn\'t survive JSON (`Date`, `Map`, `Set`, a class, `undefined` in an array, a function, `NaN`): the client sees another value', fix: 'Send plain data (ISO strings, arrays, objects)' },
|
|
312
|
+
{ code: 'Q006', severity: 'error', by: 'start', catches: 'A cached (`cache: { life }`) query reads `ctx.request`, `ctx.response` or `ctx.user`: its value is shared by every user', fix: 'Drop `cache`, or pass the value as input' },
|
|
313
|
+
{ code: 'Q007', severity: 'error', by: 'start', catches: '`redirect()` after the page started streaming (a component\'s query): the status is already sent', fix: 'Call the query in the route\'s `load`, which runs before the first byte' },
|
|
314
|
+
{ code: 'Q008', severity: 'warning', by: 'start', catches: `A mutation \`invalidates\` a tag no query declares (\`todo:*\` covers \`todo:\${id}\`): nothing refreshes`, fix: 'Fix the tag name, or add it to the query\'s `tags`' },
|
|
315
|
+
{ code: 'Q009', severity: 'error', by: 'start', catches: '`/_weft/fn/<id>` names no function: the page is older than the server, or the module isn\'t a server module', fix: 'Reload; keep `query`/`mutation` exports in server modules' },
|
|
316
|
+
{ code: 'Q010', severity: 'error', by: 'start', catches: 'A mutation request without the CSRF proof (POST, JSON, `x-weft: 1`, same origin): 403', fix: 'Call mutations through their stub, not a hand-made `fetch` or form' },
|
|
317
|
+
{ code: 'S011', severity: 'warning', by: 'runtime', catches: '`mutation({ optimistic })` updated a query its invalidated tags didn\'t cover: the prediction has no fresh value to land on (held until a refetch)', fix: 'Add one of the query\'s tags to `invalidates`' },
|
|
318
|
+
{ code: 'S001', severity: 'error', by: 'cli', catches: 'A server object or array (`q().value`, `await q.fetch()`, or a map, filter or spread of one) copied into `state`: it goes stale and its rows lose their identity', fix: 'Reference the query and `derived` from it; store ids; `state.linked` for edits, `untrack` for a deliberate snapshot' },
|
|
319
|
+
{ code: 'S002', severity: 'error', by: 'runtime', catches: 'A `defineState` scope reached on the server outside a render (module scope, `load`, a server function, a timer): it would be shared by every request', fix: 'Read it in a component, a handler or a `derived`; pass values into server code as arguments' },
|
|
320
|
+
{ code: 'U001', severity: 'error', by: 'cli', catches: 'A `share` key built from `Math.random`, `Date.now` or `crypto.randomUUID`: no other tab or client will ever have the same one', fix: `Build it from a stable id: \`share: () => \`todo:\${props.id}\`\`` },
|
|
321
|
+
{ code: 'U002', severity: 'warning', by: 'cli', catches: 'A `share` key taken from the URL or something typed: `realtime({ share })` then authorises a string the user chose', fix: 'Build it from data the server knows, and have the rule check it' },
|
|
322
|
+
{ code: 'U003', severity: 'warning', by: 'cli', catches: '`interaction({ share })` in an app with no `realtime({ share })`: tabs work, and other clients are refused (R011)', fix: 'Add `share: ({ ctx, key }) => …` to `realtime()` in `src/realtime.server.ts`, or leave it if tabs are all you want' },
|
|
323
|
+
{ code: 'U004', severity: 'error', by: 'cli', catches: 'Writing `row.remote`: it is what others are doing, and only the network writes it', fix: 'Write `row.hover`, or keep your own `state`' },
|
|
324
|
+
{ code: 'U005', severity: 'error', by: 'cli', catches: '`ui` on a warp component that does not take it: an attribute is written and nothing is drawn', fix: 'Use Button, Checkbox, Switch, Radio, Toggle, Chip or Card, or read the row yourself' },
|
|
325
|
+
{ code: 'S012', severity: 'warning', by: 'cli', catches: '`state.linked` over a query value with no `key`: a refetch resets what someone is typing', fix: 'Add `{ key: () => source.value?.id }`' },
|
|
326
|
+
{ code: 'S013', severity: 'warning', by: 'runtime', catches: 'A `local` or `session` persisted box read in a server render: the page shows `init`, then flashes to the stored value', fix: '`persist: { in: \'cookie\' }` if it\'s small, or render it client-only' },
|
|
327
|
+
{ code: 'F001', severity: 'error', by: 'runtime', catches: '`<Form of>` was given something that isn\'t a mutation', fix: '`<Form of={addTodo}>`, with `addTodo` a `mutation()` export of a server module' },
|
|
328
|
+
{ code: 'F002', severity: 'error', by: 'start', catches: 'A form POST without the same-origin proof (`Origin`, or `Sec-Fetch-Site: same-origin`): 403', fix: 'Submit through a `<Form>` on the same origin; behind a proxy, forward the host so the origins match' },
|
|
329
|
+
{ code: 'F003', severity: 'error', by: 'start', catches: 'A form POST naming no known mutation: the page is older than the server', fix: 'Reload; keep `mutation` exports in server modules' },
|
|
330
|
+
{ code: 'Q011', severity: 'warning', by: 'start', catches: 'A function read or wrote a table its `tags`/`invalidates` don\'t name (dev): a mutation of that table won\'t refresh it', fix: 'Add the table\'s name (or `table:…`) to `tags`/`invalidates`' },
|
|
331
|
+
{ code: 'Q012', severity: 'error', by: 'start', catches: '`@weft-js/start/db*` or `@weft-js/start/auth` reached the client graph', fix: 'Import it from a server module (`src/server/**`, `*.server.ts`) or a route\'s `load`' },
|
|
332
|
+
{ code: 'Q013', severity: 'error', by: 'start', catches: 'A database URL the driver can\'t open (a `postgres://` URL given to the SQLite entry, or the reverse)', fix: 'Use the matching `createDb`: `@weft-js/start/db/sqlite` or `@weft-js/start/db/postgres`' },
|
|
333
|
+
{ code: 'Q014', severity: 'error', by: 'start', catches: 'The database doesn\'t match the schema: a table or column is missing', fix: 'Run `weft db generate`, then reload (new migrations apply on the next request)' },
|
|
334
|
+
{ code: 'I001', severity: 'error', by: 'cli', catches: '`<Icon name="as">`, or `Icons.As`, where no such icon exists', fix: 'Use the name it suggests. `weft docs icons` has the set; an editor completes it from the type.' },
|
|
335
|
+
{ code: 'I002', severity: 'info', by: 'cli', catches: 'An icon name only known at runtime, so that use loads the pack', fix: 'Nothing to do if the name comes from data. To fetch single icons instead, set `icons({ source })`. A list of literals, or a map over one, ships only those.' },
|
|
336
|
+
{ code: 'I003', severity: 'info', by: 'cli', catches: 'The icon pack used as a value, so all of it ships with the page', fix: 'Use `Icons.X`, or `Icons[name]`, which loads lazily.' },
|
|
337
|
+
{ code: 'Q015', severity: 'warning', by: 'cli', catches: '`src/db/schema.ts` changed since the last `weft db generate`', fix: 'Run `weft db generate`' },
|
|
338
|
+
{ code: 'Q016', severity: 'error', by: 'start', catches: 'A request used the database while it was down, or a migration it needs failed (503; the server log has the driver\'s error). Pages that run no statements still serve', fix: 'Start the database and check `DATABASE_URL`, or fix the migration: the next statement tries again' },
|
|
339
|
+
{ code: 'A001', severity: 'error', by: 'start', catches: 'The auth secret is missing or under 32 characters on a production server', fix: 'Set `BETTER_AUTH_SECRET` (`openssl rand -base64 32`) in the server\'s environment' },
|
|
340
|
+
{ code: 'A002', severity: 'error', by: 'start', catches: 'A function with `auth: \'user\'` was called with no session (401)', fix: 'Sign in first, or guard the route with `redirect()` in its `load`' },
|
|
341
|
+
{ code: 'A003', severity: 'error', by: 'start', catches: '`auth: \'user\'`, but `ctx` has no `user`: the context file never asked for the session', fix: 'Spread `session(auth, request)` into what `src/context.server.ts` returns' },
|
|
342
|
+
{ code: 'A004', severity: 'warning', by: 'start', catches: 'A mutation changed the session cookie without a redirect: the tab keeps what the last session cached', fix: '`redirect()` after it: that\'s a full load, which drops the client cache' },
|
|
343
|
+
{ code: 'A005', severity: 'error', by: 'start', catches: 'The database provider has no Better Auth adapter', fix: 'Pass `options.database` to `createAuth`, or use a provider that has one' },
|
|
344
|
+
{ code: 'C001', severity: 'warning', by: 'cli', catches: 'A function\'s `input` or `output` schema can\'t become JSON Schema: the contract says `{}`', fix: 'Use a schema with JSON Schema support (Zod 4 has it), or drop what can\'t be represented' },
|
|
345
|
+
{ code: 'C002', severity: 'error', by: 'start', catches: 'A function\'s value failed its `output` schema (dev and tests)', fix: 'Fix `run`, or the schema' },
|
|
346
|
+
|
|
347
|
+
// Realtime (research 30 §8, research 32 §8; R for realtime).
|
|
348
|
+
{ code: 'R001', severity: 'error', by: 'start', catches: '`channel` or `sharedState` without `authorize` (`weft check`, `weft build`, and thrown at definition)', fix: 'Add it (`() => true` must be written explicitly)' },
|
|
349
|
+
{ code: 'R002', severity: 'error', by: 'cli', catches: 'The memory bus on a multi-instance preset, or `durableObjectBus` outside `cloudflare_durable` (`weft build` fails): a message reaches one instance\'s clients', fix: "`weft add redis` or `weft add nats`, or `realtime({ bus: postgresBus({ url }) })` in `src/realtime.server.ts`" },
|
|
350
|
+
{ code: 'R003', severity: 'warning', by: 'start', catches: 'A mutation wrote a table a live query reads, and their tags don\'t intersect (dev): open pages won\'t update', fix: 'Add one of the query\'s tags to `invalidates`' },
|
|
351
|
+
{ code: 'R004', severity: 'error', by: 'start', catches: 'A wildcard in `realtime({ origins })`, or a hand-written WebSocket handler (`crossws`, `defineWebSocketHandler`) that skips the Origin check: any site could join with a visitor\'s cookies', fix: 'List the origins, scheme and port included; use `channel`' },
|
|
352
|
+
{ code: 'R005', severity: 'warning', by: 'cli', catches: 'A stream\'s `input`, a channel\'s `params` or client messages, or a shared key or value has an unbounded string or array (`weft build`): a client decides how much the server holds', fix: 'Add `.max()`' },
|
|
353
|
+
{ code: 'R006', severity: 'warning', by: 'start', catches: 'A stream\'s `run` read the database before it subscribed to a topic (dev): what\'s published in between is lost', fix: 'Call `topic.subscribe()` first, or pass `after`' },
|
|
354
|
+
{ code: 'R007', severity: 'warning', by: 'cli', catches: '`ws`, `socket.io-client`, `reconnecting-websocket` or `@microsoft/fetch-event-source` in dependencies: weft owns the connection', fix: 'Use `stream`, `.live()` or `channel`' },
|
|
355
|
+
{ code: 'R008', severity: 'error', by: 'start', catches: 'A message over 1 MiB, a request or socket frame over 64 KiB, or a client over the channel rate limit (20 messages a second)', fix: 'Send less, or an id the client fetches; `limits` raises it' },
|
|
356
|
+
{ code: 'R009', severity: 'error', by: 'start', catches: 'Over 100 subscriptions or joins on a connection, or 10 connections or sockets for a user', fix: 'Read fewer keys; `limits` raises it' },
|
|
357
|
+
{ code: 'R010', severity: 'error', by: 'start', catches: 'An origin that isn\'t the app\'s own or in `realtime({ origins })` tried to open a connection', fix: 'List it, scheme and port included' },
|
|
358
|
+
{ code: 'R011', severity: 'error', by: 'start', catches: 'A channel\'s `authorize` refused a join or a write (the status is `unauthorized`), at the join or a session re-check', fix: 'Sign in, or check `authorize`' },
|
|
359
|
+
{ code: 'R012', severity: 'warning', by: 'start', catches: 'A live query reads a table `changes` reports and no tag names it, or a table `changes` doesn\'t list (dev): writes from outside the app won\'t refresh it', fix: 'Add the table\'s name to `tags`, or the table to `dbChanges`' },
|
|
360
|
+
{ code: 'R013', severity: 'error', by: 'cli', catches: 'A query marked `live: { required: true }` on a target that can\'t deliver a change feed: a functions preset with no external listener, or no `changes` at all (`weft check`, `weft build`, `weft doctor`)', fix: 'Run `weft db listen` on an always-on host and set `listener: \'external\'`, deploy to a server preset, add `changes: dbChanges(db, [tables])`, or drop `required`' },
|
|
361
|
+
{ code: 'R014', severity: 'warning', by: 'cli', catches: '`changes` on a preset with no long-running process: writes from outside the app won\'t reach `.live()`, which shows `degraded`', fix: 'Run `weft db listen` on an always-on host and set `listener: \'external\'`, or deploy to a server preset' },
|
|
362
|
+
{ code: 'R015', severity: 'error', by: 'start', catches: 'A required live query, and the server\'s startup probe found no real change feed (a pooler, no trigger, no trigger permission, a Mongo without a replica set): the server stops rather than serve stale data, and dev answers 503', fix: 'What the error names: `dbChanges(db, tables, { listenUrl })` for a direct connection, `weft db live` for triggers' },
|
|
363
|
+
{ code: 'R016', severity: 'warning', by: 'start', catches: 'The server settled on a degraded live mode (`poll` or `off`), or a working feed went down: `.live()` shows `degraded` or `reconnecting`', fix: 'What the log line names; `weft doctor` lists the options' },
|
|
364
|
+
{ code: 'R017', severity: 'error', by: 'start', catches: '`dbChanges` on a client it can\'t watch or something that isn\'t a table, or `weft db listen` on the memory bus, which stops at that process', fix: 'Use a client from `@weft-js/start/db/*` or `mongoChanges`; set `bus` in `src/realtime.server.ts`' },
|
|
365
|
+
{ code: 'R019', severity: 'error', by: 'cli', catches: 'A query\'s `live: { verify }` round trip failed against its target (`weft check`, `weft build`): the message names the link that broke, from `trigger` to `cleanup`', fix: 'What the link says: `weft db live` for a trigger, the app\'s log for a poke, `verify.row` for a row the query can\'t see' },
|
|
366
|
+
{ code: 'R020', severity: 'error', by: 'cli', catches: 'A connection string with a password in source, in a `verify` or a target: it would be committed', fix: 'Read it from the environment: `verify: { db: process.env.STAGING_DATABASE_URL }`' },
|
|
367
|
+
{ code: 'R021', severity: 'error', by: 'cli', catches: 'A `verify` target isn\'t available here: fails `weft build`, and `weft check` warns and skips', fix: 'Set `WEFT_STAGING_URL`, `STAGING_DATABASE_URL`, or the variable its target names' },
|
|
368
|
+
{ code: 'R022', severity: 'error', by: 'cli', catches: 'A `verify` target that looks like production, or that nothing marks as not: a round trip writes rows', fix: 'Name it in `live.targets` with `production: false`; a name or host that says production is always refused' },
|
|
369
|
+
{ code: 'R023', severity: 'warning', by: 'cli', catches: 'A `verify` passed with only one kind of target, so some links went untested', fix: 'Give it both a URL and a database' },
|
|
370
|
+
{ code: 'R024', severity: 'error', by: 'cli', catches: 'A `channel` or `sharedState` built for a target that can\'t hold a WebSocket (Netlify, Vercel, AWS Lambda): `weft build` fails, and where it is let through the page never dials, the channel\'s `status` is `unavailable` and a send rejects with R024', fix: '`weft deploy hosts`, then a server host (`fly`, `render`); or `--unsafe-allow R024` with why, for a demo that may stay offline there' },
|
|
371
|
+
{ code: 'R018', severity: 'warning', by: 'cli', catches: 'A table in `changes` that no migration gives a change trigger (`weft check`, `weft build`): the server would fall back to polling', fix: 'Run `weft db live`' },
|
|
372
|
+
// Deploy (research 46): what a host can run, the files and env a deploy needs, CDN and media setup. `weft check` reports
|
|
373
|
+
// them for `deploy.target` (or the host `--preset` names), `weft deploy` before it builds, `weft doctor` for every host.
|
|
374
|
+
{ code: 'E001', severity: 'error', by: 'cli', catches: 'A `channel`, `sharedState` or `realtime({ share })` on a deploy target that can\'t hold a WebSocket (Netlify, Vercel, AWS Lambda): the page loads and never connects (at runtime: R024)', fix: "`deploy: { target: 'fly' }` or `'render'`, `'cloudflare'` (Durable Objects), or the split: pages here, realtime on a server (`weft docs deploy`)" },
|
|
375
|
+
{ code: 'E002', severity: 'error', by: 'cli', catches: 'Something that needs a process between requests on a target that keeps none: a `required` live query (R013), or a `remote()` sidecar', fix: "A server host (`fly`, `render`); for a feed, `weft db listen` on one with `listener: 'external'`; for a sidecar, deploy it on its own with `rust.services.<name>.external: true`" },
|
|
376
|
+
{ code: 'E003', severity: 'error', by: 'cli', catches: 'A SQLite file or media uploads on local disk, on a target whose disk is gone after a request or a deploy, or on a server host with no volume or disk configured (a warning in `weft deploy hosts`)', fix: '`deploy.fly.volume` or `deploy.render.disk` (both billed; they mount at `/app/.data`), or a hosted database' },
|
|
377
|
+
{ code: 'E004', severity: 'warning', by: 'cli', catches: 'A native npm addon (`better-sqlite3`, `sharp`, `bcrypt`…) built on this machine and deployed from here to a Linux function host', fix: "Deploy from CI (`deploy.ci: 'github'` builds on Linux), or drop the addon" },
|
|
378
|
+
{ code: 'E005', severity: 'info', by: 'cli', catches: 'No `deploy.target` (`weft doctor`, `weft deploy hosts`): which hosts can run this app, and the one weft suggests', fix: '`weft deploy init <target>`' },
|
|
379
|
+
{ code: 'E006', severity: 'warning', by: 'cli', catches: 'A file `weft deploy init` writes for `deploy.target` (`fly.toml`, `Dockerfile`, the workflow…) is missing, or was written from an older config', fix: '`weft deploy init` (a file edited by hand is left alone; `--force` overwrites it)' },
|
|
380
|
+
{ code: 'E007', severity: 'error', by: 'cli', catches: 'A variable `weft deploy --yes` reads is unset (an error in CI or for an id the CLI can\'t find; a warning where the host CLI\'s own login stands in)', fix: "Set it in the environment, or log in with the host's CLI here. Never in a file" },
|
|
381
|
+
{ code: 'E008', severity: 'error', by: 'cli', catches: "A deploy credential in a committed file: a Netlify or Fly token, a Render deploy hook, a Cloudinary URL with its secret, or a long value for a key named token, secret, api key or password, in the config, a host file or a workflow", fix: 'Move it to the environment (host settings, or GitHub secrets as `${{ secrets.NAME }}`), and rotate it' },
|
|
382
|
+
{ code: 'E009', severity: 'warning', by: 'cli', catches: 'The memory bus on a target that runs many instances: a message reaches one instance\'s clients (`weft build` fails on it as R002)', fix: "`weft add redis` or `weft add nats`, `postgresBus({ url })`, or `durableObjectBus` on Cloudflare" },
|
|
383
|
+
{ code: 'E010', severity: 'warning', by: 'cli', catches: '`deploy.cdn.proxy` is Cloudflare, and `site.url` is the host\'s own domain (`*.fly.dev`, `*.onrender.com`): nothing goes through the proxy', fix: 'Set `site.url` to the domain Cloudflare proxies' },
|
|
384
|
+
{ code: 'E011', severity: 'warning', by: 'cli', catches: '`deploy.cdn.proxy` in front of a host that is a CDN already (Netlify, Vercel, Cloudflare): two caches, and a deploy purges only its own', fix: 'Drop `cdn.proxy`, or deploy to `fly` or `render` behind it' },
|
|
385
|
+
{ code: 'E012', severity: 'error', by: 'cli', catches: '`deploy.cdn.images` is Cloudinary, and `@weft-js/plugin-media` isn\'t in `plugins`', fix: '`weft add media`, or drop `cdn.images`' },
|
|
386
|
+
{ code: 'E013', severity: 'error', by: 'cli', catches: '`deploy.cdn.images` with no `cloud` and no `CLOUDINARY_URL`, or the two naming different clouds (a warning when only the env var is unset, at a deploy)', fix: "Set `cdn.images.cloud`, and `CLOUDINARY_URL` in the host's environment" },
|
|
387
|
+
{ code: 'E014', severity: 'info', by: 'cli', catches: '`deploy.cdn.images` is checked, and this weft doesn\'t send uploads to Cloudinary yet: they go to `media({ server: { dir } })`', fix: 'Nothing yet (research 46 §9); on a function host, uploads need a server host with a disk (E003)' },
|
|
388
|
+
{ code: 'R101', severity: 'error', by: 'cli', catches: 'A function with `impl` has no `output`, a type JSON can\'t carry (`z.date()`, `z.bigint()`, `z.void()`, `z.custom()`), or a one-way `.transform()` on its input (`weft build`, `weft check`, `weft dev`)', fix: 'Use a codec: `z.codec(z.iso.datetime(), z.date(), { decode, encode })`. `z.null()` says a function returns nothing' },
|
|
389
|
+
{ code: 'R102', severity: 'error', by: 'cli', catches: 'A plain `z.union` of objects in a function with `impl`: it has no tag, so Rust can\'t tell its arms apart reliably. Scalar unions and `.nullable()` are fine', fix: "Use `z.discriminatedUnion`, or set `rust: { unions: 'allow' }`" },
|
|
390
|
+
{ code: 'R103', severity: 'error', by: 'cli', catches: 'A sidecar doesn\'t match the contract: cargo stops where a generated type or method no longer fits (with the function\'s id), a binary built from another contract, an output the `output` schema rejects, or input serde refuses', fix: 'Fix the Rust to fit the TS declaration, which is the source of truth; `.weft/rust/<service>.rs` shows what\'s generated' },
|
|
391
|
+
{ code: 'R104', severity: 'error', by: 'cli', catches: '`native()` on a preset that can\'t run it (no file system for an addon or a `.wasm`), on a preset with no WASM when `wasm: false` is set, or a `.wasm` that won\'t build (a warning on a preset the addon runs on). At run time, a build with neither for this platform (503)', fix: 'Use `remote(\'rust://<name>\')`, the same crate behind `weft::router`. `rustup target add wasm32-unknown-unknown`, or `rust.services.<name>.wasm: false` if nothing needs a fallback' },
|
|
392
|
+
{ code: 'R105', severity: 'warning', by: 'cli', catches: 'A cached Rust query with `tags` that reports no table (`weft check` reads the method; `weft dev` sees the call): Q011 can\'t see into Rust, so nothing checks that the tags cover what it reads', fix: 'Call `ctx.touch("table")` in the Rust, or drop `tags` from a query that reads no table' },
|
|
393
|
+
{ code: 'R106', severity: 'error', by: 'start', catches: 'A `remote()` call can\'t be made: no address, no `WEFT_RUST_SECRET`, or the sidecar refused the signature (503)', fix: 'Set `WEFT_RUST_<NAME>_URL` and the same `WEFT_RUST_SECRET` on both sides. `weft dev` and `weft start` do it for what they supervise' },
|
|
394
|
+
{ code: 'R107', severity: 'error', by: 'start', catches: 'A sidecar didn\'t answer, or ran past the deadline (503, 504). Only that function fails', fix: 'Check that it\'s running, or raise `timeout` on `remote()`' },
|
|
395
|
+
{ code: 'R108', severity: 'error', by: 'start', catches: 'A sidecar function panicked or returned an undeclared error (500). The detail stays in the sidecar\'s log, under the request id', fix: 'Find the request id in the sidecar\'s log, or declare the failure in `errors`' },
|
|
396
|
+
{ code: 'R109', severity: 'error', by: 'cli', catches: 'A sidecar doesn\'t build for a reason no contract function explains: a compile error, no crate, or no `cargo`', fix: 'Fix the crate, point `rust.services.<name>.crate` at it, or set `external: true` for one built elsewhere' },
|
|
397
|
+
|
|
398
|
+
// Text, type and skeletons (research 11 §8.5; research 26 §7; research 31 §7). `cli`: weft check (text-checks.mjs).
|
|
399
|
+
{ code: 'X001', severity: 'warning', by: 'runtime', catches: '`s.text()` predicted one height and its DOM mirror renders another: `font`, `lineHeight` or `width` differs from the CSS', fix: 'Drop `font` and `lineHeight` so they come from CSS, or make them match; the message names the values' },
|
|
400
|
+
{ code: 'X002', severity: 'warning', by: 'cli', catches: '`system-ui`, `-apple-system` or a generic-only family on a measured `s.text()`: canvas and CSS can resolve it differently (also checked in dev, for fonts that come from CSS)', fix: 'Name a family' },
|
|
401
|
+
{ code: 'X003', severity: 'warning', by: 'runtime', catches: '`s.text()` prepared while its font isn\'t loaded and no `"<Family> Fallback"` face exists: it\'s measured on another font until the swap', fix: 'Declare the font in `text({ fonts })`, which generates the fallback' },
|
|
402
|
+
{ code: 'X004', severity: 'warning', by: 'runtime', catches: 'A surface text mirror uses a style line breaking can\'t predict: `text-wrap: balance|pretty`, `hyphens: auto`, `word-spacing`, `letter-spacing`, `font-feature-settings`, `font-variant-numeric`, `font-variation-settings`', fix: 'Drop the style, or pass `exact: false`' },
|
|
403
|
+
{ code: 'X005', severity: 'error', by: 'cli', catches: '`font-size` in `vw`/`vi`/`cqi`/`cqw` with no `rem` term: zoom and the user\'s font size don\'t change it (WCAG 1.4.4)', fix: '`clamp(rem, rem + cqi, rem)`; the suggestion keeps the same sizes at 375 and 1280 px' },
|
|
404
|
+
{ code: 'X006', severity: 'error', by: 'cli', catches: 'A fluid `clamp()` font size whose max/min is over 2.5: 200% zoom no longer doubles it', fix: 'Narrow the range (raise the minimum or lower the maximum)' },
|
|
405
|
+
{ code: 'X008', severity: 'error', by: 'cli', catches: 'Viewport meta with `user-scalable=no` or `maximum-scale` under 2: blocks pinch zoom', fix: 'Remove them (autofix)' },
|
|
406
|
+
{ code: 'X009', severity: 'error', by: 'cli', catches: '`<Surface>` with neither `label` nor `decorative`: a screen reader finds an unnamed image', fix: 'Add `label`, or `decorative` if it shows nothing a reader needs' },
|
|
407
|
+
{ code: 'X010', severity: 'error', by: 'cli', catches: '`fillText` or `strokeText` in a surface: text only the canvas knows, with no selection, find, translation or screen reader', fix: 'Use `s.text()`, or wrap it in `s.decorative()` if it is truly decoration' },
|
|
408
|
+
{ code: 'X011', severity: 'error', by: 'cli', catches: 'A surface text mirror hidden from assistive tech: `aria-hidden` on `<Surface>` or `s.el`, or `display: none`/`visibility: hidden` on `[data-surface-text]` (also checked in dev)', fix: 'Leave the mirror to weft; use `decorative` for a surface with nothing to read' },
|
|
409
|
+
{ code: 'X012', severity: 'error', by: 'cli', catches: 'CSS that defeats a surface\'s text fallback: `forced-color-adjust: none`, or a print rule hiding `[data-surface-text]`', fix: 'Remove it: the mirror is what forced-colours mode and print show' },
|
|
410
|
+
{ code: 'X020', severity: 'warning', by: 'cli', catches: 'A hand-written `@font-face` for a local file with no `"<Family> Fallback"` face: the font swap shifts layout', fix: 'Declare the font in `text({ fonts })` (@weft-js/text), which generates a metric-matched fallback' },
|
|
411
|
+
{ code: 'X021', severity: 'error', by: 'cli', catches: '`font-display: block` or `auto`: text is invisible while the font loads', fix: '`swap` (autofix), or `optional` for faces that may skip' },
|
|
412
|
+
{ code: 'X022', severity: 'warning', by: 'cli', catches: 'A local `@font-face` without `unicode-range` (not subset), or the font build\'s preloads over the 100 KB budget', fix: '`text({ fonts })` subsets every face; preload fewer faces' },
|
|
413
|
+
{ code: 'X023', severity: 'warning', by: 'cli', catches: 'A `<link rel="preload" as="font">` whose file no `@font-face` uses', fix: 'Remove the preload' },
|
|
414
|
+
{ code: 'X030', severity: 'error', by: 'cli', catches: '`<img>`, `<video>` or `<iframe>` without `width`/`height` or `aspect-ratio` inside a `<Boundary>`: it lands at 0 px and pushes content down', fix: '`<Media src={x}>` with a `?media` import (it always has dimensions), or set `width` and `height`' },
|
|
415
|
+
{ code: 'X031', severity: 'warning', by: 'runtime', catches: 'A `pending` fallback whose box differs from the content\'s by more than 1 px at the swap (measured by `<Boundary>` in dev)', fix: 'Size the fallback like the content: warp\'s `<Shape>` with the content\'s counts' },
|
|
416
|
+
{ code: 'X032', severity: 'warning', by: 'runtime', catches: 'A skeleton repeats a list with no count hint: no `limit` input, no `.max()` on the array, nothing seen yet. It shows 5 rows, so the swap can shift', fix: 'Add a `limit` input or `.max()` to the array in `output`' },
|
|
417
|
+
{ code: 'X033', severity: 'warning', by: 'cli', catches: '`x.pending` renders a skeleton or spinner with no check that `x` has no value: every refetch flashes it', fix: 'Let `<Boundary>` show first loads, and test `x.value === undefined` here. A refetch keeps the old value in place' },
|
|
418
|
+
{ code: 'X034', severity: 'error', by: 'cli', catches: 'A `pending` fallback with its own `setTimeout`/`setInterval`: `<Boundary>` already owns the 150 ms / 400 ms timing', fix: 'Remove the timers from the fallback' },
|
|
419
|
+
{ code: 'X040', severity: 'warning', by: 'cli', catches: 'Per-grapheme DOM spans over a string known to exceed 200 graphemes', fix: 'Use word-level motion or a Surface' },
|
|
420
|
+
{ code: 'X041', severity: 'warning', by: 'cli', catches: 'Per-character spans without `aria-hidden`, or a parent with no accessible name: a screen reader spells the word out', fix: '`aria-hidden` on each span and `aria-label` with the whole text on the parent' },
|
|
421
|
+
|
|
422
|
+
// Media (research 15 §7.5, static rules in research 26 §2.5): reported by @weft-js/plugin-media's check hook.
|
|
423
|
+
{ code: 'X035', severity: 'warning', by: 'cli', catches: 'Streamed text written straight into a scroll container: a reflowing tail moves the lines above it', fix: 'Render it with `<Text stream>` from @weft-js/core/text: the height only grows' },
|
|
424
|
+
{ code: 'X036', severity: 'warning', by: 'runtime', catches: 'A skeleton shows a string from `output` with no length hint, so its bar is a guess', fix: 'Add `.max()`, `.length()` or `.meta({ example })` to the schema, or an example in the query\'s `shape`' },
|
|
425
|
+
{ code: 'X037', severity: 'warning', by: 'cli', catches: 'A `resource` or query made in the component that renders the `<Boundary>` meant to wait for it, and read inside it: the Boundary only waits for what its children make, so no fallback shows', fix: 'Make it in a small component rendered inside the Boundary' },
|
|
426
|
+
|
|
427
|
+
{ code: 'M001', severity: 'error', by: 'plugin', owner: '@weft-js/plugin-media', catches: 'Raw `<img>`/`<video>` without `width`/`height` or `aspect-ratio`, outside a `<Boundary>` (inside it is X030)', fix: 'Insert the intrinsic `width`/`height` (autofix for an imported file), or use `<Media>`' },
|
|
428
|
+
{ code: 'M002', severity: 'error', by: 'plugin', owner: '@weft-js/plugin-media', catches: 'An image rendered through a `?url` import or a string path, bypassing the optimizer', fix: '`import x from \'./x.jpg?media\'` and `<Media src={x} alt="…" />`' },
|
|
429
|
+
{ code: 'M003', severity: 'warning', by: 'plugin', owner: '@weft-js/plugin-media', catches: 'A raw `<img>` whose file is wider than its `width` × 2 (DPR) × 1.25: bytes nobody sees', fix: '`<Media>`, which resizes' },
|
|
430
|
+
{ code: 'M004', severity: 'warning', by: 'plugin', owner: '@weft-js/plugin-media', catches: 'A GIF over 256 KB rendered raw', fix: '`<Media>` with `mediaFfmpeg()` in `plugins`, which turns GIFs into video when that saves 15%' },
|
|
431
|
+
{ code: 'M005', severity: 'error', by: 'plugin', owner: '@weft-js/plugin-media', catches: '`<video autoplay>` without `muted`: browsers block it, and sound before a gesture is hostile', fix: 'Add `muted` (autofix)' },
|
|
432
|
+
{ code: 'M006', severity: 'warning', by: 'plugin', owner: '@weft-js/plugin-media', catches: '`<video>` without `poster`: a blank box until the first frame decodes', fix: 'Add a `poster` image' },
|
|
433
|
+
{ code: 'M007', severity: 'error', by: 'plugin', owner: '@weft-js/plugin-media', catches: 'Missing `alt` on `<Media>`/`<img>`, `<Media alt="">` without `decorative`, or `alt` equal to the file name', fix: 'Describe what the image shows; `alt="" decorative` for purely visual media' },
|
|
434
|
+
{ code: 'M008', severity: 'warning', by: 'plugin', owner: '@weft-js/plugin-media', catches: 'A route\'s first media is `loading="lazy"`, or `priority` media is lazy: the likely LCP image loads late', fix: 'Mark the hero `priority` and drop `loading="lazy"`' },
|
|
435
|
+
{ code: 'M009', severity: 'warning', by: 'plugin', owner: '@weft-js/plugin-media', catches: '`priority` on more than 2 media in one route: they compete for the first bytes', fix: 'Keep `priority` on the one hero image' },
|
|
436
|
+
{ code: 'M010', severity: 'warning', by: 'plugin', owner: '@weft-js/plugin-media', catches: 'A `priority` `<Media>` without `width` or `sizes`: eager images can\'t use `sizes="auto"`, so the browser assumes 100vw', fix: 'Pass `width="1/2"` (a viewport fraction or CSS length) or `sizes`' },
|
|
437
|
+
{ code: 'M011', severity: 'warning', by: 'plugin', owner: '@weft-js/plugin-media', catches: '`<video autoplay loop>` without `controls`, or a GIF in a raw `<img>`: endless motion with no pause (WCAG 2.2.2)', fix: '`<Media>` (pause control, poster under reduced motion), or add `controls`' },
|
|
438
|
+
{ code: 'M012', severity: 'error', by: 'plugin', owner: '@weft-js/plugin-media', catches: 'An SVG with `<script>`, `foreignObject`, `on*=` handlers or external references, used as an image', fix: 'Sanitise the file; the build refuses to optimise it' },
|
|
439
|
+
{ code: 'M013', severity: 'warning', by: 'plugin', owner: '@weft-js/plugin-media', catches: 'A file in `public/` referenced by string: untracked, unoptimised, no dimensions', fix: 'Move it next to the code and `import x from \'./x.jpg?media\'`' },
|
|
440
|
+
{ code: 'M014', severity: 'info', by: 'plugin', owner: '@weft-js/plugin-media', catches: "What `encoder: 'compatible'` adds to the build for compatible copies", fix: 'Nothing: it says what the toggle costs' },
|
|
441
|
+
{ code: 'M015', severity: 'warning', by: 'plugin', owner: '@weft-js/plugin-media', catches: 'An HDR or over-8-bit video that comes out 8-bit SDR', fix: 'Convert the source on purpose, or accept the flattening' },
|
|
442
|
+
{ code: 'M016', severity: 'warning', by: 'plugin', owner: '@weft-js/plugin-media', catches: '`<Media src>` given a string: nothing knows its size before it loads, so the page shifts', fix: 'Import it with `?media`, or store what `ctx.media.ingest` returns and type the field `z.media()`' },
|
|
443
|
+
{ code: 'M017', severity: 'error', by: 'start', catches: '`ctx.media` or a `/_weft/m/` URL used with no `media.server` provider', fix: 'Add `media()` from `@weft-js/plugin-media` to `plugins` in weft.config.ts' },
|
|
444
|
+
{ code: 'M019', severity: 'error', by: 'plugin', owner: '@weft-js/plugin-media-ffmpeg', catches: 'ffmpeg did not run, is not the pinned binary, or has no encoder for a codec', fix: 'Install ffmpeg, or give `mediaFfmpeg({ binary })` its path' },
|
|
445
|
+
{ code: 'M018', severity: 'error', by: 'start', catches: 'An upload refused: it isn\'t an image weft can read, or it\'s over the size limit (422)', fix: 'Raise `media({ server: { maxBytes } })`, or check the file on the client first' },
|
|
446
|
+
|
|
447
|
+
{ code: 'P010', severity: 'error', by: 'cli', catches: 'More than one weft.config file', fix: 'Keep one: `.ts`, `.json` or `.yaml`' },
|
|
448
|
+
{ code: 'P100', severity: 'error', by: 'cli', catches: 'Two providers for a `one` slot (or the same key in a `keyed` slot)', fix: 'Remove one, or name the winner in `slots: {}`' },
|
|
449
|
+
{ code: 'P101', severity: 'error', by: 'cli', catches: 'A slot consumer is used but nothing provides it', fix: 'Add the plugin, or set the option that disables it back' },
|
|
450
|
+
{ code: 'P102', severity: 'error', by: 'reserved', catches: 'Provider implements no slot interface version this weft supports (P102w: bound through an adapter)', fix: 'Upgrade the plugin or weft' },
|
|
451
|
+
{ code: 'P103', severity: 'error', by: 'reserved', catches: "Plugin's `weft` peer range doesn't include the installed weft", fix: 'Upgrade either side' },
|
|
452
|
+
{ code: 'P104', severity: 'error', by: 'reserved', catches: '`requires` names a slot that nothing provides', fix: 'Add a provider' },
|
|
453
|
+
{ code: 'P105', severity: 'error', by: 'reserved', catches: 'Cycle in `order.before`/`after`', fix: 'Remove one edge' },
|
|
454
|
+
{ code: 'P106', severity: 'error', by: 'reserved', catches: "Two plugins' `transform` filters matched one module with no declared order", fix: 'Add `order.after` to one' },
|
|
455
|
+
{ code: 'P107', severity: 'error', by: 'reserved', catches: 'Unfiltered `resolveId`/`load`/`transform`, or a `config` hook', fix: 'Add `filter`; move config to data' },
|
|
456
|
+
{ code: 'P108', severity: 'warning', by: 'reserved', catches: 'Plugin over its HMR budget share', fix: 'Narrow filters, cache, or move work to `check`' },
|
|
457
|
+
{ code: 'P109', severity: 'error', by: 'cli', catches: 'Undeclared permission used, or permissions widened since the lock', fix: 'Declare it, then `weft plugins approve`' },
|
|
458
|
+
{ code: 'P110', severity: 'error', by: 'cli', catches: 'Installed plugin integrity differs from `.weft/plugins.lock.json`', fix: 'Reinstall, or approve the update' },
|
|
459
|
+
{ code: 'P111', severity: 'error', by: 'cli', catches: "Diagnostic code outside the plugin's namespace, or a namespace collision", fix: 'Use `<short-name>/<CODE>`' },
|
|
460
|
+
{ code: 'P112', severity: 'error', by: 'cli', catches: 'Plugin AGENTS section over 1 KB', fix: 'Shorten; move detail to the MCP `docs` tool' },
|
|
461
|
+
{ code: 'P113', severity: 'error', by: 'cli', catches: 'Token, catalog, compiler-data or Vite-config key collision', fix: 'Rename in the namespace' },
|
|
462
|
+
{ code: 'P114', severity: 'error', by: 'reserved', catches: 'Provider runtime code in an entry chunk (eager import)', fix: "Reach it only through the slot's `load()`" },
|
|
463
|
+
{ code: 'P115', severity: 'error', by: 'cli', catches: 'Invalid plugin options (Zod path given)', fix: 'Fix the option' },
|
|
464
|
+
{ code: 'P116', severity: 'error', by: 'cli', catches: 'Same plugin twice, or `name`/`version` does not match `package.json`', fix: 'Remove the duplicate' },
|
|
465
|
+
{ code: 'P117', severity: 'error', by: 'cli', catches: '`contribute()` is not pure', fix: 'Move IO to a build-side provider' },
|
|
466
|
+
{ code: 'P118', severity: 'error', by: 'reserved', catches: 'Server code registered outside a server-side slot', fix: 'Provide a slot, or move it into app code' },
|
|
467
|
+
{ code: 'P119', severity: 'error', by: 'reserved', catches: "Provider failed its slot's conformance suite", fix: 'Fix the provider; the failing cases are listed' },
|
|
468
|
+
{ code: 'P120', severity: 'error', by: 'cli', catches: "A weft plugin's `vite` part registered directly in `vite.config.ts`", fix: 'Move it to `plugins` in weft.config.ts' },
|
|
469
|
+
]
|
|
470
|
+
|
|
471
|
+
const byCode = new Map(catalog.map(e => [e.code, e]))
|
|
472
|
+
|
|
473
|
+
/** W021–W029 share W020's entry. */
|
|
474
|
+
export function entry(code) {
|
|
475
|
+
if (byCode.has(code)) return byCode.get(code)
|
|
476
|
+
if (/^W02\d$/.test(code)) return byCode.get('W020')
|
|
477
|
+
return undefined
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
/**
|
|
481
|
+
* Which compiler codes the installed compiler reports: each example must produce its own code.
|
|
482
|
+
* @param {{ transform: Function }} compiler
|
|
483
|
+
* @returns {Record<string, { implemented: boolean, autofix: boolean, suggestion: boolean }>} autofix: a safe edit; suggestion: an edit to review
|
|
484
|
+
*/
|
|
485
|
+
export function probe(compiler) {
|
|
486
|
+
const out = {}
|
|
487
|
+
for (const e of catalog) {
|
|
488
|
+
if (e.by !== 'compiler') continue
|
|
489
|
+
if (e.code === 'W000') { out[e.code] = { implemented: true, autofix: false, suggestion: false }; continue }
|
|
490
|
+
if (!e.example) { out[e.code] = { implemented: false, autofix: false, suggestion: false }; continue }
|
|
491
|
+
const { diagnostics } = compiler.transform(e.example, { filename: 'probe.tsx', checkOnly: true })
|
|
492
|
+
const hit = diagnostics.find(d => d.code === e.code)
|
|
493
|
+
out[e.code] = { implemented: Boolean(hit), autofix: hit?.fix?.safe === true, suggestion: Boolean(hit?.fix) && hit.fix.safe !== true }
|
|
494
|
+
}
|
|
495
|
+
return out
|
|
496
|
+
}
|