-
Notifications
You must be signed in to change notification settings - Fork 173
Expand file tree
/
Copy pathcode-signature.ts
More file actions
98 lines (83 loc) · 3.23 KB
/
Copy pathcode-signature.ts
File metadata and controls
98 lines (83 loc) · 3.23 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
import crypto from 'node:crypto';
import fs from 'node:fs';
import path from 'node:path';
import { findProjectRoot } from '../utils/version.ts';
const STATIC_IMPORT_RE =
/(?:^|[^\w$.])(?:import|export)\s+(?:type\s+)?(?:[^'"`]*?\s+from\s+)?['"]([^'"]+)['"]/gm;
const DYNAMIC_IMPORT_RE = /import\(\s*['"]([^'"]+)['"]\s*\)/gm;
const RESOLVABLE_EXTENSIONS = ['.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs'] as const;
export function resolveDaemonCodeSignature(): string {
const entryPath = process.argv[1];
if (!entryPath) return 'unknown';
return computeDaemonCodeSignature(entryPath);
}
export function computeDaemonCodeSignature(
entryPath: string,
root: string = findProjectRoot(),
): string {
try {
const normalizedRoot = path.resolve(root);
const normalizedEntryPath = path.resolve(entryPath);
const queue = [normalizedEntryPath];
const visited = new Set<string>();
const fingerprintParts: string[] = [];
while (queue.length > 0) {
const currentPath = queue.pop();
if (!currentPath || visited.has(currentPath)) continue;
visited.add(currentPath);
const stat = fs.statSync(currentPath);
if (!stat.isFile()) continue;
const relativePath = path.relative(normalizedRoot, currentPath) || currentPath;
fingerprintParts.push(`${relativePath}:${stat.size}:${Math.trunc(stat.mtimeMs)}`);
const content = fs.readFileSync(currentPath, 'utf8');
for (const specifier of collectRelativeImportSpecifiers(content)) {
const dependencyPath = resolveRelativeImportPath(currentPath, specifier);
if (dependencyPath) {
queue.push(dependencyPath);
}
}
}
const fingerprint = fingerprintParts.sort().join('|');
const hash = crypto.createHash('sha1').update(fingerprint).digest('hex');
return `graph:${fingerprintParts.length}:${hash}`;
} catch {
return 'unknown';
}
}
function collectRelativeImportSpecifiers(content: string): string[] {
const specifiers = new Set<string>();
collectImportMatches(content, STATIC_IMPORT_RE, specifiers);
collectImportMatches(content, DYNAMIC_IMPORT_RE, specifiers);
return [...specifiers];
}
function collectImportMatches(content: string, pattern: RegExp, specifiers: Set<string>): void {
pattern.lastIndex = 0;
let match: RegExpExecArray | null = null;
while ((match = pattern.exec(content)) !== null) {
const specifier = match[1]?.trim();
if (specifier?.startsWith('.')) {
specifiers.add(specifier);
}
}
}
function resolveRelativeImportPath(fromPath: string, specifier: string): string | null {
const basePath = path.resolve(path.dirname(fromPath), specifier);
const direct = resolveExistingFile(basePath);
if (direct) return direct;
for (const extension of RESOLVABLE_EXTENSIONS) {
const withExtension = resolveExistingFile(`${basePath}${extension}`);
if (withExtension) return withExtension;
}
for (const extension of RESOLVABLE_EXTENSIONS) {
const indexPath = resolveExistingFile(path.join(basePath, `index${extension}`));
if (indexPath) return indexPath;
}
return null;
}
function resolveExistingFile(candidatePath: string): string | null {
try {
return fs.statSync(candidatePath).isFile() ? candidatePath : null;
} catch {
return null;
}
}