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