@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
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
// Bind identities without loading dependencies or checking project types. Only explicit core
|
|
2
|
+
// value imports and stable const aliases establish state provenance; spelling alone does not.
|
|
3
|
+
import ts from 'typescript'
|
|
4
|
+
|
|
5
|
+
function peel(expression) {
|
|
6
|
+
while (
|
|
7
|
+
ts.isParenthesizedExpression(expression) ||
|
|
8
|
+
ts.isAsExpression(expression) ||
|
|
9
|
+
ts.isNonNullExpression(expression) ||
|
|
10
|
+
ts.isSatisfiesExpression(expression)
|
|
11
|
+
)
|
|
12
|
+
expression = expression.expression
|
|
13
|
+
return expression
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const member = (base, name) =>
|
|
17
|
+
base === 'namespace' && name === 'state'
|
|
18
|
+
? 'state'
|
|
19
|
+
: base === 'state' && ['linked', 'global'].includes(name)
|
|
20
|
+
? name
|
|
21
|
+
: undefined
|
|
22
|
+
|
|
23
|
+
function binder(root, graph) {
|
|
24
|
+
const files = new Map([...graph.values()].map(({ sf }) => [sf.fileName, sf]))
|
|
25
|
+
const options = {
|
|
26
|
+
noLib: true,
|
|
27
|
+
noResolve: true,
|
|
28
|
+
types: [],
|
|
29
|
+
allowJs: true,
|
|
30
|
+
jsx: ts.JsxEmit.Preserve,
|
|
31
|
+
}
|
|
32
|
+
const host = ts.createCompilerHost(options)
|
|
33
|
+
host.getSourceFile = (file) => files.get(file)
|
|
34
|
+
host.fileExists = (file) => files.has(file)
|
|
35
|
+
host.readFile = (file) => files.get(file)?.text
|
|
36
|
+
host.getCurrentDirectory = () => root
|
|
37
|
+
return ts.createProgram([...files.keys()], options, host).getTypeChecker()
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function written(node, symbol, out) {
|
|
41
|
+
node = peel(node)
|
|
42
|
+
if (ts.isIdentifier(node)) {
|
|
43
|
+
const sym = symbol(node)
|
|
44
|
+
if (sym) out.add(sym)
|
|
45
|
+
} else if (ts.isArrayLiteralExpression(node))
|
|
46
|
+
for (const element of node.elements)
|
|
47
|
+
written(ts.isSpreadElement(element) ? element.expression : element, symbol, out)
|
|
48
|
+
else if (ts.isObjectLiteralExpression(node)) {
|
|
49
|
+
for (const property of node.properties) {
|
|
50
|
+
if (ts.isShorthandPropertyAssignment(property)) written(property.name, symbol, out)
|
|
51
|
+
if (ts.isPropertyAssignment(property)) written(property.initializer, symbol, out)
|
|
52
|
+
if (ts.isSpreadAssignment(property)) written(property.expression, symbol, out)
|
|
53
|
+
}
|
|
54
|
+
} else if (ts.isBinaryExpression(node) && node.operatorToken.kind === ts.SyntaxKind.EqualsToken)
|
|
55
|
+
written(node.left, symbol, out)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function scanWrites(sf, symbol, writes, aliases) {
|
|
59
|
+
const scan = (node) => {
|
|
60
|
+
if (ts.isBinaryExpression(node)) {
|
|
61
|
+
const operator = node.operatorToken.kind
|
|
62
|
+
if (
|
|
63
|
+
operator === ts.SyntaxKind.EqualsToken ||
|
|
64
|
+
(operator >= ts.SyntaxKind.FirstCompoundAssignment &&
|
|
65
|
+
operator <= ts.SyntaxKind.LastCompoundAssignment)
|
|
66
|
+
)
|
|
67
|
+
written(node.left, symbol, writes)
|
|
68
|
+
}
|
|
69
|
+
if (ts.isPrefixUnaryExpression(node) || ts.isPostfixUnaryExpression(node)) {
|
|
70
|
+
if ([ts.SyntaxKind.PlusPlusToken, ts.SyntaxKind.MinusMinusToken].includes(node.operator))
|
|
71
|
+
written(node.operand, symbol, writes)
|
|
72
|
+
}
|
|
73
|
+
if (
|
|
74
|
+
ts.isVariableDeclaration(node) &&
|
|
75
|
+
node.initializer &&
|
|
76
|
+
node.parent.flags & ts.NodeFlags.Const
|
|
77
|
+
)
|
|
78
|
+
aliases.push(node)
|
|
79
|
+
ts.forEachChild(node, scan)
|
|
80
|
+
}
|
|
81
|
+
scan(sf)
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** Resolve state, linked and explicit global calls through local binding identities. */
|
|
85
|
+
export function coreState(root, graph) {
|
|
86
|
+
const checker = binder(root, graph)
|
|
87
|
+
const symbol = (node) => {
|
|
88
|
+
const parent = node.parent
|
|
89
|
+
return parent && ts.isShorthandPropertyAssignment(parent) && parent.name === node
|
|
90
|
+
? checker.getShorthandAssignmentValueSymbol(parent)
|
|
91
|
+
: checker.getSymbolAtLocation(node)
|
|
92
|
+
}
|
|
93
|
+
const values = new Map(),
|
|
94
|
+
writes = new Set(),
|
|
95
|
+
aliases = []
|
|
96
|
+
const remember = (node, tag) => {
|
|
97
|
+
const sym = symbol(node)
|
|
98
|
+
if (sym && tag && !writes.has(sym)) values.set(sym, tag)
|
|
99
|
+
}
|
|
100
|
+
for (const { sf } of graph.values()) {
|
|
101
|
+
scanWrites(sf, symbol, writes, aliases)
|
|
102
|
+
for (const node of sf.statements) {
|
|
103
|
+
if (
|
|
104
|
+
!ts.isImportDeclaration(node) ||
|
|
105
|
+
node.moduleSpecifier.text !== '@weft-js/core' ||
|
|
106
|
+
!node.importClause ||
|
|
107
|
+
node.importClause.isTypeOnly
|
|
108
|
+
)
|
|
109
|
+
continue
|
|
110
|
+
const named = node.importClause.namedBindings
|
|
111
|
+
if (named && ts.isNamespaceImport(named)) remember(named.name, 'namespace')
|
|
112
|
+
if (named && ts.isNamedImports(named))
|
|
113
|
+
for (const spec of named.elements)
|
|
114
|
+
if (!spec.isTypeOnly && (spec.propertyName ?? spec.name).text === 'state')
|
|
115
|
+
remember(spec.name, 'state')
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
for (const sym of writes) values.delete(sym)
|
|
119
|
+
const resolve = (expression) => {
|
|
120
|
+
expression = peel(expression)
|
|
121
|
+
if (ts.isIdentifier(expression)) return values.get(symbol(expression))
|
|
122
|
+
if (ts.isPropertyAccessExpression(expression))
|
|
123
|
+
return member(resolve(expression.expression), expression.name.text)
|
|
124
|
+
if (
|
|
125
|
+
ts.isElementAccessExpression(expression) &&
|
|
126
|
+
expression.argumentExpression &&
|
|
127
|
+
ts.isStringLiteralLike(expression.argumentExpression)
|
|
128
|
+
)
|
|
129
|
+
return member(resolve(expression.expression), expression.argumentExpression.text)
|
|
130
|
+
return undefined
|
|
131
|
+
}
|
|
132
|
+
for (let round = 0; round <= aliases.length; round++) {
|
|
133
|
+
const before = values.size
|
|
134
|
+
for (const alias of aliases) {
|
|
135
|
+
const base = resolve(alias.initializer)
|
|
136
|
+
if (!base) continue
|
|
137
|
+
if (ts.isIdentifier(alias.name) && !writes.has(symbol(alias.name))) remember(alias.name, base)
|
|
138
|
+
if (ts.isObjectBindingPattern(alias.name))
|
|
139
|
+
for (const element of alias.name.elements) {
|
|
140
|
+
if (
|
|
141
|
+
!ts.isIdentifier(element.name) ||
|
|
142
|
+
element.initializer ||
|
|
143
|
+
element.dotDotDotToken ||
|
|
144
|
+
writes.has(symbol(element.name))
|
|
145
|
+
)
|
|
146
|
+
continue
|
|
147
|
+
const name = element.propertyName ?? element.name
|
|
148
|
+
if (!ts.isIdentifier(name) && !ts.isStringLiteralLike(name)) continue
|
|
149
|
+
const tag = member(base, name.text)
|
|
150
|
+
remember(element.name, tag)
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
if (values.size === before) break
|
|
154
|
+
}
|
|
155
|
+
return (expression) => resolve(expression)
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// This syntax cannot produce undefined on normal completion. Identifiers, member reads,
|
|
159
|
+
// calls and compound expressions remain unknown; type assertions add no runtime evidence.
|
|
160
|
+
function nonUndefined(expression) {
|
|
161
|
+
expression = peel(expression)
|
|
162
|
+
return [
|
|
163
|
+
ts.SyntaxKind.NumericLiteral,
|
|
164
|
+
ts.SyntaxKind.BigIntLiteral,
|
|
165
|
+
ts.SyntaxKind.StringLiteral,
|
|
166
|
+
ts.SyntaxKind.NoSubstitutionTemplateLiteral,
|
|
167
|
+
ts.SyntaxKind.RegularExpressionLiteral,
|
|
168
|
+
ts.SyntaxKind.TrueKeyword,
|
|
169
|
+
ts.SyntaxKind.FalseKeyword,
|
|
170
|
+
ts.SyntaxKind.NullKeyword,
|
|
171
|
+
ts.SyntaxKind.ObjectLiteralExpression,
|
|
172
|
+
ts.SyntaxKind.ArrayLiteralExpression,
|
|
173
|
+
ts.SyntaxKind.FunctionExpression,
|
|
174
|
+
ts.SyntaxKind.ArrowFunction,
|
|
175
|
+
ts.SyntaxKind.ClassExpression,
|
|
176
|
+
ts.SyntaxKind.NewExpression,
|
|
177
|
+
ts.SyntaxKind.PrefixUnaryExpression,
|
|
178
|
+
ts.SyntaxKind.PostfixUnaryExpression,
|
|
179
|
+
ts.SyntaxKind.TypeOfExpression,
|
|
180
|
+
ts.SyntaxKind.DeleteExpression,
|
|
181
|
+
].includes(expression.kind)
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function iifeDefaults(call, callee, visit) {
|
|
185
|
+
const spread = call.arguments.findIndex(ts.isSpreadElement)
|
|
186
|
+
const fixed = spread < 0 ? call.arguments.length : spread
|
|
187
|
+
// A TypeScript this parameter consumes no positional argument.
|
|
188
|
+
const params = callee.parameters.filter(
|
|
189
|
+
(param) => !ts.isIdentifier(param.name) || param.name.text !== 'this',
|
|
190
|
+
)
|
|
191
|
+
for (const [index, param] of params.entries()) {
|
|
192
|
+
if (param.initializer && (index >= fixed || !nonUndefined(call.arguments[index])))
|
|
193
|
+
visit(param.initializer)
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/** Walk module evaluation, not deferred factories or instance-field initializers. */
|
|
198
|
+
export function moduleCalls(sf, report) {
|
|
199
|
+
const visit = (node) => {
|
|
200
|
+
if (ts.isCallExpression(node)) {
|
|
201
|
+
report(node)
|
|
202
|
+
const callee = peel(node.expression)
|
|
203
|
+
if (ts.isFunctionExpression(callee) || ts.isArrowFunction(callee)) {
|
|
204
|
+
iifeDefaults(node, callee, visit)
|
|
205
|
+
// Generator calls create iterators; their bodies wait for iteration.
|
|
206
|
+
if (!ts.isFunctionExpression(callee) || !callee.asteriskToken) visit(callee.body)
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
if (ts.isFunctionLike(node)) {
|
|
210
|
+
if (node.name && ts.isComputedPropertyName(node.name)) visit(node.name.expression)
|
|
211
|
+
return
|
|
212
|
+
}
|
|
213
|
+
if (
|
|
214
|
+
ts.isPropertyDeclaration(node) &&
|
|
215
|
+
!node.modifiers?.some((modifier) => modifier.kind === ts.SyntaxKind.StaticKeyword)
|
|
216
|
+
) {
|
|
217
|
+
if (ts.isComputedPropertyName(node.name)) visit(node.name.expression)
|
|
218
|
+
return
|
|
219
|
+
}
|
|
220
|
+
ts.forEachChild(node, visit)
|
|
221
|
+
}
|
|
222
|
+
visit(sf)
|
|
223
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
// Shared server-module conventions and local resolution for project state checks.
|
|
2
|
+
import { existsSync } from 'node:fs'
|
|
3
|
+
import { join, relative } from 'node:path'
|
|
4
|
+
import { local } from '../alias.mjs'
|
|
5
|
+
|
|
6
|
+
/** Whether a file uses the server module convention. */
|
|
7
|
+
export const isServerModule = (rel) =>
|
|
8
|
+
rel.startsWith('src/server/') || /\.server\.[cm]?[jt]sx?$/.test(rel)
|
|
9
|
+
/** Resolve a local project module with the existing state-check extensions. */
|
|
10
|
+
export function moduleFile(root, from, spec) {
|
|
11
|
+
const abs = local(join(root, from), spec)
|
|
12
|
+
if (!abs) return undefined
|
|
13
|
+
const base = relative(root, abs).split('\\').join('/').replace(/\.js$/, '')
|
|
14
|
+
return ['', '.ts', '.tsx', '.mts', '.js', '/index.ts']
|
|
15
|
+
.map((x) => base + x)
|
|
16
|
+
.find((f) => /\.[cm]?[jt]sx?$/.test(f) && existsSync(join(root, f)))
|
|
17
|
+
}
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
import ts from 'typescript'
|
|
2
|
+
|
|
3
|
+
const KEEPS_OBJECTS = new Set([
|
|
4
|
+
'map',
|
|
5
|
+
'filter',
|
|
6
|
+
'flatMap',
|
|
7
|
+
'slice',
|
|
8
|
+
'sort',
|
|
9
|
+
'toSorted',
|
|
10
|
+
'reverse',
|
|
11
|
+
'concat',
|
|
12
|
+
'find',
|
|
13
|
+
'at',
|
|
14
|
+
'structuredClone',
|
|
15
|
+
])
|
|
16
|
+
const ESCAPES = new Set(['untrack', 'optimistic'])
|
|
17
|
+
|
|
18
|
+
export function scan(rel, text, queries, report) {
|
|
19
|
+
const sf = ts.createSourceFile(
|
|
20
|
+
rel,
|
|
21
|
+
text,
|
|
22
|
+
ts.ScriptTarget.Latest,
|
|
23
|
+
true,
|
|
24
|
+
/\.[jt]sx$/.test(rel) ? ts.ScriptKind.TSX : ts.ScriptKind.TS,
|
|
25
|
+
)
|
|
26
|
+
const frames = [{ handles: new Set(), values: new Set() }]
|
|
27
|
+
const { peel, isQueryCall, isFetch, isHandle, serverValue } = analysis(queries, frames)
|
|
28
|
+
|
|
29
|
+
const returned = (fn) => {
|
|
30
|
+
if (!ts.isArrowFunction(fn) && !ts.isFunctionExpression(fn)) return undefined
|
|
31
|
+
if (!ts.isBlock(fn.body)) return fn.body
|
|
32
|
+
const r = fn.body.statements.find(ts.isReturnStatement)
|
|
33
|
+
return r?.expression
|
|
34
|
+
}
|
|
35
|
+
/** The first handle a function's body reads: the source `state.linked` follows. */
|
|
36
|
+
function handleIn(node) {
|
|
37
|
+
/** @type {string | undefined} */
|
|
38
|
+
let found
|
|
39
|
+
const walk = (n) => {
|
|
40
|
+
if (found) return
|
|
41
|
+
if (ts.isPropertyAccessExpression(n) && n.name.text === 'value' && isHandle(n.expression))
|
|
42
|
+
found = n.expression.getText(sf)
|
|
43
|
+
else ts.forEachChild(n, walk)
|
|
44
|
+
}
|
|
45
|
+
walk(node)
|
|
46
|
+
return found
|
|
47
|
+
}
|
|
48
|
+
const flag = (node, why) => report('S001', node.getStart(sf), node.end, why)
|
|
49
|
+
|
|
50
|
+
const context = {
|
|
51
|
+
sf,
|
|
52
|
+
frames,
|
|
53
|
+
report,
|
|
54
|
+
peel,
|
|
55
|
+
isQueryCall,
|
|
56
|
+
isFetch,
|
|
57
|
+
serverValue,
|
|
58
|
+
returned,
|
|
59
|
+
handleIn,
|
|
60
|
+
flag,
|
|
61
|
+
}
|
|
62
|
+
const visit = makeVisitor(context)
|
|
63
|
+
visit(sf)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function analysis(queries, frames) {
|
|
67
|
+
const has = (kind, name) => frames.some((f) => f[kind].has(name))
|
|
68
|
+
const peel = (e) => {
|
|
69
|
+
while (
|
|
70
|
+
ts.isParenthesizedExpression(e) ||
|
|
71
|
+
ts.isAsExpression(e) ||
|
|
72
|
+
ts.isNonNullExpression(e) ||
|
|
73
|
+
ts.isSatisfiesExpression(e)
|
|
74
|
+
)
|
|
75
|
+
e = e.expression
|
|
76
|
+
return e
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const isQueryCall = (e) =>
|
|
80
|
+
ts.isCallExpression(e) &&
|
|
81
|
+
(ts.isIdentifier(e.expression)
|
|
82
|
+
? queries.has(e.expression.text)
|
|
83
|
+
: ts.isPropertyAccessExpression(e.expression) &&
|
|
84
|
+
e.expression.name.text === 'live' &&
|
|
85
|
+
ts.isIdentifier(e.expression.expression) &&
|
|
86
|
+
queries.has(e.expression.expression.text))
|
|
87
|
+
const isFetch = (e) =>
|
|
88
|
+
ts.isCallExpression(e) &&
|
|
89
|
+
ts.isPropertyAccessExpression(e.expression) &&
|
|
90
|
+
e.expression.name.text === 'fetch' &&
|
|
91
|
+
ts.isIdentifier(e.expression.expression) &&
|
|
92
|
+
queries.has(e.expression.expression.text)
|
|
93
|
+
const isHandle = (e) => {
|
|
94
|
+
e = peel(e)
|
|
95
|
+
return isQueryCall(e) || (ts.isIdentifier(e) && has('handles', e.text))
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** The expression holds a whole server object or array, or a derived copy of one. A picked field isn't one. */
|
|
99
|
+
function serverValue(e) {
|
|
100
|
+
e = peel(e)
|
|
101
|
+
if (ts.isIdentifier(e)) return has('values', e.text)
|
|
102
|
+
if (ts.isAwaitExpression(e))
|
|
103
|
+
return isFetch(peel(e.expression)) || isQueryCall(peel(e.expression))
|
|
104
|
+
if (ts.isPropertyAccessExpression(e)) return e.name.text === 'value' && isHandle(e.expression)
|
|
105
|
+
if (ts.isElementAccessExpression(e)) return serverValue(e.expression)
|
|
106
|
+
if (
|
|
107
|
+
ts.isBinaryExpression(e) &&
|
|
108
|
+
[ts.SyntaxKind.QuestionQuestionToken, ts.SyntaxKind.BarBarToken].includes(
|
|
109
|
+
e.operatorToken.kind,
|
|
110
|
+
)
|
|
111
|
+
)
|
|
112
|
+
return serverValue(e.left) || serverValue(e.right)
|
|
113
|
+
if (ts.isConditionalExpression(e)) return serverValue(e.whenTrue) || serverValue(e.whenFalse)
|
|
114
|
+
if (ts.isCallExpression(e)) {
|
|
115
|
+
const c = e.expression
|
|
116
|
+
return (
|
|
117
|
+
ts.isPropertyAccessExpression(c) &&
|
|
118
|
+
KEEPS_OBJECTS.has(c.name.text) &&
|
|
119
|
+
serverValue(c.expression)
|
|
120
|
+
)
|
|
121
|
+
}
|
|
122
|
+
if (ts.isArrayLiteralExpression(e))
|
|
123
|
+
return e.elements.some((x) => ts.isSpreadElement(x) && serverValue(x.expression))
|
|
124
|
+
if (ts.isObjectLiteralExpression(e))
|
|
125
|
+
return e.properties.some((p) => ts.isSpreadAssignment(p) && serverValue(p.expression))
|
|
126
|
+
return false
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return { peel, isQueryCall, isFetch, isHandle, serverValue }
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function makeVisitor(context) {
|
|
133
|
+
const { sf, frames, peel, isQueryCall, serverValue, flag } = context
|
|
134
|
+
function visit(node) {
|
|
135
|
+
if (ts.isPropertyAssignment(node) && ts.isIdentifier(node.name) && ESCAPES.has(node.name.text))
|
|
136
|
+
return
|
|
137
|
+
if (ts.isMethodDeclaration(node) && ts.isIdentifier(node.name) && ESCAPES.has(node.name.text))
|
|
138
|
+
return
|
|
139
|
+
const fn = ts.isFunctionLike(node)
|
|
140
|
+
if (fn) frames.push({ handles: new Set(), values: new Set() })
|
|
141
|
+
try {
|
|
142
|
+
if (ts.isVariableDeclaration(node) && ts.isIdentifier(node.name) && node.initializer) {
|
|
143
|
+
const init = peel(node.initializer)
|
|
144
|
+
if (isQueryCall(init)) frames.at(-1).handles.add(node.name.text)
|
|
145
|
+
else if (serverValue(init)) frames.at(-1).values.add(node.name.text)
|
|
146
|
+
}
|
|
147
|
+
if (
|
|
148
|
+
ts.isBinaryExpression(node) &&
|
|
149
|
+
node.operatorToken.kind === ts.SyntaxKind.EqualsToken &&
|
|
150
|
+
ts.isPropertyAccessExpression(node.left) &&
|
|
151
|
+
node.left.name.text === 'value' &&
|
|
152
|
+
serverValue(node.right)
|
|
153
|
+
)
|
|
154
|
+
flag(
|
|
155
|
+
node,
|
|
156
|
+
`A server value is copied into \`${node.left.expression.getText(sf)}\`: the copy goes stale, and its rows lose their identity.`,
|
|
157
|
+
)
|
|
158
|
+
if (ts.isCallExpression(node) && visitCall(node, context, visit)) return
|
|
159
|
+
ts.forEachChild(node, visit)
|
|
160
|
+
} finally {
|
|
161
|
+
if (fn) frames.pop()
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
return visit
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function visitCall(node, context, visit) {
|
|
168
|
+
const { sf, frames, report, peel, isFetch, serverValue, returned, handleIn, flag } = context
|
|
169
|
+
const c = node.expression
|
|
170
|
+
if (ts.isIdentifier(c) && ESCAPES.has(c.text)) return true
|
|
171
|
+
if (
|
|
172
|
+
ts.isPropertyAccessExpression(c) &&
|
|
173
|
+
ts.isIdentifier(c.expression) &&
|
|
174
|
+
c.expression.text === 'state' &&
|
|
175
|
+
c.name.text === 'linked'
|
|
176
|
+
) {
|
|
177
|
+
const source = handleIn(node.arguments[0] ?? node)
|
|
178
|
+
if (source && node.arguments.length < 2) {
|
|
179
|
+
const at = node.arguments[0].end
|
|
180
|
+
report(
|
|
181
|
+
'S012',
|
|
182
|
+
node.getStart(sf),
|
|
183
|
+
node.end,
|
|
184
|
+
`\`state.linked\` over \`${source}\` has no \`key\`: a refetch resets what someone is typing.`,
|
|
185
|
+
{
|
|
186
|
+
description: `Key it by the row: \`{ key: () => ${source}.value?.id }\``,
|
|
187
|
+
safe: false,
|
|
188
|
+
edits: [[at, at, `, { key: () => ${source}.value?.id }`]],
|
|
189
|
+
},
|
|
190
|
+
)
|
|
191
|
+
}
|
|
192
|
+
return true // the declared editing path: not S001
|
|
193
|
+
}
|
|
194
|
+
if (
|
|
195
|
+
ts.isIdentifier(c) &&
|
|
196
|
+
c.text === 'state' &&
|
|
197
|
+
node.arguments[0] &&
|
|
198
|
+
serverValue(node.arguments[0])
|
|
199
|
+
)
|
|
200
|
+
flag(
|
|
201
|
+
node,
|
|
202
|
+
'A server value is copied into `state`: the copy goes stale, and its rows lose their identity.',
|
|
203
|
+
)
|
|
204
|
+
if (ts.isPropertyAccessExpression(c) && c.name.text === 'set' && node.arguments.length === 1) {
|
|
205
|
+
const r = returned(node.arguments[0])
|
|
206
|
+
if (r && serverValue(r))
|
|
207
|
+
flag(
|
|
208
|
+
node,
|
|
209
|
+
`A server value is copied into \`${c.expression.getText(sf)}\`: the copy goes stale, and its rows lose their identity.`,
|
|
210
|
+
)
|
|
211
|
+
}
|
|
212
|
+
// The value a callback gets from a query: `watch(() => q.value, v => …)`, `q.fetch().then(v => …)`.
|
|
213
|
+
const fed =
|
|
214
|
+
ts.isIdentifier(c) &&
|
|
215
|
+
c.text === 'watch' &&
|
|
216
|
+
returned(node.arguments[0]) &&
|
|
217
|
+
serverValue(returned(node.arguments[0]))
|
|
218
|
+
? node.arguments[1]
|
|
219
|
+
: ts.isPropertyAccessExpression(c) && c.name.text === 'then' && isFetch(peel(c.expression))
|
|
220
|
+
? node.arguments[0]
|
|
221
|
+
: undefined
|
|
222
|
+
if (
|
|
223
|
+
fed &&
|
|
224
|
+
(ts.isArrowFunction(fed) || ts.isFunctionExpression(fed)) &&
|
|
225
|
+
ts.isIdentifier(fed.parameters[0]?.name)
|
|
226
|
+
) {
|
|
227
|
+
frames.push({ handles: new Set(), values: new Set([fed.parameters[0].name.text]) })
|
|
228
|
+
ts.forEachChild(node, visit)
|
|
229
|
+
frames.pop()
|
|
230
|
+
return true
|
|
231
|
+
}
|
|
232
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
// Server entry reachability follows local runtime ESM edges, including reexports.
|
|
2
|
+
// Type-only edges do not evaluate modules. Package/dynamic imports stay outside this graph.
|
|
3
|
+
import { readFileSync } from 'node:fs'
|
|
4
|
+
import { join } from 'node:path'
|
|
5
|
+
import ts from 'typescript'
|
|
6
|
+
import { discover } from '../files.mjs'
|
|
7
|
+
import { isServerModule, moduleFile } from './modules.mjs'
|
|
8
|
+
|
|
9
|
+
const source = /\.[cm]?[jt]sx?$/
|
|
10
|
+
const ignored = /(?:\.d\.[cm]?ts$|\.(?:test|spec)\.[cm]?[jt]sx?$|\/(?:__tests__|test)\/)/
|
|
11
|
+
const entry = (file) =>
|
|
12
|
+
isServerModule(file) ||
|
|
13
|
+
/^src\/(?:entry-server|server)\.[cm]?[jt]sx?$/.test(file) ||
|
|
14
|
+
/^src\/routes\/.*\.[jt]sx$/.test(file)
|
|
15
|
+
|
|
16
|
+
function runtime(node) {
|
|
17
|
+
if (ts.isImportDeclaration(node)) {
|
|
18
|
+
const clause = node.importClause
|
|
19
|
+
if (!clause) return true
|
|
20
|
+
if (clause.isTypeOnly) return false
|
|
21
|
+
const named = clause.namedBindings
|
|
22
|
+
return (
|
|
23
|
+
!!clause.name ||
|
|
24
|
+
!named ||
|
|
25
|
+
!ts.isNamedImports(named) ||
|
|
26
|
+
!named.elements.length ||
|
|
27
|
+
named.elements.some((element) => !element.isTypeOnly)
|
|
28
|
+
)
|
|
29
|
+
}
|
|
30
|
+
if (!ts.isExportDeclaration(node) || node.isTypeOnly) return false
|
|
31
|
+
const named = node.exportClause
|
|
32
|
+
return (
|
|
33
|
+
!named ||
|
|
34
|
+
!ts.isNamedExports(named) ||
|
|
35
|
+
!named.elements.length ||
|
|
36
|
+
named.elements.some((element) => !element.isTypeOnly)
|
|
37
|
+
)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Source snapshots reachable from conventional server modules, entries and SSR route pages. */
|
|
41
|
+
export function serverGraph(root, { files, exclude = [] }) {
|
|
42
|
+
const texts = new Map(
|
|
43
|
+
discover(root, {
|
|
44
|
+
include: ['src/**/*.{ts,tsx,js,jsx,mts,cts,mjs,cjs}', '*.server.{ts,js,mts,mjs,cts,cjs}'],
|
|
45
|
+
exclude,
|
|
46
|
+
}).map((file) => [file, readFileSync(join(root, file), 'utf8')]),
|
|
47
|
+
)
|
|
48
|
+
for (const file of files) texts.set(file.rel, file.text)
|
|
49
|
+
for (const file of texts.keys()) if (!source.test(file) || ignored.test(file)) texts.delete(file)
|
|
50
|
+
const graph = new Map()
|
|
51
|
+
const visit = (file, from) => {
|
|
52
|
+
if (graph.has(file) || !texts.has(file)) return
|
|
53
|
+
const text = texts.get(file)
|
|
54
|
+
const sf = ts.createSourceFile(join(root, file), text, ts.ScriptTarget.Latest, true)
|
|
55
|
+
graph.set(file, { sf, entry: from })
|
|
56
|
+
for (const node of sf.statements) {
|
|
57
|
+
if (!runtime(node) || !node.moduleSpecifier || !ts.isStringLiteralLike(node.moduleSpecifier))
|
|
58
|
+
continue
|
|
59
|
+
const target = moduleFile(root, file, node.moduleSpecifier.text)
|
|
60
|
+
if (target) visit(target, from)
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
for (const file of [...texts.keys()].filter(entry).sort()) visit(file, file)
|
|
64
|
+
return graph
|
|
65
|
+
}
|