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