@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.
Files changed (171) hide show
  1. package/README.md +534 -54
  2. package/bin/weft.mjs +8 -1
  3. package/docs/async.md +16 -6
  4. package/docs/browser.md +22 -8
  5. package/docs/config.md +39 -9
  6. package/docs/core.md +30 -0
  7. package/docs/deploy.md +33 -8
  8. package/docs/design.md +12 -3
  9. package/docs/entrypoints.md +79 -0
  10. package/docs/fonts.md +12 -2
  11. package/docs/icons.md +13 -7
  12. package/docs/lists.md +3 -1
  13. package/docs/meta.md +13 -3
  14. package/docs/plugins.md +32 -9
  15. package/docs/props.md +13 -2
  16. package/docs/reactivity.md +26 -0
  17. package/docs/rules.md +19 -12
  18. package/docs/rust.md +47 -17
  19. package/docs/start-short.md +3 -2
  20. package/docs/start.md +226 -52
  21. package/docs/state.md +33 -7
  22. package/docs/style.md +31 -8
  23. package/docs/surface.md +61 -15
  24. package/docs/testing.md +15 -5
  25. package/docs/theme.md +182 -0
  26. package/docs/ui-state.md +19 -6
  27. package/docs/verify.md +13 -0
  28. package/package.json +13 -4
  29. package/src/add/config.mjs +93 -0
  30. package/src/add/errors.mjs +2 -0
  31. package/src/add/setup.mjs +88 -0
  32. package/src/add-rust.mjs +82 -27
  33. package/src/add.mjs +209 -181
  34. package/src/agents-md.mjs +144 -43
  35. package/src/alias.mjs +16 -5
  36. package/src/audit/page.mjs +313 -0
  37. package/src/audit/report.mjs +69 -0
  38. package/src/audit/utils.mjs +5 -0
  39. package/src/audit.mjs +159 -206
  40. package/src/browser/cli.mjs +24 -5
  41. package/src/browser/client.mjs +189 -25
  42. package/src/browser/commands.mjs +166 -51
  43. package/src/catalog/audit.mjs +129 -0
  44. package/src/catalog/auth.mjs +40 -0
  45. package/src/catalog/compiler.mjs +337 -0
  46. package/src/catalog/contracts.mjs +18 -0
  47. package/src/catalog/database.mjs +52 -0
  48. package/src/catalog/deploy.mjs +117 -0
  49. package/src/catalog/design.mjs +159 -0
  50. package/src/catalog/forms.mjs +25 -0
  51. package/src/catalog/hydration.mjs +29 -0
  52. package/src/catalog/icons.mjs +24 -0
  53. package/src/catalog/media.mjs +163 -0
  54. package/src/catalog/metadata.mjs +116 -0
  55. package/src/catalog/plugins.mjs +228 -0
  56. package/src/catalog/realtime.mjs +195 -0
  57. package/src/catalog/rust.mjs +75 -0
  58. package/src/catalog/server-functions.mjs +92 -0
  59. package/src/catalog/state.mjs +39 -0
  60. package/src/catalog/style-groups.mjs +54 -0
  61. package/src/catalog/tests.mjs +127 -0
  62. package/src/catalog/text.mjs +200 -0
  63. package/src/catalog/ui-state.mjs +42 -0
  64. package/src/catalog.mjs +67 -460
  65. package/src/check/runtime.mjs +280 -0
  66. package/src/check.mjs +198 -208
  67. package/src/cli/authoring.mjs +213 -0
  68. package/src/cli/project.mjs +302 -0
  69. package/src/cli.mjs +222 -250
  70. package/src/config.mjs +1 -1
  71. package/src/contributions.mjs +75 -25
  72. package/src/db-layer.mjs +13 -5
  73. package/src/deploy/cdn.mjs +94 -16
  74. package/src/deploy/files.mjs +100 -21
  75. package/src/deploy/fit.mjs +127 -23
  76. package/src/deploy/hosts.mjs +90 -20
  77. package/src/deploy/index/cli.mjs +20 -0
  78. package/src/deploy/index/environment.mjs +69 -0
  79. package/src/deploy/index/steps.mjs +87 -0
  80. package/src/deploy/index.mjs +228 -114
  81. package/src/deploy/needs.mjs +88 -31
  82. package/src/design-checks/accessible-names.mjs +62 -0
  83. package/src/design-checks/contrast.mjs +43 -0
  84. package/src/design-checks/csp.mjs +60 -0
  85. package/src/design-checks/files.mjs +59 -0
  86. package/src/design-checks/heading-links.mjs +61 -0
  87. package/src/design-checks/interaction.mjs +65 -0
  88. package/src/design-checks/jsx.mjs +67 -0
  89. package/src/design-checks/layout-values.mjs +68 -0
  90. package/src/design-checks/native-controls.mjs +48 -0
  91. package/src/design-checks/outline.mjs +39 -0
  92. package/src/design-checks/reporter.mjs +37 -0
  93. package/src/design-checks/rules.mjs +274 -0
  94. package/src/design-checks/values.mjs +262 -0
  95. package/src/design-checks.mjs +10 -400
  96. package/src/diag.mjs +32 -8
  97. package/src/docs.mjs +138 -43
  98. package/src/doctor.mjs +20 -8
  99. package/src/files.mjs +18 -5
  100. package/src/fix.mjs +10 -2
  101. package/src/gate/after.mjs +40 -0
  102. package/src/gate/before.mjs +54 -0
  103. package/src/gate/classify.mjs +72 -0
  104. package/src/gate/mutation.mjs +209 -0
  105. package/src/gate/resolve-cases.mjs +22 -0
  106. package/src/gate/result.mjs +63 -0
  107. package/src/gate/run.mjs +75 -0
  108. package/src/gate.mjs +104 -202
  109. package/src/infer/box-props.cjs +140 -0
  110. package/src/infer/core-boxes.cjs +46 -0
  111. package/src/infer/derived-writes.cjs +95 -0
  112. package/src/infer/engine/checks.cjs +193 -0
  113. package/src/infer/engine/flow.cjs +293 -0
  114. package/src/infer/engine/report.cjs +158 -0
  115. package/src/infer/engine/utils.cjs +15 -0
  116. package/src/infer/engine.cjs +108 -368
  117. package/src/infer/facts/sites.cjs +114 -0
  118. package/src/infer/facts.cjs +210 -117
  119. package/src/infer/hosts.cjs +69 -22
  120. package/src/infer/lsp.mjs +149 -40
  121. package/src/infer/session.cjs +73 -17
  122. package/src/infer/shadow.cjs +13 -5
  123. package/src/infer/worker.cjs +57 -9
  124. package/src/jsx-scan.mjs +63 -21
  125. package/src/load-config/schema.mjs +281 -0
  126. package/src/load-config.mjs +137 -138
  127. package/src/map.mjs +93 -26
  128. package/src/mcp/browser-tools.mjs +155 -0
  129. package/src/mcp.mjs +147 -69
  130. package/src/meta-table.mjs +43 -8
  131. package/src/mutate.mjs +120 -36
  132. package/src/plugin-checks.mjs +52 -23
  133. package/src/plugins.mjs +125 -39
  134. package/src/project.mjs +170 -79
  135. package/src/props-checks.mjs +24 -6
  136. package/src/props-listener.mjs +27 -9
  137. package/src/react-checks.mjs +35 -4
  138. package/src/server-state-checks.mjs +34 -0
  139. package/src/share-checks.mjs +121 -37
  140. package/src/site.mjs +33 -6
  141. package/src/slots.mjs +104 -19
  142. package/src/start/build.mjs +116 -0
  143. package/src/start/routes.mjs +38 -0
  144. package/src/start.mjs +193 -95
  145. package/src/state-checks/core-state.mjs +223 -0
  146. package/src/state-checks/modules.mjs +17 -0
  147. package/src/state-checks/scan.mjs +232 -0
  148. package/src/state-checks/server-graph.mjs +65 -0
  149. package/src/state-checks.mjs +109 -133
  150. package/src/style/find.mjs +121 -0
  151. package/src/style.mjs +133 -127
  152. package/src/surface-checks.mjs +212 -46
  153. package/src/tailwind.mjs +5 -4
  154. package/src/tasks.mjs +189 -27
  155. package/src/test-rules/context.mjs +109 -0
  156. package/src/test-rules/file-rules.mjs +279 -0
  157. package/src/test-rules/rule-helpers.mjs +244 -0
  158. package/src/test-rules/syntax.mjs +223 -0
  159. package/src/test-rules/test-rules.mjs +127 -0
  160. package/src/testdiff.mjs +94 -32
  161. package/src/tests.mjs +31 -483
  162. package/src/text-checks.mjs +229 -69
  163. package/src/tokens.mjs +12 -4
  164. package/src/trust/scan.mjs +95 -0
  165. package/src/trust.mjs +214 -102
  166. package/src/weft.mjs +41 -9
  167. package/src/workspace.mjs +96 -19
  168. package/ts-plugin/context.js +57 -0
  169. package/ts-plugin/index.js +4 -124
  170. package/ts-plugin/mapping.js +65 -0
  171. package/ts-plugin/proxy.js +144 -0
