@grafema/core 0.2.10 → 0.2.12-beta
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/Orchestrator.d.ts +13 -0
- package/dist/Orchestrator.d.ts.map +1 -1
- package/dist/Orchestrator.js +84 -2
- package/dist/Orchestrator.js.map +1 -1
- package/dist/ParallelAnalysisRunner.d.ts.map +1 -1
- package/dist/ParallelAnalysisRunner.js +28 -41
- package/dist/ParallelAnalysisRunner.js.map +1 -1
- package/dist/PhaseRunner.d.ts +2 -0
- package/dist/PhaseRunner.d.ts.map +1 -1
- package/dist/PhaseRunner.js +8 -3
- package/dist/PhaseRunner.js.map +1 -1
- package/dist/core/IncrementalReanalyzer.d.ts +3 -3
- package/dist/core/IncrementalReanalyzer.d.ts.map +1 -1
- package/dist/core/IncrementalReanalyzer.js +12 -12
- package/dist/core/IncrementalReanalyzer.js.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/plugins/analysis/JSASTAnalyzer.d.ts.map +1 -1
- package/dist/plugins/analysis/JSASTAnalyzer.js +35 -3
- package/dist/plugins/analysis/JSASTAnalyzer.js.map +1 -1
- package/dist/plugins/analysis/ast/GraphBuilder.d.ts.map +1 -1
- package/dist/plugins/analysis/ast/GraphBuilder.js +8 -0
- package/dist/plugins/analysis/ast/GraphBuilder.js.map +1 -1
- package/dist/plugins/analysis/ast/builders/ModuleRuntimeBuilder.d.ts.map +1 -1
- package/dist/plugins/analysis/ast/builders/ModuleRuntimeBuilder.js +4 -2
- package/dist/plugins/analysis/ast/builders/ModuleRuntimeBuilder.js.map +1 -1
- package/dist/plugins/analysis/ast/handlers/NewExpressionHandler.d.ts.map +1 -1
- package/dist/plugins/analysis/ast/handlers/NewExpressionHandler.js +2 -1
- package/dist/plugins/analysis/ast/handlers/NewExpressionHandler.js.map +1 -1
- package/dist/plugins/analysis/ast/types.d.ts +3 -0
- package/dist/plugins/analysis/ast/types.d.ts.map +1 -1
- package/dist/plugins/analysis/ast/visitors/ImportExportVisitor.d.ts.map +1 -1
- package/dist/plugins/analysis/ast/visitors/ImportExportVisitor.js +6 -2
- package/dist/plugins/analysis/ast/visitors/ImportExportVisitor.js.map +1 -1
- package/dist/plugins/enrichment/ImportExportLinker.d.ts.map +1 -1
- package/dist/plugins/enrichment/ImportExportLinker.js +15 -5
- package/dist/plugins/enrichment/ImportExportLinker.js.map +1 -1
- package/dist/storage/backends/RFDBServerBackend.d.ts +21 -7
- package/dist/storage/backends/RFDBServerBackend.d.ts.map +1 -1
- package/dist/storage/backends/RFDBServerBackend.js +48 -48
- package/dist/storage/backends/RFDBServerBackend.js.map +1 -1
- package/dist/utils/startRfdbServer.d.ts +44 -0
- package/dist/utils/startRfdbServer.d.ts.map +1 -0
- package/dist/utils/startRfdbServer.js +79 -0
- package/dist/utils/startRfdbServer.js.map +1 -0
- package/package.json +3 -3
- package/src/Orchestrator.ts +91 -2
- package/src/ParallelAnalysisRunner.ts +30 -48
- package/src/PhaseRunner.ts +10 -3
- package/src/core/IncrementalReanalyzer.ts +15 -15
- package/src/index.ts +4 -0
- package/src/plugins/analysis/JSASTAnalyzer.ts +40 -3
- package/src/plugins/analysis/ast/GraphBuilder.ts +9 -0
- package/src/plugins/analysis/ast/builders/ModuleRuntimeBuilder.ts +4 -2
- package/src/plugins/analysis/ast/handlers/NewExpressionHandler.ts +2 -1
- package/src/plugins/analysis/ast/types.ts +3 -0
- package/src/plugins/analysis/ast/visitors/ImportExportVisitor.ts +8 -2
- package/src/plugins/enrichment/ImportExportLinker.ts +15 -5
- package/src/storage/backends/RFDBServerBackend.ts +52 -60
- package/src/utils/startRfdbServer.ts +126 -0
|
@@ -9,17 +9,17 @@
|
|
|
9
9
|
* - Barrier: wait for all tasks before returning
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
|
-
import { existsSync
|
|
12
|
+
import { existsSync } from 'fs';
|
|
13
13
|
import { join, dirname } from 'path';
|
|
14
|
-
import { fileURLToPath } from 'url';
|
|
15
14
|
import type { ChildProcess } from 'child_process';
|
|
16
|
-
import { spawn, execSync } from 'child_process';
|
|
17
15
|
import { setTimeout as sleep } from 'timers/promises';
|
|
18
16
|
import { AnalysisQueue } from './core/AnalysisQueue.js';
|
|
19
17
|
import type { Plugin } from './plugins/Plugin.js';
|
|
20
18
|
import type { GraphBackend, Logger } from '@grafema/types';
|
|
21
19
|
import type { ProgressCallback } from './PhaseRunner.js';
|
|
22
20
|
import type { ParallelConfig, DiscoveryManifest } from './OrchestratorTypes.js';
|
|
21
|
+
import { findRfdbBinary } from './utils/findRfdbBinary.js';
|
|
22
|
+
import { startRfdbServer } from './utils/startRfdbServer.js';
|
|
23
23
|
|
|
24
24
|
export class ParallelAnalysisRunner {
|
|
25
25
|
private analysisQueue: AnalysisQueue | null = null;
|
|
@@ -43,10 +43,9 @@ export class ParallelAnalysisRunner {
|
|
|
43
43
|
* - Barrier waits for all tasks before ENRICHMENT phase
|
|
44
44
|
*/
|
|
45
45
|
async run(manifest: DiscoveryManifest): Promise<void> {
|
|
46
|
-
const socketPath = this.parallelConfig.socketPath || '/tmp/rfdb.sock';
|
|
47
|
-
const maxWorkers = this.parallelConfig.maxWorkers || null;
|
|
48
|
-
|
|
49
46
|
const mainDbPath = (this.graph as unknown as { dbPath?: string }).dbPath || join(manifest.projectPath, '.grafema', 'graph.rfdb');
|
|
47
|
+
const socketPath = this.parallelConfig.socketPath || join(dirname(mainDbPath), 'rfdb.sock');
|
|
48
|
+
const maxWorkers = this.parallelConfig.maxWorkers || null;
|
|
50
49
|
|
|
51
50
|
this.logger.debug('Starting queue-based parallel analysis', { database: mainDbPath });
|
|
52
51
|
|
|
@@ -67,6 +66,20 @@ export class ParallelAnalysisRunner {
|
|
|
67
66
|
await this.analysisQueue.start();
|
|
68
67
|
|
|
69
68
|
let moduleCount = 0;
|
|
69
|
+
let completedCount = 0;
|
|
70
|
+
|
|
71
|
+
this.analysisQueue.on('taskCompleted', ({ file, stats, duration }: { file: string; stats?: { nodes?: number }; duration: number }) => {
|
|
72
|
+
completedCount++;
|
|
73
|
+
this.onProgress({
|
|
74
|
+
phase: 'analysis',
|
|
75
|
+
currentPlugin: 'AnalysisQueue',
|
|
76
|
+
message: `${file.split('/').pop()} (${stats?.nodes || 0} nodes, ${duration}ms)`,
|
|
77
|
+
totalFiles: moduleCount,
|
|
78
|
+
processedFiles: completedCount,
|
|
79
|
+
currentService: file,
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
|
|
70
83
|
for await (const node of this.graph.queryNodes({ type: 'MODULE' })) {
|
|
71
84
|
if (!node.file?.match(/\.(js|jsx|ts|tsx|mjs|cjs)$/)) continue;
|
|
72
85
|
|
|
@@ -81,14 +94,6 @@ export class ParallelAnalysisRunner {
|
|
|
81
94
|
|
|
82
95
|
this.logger.debug('Queued modules for analysis', { count: moduleCount });
|
|
83
96
|
|
|
84
|
-
this.analysisQueue.on('taskCompleted', ({ file, stats, duration }: { file: string; stats?: { nodes?: number }; duration: number }) => {
|
|
85
|
-
this.onProgress({
|
|
86
|
-
phase: 'analysis',
|
|
87
|
-
currentPlugin: 'AnalysisQueue',
|
|
88
|
-
message: `${file.split('/').pop()} (${stats?.nodes || 0} nodes, ${duration}ms)`,
|
|
89
|
-
});
|
|
90
|
-
});
|
|
91
|
-
|
|
92
97
|
this.analysisQueue.on('taskFailed', ({ file, error }: { file: string; error: string }) => {
|
|
93
98
|
this.logger.error('Analysis failed', { file, error });
|
|
94
99
|
});
|
|
@@ -111,6 +116,7 @@ export class ParallelAnalysisRunner {
|
|
|
111
116
|
* Start RFDB server process (or connect to existing one).
|
|
112
117
|
*/
|
|
113
118
|
private async startRfdbServer(socketPath: string, dbPath: string): Promise<void> {
|
|
119
|
+
// Check if server is already running
|
|
114
120
|
if (existsSync(socketPath)) {
|
|
115
121
|
try {
|
|
116
122
|
const { RFDBClient } = await import('@grafema/rfdb-client');
|
|
@@ -123,49 +129,25 @@ export class ParallelAnalysisRunner {
|
|
|
123
129
|
this._serverWasExternal = true;
|
|
124
130
|
return;
|
|
125
131
|
} catch {
|
|
126
|
-
this.logger.debug('Stale socket found,
|
|
127
|
-
unlinkSync(socketPath);
|
|
132
|
+
this.logger.debug('Stale socket found, will be removed by startRfdbServer');
|
|
128
133
|
}
|
|
129
134
|
}
|
|
130
135
|
|
|
131
|
-
const
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
let binaryPath = existsSync(serverBinary) ? serverBinary : debugBinary;
|
|
136
|
-
|
|
137
|
-
if (!existsSync(binaryPath)) {
|
|
138
|
-
this.logger.debug('RFDB server binary not found, building', { path: binaryPath });
|
|
139
|
-
execSync('cargo build --bin rfdb-server', {
|
|
140
|
-
cwd: join(projectRoot, 'packages/rfdb-server'),
|
|
141
|
-
stdio: 'inherit',
|
|
142
|
-
});
|
|
143
|
-
binaryPath = debugBinary;
|
|
136
|
+
const binaryPath = findRfdbBinary();
|
|
137
|
+
if (!binaryPath) {
|
|
138
|
+
throw new Error('RFDB server binary not found');
|
|
144
139
|
}
|
|
145
140
|
|
|
146
141
|
this.logger.debug('Starting RFDB server', { binary: binaryPath, database: dbPath });
|
|
147
|
-
this.rfdbServerProcess =
|
|
148
|
-
|
|
142
|
+
this.rfdbServerProcess = await startRfdbServer({
|
|
143
|
+
dbPath,
|
|
144
|
+
socketPath,
|
|
145
|
+
binaryPath,
|
|
146
|
+
waitTimeoutMs: 3000,
|
|
147
|
+
logger: { debug: (m: string) => this.logger.debug(m) },
|
|
149
148
|
});
|
|
150
149
|
this._serverWasExternal = false;
|
|
151
150
|
|
|
152
|
-
this.rfdbServerProcess.stderr?.on('data', (data: Buffer) => {
|
|
153
|
-
const msg = data.toString().trim();
|
|
154
|
-
if (!msg.includes('FLUSH') && !msg.includes('WRITER')) {
|
|
155
|
-
this.logger.debug('rfdb-server', { message: msg });
|
|
156
|
-
}
|
|
157
|
-
});
|
|
158
|
-
|
|
159
|
-
let attempts = 0;
|
|
160
|
-
while (!existsSync(socketPath) && attempts < 30) {
|
|
161
|
-
await sleep(100);
|
|
162
|
-
attempts++;
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
if (!existsSync(socketPath)) {
|
|
166
|
-
throw new Error('RFDB server failed to start');
|
|
167
|
-
}
|
|
168
|
-
|
|
169
151
|
this.logger.debug('RFDB server started', { socketPath });
|
|
170
152
|
}
|
|
171
153
|
|
package/src/PhaseRunner.ts
CHANGED
|
@@ -50,6 +50,8 @@ export interface PhaseRunnerDeps {
|
|
|
50
50
|
resourceRegistry: ResourceRegistry;
|
|
51
51
|
configServices?: ServiceDefinition[];
|
|
52
52
|
routing?: RoutingRule[];
|
|
53
|
+
/** When true, commitBatch defers index rebuild for bulk load (REG-487). */
|
|
54
|
+
deferIndexing?: boolean;
|
|
53
55
|
}
|
|
54
56
|
|
|
55
57
|
export class PhaseRunner {
|
|
@@ -77,8 +79,9 @@ export class PhaseRunner {
|
|
|
77
79
|
): Promise<{ result: PluginResult; delta: CommitDelta | null }> {
|
|
78
80
|
const graph = pluginContext.graph;
|
|
79
81
|
|
|
80
|
-
// Fallback: backend doesn't support batching
|
|
81
|
-
if (!graph.beginBatch || !graph.commitBatch || !graph.abortBatch
|
|
82
|
+
// Fallback: backend doesn't support batching, or plugin manages its own batches
|
|
83
|
+
if (!graph.beginBatch || !graph.commitBatch || !graph.abortBatch
|
|
84
|
+
|| plugin.metadata.managesBatch) {
|
|
82
85
|
const result = await plugin.execute(pluginContext);
|
|
83
86
|
return { result, delta: null };
|
|
84
87
|
}
|
|
@@ -91,7 +94,9 @@ export class PhaseRunner {
|
|
|
91
94
|
graph.beginBatch();
|
|
92
95
|
try {
|
|
93
96
|
const result = await plugin.execute(pluginContext);
|
|
94
|
-
const
|
|
97
|
+
const deferIndex = pluginContext.deferIndexing ?? false;
|
|
98
|
+
const protectedTypes = phaseName === 'ANALYSIS' ? ['MODULE'] : undefined;
|
|
99
|
+
const delta = await graph.commitBatch(tags, deferIndex, protectedTypes);
|
|
95
100
|
return { result, delta };
|
|
96
101
|
} catch (error) {
|
|
97
102
|
graph.abortBatch();
|
|
@@ -120,6 +125,8 @@ export class PhaseRunner {
|
|
|
120
125
|
rootPrefix: (baseContext as { rootPrefix?: string }).rootPrefix,
|
|
121
126
|
// REG-256: Pass resource registry for inter-plugin communication
|
|
122
127
|
resources: resourceRegistry,
|
|
128
|
+
// REG-487: Pass deferIndexing flag for bulk load optimization
|
|
129
|
+
deferIndexing: this.deps.deferIndexing ?? false,
|
|
123
130
|
};
|
|
124
131
|
|
|
125
132
|
// REG-256: Ensure config is available with routing and services for all plugins
|
|
@@ -25,9 +25,9 @@ export interface ReanalysisOptions {
|
|
|
25
25
|
|
|
26
26
|
export interface ReanalysisProgress {
|
|
27
27
|
phase: 'clearing' | 'indexing' | 'analysis' | 'enrichment';
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
28
|
+
processedFiles: number;
|
|
29
|
+
totalFiles: number;
|
|
30
|
+
currentService?: string;
|
|
31
31
|
}
|
|
32
32
|
|
|
33
33
|
export interface ReanalysisResult {
|
|
@@ -78,9 +78,9 @@ export class IncrementalReanalyzer {
|
|
|
78
78
|
if (options.onProgress) {
|
|
79
79
|
options.onProgress({
|
|
80
80
|
phase: 'clearing',
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
81
|
+
processedFiles: i + 1,
|
|
82
|
+
totalFiles: staleModules.length,
|
|
83
|
+
currentService: module.file
|
|
84
84
|
});
|
|
85
85
|
}
|
|
86
86
|
const cleared = await clearFileNodesIfNeeded(this.graph, module.file, touchedFiles);
|
|
@@ -98,9 +98,9 @@ export class IncrementalReanalyzer {
|
|
|
98
98
|
if (options.onProgress) {
|
|
99
99
|
options.onProgress({
|
|
100
100
|
phase: 'indexing',
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
101
|
+
processedFiles: i + 1,
|
|
102
|
+
totalFiles: modifiedModules.length,
|
|
103
|
+
currentService: module.file
|
|
104
104
|
});
|
|
105
105
|
}
|
|
106
106
|
|
|
@@ -127,9 +127,9 @@ export class IncrementalReanalyzer {
|
|
|
127
127
|
if (options.onProgress) {
|
|
128
128
|
options.onProgress({
|
|
129
129
|
phase: 'analysis',
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
130
|
+
processedFiles: i + 1,
|
|
131
|
+
totalFiles: modulesToAnalyze.length,
|
|
132
|
+
currentService: module.file
|
|
133
133
|
});
|
|
134
134
|
}
|
|
135
135
|
|
|
@@ -150,7 +150,7 @@ export class IncrementalReanalyzer {
|
|
|
150
150
|
// STEP 4: Re-run enrichment plugins
|
|
151
151
|
if (!options.skipEnrichment && modulesToAnalyze.length > 0) {
|
|
152
152
|
if (options.onProgress) {
|
|
153
|
-
options.onProgress({ phase: 'enrichment',
|
|
153
|
+
options.onProgress({ phase: 'enrichment', processedFiles: 0, totalFiles: 2 });
|
|
154
154
|
}
|
|
155
155
|
|
|
156
156
|
const pluginContext: PluginContext = {
|
|
@@ -169,7 +169,7 @@ export class IncrementalReanalyzer {
|
|
|
169
169
|
}
|
|
170
170
|
|
|
171
171
|
if (options.onProgress) {
|
|
172
|
-
options.onProgress({ phase: 'enrichment',
|
|
172
|
+
options.onProgress({ phase: 'enrichment', processedFiles: 1, totalFiles: 2 });
|
|
173
173
|
}
|
|
174
174
|
|
|
175
175
|
const importExportLinker = new ImportExportLinker();
|
|
@@ -182,7 +182,7 @@ export class IncrementalReanalyzer {
|
|
|
182
182
|
}
|
|
183
183
|
|
|
184
184
|
if (options.onProgress) {
|
|
185
|
-
options.onProgress({ phase: 'enrichment',
|
|
185
|
+
options.onProgress({ phase: 'enrichment', processedFiles: 2, totalFiles: 2 });
|
|
186
186
|
}
|
|
187
187
|
}
|
|
188
188
|
|
package/src/index.ts
CHANGED
|
@@ -129,6 +129,10 @@ export { calculateFileHash, calculateFileHashAsync, calculateContentHash } from
|
|
|
129
129
|
export { findRfdbBinary, getBinaryNotFoundMessage, getPlatformDir } from './utils/findRfdbBinary.js';
|
|
130
130
|
export type { FindBinaryOptions } from './utils/findRfdbBinary.js';
|
|
131
131
|
|
|
132
|
+
// RFDB server lifecycle (RFD-40)
|
|
133
|
+
export { startRfdbServer } from './utils/startRfdbServer.js';
|
|
134
|
+
export type { StartRfdbServerOptions } from './utils/startRfdbServer.js';
|
|
135
|
+
|
|
132
136
|
// Module resolution utilities (REG-320)
|
|
133
137
|
export {
|
|
134
138
|
resolveModulePath,
|
|
@@ -275,6 +275,7 @@ export class JSASTAnalyzer extends Plugin {
|
|
|
275
275
|
]
|
|
276
276
|
},
|
|
277
277
|
dependencies: ['JSModuleIndexer'],
|
|
278
|
+
managesBatch: true,
|
|
278
279
|
fields: [
|
|
279
280
|
{ name: 'object', fieldType: 'string', nodeTypes: ['CALL'] },
|
|
280
281
|
{ name: 'method', fieldType: 'string', nodeTypes: ['CALL'] },
|
|
@@ -382,7 +383,27 @@ export class JSASTAnalyzer extends Plugin {
|
|
|
382
383
|
const queue = new PriorityQueue();
|
|
383
384
|
const pool = new WorkerPool(context.workerCount || 10);
|
|
384
385
|
|
|
386
|
+
const deferIndex = context.deferIndexing ?? false;
|
|
387
|
+
|
|
385
388
|
pool.registerHandler('ANALYZE_MODULE', async (task) => {
|
|
389
|
+
// Per-module batch: commit after each module to avoid buffering the entire
|
|
390
|
+
// graph in memory. Prevents connection timeouts on large codebases.
|
|
391
|
+
// REG-487: Pass deferIndex to skip per-commit index rebuild during bulk load.
|
|
392
|
+
if (graph.beginBatch && graph.commitBatch) {
|
|
393
|
+
graph.beginBatch();
|
|
394
|
+
try {
|
|
395
|
+
const result = await this.analyzeModule(task.data.module, graph, projectPath);
|
|
396
|
+
await graph.commitBatch(
|
|
397
|
+
['JSASTAnalyzer', 'ANALYSIS', task.data.module.file],
|
|
398
|
+
deferIndex,
|
|
399
|
+
['MODULE'],
|
|
400
|
+
);
|
|
401
|
+
return result;
|
|
402
|
+
} catch (err) {
|
|
403
|
+
if (graph.abortBatch) graph.abortBatch();
|
|
404
|
+
throw err;
|
|
405
|
+
}
|
|
406
|
+
}
|
|
386
407
|
return await this.analyzeModule(task.data.module, graph, projectPath);
|
|
387
408
|
});
|
|
388
409
|
|
|
@@ -408,7 +429,8 @@ export class JSASTAnalyzer extends Plugin {
|
|
|
408
429
|
currentPlugin: 'JSASTAnalyzer',
|
|
409
430
|
message: `Analyzing ${currentFile} (${completed}/${modulesToAnalyze.length})`,
|
|
410
431
|
totalFiles: modulesToAnalyze.length,
|
|
411
|
-
processedFiles: completed
|
|
432
|
+
processedFiles: completed,
|
|
433
|
+
currentService: currentFile
|
|
412
434
|
});
|
|
413
435
|
}
|
|
414
436
|
}, 500);
|
|
@@ -429,6 +451,14 @@ export class JSASTAnalyzer extends Plugin {
|
|
|
429
451
|
|
|
430
452
|
clearInterval(progressInterval);
|
|
431
453
|
|
|
454
|
+
// REG-487: Rebuild indexes after all deferred commits.
|
|
455
|
+
// This runs inside JSASTAnalyzer.execute() so downstream ANALYSIS plugins
|
|
456
|
+
// (which depend on JSASTAnalyzer) see rebuilt indexes.
|
|
457
|
+
if (deferIndex && graph.rebuildIndexes) {
|
|
458
|
+
logger.info('Rebuilding indexes after deferred bulk load...');
|
|
459
|
+
await graph.rebuildIndexes();
|
|
460
|
+
}
|
|
461
|
+
|
|
432
462
|
if (context.onProgress) {
|
|
433
463
|
context.onProgress({
|
|
434
464
|
phase: 'analysis',
|
|
@@ -543,7 +573,8 @@ export class JSASTAnalyzer extends Plugin {
|
|
|
543
573
|
currentPlugin: 'JSASTAnalyzer',
|
|
544
574
|
message: `Processed ${result.module.name}`,
|
|
545
575
|
totalFiles: modules.length,
|
|
546
|
-
processedFiles: results.indexOf(result) + 1
|
|
576
|
+
processedFiles: results.indexOf(result) + 1,
|
|
577
|
+
currentService: result.module.file || result.module.name
|
|
547
578
|
});
|
|
548
579
|
}
|
|
549
580
|
}
|
|
@@ -1705,6 +1736,11 @@ export class JSASTAnalyzer extends Plugin {
|
|
|
1705
1736
|
if (processedConstructorCalls.has(nodeKey)) {
|
|
1706
1737
|
return;
|
|
1707
1738
|
}
|
|
1739
|
+
|
|
1740
|
+
// Skip in-function calls — handled by NewExpressionHandler in analyzeFunctionBody
|
|
1741
|
+
const functionParent = newPath.getFunctionParent();
|
|
1742
|
+
if (functionParent) return;
|
|
1743
|
+
|
|
1708
1744
|
processedConstructorCalls.add(nodeKey);
|
|
1709
1745
|
|
|
1710
1746
|
// Determine className from callee
|
|
@@ -1728,7 +1764,8 @@ export class JSASTAnalyzer extends Plugin {
|
|
|
1728
1764
|
isBuiltin,
|
|
1729
1765
|
file: module.file,
|
|
1730
1766
|
line,
|
|
1731
|
-
column
|
|
1767
|
+
column,
|
|
1768
|
+
parentScopeId: module.id
|
|
1732
1769
|
});
|
|
1733
1770
|
|
|
1734
1771
|
// REG-334: If this is Promise constructor with executor callback,
|
|
@@ -311,6 +311,15 @@ export class GraphBuilder {
|
|
|
311
311
|
line: constructorCall.line,
|
|
312
312
|
column: constructorCall.column
|
|
313
313
|
} as GraphNode);
|
|
314
|
+
|
|
315
|
+
// SCOPE -> CONTAINS -> CONSTRUCTOR_CALL
|
|
316
|
+
if (constructorCall.parentScopeId) {
|
|
317
|
+
this._bufferEdge({
|
|
318
|
+
type: 'CONTAINS',
|
|
319
|
+
src: constructorCall.parentScopeId,
|
|
320
|
+
dst: constructorCall.id
|
|
321
|
+
});
|
|
322
|
+
}
|
|
314
323
|
}
|
|
315
324
|
|
|
316
325
|
// Phase 2: Delegate to domain builders
|
|
@@ -52,7 +52,7 @@ export class ModuleRuntimeBuilder implements DomainBuilder {
|
|
|
52
52
|
|
|
53
53
|
private bufferImportNodes(module: ModuleNode, imports: ImportInfo[]): void {
|
|
54
54
|
for (const imp of imports) {
|
|
55
|
-
const { source, specifiers, line, column, isDynamic, isResolvable, dynamicPath } = imp;
|
|
55
|
+
const { source, specifiers, line, column, importKind, isDynamic, isResolvable, dynamicPath } = imp;
|
|
56
56
|
|
|
57
57
|
// REG-273: Handle side-effect-only imports (no specifiers)
|
|
58
58
|
if (specifiers.length === 0) {
|
|
@@ -66,7 +66,8 @@ export class ModuleRuntimeBuilder implements DomainBuilder {
|
|
|
66
66
|
{
|
|
67
67
|
imported: '*', // no specific export
|
|
68
68
|
local: source, // source becomes local
|
|
69
|
-
sideEffect: true
|
|
69
|
+
sideEffect: true, // mark as side-effect import
|
|
70
|
+
importBinding: importKind || 'value'
|
|
70
71
|
}
|
|
71
72
|
);
|
|
72
73
|
|
|
@@ -110,6 +111,7 @@ export class ModuleRuntimeBuilder implements DomainBuilder {
|
|
|
110
111
|
imported: spec.imported,
|
|
111
112
|
local: spec.local,
|
|
112
113
|
sideEffect: false, // regular imports are not side-effects
|
|
114
|
+
importBinding: spec.importKind === 'type' ? 'type' : (importKind || 'value'),
|
|
113
115
|
// importType is auto-detected from imported field
|
|
114
116
|
// Dynamic import fields
|
|
115
117
|
isDynamic,
|
|
@@ -338,6 +338,7 @@ export interface ConstructorCallInfo {
|
|
|
338
338
|
file: string;
|
|
339
339
|
line: number;
|
|
340
340
|
column: number;
|
|
341
|
+
parentScopeId?: string;
|
|
341
342
|
}
|
|
342
343
|
|
|
343
344
|
// === CLASS DECLARATION INFO ===
|
|
@@ -501,6 +502,7 @@ export interface ImportInfo {
|
|
|
501
502
|
line: number;
|
|
502
503
|
column?: number; // Column position for ImportNode
|
|
503
504
|
specifiers: ImportSpecifier[];
|
|
505
|
+
importKind?: 'value' | 'type' | 'typeof'; // TypeScript: import type { ... }
|
|
504
506
|
isDynamic?: boolean; // true for dynamic import() expressions
|
|
505
507
|
isResolvable?: boolean; // true if path is a string literal (statically analyzable)
|
|
506
508
|
dynamicPath?: string; // original expression for template/variable paths
|
|
@@ -509,6 +511,7 @@ export interface ImportInfo {
|
|
|
509
511
|
export interface ImportSpecifier {
|
|
510
512
|
imported: string; // имя в экспортируемом модуле (default, *, или имя)
|
|
511
513
|
local: string; // имя в текущем модуле
|
|
514
|
+
importKind?: 'value' | 'type' | 'typeof'; // specifier-level: import { type X } from '...'
|
|
512
515
|
}
|
|
513
516
|
|
|
514
517
|
// === EXPORT INFO ===
|
|
@@ -41,6 +41,7 @@ export type ExtractVariableNamesCallback = (pattern: Node) => VariableInfo[];
|
|
|
41
41
|
interface ImportSpecifierInfo {
|
|
42
42
|
imported: string;
|
|
43
43
|
local: string;
|
|
44
|
+
importKind?: 'value' | 'type' | 'typeof'; // specifier-level: import { type X } from '...'
|
|
44
45
|
}
|
|
45
46
|
|
|
46
47
|
/**
|
|
@@ -51,6 +52,7 @@ interface ImportInfo {
|
|
|
51
52
|
specifiers: ImportSpecifierInfo[];
|
|
52
53
|
line: number;
|
|
53
54
|
column?: number;
|
|
55
|
+
importKind?: 'value' | 'type' | 'typeof'; // TypeScript: import type { ... }
|
|
54
56
|
isDynamic?: boolean; // true for dynamic import() expressions
|
|
55
57
|
isResolvable?: boolean; // true if path is a string literal (statically analyzable)
|
|
56
58
|
dynamicPath?: string; // original expression for template/variable paths
|
|
@@ -106,13 +108,16 @@ export class ImportExportVisitor extends ASTVisitor {
|
|
|
106
108
|
node.specifiers.forEach((spec) => {
|
|
107
109
|
if (spec.type === 'ImportSpecifier') {
|
|
108
110
|
// import { foo, bar } from './module'
|
|
111
|
+
// import { type Foo, bar } from './module' (specifier-level type)
|
|
109
112
|
const importSpec = spec as ImportSpecifier;
|
|
110
113
|
const importedName = importSpec.imported.type === 'Identifier'
|
|
111
114
|
? importSpec.imported.name
|
|
112
115
|
: importSpec.imported.value;
|
|
116
|
+
const specKind = (importSpec as ImportSpecifier & { importKind?: string }).importKind;
|
|
113
117
|
specifiers.push({
|
|
114
118
|
imported: importedName,
|
|
115
|
-
local: importSpec.local.name
|
|
119
|
+
local: importSpec.local.name,
|
|
120
|
+
importKind: specKind as ImportSpecifierInfo['importKind']
|
|
116
121
|
});
|
|
117
122
|
} else if (spec.type === 'ImportDefaultSpecifier') {
|
|
118
123
|
// import foo from './module'
|
|
@@ -135,7 +140,8 @@ export class ImportExportVisitor extends ASTVisitor {
|
|
|
135
140
|
source,
|
|
136
141
|
specifiers,
|
|
137
142
|
line: getLine(node),
|
|
138
|
-
column: getColumn(node)
|
|
143
|
+
column: getColumn(node),
|
|
144
|
+
importKind: (node as ImportDeclaration & { importKind?: string }).importKind as ImportInfo['importKind']
|
|
139
145
|
});
|
|
140
146
|
},
|
|
141
147
|
|
|
@@ -102,20 +102,30 @@ export class ImportExportLinker extends Plugin {
|
|
|
102
102
|
const currentDir = dirname(imp.file!);
|
|
103
103
|
const basePath = join(currentDir, imp.source!);
|
|
104
104
|
|
|
105
|
-
//
|
|
105
|
+
// Build candidate paths: append extensions + TypeScript .js→.ts redirect
|
|
106
|
+
const candidates: string[] = [];
|
|
106
107
|
const extensions = ['', '.js', '.ts', '.jsx', '.tsx', '/index.js', '/index.ts'];
|
|
108
|
+
for (const ext of extensions) {
|
|
109
|
+
candidates.push(basePath + ext);
|
|
110
|
+
}
|
|
111
|
+
// TypeScript convention: imports use .js but actual files are .ts
|
|
112
|
+
if (basePath.endsWith('.js')) {
|
|
113
|
+
candidates.push(basePath.slice(0, -3) + '.ts');
|
|
114
|
+
candidates.push(basePath.slice(0, -3) + '.tsx');
|
|
115
|
+
} else if (basePath.endsWith('.jsx')) {
|
|
116
|
+
candidates.push(basePath.slice(0, -4) + '.tsx');
|
|
117
|
+
}
|
|
118
|
+
|
|
107
119
|
let targetFile: string | null = null;
|
|
108
120
|
let targetExports: Map<string, ExportNode> | null = null;
|
|
109
121
|
|
|
110
|
-
for (const
|
|
111
|
-
const testPath = basePath + ext;
|
|
122
|
+
for (const testPath of candidates) {
|
|
112
123
|
if (exportIndex.has(testPath)) {
|
|
113
124
|
targetFile = testPath;
|
|
114
125
|
targetExports = exportIndex.get(testPath)!;
|
|
115
126
|
break;
|
|
116
127
|
}
|
|
117
|
-
|
|
118
|
-
if (!targetFile && modulesByFile.has(testPath)) {
|
|
128
|
+
if (modulesByFile.has(testPath)) {
|
|
119
129
|
targetFile = testPath;
|
|
120
130
|
targetExports = exportIndex.get(testPath) || new Map();
|
|
121
131
|
break;
|