@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.
Files changed (171) hide show
  1. package/README.md +38 -14
  2. package/bin/weft.mjs +8 -1
  3. package/docs/async.md +16 -6
  4. package/docs/browser.md +22 -8
  5. package/docs/config.md +39 -9
  6. package/docs/core.md +30 -0
  7. package/docs/deploy.md +33 -8
  8. package/docs/design.md +12 -3
  9. package/docs/entrypoints.md +79 -0
  10. package/docs/fonts.md +12 -2
  11. package/docs/icons.md +13 -7
  12. package/docs/lists.md +3 -1
  13. package/docs/meta.md +13 -3
  14. package/docs/plugins.md +32 -9
  15. package/docs/props.md +13 -2
  16. package/docs/reactivity.md +26 -0
  17. package/docs/rules.md +19 -12
  18. package/docs/rust.md +47 -17
  19. package/docs/start-short.md +3 -2
  20. package/docs/start.md +226 -52
  21. package/docs/state.md +33 -7
  22. package/docs/style.md +31 -8
  23. package/docs/surface.md +61 -15
  24. package/docs/testing.md +15 -5
  25. package/docs/theme.md +182 -0
  26. package/docs/ui-state.md +19 -6
  27. package/docs/verify.md +13 -0
  28. package/package.json +13 -4
  29. package/src/add/config.mjs +93 -0
  30. package/src/add/errors.mjs +2 -0
  31. package/src/add/setup.mjs +88 -0
  32. package/src/add-rust.mjs +82 -27
  33. package/src/add.mjs +209 -181
  34. package/src/agents-md.mjs +144 -43
  35. package/src/alias.mjs +16 -5
  36. package/src/audit/page.mjs +313 -0
  37. package/src/audit/report.mjs +69 -0
  38. package/src/audit/utils.mjs +5 -0
  39. package/src/audit.mjs +159 -206
  40. package/src/browser/cli.mjs +24 -5
  41. package/src/browser/client.mjs +189 -25
  42. package/src/browser/commands.mjs +166 -51
  43. package/src/catalog/audit.mjs +129 -0
  44. package/src/catalog/auth.mjs +40 -0
  45. package/src/catalog/compiler.mjs +337 -0
  46. package/src/catalog/contracts.mjs +18 -0
  47. package/src/catalog/database.mjs +52 -0
  48. package/src/catalog/deploy.mjs +117 -0
  49. package/src/catalog/design.mjs +159 -0
  50. package/src/catalog/forms.mjs +25 -0
  51. package/src/catalog/hydration.mjs +29 -0
  52. package/src/catalog/icons.mjs +24 -0
  53. package/src/catalog/media.mjs +163 -0
  54. package/src/catalog/metadata.mjs +116 -0
  55. package/src/catalog/plugins.mjs +228 -0
  56. package/src/catalog/realtime.mjs +195 -0
  57. package/src/catalog/rust.mjs +75 -0
  58. package/src/catalog/server-functions.mjs +92 -0
  59. package/src/catalog/state.mjs +39 -0
  60. package/src/catalog/style-groups.mjs +54 -0
  61. package/src/catalog/tests.mjs +127 -0
  62. package/src/catalog/text.mjs +200 -0
  63. package/src/catalog/ui-state.mjs +42 -0
  64. package/src/catalog.mjs +67 -460
  65. package/src/check/runtime.mjs +280 -0
  66. package/src/check.mjs +198 -208
  67. package/src/cli/authoring.mjs +213 -0
  68. package/src/cli/project.mjs +302 -0
  69. package/src/cli.mjs +222 -250
  70. package/src/config.mjs +1 -1
  71. package/src/contributions.mjs +75 -25
  72. package/src/db-layer.mjs +13 -5
  73. package/src/deploy/cdn.mjs +94 -16
  74. package/src/deploy/files.mjs +100 -21
  75. package/src/deploy/fit.mjs +127 -23
  76. package/src/deploy/hosts.mjs +90 -20
  77. package/src/deploy/index/cli.mjs +20 -0
  78. package/src/deploy/index/environment.mjs +69 -0
  79. package/src/deploy/index/steps.mjs +87 -0
  80. package/src/deploy/index.mjs +228 -114
  81. package/src/deploy/needs.mjs +88 -31
  82. package/src/design-checks/accessible-names.mjs +62 -0
  83. package/src/design-checks/contrast.mjs +43 -0
  84. package/src/design-checks/csp.mjs +60 -0
  85. package/src/design-checks/files.mjs +59 -0
  86. package/src/design-checks/heading-links.mjs +61 -0
  87. package/src/design-checks/interaction.mjs +65 -0
  88. package/src/design-checks/jsx.mjs +67 -0
  89. package/src/design-checks/layout-values.mjs +68 -0
  90. package/src/design-checks/native-controls.mjs +48 -0
  91. package/src/design-checks/outline.mjs +39 -0
  92. package/src/design-checks/reporter.mjs +37 -0
  93. package/src/design-checks/rules.mjs +274 -0
  94. package/src/design-checks/values.mjs +262 -0
  95. package/src/design-checks.mjs +10 -400
  96. package/src/diag.mjs +32 -8
  97. package/src/docs.mjs +138 -43
  98. package/src/doctor.mjs +20 -8
  99. package/src/files.mjs +18 -5
  100. package/src/fix.mjs +10 -2
  101. package/src/gate/after.mjs +40 -0
  102. package/src/gate/before.mjs +54 -0
  103. package/src/gate/classify.mjs +72 -0
  104. package/src/gate/mutation.mjs +209 -0
  105. package/src/gate/resolve-cases.mjs +22 -0
  106. package/src/gate/result.mjs +63 -0
  107. package/src/gate/run.mjs +75 -0
  108. package/src/gate.mjs +104 -202
  109. package/src/infer/box-props.cjs +140 -0
  110. package/src/infer/core-boxes.cjs +46 -0
  111. package/src/infer/derived-writes.cjs +95 -0
  112. package/src/infer/engine/checks.cjs +193 -0
  113. package/src/infer/engine/flow.cjs +293 -0
  114. package/src/infer/engine/report.cjs +158 -0
  115. package/src/infer/engine/utils.cjs +15 -0
  116. package/src/infer/engine.cjs +108 -368
  117. package/src/infer/facts/sites.cjs +114 -0
  118. package/src/infer/facts.cjs +210 -117
  119. package/src/infer/hosts.cjs +69 -22
  120. package/src/infer/lsp.mjs +149 -40
  121. package/src/infer/session.cjs +73 -17
  122. package/src/infer/shadow.cjs +13 -5
  123. package/src/infer/worker.cjs +57 -9
  124. package/src/jsx-scan.mjs +63 -21
  125. package/src/load-config/schema.mjs +281 -0
  126. package/src/load-config.mjs +137 -138
  127. package/src/map.mjs +93 -26
  128. package/src/mcp/browser-tools.mjs +155 -0
  129. package/src/mcp.mjs +147 -69
  130. package/src/meta-table.mjs +43 -8
  131. package/src/mutate.mjs +120 -36
  132. package/src/plugin-checks.mjs +52 -23
  133. package/src/plugins.mjs +125 -39
  134. package/src/project.mjs +170 -79
  135. package/src/props-checks.mjs +24 -6
  136. package/src/props-listener.mjs +27 -9
  137. package/src/react-checks.mjs +35 -4
  138. package/src/server-state-checks.mjs +34 -0
  139. package/src/share-checks.mjs +121 -37
  140. package/src/site.mjs +33 -6
  141. package/src/slots.mjs +104 -19
  142. package/src/start/build.mjs +116 -0
  143. package/src/start/routes.mjs +38 -0
  144. package/src/start.mjs +193 -95
  145. package/src/state-checks/core-state.mjs +223 -0
  146. package/src/state-checks/modules.mjs +17 -0
  147. package/src/state-checks/scan.mjs +232 -0
  148. package/src/state-checks/server-graph.mjs +65 -0
  149. package/src/state-checks.mjs +109 -133
  150. package/src/style/find.mjs +121 -0
  151. package/src/style.mjs +133 -127
  152. package/src/surface-checks.mjs +212 -46
  153. package/src/tailwind.mjs +5 -4
  154. package/src/tasks.mjs +189 -27
  155. package/src/test-rules/context.mjs +109 -0
  156. package/src/test-rules/file-rules.mjs +279 -0
  157. package/src/test-rules/rule-helpers.mjs +244 -0
  158. package/src/test-rules/syntax.mjs +223 -0
  159. package/src/test-rules/test-rules.mjs +127 -0
  160. package/src/testdiff.mjs +94 -32
  161. package/src/tests.mjs +31 -483
  162. package/src/text-checks.mjs +229 -69
  163. package/src/tokens.mjs +12 -4
  164. package/src/trust/scan.mjs +95 -0
  165. package/src/trust.mjs +214 -102
  166. package/src/weft.mjs +41 -9
  167. package/src/workspace.mjs +96 -19
  168. package/ts-plugin/context.js +57 -0
  169. package/ts-plugin/index.js +4 -124
  170. package/ts-plugin/mapping.js +65 -0
  171. package/ts-plugin/proxy.js +144 -0
