@flisk/analyze-tracking 0.7.3 → 0.7.5
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 +4 -0
- package/bin/cli.js +29 -1
- package/package.json +4 -2
- package/src/analyze/ruby/detectors.js +12 -1
- package/src/analyze/ruby/extractors.js +24 -12
- package/src/analyze/ruby/visitor.js +10 -1
- package/src/analyze/typescript/detectors/analytics-source.js +10 -5
- package/src/analyze/typescript/extractors/property-extractor.js +33 -0
- package/src/analyze/typescript/utils/function-finder.js +43 -18
- package/src/analyze/typescript/utils/type-resolver.js +16 -1
- package/src/index.js +7 -3
- package/src/utils/yamlGenerator.js +23 -4
package/README.md
CHANGED
|
@@ -29,6 +29,8 @@ npx @flisk/analyze-tracking /path/to/project [options]
|
|
|
29
29
|
- `-m, --model <model>`: Specify a model (ex: `gpt-4.1-nano`, `gpt-4o-mini`, `gemini-2.0-flash-lite-001`)
|
|
30
30
|
- `-o, --output <output_file>`: Name of the output file (default: `tracking-schema.yaml`)
|
|
31
31
|
- `-c, --customFunction <function_name>`: Specify a custom tracking function
|
|
32
|
+
- `--format <format>`: Output format, either `yaml` (default) or `json`. If an invalid value is provided, the CLI will exit with an error.
|
|
33
|
+
- `--stdout`: Print the output to the terminal instead of writing to a file (works with both YAML and JSON)
|
|
32
34
|
|
|
33
35
|
🔑 **Important:** If you are using `generateDescription`, you must set the appropriate credentials for the LLM provider you are using as an environment variable. OpenAI uses `OPENAI_API_KEY` and Google Vertex AI uses `GOOGLE_APPLICATION_CREDENTIALS`.
|
|
34
36
|
|
|
@@ -416,3 +418,5 @@ See [schema.json](schema.json) for a JSON Schema of the output.
|
|
|
416
418
|
|
|
417
419
|
## Contribute
|
|
418
420
|
We're actively improving this package. Found a bug? Have a feature request? Open an issue or submit a pull request!
|
|
421
|
+
|
|
422
|
+
[](https://join.slack.com/t/fliskcommunity/shared_invite/zt-354hesfnm-BbNzveERo9C4JwVQEWvXoA)
|
package/bin/cli.js
CHANGED
|
@@ -64,6 +64,17 @@ const optionDefinitions = [
|
|
|
64
64
|
alias: 'h',
|
|
65
65
|
type: Boolean,
|
|
66
66
|
},
|
|
67
|
+
{
|
|
68
|
+
name: 'stdout',
|
|
69
|
+
type: Boolean,
|
|
70
|
+
defaultValue: false,
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
name: 'format',
|
|
74
|
+
alias: 'f',
|
|
75
|
+
type: String,
|
|
76
|
+
defaultValue: 'yaml',
|
|
77
|
+
},
|
|
67
78
|
]
|
|
68
79
|
const options = commandLineArgs(optionDefinitions);
|
|
69
80
|
const {
|
|
@@ -77,6 +88,8 @@ const {
|
|
|
77
88
|
commitHash,
|
|
78
89
|
commitTimestamp,
|
|
79
90
|
help,
|
|
91
|
+
stdout,
|
|
92
|
+
format,
|
|
80
93
|
} = options;
|
|
81
94
|
|
|
82
95
|
if (help) {
|
|
@@ -113,4 +126,19 @@ if (generateDescription) {
|
|
|
113
126
|
}
|
|
114
127
|
}
|
|
115
128
|
|
|
116
|
-
|
|
129
|
+
if (format !== 'yaml' && format !== 'json') {
|
|
130
|
+
console.error(`Invalid format: ${format}. Please use --format yaml or --format json.`);
|
|
131
|
+
process.exit(1);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
run(
|
|
135
|
+
path.resolve(targetDir),
|
|
136
|
+
output,
|
|
137
|
+
customFunction,
|
|
138
|
+
customSourceDetails,
|
|
139
|
+
generateDescription,
|
|
140
|
+
provider,
|
|
141
|
+
model,
|
|
142
|
+
stdout,
|
|
143
|
+
format
|
|
144
|
+
);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@flisk/analyze-tracking",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.5",
|
|
4
4
|
"description": "Analyzes tracking code in a project and generates data schemas",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"bin": {
|
|
@@ -53,6 +53,8 @@
|
|
|
53
53
|
},
|
|
54
54
|
"devDependencies": {
|
|
55
55
|
"ajv": "^8.17.1",
|
|
56
|
-
"lodash": "^4.17.21"
|
|
56
|
+
"lodash": "^4.17.21",
|
|
57
|
+
"react": "^19.1.0",
|
|
58
|
+
"@types/react": "^19.1.6"
|
|
57
59
|
}
|
|
58
60
|
}
|
|
@@ -36,7 +36,18 @@ function detectSource(node, customFunction = null) {
|
|
|
36
36
|
if (node.name === 'track_struct_event') return 'snowplow';
|
|
37
37
|
|
|
38
38
|
// Custom tracking function
|
|
39
|
-
if (customFunction
|
|
39
|
+
if (customFunction) {
|
|
40
|
+
// Handle simple function names (e.g., 'customTrackFunction')
|
|
41
|
+
if (node.name === customFunction) return 'custom';
|
|
42
|
+
|
|
43
|
+
// Handle module-scoped function names (e.g., 'CustomModule.track')
|
|
44
|
+
if (customFunction.includes('.')) {
|
|
45
|
+
const [moduleName, methodName] = customFunction.split('.');
|
|
46
|
+
if (node.receiver && node.receiver.name === moduleName && node.name === methodName) {
|
|
47
|
+
return 'custom';
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
40
51
|
|
|
41
52
|
return null;
|
|
42
53
|
}
|
|
@@ -14,14 +14,17 @@ const { getValueType } = require('./types');
|
|
|
14
14
|
function extractEventName(node, source) {
|
|
15
15
|
if (source === 'segment' || source === 'rudderstack') {
|
|
16
16
|
// Both Segment and Rudderstack use the same format
|
|
17
|
-
const params = node.arguments_
|
|
17
|
+
const params = node.arguments_?.arguments_?.[0]?.elements;
|
|
18
|
+
if (!params || !Array.isArray(params)) {
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
18
21
|
const eventProperty = params.find(param => param?.key?.unescaped?.value === 'event');
|
|
19
22
|
return eventProperty?.value?.unescaped?.value || null;
|
|
20
23
|
}
|
|
21
24
|
|
|
22
25
|
if (source === 'mixpanel') {
|
|
23
26
|
// Mixpanel Ruby SDK format: tracker.track('distinct_id', 'event_name', {...})
|
|
24
|
-
const args = node.arguments_
|
|
27
|
+
const args = node.arguments_?.arguments_;
|
|
25
28
|
if (args && args.length > 1 && args[1]?.unescaped?.value) {
|
|
26
29
|
return args[1].unescaped.value;
|
|
27
30
|
}
|
|
@@ -29,8 +32,8 @@ function extractEventName(node, source) {
|
|
|
29
32
|
|
|
30
33
|
if (source === 'posthog') {
|
|
31
34
|
// PostHog Ruby SDK format: posthog.capture({distinct_id: '...', event: '...', properties: {...}})
|
|
32
|
-
const hashArg = node.arguments_
|
|
33
|
-
if (hashArg && hashArg.elements) {
|
|
35
|
+
const hashArg = node.arguments_?.arguments_?.[0];
|
|
36
|
+
if (hashArg && hashArg.elements && Array.isArray(hashArg.elements)) {
|
|
34
37
|
const eventProperty = hashArg.elements.find(elem => elem?.key?.unescaped?.value === 'event');
|
|
35
38
|
return eventProperty?.value?.unescaped?.value || null;
|
|
36
39
|
}
|
|
@@ -38,14 +41,17 @@ function extractEventName(node, source) {
|
|
|
38
41
|
|
|
39
42
|
if (source === 'snowplow') {
|
|
40
43
|
// Snowplow Ruby SDK: tracker.track_struct_event(category: '...', action: '...', ...)
|
|
41
|
-
const params = node.arguments_
|
|
44
|
+
const params = node.arguments_?.arguments_?.[0]?.elements;
|
|
45
|
+
if (!params || !Array.isArray(params)) {
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
42
48
|
const actionProperty = params.find(param => param?.key?.unescaped?.value === 'action');
|
|
43
49
|
return actionProperty?.value?.unescaped?.value || null;
|
|
44
50
|
}
|
|
45
51
|
|
|
46
52
|
if (source === 'custom') {
|
|
47
53
|
// Custom function format: customFunction('event_name', {...})
|
|
48
|
-
const args = node.arguments_
|
|
54
|
+
const args = node.arguments_?.arguments_;
|
|
49
55
|
if (args && args.length > 0 && args[0]?.unescaped?.value) {
|
|
50
56
|
return args[0].unescaped.value;
|
|
51
57
|
}
|
|
@@ -65,7 +71,10 @@ async function extractProperties(node, source) {
|
|
|
65
71
|
|
|
66
72
|
if (source === 'segment' || source === 'rudderstack') {
|
|
67
73
|
// Both Segment and Rudderstack use the same format
|
|
68
|
-
const params = node.arguments_
|
|
74
|
+
const params = node.arguments_?.arguments_?.[0]?.elements;
|
|
75
|
+
if (!params || !Array.isArray(params)) {
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
69
78
|
const properties = {};
|
|
70
79
|
|
|
71
80
|
// Process all top-level fields except 'event'
|
|
@@ -108,7 +117,7 @@ async function extractProperties(node, source) {
|
|
|
108
117
|
|
|
109
118
|
if (source === 'mixpanel') {
|
|
110
119
|
// Mixpanel Ruby SDK: tracker.track('distinct_id', 'event_name', {properties})
|
|
111
|
-
const args = node.arguments_
|
|
120
|
+
const args = node.arguments_?.arguments_;
|
|
112
121
|
const properties = {};
|
|
113
122
|
|
|
114
123
|
// Add distinct_id as property (even if it's a variable)
|
|
@@ -129,10 +138,10 @@ async function extractProperties(node, source) {
|
|
|
129
138
|
|
|
130
139
|
if (source === 'posthog') {
|
|
131
140
|
// PostHog Ruby SDK: posthog.capture({distinct_id: '...', event: '...', properties: {...}})
|
|
132
|
-
const hashArg = node.arguments_
|
|
141
|
+
const hashArg = node.arguments_?.arguments_?.[0];
|
|
133
142
|
const properties = {};
|
|
134
143
|
|
|
135
|
-
if (hashArg && hashArg.elements) {
|
|
144
|
+
if (hashArg && hashArg.elements && Array.isArray(hashArg.elements)) {
|
|
136
145
|
// Extract distinct_id if present
|
|
137
146
|
const distinctIdProperty = hashArg.elements.find(elem => elem?.key?.unescaped?.value === 'distinct_id');
|
|
138
147
|
if (distinctIdProperty?.value) {
|
|
@@ -154,7 +163,10 @@ async function extractProperties(node, source) {
|
|
|
154
163
|
|
|
155
164
|
if (source === 'snowplow') {
|
|
156
165
|
// Snowplow Ruby SDK: tracker.track_struct_event(category: '...', action: '...', ...)
|
|
157
|
-
const params = node.arguments_
|
|
166
|
+
const params = node.arguments_?.arguments_?.[0]?.elements;
|
|
167
|
+
if (!params || !Array.isArray(params)) {
|
|
168
|
+
return null;
|
|
169
|
+
}
|
|
158
170
|
const properties = {};
|
|
159
171
|
|
|
160
172
|
// Extract all struct event parameters except 'action' (which is used as the event name)
|
|
@@ -172,7 +184,7 @@ async function extractProperties(node, source) {
|
|
|
172
184
|
|
|
173
185
|
if (source === 'custom') {
|
|
174
186
|
// Custom function format: customFunction('event_name', {properties})
|
|
175
|
-
const args = node.arguments_
|
|
187
|
+
const args = node.arguments_?.arguments_;
|
|
176
188
|
if (args && args.length > 1 && args[1] instanceof HashNode) {
|
|
177
189
|
return await extractHashProperties(args[1]);
|
|
178
190
|
}
|
|
@@ -29,7 +29,16 @@ class TrackingVisitor {
|
|
|
29
29
|
if (!eventName) return;
|
|
30
30
|
|
|
31
31
|
const line = getLineNumber(this.code, node.location);
|
|
32
|
-
|
|
32
|
+
|
|
33
|
+
// For module-scoped custom functions, use the custom function name as the functionName
|
|
34
|
+
// For simple custom functions, use the wrapping function name
|
|
35
|
+
let functionName;
|
|
36
|
+
if (source === 'custom' && this.customFunction && this.customFunction.includes('.')) {
|
|
37
|
+
functionName = this.customFunction;
|
|
38
|
+
} else {
|
|
39
|
+
functionName = await findWrappingFunction(node, ancestors);
|
|
40
|
+
}
|
|
41
|
+
|
|
33
42
|
const properties = await extractProperties(node, source);
|
|
34
43
|
|
|
35
44
|
this.events.push({
|
|
@@ -44,8 +44,13 @@ function detectAnalyticsSource(node, customFunction) {
|
|
|
44
44
|
* @returns {boolean}
|
|
45
45
|
*/
|
|
46
46
|
function isCustomFunction(node, customFunction) {
|
|
47
|
-
|
|
48
|
-
|
|
47
|
+
const canBeCustomFunction = ts.isIdentifier(node.expression) ||
|
|
48
|
+
ts.isPropertyAccessExpression(node.expression) ||
|
|
49
|
+
ts.isCallExpression(node.expression) || // For chained calls like getTracker().track()
|
|
50
|
+
ts.isElementAccessExpression(node.expression) || // For array/object access like trackers['analytics'].track()
|
|
51
|
+
(ts.isPropertyAccessExpression(node.expression?.expression) && ts.isThisExpression(node.expression.expression.expression)); // For class methods like this.analytics.track()
|
|
52
|
+
|
|
53
|
+
return canBeCustomFunction && node.expression.getText() === customFunction;
|
|
49
54
|
}
|
|
50
55
|
|
|
51
56
|
/**
|
|
@@ -59,7 +64,7 @@ function detectFunctionBasedProvider(node) {
|
|
|
59
64
|
}
|
|
60
65
|
|
|
61
66
|
const functionName = node.expression.escapedText;
|
|
62
|
-
|
|
67
|
+
|
|
63
68
|
for (const provider of Object.values(ANALYTICS_PROVIDERS)) {
|
|
64
69
|
if (provider.type === 'function' && provider.functionName === functionName) {
|
|
65
70
|
return provider.name;
|
|
@@ -79,8 +84,8 @@ function detectMemberBasedProvider(node) {
|
|
|
79
84
|
return 'unknown';
|
|
80
85
|
}
|
|
81
86
|
|
|
82
|
-
const objectName = node.expression.expression
|
|
83
|
-
const methodName = node.expression.name
|
|
87
|
+
const objectName = node.expression.expression?.escapedText;
|
|
88
|
+
const methodName = node.expression.name?.escapedText;
|
|
84
89
|
|
|
85
90
|
if (!objectName || !methodName) {
|
|
86
91
|
return 'unknown';
|
|
@@ -116,6 +116,38 @@ function extractShorthandPropertySchema(checker, prop) {
|
|
|
116
116
|
if (!symbol) {
|
|
117
117
|
return { type: 'any' };
|
|
118
118
|
}
|
|
119
|
+
const declarations = symbol.declarations || [];
|
|
120
|
+
for (const decl of declarations) {
|
|
121
|
+
// Detect destructuring from useState: const [state, setState] = useState<Type>(...)
|
|
122
|
+
if (
|
|
123
|
+
ts.isBindingElement(decl) &&
|
|
124
|
+
decl.parent &&
|
|
125
|
+
ts.isArrayBindingPattern(decl.parent) &&
|
|
126
|
+
decl.parent.parent &&
|
|
127
|
+
ts.isVariableDeclaration(decl.parent.parent) &&
|
|
128
|
+
decl.parent.parent.initializer &&
|
|
129
|
+
ts.isCallExpression(decl.parent.parent.initializer) &&
|
|
130
|
+
ts.isIdentifier(decl.parent.parent.initializer.expression) &&
|
|
131
|
+
decl.parent.parent.initializer.expression.escapedText === 'useState'
|
|
132
|
+
) {
|
|
133
|
+
// Try to get type from generic argument
|
|
134
|
+
const callExpr = decl.parent.parent.initializer;
|
|
135
|
+
if (callExpr.typeArguments && callExpr.typeArguments.length > 0) {
|
|
136
|
+
const typeNode = callExpr.typeArguments[0];
|
|
137
|
+
const type = checker.getTypeFromTypeNode(typeNode);
|
|
138
|
+
const typeString = checker.typeToString(type);
|
|
139
|
+
return resolveTypeToProperties(checker, typeString);
|
|
140
|
+
}
|
|
141
|
+
// Fallback: get type from initial value
|
|
142
|
+
if (callExpr.arguments && callExpr.arguments.length > 0) {
|
|
143
|
+
const initType = checker.getTypeAtLocation(callExpr.arguments[0]);
|
|
144
|
+
const typeString = checker.typeToString(initType);
|
|
145
|
+
return resolveTypeToProperties(checker, typeString);
|
|
146
|
+
}
|
|
147
|
+
// Default to any
|
|
148
|
+
return { type: 'any' };
|
|
149
|
+
}
|
|
150
|
+
}
|
|
119
151
|
|
|
120
152
|
const propType = checker.getTypeAtLocation(prop.name);
|
|
121
153
|
const typeString = checker.typeToString(propType);
|
|
@@ -318,6 +350,7 @@ function resolveTypeSchema(checker, typeString) {
|
|
|
318
350
|
* @returns {string|null} Literal type or null
|
|
319
351
|
*/
|
|
320
352
|
function getLiteralType(node) {
|
|
353
|
+
if (!node) return null;
|
|
321
354
|
if (ts.isStringLiteral(node)) return 'string';
|
|
322
355
|
if (ts.isNumericLiteral(node)) return 'number';
|
|
323
356
|
if (node.kind === ts.SyntaxKind.TrueKeyword || node.kind === ts.SyntaxKind.FalseKeyword) return 'boolean';
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
const ts = require('typescript');
|
|
7
|
+
const { isReactHookCall } = require('./type-resolver');
|
|
7
8
|
|
|
8
9
|
/**
|
|
9
10
|
* Finds the name of the function that wraps a given node
|
|
@@ -12,17 +13,17 @@ const ts = require('typescript');
|
|
|
12
13
|
*/
|
|
13
14
|
function findWrappingFunction(node) {
|
|
14
15
|
let current = node;
|
|
15
|
-
|
|
16
|
+
|
|
16
17
|
while (current) {
|
|
17
18
|
const functionName = extractFunctionName(current);
|
|
18
|
-
|
|
19
|
+
|
|
19
20
|
if (functionName) {
|
|
20
21
|
return functionName;
|
|
21
22
|
}
|
|
22
|
-
|
|
23
|
+
|
|
23
24
|
current = current.parent;
|
|
24
25
|
}
|
|
25
|
-
|
|
26
|
+
|
|
26
27
|
return 'global';
|
|
27
28
|
}
|
|
28
29
|
|
|
@@ -36,27 +37,27 @@ function extractFunctionName(node) {
|
|
|
36
37
|
if (ts.isFunctionDeclaration(node)) {
|
|
37
38
|
return node.name ? node.name.escapedText : 'anonymous';
|
|
38
39
|
}
|
|
39
|
-
|
|
40
|
+
|
|
40
41
|
// Method declaration in class
|
|
41
42
|
if (ts.isMethodDeclaration(node)) {
|
|
42
43
|
return node.name ? node.name.escapedText : 'anonymous';
|
|
43
44
|
}
|
|
44
|
-
|
|
45
|
+
|
|
45
46
|
// Arrow function or function expression
|
|
46
47
|
if (ts.isArrowFunction(node) || ts.isFunctionExpression(node)) {
|
|
47
48
|
return findParentFunctionName(node) || 'anonymous';
|
|
48
49
|
}
|
|
49
|
-
|
|
50
|
+
|
|
50
51
|
// Constructor
|
|
51
52
|
if (ts.isConstructorDeclaration(node)) {
|
|
52
53
|
return 'constructor';
|
|
53
54
|
}
|
|
54
|
-
|
|
55
|
+
|
|
55
56
|
// Getter/Setter
|
|
56
57
|
if (ts.isGetAccessorDeclaration(node) || ts.isSetAccessorDeclaration(node)) {
|
|
57
58
|
return node.name ? `${ts.isGetAccessorDeclaration(node) ? 'get' : 'set'} ${node.name.escapedText}` : 'anonymous';
|
|
58
59
|
}
|
|
59
|
-
|
|
60
|
+
|
|
60
61
|
return null;
|
|
61
62
|
}
|
|
62
63
|
|
|
@@ -67,14 +68,38 @@ function extractFunctionName(node) {
|
|
|
67
68
|
*/
|
|
68
69
|
function findParentFunctionName(node) {
|
|
69
70
|
const parent = node.parent;
|
|
70
|
-
|
|
71
|
+
|
|
71
72
|
if (!parent) return null;
|
|
72
|
-
|
|
73
|
+
|
|
74
|
+
if (
|
|
75
|
+
ts.isCallExpression(parent) &&
|
|
76
|
+
ts.isIdentifier(parent.expression) &&
|
|
77
|
+
isReactHookCall(parent)
|
|
78
|
+
) {
|
|
79
|
+
if (
|
|
80
|
+
parent.parent &&
|
|
81
|
+
ts.isVariableDeclaration(parent.parent) &&
|
|
82
|
+
parent.parent.name
|
|
83
|
+
) {
|
|
84
|
+
return `${parent.expression.escapedText}(${parent.parent.name.escapedText})`;
|
|
85
|
+
}
|
|
86
|
+
return `${parent.expression.escapedText}()`;
|
|
87
|
+
}
|
|
88
|
+
|
|
73
89
|
// Variable declaration: const myFunc = () => {}
|
|
74
90
|
if (ts.isVariableDeclaration(parent) && parent.name) {
|
|
91
|
+
// Check if initializer is a recognized React hook call
|
|
92
|
+
if (
|
|
93
|
+
parent.initializer &&
|
|
94
|
+
ts.isCallExpression(parent.initializer) &&
|
|
95
|
+
ts.isIdentifier(parent.initializer.expression) &&
|
|
96
|
+
isReactHookCall(parent.initializer)
|
|
97
|
+
) {
|
|
98
|
+
return `${parent.initializer.expression.escapedText}(${parent.name.escapedText})`;
|
|
99
|
+
}
|
|
75
100
|
return parent.name.escapedText;
|
|
76
101
|
}
|
|
77
|
-
|
|
102
|
+
|
|
78
103
|
// Property assignment: { myFunc: () => {} }
|
|
79
104
|
if (ts.isPropertyAssignment(parent) && parent.name) {
|
|
80
105
|
if (ts.isIdentifier(parent.name)) {
|
|
@@ -84,20 +109,20 @@ function findParentFunctionName(node) {
|
|
|
84
109
|
return parent.name.text;
|
|
85
110
|
}
|
|
86
111
|
}
|
|
87
|
-
|
|
112
|
+
|
|
88
113
|
// Method property in object literal: { myFunc() {} }
|
|
89
114
|
if (ts.isMethodDeclaration(parent) && parent.name) {
|
|
90
115
|
return parent.name.escapedText;
|
|
91
116
|
}
|
|
92
|
-
|
|
117
|
+
|
|
93
118
|
// Binary expression assignment: obj.myFunc = () => {}
|
|
94
|
-
if (ts.isBinaryExpression(parent) &&
|
|
95
|
-
|
|
119
|
+
if (ts.isBinaryExpression(parent) &&
|
|
120
|
+
parent.operatorToken.kind === ts.SyntaxKind.EqualsToken) {
|
|
96
121
|
if (ts.isPropertyAccessExpression(parent.left)) {
|
|
97
122
|
return parent.left.name.escapedText;
|
|
98
123
|
}
|
|
99
124
|
}
|
|
100
|
-
|
|
125
|
+
|
|
101
126
|
// Call expression argument: someFunc(() => {})
|
|
102
127
|
if (ts.isCallExpression(parent)) {
|
|
103
128
|
const argIndex = parent.arguments.indexOf(node);
|
|
@@ -105,7 +130,7 @@ function findParentFunctionName(node) {
|
|
|
105
130
|
return `anonymous-callback-${argIndex}`;
|
|
106
131
|
}
|
|
107
132
|
}
|
|
108
|
-
|
|
133
|
+
|
|
109
134
|
return null;
|
|
110
135
|
}
|
|
111
136
|
|
|
@@ -184,10 +184,25 @@ function getBasicTypeOfArrayElement(checker, element) {
|
|
|
184
184
|
return 'any';
|
|
185
185
|
}
|
|
186
186
|
|
|
187
|
+
/**
|
|
188
|
+
* Checks if a CallExpression is a React hook (useCallback, useState, etc)
|
|
189
|
+
* @param {Object} node - CallExpression node
|
|
190
|
+
* @param {string[]} hookNames - List of hook names to check
|
|
191
|
+
* @returns {boolean}
|
|
192
|
+
*/
|
|
193
|
+
function isReactHookCall(node, hookNames = ['useCallback', 'useState', 'useEffect', 'useMemo', 'useReducer']) {
|
|
194
|
+
if (!node || !node.expression) return false;
|
|
195
|
+
if (ts.isIdentifier(node.expression)) {
|
|
196
|
+
return hookNames.includes(node.expression.escapedText);
|
|
197
|
+
}
|
|
198
|
+
return false;
|
|
199
|
+
}
|
|
200
|
+
|
|
187
201
|
module.exports = {
|
|
188
202
|
resolveIdentifierToInitializer,
|
|
189
203
|
getTypeOfNode,
|
|
190
204
|
resolveTypeToProperties,
|
|
191
205
|
isCustomType,
|
|
192
|
-
getBasicTypeOfArrayElement
|
|
206
|
+
getBasicTypeOfArrayElement,
|
|
207
|
+
isReactHookCall
|
|
193
208
|
};
|
package/src/index.js
CHANGED
|
@@ -5,13 +5,13 @@
|
|
|
5
5
|
|
|
6
6
|
const { analyzeDirectory } = require('./analyze');
|
|
7
7
|
const { getRepoDetails } = require('./utils/repoDetails');
|
|
8
|
-
const { generateYamlSchema } = require('./utils/yamlGenerator');
|
|
8
|
+
const { generateYamlSchema, generateJsonSchema } = require('./utils/yamlGenerator');
|
|
9
9
|
const { generateDescriptions } = require('./generateDescriptions');
|
|
10
10
|
|
|
11
11
|
const { ChatOpenAI } = require('@langchain/openai');
|
|
12
12
|
const { ChatVertexAI } = require('@langchain/google-vertexai');
|
|
13
13
|
|
|
14
|
-
async function run(targetDir, outputPath, customFunction, customSourceDetails, generateDescription, provider, model) {
|
|
14
|
+
async function run(targetDir, outputPath, customFunction, customSourceDetails, generateDescription, provider, model, stdout, format) {
|
|
15
15
|
let events = await analyzeDirectory(targetDir, customFunction);
|
|
16
16
|
if (generateDescription) {
|
|
17
17
|
let llm;
|
|
@@ -35,7 +35,11 @@ async function run(targetDir, outputPath, customFunction, customSourceDetails, g
|
|
|
35
35
|
events = await generateDescriptions(events, targetDir, llm);
|
|
36
36
|
}
|
|
37
37
|
const repoDetails = await getRepoDetails(targetDir, customSourceDetails);
|
|
38
|
-
|
|
38
|
+
if (format === 'json') {
|
|
39
|
+
generateJsonSchema(events, repoDetails, outputPath, stdout);
|
|
40
|
+
} else {
|
|
41
|
+
generateYamlSchema(events, repoDetails, outputPath, stdout);
|
|
42
|
+
}
|
|
39
43
|
}
|
|
40
44
|
|
|
41
45
|
module.exports = { run };
|
|
@@ -9,7 +9,7 @@ const yaml = require('js-yaml');
|
|
|
9
9
|
const VERSION = 1
|
|
10
10
|
const SCHEMA_URL = "https://raw.githubusercontent.com/fliskdata/analyze-tracking/main/schema.json";
|
|
11
11
|
|
|
12
|
-
function generateYamlSchema(events, repository, outputPath) {
|
|
12
|
+
function generateYamlSchema(events, repository, outputPath, stdout = false) {
|
|
13
13
|
const schema = {
|
|
14
14
|
version: VERSION,
|
|
15
15
|
source: repository,
|
|
@@ -21,8 +21,27 @@ function generateYamlSchema(events, repository, outputPath) {
|
|
|
21
21
|
};
|
|
22
22
|
const yamlOutput = yaml.dump(schema, options);
|
|
23
23
|
const yamlFile = `# yaml-language-server: $schema=${SCHEMA_URL}\n${yamlOutput}`;
|
|
24
|
-
|
|
25
|
-
|
|
24
|
+
if (stdout) {
|
|
25
|
+
process.stdout.write(yamlFile);
|
|
26
|
+
} else {
|
|
27
|
+
fs.writeFileSync(outputPath, yamlFile, 'utf8');
|
|
28
|
+
console.log(`Tracking schema YAML file generated: ${outputPath}`);
|
|
29
|
+
}
|
|
26
30
|
}
|
|
27
31
|
|
|
28
|
-
|
|
32
|
+
function generateJsonSchema(events, repository, outputPath, stdout = false) {
|
|
33
|
+
const schema = {
|
|
34
|
+
version: VERSION,
|
|
35
|
+
source: repository,
|
|
36
|
+
events,
|
|
37
|
+
};
|
|
38
|
+
const jsonFile = JSON.stringify(schema, null, 2);
|
|
39
|
+
if (stdout) {
|
|
40
|
+
process.stdout.write(jsonFile);
|
|
41
|
+
} else {
|
|
42
|
+
fs.writeFileSync(outputPath, jsonFile, 'utf8');
|
|
43
|
+
console.log(`Tracking schema JSON file generated: ${outputPath}`);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
module.exports = { generateYamlSchema, generateJsonSchema };
|