nx-analyze 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.
- package/cli.d.ts +9 -0
- package/cli.d.ts.map +1 -0
- package/cli.js +409 -0
- package/index.d.ts +2 -0
- package/index.d.ts.map +1 -0
- package/index.js +1 -0
- package/lib/db/project.d.ts +5 -0
- package/lib/db/project.d.ts.map +1 -0
- package/lib/db/project.js +12 -0
- package/lib/db.d.ts +57 -0
- package/lib/db.d.ts.map +1 -0
- package/lib/db.js +732 -0
- package/lib/example.d.ts +3 -0
- package/lib/example.d.ts.map +1 -0
- package/lib/example.js +81 -0
- package/lib/types.d.ts +45 -0
- package/lib/types.d.ts.map +1 -0
- package/lib/types.js +1 -0
- package/package.json +35 -0
- package/tsconfig.lib.tsbuildinfo +1 -0
package/lib/db.js
ADDED
|
@@ -0,0 +1,732 @@
|
|
|
1
|
+
import { createRequire } from 'module';
|
|
2
|
+
// Use CommonJS require for sqlite3 to ensure correct runtime shape under NodeNext/ESM
|
|
3
|
+
const sqlite3 = createRequire(import.meta.url)('sqlite3');
|
|
4
|
+
import * as path from 'path';
|
|
5
|
+
import * as fs from 'fs';
|
|
6
|
+
import { execSync } from 'child_process';
|
|
7
|
+
import { createProjectGraphAsync, createProjectFileMapUsingProjectGraph, workspaceRoot, } from '@nx/devkit';
|
|
8
|
+
import ts from 'typescript';
|
|
9
|
+
export class ProjectDatabase {
|
|
10
|
+
db;
|
|
11
|
+
dbPath;
|
|
12
|
+
constructor(dbPath) {
|
|
13
|
+
this.dbPath = dbPath || path.join(process.cwd(), 'projects.db');
|
|
14
|
+
this.db = new sqlite3.Database(this.dbPath);
|
|
15
|
+
this.initializeDatabase();
|
|
16
|
+
}
|
|
17
|
+
initializeDatabase() {
|
|
18
|
+
const createProjectsTable = `
|
|
19
|
+
CREATE TABLE IF NOT EXISTS projects (
|
|
20
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
21
|
+
name TEXT UNIQUE NOT NULL,
|
|
22
|
+
description TEXT,
|
|
23
|
+
project_type TEXT,
|
|
24
|
+
source_root TEXT,
|
|
25
|
+
root TEXT,
|
|
26
|
+
tags TEXT,
|
|
27
|
+
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
|
28
|
+
)
|
|
29
|
+
`;
|
|
30
|
+
const createFilesTable = `
|
|
31
|
+
CREATE TABLE IF NOT EXISTS project_files (
|
|
32
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
33
|
+
project_id INTEGER NOT NULL,
|
|
34
|
+
file_path TEXT NOT NULL,
|
|
35
|
+
file_type TEXT,
|
|
36
|
+
added_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
37
|
+
FOREIGN KEY (project_id) REFERENCES projects (id) ON DELETE CASCADE,
|
|
38
|
+
UNIQUE(project_id, file_path)
|
|
39
|
+
)
|
|
40
|
+
`;
|
|
41
|
+
const createCommitsTable = `
|
|
42
|
+
CREATE TABLE IF NOT EXISTS git_commits (
|
|
43
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
44
|
+
hash TEXT UNIQUE NOT NULL,
|
|
45
|
+
author TEXT NOT NULL,
|
|
46
|
+
date TEXT NOT NULL,
|
|
47
|
+
message TEXT NOT NULL,
|
|
48
|
+
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
|
49
|
+
)
|
|
50
|
+
`;
|
|
51
|
+
const createTouchedFilesTable = `
|
|
52
|
+
CREATE TABLE IF NOT EXISTS touched_files (
|
|
53
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
54
|
+
commit_id INTEGER NOT NULL,
|
|
55
|
+
file_path TEXT NOT NULL,
|
|
56
|
+
change_type TEXT NOT NULL,
|
|
57
|
+
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
58
|
+
FOREIGN KEY (commit_id) REFERENCES git_commits (id) ON DELETE CASCADE,
|
|
59
|
+
UNIQUE(commit_id, file_path)
|
|
60
|
+
)
|
|
61
|
+
`;
|
|
62
|
+
const createFileDepsTable = `
|
|
63
|
+
CREATE TABLE IF NOT EXISTS file_dependencies (
|
|
64
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
65
|
+
file_path TEXT NOT NULL,
|
|
66
|
+
depends_on_project TEXT NOT NULL,
|
|
67
|
+
depends_on_file TEXT,
|
|
68
|
+
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
69
|
+
UNIQUE(file_path, depends_on_project, depends_on_file)
|
|
70
|
+
)
|
|
71
|
+
`;
|
|
72
|
+
this.db.serialize(() => {
|
|
73
|
+
this.db.run(createProjectsTable);
|
|
74
|
+
this.db.run(createFilesTable);
|
|
75
|
+
this.db.run(createCommitsTable);
|
|
76
|
+
this.db.run(createTouchedFilesTable);
|
|
77
|
+
this.db.run(createFileDepsTable);
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
query(sql, params = []) {
|
|
81
|
+
return new Promise((resolve, reject) => {
|
|
82
|
+
this.db.all(sql, params, (err, rows) => {
|
|
83
|
+
if (err) {
|
|
84
|
+
reject(err);
|
|
85
|
+
}
|
|
86
|
+
else {
|
|
87
|
+
resolve(rows);
|
|
88
|
+
}
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
// Nx integration methods
|
|
93
|
+
async syncWithNxWorkspace(workspaceRoot) {
|
|
94
|
+
process.chdir(workspaceRoot || process.cwd());
|
|
95
|
+
try {
|
|
96
|
+
// Read Nx configuration and create project graph
|
|
97
|
+
const projectGraph = await createProjectGraphAsync({
|
|
98
|
+
exitOnError: false,
|
|
99
|
+
});
|
|
100
|
+
const fileMap = await createProjectFileMapUsingProjectGraph(projectGraph);
|
|
101
|
+
// Sync all Nx projects to database
|
|
102
|
+
for (const [projectName, files] of Object.entries(fileMap)) {
|
|
103
|
+
await this.syncNxProject(projectName, projectGraph.nodes[projectName], files);
|
|
104
|
+
}
|
|
105
|
+
return projectGraph;
|
|
106
|
+
}
|
|
107
|
+
catch (error) {
|
|
108
|
+
throw new Error(`Failed to sync with Nx workspace: ${error instanceof Error ? error.message : error}`);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
async syncNxProject(projectName, projectNode, files) {
|
|
112
|
+
const data = projectNode.data;
|
|
113
|
+
// Create or update project in database
|
|
114
|
+
const existingProject = await this.getProject(projectName);
|
|
115
|
+
if (existingProject) {
|
|
116
|
+
// Update existing project
|
|
117
|
+
await this.updateProject(projectName, {
|
|
118
|
+
description: data.description,
|
|
119
|
+
project_type: data.projectType,
|
|
120
|
+
source_root: data.sourceRoot,
|
|
121
|
+
root: data.root,
|
|
122
|
+
tags: data.tags?.join(','),
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
else {
|
|
126
|
+
// Create new project
|
|
127
|
+
await this.createProjectFromNx(projectName, {
|
|
128
|
+
description: data.description,
|
|
129
|
+
project_type: data.projectType,
|
|
130
|
+
source_root: data.sourceRoot,
|
|
131
|
+
root: data.root,
|
|
132
|
+
tags: data.tags?.join(','),
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
// Add all files from the project
|
|
136
|
+
for (const file of files) {
|
|
137
|
+
await this.addFileToProject(projectName, file.file, file.deps?.map((d) => d[0]));
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
// Project methods
|
|
141
|
+
async createProject(name, description) {
|
|
142
|
+
return new Promise((resolve, reject) => {
|
|
143
|
+
const stmt = this.db.prepare('INSERT INTO projects (name, description) VALUES (?, ?)');
|
|
144
|
+
stmt.run([name, description], function (err) {
|
|
145
|
+
if (err) {
|
|
146
|
+
reject(err);
|
|
147
|
+
}
|
|
148
|
+
else {
|
|
149
|
+
resolve(this.lastID);
|
|
150
|
+
}
|
|
151
|
+
});
|
|
152
|
+
stmt.finalize();
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
async createProjectFromNx(name, nxData) {
|
|
156
|
+
return new Promise((resolve, reject) => {
|
|
157
|
+
const stmt = this.db.prepare(`
|
|
158
|
+
INSERT INTO projects (name, description, project_type, source_root, root, tags)
|
|
159
|
+
VALUES (?, ?, ?, ?, ?, ?)
|
|
160
|
+
`);
|
|
161
|
+
stmt.run([
|
|
162
|
+
name,
|
|
163
|
+
nxData.description,
|
|
164
|
+
nxData.project_type,
|
|
165
|
+
nxData.source_root,
|
|
166
|
+
nxData.root,
|
|
167
|
+
nxData.tags,
|
|
168
|
+
], function (err) {
|
|
169
|
+
if (err) {
|
|
170
|
+
reject(err);
|
|
171
|
+
}
|
|
172
|
+
else {
|
|
173
|
+
resolve(this.lastID);
|
|
174
|
+
}
|
|
175
|
+
});
|
|
176
|
+
stmt.finalize();
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
async updateProject(name, updates) {
|
|
180
|
+
return new Promise((resolve, reject) => {
|
|
181
|
+
const stmt = this.db.prepare(`
|
|
182
|
+
UPDATE projects
|
|
183
|
+
SET description = ?, project_type = ?, source_root = ?, root = ?, tags = ?
|
|
184
|
+
WHERE name = ?
|
|
185
|
+
`);
|
|
186
|
+
stmt.run([
|
|
187
|
+
updates.description,
|
|
188
|
+
updates.project_type,
|
|
189
|
+
updates.source_root,
|
|
190
|
+
updates.root,
|
|
191
|
+
updates.tags,
|
|
192
|
+
name,
|
|
193
|
+
], function (err) {
|
|
194
|
+
if (err) {
|
|
195
|
+
reject(err);
|
|
196
|
+
}
|
|
197
|
+
else {
|
|
198
|
+
resolve((this.changes ?? 0) > 0);
|
|
199
|
+
}
|
|
200
|
+
});
|
|
201
|
+
stmt.finalize();
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
async getProject(name) {
|
|
205
|
+
return new Promise((resolve, reject) => {
|
|
206
|
+
this.db.get('SELECT * FROM projects WHERE name = ?', [name], (err, row) => {
|
|
207
|
+
if (err) {
|
|
208
|
+
reject(err);
|
|
209
|
+
}
|
|
210
|
+
else {
|
|
211
|
+
resolve(row || null);
|
|
212
|
+
}
|
|
213
|
+
});
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
async getAllProjects() {
|
|
217
|
+
return this.query('SELECT * FROM projects ORDER BY name');
|
|
218
|
+
}
|
|
219
|
+
async deleteProject(name) {
|
|
220
|
+
return new Promise((resolve, reject) => {
|
|
221
|
+
this.db.run('DELETE FROM projects WHERE name = ?', [name], function (err) {
|
|
222
|
+
if (err) {
|
|
223
|
+
reject(err);
|
|
224
|
+
}
|
|
225
|
+
else {
|
|
226
|
+
resolve((this.changes ?? 0) > 0);
|
|
227
|
+
}
|
|
228
|
+
});
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
async getProjectsByType(projectType) {
|
|
232
|
+
return this.query('SELECT * FROM projects WHERE project_type = ? ORDER BY name', [projectType]);
|
|
233
|
+
}
|
|
234
|
+
async getProjectsByTag(tag) {
|
|
235
|
+
return this.query('SELECT * FROM projects WHERE tags LIKE ? ORDER BY name', [`%${tag}%`]);
|
|
236
|
+
}
|
|
237
|
+
// File methods
|
|
238
|
+
async addFileToProject(projectName, filePath, fileDeps) {
|
|
239
|
+
const project = await this.getProject(projectName);
|
|
240
|
+
if (!project) {
|
|
241
|
+
throw new Error(`Project "${projectName}" not found`);
|
|
242
|
+
}
|
|
243
|
+
return new Promise((resolve, reject) => {
|
|
244
|
+
const stmt = this.db.prepare('INSERT OR REPLACE INTO project_files (project_id, file_path, file_type) VALUES (?, ?, ?)');
|
|
245
|
+
stmt.run([project.id, filePath, fileDeps], (err) => {
|
|
246
|
+
if (err) {
|
|
247
|
+
reject(err);
|
|
248
|
+
}
|
|
249
|
+
else {
|
|
250
|
+
resolve();
|
|
251
|
+
}
|
|
252
|
+
});
|
|
253
|
+
stmt.finalize();
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
async removeFileFromProject(projectName, filePath) {
|
|
257
|
+
const project = await this.getProject(projectName);
|
|
258
|
+
if (!project) {
|
|
259
|
+
return false;
|
|
260
|
+
}
|
|
261
|
+
return new Promise((resolve, reject) => {
|
|
262
|
+
this.db.run('DELETE FROM project_files WHERE project_id = ? AND file_path = ?', [project.id, filePath], function (err) {
|
|
263
|
+
if (err) {
|
|
264
|
+
reject(err);
|
|
265
|
+
}
|
|
266
|
+
else {
|
|
267
|
+
resolve((this.changes ?? 0) > 0);
|
|
268
|
+
}
|
|
269
|
+
});
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
async getProjectFiles(projectName) {
|
|
273
|
+
const project = await this.getProject(projectName);
|
|
274
|
+
if (!project) {
|
|
275
|
+
return [];
|
|
276
|
+
}
|
|
277
|
+
return this.query('SELECT * FROM project_files WHERE project_id = ? ORDER BY file_path', [project.id]);
|
|
278
|
+
}
|
|
279
|
+
async getFileProjects(filePath) {
|
|
280
|
+
return this.query(`
|
|
281
|
+
SELECT p.* FROM projects p
|
|
282
|
+
JOIN project_files pf ON p.id = pf.project_id
|
|
283
|
+
WHERE pf.file_path = ?
|
|
284
|
+
ORDER BY p.name
|
|
285
|
+
`, [filePath]);
|
|
286
|
+
}
|
|
287
|
+
// Nx-specific query methods
|
|
288
|
+
async getProjectDependencies(projectName) {
|
|
289
|
+
try {
|
|
290
|
+
const projectGraph = await createProjectGraphAsync({
|
|
291
|
+
exitOnError: false,
|
|
292
|
+
});
|
|
293
|
+
const dependencies = projectGraph.dependencies[projectName] || [];
|
|
294
|
+
return dependencies.map((dep) => dep.target);
|
|
295
|
+
}
|
|
296
|
+
catch (error) {
|
|
297
|
+
console.warn(`Could not get dependencies for ${projectName}:`, error);
|
|
298
|
+
return [];
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
async getProjectDependents(projectName) {
|
|
302
|
+
try {
|
|
303
|
+
const projectGraph = await createProjectGraphAsync({
|
|
304
|
+
exitOnError: false,
|
|
305
|
+
});
|
|
306
|
+
const dependents = [];
|
|
307
|
+
for (const [project, deps] of Object.entries(projectGraph.dependencies)) {
|
|
308
|
+
if (deps.some((dep) => dep.target === projectName)) {
|
|
309
|
+
dependents.push(project);
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
return dependents;
|
|
313
|
+
}
|
|
314
|
+
catch (error) {
|
|
315
|
+
console.warn(`Could not get dependents for ${projectName}:`, error);
|
|
316
|
+
return [];
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
async getAffectedProjects(changedFiles) {
|
|
320
|
+
try {
|
|
321
|
+
const affectedProjects = new Set();
|
|
322
|
+
// Find which projects contain the changed files
|
|
323
|
+
for (const filePath of changedFiles) {
|
|
324
|
+
const projects = await this.getFileProjects(filePath);
|
|
325
|
+
for (const project of projects) {
|
|
326
|
+
affectedProjects.add(project.name);
|
|
327
|
+
// Also add dependent projects
|
|
328
|
+
const dependents = await this.getProjectDependents(project.name);
|
|
329
|
+
dependents.forEach((dep) => affectedProjects.add(dep));
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
return Array.from(affectedProjects);
|
|
333
|
+
}
|
|
334
|
+
catch (error) {
|
|
335
|
+
console.warn('Could not determine affected projects:', error);
|
|
336
|
+
return [];
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
// File dependency methods (Nx file-map ingestion)
|
|
340
|
+
async clearFileDependencies() {
|
|
341
|
+
return new Promise((resolve, reject) => {
|
|
342
|
+
this.db.run('DELETE FROM file_dependencies', (err) => {
|
|
343
|
+
if (err)
|
|
344
|
+
reject(err);
|
|
345
|
+
else
|
|
346
|
+
resolve();
|
|
347
|
+
});
|
|
348
|
+
});
|
|
349
|
+
}
|
|
350
|
+
// Accept an optional Program & TypeChecker to avoid rebuilding per-file (performance)
|
|
351
|
+
findDefinition(fileName, targetPackage, program, checker) {
|
|
352
|
+
let localProgram = program;
|
|
353
|
+
let localChecker = checker;
|
|
354
|
+
// If no program/checker provided, fall back to previous behavior (single-file program)
|
|
355
|
+
if (!localProgram || !localChecker) {
|
|
356
|
+
const configFilePath = ts.findConfigFile(fileName, ts.sys.fileExists) ||
|
|
357
|
+
path.join(workspaceRoot, 'tsconfig.json');
|
|
358
|
+
const configHost = {
|
|
359
|
+
...ts.sys,
|
|
360
|
+
onUnRecoverableConfigFileDiagnostic: () => {
|
|
361
|
+
/** intentionally empty */
|
|
362
|
+
},
|
|
363
|
+
};
|
|
364
|
+
const parsed = ts.getParsedCommandLineOfConfigFile(configFilePath, undefined, configHost);
|
|
365
|
+
localProgram = ts.createProgram([fileName], parsed?.options || {});
|
|
366
|
+
localChecker = localProgram.getTypeChecker();
|
|
367
|
+
}
|
|
368
|
+
const sourceFile = localProgram.getSourceFile(fileName);
|
|
369
|
+
const foundSymbols = [];
|
|
370
|
+
function visit(node) {
|
|
371
|
+
if (ts.isImportDeclaration(node)) {
|
|
372
|
+
// Get the module name (stripping quotes)
|
|
373
|
+
const moduleName = node.moduleSpecifier
|
|
374
|
+
.getText(sourceFile)
|
|
375
|
+
.replace(/['"]/g, '');
|
|
376
|
+
if (moduleName.includes(targetPackage)) {
|
|
377
|
+
const importClause = node.importClause;
|
|
378
|
+
if (importClause?.namedBindings &&
|
|
379
|
+
ts.isNamedImports(importClause.namedBindings)) {
|
|
380
|
+
// Handle Named Imports: import { useState, useEffect } from 'react'
|
|
381
|
+
importClause.namedBindings.elements.forEach((namedImport) => {
|
|
382
|
+
const symbol = localChecker.getSymbolAtLocation(namedImport.name);
|
|
383
|
+
let defined_in_file = undefined;
|
|
384
|
+
if (symbol) {
|
|
385
|
+
// Follow the import chain to the original definition
|
|
386
|
+
const aliasedSymbol = localChecker.getAliasedSymbol(symbol);
|
|
387
|
+
const declaration = aliasedSymbol?.declarations?.[0] || symbol.declarations?.[0];
|
|
388
|
+
if (declaration) {
|
|
389
|
+
defined_in_file = declaration
|
|
390
|
+
.getSourceFile()
|
|
391
|
+
.fileName.replace(workspaceRoot + '/', '');
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
foundSymbols.push({
|
|
395
|
+
symbol: namedImport.name.text,
|
|
396
|
+
defined_in_project: targetPackage,
|
|
397
|
+
defined_in_file,
|
|
398
|
+
});
|
|
399
|
+
// Keep logging for now; can be toggled later if needed
|
|
400
|
+
// console.log(
|
|
401
|
+
// `Import: ${namedImport.name.text} in ${fileName} defined in: ${defined_in_file}`
|
|
402
|
+
// );
|
|
403
|
+
});
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
ts.forEachChild(node, visit);
|
|
408
|
+
}
|
|
409
|
+
if (sourceFile)
|
|
410
|
+
visit(sourceFile);
|
|
411
|
+
return foundSymbols;
|
|
412
|
+
}
|
|
413
|
+
async addFileDependency(filePath, dependsOn) {
|
|
414
|
+
const foundSymbols = this.findDefinition(filePath, dependsOn);
|
|
415
|
+
return Promise.all(foundSymbols.map((foundSymbol) => new Promise((resolve, reject) => {
|
|
416
|
+
const stmt = this.db.prepare('INSERT OR IGNORE INTO file_dependencies (file_path, depends_on_project, depends_on_file) VALUES (?, ?, ?)');
|
|
417
|
+
stmt.run([filePath, dependsOn, foundSymbol.defined_in_file], (err) => {
|
|
418
|
+
if (err)
|
|
419
|
+
reject(err);
|
|
420
|
+
else
|
|
421
|
+
resolve();
|
|
422
|
+
});
|
|
423
|
+
stmt.finalize();
|
|
424
|
+
})));
|
|
425
|
+
}
|
|
426
|
+
async getFileDependencies(filePath) {
|
|
427
|
+
return this.query('SELECT depends_on_project FROM file_dependencies WHERE file_path = ? ORDER BY depends_on_project', [filePath]).then((rows) => rows.map((r) => r.depends_on_project));
|
|
428
|
+
}
|
|
429
|
+
async getFileDependents(dependsOn) {
|
|
430
|
+
return this.query('SELECT file_path FROM file_dependencies WHERE depends_on_project = ? ORDER BY file_path', [dependsOn]).then((rows) => rows.map((r) => r.file_path));
|
|
431
|
+
}
|
|
432
|
+
async syncFileDependenciesFromNx(workspaceRoot) {
|
|
433
|
+
const root = workspaceRoot ? path.resolve(workspaceRoot) : process.cwd();
|
|
434
|
+
// Determine Nx cache dir from nx.json if present, default to .nx
|
|
435
|
+
let cacheDir = '.nx';
|
|
436
|
+
try {
|
|
437
|
+
const nxJsonPath = path.join(root, 'nx.json');
|
|
438
|
+
if (fs.existsSync(nxJsonPath)) {
|
|
439
|
+
const nxConfig = JSON.parse(fs.readFileSync(nxJsonPath, 'utf8'));
|
|
440
|
+
const installation = nxConfig.installation;
|
|
441
|
+
if (installation &&
|
|
442
|
+
typeof installation === 'object' &&
|
|
443
|
+
typeof installation.cacheDirectory === 'string') {
|
|
444
|
+
cacheDir = installation.cacheDirectory;
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
catch {
|
|
449
|
+
// ignore and use default
|
|
450
|
+
}
|
|
451
|
+
const fileMapPath = path.join(root, cacheDir, 'workspace-data', 'file-map.json');
|
|
452
|
+
if (!fs.existsSync(fileMapPath)) {
|
|
453
|
+
throw new Error(`Nx file map not found at ${fileMapPath}`);
|
|
454
|
+
}
|
|
455
|
+
const content = fs.readFileSync(fileMapPath, 'utf8');
|
|
456
|
+
const parsed = JSON.parse(content);
|
|
457
|
+
// Collect all file records to build a single TS program (faster than per-file)
|
|
458
|
+
const nonProjectFiles = parsed?.fileMap?.nonProjectFiles ?? [];
|
|
459
|
+
const projectFileMap = parsed?.fileMap?.projectFileMap ?? {};
|
|
460
|
+
const allEntries = [];
|
|
461
|
+
for (const entry of nonProjectFiles)
|
|
462
|
+
allEntries.push(entry);
|
|
463
|
+
for (const files of Object.values(projectFileMap)) {
|
|
464
|
+
for (const entry of files)
|
|
465
|
+
allEntries.push(entry);
|
|
466
|
+
}
|
|
467
|
+
// Build absolute file list for TS program
|
|
468
|
+
const allFileAbs = Array.from(new Set(allEntries.map((e) => path.resolve(root, e.file))));
|
|
469
|
+
// Try to create a single program; if it fails, fall back to per-file parsing
|
|
470
|
+
let program;
|
|
471
|
+
let checker;
|
|
472
|
+
try {
|
|
473
|
+
const configFilePath = ts.findConfigFile(root, ts.sys.fileExists) ||
|
|
474
|
+
path.join(root, 'tsconfig.json');
|
|
475
|
+
const configHost = {
|
|
476
|
+
...ts.sys,
|
|
477
|
+
onUnRecoverableConfigFileDiagnostic: () => {
|
|
478
|
+
/** intentionally empty */
|
|
479
|
+
},
|
|
480
|
+
};
|
|
481
|
+
const parsedConfig = ts.getParsedCommandLineOfConfigFile(configFilePath, undefined, configHost);
|
|
482
|
+
program = ts.createProgram(allFileAbs, parsedConfig?.options || {});
|
|
483
|
+
checker = program.getTypeChecker();
|
|
484
|
+
}
|
|
485
|
+
catch (e) {
|
|
486
|
+
// If program creation fails (e.g., missing files in test environment), we will
|
|
487
|
+
// gracefully fall back to the previous per-file approach below.
|
|
488
|
+
program = undefined;
|
|
489
|
+
checker = undefined;
|
|
490
|
+
}
|
|
491
|
+
let inserted = 0;
|
|
492
|
+
await this.clearFileDependencies();
|
|
493
|
+
// Gather dependency insertion records in memory first
|
|
494
|
+
const records = [];
|
|
495
|
+
const processEntry = (filePathRel, deps) => {
|
|
496
|
+
if (!deps || !Array.isArray(deps))
|
|
497
|
+
return;
|
|
498
|
+
for (const dep of deps) {
|
|
499
|
+
let depStr;
|
|
500
|
+
if (typeof dep === 'string')
|
|
501
|
+
depStr = dep;
|
|
502
|
+
else if (Array.isArray(dep) &&
|
|
503
|
+
dep.length > 0 &&
|
|
504
|
+
typeof dep[0] === 'string')
|
|
505
|
+
depStr = dep[0];
|
|
506
|
+
if (!depStr)
|
|
507
|
+
continue;
|
|
508
|
+
if (depStr.startsWith('npm:') || depStr === 'dynamic')
|
|
509
|
+
continue;
|
|
510
|
+
// Resolve file path to absolute when program provided
|
|
511
|
+
const absFile = path.resolve(root, filePathRel);
|
|
512
|
+
let found = [];
|
|
513
|
+
if (program && checker) {
|
|
514
|
+
found = this.findDefinition(absFile, depStr, program, checker);
|
|
515
|
+
}
|
|
516
|
+
else {
|
|
517
|
+
// Fallback to old behavior (slower)
|
|
518
|
+
found = this.findDefinition(filePathRel, depStr);
|
|
519
|
+
}
|
|
520
|
+
for (const f of found) {
|
|
521
|
+
records.push({
|
|
522
|
+
filePath: filePathRel,
|
|
523
|
+
dependsOn: depStr,
|
|
524
|
+
defined_in_file: f.defined_in_file ?? null,
|
|
525
|
+
});
|
|
526
|
+
inserted++;
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
};
|
|
530
|
+
// Process all entries
|
|
531
|
+
for (const entry of allEntries) {
|
|
532
|
+
processEntry(entry.file, entry.deps);
|
|
533
|
+
}
|
|
534
|
+
// Batch insert all records in a single transaction for performance
|
|
535
|
+
if (records.length > 0) {
|
|
536
|
+
await new Promise((resolve, reject) => {
|
|
537
|
+
this.db.serialize(() => {
|
|
538
|
+
this.db.run('BEGIN TRANSACTION');
|
|
539
|
+
const stmt = this.db.prepare('INSERT OR IGNORE INTO file_dependencies (file_path, depends_on_project, depends_on_file) VALUES (?, ?, ?)');
|
|
540
|
+
let pending = records.length;
|
|
541
|
+
for (const r of records) {
|
|
542
|
+
stmt.run([r.filePath, r.dependsOn, r.defined_in_file], (err) => {
|
|
543
|
+
if (err) {
|
|
544
|
+
// Ensure stmt finalized and propagate error
|
|
545
|
+
stmt.finalize();
|
|
546
|
+
reject(err);
|
|
547
|
+
return;
|
|
548
|
+
}
|
|
549
|
+
pending--;
|
|
550
|
+
if (pending === 0) {
|
|
551
|
+
stmt.finalize((err) => {
|
|
552
|
+
if (err)
|
|
553
|
+
return reject(err);
|
|
554
|
+
this.db.run('COMMIT', (err) => {
|
|
555
|
+
if (err)
|
|
556
|
+
return reject(err);
|
|
557
|
+
resolve();
|
|
558
|
+
});
|
|
559
|
+
});
|
|
560
|
+
}
|
|
561
|
+
});
|
|
562
|
+
}
|
|
563
|
+
// Handle case where records.length === 0 handled above
|
|
564
|
+
});
|
|
565
|
+
});
|
|
566
|
+
}
|
|
567
|
+
return inserted;
|
|
568
|
+
}
|
|
569
|
+
// Git commit tracking methods
|
|
570
|
+
async syncGitCommits(commitCount = 100) {
|
|
571
|
+
try {
|
|
572
|
+
// Get commit information using git log
|
|
573
|
+
const gitLogOutput = execSync(`git log --oneline --name-status -${commitCount} --pretty=format:"%H|%an|%ad|%s" --date=iso`, { encoding: 'utf8', cwd: process.cwd() });
|
|
574
|
+
const lines = gitLogOutput.split('\n').filter((line) => line.trim());
|
|
575
|
+
let currentCommit = null;
|
|
576
|
+
const touchedFiles = [];
|
|
577
|
+
for (const line of lines) {
|
|
578
|
+
if (line.includes('|')) {
|
|
579
|
+
// This is a commit line
|
|
580
|
+
if (currentCommit && touchedFiles.length > 0) {
|
|
581
|
+
// Save the previous commit and its files
|
|
582
|
+
await this.saveCommitWithFiles(currentCommit, touchedFiles);
|
|
583
|
+
touchedFiles.length = 0; // Clear the array
|
|
584
|
+
}
|
|
585
|
+
const [hash, author, date, message] = line.split('|');
|
|
586
|
+
currentCommit = {
|
|
587
|
+
hash: hash.trim(),
|
|
588
|
+
author: author.trim(),
|
|
589
|
+
date: date.trim(),
|
|
590
|
+
message: message.trim(),
|
|
591
|
+
};
|
|
592
|
+
}
|
|
593
|
+
else if (currentCommit && line.match(/^[AMDRT]\s+/)) {
|
|
594
|
+
// This is a file change line (A/M/D/R/T followed by filename)
|
|
595
|
+
const changeType = line.charAt(0);
|
|
596
|
+
const filePath = line.substring(2).trim();
|
|
597
|
+
touchedFiles.push({ filePath, changeType });
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
// Don't forget the last commit
|
|
601
|
+
if (currentCommit && touchedFiles.length > 0) {
|
|
602
|
+
await this.saveCommitWithFiles(currentCommit, touchedFiles);
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
catch (error) {
|
|
606
|
+
throw new Error(`Failed to sync git commits: ${error instanceof Error ? error.message : error}`);
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
async saveCommitWithFiles(commit, touchedFiles) {
|
|
610
|
+
// First, insert or get the commit
|
|
611
|
+
const commitId = await this.insertCommit(commit);
|
|
612
|
+
// Then insert all touched files for this commit
|
|
613
|
+
for (const file of touchedFiles) {
|
|
614
|
+
await this.insertTouchedFile(commitId, file.filePath, file.changeType);
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
async insertCommit(commit) {
|
|
618
|
+
return new Promise((resolve, reject) => {
|
|
619
|
+
// Use INSERT OR IGNORE to avoid duplicates
|
|
620
|
+
const dbRef = this.db;
|
|
621
|
+
const stmt = this.db.prepare(`
|
|
622
|
+
INSERT OR IGNORE INTO git_commits (hash, author, date, message)
|
|
623
|
+
VALUES (?, ?, ?, ?)
|
|
624
|
+
`);
|
|
625
|
+
stmt.run([commit.hash, commit.author, commit.date, commit.message], function (err) {
|
|
626
|
+
if (err) {
|
|
627
|
+
reject(err);
|
|
628
|
+
return;
|
|
629
|
+
}
|
|
630
|
+
// If we inserted a new row, use the lastID
|
|
631
|
+
if ((this.lastID ?? 0) > 0) {
|
|
632
|
+
resolve(this.lastID);
|
|
633
|
+
return;
|
|
634
|
+
}
|
|
635
|
+
// Insert was ignored (duplicate hash). Query the existing row id.
|
|
636
|
+
dbRef.get('SELECT id FROM git_commits WHERE hash = ?', [commit.hash], (err2, row) => {
|
|
637
|
+
if (err2) {
|
|
638
|
+
reject(err2);
|
|
639
|
+
}
|
|
640
|
+
else if (row && typeof row.id === 'number') {
|
|
641
|
+
resolve(row.id);
|
|
642
|
+
}
|
|
643
|
+
else {
|
|
644
|
+
reject(new Error('Failed to insert or retrieve commit'));
|
|
645
|
+
}
|
|
646
|
+
});
|
|
647
|
+
});
|
|
648
|
+
stmt.finalize();
|
|
649
|
+
});
|
|
650
|
+
}
|
|
651
|
+
async insertTouchedFile(commitId, filePath, changeType) {
|
|
652
|
+
return new Promise((resolve, reject) => {
|
|
653
|
+
const stmt = this.db.prepare(`
|
|
654
|
+
INSERT OR REPLACE INTO touched_files (commit_id, file_path, change_type)
|
|
655
|
+
VALUES (?, ?, ?)
|
|
656
|
+
`);
|
|
657
|
+
stmt.run([commitId, filePath, changeType], (err) => {
|
|
658
|
+
if (err) {
|
|
659
|
+
reject(err);
|
|
660
|
+
}
|
|
661
|
+
else {
|
|
662
|
+
resolve();
|
|
663
|
+
}
|
|
664
|
+
});
|
|
665
|
+
stmt.finalize();
|
|
666
|
+
});
|
|
667
|
+
}
|
|
668
|
+
async getCommits(limit = 50) {
|
|
669
|
+
return this.query('SELECT * FROM git_commits ORDER BY date DESC LIMIT ?', [limit]);
|
|
670
|
+
}
|
|
671
|
+
async getTouchedFiles(commitHash) {
|
|
672
|
+
let query = `
|
|
673
|
+
SELECT tf.*, gc.hash, gc.author, gc.date, gc.message
|
|
674
|
+
FROM touched_files tf
|
|
675
|
+
JOIN git_commits gc ON tf.commit_id = gc.id
|
|
676
|
+
`;
|
|
677
|
+
const params = [];
|
|
678
|
+
if (commitHash) {
|
|
679
|
+
query += ' WHERE gc.hash = ?';
|
|
680
|
+
params.push(commitHash);
|
|
681
|
+
}
|
|
682
|
+
query += ' ORDER BY gc.date DESC, tf.file_path';
|
|
683
|
+
return this.query(query, params);
|
|
684
|
+
}
|
|
685
|
+
async getFilesTouchedInLastCommits(commitCount = 100) {
|
|
686
|
+
const query = `
|
|
687
|
+
SELECT DISTINCT tf.file_path
|
|
688
|
+
FROM touched_files tf
|
|
689
|
+
WHERE tf.commit_id IN (
|
|
690
|
+
SELECT id FROM git_commits
|
|
691
|
+
ORDER BY date DESC
|
|
692
|
+
LIMIT ?
|
|
693
|
+
)
|
|
694
|
+
ORDER BY tf.file_path
|
|
695
|
+
`;
|
|
696
|
+
return this.query(query, [commitCount]).then((rows) => rows.map((row) => row.file_path));
|
|
697
|
+
}
|
|
698
|
+
async getProjectsTouchedByCommits(commitCount = 100) {
|
|
699
|
+
try {
|
|
700
|
+
const touchedFiles = await this.getFilesTouchedInLastCommits(commitCount);
|
|
701
|
+
const touchedProjects = new Set();
|
|
702
|
+
for (const filePath of touchedFiles) {
|
|
703
|
+
const projects = await this.getFileProjects(filePath);
|
|
704
|
+
for (const project of projects) {
|
|
705
|
+
touchedProjects.add(project.name);
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
return Array.from(touchedProjects);
|
|
709
|
+
}
|
|
710
|
+
catch (error) {
|
|
711
|
+
console.warn('Could not determine projects touched by commits:', error);
|
|
712
|
+
return [];
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
async close() {
|
|
716
|
+
return new Promise((resolve, reject) => {
|
|
717
|
+
this.db.close((err) => {
|
|
718
|
+
if (err)
|
|
719
|
+
reject(err);
|
|
720
|
+
else
|
|
721
|
+
resolve();
|
|
722
|
+
});
|
|
723
|
+
});
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
// Export convenience functions
|
|
727
|
+
export async function createDatabase(dbPath) {
|
|
728
|
+
return new ProjectDatabase(dbPath);
|
|
729
|
+
}
|
|
730
|
+
export function db() {
|
|
731
|
+
return 'db';
|
|
732
|
+
}
|