@weft-js/cli 0.1.0 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +38 -14
- package/bin/weft.mjs +8 -1
- package/docs/async.md +16 -6
- package/docs/browser.md +22 -8
- package/docs/config.md +39 -9
- package/docs/core.md +30 -0
- package/docs/deploy.md +108 -0
- 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 +174 -188
- 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 +68 -444
- package/src/check/runtime.mjs +280 -0
- package/src/check.mjs +199 -207
- package/src/cli/authoring.mjs +213 -0
- package/src/cli/project.mjs +302 -0
- package/src/cli.mjs +228 -224
- package/src/config.mjs +1 -1
- package/src/contributions.mjs +75 -25
- package/src/db-layer.mjs +13 -5
- package/src/deploy/cdn.mjs +142 -0
- package/src/deploy/files.mjs +266 -0
- package/src/deploy/fit.mjs +166 -0
- package/src/deploy/hosts.mjs +126 -0
- 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 +387 -0
- package/src/deploy/needs.mjs +149 -0
- 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 -42
- 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 +144 -116
- 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 +194 -92
- 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/surface-checks.mjs
CHANGED
|
@@ -14,20 +14,37 @@ const SYSTEM = /(^|,)\s*["']?(system-ui|-apple-system|BlinkMacSystemFont)["']?\s
|
|
|
14
14
|
const GENERIC_ONLY = /^\s*(serif|sans-serif|monospace|cursive|fantasy)\s*$/i
|
|
15
15
|
const GLYPHS = 200
|
|
16
16
|
|
|
17
|
-
const parse = (rel, text) =>
|
|
17
|
+
const parse = (rel, text) =>
|
|
18
|
+
ts.createSourceFile(
|
|
19
|
+
rel,
|
|
20
|
+
text,
|
|
21
|
+
ts.ScriptTarget.Latest,
|
|
22
|
+
true,
|
|
23
|
+
/\.[jt]sx$/.test(rel) ? ts.ScriptKind.TSX : ts.ScriptKind.TS,
|
|
24
|
+
)
|
|
18
25
|
|
|
19
|
-
const prop = (object, name) =>
|
|
20
|
-
|
|
26
|
+
const prop = (object, name) =>
|
|
27
|
+
object.properties.find(
|
|
28
|
+
(p) =>
|
|
29
|
+
(ts.isPropertyAssignment(p) ||
|
|
30
|
+
ts.isMethodDeclaration(p) ||
|
|
31
|
+
ts.isShorthandPropertyAssignment(p)) &&
|
|
32
|
+
p.name.getText() === name,
|
|
33
|
+
)
|
|
34
|
+
const stringOf = (node) =>
|
|
35
|
+
node && (ts.isStringLiteral(node) || ts.isNoSubstitutionTemplateLiteral(node))
|
|
36
|
+
? node.text
|
|
37
|
+
: undefined
|
|
21
38
|
|
|
22
39
|
function walk(node, visit) {
|
|
23
40
|
visit(node)
|
|
24
|
-
ts.forEachChild(node, c => walk(c, visit))
|
|
41
|
+
ts.forEachChild(node, (c) => walk(c, visit))
|
|
25
42
|
}
|
|
26
43
|
|
|
27
44
|
/** Every `defineSurface({ ... })` call in a file: the call, its options and the name of `setup`'s `s`. */
|
|
28
45
|
function surfaceCalls(sf) {
|
|
29
46
|
const out = []
|
|
30
|
-
walk(sf, node => {
|
|
47
|
+
walk(sf, (node) => {
|
|
31
48
|
if (!ts.isCallExpression(node) || node.expression.getText() !== 'defineSurface') return
|
|
32
49
|
const def = node.arguments[0]
|
|
33
50
|
if (!def || !ts.isObjectLiteralExpression(def)) return
|
|
@@ -40,9 +57,16 @@ function surfaceCalls(sf) {
|
|
|
40
57
|
|
|
41
58
|
function propNames(call) {
|
|
42
59
|
const schema = prop(call.def, 'props')?.initializer
|
|
43
|
-
if (
|
|
60
|
+
if (
|
|
61
|
+
schema &&
|
|
62
|
+
ts.isCallExpression(schema) &&
|
|
63
|
+
schema.expression.getText().endsWith('.object') &&
|
|
64
|
+
ts.isObjectLiteralExpression(schema.arguments[0] ?? schema)
|
|
65
|
+
)
|
|
66
|
+
return schema.arguments[0].properties.map((p) => p.name.getText())
|
|
44
67
|
const type = call.node.typeArguments?.[0]
|
|
45
|
-
if (type && ts.isTypeLiteralNode(type))
|
|
68
|
+
if (type && ts.isTypeLiteralNode(type))
|
|
69
|
+
return type.members.map((m) => m.name?.getText()).filter(Boolean)
|
|
46
70
|
return []
|
|
47
71
|
}
|
|
48
72
|
|
|
@@ -55,7 +79,14 @@ export function listSurfaces(root, { exclude = [] } = {}) {
|
|
|
55
79
|
const sf = parse(rel, text)
|
|
56
80
|
for (const call of surfaceCalls(sf)) {
|
|
57
81
|
const line = sf.getLineAndCharacterOfPosition(call.node.getStart(sf)).line + 1
|
|
58
|
-
out.push({
|
|
82
|
+
out.push({
|
|
83
|
+
name: stringOf(prop(call.def, 'name')?.initializer) ?? null,
|
|
84
|
+
kind: stringOf(prop(call.def, 'kind')?.initializer) ?? null,
|
|
85
|
+
props: propNames(call),
|
|
86
|
+
file: rel,
|
|
87
|
+
line,
|
|
88
|
+
...(prop(call.def, 'pixel') ? { pixel: true } : {}),
|
|
89
|
+
})
|
|
59
90
|
}
|
|
60
91
|
}
|
|
61
92
|
return out.sort((a, b) => String(a.name).localeCompare(String(b.name)))
|
|
@@ -63,27 +94,47 @@ export function listSurfaces(root, { exclude = [] } = {}) {
|
|
|
63
94
|
|
|
64
95
|
/** `[...x]`, `x.split('')` or `Array.from(x)`: the string being spread, or undefined. */
|
|
65
96
|
function glyphSource(node) {
|
|
66
|
-
if (
|
|
97
|
+
if (
|
|
98
|
+
ts.isArrayLiteralExpression(node) &&
|
|
99
|
+
node.elements.length === 1 &&
|
|
100
|
+
ts.isSpreadElement(node.elements[0])
|
|
101
|
+
)
|
|
102
|
+
return node.elements[0].expression
|
|
67
103
|
if (!ts.isCallExpression(node)) return undefined
|
|
68
104
|
const callee = node.expression.getText()
|
|
69
|
-
if (
|
|
70
|
-
|
|
105
|
+
if (
|
|
106
|
+
callee === 'Array.from' &&
|
|
107
|
+
node.arguments.length === 1 &&
|
|
108
|
+
!ts.isObjectLiteralExpression(node.arguments[0])
|
|
109
|
+
)
|
|
110
|
+
return node.arguments[0]
|
|
111
|
+
if (
|
|
112
|
+
ts.isPropertyAccessExpression(node.expression) &&
|
|
113
|
+
node.expression.name.text === 'split' &&
|
|
114
|
+
stringOf(node.arguments[0]) === ''
|
|
115
|
+
)
|
|
116
|
+
return node.expression.expression
|
|
71
117
|
return undefined
|
|
72
118
|
}
|
|
73
119
|
|
|
74
120
|
/** The first `<span>` a `.map` callback returns. */
|
|
75
121
|
function returnedSpan(fn) {
|
|
76
122
|
let span
|
|
77
|
-
walk(fn.body, n => {
|
|
123
|
+
walk(fn.body, (n) => {
|
|
78
124
|
if (span) return
|
|
79
|
-
const open = ts.isJsxElement(n)
|
|
125
|
+
const open = ts.isJsxElement(n)
|
|
126
|
+
? n.openingElement
|
|
127
|
+
: ts.isJsxSelfClosingElement(n)
|
|
128
|
+
? n
|
|
129
|
+
: undefined
|
|
80
130
|
if (open?.tagName.getText() === 'span') span = open
|
|
81
131
|
})
|
|
82
132
|
return span
|
|
83
133
|
}
|
|
84
134
|
|
|
85
|
-
const attrOf = (open, name) =>
|
|
86
|
-
|
|
135
|
+
const attrOf = (open, name) =>
|
|
136
|
+
open.attributes.properties.some((p) => !ts.isJsxSpreadAttribute(p) && p.name.getText() === name)
|
|
137
|
+
const spreads = (open) => open.attributes.properties.some(ts.isJsxSpreadAttribute)
|
|
87
138
|
|
|
88
139
|
/** The JSX element a child expression sits in. */
|
|
89
140
|
function jsxParent(node) {
|
|
@@ -97,15 +148,26 @@ function knownString(sf, node) {
|
|
|
97
148
|
if (direct !== undefined) return direct
|
|
98
149
|
if (!ts.isIdentifier(node)) return undefined
|
|
99
150
|
let found
|
|
100
|
-
walk(sf, n => {
|
|
101
|
-
if (
|
|
151
|
+
walk(sf, (n) => {
|
|
152
|
+
if (
|
|
153
|
+
ts.isVariableDeclaration(n) &&
|
|
154
|
+
n.name.getText() === node.text &&
|
|
155
|
+
ts.isVariableDeclarationList(n.parent) &&
|
|
156
|
+
n.parent.flags & ts.NodeFlags.Const
|
|
157
|
+
)
|
|
158
|
+
found ??= stringOf(n.initializer)
|
|
102
159
|
})
|
|
103
160
|
return found
|
|
104
161
|
}
|
|
105
162
|
|
|
106
163
|
function glyphChecks(sf, report) {
|
|
107
|
-
walk(sf, node => {
|
|
108
|
-
if (
|
|
164
|
+
walk(sf, (node) => {
|
|
165
|
+
if (
|
|
166
|
+
!ts.isCallExpression(node) ||
|
|
167
|
+
!ts.isPropertyAccessExpression(node.expression) ||
|
|
168
|
+
node.expression.name.text !== 'map'
|
|
169
|
+
)
|
|
170
|
+
return
|
|
109
171
|
const source = glyphSource(node.expression.expression)
|
|
110
172
|
const fn = node.arguments[0]
|
|
111
173
|
if (!source || !fn || !(ts.isArrowFunction(fn) || ts.isFunctionExpression(fn))) return
|
|
@@ -113,11 +175,25 @@ function glyphChecks(sf, report) {
|
|
|
113
175
|
if (!span) return
|
|
114
176
|
const text = knownString(sf, source)
|
|
115
177
|
const count = text === undefined ? 0 : Array.from(text).length
|
|
116
|
-
if (count > GLYPHS)
|
|
178
|
+
if (count > GLYPHS)
|
|
179
|
+
report(
|
|
180
|
+
'X040',
|
|
181
|
+
node,
|
|
182
|
+
`Per-grapheme spans over ${count} graphemes: each is a DOM node, a style and a composited layer once it animates. Use word-level motion or a Surface.`,
|
|
183
|
+
)
|
|
117
184
|
const hidden = attrOf(span, 'aria-hidden') || spreads(span)
|
|
118
185
|
const parent = jsxParent(node)
|
|
119
|
-
const named =
|
|
120
|
-
|
|
186
|
+
const named =
|
|
187
|
+
!parent ||
|
|
188
|
+
attrOf(parent, 'aria-label') ||
|
|
189
|
+
attrOf(parent, 'aria-labelledby') ||
|
|
190
|
+
spreads(parent)
|
|
191
|
+
if (!hidden || !named)
|
|
192
|
+
report(
|
|
193
|
+
'X041',
|
|
194
|
+
span,
|
|
195
|
+
`Per-character spans read as separate letters to a screen reader${hidden ? '' : ': add `aria-hidden` to each span'}${named ? '' : `${hidden ? ':' : ', and'} give the parent an \`aria-label\` with the whole text`}.`,
|
|
196
|
+
)
|
|
121
197
|
})
|
|
122
198
|
}
|
|
123
199
|
|
|
@@ -125,28 +201,68 @@ function surfaceFileChecks(sf, slots, report) {
|
|
|
125
201
|
const calls = surfaceCalls(sf)
|
|
126
202
|
if (!calls.length) return
|
|
127
203
|
const decorative = new Set()
|
|
128
|
-
walk(sf, n => {
|
|
129
|
-
if (
|
|
204
|
+
walk(sf, (n) => {
|
|
205
|
+
if (
|
|
206
|
+
ts.isCallExpression(n) &&
|
|
207
|
+
ts.isPropertyAccessExpression(n.expression) &&
|
|
208
|
+
n.expression.name.text === 'decorative'
|
|
209
|
+
)
|
|
210
|
+
decorative.add(n)
|
|
130
211
|
})
|
|
131
|
-
const inside = n => {
|
|
132
|
-
|
|
212
|
+
const inside = (n) => {
|
|
213
|
+
for (let p = n.parent; p; p = p.parent) if (decorative.has(p)) return true
|
|
214
|
+
return false
|
|
215
|
+
}
|
|
216
|
+
walk(sf, (n) => {
|
|
133
217
|
if (!ts.isCallExpression(n) || !ts.isPropertyAccessExpression(n.expression)) return
|
|
134
218
|
const method = n.expression.name.text
|
|
135
|
-
if ((method === 'fillText' || method === 'strokeText') && !inside(n))
|
|
136
|
-
|
|
219
|
+
if ((method === 'fillText' || method === 'strokeText') && !inside(n))
|
|
220
|
+
report(
|
|
221
|
+
'X010',
|
|
222
|
+
n,
|
|
223
|
+
`\`${method}\` in a surface draws text only the canvas knows about: no selection, find, translation or screen reader. Use \`s.text()\`, or \`s.decorative()\` for text that is truly decoration.`,
|
|
224
|
+
)
|
|
225
|
+
if (
|
|
226
|
+
method === 'setAttribute' &&
|
|
227
|
+
stringOf(n.arguments[0]) === 'aria-hidden' &&
|
|
228
|
+
n.expression.expression.getText().endsWith('.el')
|
|
229
|
+
)
|
|
230
|
+
report(
|
|
231
|
+
'X011',
|
|
232
|
+
n,
|
|
233
|
+
'This hides the surface or its text mirror from assistive tech. Leave the mirror to weft.',
|
|
234
|
+
)
|
|
137
235
|
})
|
|
138
236
|
for (const call of calls) {
|
|
139
237
|
const kindNode = prop(call.def, 'kind')?.initializer
|
|
140
238
|
const kind = stringOf(kindNode)
|
|
141
|
-
if (kind && slots && slots['surface.renderer'] && !slots['surface.renderer'][kind])
|
|
239
|
+
if (kind && slots && slots['surface.renderer'] && !slots['surface.renderer'][kind])
|
|
240
|
+
report(
|
|
241
|
+
'P101',
|
|
242
|
+
kindNode,
|
|
243
|
+
`Surface kind \`${kind}\` has no provider: add the plugin that provides it to \`plugins\` in weft.config.ts.`,
|
|
244
|
+
)
|
|
142
245
|
if (!call.param || !call.fn) continue
|
|
143
|
-
walk(call.fn, n => {
|
|
246
|
+
walk(call.fn, (n) => {
|
|
144
247
|
if (!ts.isCallExpression(n) || n.expression.getText() !== `${call.param}.text`) return
|
|
145
248
|
const options = n.arguments[1]
|
|
146
|
-
const font =
|
|
249
|
+
const font =
|
|
250
|
+
options && ts.isObjectLiteralExpression(options)
|
|
251
|
+
? stringOf(prop(options, 'font')?.initializer)
|
|
252
|
+
: undefined
|
|
147
253
|
const family = font?.replace(/^.*?\d[\d.]*px(\/\S+)?\s*/, '')
|
|
148
|
-
if (family !== undefined && (SYSTEM.test(family) || GENERIC_ONLY.test(family)))
|
|
149
|
-
|
|
254
|
+
if (family !== undefined && (SYSTEM.test(family) || GENERIC_ONLY.test(family)))
|
|
255
|
+
report(
|
|
256
|
+
'X002',
|
|
257
|
+
n.arguments[1],
|
|
258
|
+
`\`${font}\` can't be measured safely: \`system-ui\`, \`-apple-system\` and generic-only families resolve differently in canvas and CSS. Name a family.`,
|
|
259
|
+
)
|
|
260
|
+
if (kind === 'canvas2d' && slots && slots['text.measure'] === null)
|
|
261
|
+
report(
|
|
262
|
+
'P101',
|
|
263
|
+
n,
|
|
264
|
+
'Canvas `s.text()` needs `text.measure`, and nothing provides it: add `text()` from `@weft-js/text` to `plugins` in weft.config.ts.',
|
|
265
|
+
)
|
|
150
266
|
})
|
|
151
267
|
}
|
|
152
268
|
}
|
|
@@ -155,16 +271,38 @@ const HIDES = /(?:display\s*:\s*none|visibility\s*:\s*hidden)/
|
|
|
155
271
|
|
|
156
272
|
/** X011/X012 in a stylesheet: rules aimed at the host or the mirror. */
|
|
157
273
|
function cssChecks(file, report) {
|
|
158
|
-
const css = file.text.replace(/\/\*[\s\S]*?\*\//g, m => m.replace(/[^\n]/g, ' '))
|
|
274
|
+
const css = file.text.replace(/\/\*[\s\S]*?\*\//g, (m) => m.replace(/[^\n]/g, ' '))
|
|
159
275
|
const rule = /([^{}]*\[data-surface(-text)?\b[^{}]*)\{([^}]*)\}/g
|
|
160
276
|
for (const m of css.matchAll(rule)) {
|
|
161
277
|
const at = m.index + m[0].indexOf('[data-surface')
|
|
162
278
|
const before = css.slice(0, m.index)
|
|
163
279
|
const open = before.lastIndexOf('@media')
|
|
164
|
-
const inPrint =
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
280
|
+
const inPrint =
|
|
281
|
+
open >= 0 &&
|
|
282
|
+
/^@media[^{]*\bprint\b/.test(before.slice(open)) &&
|
|
283
|
+
(before.slice(open).match(/\{/g) ?? []).length >
|
|
284
|
+
(before.slice(open).match(/\}/g) ?? []).length
|
|
285
|
+
if (/forced-color-adjust\s*:\s*none/.test(m[3]))
|
|
286
|
+
report(
|
|
287
|
+
'X012',
|
|
288
|
+
at,
|
|
289
|
+
at + 13,
|
|
290
|
+
'`forced-color-adjust: none` on a surface keeps its author colours in forced-colours mode, where the mirror must take over.',
|
|
291
|
+
)
|
|
292
|
+
else if (inPrint && HIDES.test(m[3]))
|
|
293
|
+
report(
|
|
294
|
+
'X012',
|
|
295
|
+
at,
|
|
296
|
+
at + 13,
|
|
297
|
+
'This hides surface text when printing, where the mirror is the only copy that prints sharply.',
|
|
298
|
+
)
|
|
299
|
+
else if (m[2] && HIDES.test(m[3]))
|
|
300
|
+
report(
|
|
301
|
+
'X011',
|
|
302
|
+
at,
|
|
303
|
+
at + 18,
|
|
304
|
+
'This hides the text mirror from assistive tech, and it is the only copy a screen reader can read. Leave the mirror to weft.',
|
|
305
|
+
)
|
|
168
306
|
}
|
|
169
307
|
}
|
|
170
308
|
|
|
@@ -174,11 +312,14 @@ function cssChecks(file, report) {
|
|
|
174
312
|
*/
|
|
175
313
|
export function surfaceChecks(root, { files, exclude = [], slots }) {
|
|
176
314
|
const out = []
|
|
177
|
-
const reporter = file => {
|
|
315
|
+
const reporter = (file) => {
|
|
178
316
|
const pos = positions(file.text)
|
|
179
|
-
return (code, start, end, why) =>
|
|
317
|
+
return (code, start, end, why) =>
|
|
318
|
+
out.push(
|
|
319
|
+
diagnostic({ code, file: file.rel, span: pos.span(start, end), offset: [start, end], why }),
|
|
320
|
+
)
|
|
180
321
|
}
|
|
181
|
-
const sources = new Map(files.filter(f => CODE.test(f.rel)).map(f => [f.rel, f]))
|
|
322
|
+
const sources = new Map(files.filter((f) => CODE.test(f.rel)).map((f) => [f.rel, f]))
|
|
182
323
|
for (const rel of discover(root, { include: ['src/**/*.{ts,tsx,js,jsx}'], exclude })) {
|
|
183
324
|
if (!sources.has(rel)) sources.set(rel, { rel, text: readFileSync(join(root, rel), 'utf8') })
|
|
184
325
|
}
|
|
@@ -199,15 +340,40 @@ export function surfaceChecks(root, { files, exclude = [], slots }) {
|
|
|
199
340
|
surfaceFileChecks(sf, slots, report)
|
|
200
341
|
if (!/\.[jt]sx$/.test(f.rel)) continue
|
|
201
342
|
const scan = scanJsx(f.text, f.rel)
|
|
202
|
-
const local = scan.imports.find(
|
|
343
|
+
const local = scan.imports.find(
|
|
344
|
+
(i) => i.source === '@weft-js/core/surface' && i.imported === 'Surface',
|
|
345
|
+
)?.local
|
|
203
346
|
for (const el of scan.elements) {
|
|
204
347
|
if (el.name !== local) continue
|
|
205
|
-
const spread = el.attrs.some(a => a.kind === 'spread')
|
|
206
|
-
if (!el.attr('label') && !el.attr('decorative') && !spread)
|
|
348
|
+
const spread = el.attrs.some((a) => a.kind === 'spread')
|
|
349
|
+
if (!el.attr('label') && !el.attr('decorative') && !spread)
|
|
350
|
+
raw(
|
|
351
|
+
'X009',
|
|
352
|
+
el.start,
|
|
353
|
+
el.tagEnd,
|
|
354
|
+
`<${el.name}> has no accessible name: a screen reader finds an unnamed image. Add \`label\`, or \`decorative\` if it shows nothing a reader needs.`,
|
|
355
|
+
)
|
|
207
356
|
const hidden = el.attr('aria-hidden')
|
|
208
|
-
if (hidden && hidden.value !== 'false')
|
|
357
|
+
if (hidden && hidden.value !== 'false')
|
|
358
|
+
raw(
|
|
359
|
+
'X011',
|
|
360
|
+
hidden.start,
|
|
361
|
+
hidden.end,
|
|
362
|
+
'This hides the surface and its text mirror from assistive tech. For a surface with nothing to read use `decorative`, which leaves mirrors alone.',
|
|
363
|
+
)
|
|
209
364
|
const style = el.attr('style')
|
|
210
|
-
if (
|
|
365
|
+
if (
|
|
366
|
+
style &&
|
|
367
|
+
/forced-color-adjust\s*:\s*none|forcedColorAdjust\s*:\s*['"]none/.test(
|
|
368
|
+
style.value ?? style.expr ?? '',
|
|
369
|
+
)
|
|
370
|
+
)
|
|
371
|
+
raw(
|
|
372
|
+
'X012',
|
|
373
|
+
style.start,
|
|
374
|
+
style.end,
|
|
375
|
+
'`forced-color-adjust: none` on a surface keeps its author colours in forced-colours mode, where the mirror must take over.',
|
|
376
|
+
)
|
|
211
377
|
}
|
|
212
378
|
}
|
|
213
379
|
for (const rel of discover(root, { include: ['**/*.css'], exclude })) {
|
package/src/tailwind.mjs
CHANGED
|
@@ -12,11 +12,12 @@ export default { plugins: [tailwindcss()] }
|
|
|
12
12
|
* The top of the app's CSS. With warp, Tailwind's reset stays out: its `base` layer comes after `warp`, so it would
|
|
13
13
|
* beat every warp component. warp brings its own base. Layer order has to be declared before any import.
|
|
14
14
|
*/
|
|
15
|
-
export const cssEntry = warp =>
|
|
16
|
-
|
|
15
|
+
export const cssEntry = (warp) =>
|
|
16
|
+
warp
|
|
17
|
+
? `@layer warp, theme, base, components, utilities;
|
|
17
18
|
@import "tailwindcss/theme.css" layer(theme);
|
|
18
19
|
@import "tailwindcss/utilities.css" layer(utilities);
|
|
19
20
|
@import "@weft-js/warp/tailwind.css";
|
|
20
21
|
`
|
|
21
|
-
|
|
22
|
-
`
|
|
22
|
+
: `@import "tailwindcss";
|
|
23
|
+
`
|
package/src/tasks.mjs
CHANGED
|
@@ -6,30 +6,179 @@ import { join, relative, resolve } from 'node:path'
|
|
|
6
6
|
import { z } from 'zod'
|
|
7
7
|
import { loadConfig } from './load-config.mjs'
|
|
8
8
|
|
|
9
|
-
const files = {
|
|
9
|
+
const files = {
|
|
10
|
+
type: 'array',
|
|
11
|
+
items: { type: 'string' },
|
|
12
|
+
description: 'Files relative to the project root; default: the whole project',
|
|
13
|
+
}
|
|
10
14
|
|
|
11
15
|
/** `available: false` marks commands PLAN names that this CLI does not ship yet; `stack` ones ship for apps with src/routes. */
|
|
12
16
|
export const builtins = [
|
|
13
|
-
{
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
{
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
{
|
|
28
|
-
|
|
29
|
-
|
|
17
|
+
{
|
|
18
|
+
name: 'check',
|
|
19
|
+
description: 'Compile every component in check mode and report W- and P-diagnostics with fixes',
|
|
20
|
+
args: { type: 'object', properties: { files } },
|
|
21
|
+
sideEffects: [],
|
|
22
|
+
available: true,
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
name: 'tasks',
|
|
26
|
+
description: 'List every command and project task with args, side effects and environments',
|
|
27
|
+
args: { type: 'object', properties: {} },
|
|
28
|
+
sideEffects: [],
|
|
29
|
+
available: true,
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
name: 'docs',
|
|
33
|
+
description: 'Print a docs topic or a diagnostic code (`weft docs W014`)',
|
|
34
|
+
args: { type: 'object', properties: { topic: { type: 'string' } } },
|
|
35
|
+
sideEffects: [],
|
|
36
|
+
available: true,
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
name: 'agents-md',
|
|
40
|
+
description: 'Regenerate AGENTS.md (`--check` fails if stale)',
|
|
41
|
+
args: { type: 'object', properties: { check: { type: 'boolean' } } },
|
|
42
|
+
sideEffects: ['fs:AGENTS.md'],
|
|
43
|
+
available: true,
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
name: 'mcp',
|
|
47
|
+
description: 'These commands as MCP tools (stdio)',
|
|
48
|
+
args: { type: 'object', properties: {} },
|
|
49
|
+
sideEffects: [],
|
|
50
|
+
available: true,
|
|
51
|
+
longRunning: true,
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
name: 'style',
|
|
55
|
+
description:
|
|
56
|
+
'Style groups: `find <query>`, `explain <group|selector>`, `matrix` (writes .weft/styles.json)',
|
|
57
|
+
args: {
|
|
58
|
+
type: 'object',
|
|
59
|
+
properties: {
|
|
60
|
+
subcommand: { type: 'string', enum: ['find', 'explain', 'matrix'] },
|
|
61
|
+
query: { type: 'string' },
|
|
62
|
+
target: { type: 'string' },
|
|
63
|
+
},
|
|
64
|
+
},
|
|
65
|
+
sideEffects: ['fs:.weft/styles.json'],
|
|
66
|
+
available: true,
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
name: 'map',
|
|
70
|
+
description: 'Write `.weft/map.md`: a matrix of what to call in this app',
|
|
71
|
+
args: { type: 'object', properties: {} },
|
|
72
|
+
sideEffects: ['fs:.weft/map.*'],
|
|
73
|
+
available: true,
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
name: 'add',
|
|
77
|
+
description: 'Install a plugin and wire it into the config (`--dry-run` shows the changes)',
|
|
78
|
+
args: {
|
|
79
|
+
type: 'object',
|
|
80
|
+
properties: { plugin: { type: 'string' }, 'dry-run': { type: 'boolean' } },
|
|
81
|
+
},
|
|
82
|
+
sideEffects: ['fs:weft.config.*', 'fs:package.json', 'network:install'],
|
|
83
|
+
available: true,
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
name: 'dev',
|
|
87
|
+
description: 'Dev server',
|
|
88
|
+
sideEffects: ['network:listen'],
|
|
89
|
+
available: false,
|
|
90
|
+
stack: true,
|
|
91
|
+
longRunning: true,
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
name: 'build',
|
|
95
|
+
description: 'Production build and contract',
|
|
96
|
+
sideEffects: ['fs:.output/**'],
|
|
97
|
+
available: false,
|
|
98
|
+
stack: true,
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
name: 'start',
|
|
102
|
+
description: 'Serve the build',
|
|
103
|
+
sideEffects: ['network:listen'],
|
|
104
|
+
available: false,
|
|
105
|
+
stack: true,
|
|
106
|
+
longRunning: true,
|
|
107
|
+
},
|
|
108
|
+
{
|
|
109
|
+
name: 'db',
|
|
110
|
+
description: '`generate`: new migrations',
|
|
111
|
+
sideEffects: ['fs:drizzle/**', 'fs:migrations/**', 'fs:prisma/**'],
|
|
112
|
+
available: false,
|
|
113
|
+
stack: true,
|
|
114
|
+
},
|
|
115
|
+
{
|
|
116
|
+
name: 'test',
|
|
117
|
+
description: 'The test gate on the diff (`--gate`)',
|
|
118
|
+
args: {
|
|
119
|
+
type: 'object',
|
|
120
|
+
properties: {
|
|
121
|
+
gate: { type: 'boolean' },
|
|
122
|
+
base: { type: 'string' },
|
|
123
|
+
budget: { type: 'number' },
|
|
124
|
+
refactor: { type: 'boolean' },
|
|
125
|
+
},
|
|
126
|
+
},
|
|
127
|
+
sideEffects: ['fs:.weft/gate'],
|
|
128
|
+
available: true,
|
|
129
|
+
},
|
|
130
|
+
{
|
|
131
|
+
name: 'fix',
|
|
132
|
+
description: 'Apply the safe autofixes from `weft check`',
|
|
133
|
+
args: { type: 'object', properties: { files } },
|
|
134
|
+
sideEffects: ['fs:scope.write'],
|
|
135
|
+
available: true,
|
|
136
|
+
},
|
|
137
|
+
{
|
|
138
|
+
name: 'tokens',
|
|
139
|
+
description:
|
|
140
|
+
"warp's resolved tokens as DTCG 2025.10 in `.weft/tokens/` (Figma, Style Dictionary, Terrazzo)",
|
|
141
|
+
args: { type: 'object', properties: {} },
|
|
142
|
+
sideEffects: ['fs:.weft/tokens/**'],
|
|
143
|
+
available: true,
|
|
144
|
+
},
|
|
145
|
+
{
|
|
146
|
+
name: 'browser',
|
|
147
|
+
description:
|
|
148
|
+
'Drive the running app in any open browser by role/label/text (`weft browser help`)',
|
|
149
|
+
args: { type: 'object', properties: { command: { type: 'string' } } },
|
|
150
|
+
sideEffects: ['network:dev-server', 'fs:.weft/browser'],
|
|
151
|
+
available: true,
|
|
152
|
+
},
|
|
153
|
+
{
|
|
154
|
+
name: 'run',
|
|
155
|
+
description: 'Run a project task by name',
|
|
156
|
+
args: { type: 'object', properties: { name: { type: 'string' } } },
|
|
157
|
+
sideEffects: ['task'],
|
|
158
|
+
available: true,
|
|
159
|
+
},
|
|
30
160
|
{ name: 'create', description: 'Scaffold a weft app', sideEffects: ['fs'], available: false },
|
|
31
|
-
{
|
|
32
|
-
|
|
161
|
+
{
|
|
162
|
+
name: 'surfaces',
|
|
163
|
+
description: "List the project's surfaces: name, kind, props and file",
|
|
164
|
+
sideEffects: [],
|
|
165
|
+
available: true,
|
|
166
|
+
},
|
|
167
|
+
{
|
|
168
|
+
name: 'plugins',
|
|
169
|
+
description:
|
|
170
|
+
'List and explain plugins and slot bindings; `sync` regenerates .weft/slots and plugin tokens; `audit` and `approve` check and write the permissions lock',
|
|
171
|
+
args: {
|
|
172
|
+
type: 'object',
|
|
173
|
+
properties: {
|
|
174
|
+
subcommand: { type: 'string', enum: ['list', 'explain', 'sync', 'audit', 'approve'] },
|
|
175
|
+
slot: { type: 'string' },
|
|
176
|
+
all: { type: 'boolean' },
|
|
177
|
+
},
|
|
178
|
+
},
|
|
179
|
+
sideEffects: ['fs:.weft/**', 'fs:.gitignore'],
|
|
180
|
+
available: true,
|
|
181
|
+
},
|
|
33
182
|
]
|
|
34
183
|
|
|
35
184
|
function jsonSchema(args) {
|
|
@@ -49,11 +198,17 @@ export async function runTask(root, name) {
|
|
|
49
198
|
root = resolve(root)
|
|
50
199
|
const { config } = await loadConfig(root)
|
|
51
200
|
const task = config.tasks?.[name]
|
|
52
|
-
if (!task)
|
|
201
|
+
if (!task)
|
|
202
|
+
throw Object.assign(
|
|
203
|
+
new Error(
|
|
204
|
+
`no task "${name}" (tasks: ${Object.keys(config.tasks ?? {}).join(', ') || 'none'})`,
|
|
205
|
+
),
|
|
206
|
+
{ code: 'E_USAGE' },
|
|
207
|
+
)
|
|
53
208
|
return new Promise((ok, fail) => {
|
|
54
209
|
const child = spawn(task.run, { cwd: root, shell: true, stdio: 'inherit' })
|
|
55
210
|
child.on('error', fail)
|
|
56
|
-
child.on('close', code => ok(code ?? 1))
|
|
211
|
+
child.on('close', (code) => ok(code ?? 1))
|
|
57
212
|
})
|
|
58
213
|
}
|
|
59
214
|
|
|
@@ -63,10 +218,13 @@ export async function tasks(root) {
|
|
|
63
218
|
const { file, config, diagnostics } = await loadConfig(root)
|
|
64
219
|
const environments = Object.keys(config.environments ?? {})
|
|
65
220
|
const stack = existsSync(join(root, 'src', 'routes'))
|
|
66
|
-
const list = builtins.map(b => ({
|
|
221
|
+
const list = builtins.map((b) => ({
|
|
67
222
|
name: b.name,
|
|
68
223
|
kind: 'builtin',
|
|
69
|
-
command:
|
|
224
|
+
command:
|
|
225
|
+
{ check: 'weft check --json', tasks: 'weft tasks --json', docs: 'weft docs <topic>' }[
|
|
226
|
+
b.name
|
|
227
|
+
] ?? `weft ${b.name}`,
|
|
70
228
|
description: b.description,
|
|
71
229
|
args: b.args ?? { type: 'object', properties: {} },
|
|
72
230
|
sideEffects: b.sideEffects,
|
|
@@ -88,12 +246,16 @@ export async function tasks(root) {
|
|
|
88
246
|
}
|
|
89
247
|
return {
|
|
90
248
|
version: 1,
|
|
91
|
-
ok: !diagnostics.some(d => d.severity === 'error'),
|
|
249
|
+
ok: !diagnostics.some((d) => d.severity === 'error'),
|
|
92
250
|
root,
|
|
93
251
|
config: file ? relative(root, file).split('\\').join('/') : null,
|
|
94
252
|
tasks: list,
|
|
95
|
-
scope: {
|
|
253
|
+
scope: {
|
|
254
|
+
write: config.scope?.write ?? null,
|
|
255
|
+
read: config.scope?.read ?? null,
|
|
256
|
+
forbid: config.scope?.forbid ?? [],
|
|
257
|
+
},
|
|
96
258
|
environments,
|
|
97
|
-
diagnostics: diagnostics.filter(d => ['P003', 'P004', 'P007'].includes(d.code)),
|
|
259
|
+
diagnostics: diagnostics.filter((d) => ['P003', 'P004', 'P007'].includes(d.code)),
|
|
98
260
|
}
|
|
99
261
|
}
|