@eventra_dev/eventra-cli 0.1.26 → 0.1.28
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 +62 -50
- package/dist/engine/scanner.js +16 -21
- package/dist/utils/universal.js +44 -22
- package/package.json +1 -1
- package/src/engine/scanner.ts +16 -26
- package/src/utils/universal.ts +62 -22
package/README.md
CHANGED
|
@@ -9,23 +9,29 @@
|
|
|
9
9
|
[](https://www.typescriptlang.org/)
|
|
10
10
|
[]()
|
|
11
11
|
|
|
12
|
-
Eventra CLI automatically discovers
|
|
12
|
+
Eventra CLI automatically discovers and tracks event usage across your codebase — even through abstractions like wrappers, variables, and cross-file calls.
|
|
13
13
|
|
|
14
14
|
---
|
|
15
15
|
|
|
16
|
-
|
|
16
|
+
## Overview
|
|
17
17
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
18
|
+
Eventra scans your codebase using a TypeScript-powered engine and extracts event usage — even through abstractions like wrappers, variables, and cross-file calls.
|
|
19
|
+
|
|
20
|
+
It requires no runtime instrumentation and fits naturally into CI/CD workflows.
|
|
21
|
+
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
## Installation
|
|
21
25
|
|
|
22
26
|
```bash
|
|
27
|
+
npm install -D @eventra_dev/eventra-cli
|
|
28
|
+
# or
|
|
23
29
|
pnpm add -D @eventra_dev/eventra-cli
|
|
24
30
|
```
|
|
25
31
|
|
|
26
32
|
---
|
|
27
33
|
|
|
28
|
-
|
|
34
|
+
## Quick Start
|
|
29
35
|
|
|
30
36
|
```bash
|
|
31
37
|
eventra init
|
|
@@ -36,22 +42,43 @@ eventra send
|
|
|
36
42
|
|
|
37
43
|
---
|
|
38
44
|
|
|
39
|
-
|
|
45
|
+
## How it works
|
|
40
46
|
|
|
41
|
-
|
|
47
|
+
1. Parses your project using the TypeScript AST
|
|
48
|
+
2. Resolves function calls, imports, and dependencies
|
|
49
|
+
3. Tracks event propagation through variables and wrappers
|
|
50
|
+
4. Extracts event names and aggregates them
|
|
42
51
|
|
|
43
|
-
|
|
52
|
+
No runtime required. No SDK needed.
|
|
44
53
|
|
|
45
54
|
---
|
|
46
55
|
|
|
47
|
-
##
|
|
56
|
+
## Why Eventra
|
|
57
|
+
|
|
58
|
+
- Prevent missing or inconsistent event tracking
|
|
59
|
+
- Keep your analytics schema in sync with code
|
|
60
|
+
- Detect dead or unused events
|
|
61
|
+
- Integrate validation into CI pipelines
|
|
62
|
+
|
|
63
|
+
---
|
|
48
64
|
|
|
49
|
-
|
|
65
|
+
## Commands
|
|
50
66
|
|
|
51
|
-
###
|
|
67
|
+
### `eventra init`
|
|
68
|
+
Creates `eventra.json` configuration file.
|
|
69
|
+
|
|
70
|
+
---
|
|
52
71
|
|
|
53
|
-
|
|
54
|
-
|
|
72
|
+
### `eventra sync`
|
|
73
|
+
Scans your project and updates the list of detected events.
|
|
74
|
+
|
|
75
|
+
#### Supports
|
|
76
|
+
|
|
77
|
+
- Direct calls
|
|
78
|
+
```ts
|
|
79
|
+
track("event")
|
|
80
|
+
analytics.track("event")
|
|
81
|
+
```
|
|
55
82
|
|
|
56
83
|
- Variables
|
|
57
84
|
```ts
|
|
@@ -74,7 +101,7 @@ track(`click_${type}`)
|
|
|
74
101
|
track(condition ? "a" : "b")
|
|
75
102
|
```
|
|
76
103
|
|
|
77
|
-
- Object
|
|
104
|
+
- Object payloads
|
|
78
105
|
```ts
|
|
79
106
|
track({ event: "click" })
|
|
80
107
|
```
|
|
@@ -88,36 +115,27 @@ const trackFeature = (name: string) => {
|
|
|
88
115
|
trackFeature("click")
|
|
89
116
|
```
|
|
90
117
|
|
|
91
|
-
-
|
|
92
|
-
|
|
93
|
-
trackFeature("test", "click")
|
|
94
|
-
```
|
|
95
|
-
|
|
96
|
-
- Cross-file tracking
|
|
97
|
-
|
|
98
|
-
- Vue / Svelte / Astro support
|
|
118
|
+
- Cross-file event resolution
|
|
119
|
+
- Framework templates (Vue, Svelte, Astro)
|
|
99
120
|
|
|
100
121
|
---
|
|
101
122
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
Validate events without modifying config.
|
|
123
|
+
### `eventra check`
|
|
124
|
+
Validates detected events against config.
|
|
105
125
|
|
|
106
126
|
```bash
|
|
107
127
|
eventra check
|
|
108
128
|
```
|
|
109
129
|
|
|
110
|
-
|
|
111
|
-
|
|
130
|
+
Detects:
|
|
112
131
|
- New events
|
|
113
132
|
- Removed events
|
|
114
|
-
-
|
|
133
|
+
- Inconsistent tracking usage
|
|
115
134
|
|
|
116
135
|
---
|
|
117
136
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
Auto-fix mode.
|
|
137
|
+
### `eventra check --fix`
|
|
138
|
+
Automatically updates config to match detected events.
|
|
121
139
|
|
|
122
140
|
```bash
|
|
123
141
|
eventra check --fix
|
|
@@ -125,9 +143,8 @@ eventra check --fix
|
|
|
125
143
|
|
|
126
144
|
---
|
|
127
145
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
Real-time event detection.
|
|
146
|
+
### `eventra watch`
|
|
147
|
+
Runs in watch mode and updates events in real time.
|
|
131
148
|
|
|
132
149
|
```bash
|
|
133
150
|
eventra watch
|
|
@@ -135,9 +152,8 @@ eventra watch
|
|
|
135
152
|
|
|
136
153
|
---
|
|
137
154
|
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
Send events to backend.
|
|
155
|
+
### `eventra send`
|
|
156
|
+
Sends events to Eventra backend.
|
|
141
157
|
|
|
142
158
|
```bash
|
|
143
159
|
eventra send
|
|
@@ -145,11 +161,12 @@ eventra send
|
|
|
145
161
|
|
|
146
162
|
---
|
|
147
163
|
|
|
148
|
-
|
|
164
|
+
## Configuration
|
|
149
165
|
|
|
150
166
|
```json
|
|
151
167
|
{
|
|
152
168
|
"apiKey": "",
|
|
169
|
+
"endpoint": "",
|
|
153
170
|
"events": [],
|
|
154
171
|
"wrappers": [],
|
|
155
172
|
"functionWrappers": [],
|
|
@@ -163,25 +180,20 @@ eventra send
|
|
|
163
180
|
|
|
164
181
|
---
|
|
165
182
|
|
|
166
|
-
|
|
183
|
+
## Supported Environments
|
|
184
|
+
|
|
185
|
+
Eventra works with **any JavaScript or TypeScript codebase**.
|
|
167
186
|
|
|
168
|
-
|
|
169
|
-
- Vue
|
|
170
|
-
- Svelte
|
|
171
|
-
- Astro
|
|
172
|
-
- Node.js
|
|
173
|
-
- Express
|
|
174
|
-
- NestJS
|
|
175
|
-
- Fastify
|
|
187
|
+
No framework limitations.
|
|
176
188
|
|
|
177
189
|
---
|
|
178
190
|
|
|
179
|
-
|
|
191
|
+
## Requirements
|
|
180
192
|
|
|
181
193
|
- Node.js 18+
|
|
182
194
|
|
|
183
195
|
---
|
|
184
196
|
|
|
185
|
-
|
|
197
|
+
## License
|
|
186
198
|
|
|
187
199
|
MIT
|
package/dist/engine/scanner.js
CHANGED
|
@@ -9,7 +9,6 @@ const resolver_1 = require("./resolver");
|
|
|
9
9
|
const callResolver_1 = require("./callResolver");
|
|
10
10
|
const exportResolver_1 = require("./exportResolver");
|
|
11
11
|
const functionAnalyzer_1 = require("./functionAnalyzer");
|
|
12
|
-
const boundary_1 = require("./boundary");
|
|
13
12
|
const trackingDetector_1 = require("./trackingDetector");
|
|
14
13
|
function getCallName(expr) {
|
|
15
14
|
if (typescript_1.default.isIdentifier(expr))
|
|
@@ -70,35 +69,30 @@ function scanSource(source, checker, config, visited = new Set(), depth = 0, inh
|
|
|
70
69
|
const events = new Set();
|
|
71
70
|
const detectedFunctionWrappers = new Set();
|
|
72
71
|
const detectedComponentWrappers = new Map();
|
|
73
|
-
if (visited.has(source.fileName)
|
|
74
|
-
depth > 10 ||
|
|
75
|
-
visited.size > 200) {
|
|
72
|
+
if (visited.size > 200 || visited.has(source.fileName)) {
|
|
76
73
|
return { events, detectedFunctionWrappers, detectedComponentWrappers };
|
|
77
74
|
}
|
|
78
75
|
visited.add(source.fileName);
|
|
79
76
|
function visit(node) {
|
|
80
77
|
// CALL EXPRESSIONS
|
|
81
78
|
if (typescript_1.default.isCallExpression(node)) {
|
|
82
|
-
const
|
|
83
|
-
const name = getCallName(expr);
|
|
79
|
+
const name = getCallName(node.expression);
|
|
84
80
|
const isKnownWrapper = config.functionWrappers.some(w => w.name === name);
|
|
85
81
|
const isTrackingByType = (0, trackingDetector_1.isTrackingCall)(node, checker);
|
|
86
|
-
let resolvedFn = (0, callResolver_1.resolveFunctionFromCall)(
|
|
82
|
+
let resolvedFn = (0, callResolver_1.resolveFunctionFromCall)(node.expression, checker);
|
|
87
83
|
const isAutoWrapper = resolvedFn && (0, functionAnalyzer_1.findTrackParamIndex)(resolvedFn) !== null;
|
|
88
84
|
let isTracking = isKnownWrapper ||
|
|
89
85
|
isTrackingByType ||
|
|
90
|
-
|
|
91
|
-
(
|
|
92
|
-
|
|
93
|
-
!(0, boundary_1.isExternalFile)(resolvedFn.getSourceFile().fileName));
|
|
94
|
-
// fallback
|
|
86
|
+
isAutoWrapper ||
|
|
87
|
+
name.startsWith("$");
|
|
88
|
+
// fallback track(...)
|
|
95
89
|
if (!isTracking) {
|
|
96
|
-
if (typescript_1.default.isPropertyAccessExpression(
|
|
97
|
-
|
|
90
|
+
if (typescript_1.default.isPropertyAccessExpression(node.expression) &&
|
|
91
|
+
node.expression.name.text === "track") {
|
|
98
92
|
isTracking = true;
|
|
99
93
|
}
|
|
100
|
-
else if (typescript_1.default.isIdentifier(
|
|
101
|
-
|
|
94
|
+
else if (typescript_1.default.isIdentifier(node.expression) &&
|
|
95
|
+
node.expression.text === "track") {
|
|
102
96
|
isTracking = true;
|
|
103
97
|
}
|
|
104
98
|
}
|
|
@@ -106,13 +100,14 @@ function scanSource(source, checker, config, visited = new Set(), depth = 0, inh
|
|
|
106
100
|
typescript_1.default.forEachChild(node, visit);
|
|
107
101
|
return;
|
|
108
102
|
}
|
|
109
|
-
//
|
|
103
|
+
// DETECT WRAPPERS
|
|
110
104
|
if (!isKnownWrapper &&
|
|
111
105
|
isAutoWrapper &&
|
|
112
106
|
!isTrackingByType &&
|
|
113
107
|
!name.startsWith("track")) {
|
|
114
108
|
detectedFunctionWrappers.add(name);
|
|
115
109
|
}
|
|
110
|
+
// PARAM MAP
|
|
116
111
|
const paramMap = new Map(inheritedParamMap ?? []);
|
|
117
112
|
if (resolvedFn && typescript_1.default.isFunctionLike(resolvedFn)) {
|
|
118
113
|
resolvedFn.parameters.forEach((param, i) => {
|
|
@@ -129,7 +124,7 @@ function scanSource(source, checker, config, visited = new Set(), depth = 0, inh
|
|
|
129
124
|
if (resolvedFn) {
|
|
130
125
|
const idx = (0, functionAnalyzer_1.findTrackParamIndex)(resolvedFn);
|
|
131
126
|
if (idx !== null && node.arguments[idx]) {
|
|
132
|
-
const res = (0, resolver_1.resolveNodeValue)(
|
|
127
|
+
const res = (0, resolver_1.resolveNodeValue)(node.arguments[idx], checker, paramMap);
|
|
133
128
|
res.values.forEach(v => {
|
|
134
129
|
if (isValidEvent(v))
|
|
135
130
|
events.add(v);
|
|
@@ -137,10 +132,10 @@ function scanSource(source, checker, config, visited = new Set(), depth = 0, inh
|
|
|
137
132
|
handled = true;
|
|
138
133
|
}
|
|
139
134
|
}
|
|
140
|
-
//
|
|
135
|
+
// fallback
|
|
141
136
|
if (!handled) {
|
|
142
137
|
for (const arg of node.arguments) {
|
|
143
|
-
const res = (0, resolver_1.resolveNodeValue)(
|
|
138
|
+
const res = (0, resolver_1.resolveNodeValue)(arg, checker, paramMap);
|
|
144
139
|
res.values.forEach(v => {
|
|
145
140
|
if (isValidEvent(v))
|
|
146
141
|
events.add(v);
|
|
@@ -156,7 +151,7 @@ function scanSource(source, checker, config, visited = new Set(), depth = 0, inh
|
|
|
156
151
|
}
|
|
157
152
|
}
|
|
158
153
|
}
|
|
159
|
-
// JSX
|
|
154
|
+
// JSX
|
|
160
155
|
if (typescript_1.default.isJsxSelfClosingElement(node) ||
|
|
161
156
|
typescript_1.default.isJsxOpeningElement(node)) {
|
|
162
157
|
const tagNode = typescript_1.default.isPropertyAccessExpression(node.tagName)
|
package/dist/utils/universal.js
CHANGED
|
@@ -5,18 +5,14 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
exports.parseUniversal = parseUniversal;
|
|
7
7
|
const path_1 = __importDefault(require("path"));
|
|
8
|
-
|
|
9
|
-
* Strip comments (HTML + JS)
|
|
10
|
-
*/
|
|
8
|
+
// Strip comments (HTML + JS)
|
|
11
9
|
function stripComments(input) {
|
|
12
10
|
return input
|
|
13
11
|
.replace(/<!--[\s\S]*?-->/g, "")
|
|
14
12
|
.replace(/\/\*[\s\S]*?\*\//g, "")
|
|
15
13
|
.replace(/\/\/.*$/gm, "");
|
|
16
14
|
}
|
|
17
|
-
|
|
18
|
-
* Extract JS expressions
|
|
19
|
-
*/
|
|
15
|
+
// Extract JS expressions inside {}
|
|
20
16
|
function extractJS(input) {
|
|
21
17
|
const result = [];
|
|
22
18
|
let i = 0;
|
|
@@ -63,9 +59,40 @@ function extractJS(input) {
|
|
|
63
59
|
}
|
|
64
60
|
return result;
|
|
65
61
|
}
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
62
|
+
// Extract inline handlers (onclick="...")
|
|
63
|
+
function extractInlineHandlers(input) {
|
|
64
|
+
const result = [];
|
|
65
|
+
const regex = /\bon\w+\s*=\s*["']([^"']+)["']/g;
|
|
66
|
+
let match;
|
|
67
|
+
while ((match = regex.exec(input))) {
|
|
68
|
+
const code = match[1].trim();
|
|
69
|
+
if (code && /[a-zA-Z0-9_$]\s*\(/.test(code)) {
|
|
70
|
+
result.push(code);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return result;
|
|
74
|
+
}
|
|
75
|
+
// Extract dynamic imports
|
|
76
|
+
function extractDynamicImports(input, file) {
|
|
77
|
+
const deps = [];
|
|
78
|
+
const calls = [];
|
|
79
|
+
const regex = /import\(\s*["'](.+?)["']\s*\)/g;
|
|
80
|
+
let match;
|
|
81
|
+
while ((match = regex.exec(input))) {
|
|
82
|
+
const mod = match[1];
|
|
83
|
+
const resolved = mod.startsWith("/")
|
|
84
|
+
? path_1.default.resolve(process.cwd(), "." + mod)
|
|
85
|
+
: path_1.default.resolve(path_1.default.dirname(file), mod);
|
|
86
|
+
deps.push(resolved);
|
|
87
|
+
}
|
|
88
|
+
// also extract calls chained after import
|
|
89
|
+
const callRegex = /import\([^)]+\)\.then\(([^)]+)\)/g;
|
|
90
|
+
while ((match = callRegex.exec(input))) {
|
|
91
|
+
calls.push(match[1]);
|
|
92
|
+
}
|
|
93
|
+
return { deps, calls };
|
|
94
|
+
}
|
|
95
|
+
// Loose JS extractor
|
|
69
96
|
function extractLooseJS(input) {
|
|
70
97
|
const result = [];
|
|
71
98
|
const lines = input.split("\n");
|
|
@@ -81,16 +108,7 @@ function extractLooseJS(input) {
|
|
|
81
108
|
}
|
|
82
109
|
return result;
|
|
83
110
|
}
|
|
84
|
-
|
|
85
|
-
* UNIVERSAL PARSER
|
|
86
|
-
* Support:
|
|
87
|
-
* - React / JSX
|
|
88
|
-
* - Vue
|
|
89
|
-
* - Svelte
|
|
90
|
-
* - Astro
|
|
91
|
-
* - HTML + JS
|
|
92
|
-
* - SSR
|
|
93
|
-
*/
|
|
111
|
+
// UNIVERSAL PARSER
|
|
94
112
|
function parseUniversal(content, file) {
|
|
95
113
|
const parts = [];
|
|
96
114
|
const deps = [];
|
|
@@ -113,15 +131,20 @@ function parseUniversal(content, file) {
|
|
|
113
131
|
const scripts = clean.matchAll(/<script\b([^>]*)>([\s\S]*?)<\/script>/g);
|
|
114
132
|
for (const m of scripts) {
|
|
115
133
|
const attrs = m[1];
|
|
116
|
-
// skip JSON
|
|
117
134
|
if (/type=["']application\/json/.test(attrs))
|
|
118
135
|
continue;
|
|
119
136
|
if (m[2].trim()) {
|
|
120
137
|
parts.push(m[2]);
|
|
121
138
|
}
|
|
122
139
|
}
|
|
123
|
-
// REMOVE
|
|
140
|
+
// REMOVE scripts
|
|
124
141
|
const noScripts = clean.replace(/<script[\s\S]*?<\/script>/g, "");
|
|
142
|
+
// INLINE HANDLERS
|
|
143
|
+
parts.push(...extractInlineHandlers(noScripts));
|
|
144
|
+
// DYNAMIC IMPORTS
|
|
145
|
+
const dyn = extractDynamicImports(clean, file);
|
|
146
|
+
deps.push(...dyn.deps);
|
|
147
|
+
parts.push(...dyn.calls);
|
|
125
148
|
// LOOSE JS
|
|
126
149
|
parts.push(...extractLooseJS(noScripts));
|
|
127
150
|
// EXPRESSIONS
|
|
@@ -129,7 +152,6 @@ function parseUniversal(content, file) {
|
|
|
129
152
|
for (const e of extracted) {
|
|
130
153
|
parts.push(e + ";");
|
|
131
154
|
}
|
|
132
|
-
// RESULT
|
|
133
155
|
return {
|
|
134
156
|
code: [...new Set(parts)].join("\n"),
|
|
135
157
|
deps: [...new Set(deps.filter(Boolean))],
|
package/package.json
CHANGED
package/src/engine/scanner.ts
CHANGED
|
@@ -97,11 +97,7 @@ export function scanSource(
|
|
|
97
97
|
const detectedFunctionWrappers = new Set<string>();
|
|
98
98
|
const detectedComponentWrappers = new Map<string, string>();
|
|
99
99
|
|
|
100
|
-
if (
|
|
101
|
-
visited.has(source.fileName) ||
|
|
102
|
-
depth > 10 ||
|
|
103
|
-
visited.size > 200
|
|
104
|
-
) {
|
|
100
|
+
if (visited.size > 200 || visited.has(source.fileName)) {
|
|
105
101
|
return { events, detectedFunctionWrappers, detectedComponentWrappers };
|
|
106
102
|
}
|
|
107
103
|
|
|
@@ -110,8 +106,7 @@ export function scanSource(
|
|
|
110
106
|
function visit(node: ts.Node) {
|
|
111
107
|
// CALL EXPRESSIONS
|
|
112
108
|
if (ts.isCallExpression(node)) {
|
|
113
|
-
const
|
|
114
|
-
const name = getCallName(expr);
|
|
109
|
+
const name = getCallName(node.expression);
|
|
115
110
|
|
|
116
111
|
const isKnownWrapper = config.functionWrappers.some(
|
|
117
112
|
w => w.name === name
|
|
@@ -119,7 +114,7 @@ export function scanSource(
|
|
|
119
114
|
|
|
120
115
|
const isTrackingByType = isTrackingCall(node, checker);
|
|
121
116
|
|
|
122
|
-
let resolvedFn = resolveFunctionFromCall(
|
|
117
|
+
let resolvedFn = resolveFunctionFromCall(node.expression, checker);
|
|
123
118
|
|
|
124
119
|
const isAutoWrapper =
|
|
125
120
|
resolvedFn && findTrackParamIndex(resolvedFn) !== null;
|
|
@@ -127,21 +122,19 @@ export function scanSource(
|
|
|
127
122
|
let isTracking =
|
|
128
123
|
isKnownWrapper ||
|
|
129
124
|
isTrackingByType ||
|
|
130
|
-
|
|
131
|
-
(
|
|
132
|
-
resolvedFn &&
|
|
133
|
-
!isExternalFile(resolvedFn.getSourceFile().fileName));
|
|
125
|
+
isAutoWrapper ||
|
|
126
|
+
name.startsWith("$");
|
|
134
127
|
|
|
135
|
-
// fallback
|
|
128
|
+
// fallback track(...)
|
|
136
129
|
if (!isTracking) {
|
|
137
130
|
if (
|
|
138
|
-
ts.isPropertyAccessExpression(
|
|
139
|
-
|
|
131
|
+
ts.isPropertyAccessExpression(node.expression) &&
|
|
132
|
+
node.expression.name.text === "track"
|
|
140
133
|
) {
|
|
141
134
|
isTracking = true;
|
|
142
135
|
} else if (
|
|
143
|
-
ts.isIdentifier(
|
|
144
|
-
|
|
136
|
+
ts.isIdentifier(node.expression) &&
|
|
137
|
+
node.expression.text === "track"
|
|
145
138
|
) {
|
|
146
139
|
isTracking = true;
|
|
147
140
|
}
|
|
@@ -152,7 +145,7 @@ export function scanSource(
|
|
|
152
145
|
return;
|
|
153
146
|
}
|
|
154
147
|
|
|
155
|
-
//
|
|
148
|
+
// DETECT WRAPPERS
|
|
156
149
|
if (
|
|
157
150
|
!isKnownWrapper &&
|
|
158
151
|
isAutoWrapper &&
|
|
@@ -162,6 +155,7 @@ export function scanSource(
|
|
|
162
155
|
detectedFunctionWrappers.add(name);
|
|
163
156
|
}
|
|
164
157
|
|
|
158
|
+
// PARAM MAP
|
|
165
159
|
const paramMap = new Map(inheritedParamMap ?? []);
|
|
166
160
|
|
|
167
161
|
if (resolvedFn && ts.isFunctionLike(resolvedFn)) {
|
|
@@ -183,7 +177,7 @@ export function scanSource(
|
|
|
183
177
|
|
|
184
178
|
if (idx !== null && node.arguments[idx]) {
|
|
185
179
|
const res = resolveNodeValue(
|
|
186
|
-
|
|
180
|
+
node.arguments[idx],
|
|
187
181
|
checker,
|
|
188
182
|
paramMap
|
|
189
183
|
);
|
|
@@ -196,14 +190,10 @@ export function scanSource(
|
|
|
196
190
|
}
|
|
197
191
|
}
|
|
198
192
|
|
|
199
|
-
//
|
|
193
|
+
// fallback
|
|
200
194
|
if (!handled) {
|
|
201
195
|
for (const arg of node.arguments) {
|
|
202
|
-
const res = resolveNodeValue(
|
|
203
|
-
unwrap(arg),
|
|
204
|
-
checker,
|
|
205
|
-
paramMap
|
|
206
|
-
);
|
|
196
|
+
const res = resolveNodeValue(arg, checker, paramMap);
|
|
207
197
|
|
|
208
198
|
res.values.forEach(v => {
|
|
209
199
|
if (isValidEvent(v)) events.add(v);
|
|
@@ -230,7 +220,7 @@ export function scanSource(
|
|
|
230
220
|
}
|
|
231
221
|
}
|
|
232
222
|
|
|
233
|
-
// JSX
|
|
223
|
+
// JSX
|
|
234
224
|
if (
|
|
235
225
|
ts.isJsxSelfClosingElement(node) ||
|
|
236
226
|
ts.isJsxOpeningElement(node)
|
package/src/utils/universal.ts
CHANGED
|
@@ -1,9 +1,7 @@
|
|
|
1
1
|
import path from "path";
|
|
2
2
|
import { ParseResult } from "../types";
|
|
3
3
|
|
|
4
|
-
|
|
5
|
-
* Strip comments (HTML + JS)
|
|
6
|
-
*/
|
|
4
|
+
// Strip comments (HTML + JS)
|
|
7
5
|
function stripComments(input: string): string {
|
|
8
6
|
return input
|
|
9
7
|
.replace(/<!--[\s\S]*?-->/g, "")
|
|
@@ -11,9 +9,7 @@ function stripComments(input: string): string {
|
|
|
11
9
|
.replace(/\/\/.*$/gm, "");
|
|
12
10
|
}
|
|
13
11
|
|
|
14
|
-
|
|
15
|
-
* Extract JS expressions
|
|
16
|
-
*/
|
|
12
|
+
// Extract JS expressions inside {}
|
|
17
13
|
function extractJS(input: string): string[] {
|
|
18
14
|
const result: string[] = [];
|
|
19
15
|
|
|
@@ -68,9 +64,56 @@ function extractJS(input: string): string[] {
|
|
|
68
64
|
return result;
|
|
69
65
|
}
|
|
70
66
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
67
|
+
// Extract inline handlers (onclick="...")
|
|
68
|
+
function extractInlineHandlers(input: string): string[] {
|
|
69
|
+
const result: string[] = [];
|
|
70
|
+
|
|
71
|
+
const regex = /\bon\w+\s*=\s*["']([^"']+)["']/g;
|
|
72
|
+
|
|
73
|
+
let match;
|
|
74
|
+
while ((match = regex.exec(input))) {
|
|
75
|
+
const code = match[1].trim();
|
|
76
|
+
|
|
77
|
+
if (code && /[a-zA-Z0-9_$]\s*\(/.test(code)) {
|
|
78
|
+
result.push(code);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return result;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Extract dynamic imports
|
|
86
|
+
function extractDynamicImports(
|
|
87
|
+
input: string,
|
|
88
|
+
file: string
|
|
89
|
+
): { deps: string[]; calls: string[] } {
|
|
90
|
+
const deps: string[] = [];
|
|
91
|
+
const calls: string[] = [];
|
|
92
|
+
|
|
93
|
+
const regex = /import\(\s*["'](.+?)["']\s*\)/g;
|
|
94
|
+
|
|
95
|
+
let match;
|
|
96
|
+
while ((match = regex.exec(input))) {
|
|
97
|
+
const mod = match[1];
|
|
98
|
+
|
|
99
|
+
const resolved = mod.startsWith("/")
|
|
100
|
+
? path.resolve(process.cwd(), "." + mod)
|
|
101
|
+
: path.resolve(path.dirname(file), mod);
|
|
102
|
+
|
|
103
|
+
deps.push(resolved);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// also extract calls chained after import
|
|
107
|
+
const callRegex = /import\([^)]+\)\.then\(([^)]+)\)/g;
|
|
108
|
+
|
|
109
|
+
while ((match = callRegex.exec(input))) {
|
|
110
|
+
calls.push(match[1]);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return { deps, calls };
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Loose JS extractor
|
|
74
117
|
function extractLooseJS(input: string): string[] {
|
|
75
118
|
const result: string[] = [];
|
|
76
119
|
|
|
@@ -90,16 +133,7 @@ function extractLooseJS(input: string): string[] {
|
|
|
90
133
|
return result;
|
|
91
134
|
}
|
|
92
135
|
|
|
93
|
-
|
|
94
|
-
* UNIVERSAL PARSER
|
|
95
|
-
* Support:
|
|
96
|
-
* - React / JSX
|
|
97
|
-
* - Vue
|
|
98
|
-
* - Svelte
|
|
99
|
-
* - Astro
|
|
100
|
-
* - HTML + JS
|
|
101
|
-
* - SSR
|
|
102
|
-
*/
|
|
136
|
+
// UNIVERSAL PARSER
|
|
103
137
|
export function parseUniversal(
|
|
104
138
|
content: string,
|
|
105
139
|
file: string
|
|
@@ -138,7 +172,6 @@ export function parseUniversal(
|
|
|
138
172
|
for (const m of scripts) {
|
|
139
173
|
const attrs = m[1];
|
|
140
174
|
|
|
141
|
-
// skip JSON
|
|
142
175
|
if (/type=["']application\/json/.test(attrs)) continue;
|
|
143
176
|
|
|
144
177
|
if (m[2].trim()) {
|
|
@@ -146,12 +179,20 @@ export function parseUniversal(
|
|
|
146
179
|
}
|
|
147
180
|
}
|
|
148
181
|
|
|
149
|
-
// REMOVE
|
|
182
|
+
// REMOVE scripts
|
|
150
183
|
const noScripts = clean.replace(
|
|
151
184
|
/<script[\s\S]*?<\/script>/g,
|
|
152
185
|
""
|
|
153
186
|
);
|
|
154
187
|
|
|
188
|
+
// INLINE HANDLERS
|
|
189
|
+
parts.push(...extractInlineHandlers(noScripts));
|
|
190
|
+
|
|
191
|
+
// DYNAMIC IMPORTS
|
|
192
|
+
const dyn = extractDynamicImports(clean, file);
|
|
193
|
+
deps.push(...dyn.deps);
|
|
194
|
+
parts.push(...dyn.calls);
|
|
195
|
+
|
|
155
196
|
// LOOSE JS
|
|
156
197
|
parts.push(...extractLooseJS(noScripts));
|
|
157
198
|
|
|
@@ -161,7 +202,6 @@ export function parseUniversal(
|
|
|
161
202
|
parts.push(e + ";");
|
|
162
203
|
}
|
|
163
204
|
|
|
164
|
-
// RESULT
|
|
165
205
|
return {
|
|
166
206
|
code: [...new Set(parts)].join("\n"),
|
|
167
207
|
deps: [...new Set(deps.filter(Boolean))],
|