package/src/mutate.mjs CHANGED
@@ -8,12 +8,22 @@ import { parse } from './tests.mjs'
8
8
 
9
9
  const K = ts.SyntaxKind
10
10
  const swaps = new Map([
11
- [K.EqualsEqualsEqualsToken, ['!==']], [K.ExclamationEqualsEqualsToken, ['===']],
12
- [K.EqualsEqualsToken, ['!=']], [K.ExclamationEqualsToken, ['==']],
13
- [K.LessThanToken, ['<=', '>=']], [K.LessThanEqualsToken, ['<', '>']],
14
- [K.GreaterThanToken, ['>=', '<=']], [K.GreaterThanEqualsToken, ['>', '<']],
15
- [K.AmpersandAmpersandToken, ['||']], [K.BarBarToken, ['&&']], [K.QuestionQuestionToken, ['&&']],
16
- [K.PlusToken, ['-']], [K.MinusToken, ['+']], [K.AsteriskToken, ['/']], [K.SlashToken, ['*']], [K.PercentToken, ['*']],
11
+ [K.EqualsEqualsEqualsToken, ['!==']],
12
+ [K.ExclamationEqualsEqualsToken, ['===']],
13
+ [K.EqualsEqualsToken, ['!=']],
14
+ [K.ExclamationEqualsToken, ['==']],
15
+ [K.LessThanToken, ['<=', '>=']],
16
+ [K.LessThanEqualsToken, ['<', '>']],
17
+ [K.GreaterThanToken, ['>=', '<=']],
18
+ [K.GreaterThanEqualsToken, ['>', '<']],
19
+ [K.AmpersandAmpersandToken, ['||']],
20
+ [K.BarBarToken, ['&&']],
21
+ [K.QuestionQuestionToken, ['&&']],
22
+ [K.PlusToken, ['-']],
23
+ [K.MinusToken, ['+']],
24
+ [K.AsteriskToken, ['/']],
25
+ [K.SlashToken, ['*']],
26
+ [K.PercentToken, ['*']],
17
27
  ])
18
28
 
19
29
  /** Lines (1-based) of `after` that are new or changed since `before` (null: every line). LCS over lines. */
