@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
@@ -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) => ts.createSourceFile(rel, text, ts.ScriptTarget.Latest, true, /\.[jt]sx$/.test(rel) ? ts.ScriptKind.TSX : ts.ScriptKind.TS)
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) => object.properties.find(p => (ts.isPropertyAssignment(p) || ts.isMethodDeclaration(p) || ts.isShorthandPropertyAssignment(p)) && p.name.getText() === name)
20
- const stringOf = node => (node && (ts.isStringLiteral(node) || ts.isNoSubstitutionTemplateLiteral(node)) ? node.text : undefined)
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 (schema && ts.isCallExpression(schema) && schema.expression.getText().endsWith('.object') && ts.isObjectLiteralExpression(schema.arguments[0] ?? schema)) return schema.arguments[0].properties.map(p => p.name.getText())
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)) return type.members.map(m => m.name?.getText()).filter(Boolean)
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({ name: stringOf(prop(call.def, 'name')?.initializer) ?? null, kind: stringOf(prop(call.def, 'kind')?.initializer) ?? null, props: propNames(call), file: rel, line, ...(prop(call.def, 'pixel') ? { pixel: true } : {}) })
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 (ts.isArrayLiteralExpression(node) && node.elements.length === 1 && ts.isSpreadElement(node.elements[0])) return node.elements[0].expression
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 (callee === 'Array.from' && node.arguments.length === 1 && !ts.isObjectLiteralExpression(node.arguments[0])) return node.arguments[0]
70
- if (ts.isPropertyAccessExpression(node.expression) && node.expression.name.text === 'split' && stringOf(node.arguments[0]) === '') return node.expression.expression
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) ? n.openingElement : ts.isJsxSelfClosingElement(n) ? n : undefined
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) => open.attributes.properties.some(p => !ts.isJsxSpreadAttribute(p) && p.name.getText() === name)
86
- const spreads = open => open.attributes.properties.some(ts.isJsxSpreadAttribute)
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 (ts.isVariableDeclaration(n) && n.name.getText() === node.text && ts.isVariableDeclarationList(n.parent) && n.parent.flags & ts.NodeFlags.Const) found ??= stringOf(n.initializer)
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 (!ts.isCallExpression(node) || !ts.isPropertyAccessExpression(node.expression) || node.expression.name.text !== 'map') return
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) report('X040', node, `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.`)
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 = !parent || attrOf(parent, 'aria-label') || attrOf(parent, 'aria-labelledby') || spreads(parent)
120
- if (!hidden || !named) report('X041', span, `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`}.`)
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 (ts.isCallExpression(n) && ts.isPropertyAccessExpression(n.expression) && n.expression.name.text === 'decorative') decorative.add(n)
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 => { for (let p = n.parent; p; p = p.parent) if (decorative.has(p)) return true; return false }
132
- walk(sf, n => {
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)) report('X010', n, `\`${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.`)
136
- if (method === 'setAttribute' && stringOf(n.arguments[0]) === 'aria-hidden' && n.expression.expression.getText().endsWith('.el')) report('X011', n, 'This hides the surface or its text mirror from assistive tech. Leave the mirror to weft.')
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]) report('P101', kindNode, `Surface kind \`${kind}\` has no provider: add the plugin that provides it to \`plugins\` in weft.config.ts.`)
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 = options && ts.isObjectLiteralExpression(options) ? stringOf(prop(options, 'font')?.initializer) : undefined
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))) report('X002', n.arguments[1], `\`${font}\` can't be measured safely: \`system-ui\`, \`-apple-system\` and generic-only families resolve differently in canvas and CSS. Name a family.`)
149
- if (kind === 'canvas2d' && slots && slots['text.measure'] === null) report('P101', n, 'Canvas `s.text()` needs `text.measure`, and nothing provides it: add `text()` from `@weft-js/text` to `plugins` in weft.config.ts.')
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 = open >= 0 && /^@media[^{]*\bprint\b/.test(before.slice(open)) && (before.slice(open).match(/\{/g) ?? []).length > (before.slice(open).match(/\}/g) ?? []).length
165
- if (/forced-color-adjust\s*:\s*none/.test(m[3])) report('X012', at, at + 13, '`forced-color-adjust: none` on a surface keeps its author colours in forced-colours mode, where the mirror must take over.')
166
- else if (inPrint && HIDES.test(m[3])) report('X012', at, at + 13, 'This hides surface text when printing, where the mirror is the only copy that prints sharply.')
167
- else if (m[2] && HIDES.test(m[3])) report('X011', at, at + 18, 'This hides the text mirror from assistive tech, and it is the only copy a screen reader can read. Leave the mirror to weft.')
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) => out.push(diagnostic({ code, file: file.rel, span: pos.span(start, end), offset: [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(i => i.source === '@weft-js/core/surface' && i.imported === 'Surface')?.local
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) raw('X009', el.start, el.tagEnd, `<${el.name}> has no accessible name: a screen reader finds an unnamed image. Add \`label\`, or \`decorative\` if it shows nothing a reader needs.`)
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') raw('X011', hidden.start, hidden.end, 'This hides the surface and its text mirror from assistive tech. For a surface with nothing to read use `decorative`, which leaves mirrors alone.')
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 (style && /forced-color-adjust\s*:\s*none|forcedColorAdjust\s*:\s*['"]none/.test(style.value ?? style.expr ?? '')) raw('X012', style.start, style.end, '`forced-color-adjust: none` on a surface keeps its author colours in forced-colours mode, where the mirror must take over.')
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 => (warp
16
- ? `@layer warp, theme, base, components, utilities;
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
- : `@import "tailwindcss";
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 = { type: 'array', items: { type: 'string' }, description: 'Files relative to the project root; default: the whole project' }
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
- { name: 'check', description: 'Compile every component in check mode and report W- and P-diagnostics with fixes', args: { type: 'object', properties: { files } }, sideEffects: [], available: true },
14
- { name: 'tasks', description: 'List every command and project task with args, side effects and environments', args: { type: 'object', properties: {} }, sideEffects: [], available: true },
15
- { name: 'docs', description: 'Print a docs topic or a diagnostic code (`weft docs W014`)', args: { type: 'object', properties: { topic: { type: 'string' } } }, sideEffects: [], available: true },
16
- { name: 'agents-md', description: 'Regenerate AGENTS.md (`--check` fails if stale)', args: { type: 'object', properties: { check: { type: 'boolean' } } }, sideEffects: ['fs:AGENTS.md'], available: true },
17
- { name: 'mcp', description: 'These commands as MCP tools (stdio)', args: { type: 'object', properties: {} }, sideEffects: [], available: true, longRunning: true },
18
- { name: 'style', description: 'Style groups: `find <query>`, `explain <group|selector>`, `matrix` (writes .weft/styles.json)', args: { type: 'object', properties: { subcommand: { type: 'string', enum: ['find', 'explain', 'matrix'] }, query: { type: 'string' }, target: { type: 'string' } } }, sideEffects: ['fs:.weft/styles.json'], available: true },
19
- { name: 'map', description: 'Write `.weft/map.md`: a matrix of what to call in this app', args: { type: 'object', properties: {} }, sideEffects: ['fs:.weft/map.*'], available: true },
20
- { name: 'add', description: 'Install a plugin and wire it into the config (`--dry-run` shows the changes)', args: { type: 'object', properties: { plugin: { type: 'string' }, 'dry-run': { type: 'boolean' } } }, sideEffects: ['fs:weft.config.*', 'fs:package.json', 'network:install'], available: true },
21
- { name: 'dev', description: 'Dev server', sideEffects: ['network:listen'], available: false, stack: true, longRunning: true },
22
- { name: 'build', description: 'Production build and contract', sideEffects: ['fs:.output/**'], available: false, stack: true },
23
- { name: 'start', description: 'Serve the build', sideEffects: ['network:listen'], available: false, stack: true, longRunning: true },
24
- { name: 'db', description: '`generate`: new migrations', sideEffects: ['fs:drizzle/**', 'fs:migrations/**', 'fs:prisma/**'], available: false, stack: true },
25
- { name: 'test', description: 'The test gate on the diff (`--gate`)', args: { type: 'object', properties: { gate: { type: 'boolean' }, base: { type: 'string' }, budget: { type: 'number' }, refactor: { type: 'boolean' } } }, sideEffects: ['fs:.weft/gate'], available: true },
26
- { name: 'fix', description: 'Apply the safe autofixes from `weft check`', args: { type: 'object', properties: { files } }, sideEffects: ['fs:scope.write'], available: true },
27
- { name: 'tokens', description: "warp's resolved tokens as DTCG 2025.10 in `.weft/tokens/` (Figma, Style Dictionary, Terrazzo)", args: { type: 'object', properties: {} }, sideEffects: ['fs:.weft/tokens/**'], available: true },
28
- { name: 'browser', description: 'Drive the running app in any open browser by role/label/text (`weft browser help`)', args: { type: 'object', properties: { command: { type: 'string' } } }, sideEffects: ['network:dev-server', 'fs:.weft/browser'], available: true },
29
- { name: 'run', description: 'Run a project task by name', args: { type: 'object', properties: { name: { type: 'string' } } }, sideEffects: ['task'], available: true },
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
- { name: 'surfaces', description: "List the project's surfaces: name, kind, props and file", sideEffects: [], available: true },
32
- { name: 'plugins', description: 'List and explain plugins and slot bindings; `sync` regenerates .weft/slots and plugin tokens; `audit` and `approve` check and write the permissions lock', args: { type: 'object', properties: { subcommand: { type: 'string', enum: ['list', 'explain', 'sync', 'audit', 'approve'] }, slot: { type: 'string' }, all: { type: 'boolean' } } }, sideEffects: ['fs:.weft/**', 'fs:.gitignore'], available: true },
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) throw Object.assign(new Error(`no task "${name}" (tasks: ${Object.keys(config.tasks ?? {}).join(', ') || 'none'})`), { code: 'E_USAGE' })
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: { check: 'weft check --json', tasks: 'weft tasks --json', docs: 'weft docs <topic>' }[b.name] ?? `weft ${b.name}`,
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: { write: config.scope?.write ?? null, read: config.scope?.read ?? null, forbid: config.scope?.forbid ?? [] },
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
  }