@weft-js/cli 0.1.1 → 0.1.2

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