@@ -24,17 +34,30 @@ export function changedLines(before, after) {
24
34
  // Trim the common head and tail; the middle is usually small.
25
35
  let start = 0
26
36
  while (start < a.length && start < b.length && a[start] === b[start]) start++
27
- let ea = a.length, eb = b.length
28
- while (ea > start && eb > start && a[ea - 1] === b[eb - 1]) { ea--; eb-- }
29
- const A = a.slice(start, ea), B = b.slice(start, eb)
37
+ let ea = a.length,
38
+ eb = b.length
39
+ while (ea > start && eb > start && a[ea - 1] === b[eb - 1]) {
40
+ ea--
41
+ eb--
42
+ }
43
+ const A = a.slice(start, ea),
44
+ B = b.slice(start, eb)
30
45
  const out = new Set()
31
- if (A.length * B.length > 4e6) { for (let i = start; i < eb; i++) out.add(i + 1); return out }
46
+ if (A.length * B.length > 4e6) {
47
+ for (let i = start; i < eb; i++) out.add(i + 1)
48
+ return out
49
+ }
32
50
  const dp = Array.from({ length: A.length + 1 }, () => new Uint32Array(B.length + 1))
33
- for (let i = A.length - 1; i >= 0; i--) for (let j = B.length - 1; j >= 0; j--) dp[i][j] = A[i] === B[j] ? dp[i + 1][j + 1] + 1 : Math.max(dp[i + 1][j], dp[i][j + 1])
34
- let i = 0, j = 0
51
+ for (let i = A.length - 1; i >= 0; i--)
52
+ for (let j = B.length - 1; j >= 0; j--)
53
+ dp[i][j] = A[i] === B[j] ? dp[i + 1][j + 1] + 1 : Math.max(dp[i + 1][j], dp[i][j + 1])
54
+ let i = 0,
55
+ j = 0
35
56
  while (j < B.length) {
36
- if (i < A.length && A[i] === B[j]) { i++; j++ }
37
- else if (i < A.length && dp[i + 1][j] >= dp[i][j + 1]) i++
57
+ if (i < A.length && A[i] === B[j]) {
58
+ i++
59
+ j++
60
+ } else if (i < A.length && dp[i + 1][j] >= dp[i][j + 1]) i++
38
61
  else out.add(start + ++j)
39
62
  }
40
63
  return out
@@ -53,10 +76,26 @@ export function mutants(text, rel, lines) {
53
76
  if (!lines.has(line)) return
54
77
  const original = text.slice(start, end)
55
78
  if (original === replacement) return
56
- out.push({ id: `${rel}:${line}:${column}:${op}:${out.length}`, file: rel, line, column, op, start, end, original, replacement })
79
+ out.push({
80
+ id: `${rel}:${line}:${column}:${op}:${out.length}`,
81
+ file: rel,
82
+ line,
83
+ column,
84
+ op,
85
+ start,
86
+ end,
87
+ original,
88
+ replacement,
89
+ })
57
90
  }
58
- const skip = n => ts.isImportDeclaration(n) || ts.isExportDeclaration(n) || ts.isTypeNode(n) || ts.isInterfaceDeclaration(n) || ts.isTypeAliasDeclaration(n) || ts.isEnumDeclaration(n)
59
- const visit = n => {
91
+ const skip = (n) =>
92
+ ts.isImportDeclaration(n) ||
93
+ ts.isExportDeclaration(n) ||
94
+ ts.isTypeNode(n) ||
95
+ ts.isInterfaceDeclaration(n) ||
96
+ ts.isTypeAliasDeclaration(n) ||
97
+ ts.isEnumDeclaration(n)
98
+ const visit = (n) => {
60
99
  if (skip(n)) return
61
100
  if (ts.isBinaryExpression(n)) {
62
101
  for (const r of swaps.get(n.operatorToken.kind) ?? []) add(n.operatorToken, 'operator', r)
@@ -67,7 +106,11 @@ export function mutants(text, rel, lines) {
67
106
  } else if (ts.isNumericLiteral(n)) {
68
107
  const v = Number(n.text)
69
108
  if (Number.isFinite(v)) add(n, 'number', String(v === 0 ? 1 : v + 1))
70
- } else if ((ts.isStringLiteral(n) || ts.isNoSubstitutionTemplateLiteral(n)) && n.text && !isKey(n)) {
109
+ } else if (
110
+ (ts.isStringLiteral(n) || ts.isNoSubstitutionTemplateLiteral(n)) &&
111
+ n.text &&
112
+ !isKey(n)
113
+ ) {
71
114
  add(n, 'string', '""')
72
115
  } else if (ts.isConditionalExpression(n)) {
73
116
  add(n.condition, 'condition', 'true')
@@ -75,12 +118,22 @@ export function mutants(text, rel, lines) {
75
118
  } else if (ts.isIfStatement(n)) {
76
119
  add(n.expression, 'condition', 'true')
77
120
  add(n.expression, 'condition', 'false')
78
- } else if (ts.isExpressionStatement(n) && ts.isCallExpression(unparen(n.expression)) && !isDirective(n)) {
121
+ } else if (
122
+ ts.isExpressionStatement(n) &&
123
+ ts.isCallExpression(unparen(n.expression)) &&
124
+ !isDirective(n)
125
+ ) {
79
126
  add(n, 'call', ';')
80
127
  } else if (ts.isJsxText(n) && n.text.trim()) {
81
128
  const start = n.pos + n.text.indexOf(n.text.trim()) // JSX text: `text` starts at `pos`, before any trivia
82
129
  add(n, 'jsx-text', '', start, start + n.text.trim().length)
83
- } else if (ts.isReturnStatement(n) && n.expression && isFn(n.parent?.parent) && !ts.isJsxElement(unparen(n.expression)) && !ts.isJsxFragment(unparen(n.expression))) {
130
+ } else if (
131
+ ts.isReturnStatement(n) &&
132
+ n.expression &&
133
+ isFn(n.parent?.parent) &&
134
+ !ts.isJsxElement(unparen(n.expression)) &&
135
+ !ts.isJsxFragment(unparen(n.expression))
136
+ ) {
84
137
  add(n.expression, 'return', 'undefined')
85
138
  }
86
139
  ts.forEachChild(n, visit)
@@ -89,14 +142,29 @@ export function mutants(text, rel, lines) {
89
142
  return out
90
143
  }
91
144
 
92
- const unparen = n => { while (ts.isParenthesizedExpression(n)) n = n.expression; return n }
93
- const isFn = n => n && (ts.isFunctionDeclaration(n) || ts.isFunctionExpression(n) || ts.isArrowFunction(n) || ts.isMethodDeclaration(n))
94
- const isDirective = n => ts.isStringLiteral(n.expression)
145
+ const unparen = (n) => {
146
+ while (ts.isParenthesizedExpression(n)) n = n.expression
147
+ return n
148
+ }
149
+ const isFn = (n) =>
150
+ n &&
151
+ (ts.isFunctionDeclaration(n) ||
152
+ ts.isFunctionExpression(n) ||
153
+ ts.isArrowFunction(n) ||
154
+ ts.isMethodDeclaration(n))
155
+ const isDirective = (n) => ts.isStringLiteral(n.expression)
95
156
  /** Strings that name things rather than being data: property keys, element-access keys, `import()` specifiers. */
96
- const isKey = n => {
157
+ const isKey = (n) => {
97
158
  const p = n.parent
98
- return (ts.isPropertyAssignment(p) && p.name === n) || ts.isElementAccessExpression(p) || ts.isLiteralTypeNode(p) || ts.isExternalModuleReference(p) ||
99
- (ts.isCallExpression(p) && p.expression.kind === K.ImportKeyword) || ts.isJsxAttribute(p) && /^(key|class|className|style|id|type|role|for|htmlFor|name)$/.test(p.name.getText())
159
+ return (
160
+ (ts.isPropertyAssignment(p) && p.name === n) ||
161
+ ts.isElementAccessExpression(p) ||
162
+ ts.isLiteralTypeNode(p) ||
163
+ ts.isExternalModuleReference(p) ||
164
+ (ts.isCallExpression(p) && p.expression.kind === K.ImportKeyword) ||
165
+ (ts.isJsxAttribute(p) &&
166
+ /^(key|class|className|style|id|type|role|for|htmlFor|name)$/.test(p.name.getText()))
167
+ )
100
168
  }
101
169
 
102
170
  export const apply = (text, m) => text.slice(0, m.start) + m.replacement + text.slice(m.end)
@@ -114,7 +182,12 @@ export function sample(list, max) {
114
182
  const out = []
115
183
  for (let round = 0; out.length < max; round++) {
116
184
  let any = false
117
- for (const q of queues) if (q[round]) { out.push(q[round]); any = true; if (out.length >= max) break }
185
+ for (const q of queues)
186
+ if (q[round]) {
187
+ out.push(q[round])
188
+ any = true
189
+ if (out.length >= max) break
190
+ }
118
191
  if (!any) break
119
192
  }
120
193
  return out
@@ -125,16 +198,27 @@ export function suggest(m) {
125
198
  const at = `${m.file}:${m.line}`
126
199
  switch (m.op) {
127
200
  case 'operator':
128
- if (/^[<>]=?$/.test(m.original)) return `${at}: \`${m.original}\` → \`${m.replacement}\` survives. Assert the boundary: the value exactly at the limit.`
129
- if (/^(&&|\|\||\?\?)$/.test(m.original)) return `${at}: \`${m.original}\` → \`${m.replacement}\` survives. Assert a case where only one side of the condition holds.`
130
- if (/^[!=]=/.test(m.original)) return `${at}: \`${m.original}\` → \`${m.replacement}\` survives. Assert both the matching and the non-matching case.`
201
+ if (/^[<>]=?$/.test(m.original))
202
+ return `${at}: \`${m.original}\` → \`${m.replacement}\` survives. Assert the boundary: the value exactly at the limit.`
203
+ if (/^(&&|\|\||\?\?)$/.test(m.original))
204
+ return `${at}: \`${m.original}\` → \`${m.replacement}\` survives. Assert a case where only one side of the condition holds.`
205
+ if (/^[!=]=/.test(m.original))
206
+ return `${at}: \`${m.original}\` → \`${m.replacement}\` survives. Assert both the matching and the non-matching case.`
131
207
  return `${at}: \`${m.original}\` → \`${m.replacement}\` survives. Assert the computed value, not only that something rendered.`
132
- case 'boolean': case 'condition': case 'negation':
208
+ case 'boolean':
209
+ case 'condition':
210
+ case 'negation':
133
211
  return `${at}: \`${m.original}\` → \`${m.replacement}\` survives. Assert what the user sees on the branch this condition guards.`
134
- case 'number': return `${at}: ${m.original} → ${m.replacement} survives. Assert the exact value this number produces.`
135
- case 'string': case 'jsx-text': return `${at}: the text ${JSON.stringify(m.original.slice(0, 40))} can be blanked. Assert the text the user sees.`
136
- case 'call': return `${at}: removing \`${m.original.slice(0, 60)}\` survives. Assert the effect of that call.`
137
- case 'return': return `${at}: returning undefined survives. Assert what this function's result shows.`
138
- default: return `${at}: ${m.op} survives`
212
+ case 'number':
213
+ return `${at}: ${m.original} → ${m.replacement} survives. Assert the exact value this number produces.`
214
+ case 'string':
215
+ case 'jsx-text':
216
+ return `${at}: the text ${JSON.stringify(m.original.slice(0, 40))} can be blanked. Assert the text the user sees.`
217
+ case 'call':
218
+ return `${at}: removing \`${m.original.slice(0, 60)}\` survives. Assert the effect of that call.`
219
+ case 'return':
220
+ return `${at}: returning undefined survives. Assert what this function's result shows.`
221
+ default:
222
+ return `${at}: ${m.op} survives`
139
223
  }
140
224
  }
@@ -16,13 +16,14 @@ import { importOf, scanJsx } from './jsx-scan.mjs'
16
16
  */
17
17
  export async function pluginChecks(root, loaded, { files, exclude = [] }) {
18
18
  const out = []
19
- const texts = new Map(files.map(f => [f.rel, f.text]))
20
- const read = rel => {
21
- if (!texts.has(rel)) texts.set(rel, existsSync(join(root, rel)) ? readFileSync(join(root, rel), 'utf8') : '')
19
+ const texts = new Map(files.map((f) => [f.rel, f.text]))
20
+ const read = (rel) => {
21
+ if (!texts.has(rel))
22
+ texts.set(rel, existsSync(join(root, rel)) ? readFileSync(join(root, rel), 'utf8') : '')
22
23
  return texts.get(rel)
23
24
  }
24
25
  const scans = new Map()
25
- const jsx = rel => {
26
+ const jsx = (rel) => {
26
27
  if (!scans.has(rel)) {
27
28
  try {
28
29
  scans.set(rel, scanJsx(read(rel), rel))
@@ -39,9 +40,12 @@ export async function pluginChecks(root, loaded, { files, exclude = [] }) {
39
40
  const include = hook.include ?? ['**/*.{tsx,jsx}']
40
41
  const wanted = matcher(include)
41
42
  /** @type {Set<string>} */
42
- const chosen = new Set([...files.map(f => f.rel).filter(wanted), ...discover(root, { include, exclude })])
43
+ const chosen = new Set([
44
+ ...files.map((f) => f.rel).filter(wanted),
45
+ ...discover(root, { include, exclude }),
46
+ ])
43
47
  const rels = [...chosen].sort()
44
- const declared = new Map((c.manifest.diagnostics ?? []).map(d => [d.code, d]))
48
+ const declared = new Map((c.manifest.diagnostics ?? []).map((d) => [d.code, d]))
45
49
  const ctx = Object.freeze({
46
50
  root,
47
51
  files: rels,
@@ -49,26 +53,47 @@ export async function pluginChecks(root, loaded, { files, exclude = [] }) {
49
53
  jsx,
50
54
  importOf,
51
55
  slots: loaded.slots ?? {},
52
- exists: rel => existsSync(join(root, rel)),
53
- bytes: rel => (existsSync(join(root, rel)) ? new Uint8Array(readFileSync(join(root, rel))) : undefined),
56
+ exists: (rel) => existsSync(join(root, rel)),
57
+ bytes: (rel) =>
58
+ existsSync(join(root, rel)) ? new Uint8Array(readFileSync(join(root, rel))) : undefined,
54
59
  })
55
60
  let found
56
61
  try {
57
62
  found = (await hook.run(ctx)) ?? []
58
63
  } catch (error) {
59
- out.push(diagnostic({ code: 'P119', owner: c.name, file: 'weft.config.ts', why: `\`${c.name}\`'s check hook threw: ${String(error?.message ?? error).split('\n')[0]}` }))
64
+ out.push(
65
+ diagnostic({
66
+ code: 'P119',
67
+ owner: c.name,
68
+ file: 'weft.config.ts',
69
+ why: `\`${c.name}\`'s check hook threw: ${String(error?.message ?? error).split('\n')[0]}`,
70
+ }),
71
+ )
60
72
  continue
61
73
  }
62
- for (const d of found) {
63
- if (!d.code.startsWith(c.namespace)) {
64
- out.push(diagnostic({ code: 'P111', file: 'weft.config.ts', why: `\`${c.name}\`'s check reported \`${d.code}\`, outside its namespace \`${c.namespace}\`.` }))
65
- continue
66
- }
67
- const text = read(d.file)
68
- const pos = positions(text)
69
- const [start, end] = [d.start ?? 0, d.end ?? d.start ?? 0]
70
- const meta = declared.get(d.code)
71
- out.push(diagnostic({
74
+ appendFindings({ found, c, out, declared, read })
75
+ }
76
+ return out
77
+ }
78
+
79
+ function appendFindings({ found, c, out, declared, read }) {
80
+ for (const d of found) {
81
+ if (!d.code.startsWith(c.namespace)) {
82
+ out.push(
83
+ diagnostic({
84
+ code: 'P111',
85
+ file: 'weft.config.ts',
86
+ why: `\`${c.name}\`'s check reported \`${d.code}\`, outside its namespace \`${c.namespace}\`.`,
87
+ }),
88
+ )
89
+ continue
90
+ }
91
+ const text = read(d.file)
92
+ const pos = positions(text)
93
+ const [start, end] = [d.start ?? 0, d.end ?? d.start ?? 0]
94
+ const meta = declared.get(d.code)
95
+ out.push(
96
+ diagnostic({
72
97
  code: d.code,
73
98
  owner: c.name,
74
99
  severity: d.severity ?? meta?.severity ?? 'error',
@@ -80,10 +105,14 @@ export async function pluginChecks(root, loaded, { files, exclude = [] }) {
80
105
  fix: d.fix && {
81
106
  description: d.fix.description,
82
107
  safe: d.fix.safe === true,
83
- edits: d.fix.edits.map(e => ({ file: e.file ?? d.file, span: positions(read(e.file ?? d.file)).span(e.start, e.end), offset: [e.start, e.end], text: e.text })),
108
+ edits: d.fix.edits.map((e) => ({
109
+ file: e.file ?? d.file,
110
+ span: positions(read(e.file ?? d.file)).span(e.start, e.end),
111
+ offset: [e.start, e.end],
112
+ text: e.text,
113
+ })),
84
114
  },
85
- }))
86
- }
115
+ }),
116
+ )
87
117
  }
88
- return out
89
118
  }
package/src/plugins.mjs CHANGED
@@ -7,15 +7,16 @@ import { loadConfig } from './load-config.mjs'
7
7
  import { slotDef } from './slots.mjs'
8
8
 
9
9
  const byName = (a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0)
10
- const header = '// Generated from weft.config.ts. Do not edit: change weft.config.ts and run `weft plugins sync`.'
10
+ const header =
11
+ '// Generated from weft.config.ts. Do not edit: change weft.config.ts and run `weft plugins sync`.'
11
12
 
12
- const posix = p => p.split('\\').join('/')
13
- const slotFile = id => `.weft/slots/${id}.ts`
13
+ const posix = (p) => p.split('\\').join('/')
14
+ const slotFile = (id) => `.weft/slots/${id}.ts`
14
15
 
15
16
  /** The ProviderRef a bound plugin declared for `slot` (and `key`). */
16
17
  function providerRef(loaded, plugin, slot, key = '') {
17
- const c = loaded.contributions.find(x => x.name === plugin)
18
- return c?.manifest.provides?.find(p => p.slot.id === slot && (p.key ?? '') === key) ?? null
18
+ const c = loaded.contributions.find((x) => x.name === plugin)
19
+ return c?.manifest.provides?.find((p) => p.slot.id === slot && (p.key ?? '') === key) ?? null
19
20
  }
20
21
 
21
22
  /** `module` relative to `.weft/slots/` when the plugin gave a relative path from the project root. */
@@ -28,28 +29,55 @@ function specifier(root, module) {
28
29
 
29
30
  function loader(root, ref) {
30
31
  const side = ref.sides.client ?? ref.sides[Object.keys(ref.sides).sort()[0]]
31
- if (!side) return { comment: 'no sides', code: `() => Promise.reject(new Error('provider declares no module'))` }
32
- const call = side.options === undefined ? `m[${JSON.stringify(side.export)}]` : `m[${JSON.stringify(side.export)}](${JSON.stringify(side.options)})`
33
- return { comment: Object.keys(ref.sides).sort().join(', '), code: `() => import(${JSON.stringify(specifier(root, side.module))}).then(m => ${call})` }
32
+ if (!side)
33
+ return {
34
+ comment: 'no sides',
35
+ code: `() => Promise.reject(new Error('provider declares no module'))`,
36
+ }
37
+ const call =
38
+ side.options === undefined
39
+ ? `m[${JSON.stringify(side.export)}]`
40
+ : `m[${JSON.stringify(side.export)}](${JSON.stringify(side.options)})`
41
+ return {
42
+ comment: Object.keys(ref.sides).sort().join(', '),
43
+ code: `() => import(${JSON.stringify(specifier(root, side.module))}).then(m => ${call})`,
44
+ }
34
45
  }
35
46
 
36
- const unbound = id =>
47
+ const unbound = (id) =>
37
48
  `export const provider = null\nexport const load = (): never => {\n throw Object.assign(new Error(${JSON.stringify(`P101 ${id}: nothing provides this slot. Add a plugin that provides it to \`plugins\` in weft.config.ts.`)}), { code: 'P101' })\n}\n`
38
49
  const builtin = (id, name) =>
39
50
  `export const provider = ${JSON.stringify(name)}\nexport const load = (): never => {\n throw new Error(${JSON.stringify(`${id}: built in, provided by weft (${name}).`)})\n}\n`
40
51
 
52
+ function addCssEntries(files, c) {
53
+ const t = c.manifest.tokens
54
+ if (t?.cssEntries === undefined) return
55
+ if (!t.cssEntries || typeof t.cssEntries !== 'object' || Array.isArray(t.cssEntries))
56
+ throw new TypeError('tokens.cssEntries must be an object')
57
+ for (const [name, css] of Object.entries(t.cssEntries)) {
58
+ if (!/^[a-z][a-z0-9-]*$/.test(name) || typeof css !== 'string')
59
+ throw new TypeError(`Invalid tokens.cssEntries entry: ${name}`)
60
+ files[`.weft/tokens/${t.namespace}.${name}.css`] =
61
+ `/* Generated from weft.config.ts (${c.name}). Do not edit. */\n${css}`
62
+ }
63
+ }
64
+
41
65
  /** File contents for every slot module, the slots.d.ts and token CSS, keyed by path relative to the root. */
42
66
  export function generated(root, loaded) {
43
67
  const files = {}
44
68
  const typed = []
45
69
  for (const [id, binding] of Object.entries(loaded.slots).sort(([a], [b]) => (a < b ? -1 : 1))) {
46
- const def = slotDef(id) ?? { version: 1, cardinality: typeof binding === 'object' && binding ? 'keyed' : 'one' }
70
+ const def = slotDef(id) ?? {
71
+ version: 1,
72
+ cardinality: typeof binding === 'object' && binding ? 'keyed' : 'one',
73
+ }
47
74
  let body
48
75
  if (def.cardinality === 'one') {
49
- const ref = binding && !binding.startsWith('weft:default') ? providerRef(loaded, binding, id) : null
76
+ const ref =
77
+ binding && !binding.startsWith('weft:default') ? providerRef(loaded, binding, id) : null
50
78
  if (ref) {
51
79
  const l = loader(root, ref)
52
- const plugin = loaded.contributions.find(x => x.name === binding)
80
+ const plugin = loaded.contributions.find((x) => x.name === binding)
53
81
  body = `// provider: ${binding}@${plugin.version} · implements ${id}@${ref.versions.join(',')} · sides: ${l.comment}\nexport const provider = ${JSON.stringify({ plugin: binding, version: plugin.version, interface: Math.max(...ref.versions) })} as const\nexport const load = ${l.code}\n`
54
82
  typed.push(id)
55
83
  } else if (binding?.startsWith('weft:default')) body = builtin(id, binding)
@@ -58,8 +86,9 @@ export function generated(root, loaded) {
58
86
  const lines = []
59
87
  const providers = {}
60
88
  for (const [key, name] of Object.entries(binding ?? {})) {
61
- const ref = name && !name.startsWith('weft:default') ? providerRef(loaded, name, id, key) : null
62
- providers[key] = ref ? name : name ?? null
89
+ const ref =
90
+ name && !name.startsWith('weft:default') ? providerRef(loaded, name, id, key) : null
91
+ providers[key] = ref ? name : (name ?? null)
63
92
  if (ref) lines.push(` ${JSON.stringify(key)}: ${loader(root, ref).code},`)
64
93
  }
65
94
  body = `export const providers = ${JSON.stringify(providers)} as const\nconst loaders: Record<string, () => Promise<unknown>> = {\n${lines.join('\n')}${lines.length ? '\n' : ''}}\nexport const load = (key: string): Promise<unknown> => {\n const l = loaders[key]\n if (l) return l()\n throw Object.assign(new Error(\`P101 ${id} key \${key}: \${providers[key as keyof typeof providers] ? 'built in, provided by weft' : 'nothing provides it'}.\`), { code: 'P101' })\n}\n`
@@ -72,26 +101,45 @@ export function generated(root, loaded) {
72
101
  if (binding && typeof binding === 'object') {
73
102
  const keys = {}
74
103
  for (const [key, name] of Object.entries(binding)) {
75
- const side = name && !name.startsWith('weft:default') ? providerRef(loaded, name, id, key)?.sides.build : null
76
- if (side) keys[key] = { plugin: name, module: side.module, export: side.export, options: side.options }
104
+ const side =
105
+ name && !name.startsWith('weft:default')
106
+ ? providerRef(loaded, name, id, key)?.sides.build
107
+ : null
108
+ if (side)
109
+ keys[key] = {
110
+ plugin: name,
111
+ module: side.module,
112
+ export: side.export,
113
+ options: side.options,
114
+ }
77
115
  }
78
- if (Object.keys(keys).length) files[`.weft/slots/${id}.build.json`] = `${JSON.stringify({ keys }, null, 2)}\n`
116
+ if (Object.keys(keys).length)
117
+ files[`.weft/slots/${id}.build.json`] = `${JSON.stringify({ keys }, null, 2)}\n`
79
118
  continue
80
119
  }
81
120
  if (typeof binding !== 'string' || binding.startsWith('weft:default')) continue
82
121
  const side = providerRef(loaded, binding, id)?.sides.build
83
122
  if (!side) continue
84
- const plugin = loaded.contributions.find(x => x.name === binding)
123
+ const plugin = loaded.contributions.find((x) => x.name === binding)
85
124
  const module = side.module.startsWith('.') ? posix(resolve(root, side.module)) : side.module
86
- files[`.weft/slots/${id}.build.json`] = `${JSON.stringify({ plugin: binding, version: plugin.version, module, export: side.export, ...(side.options === undefined ? {} : { options: side.options }) }, null, 2)}\n`
125
+ files[`.weft/slots/${id}.build.json`] =
126
+ `${JSON.stringify({ plugin: binding, version: plugin.version, module, export: side.export, ...(side.options === undefined ? {} : { options: side.options }) }, null, 2)}\n`
87
127
  }
88
- files['.weft/slots.d.ts'] = `${header}\n// Only bound slots are declared: importing an unbound one is a type error (P101).\n${typed
89
- .map(id => `declare module 'weft:slot/${id}' {\n export const provider: unknown\n export const providers: Record<string, unknown>\n export const load: (key?: string) => Promise<any>\n}\n`)
90
- .join('')}`
128
+ files['.weft/slots.d.ts'] =
129
+ `${header}\n// Only bound slots are declared: importing an unbound one is a type error (P101).\n${typed
130
+ .map(
131
+ (id) =>
132
+ `declare module 'weft:slot/${id}' {\n export const provider: unknown\n export const providers: Record<string, unknown>\n export const load: (key?: string) => Promise<any>\n}\n`,
133
+ )
134
+ .join('')}`
91
135
  for (const c of loaded.contributions) {
92
136
  const t = c.manifest.tokens
93
- if (t?.css) files[`.weft/tokens/${t.namespace}.css`] = `/* Generated from weft.config.ts (${c.name}). Do not edit. */\n${t.css}`
94
- if (t?.alias) files[`.weft/tokens/${t.namespace}.alias.json`] = `${JSON.stringify(t.alias, null, 2)}\n`
137
+ if (t?.css)
138
+ files[`.weft/tokens/${t.namespace}.css`] =
139
+ `/* Generated from weft.config.ts (${c.name}). Do not edit. */\n${t.css}`
140
+ addCssEntries(files, c)
141
+ if (t?.alias)
142
+ files[`.weft/tokens/${t.namespace}.alias.json`] = `${JSON.stringify(t.alias, null, 2)}\n`
95
143
  }
96
144
  return files
97
145
  }
@@ -120,14 +168,21 @@ export function sync(root, loaded) {
120
168
  export async function list(root) {
121
169
  root = resolve(root)
122
170
  const loaded = await loadConfig(root)
123
- const unique = (loaded.config.plugins ?? []).filter((p, i, all) => all.findIndex(q => q.name === p.name) === i)
124
- const plugins = [...unique].sort(byName).map(p => {
125
- const c = loaded.contributions.find(x => x.name === p.name)
126
- const provides = (p.provides ?? []).map(item => {
171
+ const unique = (loaded.config.plugins ?? []).filter(
172
+ (p, i, all) => all.findIndex((q) => q.name === p.name) === i,
173
+ )
174
+ const plugins = [...unique].sort(byName).map((p) => {
175
+ const c = loaded.contributions.find((x) => x.name === p.name)
176
+ const provides = (p.provides ?? []).map((item) => {
127
177
  const { slot, key } = typeof item === 'string' ? { slot: item } : item
128
178
  const binding = loaded.slots[slot]
129
179
  const bound = key ? binding?.[key] === p.name : binding === p.name
130
- return { slot, ...(key ? { key } : {}), interface: providerRef(loaded, p.name, slot, key)?.versions ?? null, bound }
180
+ return {
181
+ slot,
182
+ ...(key ? { key } : {}),
183
+ interface: providerRef(loaded, p.name, slot, key)?.versions ?? null,
184
+ bound,
185
+ }
131
186
  })
132
187
  return {
133
188
  name: p.name,
@@ -135,38 +190,69 @@ export async function list(root) {
135
190
  provides,
136
191
  permissions: c?.permissions ?? {},
137
192
  namespace: c?.namespace ?? null,
138
- diagnostics: (c?.manifest.diagnostics ?? []).map(d => d.code),
193
+ diagnostics: (c?.manifest.diagnostics ?? []).map((d) => d.code),
139
194
  tokens: c?.manifest.tokens?.namespace ?? null,
140
- catalog: (c?.manifest.catalog ?? []).map(e => e.name),
195
+ catalog: (c?.manifest.catalog ?? []).map((e) => e.name),
141
196
  agents: c?.manifest.agents?.title ?? null,
142
197
  }
143
198
  })
144
- return { ok: !loaded.diagnostics.some(d => d.severity === 'error'), plugins, slots: loaded.slots, diagnostics: loaded.diagnostics }
199
+ return {
200
+ ok: !loaded.diagnostics.some((d) => d.severity === 'error'),
201
+ plugins,
202
+ slots: loaded.slots,
203
+ diagnostics: loaded.diagnostics,
204
+ }
145
205
  }
146
206
 
147
207
  /** `weft plugins explain <slot> --json`: config entry → plugin → module → generated file. */
148
208
  export async function explain(root, id) {
149
209
  root = resolve(root)
150
210
  const loaded = await loadConfig(root)
151
- if (!id || !(id in loaded.slots)) throw Object.assign(new Error(`unknown slot ${id ?? '(none)'}; known: ${Object.keys(loaded.slots).join(', ')}`), { code: 'E_USAGE' })
211
+ if (!id || !(id in loaded.slots))
212
+ throw Object.assign(
213
+ new Error(`unknown slot ${id ?? '(none)'}; known: ${Object.keys(loaded.slots).join(', ')}`),
214
+ { code: 'E_USAGE' },
215
+ )
152
216
  const binding = loaded.slots[id]
153
217
  const def = slotDef(id) ?? null
154
- const chain = key => {
218
+ const chain = (key) => {
155
219
  const name = key === undefined ? binding : binding?.[key]
156
220
  if (!name) return { provider: null, why: 'nothing provides it (P101 for consumers)' }
157
- if (name.startsWith('weft:default')) return { provider: name, why: 'built in, provided by weft' }
221
+ if (name.startsWith('weft:default'))
222
+ return { provider: name, why: 'built in, provided by weft' }
158
223
  const ref = providerRef(loaded, name, id, key ?? '')
159
224
  const side = ref && (ref.sides.client ?? ref.sides[Object.keys(ref.sides).sort()[0]])
160
225
  return {
161
226
  config: loaded.file ? posix(relative(root, loaded.file)) : null,
162
227
  chosenBy: loaded.config.slots?.[id] ? 'slots' : 'only provider',
163
228
  plugin: name,
164
- version: loaded.contributions.find(c => c.name === name)?.version ?? null,
229
+ version: loaded.contributions.find((c) => c.name === name)?.version ?? null,
165
230
  interface: ref?.versions ?? null,
166
231
  sides: ref ? Object.keys(ref.sides).sort() : [],
167
- module: side ? { module: side.module, export: side.export, ...(side.options === undefined ? {} : { options: side.options }) } : null,
232
+ module: side
233
+ ? {
234
+ module: side.module,
235
+ export: side.export,
236
+ ...(side.options === undefined ? {} : { options: side.options }),
237
+ }
238
+ : null,
168
239
  }
169
240
  }
170
- const bindings = def?.cardinality === 'keyed' || (binding && typeof binding === 'object') ? Object.fromEntries(Object.keys(binding ?? {}).sort().map(k => [k, chain(k)])) : chain()
171
- return { slot: id, version: def?.version ?? null, cardinality: def?.cardinality ?? 'one', sides: def?.sides ?? [], binding: bindings, file: slotFile(id), import: `weft:slot/${id}` }
241
+ const bindings =
242
+ def?.cardinality === 'keyed' || (binding && typeof binding === 'object')
243
+ ? Object.fromEntries(
244
+ Object.keys(binding ?? {})
245
+ .sort()
246
+ .map((k) => [k, chain(k)]),
247
+ )
248
+ : chain()
249
+ return {
250
+ slot: id,
251
+ version: def?.version ?? null,
252
+ cardinality: def?.cardinality ?? 'one',
253
+ sides: def?.sides ?? [],
254
+ binding: bindings,
255
+ file: slotFile(id),
256
+ import: `weft:slot/${id}`,
257
+ }
172
258
  }