@optave/codegraph 3.8.1 → 3.9.1
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 +12 -7
- package/dist/ast-analysis/engine.d.ts.map +1 -1
- package/dist/ast-analysis/engine.js +121 -48
- package/dist/ast-analysis/engine.js.map +1 -1
- package/dist/ast-analysis/visitors/ast-store-visitor.d.ts.map +1 -1
- package/dist/ast-analysis/visitors/ast-store-visitor.js +15 -18
- package/dist/ast-analysis/visitors/ast-store-visitor.js.map +1 -1
- package/dist/ast-analysis/visitors/complexity-visitor.d.ts.map +1 -1
- package/dist/ast-analysis/visitors/complexity-visitor.js +50 -1
- package/dist/ast-analysis/visitors/complexity-visitor.js.map +1 -1
- package/dist/cli/commands/branch-compare.d.ts.map +1 -1
- package/dist/cli/commands/branch-compare.js +4 -0
- package/dist/cli/commands/branch-compare.js.map +1 -1
- package/dist/cli/commands/diff-impact.d.ts.map +1 -1
- package/dist/cli/commands/diff-impact.js +2 -1
- package/dist/cli/commands/diff-impact.js.map +1 -1
- package/dist/cli/commands/info.d.ts.map +1 -1
- package/dist/cli/commands/info.js +3 -2
- package/dist/cli/commands/info.js.map +1 -1
- package/dist/db/connection.d.ts +1 -0
- package/dist/db/connection.d.ts.map +1 -1
- package/dist/db/connection.js +22 -4
- package/dist/db/connection.js.map +1 -1
- package/dist/db/repository/base.d.ts +41 -0
- package/dist/db/repository/base.d.ts.map +1 -1
- package/dist/db/repository/base.js +22 -0
- package/dist/db/repository/base.js.map +1 -1
- package/dist/db/repository/index.d.ts +1 -0
- package/dist/db/repository/index.d.ts.map +1 -1
- package/dist/db/repository/index.js.map +1 -1
- package/dist/db/repository/native-repository.d.ts +8 -1
- package/dist/db/repository/native-repository.d.ts.map +1 -1
- package/dist/db/repository/native-repository.js +69 -1
- package/dist/db/repository/native-repository.js.map +1 -1
- package/dist/db/repository/sqlite-repository.d.ts +1 -0
- package/dist/db/repository/sqlite-repository.d.ts.map +1 -1
- package/dist/db/repository/sqlite-repository.js +25 -0
- package/dist/db/repository/sqlite-repository.js.map +1 -1
- package/dist/domain/analysis/dependencies.d.ts +1 -28
- package/dist/domain/analysis/dependencies.d.ts.map +1 -1
- package/dist/domain/analysis/dependencies.js +24 -8
- package/dist/domain/analysis/dependencies.js.map +1 -1
- package/dist/domain/graph/builder/incremental.d.ts.map +1 -1
- package/dist/domain/graph/builder/incremental.js +18 -0
- package/dist/domain/graph/builder/incremental.js.map +1 -1
- package/dist/domain/graph/builder/pipeline.d.ts.map +1 -1
- package/dist/domain/graph/builder/pipeline.js +298 -206
- package/dist/domain/graph/builder/pipeline.js.map +1 -1
- package/dist/domain/graph/builder/stages/build-edges.d.ts.map +1 -1
- package/dist/domain/graph/builder/stages/build-edges.js +56 -3
- package/dist/domain/graph/builder/stages/build-edges.js.map +1 -1
- package/dist/domain/graph/builder/stages/resolve-imports.d.ts.map +1 -1
- package/dist/domain/graph/builder/stages/resolve-imports.js +19 -23
- package/dist/domain/graph/builder/stages/resolve-imports.js.map +1 -1
- package/dist/domain/graph/watcher.d.ts.map +1 -1
- package/dist/domain/graph/watcher.js +99 -95
- package/dist/domain/graph/watcher.js.map +1 -1
- package/dist/domain/parser.d.ts +4 -0
- package/dist/domain/parser.d.ts.map +1 -1
- package/dist/domain/parser.js +130 -61
- package/dist/domain/parser.js.map +1 -1
- package/dist/domain/search/models.d.ts.map +1 -1
- package/dist/domain/search/models.js +7 -5
- package/dist/domain/search/models.js.map +1 -1
- package/dist/extractors/go.js +53 -35
- package/dist/extractors/go.js.map +1 -1
- package/dist/extractors/javascript.js +85 -36
- package/dist/extractors/javascript.js.map +1 -1
- package/dist/features/complexity.d.ts.map +1 -1
- package/dist/features/complexity.js +78 -58
- package/dist/features/complexity.js.map +1 -1
- package/dist/features/dataflow.d.ts.map +1 -1
- package/dist/features/dataflow.js +109 -118
- package/dist/features/dataflow.js.map +1 -1
- package/dist/features/structure.d.ts.map +1 -1
- package/dist/features/structure.js +147 -97
- package/dist/features/structure.js.map +1 -1
- package/dist/graph/algorithms/louvain.d.ts.map +1 -1
- package/dist/graph/algorithms/louvain.js +4 -2
- package/dist/graph/algorithms/louvain.js.map +1 -1
- package/dist/graph/classifiers/roles.d.ts +2 -0
- package/dist/graph/classifiers/roles.d.ts.map +1 -1
- package/dist/graph/classifiers/roles.js +13 -5
- package/dist/graph/classifiers/roles.js.map +1 -1
- package/dist/presentation/communities.d.ts.map +1 -1
- package/dist/presentation/communities.js +38 -34
- package/dist/presentation/communities.js.map +1 -1
- package/dist/presentation/manifesto.d.ts.map +1 -1
- package/dist/presentation/manifesto.js +31 -33
- package/dist/presentation/manifesto.js.map +1 -1
- package/dist/presentation/queries-cli/inspect.d.ts.map +1 -1
- package/dist/presentation/queries-cli/inspect.js +47 -46
- package/dist/presentation/queries-cli/inspect.js.map +1 -1
- package/dist/shared/file-utils.d.ts.map +1 -1
- package/dist/shared/file-utils.js +94 -72
- package/dist/shared/file-utils.js.map +1 -1
- package/dist/types.d.ts +83 -2
- package/dist/types.d.ts.map +1 -1
- package/grammars/tree-sitter-erlang.wasm +0 -0
- package/grammars/tree-sitter-gleam.wasm +0 -0
- package/package.json +9 -9
- package/src/ast-analysis/engine.ts +150 -55
- package/src/ast-analysis/visitors/ast-store-visitor.ts +19 -21
- package/src/ast-analysis/visitors/complexity-visitor.ts +55 -1
- package/src/cli/commands/branch-compare.ts +4 -0
- package/src/cli/commands/diff-impact.ts +2 -1
- package/src/cli/commands/info.ts +3 -2
- package/src/db/connection.ts +24 -5
- package/src/db/repository/base.ts +57 -0
- package/src/db/repository/index.ts +1 -0
- package/src/db/repository/native-repository.ts +92 -1
- package/src/db/repository/sqlite-repository.ts +26 -0
- package/src/domain/analysis/dependencies.ts +24 -6
- package/src/domain/graph/builder/incremental.ts +21 -0
- package/src/domain/graph/builder/pipeline.ts +396 -245
- package/src/domain/graph/builder/stages/build-edges.ts +53 -2
- package/src/domain/graph/builder/stages/resolve-imports.ts +20 -20
- package/src/domain/graph/watcher.ts +118 -98
- package/src/domain/parser.ts +131 -63
- package/src/domain/search/models.ts +11 -5
- package/src/extractors/go.ts +57 -32
- package/src/extractors/javascript.ts +88 -35
- package/src/features/complexity.ts +94 -58
- package/src/features/dataflow.ts +153 -132
- package/src/features/structure.ts +167 -95
- package/src/graph/algorithms/louvain.ts +5 -2
- package/src/graph/classifiers/roles.ts +14 -5
- package/src/presentation/communities.ts +44 -39
- package/src/presentation/manifesto.ts +35 -38
- package/src/presentation/queries-cli/inspect.ts +48 -46
- package/src/shared/file-utils.ts +116 -77
- package/src/types.ts +87 -1
|
@@ -119,6 +119,23 @@ function buildImportEdges(
|
|
|
119
119
|
: 'imports';
|
|
120
120
|
allEdgeRows.push([fileNodeId, targetRow.id, edgeKind, 1.0, 0]);
|
|
121
121
|
|
|
122
|
+
// Type-only imports: create symbol-level edges so the target symbols
|
|
123
|
+
// get fan-in credit and aren't falsely classified as dead code.
|
|
124
|
+
if (imp.typeOnly && ctx.nodesByNameAndFile) {
|
|
125
|
+
for (const name of imp.names) {
|
|
126
|
+
const cleanName = name.replace(/^\*\s+as\s+/, '');
|
|
127
|
+
let targetFile = resolvedPath;
|
|
128
|
+
if (isBarrelFile(ctx, resolvedPath)) {
|
|
129
|
+
const actual = resolveBarrelExport(ctx, resolvedPath, cleanName);
|
|
130
|
+
if (actual) targetFile = actual;
|
|
131
|
+
}
|
|
132
|
+
const candidates = ctx.nodesByNameAndFile.get(`${cleanName}|${targetFile}`);
|
|
133
|
+
if (candidates && candidates.length > 0) {
|
|
134
|
+
allEdgeRows.push([fileNodeId, candidates[0]!.id, 'imports-type', 1.0, 0]);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
122
139
|
if (!imp.reexport && isBarrelFile(ctx, resolvedPath)) {
|
|
123
140
|
buildBarrelEdges(ctx, imp, resolvedPath, fileNodeId, edgeKind, getNodeIdStmt, allEdgeRows);
|
|
124
141
|
}
|
|
@@ -280,7 +297,18 @@ function buildImportEdgesNative(
|
|
|
280
297
|
}
|
|
281
298
|
}
|
|
282
299
|
|
|
283
|
-
// 6.
|
|
300
|
+
// 6. Build symbol node entries for type-only import resolution
|
|
301
|
+
const symbolNodes: Array<{ name: string; file: string; nodeId: number }> = [];
|
|
302
|
+
if (ctx.nodesByNameAndFile) {
|
|
303
|
+
for (const [key, nodes] of ctx.nodesByNameAndFile) {
|
|
304
|
+
if (nodes.length > 0) {
|
|
305
|
+
const [name, file] = key.split('|');
|
|
306
|
+
symbolNodes.push({ name: name!, file: file!, nodeId: nodes[0]!.id });
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// 7. Call native
|
|
284
312
|
const nativeEdges = native.buildImportEdges!(
|
|
285
313
|
files,
|
|
286
314
|
resolvedImports,
|
|
@@ -288,6 +316,7 @@ function buildImportEdgesNative(
|
|
|
288
316
|
fileNodeIds,
|
|
289
317
|
barrelFiles,
|
|
290
318
|
rootDir,
|
|
319
|
+
symbolNodes,
|
|
291
320
|
) as NativeEdge[];
|
|
292
321
|
|
|
293
322
|
for (const e of nativeEdges) {
|
|
@@ -354,7 +383,10 @@ function buildImportedNamesForNative(
|
|
|
354
383
|
rootDir: string,
|
|
355
384
|
): Array<{ name: string; file: string }> {
|
|
356
385
|
const importedNames: Array<{ name: string; file: string }> = [];
|
|
357
|
-
|
|
386
|
+
// Process dynamic imports first (lower priority), then static imports
|
|
387
|
+
// (higher priority). Rust HashMap::collect keeps the last entry per key,
|
|
388
|
+
// so static imports win when both contribute the same name.
|
|
389
|
+
const addImports = (imp: (typeof symbols.imports)[number]) => {
|
|
358
390
|
const resolvedPath = getResolved(ctx, path.join(rootDir, relPath), imp.source);
|
|
359
391
|
for (const name of imp.names) {
|
|
360
392
|
const cleanName = name.replace(/^\*\s+as\s+/, '');
|
|
@@ -365,6 +397,12 @@ function buildImportedNamesForNative(
|
|
|
365
397
|
}
|
|
366
398
|
importedNames.push({ name: cleanName, file: targetFile });
|
|
367
399
|
}
|
|
400
|
+
};
|
|
401
|
+
for (const imp of symbols.imports) {
|
|
402
|
+
if (imp.dynamicImport) addImports(imp);
|
|
403
|
+
}
|
|
404
|
+
for (const imp of symbols.imports) {
|
|
405
|
+
if (!imp.dynamicImport) addImports(imp);
|
|
368
406
|
}
|
|
369
407
|
return importedNames;
|
|
370
408
|
}
|
|
@@ -409,12 +447,25 @@ function buildImportedNamesMap(
|
|
|
409
447
|
rootDir: string,
|
|
410
448
|
): Map<string, string> {
|
|
411
449
|
const importedNames = new Map<string, string>();
|
|
450
|
+
// Process dynamic imports first (lower priority), then static imports
|
|
451
|
+
// (higher priority). Static imports represent direct bindings while dynamic
|
|
452
|
+
// imports often use aliased destructuring (`{ foo: bar } = await import(…)`).
|
|
453
|
+
// When both contribute the same name, the static binding is authoritative.
|
|
412
454
|
for (const imp of symbols.imports) {
|
|
455
|
+
if (!imp.dynamicImport) continue;
|
|
413
456
|
const resolvedPath = getResolved(ctx, path.join(rootDir, relPath), imp.source);
|
|
414
457
|
for (const name of imp.names) {
|
|
415
458
|
importedNames.set(name.replace(/^\*\s+as\s+/, ''), resolvedPath);
|
|
416
459
|
}
|
|
417
460
|
}
|
|
461
|
+
for (const imp of symbols.imports) {
|
|
462
|
+
if (imp.dynamicImport) continue;
|
|
463
|
+
const resolvedPath = getResolved(ctx, path.join(rootDir, relPath), imp.source);
|
|
464
|
+
for (const name of imp.names) {
|
|
465
|
+
const cleanName = name.replace(/^\*\s+as\s+/, '');
|
|
466
|
+
importedNames.set(cleanName, resolvedPath);
|
|
467
|
+
}
|
|
468
|
+
}
|
|
418
469
|
return importedNames;
|
|
419
470
|
}
|
|
420
471
|
|
|
@@ -180,6 +180,13 @@ export function isBarrelFile(ctx: PipelineContext, relPath: string): boolean {
|
|
|
180
180
|
return reexports.length >= ownDefs;
|
|
181
181
|
}
|
|
182
182
|
|
|
183
|
+
/** Check if a re-export source directly defines the symbol. */
|
|
184
|
+
function sourceDefinesSymbol(ctx: PipelineContext, source: string, symbolName: string): boolean {
|
|
185
|
+
const targetSymbols = ctx.fileSymbols.get(source);
|
|
186
|
+
if (!targetSymbols) return false;
|
|
187
|
+
return targetSymbols.definitions.some((d) => d.name === symbolName);
|
|
188
|
+
}
|
|
189
|
+
|
|
183
190
|
export function resolveBarrelExport(
|
|
184
191
|
ctx: PipelineContext,
|
|
185
192
|
barrelPath: string,
|
|
@@ -188,31 +195,24 @@ export function resolveBarrelExport(
|
|
|
188
195
|
): string | null {
|
|
189
196
|
if (visited.has(barrelPath)) return null;
|
|
190
197
|
visited.add(barrelPath);
|
|
198
|
+
|
|
191
199
|
const reexports = ctx.reexportMap.get(barrelPath) as ReexportEntry[] | undefined;
|
|
192
200
|
if (!reexports) return null;
|
|
201
|
+
|
|
193
202
|
for (const re of reexports) {
|
|
203
|
+
// Named re-export: only follow if the symbol is in the export list
|
|
194
204
|
if (re.names.length > 0 && !re.wildcardReexport) {
|
|
195
|
-
if (re.names.includes(symbolName))
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
if (hasDef) return re.source;
|
|
200
|
-
const deeper = resolveBarrelExport(ctx, re.source, symbolName, visited);
|
|
201
|
-
if (deeper) return deeper;
|
|
202
|
-
}
|
|
203
|
-
return re.source;
|
|
204
|
-
}
|
|
205
|
-
continue;
|
|
206
|
-
}
|
|
207
|
-
if (re.wildcardReexport || re.names.length === 0) {
|
|
208
|
-
const targetSymbols = ctx.fileSymbols.get(re.source);
|
|
209
|
-
if (targetSymbols) {
|
|
210
|
-
const hasDef = targetSymbols.definitions.some((d) => d.name === symbolName);
|
|
211
|
-
if (hasDef) return re.source;
|
|
212
|
-
const deeper = resolveBarrelExport(ctx, re.source, symbolName, visited);
|
|
213
|
-
if (deeper) return deeper;
|
|
214
|
-
}
|
|
205
|
+
if (!re.names.includes(symbolName)) continue;
|
|
206
|
+
if (sourceDefinesSymbol(ctx, re.source, symbolName)) return re.source;
|
|
207
|
+
const deeper = resolveBarrelExport(ctx, re.source, symbolName, visited);
|
|
208
|
+
return deeper ?? re.source;
|
|
215
209
|
}
|
|
210
|
+
|
|
211
|
+
// Wildcard or namespace re-export: check if target defines the symbol
|
|
212
|
+
if (sourceDefinesSymbol(ctx, re.source, symbolName)) return re.source;
|
|
213
|
+
const deeper = resolveBarrelExport(ctx, re.source, symbolName, visited);
|
|
214
|
+
if (deeper) return deeper;
|
|
216
215
|
}
|
|
216
|
+
|
|
217
217
|
return null;
|
|
218
218
|
}
|
|
@@ -2,20 +2,16 @@ import fs from 'node:fs';
|
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { closeDb, getNodeId as getNodeIdQuery, initSchema, openDb } from '../../db/index.js';
|
|
4
4
|
import { debug, info } from '../../infrastructure/logger.js';
|
|
5
|
-
import {
|
|
5
|
+
import { isSupportedFile, normalizePath, shouldIgnore } from '../../shared/constants.js';
|
|
6
6
|
import { DbError } from '../../shared/errors.js';
|
|
7
7
|
import { createParseTreeCache, getActiveEngine } from '../parser.js';
|
|
8
8
|
import { type IncrementalStmts, rebuildFile } from './builder/incremental.js';
|
|
9
9
|
import { appendChangeEvents, buildChangeEvent, diffSymbols } from './change-journal.js';
|
|
10
10
|
import { appendJournalEntries } from './journal.js';
|
|
11
11
|
|
|
12
|
-
function
|
|
12
|
+
function shouldIgnorePath(filePath: string): boolean {
|
|
13
13
|
const parts = filePath.split(path.sep);
|
|
14
|
-
return parts.some((p) =>
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
function isTrackedExt(filePath: string): boolean {
|
|
18
|
-
return EXTENSIONS.has(path.extname(filePath));
|
|
14
|
+
return parts.some((p) => shouldIgnore(p));
|
|
19
15
|
}
|
|
20
16
|
|
|
21
17
|
/** Prepare all SQL statements needed by the watcher's incremental rebuild. */
|
|
@@ -141,24 +137,35 @@ function collectTrackedFiles(dir: string, result: string[]): void {
|
|
|
141
137
|
let entries: fs.Dirent[];
|
|
142
138
|
try {
|
|
143
139
|
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
144
|
-
} catch {
|
|
140
|
+
} catch (e: unknown) {
|
|
141
|
+
debug(`collectTrackedFiles: cannot read ${dir}: ${(e as Error).message}`);
|
|
145
142
|
return;
|
|
146
143
|
}
|
|
147
144
|
for (const entry of entries) {
|
|
148
|
-
if (
|
|
145
|
+
if (shouldIgnore(entry.name)) continue;
|
|
149
146
|
const full = path.join(dir, entry.name);
|
|
150
147
|
if (entry.isDirectory()) {
|
|
151
148
|
collectTrackedFiles(full, result);
|
|
152
|
-
} else if (
|
|
149
|
+
} else if (isSupportedFile(entry.name)) {
|
|
153
150
|
result.push(full);
|
|
154
151
|
}
|
|
155
152
|
}
|
|
156
153
|
}
|
|
157
154
|
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
155
|
+
/** Shared watcher state passed between setup and watcher sub-functions. */
|
|
156
|
+
interface WatcherContext {
|
|
157
|
+
rootDir: string;
|
|
158
|
+
db: ReturnType<typeof openDb>;
|
|
159
|
+
stmts: IncrementalStmts;
|
|
160
|
+
engineOpts: import('../../types.js').EngineOpts;
|
|
161
|
+
cache: ReturnType<typeof createParseTreeCache>;
|
|
162
|
+
pending: Set<string>;
|
|
163
|
+
timer: ReturnType<typeof setTimeout> | null;
|
|
164
|
+
debounceMs: number;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/** Initialize DB, engine, cache, and statements for watch mode. */
|
|
168
|
+
function setupWatcher(rootDir: string, opts: { engine?: string }): WatcherContext {
|
|
162
169
|
const dbPath = path.join(rootDir, '.codegraph', 'graph.db');
|
|
163
170
|
if (!fs.existsSync(dbPath)) {
|
|
164
171
|
throw new DbError('No graph.db found. Run `codegraph build` first.', { file: dbPath });
|
|
@@ -183,111 +190,124 @@ export async function watchProject(
|
|
|
183
190
|
|
|
184
191
|
const stmts = prepareWatcherStatements(db);
|
|
185
192
|
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
193
|
+
return {
|
|
194
|
+
rootDir,
|
|
195
|
+
db,
|
|
196
|
+
stmts,
|
|
197
|
+
engineOpts,
|
|
198
|
+
cache,
|
|
199
|
+
pending: new Set<string>(),
|
|
200
|
+
timer: null,
|
|
201
|
+
debounceMs: 300,
|
|
202
|
+
};
|
|
203
|
+
}
|
|
192
204
|
|
|
193
|
-
|
|
194
|
-
|
|
205
|
+
/** Schedule debounced processing of pending files. */
|
|
206
|
+
function scheduleDebouncedProcess(ctx: WatcherContext): void {
|
|
207
|
+
if (ctx.timer) clearTimeout(ctx.timer);
|
|
208
|
+
ctx.timer = setTimeout(async () => {
|
|
209
|
+
const files = [...ctx.pending];
|
|
210
|
+
ctx.pending.clear();
|
|
211
|
+
await processPendingFiles(files, ctx.db, ctx.rootDir, ctx.stmts, ctx.engineOpts, ctx.cache);
|
|
212
|
+
}, ctx.debounceMs);
|
|
213
|
+
}
|
|
195
214
|
|
|
196
|
-
|
|
215
|
+
/** Start polling-based file watcher. Returns cleanup function. */
|
|
216
|
+
function startPollingWatcher(ctx: WatcherContext, pollIntervalMs: number): () => void {
|
|
217
|
+
const mtimeMap = new Map<string, number>();
|
|
218
|
+
|
|
219
|
+
const initial: string[] = [];
|
|
220
|
+
collectTrackedFiles(ctx.rootDir, initial);
|
|
221
|
+
for (const f of initial) {
|
|
222
|
+
try {
|
|
223
|
+
mtimeMap.set(f, fs.statSync(f).mtimeMs);
|
|
224
|
+
} catch {
|
|
225
|
+
/* deleted between collect and stat */
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
info(`Polling ${initial.length} tracked files every ${pollIntervalMs}ms`);
|
|
197
229
|
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
const
|
|
230
|
+
const pollTimer = setInterval(() => {
|
|
231
|
+
const current: string[] = [];
|
|
232
|
+
collectTrackedFiles(ctx.rootDir, current);
|
|
233
|
+
const currentSet = new Set(current);
|
|
202
234
|
|
|
203
|
-
|
|
204
|
-
const initial: string[] = [];
|
|
205
|
-
collectTrackedFiles(rootDir, initial);
|
|
206
|
-
for (const f of initial) {
|
|
235
|
+
for (const f of current) {
|
|
207
236
|
try {
|
|
208
|
-
|
|
237
|
+
const mtime = fs.statSync(f).mtimeMs;
|
|
238
|
+
const prev = mtimeMap.get(f);
|
|
239
|
+
if (prev === undefined || mtime !== prev) {
|
|
240
|
+
mtimeMap.set(f, mtime);
|
|
241
|
+
ctx.pending.add(f);
|
|
242
|
+
}
|
|
209
243
|
} catch {
|
|
210
244
|
/* deleted between collect and stat */
|
|
211
245
|
}
|
|
212
246
|
}
|
|
213
|
-
info(`Polling ${initial.length} tracked files every ${POLL_INTERVAL_MS}ms`);
|
|
214
|
-
|
|
215
|
-
const pollTimer = setInterval(() => {
|
|
216
|
-
const current: string[] = [];
|
|
217
|
-
collectTrackedFiles(rootDir, current);
|
|
218
|
-
const currentSet = new Set(current);
|
|
219
|
-
|
|
220
|
-
// Detect modified or new files
|
|
221
|
-
for (const f of current) {
|
|
222
|
-
try {
|
|
223
|
-
const mtime = fs.statSync(f).mtimeMs;
|
|
224
|
-
const prev = mtimeMap.get(f);
|
|
225
|
-
if (prev === undefined || mtime !== prev) {
|
|
226
|
-
mtimeMap.set(f, mtime);
|
|
227
|
-
pending.add(f);
|
|
228
|
-
}
|
|
229
|
-
} catch {
|
|
230
|
-
/* deleted between collect and stat */
|
|
231
|
-
}
|
|
232
|
-
}
|
|
233
247
|
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
pending.add(f);
|
|
239
|
-
}
|
|
248
|
+
for (const f of mtimeMap.keys()) {
|
|
249
|
+
if (!currentSet.has(f)) {
|
|
250
|
+
mtimeMap.delete(f);
|
|
251
|
+
ctx.pending.add(f);
|
|
240
252
|
}
|
|
253
|
+
}
|
|
241
254
|
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
const watcher = fs.watch(rootDir, { recursive: true }, (_eventType, filename) => {
|
|
257
|
-
if (!filename) return;
|
|
258
|
-
if (shouldIgnore(filename)) return;
|
|
259
|
-
if (!isTrackedExt(filename)) return;
|
|
260
|
-
|
|
261
|
-
const fullPath = path.join(rootDir, filename);
|
|
262
|
-
pending.add(fullPath);
|
|
263
|
-
|
|
264
|
-
if (timer) clearTimeout(timer);
|
|
265
|
-
timer = setTimeout(async () => {
|
|
266
|
-
const files = [...pending];
|
|
267
|
-
pending.clear();
|
|
268
|
-
await processPendingFiles(files, db, rootDir, stmts, engineOpts, cache);
|
|
269
|
-
}, DEBOUNCE_MS);
|
|
270
|
-
});
|
|
271
|
-
|
|
272
|
-
cleanup = () => watcher.close();
|
|
273
|
-
}
|
|
255
|
+
if (ctx.pending.size > 0) {
|
|
256
|
+
scheduleDebouncedProcess(ctx);
|
|
257
|
+
}
|
|
258
|
+
}, pollIntervalMs);
|
|
259
|
+
|
|
260
|
+
return () => clearInterval(pollTimer);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/** Start native OS file watcher. Returns cleanup function. */
|
|
264
|
+
function startNativeWatcher(ctx: WatcherContext): () => void {
|
|
265
|
+
const watcher = fs.watch(ctx.rootDir, { recursive: true }, (_eventType, filename) => {
|
|
266
|
+
if (!filename) return;
|
|
267
|
+
if (shouldIgnorePath(filename)) return;
|
|
268
|
+
if (!isSupportedFile(filename)) return;
|
|
274
269
|
|
|
275
|
-
|
|
270
|
+
ctx.pending.add(path.join(ctx.rootDir, filename));
|
|
271
|
+
scheduleDebouncedProcess(ctx);
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
return () => watcher.close();
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/** Register SIGINT handler to flush journal and clean up. */
|
|
278
|
+
function setupShutdownHandler(ctx: WatcherContext, cleanup: () => void): void {
|
|
279
|
+
process.once('SIGINT', () => {
|
|
276
280
|
info('Stopping watcher...');
|
|
277
281
|
cleanup();
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
file: normalizePath(path.relative(rootDir, filePath)),
|
|
282
|
+
if (ctx.pending.size > 0) {
|
|
283
|
+
const entries = [...ctx.pending].map((filePath) => ({
|
|
284
|
+
file: normalizePath(path.relative(ctx.rootDir, filePath)),
|
|
282
285
|
}));
|
|
283
286
|
try {
|
|
284
|
-
appendJournalEntries(rootDir, entries);
|
|
287
|
+
appendJournalEntries(ctx.rootDir, entries);
|
|
285
288
|
} catch (e: unknown) {
|
|
286
289
|
debug(`Journal flush on exit failed (non-fatal): ${(e as Error).message}`);
|
|
287
290
|
}
|
|
288
291
|
}
|
|
289
|
-
if (cache) cache.clear();
|
|
290
|
-
closeDb(db);
|
|
292
|
+
if (ctx.cache) ctx.cache.clear();
|
|
293
|
+
closeDb(ctx.db);
|
|
291
294
|
process.exit(0);
|
|
292
295
|
});
|
|
293
296
|
}
|
|
297
|
+
|
|
298
|
+
export async function watchProject(
|
|
299
|
+
rootDir: string,
|
|
300
|
+
opts: { engine?: string; poll?: boolean; pollInterval?: number } = {},
|
|
301
|
+
): Promise<void> {
|
|
302
|
+
const ctx = setupWatcher(rootDir, opts);
|
|
303
|
+
|
|
304
|
+
const usePoll = opts.poll ?? process.platform === 'win32';
|
|
305
|
+
const pollIntervalMs = opts.pollInterval ?? 2000;
|
|
306
|
+
|
|
307
|
+
info(`Watching ${rootDir} for changes${usePoll ? ' (polling mode)' : ''}...`);
|
|
308
|
+
info('Press Ctrl+C to stop.');
|
|
309
|
+
|
|
310
|
+
const cleanup = usePoll ? startPollingWatcher(ctx, pollIntervalMs) : startNativeWatcher(ctx);
|
|
311
|
+
|
|
312
|
+
setupShutdownHandler(ctx, cleanup);
|
|
313
|
+
}
|