@flisk/analyze-tracking 0.8.3 → 0.8.4
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/package.json +1 -1
- package/src/analyze/index.js +69 -36
- package/src/analyze/typescript/index.js +135 -17
- package/src/analyze/typescript/parser.js +129 -42
package/package.json
CHANGED
package/src/analyze/index.js
CHANGED
|
@@ -8,32 +8,59 @@ const path = require('path');
|
|
|
8
8
|
const { parseCustomFunctionSignature } = require('./utils/customFunctionParser');
|
|
9
9
|
const { getAllFiles } = require('../utils/fileProcessor');
|
|
10
10
|
const { analyzeJsFile } = require('./javascript');
|
|
11
|
-
const {
|
|
11
|
+
const { analyzeTsFiles } = require('./typescript');
|
|
12
12
|
const { analyzePythonFile } = require('./python');
|
|
13
13
|
const { analyzeRubyFile } = require('./ruby');
|
|
14
14
|
const { analyzeGoFile } = require('./go');
|
|
15
15
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
16
|
+
/**
|
|
17
|
+
* Adds an event to the events collection, merging properties if event already exists
|
|
18
|
+
* @param {Object} allEvents - Collection of all events
|
|
19
|
+
* @param {Object} event - Event to add
|
|
20
|
+
* @param {string} baseDir - Base directory for relative path calculation
|
|
21
|
+
*/
|
|
22
|
+
function addEventToCollection(allEvents, event, baseDir) {
|
|
23
|
+
const relativeFilePath = path.relative(baseDir, event.filePath);
|
|
24
|
+
|
|
25
|
+
const implementation = {
|
|
26
|
+
path: relativeFilePath,
|
|
27
|
+
line: event.line,
|
|
28
|
+
function: event.functionName,
|
|
29
|
+
destination: event.source
|
|
30
|
+
};
|
|
20
31
|
|
|
21
|
-
|
|
32
|
+
if (!allEvents[event.eventName]) {
|
|
33
|
+
allEvents[event.eventName] = {
|
|
34
|
+
implementations: [implementation],
|
|
35
|
+
properties: event.properties,
|
|
36
|
+
};
|
|
37
|
+
} else {
|
|
38
|
+
allEvents[event.eventName].implementations.push(implementation);
|
|
39
|
+
allEvents[event.eventName].properties = {
|
|
40
|
+
...allEvents[event.eventName].properties,
|
|
41
|
+
...event.properties,
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
}
|
|
22
45
|
|
|
46
|
+
/**
|
|
47
|
+
* Processes all files that are not TypeScript files
|
|
48
|
+
* @param {Array<string>} files - Array of file paths
|
|
49
|
+
* @param {Object} allEvents - Collection to add events to
|
|
50
|
+
* @param {string} baseDir - Base directory for relative paths
|
|
51
|
+
* @param {Array} customFunctionSignatures - Custom function signatures to detect
|
|
52
|
+
*/
|
|
53
|
+
async function processFiles(files, allEvents, baseDir, customFunctionSignatures) {
|
|
23
54
|
for (const file of files) {
|
|
24
55
|
let events = [];
|
|
25
56
|
|
|
26
57
|
const isJsFile = /\.(jsx?)$/.test(file);
|
|
27
|
-
const isTsFile = /\.(tsx?)$/.test(file);
|
|
28
58
|
const isPythonFile = /\.(py)$/.test(file);
|
|
29
59
|
const isRubyFile = /\.(rb)$/.test(file);
|
|
30
60
|
const isGoFile = /\.(go)$/.test(file);
|
|
31
61
|
|
|
32
62
|
if (isJsFile) {
|
|
33
63
|
events = analyzeJsFile(file, customFunctionSignatures);
|
|
34
|
-
} else if (isTsFile) {
|
|
35
|
-
// Pass null program so analyzeTsFile will create a per-file program using the file's nearest tsconfig.json
|
|
36
|
-
events = analyzeTsFile(file, null, customFunctionSignatures);
|
|
37
64
|
} else if (isPythonFile) {
|
|
38
65
|
events = await analyzePythonFile(file, customFunctionSignatures);
|
|
39
66
|
} else if (isRubyFile) {
|
|
@@ -41,36 +68,42 @@ async function analyzeDirectory(dirPath, customFunctions) {
|
|
|
41
68
|
} else if (isGoFile) {
|
|
42
69
|
events = await analyzeGoFile(file, customFunctionSignatures);
|
|
43
70
|
} else {
|
|
44
|
-
continue;
|
|
71
|
+
continue; // Skip unsupported file types
|
|
45
72
|
}
|
|
46
73
|
|
|
47
|
-
events.forEach(
|
|
48
|
-
|
|
74
|
+
events.forEach(event => addEventToCollection(allEvents, event, baseDir));
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
async function analyzeDirectory(dirPath, customFunctions) {
|
|
79
|
+
const allEvents = {};
|
|
80
|
+
|
|
81
|
+
const customFunctionSignatures = (customFunctions?.length > 0)
|
|
82
|
+
? customFunctions.map(parseCustomFunctionSignature)
|
|
83
|
+
: null;
|
|
84
|
+
|
|
85
|
+
const files = getAllFiles(dirPath);
|
|
86
|
+
|
|
87
|
+
// Separate TypeScript files from others for optimized processing
|
|
88
|
+
const tsFiles = [];
|
|
89
|
+
const otherFiles = [];
|
|
90
|
+
|
|
91
|
+
for (const file of files) {
|
|
92
|
+
const isTsFile = /\.(tsx?)$/.test(file);
|
|
93
|
+
if (isTsFile) {
|
|
94
|
+
tsFiles.push(file);
|
|
95
|
+
} else {
|
|
96
|
+
otherFiles.push(file);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
49
99
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
implementations: [{
|
|
53
|
-
path: relativeFilePath,
|
|
54
|
-
line: event.line,
|
|
55
|
-
function: event.functionName,
|
|
56
|
-
destination: event.source
|
|
57
|
-
}],
|
|
58
|
-
properties: event.properties,
|
|
59
|
-
};
|
|
60
|
-
} else {
|
|
61
|
-
allEvents[event.eventName].implementations.push({
|
|
62
|
-
path: relativeFilePath,
|
|
63
|
-
line: event.line,
|
|
64
|
-
function: event.functionName,
|
|
65
|
-
destination: event.source
|
|
66
|
-
});
|
|
100
|
+
// First process non-TypeScript files
|
|
101
|
+
await processFiles(otherFiles, allEvents, dirPath, customFunctionSignatures);
|
|
67
102
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
}
|
|
73
|
-
});
|
|
103
|
+
// Process TypeScript files with optimized batch processing
|
|
104
|
+
if (tsFiles.length > 0) {
|
|
105
|
+
const tsEvents = analyzeTsFiles(tsFiles, customFunctionSignatures);
|
|
106
|
+
tsEvents.forEach(event => addEventToCollection(allEvents, event, dirPath));
|
|
74
107
|
}
|
|
75
108
|
|
|
76
109
|
return allEvents;
|
|
@@ -3,40 +3,130 @@
|
|
|
3
3
|
* @module analyze/typescript
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
const { getProgram, findTrackingEvents, ProgramError, SourceFileError } = require('./parser');
|
|
6
|
+
const { getProgram, findTrackingEvents, ProgramError, SourceFileError, DEFAULT_COMPILER_OPTIONS } = require('./parser');
|
|
7
|
+
const ts = require('typescript');
|
|
8
|
+
const path = require('path');
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Creates a standalone TypeScript program for a single file
|
|
12
|
+
* This is used as a fallback when the main program can't resolve the file
|
|
13
|
+
* @param {string} filePath - Path to the TypeScript file
|
|
14
|
+
* @returns {Object} TypeScript program
|
|
15
|
+
*/
|
|
16
|
+
function createStandaloneProgram(filePath) {
|
|
17
|
+
const compilerOptions = {
|
|
18
|
+
...DEFAULT_COMPILER_OPTIONS,
|
|
19
|
+
// We intentionally allow module resolution here so that imported constants
|
|
20
|
+
// (e.g. event name strings defined in a sibling file) can be followed by the
|
|
21
|
+
// TypeScript compiler.
|
|
22
|
+
isolatedModules: true
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
return ts.createProgram([filePath], compilerOptions);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Deduplicates events based on source, eventName, line, and functionName
|
|
30
|
+
* @param {Array<Object>} events - Array of events to deduplicate
|
|
31
|
+
* @returns {Array<Object>} Deduplicated events
|
|
32
|
+
*/
|
|
33
|
+
function deduplicateEvents(events) {
|
|
34
|
+
const uniqueEvents = new Map();
|
|
35
|
+
|
|
36
|
+
for (const event of events) {
|
|
37
|
+
const key = `${event.source}|${event.eventName}|${event.line}|${event.functionName}`;
|
|
38
|
+
if (!uniqueEvents.has(key)) {
|
|
39
|
+
uniqueEvents.set(key, event);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return Array.from(uniqueEvents.values());
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Attempts to analyze a file using a standalone program as fallback
|
|
48
|
+
* @param {string} filePath - Path to the TypeScript file
|
|
49
|
+
* @param {Array} customFunctionSignatures - Custom function signatures to detect
|
|
50
|
+
* @returns {Array<Object>} Array of events or empty array if failed
|
|
51
|
+
*/
|
|
52
|
+
function tryStandaloneAnalysis(filePath, customFunctionSignatures) {
|
|
53
|
+
try {
|
|
54
|
+
console.warn(`Unable to resolve ${filePath} in main program. Attempting standalone analysis.`);
|
|
55
|
+
|
|
56
|
+
const standaloneProgram = createStandaloneProgram(filePath);
|
|
57
|
+
const sourceFile = standaloneProgram.getSourceFile(filePath);
|
|
58
|
+
|
|
59
|
+
if (!sourceFile) {
|
|
60
|
+
console.warn(`Standalone analysis failed: could not get source file for ${filePath}`);
|
|
61
|
+
return [];
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const checker = standaloneProgram.getTypeChecker();
|
|
65
|
+
const events = findTrackingEvents(sourceFile, checker, filePath, customFunctionSignatures || []);
|
|
66
|
+
|
|
67
|
+
return deduplicateEvents(events);
|
|
68
|
+
} catch (standaloneError) {
|
|
69
|
+
console.warn(`Standalone analysis failed for ${filePath}: ${standaloneError.message}`);
|
|
70
|
+
return [];
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Gets or creates a cached TypeScript program for efficient reuse
|
|
76
|
+
* @param {string} filePath - Path to the TypeScript file
|
|
77
|
+
* @param {Map} programCache - Map of tsconfig paths to programs
|
|
78
|
+
* @returns {Object} TypeScript program
|
|
79
|
+
*/
|
|
80
|
+
function getCachedTsProgram(filePath, programCache) {
|
|
81
|
+
// Locate nearest tsconfig.json (may be undefined)
|
|
82
|
+
const searchPath = path.dirname(filePath);
|
|
83
|
+
const configPath = ts.findConfigFile(searchPath, ts.sys.fileExists, 'tsconfig.json');
|
|
84
|
+
|
|
85
|
+
// We only cache when a tsconfig.json exists because the resulting program
|
|
86
|
+
// represents an entire project. If no config is present we build a
|
|
87
|
+
// stand-alone program that should not be reused for other files – otherwise
|
|
88
|
+
// later files would be missing from the program (which is precisely what
|
|
89
|
+
// caused the regression we are fixing).
|
|
90
|
+
const shouldCache = Boolean(configPath);
|
|
91
|
+
const cacheKey = configPath; // undefined when shouldCache is false
|
|
92
|
+
|
|
93
|
+
if (shouldCache && programCache.has(cacheKey)) {
|
|
94
|
+
return programCache.get(cacheKey);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const program = getProgram(filePath, null);
|
|
98
|
+
|
|
99
|
+
if (shouldCache) {
|
|
100
|
+
programCache.set(cacheKey, program);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return program;
|
|
104
|
+
}
|
|
7
105
|
|
|
8
106
|
/**
|
|
9
107
|
* Analyzes a TypeScript file for analytics tracking calls
|
|
10
108
|
* @param {string} filePath - Path to the TypeScript file to analyze
|
|
11
109
|
* @param {Object} [program] - Optional existing TypeScript program to reuse
|
|
12
|
-
* @param {
|
|
110
|
+
* @param {Array} [customFunctionSignatures] - Optional custom function signatures to detect
|
|
13
111
|
* @returns {Array<Object>} Array of tracking events found in the file
|
|
14
112
|
*/
|
|
15
113
|
function analyzeTsFile(filePath, program = null, customFunctionSignatures = null) {
|
|
16
114
|
try {
|
|
17
|
-
// Get or create TypeScript program
|
|
115
|
+
// Get or create TypeScript program
|
|
18
116
|
const tsProgram = getProgram(filePath, program);
|
|
19
117
|
|
|
20
118
|
// Get source file from program
|
|
21
119
|
const sourceFile = tsProgram.getSourceFile(filePath);
|
|
22
120
|
if (!sourceFile) {
|
|
23
|
-
|
|
121
|
+
// Try standalone analysis as fallback
|
|
122
|
+
return tryStandaloneAnalysis(filePath, customFunctionSignatures);
|
|
24
123
|
}
|
|
25
124
|
|
|
26
|
-
// Get type checker
|
|
125
|
+
// Get type checker and find tracking events
|
|
27
126
|
const checker = tsProgram.getTypeChecker();
|
|
28
|
-
|
|
29
|
-
// Single-pass collection covering built-in + all custom configs
|
|
30
127
|
const events = findTrackingEvents(sourceFile, checker, filePath, customFunctionSignatures || []);
|
|
31
128
|
|
|
32
|
-
|
|
33
|
-
const unique = new Map();
|
|
34
|
-
for (const evt of events) {
|
|
35
|
-
const key = `${evt.source}|${evt.eventName}|${evt.line}|${evt.functionName}`;
|
|
36
|
-
if (!unique.has(key)) unique.set(key, evt);
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
return Array.from(unique.values());
|
|
129
|
+
return deduplicateEvents(events);
|
|
40
130
|
|
|
41
131
|
} catch (error) {
|
|
42
132
|
if (error instanceof ProgramError) {
|
|
@@ -46,9 +136,37 @@ function analyzeTsFile(filePath, program = null, customFunctionSignatures = null
|
|
|
46
136
|
} else {
|
|
47
137
|
console.error(`Error analyzing TypeScript file ${filePath}: ${error.message}`);
|
|
48
138
|
}
|
|
139
|
+
|
|
140
|
+
return [];
|
|
49
141
|
}
|
|
142
|
+
}
|
|
50
143
|
|
|
51
|
-
|
|
144
|
+
/**
|
|
145
|
+
* Analyzes multiple TypeScript files with program reuse for better performance
|
|
146
|
+
* @param {Array<string>} tsFiles - Array of TypeScript file paths
|
|
147
|
+
* @param {Array} customFunctionSignatures - Custom function signatures to detect
|
|
148
|
+
* @returns {Array<Object>} Array of all tracking events found across all files
|
|
149
|
+
*/
|
|
150
|
+
function analyzeTsFiles(tsFiles, customFunctionSignatures) {
|
|
151
|
+
const allEvents = [];
|
|
152
|
+
const tsProgramCache = new Map(); // tsconfig path -> program
|
|
153
|
+
|
|
154
|
+
for (const file of tsFiles) {
|
|
155
|
+
try {
|
|
156
|
+
// Use cached program or create new one
|
|
157
|
+
const program = getCachedTsProgram(file, tsProgramCache);
|
|
158
|
+
const events = analyzeTsFile(file, program, customFunctionSignatures);
|
|
159
|
+
|
|
160
|
+
allEvents.push(...events);
|
|
161
|
+
} catch (error) {
|
|
162
|
+
console.warn(`Error processing TypeScript file ${file}: ${error.message}`);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
return allEvents;
|
|
52
167
|
}
|
|
53
168
|
|
|
54
|
-
module.exports = {
|
|
169
|
+
module.exports = {
|
|
170
|
+
analyzeTsFile,
|
|
171
|
+
analyzeTsFiles
|
|
172
|
+
};
|
|
@@ -32,6 +32,103 @@ class SourceFileError extends Error {
|
|
|
32
32
|
}
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
+
/**
|
|
36
|
+
* Default TypeScript compiler options for analysis
|
|
37
|
+
*/
|
|
38
|
+
const DEFAULT_COMPILER_OPTIONS = {
|
|
39
|
+
target: ts.ScriptTarget.Latest,
|
|
40
|
+
module: ts.ModuleKind.CommonJS,
|
|
41
|
+
allowJs: true,
|
|
42
|
+
checkJs: false,
|
|
43
|
+
noEmit: true,
|
|
44
|
+
jsx: ts.JsxEmit.Preserve,
|
|
45
|
+
moduleResolution: ts.ModuleResolutionKind.NodeJs,
|
|
46
|
+
allowSyntheticDefaultImports: true,
|
|
47
|
+
esModuleInterop: true,
|
|
48
|
+
skipLibCheck: true
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Maximum number of files to include in TypeScript program for performance
|
|
53
|
+
*/
|
|
54
|
+
const MAX_FILES_THRESHOLD = 10000;
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Attempts to parse tsconfig.json and extract compiler options and file names
|
|
58
|
+
* @param {string} configPath - Path to tsconfig.json
|
|
59
|
+
* @returns {Object|null} Parsed config with options and fileNames, or null if failed
|
|
60
|
+
*/
|
|
61
|
+
function parseTsConfig(configPath) {
|
|
62
|
+
try {
|
|
63
|
+
const readResult = ts.readConfigFile(configPath, ts.sys.readFile);
|
|
64
|
+
if (readResult.error || !readResult.config) {
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const parseResult = ts.parseJsonConfigFileContent(
|
|
69
|
+
readResult.config,
|
|
70
|
+
ts.sys,
|
|
71
|
+
path.dirname(configPath)
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
if (parseResult.errors && parseResult.errors.length > 0) {
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return {
|
|
79
|
+
options: parseResult.options,
|
|
80
|
+
fileNames: parseResult.fileNames
|
|
81
|
+
};
|
|
82
|
+
} catch (error) {
|
|
83
|
+
console.warn(`Failed to parse tsconfig.json at ${configPath}. Error: ${error.message}`);
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Determines the appropriate files to include in the TypeScript program
|
|
90
|
+
* @param {string} filePath - Target file path
|
|
91
|
+
* @param {string|null} configPath - Path to tsconfig.json if found
|
|
92
|
+
* @returns {Object} Configuration with compilerOptions and rootNames
|
|
93
|
+
*/
|
|
94
|
+
function getProgramConfiguration(filePath, configPath) {
|
|
95
|
+
let compilerOptions = { ...DEFAULT_COMPILER_OPTIONS };
|
|
96
|
+
let rootNames = [filePath];
|
|
97
|
+
|
|
98
|
+
if (!configPath) {
|
|
99
|
+
return { compilerOptions, rootNames };
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const config = parseTsConfig(configPath);
|
|
103
|
+
if (!config) {
|
|
104
|
+
console.warn(`Failed to parse tsconfig.json at ${configPath}. Analyzing ${filePath} in isolation.`);
|
|
105
|
+
return { compilerOptions, rootNames };
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Inherit compiler options from tsconfig
|
|
109
|
+
compilerOptions = { ...compilerOptions, ...config.options };
|
|
110
|
+
|
|
111
|
+
// Determine file inclusion strategy based on project size
|
|
112
|
+
const projectFileCount = config.fileNames.length;
|
|
113
|
+
|
|
114
|
+
if (projectFileCount > 0 && projectFileCount <= MAX_FILES_THRESHOLD) {
|
|
115
|
+
// Small to medium project: include all files for better type checking
|
|
116
|
+
rootNames = [...config.fileNames];
|
|
117
|
+
if (!rootNames.includes(filePath)) {
|
|
118
|
+
rootNames.push(filePath);
|
|
119
|
+
}
|
|
120
|
+
} else if (projectFileCount > MAX_FILES_THRESHOLD) {
|
|
121
|
+
// Large project: only include the target file to avoid performance issues
|
|
122
|
+
console.warn(
|
|
123
|
+
`Large TypeScript project detected (${projectFileCount} files). ` +
|
|
124
|
+
`Analyzing ${filePath} in isolation for performance.`
|
|
125
|
+
);
|
|
126
|
+
rootNames = [filePath];
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return { compilerOptions, rootNames };
|
|
130
|
+
}
|
|
131
|
+
|
|
35
132
|
/**
|
|
36
133
|
* Gets or creates a TypeScript program for analysis
|
|
37
134
|
* @param {string} filePath - Path to the TypeScript file
|
|
@@ -45,38 +142,15 @@ function getProgram(filePath, existingProgram) {
|
|
|
45
142
|
}
|
|
46
143
|
|
|
47
144
|
try {
|
|
48
|
-
//
|
|
145
|
+
// Find the nearest tsconfig.json
|
|
49
146
|
const searchPath = path.dirname(filePath);
|
|
50
147
|
const configPath = ts.findConfigFile(searchPath, ts.sys.fileExists, 'tsconfig.json');
|
|
51
148
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
module: ts.ModuleKind.CommonJS,
|
|
55
|
-
allowJs: true,
|
|
56
|
-
checkJs: false,
|
|
57
|
-
noEmit: true,
|
|
58
|
-
jsx: ts.JsxEmit.Preserve
|
|
59
|
-
};
|
|
60
|
-
let rootNames = [filePath];
|
|
61
|
-
|
|
62
|
-
if (configPath) {
|
|
63
|
-
// Read and parse the tsconfig.json
|
|
64
|
-
const readResult = ts.readConfigFile(configPath, ts.sys.readFile);
|
|
65
|
-
if (!readResult.error && readResult.config) {
|
|
66
|
-
const parseResult = ts.parseJsonConfigFileContent(
|
|
67
|
-
readResult.config,
|
|
68
|
-
ts.sys,
|
|
69
|
-
path.dirname(configPath)
|
|
70
|
-
);
|
|
71
|
-
if (!parseResult.errors || parseResult.errors.length === 0) {
|
|
72
|
-
compilerOptions = { ...compilerOptions, ...parseResult.options };
|
|
73
|
-
rootNames = parseResult.fileNames.length > 0 ? parseResult.fileNames : rootNames;
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
}
|
|
149
|
+
// Get program configuration
|
|
150
|
+
const { compilerOptions, rootNames } = getProgramConfiguration(filePath, configPath);
|
|
77
151
|
|
|
78
|
-
|
|
79
|
-
return
|
|
152
|
+
// Create and return the TypeScript program
|
|
153
|
+
return ts.createProgram(rootNames, compilerOptions);
|
|
80
154
|
} catch (error) {
|
|
81
155
|
throw new ProgramError(filePath, error);
|
|
82
156
|
}
|
|
@@ -94,27 +168,37 @@ function findTrackingEvents(sourceFile, checker, filePath, customConfigs = []) {
|
|
|
94
168
|
const events = [];
|
|
95
169
|
|
|
96
170
|
/**
|
|
97
|
-
*
|
|
98
|
-
*
|
|
171
|
+
* Tests if a CallExpression matches a custom function name
|
|
172
|
+
* @param {Object} callNode - The call expression node
|
|
173
|
+
* @param {string} functionName - Function name to match
|
|
174
|
+
* @returns {boolean} True if matches
|
|
99
175
|
*/
|
|
100
|
-
|
|
101
|
-
if (!
|
|
176
|
+
function matchesCustomFunction(callNode, functionName) {
|
|
177
|
+
if (!functionName || !callNode.expression) {
|
|
178
|
+
return false;
|
|
179
|
+
}
|
|
180
|
+
|
|
102
181
|
try {
|
|
103
|
-
return callNode.expression
|
|
182
|
+
return callNode.expression.getText() === functionName;
|
|
104
183
|
} catch {
|
|
105
184
|
return false;
|
|
106
185
|
}
|
|
107
|
-
}
|
|
186
|
+
}
|
|
108
187
|
|
|
188
|
+
/**
|
|
189
|
+
* Recursively visits AST nodes to find tracking calls
|
|
190
|
+
* @param {Object} node - Current AST node
|
|
191
|
+
*/
|
|
109
192
|
function visit(node) {
|
|
110
193
|
try {
|
|
111
194
|
if (ts.isCallExpression(node)) {
|
|
112
|
-
let
|
|
195
|
+
let matchedCustomConfig = null;
|
|
113
196
|
|
|
197
|
+
// Check for custom function matches
|
|
114
198
|
if (Array.isArray(customConfigs) && customConfigs.length > 0) {
|
|
115
|
-
for (const
|
|
116
|
-
if (
|
|
117
|
-
|
|
199
|
+
for (const config of customConfigs) {
|
|
200
|
+
if (config && matchesCustomFunction(node, config.functionName)) {
|
|
201
|
+
matchedCustomConfig = config;
|
|
118
202
|
break;
|
|
119
203
|
}
|
|
120
204
|
}
|
|
@@ -125,9 +209,12 @@ function findTrackingEvents(sourceFile, checker, filePath, customConfigs = []) {
|
|
|
125
209
|
sourceFile,
|
|
126
210
|
checker,
|
|
127
211
|
filePath,
|
|
128
|
-
|
|
212
|
+
matchedCustomConfig
|
|
129
213
|
);
|
|
130
|
-
|
|
214
|
+
|
|
215
|
+
if (event) {
|
|
216
|
+
events.push(event);
|
|
217
|
+
}
|
|
131
218
|
}
|
|
132
219
|
|
|
133
220
|
ts.forEachChild(node, visit);
|
|
@@ -137,7 +224,6 @@ function findTrackingEvents(sourceFile, checker, filePath, customConfigs = []) {
|
|
|
137
224
|
}
|
|
138
225
|
|
|
139
226
|
ts.forEachChild(sourceFile, visit);
|
|
140
|
-
|
|
141
227
|
return events;
|
|
142
228
|
}
|
|
143
229
|
|
|
@@ -172,5 +258,6 @@ module.exports = {
|
|
|
172
258
|
getProgram,
|
|
173
259
|
findTrackingEvents,
|
|
174
260
|
ProgramError,
|
|
175
|
-
SourceFileError
|
|
261
|
+
SourceFileError,
|
|
262
|
+
DEFAULT_COMPILER_OPTIONS
|
|
176
263
|
};
|