@crouton-kit/tsym 0.1.0

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 (116) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +51 -0
  3. package/bin/tsym.js +2 -0
  4. package/dist/cli.d.ts +1 -0
  5. package/dist/cli.js +89 -0
  6. package/dist/commands/handlers/check.d.ts +3 -0
  7. package/dist/commands/handlers/check.js +94 -0
  8. package/dist/commands/handlers/find.d.ts +3 -0
  9. package/dist/commands/handlers/find.js +56 -0
  10. package/dist/commands/handlers/inspect.d.ts +3 -0
  11. package/dist/commands/handlers/inspect.js +81 -0
  12. package/dist/commands/handlers/outline.d.ts +3 -0
  13. package/dist/commands/handlers/outline.js +101 -0
  14. package/dist/commands/handlers/query.d.ts +3 -0
  15. package/dist/commands/handlers/query.js +196 -0
  16. package/dist/commands/handlers/rename.d.ts +3 -0
  17. package/dist/commands/handlers/rename.js +53 -0
  18. package/dist/commands/handlers/search.d.ts +5 -0
  19. package/dist/commands/handlers/search.js +32 -0
  20. package/dist/commands/handlers/server-reload.d.ts +3 -0
  21. package/dist/commands/handlers/server-reload.js +7 -0
  22. package/dist/commands/handlers/server-status.d.ts +3 -0
  23. package/dist/commands/handlers/server-status.js +7 -0
  24. package/dist/commands/handlers/server-stop.d.ts +3 -0
  25. package/dist/commands/handlers/server-stop.js +7 -0
  26. package/dist/commands/handlers/show.d.ts +3 -0
  27. package/dist/commands/handlers/show.js +63 -0
  28. package/dist/commands/handlers/type.d.ts +3 -0
  29. package/dist/commands/handlers/type.js +45 -0
  30. package/dist/commands/handlers.d.ts +12 -0
  31. package/dist/commands/handlers.js +44 -0
  32. package/dist/commands/help.d.ts +2 -0
  33. package/dist/commands/help.js +27 -0
  34. package/dist/commands/rename-apply.d.ts +37 -0
  35. package/dist/commands/rename-apply.js +115 -0
  36. package/dist/commands/tree.d.ts +9 -0
  37. package/dist/commands/tree.js +277 -0
  38. package/dist/commands/types.d.ts +80 -0
  39. package/dist/commands/types.js +1 -0
  40. package/dist/core/declarations.d.ts +48 -0
  41. package/dist/core/declarations.js +153 -0
  42. package/dist/core/receipt.d.ts +7 -0
  43. package/dist/core/receipt.js +9 -0
  44. package/dist/core/search.d.ts +29 -0
  45. package/dist/core/search.js +47 -0
  46. package/dist/core/source.d.ts +32 -0
  47. package/dist/core/source.js +96 -0
  48. package/dist/core/workspace.d.ts +28 -0
  49. package/dist/core/workspace.js +137 -0
  50. package/dist/index/build.d.ts +22 -0
  51. package/dist/index/build.js +79 -0
  52. package/dist/index/classify.d.ts +20 -0
  53. package/dist/index/classify.js +60 -0
  54. package/dist/index/commits.d.ts +15 -0
  55. package/dist/index/commits.js +201 -0
  56. package/dist/index/descriptor.d.ts +19 -0
  57. package/dist/index/descriptor.js +295 -0
  58. package/dist/index/graph.d.ts +82 -0
  59. package/dist/index/graph.js +1 -0
  60. package/dist/index/heritage.d.ts +15 -0
  61. package/dist/index/heritage.js +122 -0
  62. package/dist/index/refresh.d.ts +28 -0
  63. package/dist/index/refresh.js +110 -0
  64. package/dist/index/scope.d.ts +4 -0
  65. package/dist/index/scope.js +21 -0
  66. package/dist/index/state.d.ts +22 -0
  67. package/dist/index/state.js +63 -0
  68. package/dist/index/walk.d.ts +23 -0
  69. package/dist/index/walk.js +550 -0
  70. package/dist/index/watch.d.ts +18 -0
  71. package/dist/index/watch.js +175 -0
  72. package/dist/index.d.ts +4 -0
  73. package/dist/index.js +2 -0
  74. package/dist/output.d.ts +12 -0
  75. package/dist/output.js +54 -0
  76. package/dist/read/read.d.ts +32 -0
  77. package/dist/read/read.js +89 -0
  78. package/dist/relations/graph.d.ts +23 -0
  79. package/dist/relations/graph.js +115 -0
  80. package/dist/relations/mermaid.d.ts +3 -0
  81. package/dist/relations/mermaid.js +37 -0
  82. package/dist/relations/store-relations.d.ts +20 -0
  83. package/dist/relations/store-relations.js +158 -0
  84. package/dist/relations/types.d.ts +58 -0
  85. package/dist/relations/types.js +12 -0
  86. package/dist/server/client.d.ts +5 -0
  87. package/dist/server/client.js +215 -0
  88. package/dist/server/context.d.ts +21 -0
  89. package/dist/server/context.js +12 -0
  90. package/dist/server/lifecycle.d.ts +28 -0
  91. package/dist/server/lifecycle.js +62 -0
  92. package/dist/server/main.d.ts +1 -0
  93. package/dist/server/main.js +541 -0
  94. package/dist/server/projects.d.ts +21 -0
  95. package/dist/server/projects.js +28 -0
  96. package/dist/server/wire.d.ts +39 -0
  97. package/dist/server/wire.js +19 -0
  98. package/dist/store/load.d.ts +67 -0
  99. package/dist/store/load.js +193 -0
  100. package/dist/store/meta.d.ts +28 -0
  101. package/dist/store/meta.js +137 -0
  102. package/dist/store/query.d.ts +5 -0
  103. package/dist/store/query.js +4 -0
  104. package/dist/store/schema.d.ts +4 -0
  105. package/dist/store/schema.js +19 -0
  106. package/dist/store/store.d.ts +36 -0
  107. package/dist/store/store.js +240 -0
  108. package/dist/ts7/api.d.ts +35 -0
  109. package/dist/ts7/api.js +164 -0
  110. package/dist/ts7/ast.d.ts +1 -0
  111. package/dist/ts7/ast.js +1 -0
  112. package/dist/ts7/checker.d.ts +55 -0
  113. package/dist/ts7/checker.js +383 -0
  114. package/dist/ts7/projects.d.ts +27 -0
  115. package/dist/ts7/projects.js +94 -0
  116. package/package.json +47 -0
