@weft-js/cli 0.1.0 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +38 -14
- package/bin/weft.mjs +8 -1
- package/docs/async.md +16 -6
- package/docs/browser.md +22 -8
- package/docs/config.md +39 -9
- package/docs/core.md +30 -0
- package/docs/deploy.md +108 -0
- package/docs/design.md +12 -3
- package/docs/entrypoints.md +79 -0
- package/docs/fonts.md +12 -2
- package/docs/icons.md +13 -7
- package/docs/lists.md +3 -1
- package/docs/meta.md +13 -3
- package/docs/plugins.md +32 -9
- package/docs/props.md +13 -2
- package/docs/reactivity.md +26 -0
- package/docs/rules.md +19 -12
- package/docs/rust.md +47 -17
- package/docs/start-short.md +3 -2
- package/docs/start.md +226 -52
- package/docs/state.md +33 -7
- package/docs/style.md +31 -8
- package/docs/surface.md +61 -15
- package/docs/testing.md +15 -5
- package/docs/theme.md +182 -0
- package/docs/ui-state.md +19 -6
- package/docs/verify.md +13 -0
- package/package.json +13 -4
- package/src/add/config.mjs +93 -0
- package/src/add/errors.mjs +2 -0
- package/src/add/setup.mjs +88 -0
- package/src/add-rust.mjs +82 -27
- package/src/add.mjs +209 -181
- package/src/agents-md.mjs +144 -43
- package/src/alias.mjs +16 -5
- package/src/audit/page.mjs +313 -0
- package/src/audit/report.mjs +69 -0
- package/src/audit/utils.mjs +5 -0
- package/src/audit.mjs +174 -188
- package/src/browser/cli.mjs +24 -5
- package/src/browser/client.mjs +189 -25
- package/src/browser/commands.mjs +166 -51
- package/src/catalog/audit.mjs +129 -0
- package/src/catalog/auth.mjs +40 -0
- package/src/catalog/compiler.mjs +337 -0
- package/src/catalog/contracts.mjs +18 -0
- package/src/catalog/database.mjs +52 -0
- package/src/catalog/deploy.mjs +117 -0
- package/src/catalog/design.mjs +159 -0
- package/src/catalog/forms.mjs +25 -0
- package/src/catalog/hydration.mjs +29 -0
- package/src/catalog/icons.mjs +24 -0
- package/src/catalog/media.mjs +163 -0
- package/src/catalog/metadata.mjs +116 -0
- package/src/catalog/plugins.mjs +228 -0
- package/src/catalog/realtime.mjs +195 -0
- package/src/catalog/rust.mjs +75 -0
- package/src/catalog/server-functions.mjs +92 -0
- package/src/catalog/state.mjs +39 -0
- package/src/catalog/style-groups.mjs +54 -0
- package/src/catalog/tests.mjs +127 -0
- package/src/catalog/text.mjs +200 -0
- package/src/catalog/ui-state.mjs +42 -0
- package/src/catalog.mjs +68 -444
- package/src/check/runtime.mjs +280 -0
- package/src/check.mjs +199 -207
- package/src/cli/authoring.mjs +213 -0
- package/src/cli/project.mjs +302 -0
- package/src/cli.mjs +228 -224
- package/src/config.mjs +1 -1
- package/src/contributions.mjs +75 -25
- package/src/db-layer.mjs +13 -5
- package/src/deploy/cdn.mjs +142 -0
- package/src/deploy/files.mjs +266 -0
- package/src/deploy/fit.mjs +166 -0
- package/src/deploy/hosts.mjs +126 -0
- package/src/deploy/index/cli.mjs +20 -0
- package/src/deploy/index/environment.mjs +69 -0
- package/src/deploy/index/steps.mjs +87 -0
- package/src/deploy/index.mjs +387 -0
- package/src/deploy/needs.mjs +149 -0
- package/src/design-checks/accessible-names.mjs +62 -0
- package/src/design-checks/contrast.mjs +43 -0
- package/src/design-checks/csp.mjs +60 -0
- package/src/design-checks/files.mjs +59 -0
- package/src/design-checks/heading-links.mjs +61 -0
- package/src/design-checks/interaction.mjs +65 -0
- package/src/design-checks/jsx.mjs +67 -0
- package/src/design-checks/layout-values.mjs +68 -0
- package/src/design-checks/native-controls.mjs +48 -0
- package/src/design-checks/outline.mjs +39 -0
- package/src/design-checks/reporter.mjs +37 -0
- package/src/design-checks/rules.mjs +274 -0
- package/src/design-checks/values.mjs +262 -0
- package/src/design-checks.mjs +10 -400
- package/src/diag.mjs +32 -8
- package/src/docs.mjs +138 -42
- package/src/doctor.mjs +20 -8
- package/src/files.mjs +18 -5
- package/src/fix.mjs +10 -2
- package/src/gate/after.mjs +40 -0
- package/src/gate/before.mjs +54 -0
- package/src/gate/classify.mjs +72 -0
- package/src/gate/mutation.mjs +209 -0
- package/src/gate/resolve-cases.mjs +22 -0
- package/src/gate/result.mjs +63 -0
- package/src/gate/run.mjs +75 -0
- package/src/gate.mjs +104 -202
- package/src/infer/box-props.cjs +140 -0
- package/src/infer/core-boxes.cjs +46 -0
- package/src/infer/derived-writes.cjs +95 -0
- package/src/infer/engine/checks.cjs +193 -0
- package/src/infer/engine/flow.cjs +293 -0
- package/src/infer/engine/report.cjs +158 -0
- package/src/infer/engine/utils.cjs +15 -0
- package/src/infer/engine.cjs +108 -368
- package/src/infer/facts/sites.cjs +114 -0
- package/src/infer/facts.cjs +210 -117
- package/src/infer/hosts.cjs +69 -22
- package/src/infer/lsp.mjs +149 -40
- package/src/infer/session.cjs +73 -17
- package/src/infer/shadow.cjs +13 -5
- package/src/infer/worker.cjs +57 -9
- package/src/jsx-scan.mjs +63 -21
- package/src/load-config/schema.mjs +281 -0
- package/src/load-config.mjs +144 -116
- package/src/map.mjs +93 -26
- package/src/mcp/browser-tools.mjs +155 -0
- package/src/mcp.mjs +147 -69
- package/src/meta-table.mjs +43 -8
- package/src/mutate.mjs +120 -36
- package/src/plugin-checks.mjs +52 -23
- package/src/plugins.mjs +125 -39
- package/src/project.mjs +170 -79
- package/src/props-checks.mjs +24 -6
- package/src/props-listener.mjs +27 -9
- package/src/react-checks.mjs +35 -4
- package/src/server-state-checks.mjs +34 -0
- package/src/share-checks.mjs +121 -37
- package/src/site.mjs +33 -6
- package/src/slots.mjs +104 -19
- package/src/start/build.mjs +116 -0
- package/src/start/routes.mjs +38 -0
- package/src/start.mjs +194 -92
- package/src/state-checks/core-state.mjs +223 -0
- package/src/state-checks/modules.mjs +17 -0
- package/src/state-checks/scan.mjs +232 -0
- package/src/state-checks/server-graph.mjs +65 -0
- package/src/state-checks.mjs +109 -133
- package/src/style/find.mjs +121 -0
- package/src/style.mjs +133 -127
- package/src/surface-checks.mjs +212 -46
- package/src/tailwind.mjs +5 -4
- package/src/tasks.mjs +189 -27
- package/src/test-rules/context.mjs +109 -0
- package/src/test-rules/file-rules.mjs +279 -0
- package/src/test-rules/rule-helpers.mjs +244 -0
- package/src/test-rules/syntax.mjs +223 -0
- package/src/test-rules/test-rules.mjs +127 -0
- package/src/testdiff.mjs +94 -32
- package/src/tests.mjs +31 -483
- package/src/text-checks.mjs +229 -69
- package/src/tokens.mjs +12 -4
- package/src/trust/scan.mjs +95 -0
- package/src/trust.mjs +214 -102
- package/src/weft.mjs +41 -9
- package/src/workspace.mjs +96 -19
- package/ts-plugin/context.js +57 -0
- package/ts-plugin/index.js +4 -124
- package/ts-plugin/mapping.js +65 -0
- package/ts-plugin/proxy.js +144 -0
package/src/state-checks.mjs
CHANGED
|
@@ -3,140 +3,53 @@
|
|
|
3
3
|
// These need to know which imports are queries, so they read the server modules; the compiler sees one file at a time. TypeScript's parser only, no type checking, and a name's taint lasts for the
|
|
4
4
|
// function it was declared in, so a same-named primitive elsewhere isn't flagged.
|
|
5
5
|
|
|
6
|
-
import {
|
|
7
|
-
import { join
|
|
6
|
+
import { readFileSync } from 'node:fs'
|
|
7
|
+
import { join } from 'node:path'
|
|
8
8
|
import ts from 'typescript'
|
|
9
|
-
import { local } from './alias.mjs'
|
|
10
9
|
import { diagnostic, positions } from './diag.mjs'
|
|
11
10
|
import { discover } from './files.mjs'
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
const KEEPS_OBJECTS = new Set(['map', 'filter', 'flatMap', 'slice', 'sort', 'toSorted', 'reverse', 'concat', 'find', 'at', 'structuredClone'])
|
|
15
|
-
const ESCAPES = new Set(['untrack', 'optimistic'])
|
|
11
|
+
import { scan } from './state-checks/scan.mjs'
|
|
12
|
+
import { isServerModule, moduleFile } from './state-checks/modules.mjs'
|
|
16
13
|
|
|
17
14
|
/** `export const name = query(` in a module: the names of its queries. */
|
|
18
|
-
const queriesOf =
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
if (!abs) return undefined
|
|
23
|
-
const base = relative(root, abs).split('\\').join('/').replace(/\.js$/, '')
|
|
24
|
-
return ['', '.ts', '.tsx', '.mts', '.js', '/index.ts'].map(x => base + x).find(f => /\.[cm]?[jt]sx?$/.test(f) && existsSync(join(root, f)))
|
|
25
|
-
}
|
|
15
|
+
const queriesOf = (text) =>
|
|
16
|
+
new Set(
|
|
17
|
+
[...text.matchAll(/export\s+const\s+([A-Za-z_$][\w$]*)\s*=\s*query\s*\(/g)].map((m) => m[1]),
|
|
18
|
+
)
|
|
26
19
|
|
|
27
20
|
/** Local names of the queries `sf` imports from server modules. */
|
|
28
21
|
function importedQueries(root, rel, sf, cache) {
|
|
29
22
|
const out = new Set()
|
|
30
23
|
for (const n of sf.statements) {
|
|
31
|
-
if (
|
|
24
|
+
if (
|
|
25
|
+
!ts.isImportDeclaration(n) ||
|
|
26
|
+
!n.importClause?.namedBindings ||
|
|
27
|
+
!ts.isNamedImports(n.importClause.namedBindings)
|
|
28
|
+
)
|
|
29
|
+
continue
|
|
32
30
|
const file = moduleFile(root, rel, n.moduleSpecifier.text)
|
|
33
31
|
if (!file || !isServerModule(file)) continue
|
|
34
32
|
if (!cache.has(file)) cache.set(file, queriesOf(readFileSync(join(root, file), 'utf8')))
|
|
35
|
-
for (const s of n.importClause.namedBindings.elements)
|
|
33
|
+
for (const s of n.importClause.namedBindings.elements)
|
|
34
|
+
if (cache.get(file).has((s.propertyName ?? s.name).text)) out.add(s.name.text)
|
|
36
35
|
}
|
|
37
36
|
return out
|
|
38
37
|
}
|
|
39
38
|
|
|
40
|
-
function scan(rel, text, queries, report) {
|
|
41
|
-
const sf = ts.createSourceFile(rel, text, ts.ScriptTarget.Latest, true, /\.[jt]sx$/.test(rel) ? ts.ScriptKind.TSX : ts.ScriptKind.TS)
|
|
42
|
-
const frames = [{ handles: new Set(), values: new Set() }]
|
|
43
|
-
const has = (kind, name) => frames.some(f => f[kind].has(name))
|
|
44
|
-
const peel = e => { while (ts.isParenthesizedExpression(e) || ts.isAsExpression(e) || ts.isNonNullExpression(e) || ts.isSatisfiesExpression(e)) e = e.expression; return e }
|
|
45
|
-
|
|
46
|
-
const isQueryCall = e => ts.isCallExpression(e) && (ts.isIdentifier(e.expression) ? queries.has(e.expression.text) : ts.isPropertyAccessExpression(e.expression) && e.expression.name.text === 'live' && ts.isIdentifier(e.expression.expression) && queries.has(e.expression.expression.text))
|
|
47
|
-
const isFetch = e => ts.isCallExpression(e) && ts.isPropertyAccessExpression(e.expression) && e.expression.name.text === 'fetch' && ts.isIdentifier(e.expression.expression) && queries.has(e.expression.expression.text)
|
|
48
|
-
const isHandle = e => { e = peel(e); return isQueryCall(e) || (ts.isIdentifier(e) && has('handles', e.text)) }
|
|
49
|
-
|
|
50
|
-
/** The expression holds a whole server object or array, or a derived copy of one. A picked field isn't one. */
|
|
51
|
-
function serverValue(e) {
|
|
52
|
-
e = peel(e)
|
|
53
|
-
if (ts.isIdentifier(e)) return has('values', e.text)
|
|
54
|
-
if (ts.isAwaitExpression(e)) return isFetch(peel(e.expression)) || isQueryCall(peel(e.expression))
|
|
55
|
-
if (ts.isPropertyAccessExpression(e)) return e.name.text === 'value' && isHandle(e.expression)
|
|
56
|
-
if (ts.isElementAccessExpression(e)) return serverValue(e.expression)
|
|
57
|
-
if (ts.isBinaryExpression(e) && [ts.SyntaxKind.QuestionQuestionToken, ts.SyntaxKind.BarBarToken].includes(e.operatorToken.kind)) return serverValue(e.left) || serverValue(e.right)
|
|
58
|
-
if (ts.isConditionalExpression(e)) return serverValue(e.whenTrue) || serverValue(e.whenFalse)
|
|
59
|
-
if (ts.isCallExpression(e)) {
|
|
60
|
-
const c = e.expression
|
|
61
|
-
return ts.isPropertyAccessExpression(c) && KEEPS_OBJECTS.has(c.name.text) && serverValue(c.expression)
|
|
62
|
-
}
|
|
63
|
-
if (ts.isArrayLiteralExpression(e)) return e.elements.some(x => ts.isSpreadElement(x) && serverValue(x.expression))
|
|
64
|
-
if (ts.isObjectLiteralExpression(e)) return e.properties.some(p => ts.isSpreadAssignment(p) && serverValue(p.expression))
|
|
65
|
-
return false
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
const returned = fn => {
|
|
69
|
-
if (!ts.isArrowFunction(fn) && !ts.isFunctionExpression(fn)) return undefined
|
|
70
|
-
if (!ts.isBlock(fn.body)) return fn.body
|
|
71
|
-
const r = fn.body.statements.find(ts.isReturnStatement)
|
|
72
|
-
return r?.expression
|
|
73
|
-
}
|
|
74
|
-
/** The first handle a function's body reads: the source `state.linked` follows. */
|
|
75
|
-
function handleIn(node) {
|
|
76
|
-
/** @type {string | undefined} */
|
|
77
|
-
let found
|
|
78
|
-
const walk = n => {
|
|
79
|
-
if (found) return
|
|
80
|
-
if (ts.isPropertyAccessExpression(n) && n.name.text === 'value' && isHandle(n.expression)) found = n.expression.getText(sf)
|
|
81
|
-
else ts.forEachChild(n, walk)
|
|
82
|
-
}
|
|
83
|
-
walk(node)
|
|
84
|
-
return found
|
|
85
|
-
}
|
|
86
|
-
const flag = (node, why) => report('S001', node.getStart(sf), node.end, why)
|
|
87
|
-
|
|
88
|
-
function visit(node) {
|
|
89
|
-
if (ts.isPropertyAssignment(node) && ts.isIdentifier(node.name) && ESCAPES.has(node.name.text)) return
|
|
90
|
-
if (ts.isMethodDeclaration(node) && ts.isIdentifier(node.name) && ESCAPES.has(node.name.text)) return
|
|
91
|
-
const fn = ts.isFunctionLike(node)
|
|
92
|
-
if (fn) frames.push({ handles: new Set(), values: new Set() })
|
|
93
|
-
try {
|
|
94
|
-
if (ts.isVariableDeclaration(node) && ts.isIdentifier(node.name) && node.initializer) {
|
|
95
|
-
const init = peel(node.initializer)
|
|
96
|
-
if (isQueryCall(init)) frames.at(-1).handles.add(node.name.text)
|
|
97
|
-
else if (serverValue(init)) frames.at(-1).values.add(node.name.text)
|
|
98
|
-
}
|
|
99
|
-
if (ts.isBinaryExpression(node) && node.operatorToken.kind === ts.SyntaxKind.EqualsToken && ts.isPropertyAccessExpression(node.left) && node.left.name.text === 'value' && serverValue(node.right))
|
|
100
|
-
flag(node, `A server value is copied into \`${node.left.expression.getText(sf)}\`: the copy goes stale, and its rows lose their identity.`)
|
|
101
|
-
if (ts.isCallExpression(node)) {
|
|
102
|
-
const c = node.expression
|
|
103
|
-
if (ts.isIdentifier(c) && ESCAPES.has(c.text)) return
|
|
104
|
-
if (ts.isPropertyAccessExpression(c) && ts.isIdentifier(c.expression) && c.expression.text === 'state' && c.name.text === 'linked') {
|
|
105
|
-
const source = handleIn(node.arguments[0] ?? node)
|
|
106
|
-
if (source && node.arguments.length < 2) {
|
|
107
|
-
const at = node.arguments[0].end
|
|
108
|
-
report('S012', node.getStart(sf), node.end, `\`state.linked\` over \`${source}\` has no \`key\`: a refetch resets what someone is typing.`,
|
|
109
|
-
{ description: `Key it by the row: \`{ key: () => ${source}.value?.id }\``, safe: false, edits: [[at, at, `, { key: () => ${source}.value?.id }`]] })
|
|
110
|
-
}
|
|
111
|
-
return // the declared editing path: not S001
|
|
112
|
-
}
|
|
113
|
-
if (ts.isIdentifier(c) && c.text === 'state' && node.arguments[0] && serverValue(node.arguments[0]))
|
|
114
|
-
flag(node, 'A server value is copied into `state`: the copy goes stale, and its rows lose their identity.')
|
|
115
|
-
if (ts.isPropertyAccessExpression(c) && c.name.text === 'set' && node.arguments.length === 1) {
|
|
116
|
-
const r = returned(node.arguments[0])
|
|
117
|
-
if (r && serverValue(r)) flag(node, `A server value is copied into \`${c.expression.getText(sf)}\`: the copy goes stale, and its rows lose their identity.`)
|
|
118
|
-
}
|
|
119
|
-
// The value a callback gets from a query: `watch(() => q.value, v => …)`, `q.fetch().then(v => …)`.
|
|
120
|
-
const fed = ts.isIdentifier(c) && c.text === 'watch' && returned(node.arguments[0]) && serverValue(returned(node.arguments[0])) ? node.arguments[1]
|
|
121
|
-
: ts.isPropertyAccessExpression(c) && c.name.text === 'then' && isFetch(peel(c.expression)) ? node.arguments[0] : undefined
|
|
122
|
-
if (fed && (ts.isArrowFunction(fed) || ts.isFunctionExpression(fed)) && ts.isIdentifier(fed.parameters[0]?.name)) {
|
|
123
|
-
frames.push({ handles: new Set(), values: new Set([fed.parameters[0].name.text]) })
|
|
124
|
-
ts.forEachChild(node, visit)
|
|
125
|
-
frames.pop()
|
|
126
|
-
return
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
ts.forEachChild(node, visit)
|
|
130
|
-
} finally {
|
|
131
|
-
if (fn) frames.pop()
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
visit(sf)
|
|
135
|
-
}
|
|
136
|
-
|
|
137
39
|
/** The local names `resource` is imported as from @weft-js/core. */
|
|
138
|
-
const resourceNames =
|
|
139
|
-
|
|
40
|
+
const resourceNames = (sf) =>
|
|
41
|
+
new Set(
|
|
42
|
+
sf.statements.flatMap((n) =>
|
|
43
|
+
ts.isImportDeclaration(n) &&
|
|
44
|
+
n.moduleSpecifier.text === '@weft-js/core' &&
|
|
45
|
+
n.importClause?.namedBindings &&
|
|
46
|
+
ts.isNamedImports(n.importClause.namedBindings)
|
|
47
|
+
? n.importClause.namedBindings.elements
|
|
48
|
+
.filter((s) => (s.propertyName ?? s.name).text === 'resource')
|
|
49
|
+
.map((s) => s.name.text)
|
|
50
|
+
: [],
|
|
51
|
+
),
|
|
52
|
+
)
|
|
140
53
|
|
|
141
54
|
/**
|
|
142
55
|
* X037: a `resource` or query made in a component's own body and read inside a `<Boundary>` that component renders. A
|
|
@@ -145,15 +58,32 @@ const resourceNames = sf => new Set(sf.statements.flatMap(n => ts.isImportDeclar
|
|
|
145
58
|
* @param {ts.SourceFile} sf @param {Set<string>} makers names whose call makes one (`resource` and the queries)
|
|
146
59
|
*/
|
|
147
60
|
function madeBesideBoundary(sf, makers, report) {
|
|
148
|
-
const peel = e => {
|
|
149
|
-
|
|
61
|
+
const peel = (e) => {
|
|
62
|
+
while (
|
|
63
|
+
ts.isParenthesizedExpression(e) ||
|
|
64
|
+
ts.isAsExpression(e) ||
|
|
65
|
+
ts.isNonNullExpression(e) ||
|
|
66
|
+
ts.isSatisfiesExpression(e)
|
|
67
|
+
)
|
|
68
|
+
e = e.expression
|
|
69
|
+
return e
|
|
70
|
+
}
|
|
71
|
+
const maker = (e) => {
|
|
150
72
|
e = peel(e)
|
|
151
73
|
if (!ts.isCallExpression(e)) return undefined
|
|
152
74
|
const c = e.expression
|
|
153
|
-
const name = ts.isIdentifier(c)
|
|
75
|
+
const name = ts.isIdentifier(c)
|
|
76
|
+
? c.text
|
|
77
|
+
: ts.isPropertyAccessExpression(c) && c.name.text === 'live' && ts.isIdentifier(c.expression)
|
|
78
|
+
? c.expression.text
|
|
79
|
+
: undefined
|
|
154
80
|
return name !== undefined && makers.has(name) ? e : undefined
|
|
155
81
|
}
|
|
156
|
-
const nested =
|
|
82
|
+
const nested = (n) =>
|
|
83
|
+
ts.isFunctionDeclaration(n) ||
|
|
84
|
+
(ts.isVariableDeclaration(n) &&
|
|
85
|
+
!!n.initializer &&
|
|
86
|
+
(ts.isArrowFunction(n.initializer) || ts.isFunctionExpression(n.initializer)))
|
|
157
87
|
function component(fn) {
|
|
158
88
|
/** @type {Map<string, ts.CallExpression>} */
|
|
159
89
|
const made = new Map()
|
|
@@ -166,23 +96,39 @@ function madeBesideBoundary(sf, makers, report) {
|
|
|
166
96
|
}
|
|
167
97
|
if (!made.size) return
|
|
168
98
|
const done = new Set()
|
|
169
|
-
const read = n => {
|
|
170
|
-
if (
|
|
99
|
+
const read = (n) => {
|
|
100
|
+
if (
|
|
101
|
+
ts.isIdentifier(n) &&
|
|
102
|
+
made.has(n.text) &&
|
|
103
|
+
!done.has(n.text) &&
|
|
104
|
+
!(ts.isPropertyAccessExpression(n.parent) && n.parent.name === n)
|
|
105
|
+
) {
|
|
171
106
|
done.add(n.text)
|
|
172
107
|
const call = made.get(n.text)
|
|
173
|
-
report(
|
|
108
|
+
report(
|
|
109
|
+
'X037',
|
|
110
|
+
call.getStart(sf),
|
|
111
|
+
call.end,
|
|
112
|
+
`\`${n.text}\` is made here, beside the <Boundary> that shows it, so that Boundary never waits for it: no fallback shows, and the content renders before the data. Make it in a component rendered inside the Boundary.`,
|
|
113
|
+
)
|
|
174
114
|
}
|
|
175
115
|
ts.forEachChild(n, read)
|
|
176
116
|
}
|
|
177
|
-
const find = n => {
|
|
117
|
+
const find = (n) => {
|
|
178
118
|
if (nested(n)) return // another component, with its own body
|
|
179
|
-
if (ts.isJsxElement(n) && n.openingElement.tagName.getText(sf) === 'Boundary')
|
|
119
|
+
if (ts.isJsxElement(n) && n.openingElement.tagName.getText(sf) === 'Boundary')
|
|
120
|
+
n.children.forEach(read)
|
|
180
121
|
else ts.forEachChild(n, find)
|
|
181
122
|
}
|
|
182
123
|
fn.body.statements.forEach(find)
|
|
183
124
|
}
|
|
184
|
-
const visit = n => {
|
|
185
|
-
if (
|
|
125
|
+
const visit = (n) => {
|
|
126
|
+
if (
|
|
127
|
+
(ts.isFunctionDeclaration(n) || ts.isArrowFunction(n) || ts.isFunctionExpression(n)) &&
|
|
128
|
+
n.body &&
|
|
129
|
+
ts.isBlock(n.body)
|
|
130
|
+
)
|
|
131
|
+
component(n)
|
|
186
132
|
ts.forEachChild(n, visit)
|
|
187
133
|
}
|
|
188
134
|
visit(sf)
|
|
@@ -192,19 +138,49 @@ function madeBesideBoundary(sf, makers, report) {
|
|
|
192
138
|
export function stateChecks(root, { files, exclude = [] }) {
|
|
193
139
|
const out = []
|
|
194
140
|
const cache = new Map()
|
|
195
|
-
const checked = new Set(files.map(f => f.rel))
|
|
141
|
+
const checked = new Set(files.map((f) => f.rel))
|
|
196
142
|
// Scopes usually live in plain .ts files, which the compiler pass doesn't read.
|
|
197
|
-
const extra = discover(root, { include: ['src/**/*.ts'], exclude }).filter(
|
|
198
|
-
|
|
143
|
+
const extra = discover(root, { include: ['src/**/*.ts'], exclude }).filter(
|
|
144
|
+
(f) => !checked.has(f) && !isServerModule(f) && !/\.(d|test|spec)\.ts$/.test(f),
|
|
145
|
+
)
|
|
146
|
+
const all = [
|
|
147
|
+
...files.map((f) => [f.rel, f.text]),
|
|
148
|
+
...extra.map((f) => [f, readFileSync(join(root, f), 'utf8')]),
|
|
149
|
+
]
|
|
199
150
|
for (const [rel, text] of all) {
|
|
200
|
-
const copies = /\b(state|watch|set)\b/.test(text),
|
|
151
|
+
const copies = /\b(state|watch|set)\b/.test(text),
|
|
152
|
+
boundary = /\.[jt]sx$/.test(rel) && text.includes('<Boundary')
|
|
201
153
|
if (isServerModule(rel) || !(copies || boundary)) continue
|
|
202
|
-
const sf = ts.createSourceFile(
|
|
154
|
+
const sf = ts.createSourceFile(
|
|
155
|
+
rel,
|
|
156
|
+
text,
|
|
157
|
+
ts.ScriptTarget.Latest,
|
|
158
|
+
boundary,
|
|
159
|
+
/\.[jt]sx$/.test(rel) ? ts.ScriptKind.TSX : ts.ScriptKind.TS,
|
|
160
|
+
)
|
|
203
161
|
const queries = importedQueries(root, rel, sf, cache)
|
|
204
162
|
const pos = positions(text)
|
|
205
163
|
const report = (code, start, end, why, fix) => {
|
|
206
164
|
const offset = [start, end]
|
|
207
|
-
out.push(
|
|
165
|
+
out.push(
|
|
166
|
+
diagnostic({
|
|
167
|
+
code,
|
|
168
|
+
file: rel,
|
|
169
|
+
span: pos.span(start, end),
|
|
170
|
+
offset,
|
|
171
|
+
why,
|
|
172
|
+
fix: fix && {
|
|
173
|
+
description: fix.description,
|
|
174
|
+
safe: fix.safe,
|
|
175
|
+
edits: fix.edits.map(([a, b, t]) => ({
|
|
176
|
+
file: rel,
|
|
177
|
+
span: pos.span(a, b),
|
|
178
|
+
offset: [a, b],
|
|
179
|
+
text: t,
|
|
180
|
+
})),
|
|
181
|
+
},
|
|
182
|
+
}),
|
|
183
|
+
)
|
|
208
184
|
}
|
|
209
185
|
if (copies && queries.size) scan(rel, text, queries, report)
|
|
210
186
|
if (boundary) madeBesideBoundary(sf, new Set([...queries, ...resourceNames(sf)]), report)
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
export const usageError = (message) => Object.assign(new Error(message), { code: 'E_USAGE' })
|
|
2
|
+
|
|
3
|
+
const words = (s) =>
|
|
4
|
+
s
|
|
5
|
+
.toLowerCase()
|
|
6
|
+
.split(/[\s\-_.:/=,]+|(?<=[a-z])(?=[A-Z])/)
|
|
7
|
+
.filter(Boolean)
|
|
8
|
+
|
|
9
|
+
/** Edit distance with adjacent swaps as one edit (`ordre` → `order`). */
|
|
10
|
+
function distance(a, b) {
|
|
11
|
+
const d = Array.from({ length: a.length + 1 }, (_, i) =>
|
|
12
|
+
Array.from({ length: b.length + 1 }, (__, j) => (i ? (j ? 0 : i) : j)),
|
|
13
|
+
)
|
|
14
|
+
for (let i = 1; i <= a.length; i++)
|
|
15
|
+
for (let j = 1; j <= b.length; j++) {
|
|
16
|
+
d[i][j] = Math.min(
|
|
17
|
+
d[i - 1][j] + 1,
|
|
18
|
+
d[i][j - 1] + 1,
|
|
19
|
+
d[i - 1][j - 1] + (a[i - 1] === b[j - 1] ? 0 : 1),
|
|
20
|
+
)
|
|
21
|
+
if (i > 1 && j > 1 && a[i - 1] === b[j - 2] && a[i - 2] === b[j - 1])
|
|
22
|
+
d[i][j] = Math.min(d[i][j], d[i - 2][j - 2] + 1)
|
|
23
|
+
}
|
|
24
|
+
return d[a.length][b.length]
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** The names closest to `word` (edit distance within a third of its length, or containing it). */
|
|
28
|
+
export function closest(word, names, n = 3) {
|
|
29
|
+
const w = word.toLowerCase()
|
|
30
|
+
const limit = Math.max(1, Math.floor(w.length / 3))
|
|
31
|
+
return names
|
|
32
|
+
.map((x) => [distance(w, x.toLowerCase()), x])
|
|
33
|
+
.filter(([d, x]) => d <= limit || x.toLowerCase().includes(w) || w.includes(x.toLowerCase()))
|
|
34
|
+
.sort((a, b) => a[0] - b[0])
|
|
35
|
+
.slice(0, n)
|
|
36
|
+
.map(([, x]) => x)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* `weft style find <query>`: the groups that fit, best first. Every word of the query must hit the group's name,
|
|
41
|
+
* doc, parts, axes, values, states or tokens (a name word one typo off counts); with no such group, groups that hit
|
|
42
|
+
* any word.
|
|
43
|
+
*/
|
|
44
|
+
export function findGroups(matrix, query) {
|
|
45
|
+
const terms = words(query ?? '')
|
|
46
|
+
if (!terms.length)
|
|
47
|
+
throw usageError(
|
|
48
|
+
'weft style find <query>: say what the thing is (`card`, `status badge`, `critical`)',
|
|
49
|
+
)
|
|
50
|
+
const score = (name, g) => {
|
|
51
|
+
const fields = [
|
|
52
|
+
[words(name), 30],
|
|
53
|
+
[words(g.doc ?? ''), 10],
|
|
54
|
+
[[...g.parts, ...g.states, ...Object.keys(g.axes)].flatMap(words), 8],
|
|
55
|
+
[
|
|
56
|
+
Object.values(g.axes)
|
|
57
|
+
.flatMap((v) => (Array.isArray(v) ? v : []))
|
|
58
|
+
.flatMap(words),
|
|
59
|
+
6,
|
|
60
|
+
],
|
|
61
|
+
[g.tokens.flatMap(words), 4],
|
|
62
|
+
]
|
|
63
|
+
let total = name === query ? 100 : 0
|
|
64
|
+
const matched = []
|
|
65
|
+
let hits = 0
|
|
66
|
+
for (const t of terms) {
|
|
67
|
+
let best = 0
|
|
68
|
+
let why = null
|
|
69
|
+
for (const [ws, weight] of fields) {
|
|
70
|
+
for (const w of ws) {
|
|
71
|
+
const s =
|
|
72
|
+
w === t
|
|
73
|
+
? weight
|
|
74
|
+
: w.startsWith(t) || (t.length > 3 && w.includes(t))
|
|
75
|
+
? weight * 0.7
|
|
76
|
+
: weight >= 30 && t.length > 3 && distance(w, t) === 1
|
|
77
|
+
? weight * 0.5
|
|
78
|
+
: 0
|
|
79
|
+
if (s > best) [best, why] = [s, w]
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
if (best) {
|
|
83
|
+
hits++
|
|
84
|
+
matched.push(why)
|
|
85
|
+
}
|
|
86
|
+
total += best
|
|
87
|
+
}
|
|
88
|
+
return { total, hits, matched: [...new Set(matched)] }
|
|
89
|
+
}
|
|
90
|
+
const all = Object.entries(matrix.groups)
|
|
91
|
+
.map(([name, g]) => ({ name, g, ...score(name, g) }))
|
|
92
|
+
.filter((x) => x.total > 0)
|
|
93
|
+
const full = all.filter((x) => x.hits === terms.length)
|
|
94
|
+
const ranked = (full.length ? full : all).sort(
|
|
95
|
+
(a, b) => b.total - a.total || a.name.localeCompare(b.name),
|
|
96
|
+
)
|
|
97
|
+
// A name or doc hit outranks groups that only share a token (`row` → product-row, not every `layout: row`).
|
|
98
|
+
const picked = ranked.filter((x) => x.total >= (ranked[0]?.total ?? 0) / 4)
|
|
99
|
+
return {
|
|
100
|
+
query,
|
|
101
|
+
groups: picked.map(({ name, g, total, matched }) => ({
|
|
102
|
+
name,
|
|
103
|
+
score: Math.round(total),
|
|
104
|
+
matched,
|
|
105
|
+
doc: g.doc,
|
|
106
|
+
file: g.file,
|
|
107
|
+
import:
|
|
108
|
+
g.binding && g.exported ? { name: g.binding, from: g.file.replace(/:\d+$/, '') } : null,
|
|
109
|
+
axes: g.axes,
|
|
110
|
+
defaults: g.defaults,
|
|
111
|
+
parts: g.parts,
|
|
112
|
+
states: g.states,
|
|
113
|
+
uses: g.used.length,
|
|
114
|
+
similar: matrix.similar
|
|
115
|
+
.filter((s) => s[0] === name || s[1] === name)
|
|
116
|
+
.map((s) => (s[0] === name ? s[1] : s[0])),
|
|
117
|
+
})),
|
|
118
|
+
partial: !full.length && all.length > 0,
|
|
119
|
+
total: Object.keys(matrix.groups).length,
|
|
120
|
+
}
|
|
121
|
+
}
|