package/src/style.mjs CHANGED
@@ -11,7 +11,9 @@ import { loadConfig } from './load-config.mjs'
11
11
  import { isTestFile } from './tests.mjs'
12
12
  import { loadCompiler, resolveWeft } from './weft.mjs'
13
13
 
14
- const usageError = message => Object.assign(new Error(message), { code: 'E_USAGE' })
14
+ import { closest, usageError } from './style/find.mjs'
15
+
16
+ export { closest, findGroups } from './style/find.mjs'
15
17
 
16
18
  /** The installed weft's matrix module, or null when this weft has no style groups. */
17
19
  export async function styleModule(weft) {
@@ -23,7 +25,9 @@ export async function styleModule(weft) {
23
25
  /** Source files that can define or use groups: components and plain modules, not tests. */
24
26
  export function styleFiles(root, config = {}) {
25
27
  const exclude = [...(config.check?.exclude ?? []), ...(config.scope?.forbid ?? [])]
26
- return discover(root, { include: ['**/*.{ts,tsx,jsx,mts}'], exclude }).filter(f => !isTestFile(f) && !f.endsWith('.d.ts'))
28
+ return discover(root, { include: ['**/*.{ts,tsx,jsx,mts}'], exclude }).filter(
29
+ (f) => !isTestFile(f) && !f.endsWith('.d.ts'),
30
+ )
27
31
  }
28
32
 
29
33
  /**
@@ -33,7 +37,11 @@ export function styleFiles(root, config = {}) {
33
37
  export async function styleMatrix(root, { write = false } = {}) {
34
38
  const weft = resolveWeft(root)
35
39
  const mod = await styleModule(weft)
36
- if (!mod) throw Object.assign(new Error(`weft@${weft.version} has no style groups (@weft-js/core/style/matrix)`), { code: 'E_NO_STYLE' })
40
+ if (!mod)
41
+ throw Object.assign(
42
+ new Error(`weft@${weft.version} has no style groups (@weft-js/core/style/matrix)`),
43
+ { code: 'E_NO_STYLE' },
44
+ )
37
45
  const { config } = await loadConfig(root)
38
46
  const compiler = loadCompiler(weft)
39
47
  const scan = mod.scanProject(root, styleFiles(root, config), (...a) => compiler.transform(...a))
@@ -52,93 +60,6 @@ export function writeMatrixFile(root, matrix) {
52
60
  return true
53
61
  }
54
62
 
55
- const words = s => s.toLowerCase().split(/[\s\-_.:/=,]+|(?<=[a-z])(?=[A-Z])/).filter(Boolean)
56
-
57
- /** Edit distance with adjacent swaps as one edit (`ordre` → `order`). */
58
- function distance(a, b) {
59
- const d = Array.from({ length: a.length + 1 }, (_, i) => Array.from({ length: b.length + 1 }, (__, j) => (i ? (j ? 0 : i) : j)))
60
- for (let i = 1; i <= a.length; i++)
61
- for (let j = 1; j <= b.length; j++) {
62
- d[i][j] = Math.min(d[i - 1][j] + 1, d[i][j - 1] + 1, d[i - 1][j - 1] + (a[i - 1] === b[j - 1] ? 0 : 1))
63
- if (i > 1 && j > 1 && a[i - 1] === b[j - 2] && a[i - 2] === b[j - 1]) d[i][j] = Math.min(d[i][j], d[i - 2][j - 2] + 1)
64
- }
65
- return d[a.length][b.length]
66
- }
67
-
68
- /** The names closest to `word` (edit distance within a third of its length, or containing it). */
69
- export function closest(word, names, n = 3) {
70
- const w = word.toLowerCase()
71
- const limit = Math.max(1, Math.floor(w.length / 3))
72
- return names
73
- .map(x => [distance(w, x.toLowerCase()), x])
74
- .filter(([d, x]) => d <= limit || x.toLowerCase().includes(w) || w.includes(x.toLowerCase()))
75
- .sort((a, b) => a[0] - b[0])
76
- .slice(0, n)
77
- .map(([, x]) => x)
78
- }
79
-
80
- /**
81
- * `weft style find <query>`: the groups that fit, best first. Every word of the query must hit the group's name,
82
- * doc, parts, axes, values, states or tokens (a name word one typo off counts); with no such group, groups that hit
83
- * any word.
84
- */
85
- export function findGroups(matrix, query) {
86
- const terms = words(query ?? '')
87
- if (!terms.length) throw usageError('weft style find <query>: say what the thing is (`card`, `status badge`, `critical`)')
88
- const score = (name, g) => {
89
- const fields = [
90
- [words(name), 30],
91
- [words(g.doc ?? ''), 10],
92
- [[...g.parts, ...g.states, ...Object.keys(g.axes)].flatMap(words), 8],
93
- [Object.values(g.axes).flatMap(v => (Array.isArray(v) ? v : [])).flatMap(words), 6],
94
- [g.tokens.flatMap(words), 4],
95
- ]
96
- let total = name === query ? 100 : 0
97
- const matched = []
98
- let hits = 0
99
- for (const t of terms) {
100
- let best = 0
101
- let why = null
102
- for (const [ws, weight] of fields) {
103
- for (const w of ws) {
104
- const s = w === t ? weight : w.startsWith(t) || (t.length > 3 && w.includes(t)) ? weight * 0.7 : weight >= 30 && t.length > 3 && distance(w, t) === 1 ? weight * 0.5 : 0
105
- if (s > best) [best, why] = [s, w]
106
- }
107
- }
108
- if (best) {
109
- hits++
110
- matched.push(why)
111
- }
112
- total += best
113
- }
114
- return { total, hits, matched: [...new Set(matched)] }
115
- }
116
- const all = Object.entries(matrix.groups).map(([name, g]) => ({ name, g, ...score(name, g) })).filter(x => x.total > 0)
117
- const full = all.filter(x => x.hits === terms.length)
118
- const ranked = (full.length ? full : all).sort((a, b) => b.total - a.total || a.name.localeCompare(b.name))
119
- // A name or doc hit outranks groups that only share a token (`row` → product-row, not every `layout: row`).
120
- const picked = ranked.filter(x => x.total >= (ranked[0]?.total ?? 0) / 4)
121
- return {
122
- query,
123
- groups: picked.map(({ name, g, total, matched }) => ({
124
- name,
125
- score: Math.round(total),
126
- matched,
127
- doc: g.doc,
128
- file: g.file,
129
- import: g.binding && g.exported ? { name: g.binding, from: g.file.replace(/:\d+$/, '') } : null,
130
- axes: g.axes,
131
- defaults: g.defaults,
132
- parts: g.parts,
133
- states: g.states,
134
- uses: g.used.length,
135
- similar: matrix.similar.filter(s => s[0] === name || s[1] === name).map(s => (s[0] === name ? s[1] : s[0])),
136
- })),
137
- partial: !full.length && all.length > 0,
138
- total: Object.keys(matrix.groups).length,
139
- }
140
- }
141
-
142
63
  const ARIA = new Set(['selected', 'pressed', 'checked', 'expanded', 'current', 'invalid', 'busy'])
143
64
 
144
65
  /**
@@ -147,21 +68,31 @@ const ARIA = new Set(['selected', 'pressed', 'checked', 'expanded', 'current', '
147
68
  */
148
69
  export function parseTarget(target) {
149
70
  const t = (target ?? '').trim()
150
- if (!t) throw usageError('weft style explain <group|selector>: a group name (`order-card`), `order-card.title`, or a selector')
71
+ if (!t)
72
+ throw usageError(
73
+ 'weft style explain <group|selector>: a group name (`order-card`), `order-card.title`, or a selector',
74
+ )
151
75
  const out = { group: null, part: null, axes: {}, states: [], accent: {} }
152
76
  const head = /^([a-z][\w-]*)(?:\.([\w-]+))?/i.exec(t)
153
77
  if (head && !t.startsWith('[')) {
154
78
  out.group = head[1]
155
79
  out.part = head[2] ?? null
156
80
  }
157
- for (const m of t.matchAll(/\[\s*([\w-]+)\s*(?:[~|^$*]?=\s*(?:"([^"]*)"|'([^']*)'|([^\]\s]*)))?\s*\]/g)) {
81
+ for (const m of t.matchAll(
82
+ /\[\s*([\w-]+)\s*(?:[~|^$*]?=\s*(?:"([^"]*)"|'([^']*)'|([^\]\s]*)))?\s*\]/g,
83
+ )) {
158
84
  const [, attr, ...vals] = m
159
- const value = vals.find(v => v !== undefined)
85
+ const value = vals.find((v) => v !== undefined)
160
86
  if (attr === 'data-g') out.group = value
161
87
  else if (attr === 'data-part') out.part = value
162
- else if (attr === 'data-accent') for (const w of (value ?? '').split(/\s+/).filter(Boolean)) { const [k, v] = w.split(':'); out.accent[k] = v }
163
- else if (attr.startsWith('aria-')) { if (value !== 'false') out.states.push(attr.slice(5)) }
164
- else {
88
+ else if (attr === 'data-accent')
89
+ for (const w of (value ?? '').split(/\s+/).filter(Boolean)) {
90
+ const [k, v] = w.split(':')
91
+ out.accent[k] = v
92
+ }
93
+ else if (attr.startsWith('aria-')) {
94
+ if (value !== 'false') out.states.push(attr.slice(5))
95
+ } else {
165
96
  const name = attr.replace(/^data-/, '')
166
97
  if (value === undefined || value === '') out.states.push(name)
167
98
  else out.axes[name] = value
@@ -181,23 +112,44 @@ export function explainTarget({ matrix, scan, mod }, target, extra = {}) {
181
112
  const g = matrix.groups[t.group]
182
113
  if (!g) {
183
114
  const near = closest(t.group, Object.keys(matrix.groups))
184
- throw Object.assign(new Error(`D025 no group \`${t.group}\`${near.length ? `; did you mean ${near.map(n => `\`${n}\``).join(' or ')}?` : ''} (\`weft style find\` searches by what it is)`), { code: 'E_UNKNOWN_GROUP' })
115
+ throw Object.assign(
116
+ new Error(
117
+ `D025 no group \`${t.group}\`${near.length ? `; did you mean ${near.map((n) => `\`${n}\``).join(' or ')}?` : ''} (\`weft style find\` searches by what it is)`,
118
+ ),
119
+ { code: 'E_UNKNOWN_GROUP' },
120
+ )
185
121
  }
186
- const source = scan.groups.find(s => s.name === t.group)
122
+ const source = scan.groups.find((s) => s.name === t.group)
187
123
  // A state written as data-x="…" in a selector, or an axis passed as --x: sort them by what the group has.
188
124
  const axes = { ...t.axes, ...extra.axes }
189
125
  const states = [...t.states, ...(extra.states ?? [])]
190
- for (const [k, v] of Object.entries(axes)) if (!(k in g.axes) && g.states.includes(k) && (v === 'true' || v === '')) { delete axes[k]; states.push(k) }
191
- const selection = { axes, states: [...new Set(states)],accent: { ...t.accent, ...extra.accent }, part: extra.part ?? t.part ?? undefined }
126
+ for (const [k, v] of Object.entries(axes))
127
+ if (!(k in g.axes) && g.states.includes(k) && (v === 'true' || v === '')) {
128
+ delete axes[k]
129
+ states.push(k)
130
+ }
131
+ const selection = {
132
+ axes,
133
+ states: [...new Set(states)],
134
+ accent: { ...t.accent, ...extra.accent },
135
+ part: extra.part ?? t.part ?? undefined,
136
+ }
192
137
  let explained
193
138
  try {
194
139
  explained = mod.explainGroup(source, selection)
195
140
  } catch (e) {
196
- if (e.constructor?.name === 'GroupError') throw Object.assign(new Error(`D025 ${e.message}`), { code: 'E_UNKNOWN_NAME' })
141
+ if (e.constructor?.name === 'GroupError')
142
+ throw Object.assign(new Error(`D025 ${e.message}`), { code: 'E_UNKNOWN_NAME' })
197
143
  throw e
198
144
  }
199
145
  const sel = explained.selection
200
- const sites = g.used.filter(u => !u.part && Object.entries(sel.axes).every(([a, v]) => !u.axes[a] || u.axes[a] === 'dynamic' || u.axes[a].includes(v)))
146
+ const sites = g.used.filter(
147
+ (u) =>
148
+ !u.part &&
149
+ Object.entries(sel.axes).every(
150
+ ([a, v]) => !u.axes[a] || u.axes[a] === 'dynamic' || u.axes[a].includes(v),
151
+ ),
152
+ )
201
153
  return {
202
154
  group: t.group,
203
155
  doc: g.doc,
@@ -213,33 +165,51 @@ export function explainTarget({ matrix, scan, mod }, target, extra = {}) {
213
165
  css: explained.css,
214
166
  sites,
215
167
  accents: g.accents,
216
- similar: matrix.similar.filter(s => s[0] === t.group || s[1] === t.group),
168
+ similar: matrix.similar.filter((s) => s[0] === t.group || s[1] === t.group),
217
169
  }
218
170
  }
219
171
 
220
172
  /** What the selection looks like in the DOM, as a person would find it in devtools. */
221
173
  function attributesOf(group, sel, part) {
222
- const root = [`data-g="${group}"`, ...Object.entries(sel.axes).map(([a, v]) => `data-${a}="${v}"`), ...sel.states.map(s => (ARIA.has(s) ? `aria-${s}="true"` : `data-${s}`))]
223
- const accent = Object.entries(sel.accent).sort(([a], [b]) => (a < b ? -1 : 1)).map(([k, v]) => `${k}:${v}`).join(' ')
174
+ const root = [
175
+ `data-g="${group}"`,
176
+ ...Object.entries(sel.axes).map(([a, v]) => `data-${a}="${v}"`),
177
+ ...sel.states.map((s) => (ARIA.has(s) ? `aria-${s}="true"` : `data-${s}`)),
178
+ ]
179
+ const accent = Object.entries(sel.accent)
180
+ .sort(([a], [b]) => (a < b ? -1 : 1))
181
+ .map(([k, v]) => `${k}:${v}`)
182
+ .join(' ')
224
183
  if (accent) root.push(`data-accent="${accent}"`)
225
184
  return { root: root.join(' '), ...(part ? { part: `data-part="${part}"` } : {}) }
226
185
  }
227
186
 
228
187
  export function formatFind(r) {
229
- if (!r.groups.length) return `no group fits "${r.query}" (${r.total} groups). Define one with group() from @weft-js/core/style; \`weft docs style\` has the rules.`
230
- const lines = r.groups.map(g => {
231
- const axes = Object.entries(g.axes).map(([a, v]) => `${a}: ${Array.isArray(v) ? v.join('|') : v}`).join(' · ')
188
+ if (!r.groups.length)
189
+ return `no group fits "${r.query}" (${r.total} groups). Define one with group() from @weft-js/core/style; \`weft docs style\` has the rules.`
190
+ const lines = r.groups.map((g) => {
191
+ const axes = Object.entries(g.axes)
192
+ .map(([a, v]) => `${a}: ${Array.isArray(v) ? v.join('|') : v}`)
193
+ .join(' · ')
232
194
  return `${g.name.padEnd(18)} ${g.file}${g.doc ? `\n ${g.doc}` : ''}\n ${[axes, g.parts.length ? `parts: ${g.parts.join(', ')}` : '', g.states.length ? `states: ${g.states.join(', ')}` : '', `${g.uses} uses`].filter(Boolean).join(' · ')}`
233
195
  })
234
- return (r.partial ? `no group matches every word of "${r.query}"; closest:\n` : '') + lines.join('\n')
196
+ return (
197
+ (r.partial ? `no group matches every word of "${r.query}"; closest:\n` : '') + lines.join('\n')
198
+ )
235
199
  }
236
200
 
237
201
  export function formatExplain(r) {
238
- const out = [`${r.group} ${r.file}${r.doc ? ` — ${r.doc}` : ''}`, ` <el ${r.attributes.root}>${r.attributes.part ? ` part: <el ${r.attributes.part}>` : ''}`]
202
+ const out = [
203
+ `${r.group} ${r.file}${r.doc ? ` — ${r.doc}` : ''}`,
204
+ ` <el ${r.attributes.root}>${r.attributes.part ? ` part: <el ${r.attributes.part}>` : ''}`,
205
+ ]
239
206
  for (const el of r.elements) {
240
207
  if (!el.decls.length) continue
241
208
  out.push(` ${el.element}:`)
242
- for (const d of el.decls) out.push(` ${d.overridden ? '~' : ' '} ${`${d.prop}: ${d.value}`.padEnd(64)} ← ${d.from} (${d.at})${d.overridden ? ', overridden' : ''}`)
209
+ for (const d of el.decls)
210
+ out.push(
211
+ ` ${d.overridden ? '~' : ' '} ${`${d.prop}: ${d.value}`.padEnd(64)} ← ${d.from} (${d.at})${d.overridden ? ', overridden' : ''}`,
212
+ )
243
213
  }
244
214
  out.push('', r.css.trimEnd())
245
215
  if (r.contextTone) out.push('', 'Reads its tone from the nearest data-tone (warp context tone).')
@@ -271,15 +241,35 @@ export function classSoup(files) {
271
241
  const classes = value.split(/\s+/).filter(Boolean)
272
242
  if (!classes.length) continue
273
243
  const before = text.slice(0, m.index)
274
- const component = [...before.matchAll(/(?:function\s+([A-Z]\w*)|(?:const|let)\s+([A-Z]\w*)\s*=)/g)].at(-1)
244
+ const component = [
245
+ ...before.matchAll(/(?:function\s+([A-Z]\w*)|(?:const|let)\s+([A-Z]\w*)\s*=)/g),
246
+ ].at(-1)
275
247
  const tag = [...before.matchAll(/<([a-z][\w-]*)\b/g)].at(-1)?.[1] ?? 'div'
276
- const base = component ? (component[1] ?? component[2]).replace(/([a-z0-9])([A-Z])/g, '$1-$2').toLowerCase() : rel.replace(/^.*\//, '').replace(/\.\w+$/, '').toLowerCase()
248
+ const base = component
249
+ ? (component[1] ?? component[2]).replace(/([a-z0-9])([A-Z])/g, '$1-$2').toLowerCase()
250
+ : rel
251
+ .replace(/^.*\//, '')
252
+ .replace(/\.\w+$/, '')
253
+ .toLowerCase()
277
254
  const n = (named.get(base) ?? 0) + 1
278
255
  named.set(base, n)
279
256
  const name = n === 1 ? base : `${base}-${tag}`
280
- const at = { span: pos.span(m.index, m.index + m[0].length), offset: [m.index, m.index + m[0].length] }
257
+ const at = {
258
+ span: pos.span(m.index, m.index + m[0].length),
259
+ offset: [m.index, m.index + m[0].length],
260
+ }
281
261
  const site = { rel, at, name, count: classes.length }
282
- if (classes.length > 8) out.push(diagnostic({ code: 'D020', severity: 'warning', file: rel, ...at, why: `<${tag}> carries ${classes.length} classes: nobody can read what it is or which variation it's in.`, help: groupHelp(name) }))
262
+ if (classes.length > 8)
263
+ out.push(
264
+ diagnostic({
265
+ code: 'D020',
266
+ severity: 'warning',
267
+ file: rel,
268
+ ...at,
269
+ why: `<${tag}> carries ${classes.length} classes: nobody can read what it is or which variation it's in.`,
270
+ help: groupHelp(name),
271
+ }),
272
+ )
283
273
  if (classes.length >= 4) {
284
274
  const key = [...classes].sort().join(' ')
285
275
  lists.set(key, [...(lists.get(key) ?? []), site])
@@ -290,31 +280,47 @@ export function classSoup(files) {
290
280
  if (sites.length < 2) continue
291
281
  for (const s of sites.slice(1)) {
292
282
  const first = sites[0]
293
- out.push(diagnostic({
294
- code: 'D020', severity: 'warning', file: s.rel, ...s.at,
295
- why: `The same ${s.count} classes are written in ${sites.length} places (first at ${first.rel}:${first.at.span[0]}): one look, copied by hand.`,
296
- help: groupHelp(first.name),
297
- related: sites.filter(x => x !== s).map(x => ({ file: x.rel, span: x.at.span })),
298
- }))
283
+ out.push(
284
+ diagnostic({
285
+ code: 'D020',
286
+ severity: 'warning',
287
+ file: s.rel,
288
+ ...s.at,
289
+ why: `The same ${s.count} classes are written in ${sites.length} places (first at ${first.rel}:${first.at.span[0]}): one look, copied by hand.`,
290
+ help: groupHelp(first.name),
291
+ related: sites.filter((x) => x !== s).map((x) => ({ file: x.rel, span: x.at.span })),
292
+ }),
293
+ )
299
294
  }
300
295
  }
301
296
  return out
302
297
  }
303
298
 
304
- const groupHelp = name => `Extract a group: \`export const ${name.replace(/-(\w)/g, (_, c) => c.toUpperCase())} = group('${name}', { base: { … } })\` and spread it (\`{...${name.replace(/-(\w)/g, (_, c) => c.toUpperCase())}()}\`). \`weft style find\` first: one may already fit.`
299
+ const groupHelp = (name) =>
300
+ `Extract a group: \`export const ${name.replace(/-(\w)/g, (_, c) => c.toUpperCase())} = group('${name}', { base: { … } })\` and spread it (\`{...${name.replace(/-(\w)/g, (_, c) => c.toUpperCase())}()}\`). \`weft style find\` first: one may already fit.`
305
301
 
306
302
  /** Matrix diagnostics (D021–D025) as `weft check` diagnostics, located on the group or the site. */
307
303
  export function matrixDiagnostics(root, matrix) {
308
304
  const texts = new Map()
309
- const text = file => {
310
- if (!texts.has(file)) texts.set(file, existsSync(join(root, file)) ? readFileSync(join(root, file), 'utf8') : '')
305
+ const text = (file) => {
306
+ if (!texts.has(file))
307
+ texts.set(file, existsSync(join(root, file)) ? readFileSync(join(root, file), 'utf8') : '')
311
308
  return texts.get(file)
312
309
  }
313
- return matrix.diagnostics.map(d => {
310
+ return matrix.diagnostics.map((d) => {
314
311
  const lines = text(d.file).split('\n')
315
312
  const line = Math.max(1, Math.min(d.line, lines.length))
316
313
  const col = (lines[line - 1]?.search(/\S/) ?? 0) + 1
317
- const loc = d.line ? { span: [line, col, line, (lines[line - 1]?.length ?? 0) + 1] } : locate(text(d.file), d.group)
318
- return diagnostic({ code: d.code, severity: d.severity, file: d.file, ...loc, why: d.message, help: d.help })
314
+ const loc = d.line
315
+ ? { span: [line, col, line, (lines[line - 1]?.length ?? 0) + 1] }
316
+ : locate(text(d.file), d.group)
317
+ return diagnostic({
318
+ code: d.code,
319
+ severity: d.severity,
320
+ file: d.file,
321
+ ...loc,
322
+ why: d.message,
323
+ help: d.help,
324
+ })
319
325
  })
320
326
  }