@@ -0,0 +1,383 @@
1
+ import path from 'node:path';
2
+ import { DiagnosticCategory, SymbolFlags } from 'typescript/unstable/sync';
3
+ import { SyntaxKind, isFunctionLikeDeclaration, isTypeNode } from 'typescript/unstable/ast';
4
+ import * as ts from './ast.js';
5
+ export class RenamePreflightError extends Error {
6
+ code;
7
+ site;
8
+ constructor(code, site, message) {
9
+ super(message);
10
+ this.code = code;
11
+ this.site = site;
12
+ }
13
+ }
14
+ export function typeAt(host, root, config, file, position, fallback) {
15
+ return host.withSnapshot((snapshot) => {
16
+ const project = snapshot.getProject(config);
17
+ if (!project)
18
+ throw new Error(`TypeScript project ${config} is not loaded.`);
19
+ const symbol = project.checker.getSymbolAtPosition(file, position);
20
+ const type = project.checker.getTypeAtPosition(file, position);
21
+ if (!symbol || !type)
22
+ return null;
23
+ const declaration = symbol.declarations.find((handle) => handle.path === file)?.resolve(project) ?? symbol.declarations[0]?.resolve(project);
24
+ const signature = renderSignature(project, declaration, type, symbol.name, fallback.kind);
25
+ const definitions = typeDefinitions(project, root, declaration, fallback.address);
26
+ return {
27
+ kind: fallback.kind,
28
+ modifiers: declaration ? modifiers(declaration) : undefined,
29
+ signature,
30
+ documentation: project.checker.getDocumentationCommentOfSymbol(symbol),
31
+ tags: project.checker.getJsDocTagsOfSymbol(symbol).map((tag) => ({ name: tag.name, text: tag.text ?? '' })),
32
+ definitions,
33
+ };
34
+ });
35
+ }
36
+ export function diagnosticsForFiles(host, root, scopes) {
37
+ return host.withSnapshot((snapshot) => {
38
+ const diagnostics = [];
39
+ for (const scope of scopes) {
40
+ const project = snapshot.getProject(scope.config);
41
+ if (!project)
42
+ throw new Error(`TypeScript project ${scope.config} is not loaded.`);
43
+ for (const file of scope.files)
44
+ diagnostics.push(...normalizeDiagnostics(root, project, [
45
+ ...project.program.getSyntacticDiagnostics(file),
46
+ ...project.program.getSemanticDiagnostics(file),
47
+ ...project.program.getSuggestionDiagnostics(file),
48
+ ]));
49
+ }
50
+ return unique(diagnostics);
51
+ });
52
+ }
53
+ /** Builds rename edits from indexed reference sites and rejects shapes TS7 cannot rewrite safely. */
54
+ export function renameFromOccurrences(host, root, projects, declaration, occurrences, newName) {
55
+ return host.withSnapshot((snapshot) => {
56
+ const declarationProject = projects.membership(declaration.file);
57
+ if (!declarationProject)
58
+ throw new Error(`The declaration ${declaration.address} is not in a configured TypeScript project.`);
59
+ const project = snapshot.getProject(declarationProject.config);
60
+ if (!project)
61
+ throw new Error(`TypeScript project ${declarationProject.config} is not loaded.`);
62
+ const renamed = symbolAt(project, declaration.file, declaration.nameSpan.start);
63
+ if (!renamed)
64
+ throw new Error(`TypeScript could not resolve ${declaration.address}.`);
65
+ const target = unalias(project, renamed);
66
+ assertWorkspaceDeclarations(root, project, target, declaration.address);
67
+ assertNoStringLiteralRename(root, projects, snapshot, target, declaration.name);
68
+ const declarationSource = project.program.getSourceFile(declaration.file);
69
+ if (!declarationSource)
70
+ throw new Error(`TypeScript could not load ${declaration.file} for rename.`);
71
+ const declarationPoint = declarationSource.getLineAndCharacterOfPosition(declaration.nameSpan.start);
72
+ const declarationOccurrence = { file: relative(root, declaration.file), line: declarationPoint.line + 1, col: declarationPoint.character + 1 };
73
+ const edits = new Map();
74
+ const sites = new Map();
75
+ for (const occurrence of [...occurrences, declarationOccurrence, ...shapeOccurrences(root, projects, snapshot, target)])
76
+ sites.set(address(occurrence), occurrence);
77
+ for (const occurrence of sites.values()) {
78
+ const file = path.resolve(root, occurrence.file);
79
+ const membership = projects.membership(file);
80
+ if (!membership)
81
+ throw new Error(`The indexed rename site ${address(occurrence)} is not in a configured TypeScript project.`);
82
+ const siteProject = snapshot.getProject(membership.config);
83
+ if (!siteProject)
84
+ throw new Error(`TypeScript project ${membership.config} is not loaded.`);
85
+ const source = siteProject.program.getSourceFile(file);
86
+ if (!source)
87
+ throw new Error(`TypeScript could not load ${occurrence.file} for rename.`);
88
+ const position = source.getPositionOfLineAndCharacter(occurrence.line - 1, occurrence.col - 1);
89
+ const node = nodeAt(source, position);
90
+ if (!node || !ts.isIdentifier(node))
91
+ throw new Error(`The indexed rename site ${address(occurrence)} is not an identifier in the current program.`);
92
+ if (hasComputedName(node))
93
+ throw new RenamePreflightError('rename-through-computed-name', address(occurrence), `Cannot rename through computed property name at ${address(occurrence)}.`);
94
+ if (isExplicitImportAliasLocal(siteProject, file, node))
95
+ continue;
96
+ if (!renameSymbols(siteProject, file, node).some((candidate) => sameSymbol(siteProject, candidate, target)))
97
+ throw new Error(`The indexed rename site ${address(occurrence)} no longer resolves to ${declaration.address}.`);
98
+ if (isExportAlias(node))
99
+ continue;
100
+ assertNoConflict(siteProject, target, newName, node, address(occurrence));
101
+ const edit = editForNode(siteProject, source, node, target, newName);
102
+ edits.set(`${occurrence.file}:${edit.startUtf16}:${edit.endUtf16}`, { ...occurrence, ...edit });
103
+ }
104
+ return [...edits.values()].sort((left, right) => left.file.localeCompare(right.file) || left.startUtf16 - right.startUtf16 || left.endUtf16 - right.endUtf16);
105
+ });
106
+ }
107
+ function assertWorkspaceDeclarations(root, project, symbol, site) {
108
+ for (const handle of symbol.declarations) {
109
+ const declaration = handle.resolve(project);
110
+ if (declaration && !inside(root, declaration.getSourceFile().fileName))
111
+ throw new RenamePreflightError('rename-outside-workspace', site, `Cannot rename ${symbol.name}: a declaration is outside the workspace at ${declaration.getSourceFile().fileName}.`);
112
+ }
113
+ }
114
+ function assertNoStringLiteralRename(root, projects, snapshot, target, name) {
115
+ for (const scope of projects.records()) {
116
+ const project = snapshot.getProject(scope.config);
117
+ if (!project)
118
+ continue;
119
+ for (const file of scope.files) {
120
+ const source = project.program.getSourceFile(file);
121
+ if (!source)
122
+ continue;
123
+ const string = firstStringElementAccess(source, (node) => node.text === name, (argument) => {
124
+ const symbol = symbolAt(project, file, argument.getStart(source));
125
+ return !!symbol && sameSymbol(project, symbol, target);
126
+ });
127
+ if (string) {
128
+ const point = source.getLineAndCharacterOfPosition(string.getStart(source));
129
+ throw new RenamePreflightError('rename-through-string-literal', `${relative(root, file)}:${point.line + 1}:${point.character + 1}`, `Cannot rename through string-literal element access at ${relative(root, file)}:${point.line + 1}:${point.character + 1}.`);
130
+ }
131
+ }
132
+ }
133
+ }
134
+ function shapeOccurrences(root, projects, snapshot, target) {
135
+ const sites = new Map();
136
+ const add = (file, source, node) => {
137
+ if (!ts.isIdentifier(node))
138
+ return;
139
+ const point = source.getLineAndCharacterOfPosition(node.getStart(source));
140
+ const site = { file: relative(root, file), line: point.line + 1, col: point.character + 1 };
141
+ sites.set(address(site), site);
142
+ };
143
+ const visit = (project, file, source, node) => {
144
+ if (ts.isShorthandPropertyAssignment(node) && ts.isIdentifier(node.name)) {
145
+ const value = project.checker.getShorthandAssignmentValueSymbol(node);
146
+ if (value && sameSymbol(project, value, target))
147
+ add(file, source, node.name);
148
+ }
149
+ if (ts.isExportSpecifier(node)) {
150
+ const value = project.checker.getExportSpecifierLocalTargetSymbol(node);
151
+ if (value && sameSymbol(project, value, target))
152
+ add(file, source, node.propertyName ?? node.name);
153
+ }
154
+ if (ts.isExportAssignment(node) && !node.isExportEquals && ts.isIdentifier(node.expression)) {
155
+ const value = symbolAt(project, file, node.expression.getStart(source));
156
+ if (value && sameSymbol(project, value, target))
157
+ add(file, source, node.expression);
158
+ }
159
+ if (ts.isObjectLiteralElementLike(node)) {
160
+ const name = 'name' in node ? node.name : undefined;
161
+ if (name && ts.isIdentifier(name)) {
162
+ const property = contextualObjectProperty(project, name);
163
+ if (property && sameSymbol(project, property, target))
164
+ add(file, source, name);
165
+ }
166
+ }
167
+ if (ts.isBindingElement(node)) {
168
+ const bindingName = node.name;
169
+ if (bindingName && ts.isIdentifier(bindingName)) {
170
+ const property = destructuredProperty(project, bindingName);
171
+ if (property && sameSymbol(project, property, target))
172
+ add(file, source, bindingName);
173
+ }
174
+ }
175
+ node.forEachChild((child) => visit(project, file, source, child));
176
+ };
177
+ for (const scope of projects.records()) {
178
+ const project = snapshot.getProject(scope.config);
179
+ if (!project)
180
+ continue;
181
+ for (const file of scope.files) {
182
+ if (projects.membership(file)?.config !== scope.config)
183
+ continue;
184
+ const source = project.program.getSourceFile(file);
185
+ if (source)
186
+ visit(project, file, source, source);
187
+ }
188
+ }
189
+ return [...sites.values()];
190
+ }
191
+ function firstStringElementAccess(node, namesSymbol, matches) {
192
+ if (ts.isElementAccessExpression(node) && ts.isStringLiteral(node.argumentExpression) && namesSymbol(node.argumentExpression) && matches(node.argumentExpression))
193
+ return node.argumentExpression;
194
+ let found;
195
+ node.forEachChild((child) => { if (!found)
196
+ found = firstStringElementAccess(child, namesSymbol, matches); });
197
+ return found;
198
+ }
199
+ function assertNoConflict(project, target, name, location, site) {
200
+ const resolved = project.checker.resolveName(name, target.flags, location);
201
+ if (resolved && !sameSymbol(project, resolved, target))
202
+ throw conflict(project, name, site, resolved);
203
+ const member = memberNamed(project, location, name);
204
+ if (member && !sameSymbol(project, member, target))
205
+ throw conflict(project, name, site, member);
206
+ }
207
+ function memberNamed(project, location, name) {
208
+ if (ts.isPropertyAccessExpression(location.parent) && location.parent.name === location) {
209
+ const type = project.checker.getTypeAtLocation(location.parent.expression);
210
+ return type && project.checker.getPropertyOfType(type, name);
211
+ }
212
+ for (let current = location.parent; current; current = current.parent) {
213
+ if (ts.isClassLikeDeclaration(current) || ts.isInterfaceDeclaration(current)) {
214
+ const type = project.checker.getTypeAtLocation(current);
215
+ return type && project.checker.getPropertyOfType(type, name);
216
+ }
217
+ }
218
+ return undefined;
219
+ }
220
+ function conflict(project, name, site, resolved) {
221
+ return new RenamePreflightError('rename-conflict', site, `Cannot rename to ${name}: it resolves to ${unalias(project, resolved).name} at ${site}.`);
222
+ }
223
+ function renameSymbols(project, file, node) {
224
+ const symbols = [
225
+ ts.isShorthandPropertyAssignment(node.parent) && node.parent.name === node ? project.checker.getShorthandAssignmentValueSymbol(node.parent) : undefined,
226
+ ts.isExportSpecifier(node.parent) ? project.checker.getExportSpecifierLocalTargetSymbol(node.parent) : undefined,
227
+ contextualObjectProperty(project, node),
228
+ destructuredProperty(project, node),
229
+ symbolAt(project, file, node.getStart(node.getSourceFile())),
230
+ ];
231
+ return symbols.filter((symbol) => !!symbol);
232
+ }
233
+ function contextualObjectProperty(project, node) {
234
+ const member = node.parent;
235
+ if (!member || !ts.isObjectLiteralElementLike(member) || !('name' in member) || member.name !== node)
236
+ return undefined;
237
+ const object = member.parent;
238
+ const contextual = object && ts.isObjectLiteralExpression(object) ? project.checker.getContextualType(object) : undefined;
239
+ return contextual && project.checker.getPropertyOfType(contextual, node.text);
240
+ }
241
+ function destructuredProperty(project, node) {
242
+ const element = node.parent;
243
+ if (!element || !ts.isBindingElement(element) || element.name !== node || element.propertyName)
244
+ return undefined;
245
+ const pattern = element.parent;
246
+ const declaration = pattern?.parent;
247
+ if (!pattern || !ts.isObjectBindingPattern(pattern) || !declaration || !ts.isVariableDeclaration(declaration) || !declaration.initializer)
248
+ return undefined;
249
+ const type = project.checker.getTypeAtLocation(declaration.initializer);
250
+ return type && project.checker.getPropertyOfType(type, node.text);
251
+ }
252
+ function isExportAlias(node) { return ts.isExportSpecifier(node.parent) && node.parent.propertyName !== undefined && node.parent.name === node; }
253
+ function isExplicitImportAliasLocal(project, file, node) {
254
+ const symbol = symbolAt(project, file, node.getStart(node.getSourceFile()));
255
+ if (!symbol || !(symbol.flags & SymbolFlags.Alias))
256
+ return false;
257
+ return symbol.declarations.some((handle) => {
258
+ const declaration = handle.resolve(project);
259
+ return !!declaration && ts.isImportSpecifier(declaration) && declaration.propertyName !== undefined;
260
+ });
261
+ }
262
+ function editForNode(project, source, node, target, newName) {
263
+ const parent = node.parent;
264
+ const oldText = node.getText(source);
265
+ if (ts.isShorthandPropertyAssignment(parent) && parent.name === node) {
266
+ const value = project.checker.getShorthandAssignmentValueSymbol(parent);
267
+ if (value)
268
+ return { startUtf16: node.getStart(source), endUtf16: node.getEnd(), oldText, replacement: sameSymbol(project, value, target) ? `${oldText}: ${newName}` : `${newName}: ${oldText}` };
269
+ }
270
+ if (ts.isBindingElement(parent) && parent.name === node && !parent.propertyName && ts.isObjectBindingPattern(parent.parent)) {
271
+ const binding = symbolAt(project, source.fileName, node.getStart(source));
272
+ return { startUtf16: node.getStart(source), endUtf16: node.getEnd(), oldText, replacement: binding && sameSymbol(project, binding, target) ? `${oldText}: ${newName}` : `${newName}: ${oldText}` };
273
+ }
274
+ if (ts.isExportSpecifier(parent)) {
275
+ const local = project.checker.getExportSpecifierLocalTargetSymbol(parent);
276
+ if (local && sameSymbol(project, local, symbolAt(project, source.fileName, node.getStart(source)))) {
277
+ if (parent.name === node && !parent.propertyName)
278
+ return { startUtf16: node.getStart(source), endUtf16: node.getEnd(), oldText, replacement: `${newName} as ${oldText}` };
279
+ }
280
+ }
281
+ return { startUtf16: node.getStart(source), endUtf16: node.getEnd(), oldText, replacement: newName };
282
+ }
283
+ function hasComputedName(node) {
284
+ for (let current = node; current; current = current.parent)
285
+ if (ts.isComputedPropertyName(current))
286
+ return true;
287
+ return false;
288
+ }
289
+ function nodeAt(node, position) {
290
+ let found;
291
+ node.forEachChild((child) => { if (position >= child.getFullStart() && position < child.getEnd())
292
+ found = nodeAt(child, position) ?? found; });
293
+ return found ?? (node.getStart(node.getSourceFile()) === position ? node : undefined);
294
+ }
295
+ function symbolAt(project, file, position) { return project.checker.getSymbolAtPosition(file, position); }
296
+ function unalias(project, symbol) { return symbol.flags & SymbolFlags.Alias ? project.checker.getAliasedSymbol(symbol) : symbol; }
297
+ function sameSymbol(project, left, right) {
298
+ const resolvedLeft = unalias(project, left);
299
+ const resolvedRight = unalias(project, right);
300
+ return resolvedLeft.id !== 0 && resolvedLeft.id === resolvedRight.id || resolvedLeft.declarations.some((leftDeclaration) => resolvedRight.declarations.some((rightDeclaration) => leftDeclaration.path === rightDeclaration.path && leftDeclaration.index === rightDeclaration.index));
301
+ }
302
+ function address(site) { return `${site.file}:${site.line}:${site.col}`; }
303
+ function renderSignature(project, declaration, type, name, kind) {
304
+ if (declaration && isFunctionLikeDeclaration(declaration)) {
305
+ const signature = project.checker.getSignatureFromDeclaration(declaration);
306
+ if (signature) {
307
+ const generated = project.checker.signatureToSignatureDeclaration(signature, SyntaxKind.FunctionDeclaration);
308
+ if (generated) {
309
+ const text = project.emitter.printNode(generated).replace(/;$/, '');
310
+ if (text.startsWith('function '))
311
+ return `function ${name}${text.slice('function'.length).trimStart()}`;
312
+ }
313
+ }
314
+ }
315
+ const value = project.checker.typeToString(type, declaration);
316
+ if (kind === 'interface' || kind === 'class' || kind === 'type' || kind === 'enum')
317
+ return `${kind} ${name}`;
318
+ return value;
319
+ }
320
+ function typeDefinitions(project, root, declaration, fallback) {
321
+ if (!declaration)
322
+ return [fallback];
323
+ const nodes = annotationNodes(declaration);
324
+ const definitions = nodes.flatMap((node) => {
325
+ const type = project.checker.getTypeFromTypeNode(node);
326
+ return type ? declarationAddresses(project, root, type.getSymbol()?.declarations ?? []) : [];
327
+ });
328
+ return definitions.length > 0 ? uniqueStrings(definitions) : [fallback];
329
+ }
330
+ function annotationNodes(declaration) {
331
+ const typed = declaration;
332
+ return [...(typed.parameters ?? []).flatMap((parameter) => parameter.type && isTypeNode(parameter.type) ? [parameter.type] : []), ...(typed.type && isTypeNode(typed.type) ? [typed.type] : [])];
333
+ }
334
+ function declarationAddresses(project, root, declarations) {
335
+ return declarations.flatMap((handle) => {
336
+ const declaration = handle.resolve(project);
337
+ if (!declaration)
338
+ return [];
339
+ const source = declaration.getSourceFile();
340
+ if (!inside(root, source.fileName))
341
+ return [];
342
+ const name = declaration.name ?? declaration;
343
+ const location = source.getLineAndCharacterOfPosition(name.getStart(source));
344
+ return [`${relative(root, source.fileName)}:${location.line + 1}:${location.character + 1}`];
345
+ });
346
+ }
347
+ function modifiers(declaration) {
348
+ const values = declaration.modifiers?.map((modifier) => modifier.getText(declaration.getSourceFile())).filter((modifier) => modifier !== 'async').join(' ');
349
+ return values || undefined;
350
+ }
351
+ function normalizeDiagnostics(root, project, diagnostics) {
352
+ return diagnostics.flatMap((diagnostic) => {
353
+ if (!diagnostic.fileName || !inside(root, diagnostic.fileName))
354
+ return [];
355
+ const source = project.program.getSourceFile(diagnostic.fileName);
356
+ if (!source)
357
+ return [];
358
+ const location = source.getLineAndCharacterOfPosition(Math.max(0, diagnostic.pos));
359
+ return [{
360
+ file: relative(root, diagnostic.fileName),
361
+ line: location.line + 1,
362
+ column: location.character + 1,
363
+ severity: severity(diagnostic.category),
364
+ code: diagnostic.code,
365
+ message: diagnostic.text,
366
+ }];
367
+ });
368
+ }
369
+ function severity(category) {
370
+ if (category === DiagnosticCategory.Warning)
371
+ return 'warning';
372
+ if (category === DiagnosticCategory.Error)
373
+ return 'error';
374
+ if (category === DiagnosticCategory.Suggestion)
375
+ return 'suggestion';
376
+ return 'message';
377
+ }
378
+ function unique(values) {
379
+ return [...new Map(values.map((diagnostic) => [`${diagnostic.file}:${diagnostic.line}:${diagnostic.column}:${diagnostic.code}:${diagnostic.severity}:${diagnostic.message}`, diagnostic])).values()];
380
+ }
381
+ function uniqueStrings(values) { return [...new Set(values)]; }
382
+ function inside(root, file) { const value = path.relative(root, file); return value === '' || !value.startsWith(`..${path.sep}`) && value !== '..' && !path.isAbsolute(value); }
383
+ function relative(root, file) { return path.relative(root, file).split(path.sep).join('/'); }
@@ -0,0 +1,27 @@
1
+ import { Ts7Host } from './api.js';
2
+ export declare const skippedDirectories: ReadonlySet<string>;
3
+ export declare function isProgramFileCandidate(file: string): boolean;
4
+ export interface ConfiguredProject {
5
+ config: string;
6
+ files: ReadonlySet<string>;
7
+ bootstrap: string | null;
8
+ }
9
+ /** Discovers configured projects and applies the first-config ownership rule. */
10
+ export declare class ConfiguredProjects {
11
+ readonly root: string;
12
+ readonly program: Ts7Host;
13
+ private loaded;
14
+ private projects;
15
+ constructor(root: string, program: Ts7Host);
16
+ load(): readonly ConfiguredProject[];
17
+ /** Re-discovers configured projects after a workspace shape change. */
18
+ reload(): readonly ConfiguredProject[];
19
+ membership(file: string): ConfiguredProject | null;
20
+ refreshFile(file: string): void;
21
+ sourceFile(file: string): import("typescript/unstable/ast").SourceFile | undefined;
22
+ anySourceFile(file: string): import("typescript/unstable/ast").SourceFile | undefined;
23
+ records(): readonly ConfiguredProject[];
24
+ }
25
+ /** Every non-skipped directory in the workspace, including the root. */
26
+ export declare function discoverDirectories(root: string): string[];
27
+ export declare function discoverConfigs(root: string): string[];
@@ -0,0 +1,94 @@
1
+ import { existsSync, readdirSync, statSync } from 'node:fs';
2
+ import path from 'node:path';
3
+ const sourceExtensions = new Set(['.ts', '.tsx', '.mts', '.cts', '.js', '.jsx', '.mjs', '.cjs']);
4
+ export const skippedDirectories = new Set(['node_modules', '.git', '.crouter', 'dist', 'build', 'coverage']);
5
+ export function isProgramFileCandidate(file) {
6
+ const basename = path.basename(file);
7
+ return sourceExtensions.has(path.extname(file)) || basename === 'tsconfig.json' || basename === 'package.json';
8
+ }
9
+ /** Discovers configured projects and applies the first-config ownership rule. */
10
+ export class ConfiguredProjects {
11
+ root;
12
+ program;
13
+ loaded = false;
14
+ projects = new Map();
15
+ constructor(root, program) {
16
+ this.root = root;
17
+ this.program = program;
18
+ }
19
+ load() {
20
+ if (this.loaded)
21
+ return [...this.projects.values()];
22
+ const configs = discoverConfigs(this.root);
23
+ const projects = this.program.load(configs);
24
+ for (const config of configs) {
25
+ const project = projects.find((candidate) => path.resolve(candidate.configFileName) === config);
26
+ if (!project)
27
+ continue;
28
+ const files = new Set(project.rootFiles.map((file) => path.resolve(file)).filter((file) => workspaceSourceFile(this.root, file)));
29
+ if (files.size === 0)
30
+ continue;
31
+ this.projects.set(config, { config, files, bootstrap: [...files].find((file) => sourceExtensions.has(path.extname(file))) ?? null });
32
+ }
33
+ this.loaded = true;
34
+ if (this.projects.size === 0)
35
+ throw new Error(`No configured TypeScript project exists under ${this.root}.`);
36
+ return [...this.projects.values()];
37
+ }
38
+ /** Re-discovers configured projects after a workspace shape change. */
39
+ reload() {
40
+ this.loaded = false;
41
+ this.projects.clear();
42
+ return this.load();
43
+ }
44
+ membership(file) {
45
+ this.load();
46
+ const absolute = path.resolve(file);
47
+ for (const project of this.projects.values())
48
+ if (project.files.has(absolute))
49
+ return project;
50
+ return null;
51
+ }
52
+ refreshFile(file) { this.program.update({ fileChanges: { changed: [path.resolve(file)] } }); }
53
+ sourceFile(file) {
54
+ const project = this.membership(file);
55
+ return project ? this.program.sourceFile(project.config, path.resolve(file)) : undefined;
56
+ }
57
+ anySourceFile(file) { return this.program.sourceFile(undefined, path.resolve(file)); }
58
+ records() { return [...this.projects.values()]; }
59
+ }
60
+ /** Every non-skipped directory in the workspace, including the root. */
61
+ export function discoverDirectories(root) {
62
+ const directories = [path.resolve(root)];
63
+ const visit = (dir) => {
64
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
65
+ if (!entry.isDirectory() || skippedDirectories.has(entry.name))
66
+ continue;
67
+ const full = path.resolve(dir, entry.name);
68
+ directories.push(full);
69
+ visit(full);
70
+ }
71
+ };
72
+ visit(root);
73
+ return directories;
74
+ }
75
+ export function discoverConfigs(root) {
76
+ const configs = discoverDirectories(root)
77
+ .map((directory) => path.join(directory, 'tsconfig.json'))
78
+ .filter((config) => statOrNull(config)?.isFile() === true);
79
+ return configs.sort((left, right) => path.relative(root, left).localeCompare(path.relative(root, right)));
80
+ }
81
+ function statOrNull(file) {
82
+ try {
83
+ return statSync(file);
84
+ }
85
+ catch {
86
+ return null;
87
+ }
88
+ }
89
+ function workspaceSourceFile(root, file) {
90
+ if (!existsSync(file) || !sourceExtensions.has(path.extname(file)))
91
+ return false;
92
+ const relative = path.relative(root, file);
93
+ return relative !== '' && !relative.startsWith(`..${path.sep}`) && !path.isAbsolute(relative) && !relative.split(path.sep).some((part) => skippedDirectories.has(part));
94
+ }
package/package.json ADDED
@@ -0,0 +1,47 @@
1
+ {
2
+ "name": "@crouton-kit/tsym",
3
+ "version": "0.1.0",
4
+ "description": "Symbol-level reading of a TypeScript codebase for agents.",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "bin": {
8
+ "tsym": "./bin/tsym.js"
9
+ },
10
+ "main": "./dist/index.js",
11
+ "types": "./dist/index.d.ts",
12
+ "exports": {
13
+ ".": {
14
+ "types": "./dist/index.d.ts",
15
+ "default": "./dist/index.js"
16
+ }
17
+ },
18
+ "files": [
19
+ "bin",
20
+ "dist"
21
+ ],
22
+ "publishConfig": {
23
+ "access": "public"
24
+ },
25
+ "scripts": {
26
+ "build": "rm -rf dist && node node_modules/ts5/bin/tsc",
27
+ "prepack": "npm run build",
28
+ "typecheck": "node node_modules/ts5/bin/tsc --noEmit",
29
+ "test": "node --test --import tsx test/*.test.ts",
30
+ "dev": "tsx src/cli.ts",
31
+ "validate:crouter": "tsx scripts/validate-crouter.ts",
32
+ "validate:queries": "tsx scripts/validate-queries.ts"
33
+ },
34
+ "engines": {
35
+ "node": ">=22.13"
36
+ },
37
+ "dependencies": {
38
+ "@ast-grep/napi": "^0.45.3",
39
+ "@ladybugdb/core": "0.20.4",
40
+ "ts5": "npm:typescript@5.9.3",
41
+ "typescript": "7.0.2"
42
+ },
43
+ "devDependencies": {
44
+ "@types/node": "^22.0.0",
45
+ "tsx": "^4.19.0"
46
+ }
47
+ }