typegraph-mcp 0.9.1 → 0.9.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/dist/config.js ADDED
@@ -0,0 +1,13 @@
1
+ // config.ts
2
+ import * as path from "path";
3
+ function resolveConfig(toolDir) {
4
+ const cwd = process.cwd();
5
+ const projectRoot = process.env["TYPEGRAPH_PROJECT_ROOT"] ? path.resolve(cwd, process.env["TYPEGRAPH_PROJECT_ROOT"]) : path.basename(path.dirname(toolDir)) === "plugins" ? path.resolve(toolDir, "../..") : cwd;
6
+ const tsconfigPath = process.env["TYPEGRAPH_TSCONFIG"] || "./tsconfig.json";
7
+ const toolIsEmbedded = toolDir.startsWith(projectRoot + path.sep);
8
+ const toolRelPath = toolIsEmbedded ? path.relative(projectRoot, toolDir) : toolDir;
9
+ return { projectRoot, tsconfigPath, toolDir, toolIsEmbedded, toolRelPath };
10
+ }
11
+ export {
12
+ resolveConfig
13
+ };
@@ -0,0 +1,279 @@
1
+ // graph-queries.ts
2
+ import * as fs from "fs";
3
+ import * as path from "path";
4
+ function shouldIncludeEdge(edge, includeTypeOnly) {
5
+ if (!includeTypeOnly && edge.isTypeOnly) return false;
6
+ return true;
7
+ }
8
+ function dependencyTree(graph, file, opts = {}) {
9
+ const { depth = Infinity, includeTypeOnly = false } = opts;
10
+ const visited = /* @__PURE__ */ new Set();
11
+ const result = [];
12
+ let frontier = [file];
13
+ visited.add(file);
14
+ let currentDepth = 0;
15
+ while (frontier.length > 0 && currentDepth < depth) {
16
+ const nextFrontier = [];
17
+ for (const f of frontier) {
18
+ const edges = graph.forward.get(f) ?? [];
19
+ for (const edge of edges) {
20
+ if (!shouldIncludeEdge(edge, includeTypeOnly)) continue;
21
+ if (visited.has(edge.target)) continue;
22
+ visited.add(edge.target);
23
+ result.push(edge.target);
24
+ nextFrontier.push(edge.target);
25
+ }
26
+ }
27
+ frontier = nextFrontier;
28
+ currentDepth++;
29
+ }
30
+ return { root: file, nodes: result.length, files: result };
31
+ }
32
+ var packageNameCache = /* @__PURE__ */ new Map();
33
+ function findPackageName(filePath) {
34
+ let dir = path.dirname(filePath);
35
+ while (dir !== path.dirname(dir)) {
36
+ if (packageNameCache.has(dir)) return packageNameCache.get(dir);
37
+ const pkgJsonPath = path.join(dir, "package.json");
38
+ try {
39
+ if (fs.existsSync(pkgJsonPath)) {
40
+ const pkg = JSON.parse(fs.readFileSync(pkgJsonPath, "utf-8"));
41
+ const name = pkg.name ?? path.basename(dir);
42
+ packageNameCache.set(dir, name);
43
+ return name;
44
+ }
45
+ } catch {
46
+ }
47
+ dir = path.dirname(dir);
48
+ }
49
+ return "<root>";
50
+ }
51
+ function dependents(graph, file, opts = {}) {
52
+ const { depth = Infinity, includeTypeOnly = false } = opts;
53
+ const visited = /* @__PURE__ */ new Set();
54
+ const result = [];
55
+ let directCount = 0;
56
+ let frontier = [file];
57
+ visited.add(file);
58
+ let currentDepth = 0;
59
+ while (frontier.length > 0 && currentDepth < depth) {
60
+ const nextFrontier = [];
61
+ for (const f of frontier) {
62
+ const edges = graph.reverse.get(f) ?? [];
63
+ for (const edge of edges) {
64
+ if (!shouldIncludeEdge(edge, includeTypeOnly)) continue;
65
+ if (visited.has(edge.target)) continue;
66
+ visited.add(edge.target);
67
+ result.push(edge.target);
68
+ if (currentDepth === 0) directCount++;
69
+ nextFrontier.push(edge.target);
70
+ }
71
+ }
72
+ frontier = nextFrontier;
73
+ currentDepth++;
74
+ }
75
+ const byPackage = {};
76
+ for (const f of result) {
77
+ const pkgName = findPackageName(f);
78
+ if (!byPackage[pkgName]) byPackage[pkgName] = [];
79
+ byPackage[pkgName].push(f);
80
+ }
81
+ return { root: file, nodes: result.length, directCount, files: result, byPackage };
82
+ }
83
+ function importCycles(graph, opts = {}) {
84
+ const { file, package: pkgDir } = opts;
85
+ let index = 0;
86
+ const stack = [];
87
+ const onStack = /* @__PURE__ */ new Set();
88
+ const indices = /* @__PURE__ */ new Map();
89
+ const lowlinks = /* @__PURE__ */ new Map();
90
+ const sccs = [];
91
+ function strongconnect(v) {
92
+ indices.set(v, index);
93
+ lowlinks.set(v, index);
94
+ index++;
95
+ stack.push(v);
96
+ onStack.add(v);
97
+ const edges = graph.forward.get(v) ?? [];
98
+ for (const edge of edges) {
99
+ const w = edge.target;
100
+ if (!graph.files.has(w)) continue;
101
+ if (!indices.has(w)) {
102
+ strongconnect(w);
103
+ lowlinks.set(v, Math.min(lowlinks.get(v), lowlinks.get(w)));
104
+ } else if (onStack.has(w)) {
105
+ lowlinks.set(v, Math.min(lowlinks.get(v), indices.get(w)));
106
+ }
107
+ }
108
+ if (lowlinks.get(v) === indices.get(v)) {
109
+ const scc = [];
110
+ let w;
111
+ do {
112
+ w = stack.pop();
113
+ onStack.delete(w);
114
+ scc.push(w);
115
+ } while (w !== v);
116
+ if (scc.length > 1) {
117
+ sccs.push(scc);
118
+ }
119
+ }
120
+ }
121
+ for (const f of graph.files) {
122
+ if (!indices.has(f)) {
123
+ strongconnect(f);
124
+ }
125
+ }
126
+ let cycles = sccs;
127
+ if (file) {
128
+ cycles = cycles.filter((scc) => scc.includes(file));
129
+ }
130
+ if (pkgDir) {
131
+ const absPkgDir = path.resolve(pkgDir);
132
+ cycles = cycles.filter(
133
+ (scc) => scc.every((f) => f.startsWith(absPkgDir))
134
+ );
135
+ }
136
+ return { count: cycles.length, cycles };
137
+ }
138
+ function shortestPath(graph, from, to, opts = {}) {
139
+ const { includeTypeOnly = false } = opts;
140
+ if (from === to) {
141
+ return { path: [from], hops: 0, chain: [{ file: from, imports: [] }] };
142
+ }
143
+ const visited = /* @__PURE__ */ new Set();
144
+ const parent = /* @__PURE__ */ new Map();
145
+ visited.add(from);
146
+ let frontier = [from];
147
+ while (frontier.length > 0) {
148
+ const nextFrontier = [];
149
+ for (const f of frontier) {
150
+ const edges = graph.forward.get(f) ?? [];
151
+ for (const edge of edges) {
152
+ if (!shouldIncludeEdge(edge, includeTypeOnly)) continue;
153
+ if (visited.has(edge.target)) continue;
154
+ visited.add(edge.target);
155
+ parent.set(edge.target, { from: f, specifiers: edge.specifiers });
156
+ if (edge.target === to) {
157
+ const filePath = [to];
158
+ let current = to;
159
+ while (parent.has(current)) {
160
+ current = parent.get(current).from;
161
+ filePath.unshift(current);
162
+ }
163
+ const chain = [];
164
+ for (let i = 0; i < filePath.length; i++) {
165
+ const p = parent.get(filePath[i]);
166
+ chain.push({
167
+ file: filePath[i],
168
+ imports: p?.specifiers ?? []
169
+ });
170
+ }
171
+ return { path: filePath, hops: filePath.length - 1, chain };
172
+ }
173
+ nextFrontier.push(edge.target);
174
+ }
175
+ }
176
+ frontier = nextFrontier;
177
+ }
178
+ return { path: null, hops: -1, chain: [] };
179
+ }
180
+ function subgraph(graph, files, opts = {}) {
181
+ const { depth = 1, direction = "both" } = opts;
182
+ const visited = new Set(files);
183
+ let frontier = [...files];
184
+ for (let d = 0; d < depth && frontier.length > 0; d++) {
185
+ const nextFrontier = [];
186
+ for (const f of frontier) {
187
+ if (direction === "imports" || direction === "both") {
188
+ for (const edge of graph.forward.get(f) ?? []) {
189
+ if (!visited.has(edge.target)) {
190
+ visited.add(edge.target);
191
+ nextFrontier.push(edge.target);
192
+ }
193
+ }
194
+ }
195
+ if (direction === "dependents" || direction === "both") {
196
+ for (const edge of graph.reverse.get(f) ?? []) {
197
+ if (!visited.has(edge.target)) {
198
+ visited.add(edge.target);
199
+ nextFrontier.push(edge.target);
200
+ }
201
+ }
202
+ }
203
+ }
204
+ frontier = nextFrontier;
205
+ }
206
+ const nodes = [...visited];
207
+ const edges = [];
208
+ for (const f of nodes) {
209
+ for (const edge of graph.forward.get(f) ?? []) {
210
+ if (visited.has(edge.target)) {
211
+ edges.push({
212
+ from: f,
213
+ to: edge.target,
214
+ specifiers: edge.specifiers,
215
+ isTypeOnly: edge.isTypeOnly
216
+ });
217
+ }
218
+ }
219
+ }
220
+ return { nodes, edges, stats: { nodeCount: nodes.length, edgeCount: edges.length } };
221
+ }
222
+ function moduleBoundary(graph, files) {
223
+ const fileSet = new Set(files);
224
+ let internalEdges = 0;
225
+ const incomingEdges = [];
226
+ const outgoingEdges = [];
227
+ const outgoingTargets = /* @__PURE__ */ new Set();
228
+ for (const f of files) {
229
+ for (const edge of graph.forward.get(f) ?? []) {
230
+ if (fileSet.has(edge.target)) {
231
+ internalEdges++;
232
+ } else {
233
+ outgoingEdges.push({
234
+ from: f,
235
+ to: edge.target,
236
+ specifiers: edge.specifiers
237
+ });
238
+ outgoingTargets.add(edge.target);
239
+ }
240
+ }
241
+ }
242
+ for (const f of files) {
243
+ for (const edge of graph.reverse.get(f) ?? []) {
244
+ if (!fileSet.has(edge.target)) {
245
+ incomingEdges.push({
246
+ from: edge.target,
247
+ to: f,
248
+ specifiers: edge.specifiers
249
+ });
250
+ }
251
+ }
252
+ }
253
+ const depCounts = /* @__PURE__ */ new Map();
254
+ for (const f of files) {
255
+ for (const edge of graph.forward.get(f) ?? []) {
256
+ if (!fileSet.has(edge.target)) {
257
+ depCounts.set(edge.target, (depCounts.get(edge.target) ?? 0) + 1);
258
+ }
259
+ }
260
+ }
261
+ const sharedDependencies = [...depCounts.entries()].filter(([, count]) => count > 1).map(([dep]) => dep);
262
+ const total = internalEdges + incomingEdges.length + outgoingEdges.length;
263
+ const isolationScore = total === 0 ? 1 : internalEdges / total;
264
+ return {
265
+ internalEdges,
266
+ incomingEdges,
267
+ outgoingEdges,
268
+ sharedDependencies,
269
+ isolationScore
270
+ };
271
+ }
272
+ export {
273
+ dependencyTree,
274
+ dependents,
275
+ importCycles,
276
+ moduleBoundary,
277
+ shortestPath,
278
+ subgraph
279
+ };
@@ -0,0 +1,336 @@
1
+ // module-graph.ts
2
+ import { parseSync } from "oxc-parser";
3
+ import { ResolverFactory } from "oxc-resolver";
4
+ import * as fs from "fs";
5
+ import * as path from "path";
6
+ var log = (...args) => console.error("[typegraph/graph]", ...args);
7
+ var TS_EXTENSIONS = /* @__PURE__ */ new Set([".ts", ".tsx", ".mts", ".cts"]);
8
+ var SKIP_DIRS = /* @__PURE__ */ new Set([
9
+ "node_modules",
10
+ "dist",
11
+ "build",
12
+ "out",
13
+ ".wrangler",
14
+ ".mf",
15
+ ".git",
16
+ ".next",
17
+ ".turbo",
18
+ "coverage"
19
+ ]);
20
+ var SKIP_FILES = /* @__PURE__ */ new Set(["routeTree.gen.ts"]);
21
+ function discoverFiles(rootDir) {
22
+ const files = [];
23
+ function walk(dir) {
24
+ let entries;
25
+ try {
26
+ entries = fs.readdirSync(dir, { withFileTypes: true });
27
+ } catch {
28
+ return;
29
+ }
30
+ for (const entry of entries) {
31
+ if (entry.isDirectory()) {
32
+ if (SKIP_DIRS.has(entry.name)) continue;
33
+ if (entry.name.startsWith(".") && dir !== rootDir) continue;
34
+ walk(path.join(dir, entry.name));
35
+ } else if (entry.isFile()) {
36
+ const name = entry.name;
37
+ if (SKIP_FILES.has(name)) continue;
38
+ if (name.endsWith(".d.ts") || name.endsWith(".d.mts") || name.endsWith(".d.cts")) continue;
39
+ const ext = path.extname(name);
40
+ if (TS_EXTENSIONS.has(ext)) {
41
+ files.push(path.join(dir, name));
42
+ }
43
+ }
44
+ }
45
+ }
46
+ walk(rootDir);
47
+ return files;
48
+ }
49
+ function parseFileImports(filePath, source) {
50
+ const result = parseSync(filePath, source);
51
+ const imports = [];
52
+ for (const imp of result.module.staticImports) {
53
+ const specifier = imp.moduleRequest.value;
54
+ const names = [];
55
+ let allTypeOnly = true;
56
+ for (const entry of imp.entries) {
57
+ const kind = entry.importName.kind;
58
+ const name = kind === "Default" ? "default" : kind === "All" || kind === "AllButDefault" || kind === "NamespaceObject" ? "*" : entry.importName.name ?? entry.localName.value;
59
+ names.push(name);
60
+ if (!entry.isType) allTypeOnly = false;
61
+ }
62
+ if (names.length === 0) {
63
+ imports.push({ specifier, names: ["*"], isTypeOnly: false, isDynamic: false });
64
+ } else {
65
+ imports.push({ specifier, names, isTypeOnly: allTypeOnly, isDynamic: false });
66
+ }
67
+ }
68
+ for (const exp of result.module.staticExports) {
69
+ for (const entry of exp.entries) {
70
+ const moduleRequest = entry.moduleRequest;
71
+ if (!moduleRequest) continue;
72
+ const specifier = moduleRequest.value;
73
+ const entryKind = entry.importName.kind;
74
+ const name = entryKind === "AllButDefault" || entryKind === "All" || entryKind === "NamespaceObject" ? "*" : entry.importName.name ?? "*";
75
+ const existing = imports.find((i) => i.specifier === specifier && !i.isDynamic);
76
+ if (existing) {
77
+ if (!existing.names.includes(name)) existing.names.push(name);
78
+ } else {
79
+ imports.push({ specifier, names: [name], isTypeOnly: false, isDynamic: false });
80
+ }
81
+ }
82
+ }
83
+ for (const di of result.module.dynamicImports) {
84
+ if (di.moduleRequest) {
85
+ const sliced = source.slice(di.moduleRequest.start, di.moduleRequest.end);
86
+ if (sliced.startsWith("'") || sliced.startsWith('"')) {
87
+ const specifier = sliced.slice(1, -1);
88
+ imports.push({ specifier, names: ["*"], isTypeOnly: false, isDynamic: true });
89
+ }
90
+ }
91
+ }
92
+ return imports;
93
+ }
94
+ var SOURCE_EXTS = [".ts", ".tsx", ".mts", ".cts"];
95
+ function distToSource(resolvedPath, projectRoot) {
96
+ if (!resolvedPath.startsWith(projectRoot)) return resolvedPath;
97
+ const rel = path.relative(projectRoot, resolvedPath);
98
+ const distIdx = rel.indexOf("dist" + path.sep);
99
+ if (distIdx === -1) return resolvedPath;
100
+ const prefix = rel.slice(0, distIdx);
101
+ const afterDist = rel.slice(distIdx + 5);
102
+ const withoutExt = afterDist.replace(/\.(m?j|c)s$/, "");
103
+ for (const ext of SOURCE_EXTS) {
104
+ const candidate = path.resolve(projectRoot, prefix, "src", withoutExt + ext);
105
+ if (fs.existsSync(candidate)) return candidate;
106
+ }
107
+ for (const ext of SOURCE_EXTS) {
108
+ const candidate = path.resolve(projectRoot, prefix, withoutExt + ext);
109
+ if (fs.existsSync(candidate)) return candidate;
110
+ }
111
+ if (withoutExt.endsWith("/index")) {
112
+ const dirPath = withoutExt.slice(0, -6);
113
+ for (const ext of SOURCE_EXTS) {
114
+ const candidate = path.resolve(projectRoot, prefix, "src", dirPath + ext);
115
+ if (fs.existsSync(candidate)) return candidate;
116
+ }
117
+ }
118
+ return resolvedPath;
119
+ }
120
+ function resolveImport(resolver, fromDir, specifier, projectRoot) {
121
+ try {
122
+ const result = resolver.sync(fromDir, specifier);
123
+ if (result.path && !result.path.includes("node_modules")) {
124
+ const mapped = distToSource(result.path, projectRoot);
125
+ const ext = path.extname(mapped);
126
+ if (!TS_EXTENSIONS.has(ext)) return null;
127
+ if (SKIP_FILES.has(path.basename(mapped))) return null;
128
+ return mapped;
129
+ }
130
+ } catch {
131
+ }
132
+ return null;
133
+ }
134
+ function createResolver(projectRoot, tsconfigPath) {
135
+ return new ResolverFactory({
136
+ tsconfig: {
137
+ configFile: path.resolve(projectRoot, tsconfigPath),
138
+ references: "auto"
139
+ },
140
+ extensions: [".ts", ".tsx", ".mts", ".cts", ".js", ".jsx", ".mjs", ".cjs"],
141
+ extensionAlias: {
142
+ ".js": [".ts", ".tsx", ".js"],
143
+ ".jsx": [".tsx", ".jsx"],
144
+ ".mjs": [".mts", ".mjs"],
145
+ ".cjs": [".cts", ".cjs"]
146
+ },
147
+ conditionNames: ["import", "require"],
148
+ mainFields: ["module", "main"]
149
+ });
150
+ }
151
+ function buildForwardEdges(files, resolver, projectRoot) {
152
+ const forward = /* @__PURE__ */ new Map();
153
+ const parseFailures = [];
154
+ for (const filePath of files) {
155
+ let source;
156
+ try {
157
+ source = fs.readFileSync(filePath, "utf-8");
158
+ } catch {
159
+ continue;
160
+ }
161
+ let rawImports;
162
+ try {
163
+ rawImports = parseFileImports(filePath, source);
164
+ } catch (err) {
165
+ parseFailures.push(filePath);
166
+ continue;
167
+ }
168
+ const edges = [];
169
+ const fromDir = path.dirname(filePath);
170
+ for (const raw of rawImports) {
171
+ const target = resolveImport(resolver, fromDir, raw.specifier, projectRoot);
172
+ if (target) {
173
+ edges.push({
174
+ target,
175
+ specifiers: raw.names,
176
+ isTypeOnly: raw.isTypeOnly,
177
+ isDynamic: raw.isDynamic
178
+ });
179
+ }
180
+ }
181
+ forward.set(filePath, edges);
182
+ }
183
+ return { forward, parseFailures };
184
+ }
185
+ function buildReverseMap(forward) {
186
+ const reverse = /* @__PURE__ */ new Map();
187
+ for (const [source, edges] of forward) {
188
+ for (const edge of edges) {
189
+ let revEdges = reverse.get(edge.target);
190
+ if (!revEdges) {
191
+ revEdges = [];
192
+ reverse.set(edge.target, revEdges);
193
+ }
194
+ revEdges.push({
195
+ target: source,
196
+ // reverse: the "target" is the file that imports
197
+ specifiers: edge.specifiers,
198
+ isTypeOnly: edge.isTypeOnly,
199
+ isDynamic: edge.isDynamic
200
+ });
201
+ }
202
+ }
203
+ return reverse;
204
+ }
205
+ async function buildGraph(projectRoot, tsconfigPath) {
206
+ const startTime = performance.now();
207
+ const resolver = createResolver(projectRoot, tsconfigPath);
208
+ const fileList = discoverFiles(projectRoot);
209
+ log(`Discovered ${fileList.length} source files`);
210
+ const { forward, parseFailures } = buildForwardEdges(fileList, resolver, projectRoot);
211
+ const reverse = buildReverseMap(forward);
212
+ const files = new Set(fileList);
213
+ const edgeCount = [...forward.values()].reduce((sum, edges) => sum + edges.length, 0);
214
+ const elapsed = (performance.now() - startTime).toFixed(0);
215
+ log(`Graph built: ${files.size} files, ${edgeCount} edges [${elapsed}ms]`);
216
+ if (parseFailures.length > 0) {
217
+ log(`Parse failures: ${parseFailures.length} files`);
218
+ }
219
+ return {
220
+ graph: { forward, reverse, files },
221
+ resolver
222
+ };
223
+ }
224
+ function updateFile(graph, filePath, resolver, projectRoot) {
225
+ const oldEdges = graph.forward.get(filePath) ?? [];
226
+ for (const edge of oldEdges) {
227
+ const revEdges = graph.reverse.get(edge.target);
228
+ if (revEdges) {
229
+ const idx = revEdges.findIndex((r) => r.target === filePath);
230
+ if (idx !== -1) revEdges.splice(idx, 1);
231
+ if (revEdges.length === 0) graph.reverse.delete(edge.target);
232
+ }
233
+ }
234
+ let source;
235
+ try {
236
+ source = fs.readFileSync(filePath, "utf-8");
237
+ } catch {
238
+ removeFile(graph, filePath);
239
+ return;
240
+ }
241
+ let rawImports;
242
+ try {
243
+ rawImports = parseFileImports(filePath, source);
244
+ } catch {
245
+ log(`Parse error on update: ${filePath}`);
246
+ graph.forward.set(filePath, []);
247
+ return;
248
+ }
249
+ const fromDir = path.dirname(filePath);
250
+ const newEdges = [];
251
+ for (const raw of rawImports) {
252
+ const target = resolveImport(resolver, fromDir, raw.specifier, projectRoot);
253
+ if (target) {
254
+ newEdges.push({
255
+ target,
256
+ specifiers: raw.names,
257
+ isTypeOnly: raw.isTypeOnly,
258
+ isDynamic: raw.isDynamic
259
+ });
260
+ }
261
+ }
262
+ graph.forward.set(filePath, newEdges);
263
+ graph.files.add(filePath);
264
+ for (const edge of newEdges) {
265
+ let revEdges = graph.reverse.get(edge.target);
266
+ if (!revEdges) {
267
+ revEdges = [];
268
+ graph.reverse.set(edge.target, revEdges);
269
+ }
270
+ revEdges.push({
271
+ target: filePath,
272
+ specifiers: edge.specifiers,
273
+ isTypeOnly: edge.isTypeOnly,
274
+ isDynamic: edge.isDynamic
275
+ });
276
+ }
277
+ }
278
+ function removeFile(graph, filePath) {
279
+ const edges = graph.forward.get(filePath) ?? [];
280
+ for (const edge of edges) {
281
+ const revEdges2 = graph.reverse.get(edge.target);
282
+ if (revEdges2) {
283
+ const idx = revEdges2.findIndex((r) => r.target === filePath);
284
+ if (idx !== -1) revEdges2.splice(idx, 1);
285
+ if (revEdges2.length === 0) graph.reverse.delete(edge.target);
286
+ }
287
+ }
288
+ const revEdges = graph.reverse.get(filePath) ?? [];
289
+ for (const revEdge of revEdges) {
290
+ const fwdEdges = graph.forward.get(revEdge.target);
291
+ if (fwdEdges) {
292
+ const idx = fwdEdges.findIndex((e) => e.target === filePath);
293
+ if (idx !== -1) fwdEdges.splice(idx, 1);
294
+ }
295
+ }
296
+ graph.forward.delete(filePath);
297
+ graph.reverse.delete(filePath);
298
+ graph.files.delete(filePath);
299
+ }
300
+ function startWatcher(projectRoot, graph, resolver) {
301
+ try {
302
+ const watcher = fs.watch(
303
+ projectRoot,
304
+ { recursive: true },
305
+ (_eventType, filename) => {
306
+ if (!filename) return;
307
+ const ext = path.extname(filename);
308
+ if (!TS_EXTENSIONS.has(ext)) return;
309
+ const parts = filename.split(path.sep);
310
+ if (parts.some((p) => SKIP_DIRS.has(p))) return;
311
+ if (SKIP_FILES.has(path.basename(filename))) return;
312
+ if (filename.endsWith(".d.ts") || filename.endsWith(".d.mts") || filename.endsWith(".d.cts"))
313
+ return;
314
+ const absPath = path.resolve(projectRoot, filename);
315
+ if (fs.existsSync(absPath)) {
316
+ updateFile(graph, absPath, resolver, projectRoot);
317
+ } else {
318
+ removeFile(graph, absPath);
319
+ }
320
+ }
321
+ );
322
+ process.on("SIGINT", () => watcher.close());
323
+ process.on("SIGTERM", () => watcher.close());
324
+ log("File watcher started");
325
+ } catch (err) {
326
+ log("Failed to start file watcher:", err);
327
+ }
328
+ }
329
+ export {
330
+ buildGraph,
331
+ createResolver,
332
+ discoverFiles,
333
+ removeFile,
334
+ startWatcher,
335
+ updateFile
336
+ };