@eventra_dev/eventra-cli 0.1.22 → 0.1.24
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/dist/commands/watch.js +50 -23
- package/dist/engine/functionAnalyzer.js +18 -10
- package/dist/engine/resolver.js +97 -21
- package/dist/utils/universal.js +47 -27
- package/package.json +1 -1
- package/src/commands/watch.ts +67 -28
- package/src/engine/functionAnalyzer.ts +22 -12
- package/src/engine/resolver.ts +126 -31
- package/src/utils/universal.ts +59 -31
package/dist/commands/watch.js
CHANGED
|
@@ -12,6 +12,12 @@ const path_1 = __importDefault(require("path"));
|
|
|
12
12
|
const config_1 = require("../utils/config");
|
|
13
13
|
const engine_1 = require("../engine/engine");
|
|
14
14
|
const processFile_1 = require("../utils/processFile");
|
|
15
|
+
// stabile virtual mapping
|
|
16
|
+
function getVirtualFile(file) {
|
|
17
|
+
return /\.(vue|svelte|astro|html)$/i.test(file)
|
|
18
|
+
? file + ".tsx"
|
|
19
|
+
: file;
|
|
20
|
+
}
|
|
15
21
|
async function watch() {
|
|
16
22
|
const config = await (0, config_1.loadConfig)();
|
|
17
23
|
if (!config)
|
|
@@ -21,54 +27,65 @@ async function watch() {
|
|
|
21
27
|
ignore: config.sync.exclude,
|
|
22
28
|
});
|
|
23
29
|
const engine = new engine_1.EventraEngine(process.cwd());
|
|
24
|
-
const
|
|
25
|
-
//
|
|
30
|
+
const watchedDeps = new Set();
|
|
31
|
+
// INITIAL LOAD
|
|
26
32
|
for (const file of files) {
|
|
27
33
|
try {
|
|
28
34
|
const abs = path_1.default.resolve(file);
|
|
29
35
|
const raw = await promises_1.default.readFile(abs, "utf-8");
|
|
30
|
-
const { content,
|
|
31
|
-
|
|
36
|
+
const { content, deps } = (0, processFile_1.processFile)(abs, raw);
|
|
37
|
+
const virtual = getVirtualFile(abs);
|
|
38
|
+
engine.scanFile(virtual, content, config);
|
|
39
|
+
// deps
|
|
40
|
+
for (const dep of deps) {
|
|
41
|
+
watchedDeps.add(path_1.default.resolve(dep));
|
|
42
|
+
}
|
|
32
43
|
}
|
|
33
44
|
catch {
|
|
34
45
|
console.log(chalk_1.default.gray(`skip: ${file}`));
|
|
35
46
|
}
|
|
36
47
|
}
|
|
37
|
-
// INITIAL SCAN
|
|
38
|
-
for (const { file, content } of fileCache) {
|
|
39
|
-
engine.scanFile(file, content, config);
|
|
40
|
-
}
|
|
41
48
|
config.events = engine.getAllEvents().sort();
|
|
42
49
|
await (0, config_1.saveConfig)(config);
|
|
43
50
|
console.log(chalk_1.default.gray(`Initial: ${config.events.length} events\n`));
|
|
51
|
+
// QUEUE
|
|
44
52
|
let locked = false;
|
|
45
|
-
let queued = new
|
|
53
|
+
let queued = new Map();
|
|
46
54
|
let timer = null;
|
|
47
55
|
const run = async () => {
|
|
48
|
-
if (locked)
|
|
56
|
+
if (locked) {
|
|
57
|
+
timer = setTimeout(run, 100);
|
|
49
58
|
return;
|
|
59
|
+
}
|
|
50
60
|
locked = true;
|
|
51
|
-
const batch = [...queued];
|
|
61
|
+
const batch = [...queued.values()];
|
|
52
62
|
queued.clear();
|
|
53
|
-
for (const
|
|
63
|
+
for (const item of batch) {
|
|
54
64
|
try {
|
|
55
|
-
const raw = await promises_1.default.readFile(
|
|
56
|
-
const { content,
|
|
57
|
-
engine.updateFile(
|
|
58
|
-
//
|
|
65
|
+
const raw = await promises_1.default.readFile(item.real, "utf-8");
|
|
66
|
+
const { content, deps } = (0, processFile_1.processFile)(item.real, raw);
|
|
67
|
+
engine.updateFile(item.virtual, content, config);
|
|
68
|
+
// DEPS
|
|
59
69
|
for (const dep of deps) {
|
|
70
|
+
const absDep = path_1.default.resolve(dep);
|
|
71
|
+
if (!watchedDeps.has(absDep)) {
|
|
72
|
+
watcher.add(absDep);
|
|
73
|
+
watchedDeps.add(absDep);
|
|
74
|
+
}
|
|
60
75
|
try {
|
|
61
|
-
const depRaw = await promises_1.default.readFile(
|
|
62
|
-
const { content: depContent
|
|
76
|
+
const depRaw = await promises_1.default.readFile(absDep, "utf-8");
|
|
77
|
+
const { content: depContent } = (0, processFile_1.processFile)(absDep, depRaw);
|
|
78
|
+
const depVirtual = getVirtualFile(absDep);
|
|
63
79
|
engine.updateFile(depVirtual, depContent, config);
|
|
64
80
|
}
|
|
65
81
|
catch { }
|
|
66
82
|
}
|
|
67
83
|
}
|
|
68
84
|
catch {
|
|
69
|
-
console.log(chalk_1.default.gray(`skip: ${
|
|
85
|
+
console.log(chalk_1.default.gray(`skip: ${item.real}`));
|
|
70
86
|
}
|
|
71
87
|
}
|
|
88
|
+
// EVENTS DIFF
|
|
72
89
|
const next = engine.getAllEvents();
|
|
73
90
|
const added = next.filter(e => !config.events.includes(e));
|
|
74
91
|
const removed = config.events.filter(e => !next.includes(e));
|
|
@@ -81,12 +98,16 @@ async function watch() {
|
|
|
81
98
|
}
|
|
82
99
|
locked = false;
|
|
83
100
|
};
|
|
101
|
+
// SCHEDULER
|
|
84
102
|
const schedule = (file) => {
|
|
85
|
-
|
|
103
|
+
const abs = path_1.default.resolve(file);
|
|
104
|
+
const virtual = getVirtualFile(abs);
|
|
105
|
+
queued.set(abs, { real: abs, virtual });
|
|
86
106
|
if (timer)
|
|
87
107
|
clearTimeout(timer);
|
|
88
|
-
timer = setTimeout(run,
|
|
108
|
+
timer = setTimeout(run, 300);
|
|
89
109
|
};
|
|
110
|
+
// WATCHER
|
|
90
111
|
const watcher = chokidar_1.default.watch(config.sync.include, {
|
|
91
112
|
ignored: config.sync.exclude,
|
|
92
113
|
ignoreInitial: true,
|
|
@@ -97,13 +118,19 @@ async function watch() {
|
|
|
97
118
|
.on("unlink", async (file) => {
|
|
98
119
|
const abs = path_1.default.resolve(file);
|
|
99
120
|
console.log(chalk_1.default.gray(`remove: ${file}`));
|
|
100
|
-
const
|
|
101
|
-
engine.removeFile(
|
|
121
|
+
const virtual = getVirtualFile(abs);
|
|
122
|
+
engine.removeFile(virtual, config);
|
|
123
|
+
watchedDeps.delete(abs);
|
|
102
124
|
const next = engine.getAllEvents();
|
|
103
125
|
config.events = next.sort();
|
|
104
126
|
await (0, config_1.saveConfig)(config);
|
|
105
127
|
console.log(chalk_1.default.gray(`Total: ${config.events.length} events\n`));
|
|
106
128
|
});
|
|
129
|
+
// WATCH DEPS
|
|
130
|
+
for (const dep of watchedDeps) {
|
|
131
|
+
watcher.add(dep);
|
|
132
|
+
}
|
|
133
|
+
// EXIT
|
|
107
134
|
process.on("SIGINT", async () => {
|
|
108
135
|
console.log(chalk_1.default.yellow("\nStopping watcher..."));
|
|
109
136
|
await watcher.close();
|
|
@@ -15,8 +15,20 @@ function findTrackParamIndex(fn) {
|
|
|
15
15
|
let result = null;
|
|
16
16
|
function visit(node) {
|
|
17
17
|
if (typescript_1.default.isCallExpression(node)) {
|
|
18
|
-
|
|
19
|
-
if (
|
|
18
|
+
let isTrackCall = false;
|
|
19
|
+
if (typescript_1.default.isPropertyAccessExpression(node.expression)) {
|
|
20
|
+
const method = node.expression.name.text;
|
|
21
|
+
if (method === "track") {
|
|
22
|
+
isTrackCall = true;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
// fallback
|
|
26
|
+
if (typescript_1.default.isIdentifier(node.expression)) {
|
|
27
|
+
if (node.expression.text === "track") {
|
|
28
|
+
isTrackCall = true;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
if (isTrackCall) {
|
|
20
32
|
for (const arg of node.arguments) {
|
|
21
33
|
if (typescript_1.default.isIdentifier(arg)) {
|
|
22
34
|
const idx = paramIndex.get(arg.text);
|
|
@@ -25,6 +37,10 @@ function findTrackParamIndex(fn) {
|
|
|
25
37
|
return;
|
|
26
38
|
}
|
|
27
39
|
}
|
|
40
|
+
if (typescript_1.default.isStringLiteral(arg)) {
|
|
41
|
+
result = 0;
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
28
44
|
}
|
|
29
45
|
}
|
|
30
46
|
}
|
|
@@ -35,11 +51,3 @@ function findTrackParamIndex(fn) {
|
|
|
35
51
|
}
|
|
36
52
|
return result;
|
|
37
53
|
}
|
|
38
|
-
function getCallName(expr) {
|
|
39
|
-
if (typescript_1.default.isIdentifier(expr))
|
|
40
|
-
return expr.text;
|
|
41
|
-
if (typescript_1.default.isPropertyAccessExpression(expr)) {
|
|
42
|
-
return expr.name.text;
|
|
43
|
-
}
|
|
44
|
-
return "";
|
|
45
|
-
}
|
package/dist/engine/resolver.js
CHANGED
|
@@ -11,42 +11,60 @@ function resolveNodeValue(node, checker, paramMap, seen = new Set()) {
|
|
|
11
11
|
return { values: [], dynamic: true };
|
|
12
12
|
}
|
|
13
13
|
seen.add(node);
|
|
14
|
+
// param mapping
|
|
14
15
|
if (typescript_1.default.isIdentifier(node) && paramMap?.has(node.text)) {
|
|
15
16
|
return resolveNodeValue(paramMap.get(node.text), checker, paramMap, seen);
|
|
16
17
|
}
|
|
18
|
+
// STRING
|
|
17
19
|
if (typescript_1.default.isStringLiteral(node)) {
|
|
18
20
|
return { values: [node.text], dynamic: false };
|
|
19
21
|
}
|
|
22
|
+
// ARRAY
|
|
23
|
+
if (typescript_1.default.isArrayLiteralExpression(node)) {
|
|
24
|
+
const values = [];
|
|
25
|
+
for (const el of node.elements) {
|
|
26
|
+
const res = resolveNodeValue(el, checker, paramMap, seen);
|
|
27
|
+
if (res.values.length) {
|
|
28
|
+
values.push(...res.values);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
return { values, dynamic: true };
|
|
32
|
+
}
|
|
33
|
+
// TEMPLATE
|
|
20
34
|
if (typescript_1.default.isTemplateExpression(node)) {
|
|
21
|
-
let
|
|
35
|
+
let results = [node.head.text];
|
|
22
36
|
for (const span of node.templateSpans) {
|
|
23
|
-
|
|
24
|
-
|
|
37
|
+
const res = resolveNodeValue(span.expression, checker, paramMap, seen);
|
|
38
|
+
if (res.values.length) {
|
|
39
|
+
results = results.flatMap(r => res.values.map(v => r + v));
|
|
40
|
+
}
|
|
41
|
+
else {
|
|
42
|
+
results = results.map(r => r + "*");
|
|
43
|
+
}
|
|
44
|
+
results = results.map(r => r + span.literal.text);
|
|
25
45
|
}
|
|
26
|
-
return { values:
|
|
46
|
+
return { values: results, dynamic: true };
|
|
27
47
|
}
|
|
48
|
+
// CONDITIONAL
|
|
28
49
|
if (typescript_1.default.isConditionalExpression(node)) {
|
|
29
50
|
const a = resolveNodeValue(node.whenTrue, checker, paramMap, seen);
|
|
30
51
|
const b = resolveNodeValue(node.whenFalse, checker, paramMap, seen);
|
|
31
52
|
return {
|
|
32
|
-
values: [...new Set([...
|
|
53
|
+
values: [...new Set([...a.values, ...b.values])],
|
|
33
54
|
dynamic: true,
|
|
34
55
|
};
|
|
35
56
|
}
|
|
57
|
+
// IDENTIFIER
|
|
36
58
|
if (typescript_1.default.isIdentifier(node)) {
|
|
37
59
|
let symbol = checker.getSymbolAtLocation(node);
|
|
38
60
|
if (!symbol)
|
|
39
61
|
return { values: [], dynamic: true };
|
|
40
|
-
|
|
41
|
-
if (typescript_1.default.isVariableDeclaration(decl) && decl.initializer) {
|
|
42
|
-
return resolveNodeValue(decl.initializer, checker, paramMap, seen);
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
const original = symbol;
|
|
62
|
+
// unwrap export
|
|
46
63
|
symbol = (0, exportResolver_1.resolveExportedSymbol)(symbol, checker) ?? symbol;
|
|
47
|
-
if (symbol
|
|
64
|
+
if (symbol.flags & typescript_1.default.SymbolFlags.Alias) {
|
|
48
65
|
symbol = checker.getAliasedSymbol(symbol);
|
|
49
66
|
}
|
|
67
|
+
// inline variable
|
|
50
68
|
for (const decl of symbol.getDeclarations() ?? []) {
|
|
51
69
|
if (typescript_1.default.isVariableDeclaration(decl) && decl.initializer) {
|
|
52
70
|
return resolveNodeValue(decl.initializer, checker, paramMap, seen);
|
|
@@ -60,12 +78,30 @@ function resolveNodeValue(node, checker, paramMap, seen = new Set()) {
|
|
|
60
78
|
}
|
|
61
79
|
}
|
|
62
80
|
}
|
|
81
|
+
// UNION TYPES
|
|
82
|
+
const type = checker.getTypeAtLocation(node);
|
|
83
|
+
if (type.isUnion()) {
|
|
84
|
+
const values = [];
|
|
85
|
+
for (const t of type.types) {
|
|
86
|
+
if (t.isStringLiteral?.()) {
|
|
87
|
+
values.push(t.value);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
if (values.length) {
|
|
91
|
+
return { values, dynamic: false };
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
// TYPE FALLBACK (as const)
|
|
95
|
+
if (type.isStringLiteral?.()) {
|
|
96
|
+
return { values: [type.value], dynamic: false };
|
|
97
|
+
}
|
|
63
98
|
return { values: [], dynamic: true };
|
|
64
99
|
}
|
|
100
|
+
// PROPERTY ACCESS (obj.a.b)
|
|
65
101
|
if (typescript_1.default.isPropertyAccessExpression(node)) {
|
|
66
102
|
const symbol = checker.getSymbolAtLocation(node);
|
|
67
103
|
if (symbol) {
|
|
68
|
-
let s = symbol;
|
|
104
|
+
let s = (0, exportResolver_1.resolveExportedSymbol)(symbol, checker) ?? symbol;
|
|
69
105
|
if (s.flags & typescript_1.default.SymbolFlags.Alias) {
|
|
70
106
|
s = checker.getAliasedSymbol(s);
|
|
71
107
|
}
|
|
@@ -80,6 +116,20 @@ function resolveNodeValue(node, checker, paramMap, seen = new Set()) {
|
|
|
80
116
|
}
|
|
81
117
|
}
|
|
82
118
|
}
|
|
119
|
+
// constant enum / readonly
|
|
120
|
+
const constantValue = checker.getConstantValue(node);
|
|
121
|
+
if (typeof constantValue === "string") {
|
|
122
|
+
return { values: [constantValue], dynamic: false };
|
|
123
|
+
}
|
|
124
|
+
// type fallback
|
|
125
|
+
const type = checker.getTypeAtLocation(node);
|
|
126
|
+
if (type.isStringLiteral?.()) {
|
|
127
|
+
return {
|
|
128
|
+
values: [type.value],
|
|
129
|
+
dynamic: false,
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
// object literal resolve
|
|
83
133
|
if (typescript_1.default.isIdentifier(node.expression)) {
|
|
84
134
|
const objSymbol = checker.getSymbolAtLocation(node.expression);
|
|
85
135
|
if (objSymbol) {
|
|
@@ -101,33 +151,59 @@ function resolveNodeValue(node, checker, paramMap, seen = new Set()) {
|
|
|
101
151
|
}
|
|
102
152
|
return { values: [], dynamic: true };
|
|
103
153
|
}
|
|
154
|
+
// ARRAY ACCESS (arr[0])
|
|
155
|
+
if (typescript_1.default.isElementAccessExpression(node)) {
|
|
156
|
+
const obj = resolveNodeValue(node.expression, checker, paramMap, seen);
|
|
157
|
+
if (!node.argumentExpression) {
|
|
158
|
+
return { values: obj.values, dynamic: true };
|
|
159
|
+
}
|
|
160
|
+
const index = resolveNodeValue(node.argumentExpression, checker, paramMap, seen);
|
|
161
|
+
// if index known
|
|
162
|
+
if (index.values.length === 1) {
|
|
163
|
+
const i = Number(index.values[0]);
|
|
164
|
+
if (!Number.isNaN(i) && obj.values[i] !== undefined) {
|
|
165
|
+
return {
|
|
166
|
+
values: [obj.values[i]],
|
|
167
|
+
dynamic: false,
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
// fallback → dynamic
|
|
172
|
+
return obj.values.length
|
|
173
|
+
? { values: obj.values, dynamic: true }
|
|
174
|
+
: { values: [], dynamic: true };
|
|
175
|
+
}
|
|
176
|
+
// OBJECT
|
|
104
177
|
if (typescript_1.default.isObjectLiteralExpression(node)) {
|
|
105
178
|
const values = [];
|
|
106
179
|
for (const prop of node.properties) {
|
|
107
180
|
if (!typescript_1.default.isPropertyAssignment(prop))
|
|
108
181
|
continue;
|
|
109
182
|
const name = prop.name.getText();
|
|
110
|
-
if (name
|
|
183
|
+
if (["event", "name", "type"].includes(name)) {
|
|
111
184
|
const res = resolveNodeValue(prop.initializer, checker, paramMap, seen);
|
|
112
185
|
values.push(...res.values);
|
|
113
186
|
}
|
|
114
187
|
}
|
|
115
188
|
return { values, dynamic: true };
|
|
116
189
|
}
|
|
190
|
+
// BINARY ("btn_" + name)
|
|
117
191
|
if (typescript_1.default.isBinaryExpression(node)) {
|
|
118
192
|
if (node.operatorToken.kind === typescript_1.default.SyntaxKind.PlusToken) {
|
|
119
193
|
const left = resolveNodeValue(node.left, checker, paramMap, seen);
|
|
120
194
|
const right = resolveNodeValue(node.right, checker, paramMap, seen);
|
|
121
|
-
if (left
|
|
122
|
-
return {
|
|
123
|
-
values: left.values.flatMap(l => right.values.length
|
|
124
|
-
? right.values.map(r => l + r)
|
|
125
|
-
: [l + "*"]),
|
|
126
|
-
dynamic: true,
|
|
127
|
-
};
|
|
195
|
+
if (!left.values.length) {
|
|
196
|
+
return { values: [], dynamic: true };
|
|
128
197
|
}
|
|
198
|
+
return {
|
|
199
|
+
values: left.values.flatMap(l => right.values.length
|
|
200
|
+
? right.values.map(r => l + r)
|
|
201
|
+
: [l + "*"]),
|
|
202
|
+
dynamic: true,
|
|
203
|
+
};
|
|
129
204
|
}
|
|
130
205
|
}
|
|
206
|
+
// CALL → dynamic
|
|
131
207
|
if (typescript_1.default.isCallExpression(node)) {
|
|
132
208
|
return { values: [], dynamic: true };
|
|
133
209
|
}
|
package/dist/utils/universal.js
CHANGED
|
@@ -6,10 +6,16 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
6
6
|
exports.parseUniversal = parseUniversal;
|
|
7
7
|
const path_1 = __importDefault(require("path"));
|
|
8
8
|
/**
|
|
9
|
-
*
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
9
|
+
* Strip comments (HTML + JS)
|
|
10
|
+
*/
|
|
11
|
+
function stripComments(input) {
|
|
12
|
+
return input
|
|
13
|
+
.replace(/<!--[\s\S]*?-->/g, "")
|
|
14
|
+
.replace(/\/\*[\s\S]*?\*\//g, "")
|
|
15
|
+
.replace(/\/\/.*$/gm, "");
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Extract JS expressions safely
|
|
13
19
|
*/
|
|
14
20
|
function extractJS(input) {
|
|
15
21
|
const result = [];
|
|
@@ -66,8 +72,13 @@ function extractJS(input) {
|
|
|
66
72
|
}
|
|
67
73
|
if (depth === 0) {
|
|
68
74
|
const expr = input.slice(i + 1, j - 1).trim();
|
|
69
|
-
if (expr
|
|
75
|
+
if (expr &&
|
|
76
|
+
(expr.includes("(") ||
|
|
77
|
+
expr.includes("=") ||
|
|
78
|
+
expr.includes(".") ||
|
|
79
|
+
expr.includes("track"))) {
|
|
70
80
|
result.push(expr);
|
|
81
|
+
}
|
|
71
82
|
i = j;
|
|
72
83
|
continue;
|
|
73
84
|
}
|
|
@@ -89,8 +100,12 @@ function extractJS(input) {
|
|
|
89
100
|
val += c;
|
|
90
101
|
j++;
|
|
91
102
|
}
|
|
92
|
-
if (val.trim()
|
|
103
|
+
if (val.trim() &&
|
|
104
|
+
(val.includes("(") ||
|
|
105
|
+
val.includes("=>") ||
|
|
106
|
+
val.includes("track"))) {
|
|
93
107
|
result.push(val.trim());
|
|
108
|
+
}
|
|
94
109
|
i = j + 1;
|
|
95
110
|
continue;
|
|
96
111
|
}
|
|
@@ -113,6 +128,9 @@ function extractJS(input) {
|
|
|
113
128
|
}
|
|
114
129
|
return result;
|
|
115
130
|
}
|
|
131
|
+
/**
|
|
132
|
+
* Extract loose JS but less noisy
|
|
133
|
+
*/
|
|
116
134
|
function extractLooseJS(input) {
|
|
117
135
|
const result = [];
|
|
118
136
|
const lines = input.split("\n");
|
|
@@ -122,33 +140,30 @@ function extractLooseJS(input) {
|
|
|
122
140
|
continue;
|
|
123
141
|
if (l.startsWith("<"))
|
|
124
142
|
continue;
|
|
125
|
-
|
|
126
|
-
|
|
143
|
+
// strict filter
|
|
144
|
+
if (l.includes("track") ||
|
|
145
|
+
l.includes(".track(") ||
|
|
127
146
|
l.includes("=>") ||
|
|
128
|
-
l.
|
|
129
|
-
l.includes("
|
|
147
|
+
l.startsWith("function") ||
|
|
148
|
+
l.includes("(")) {
|
|
130
149
|
result.push(l);
|
|
131
150
|
}
|
|
132
151
|
}
|
|
133
152
|
return result;
|
|
134
153
|
}
|
|
135
154
|
/**
|
|
136
|
-
* Universal
|
|
137
|
-
* - Astro
|
|
138
|
-
* - Vue
|
|
139
|
-
* - Svelte
|
|
140
|
-
* - React / JSX
|
|
141
|
-
* - Node / backend
|
|
155
|
+
* Universal parser
|
|
142
156
|
*/
|
|
143
157
|
function parseUniversal(content, file) {
|
|
144
158
|
const parts = [];
|
|
145
159
|
const deps = [];
|
|
146
|
-
|
|
147
|
-
|
|
160
|
+
const clean = stripComments(content);
|
|
161
|
+
// Astro frontmatter
|
|
162
|
+
const fm = clean.match(/^---([\s\S]*?)---/);
|
|
148
163
|
if (fm?.[1])
|
|
149
164
|
parts.push(fm[1]);
|
|
150
165
|
// <script src="">
|
|
151
|
-
const scriptSrc =
|
|
166
|
+
const scriptSrc = clean.matchAll(/<script\b[^>]*?\bsrc=["'](.+?)["'][^>]*>/g);
|
|
152
167
|
for (const s of scriptSrc) {
|
|
153
168
|
const src = s[1];
|
|
154
169
|
const resolved = src.startsWith("/")
|
|
@@ -157,18 +172,23 @@ function parseUniversal(content, file) {
|
|
|
157
172
|
deps.push(resolved);
|
|
158
173
|
}
|
|
159
174
|
// inline <script>
|
|
160
|
-
const scripts =
|
|
175
|
+
const scripts = clean.matchAll(/<script\b([^>]*)>([\s\S]*?)<\/script>/g);
|
|
161
176
|
for (const m of scripts) {
|
|
162
|
-
|
|
163
|
-
|
|
177
|
+
const attrs = m[1];
|
|
178
|
+
// skip JSON / data
|
|
179
|
+
if (/type=["']application\/json/.test(attrs))
|
|
180
|
+
continue;
|
|
181
|
+
if (m[2].trim()) {
|
|
182
|
+
parts.push(m[2]);
|
|
183
|
+
}
|
|
164
184
|
}
|
|
165
|
-
|
|
185
|
+
// remove scripts
|
|
186
|
+
const noScripts = clean.replace(/<script[\s\S]*?<\/script>/g, "");
|
|
187
|
+
// loose JS
|
|
166
188
|
const loose = extractLooseJS(noScripts);
|
|
167
|
-
|
|
168
|
-
parts.push(l);
|
|
169
|
-
}
|
|
189
|
+
parts.push(...loose);
|
|
170
190
|
// expressions
|
|
171
|
-
const extracted = extractJS(
|
|
191
|
+
const extracted = extractJS(clean);
|
|
172
192
|
for (const e of extracted) {
|
|
173
193
|
parts.push(e + ";");
|
|
174
194
|
}
|
package/package.json
CHANGED
package/src/commands/watch.ts
CHANGED
|
@@ -8,6 +8,18 @@ import { loadConfig, saveConfig } from "../utils/config";
|
|
|
8
8
|
import { EventraEngine } from "../engine/engine";
|
|
9
9
|
import { processFile } from "../utils/processFile";
|
|
10
10
|
|
|
11
|
+
type QueueItem = {
|
|
12
|
+
real: string;
|
|
13
|
+
virtual: string;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
// stabile virtual mapping
|
|
17
|
+
function getVirtualFile(file: string) {
|
|
18
|
+
return /\.(vue|svelte|astro|html)$/i.test(file)
|
|
19
|
+
? file + ".tsx"
|
|
20
|
+
: file;
|
|
21
|
+
}
|
|
22
|
+
|
|
11
23
|
export async function watch() {
|
|
12
24
|
const config = await loadConfig();
|
|
13
25
|
if (!config) return;
|
|
@@ -20,25 +32,29 @@ export async function watch() {
|
|
|
20
32
|
|
|
21
33
|
const engine = new EventraEngine(process.cwd());
|
|
22
34
|
|
|
23
|
-
const
|
|
35
|
+
const watchedDeps = new Set<string>();
|
|
24
36
|
|
|
25
|
-
//
|
|
37
|
+
// INITIAL LOAD
|
|
26
38
|
for (const file of files) {
|
|
27
39
|
try {
|
|
28
40
|
const abs = path.resolve(file);
|
|
29
41
|
const raw = await fs.readFile(abs, "utf-8");
|
|
30
|
-
|
|
31
|
-
|
|
42
|
+
|
|
43
|
+
const { content, deps } = processFile(abs, raw);
|
|
44
|
+
const virtual = getVirtualFile(abs);
|
|
45
|
+
|
|
46
|
+
engine.scanFile(virtual, content, config);
|
|
47
|
+
|
|
48
|
+
// deps
|
|
49
|
+
for (const dep of deps) {
|
|
50
|
+
watchedDeps.add(path.resolve(dep));
|
|
51
|
+
}
|
|
52
|
+
|
|
32
53
|
} catch {
|
|
33
54
|
console.log(chalk.gray(`skip: ${file}`));
|
|
34
55
|
}
|
|
35
56
|
}
|
|
36
57
|
|
|
37
|
-
// INITIAL SCAN
|
|
38
|
-
for (const { file, content } of fileCache) {
|
|
39
|
-
engine.scanFile(file, content, config);
|
|
40
|
-
}
|
|
41
|
-
|
|
42
58
|
config.events = engine.getAllEvents().sort();
|
|
43
59
|
await saveConfig(config);
|
|
44
60
|
|
|
@@ -46,45 +62,55 @@ export async function watch() {
|
|
|
46
62
|
chalk.gray(`Initial: ${config.events.length} events\n`)
|
|
47
63
|
);
|
|
48
64
|
|
|
65
|
+
// QUEUE
|
|
49
66
|
let locked = false;
|
|
50
|
-
let queued = new
|
|
67
|
+
let queued = new Map<string, QueueItem>();
|
|
51
68
|
let timer: NodeJS.Timeout | null = null;
|
|
52
69
|
|
|
53
70
|
const run = async () => {
|
|
54
|
-
if (locked)
|
|
71
|
+
if (locked) {
|
|
72
|
+
timer = setTimeout(run, 100);
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
|
|
55
76
|
locked = true;
|
|
56
77
|
|
|
57
|
-
const batch = [...queued];
|
|
78
|
+
const batch = [...queued.values()];
|
|
58
79
|
queued.clear();
|
|
59
80
|
|
|
60
|
-
for (const
|
|
81
|
+
for (const item of batch) {
|
|
61
82
|
try {
|
|
62
|
-
const raw = await fs.readFile(
|
|
83
|
+
const raw = await fs.readFile(item.real, "utf-8");
|
|
63
84
|
|
|
64
|
-
const { content,
|
|
85
|
+
const { content, deps } = processFile(item.real, raw);
|
|
65
86
|
|
|
66
|
-
engine.updateFile(
|
|
87
|
+
engine.updateFile(item.virtual, content, config);
|
|
67
88
|
|
|
68
|
-
//
|
|
89
|
+
// DEPS
|
|
69
90
|
for (const dep of deps) {
|
|
91
|
+
const absDep = path.resolve(dep);
|
|
92
|
+
|
|
93
|
+
if (!watchedDeps.has(absDep)) {
|
|
94
|
+
watcher.add(absDep);
|
|
95
|
+
watchedDeps.add(absDep);
|
|
96
|
+
}
|
|
97
|
+
|
|
70
98
|
try {
|
|
71
|
-
const depRaw = await fs.readFile(
|
|
99
|
+
const depRaw = await fs.readFile(absDep, "utf-8");
|
|
100
|
+
const { content: depContent } = processFile(absDep, depRaw);
|
|
72
101
|
|
|
73
|
-
const
|
|
74
|
-
content: depContent,
|
|
75
|
-
virtualFile: depVirtual,
|
|
76
|
-
} = processFile(dep, depRaw);
|
|
102
|
+
const depVirtual = getVirtualFile(absDep);
|
|
77
103
|
|
|
78
104
|
engine.updateFile(depVirtual, depContent, config);
|
|
79
|
-
|
|
80
105
|
} catch {}
|
|
81
106
|
}
|
|
82
107
|
|
|
83
108
|
} catch {
|
|
84
|
-
console.log(chalk.gray(`skip: ${
|
|
109
|
+
console.log(chalk.gray(`skip: ${item.real}`));
|
|
85
110
|
}
|
|
86
111
|
}
|
|
87
112
|
|
|
113
|
+
// EVENTS DIFF
|
|
88
114
|
const next = engine.getAllEvents();
|
|
89
115
|
|
|
90
116
|
const added = next.filter(e => !config.events.includes(e));
|
|
@@ -105,13 +131,18 @@ export async function watch() {
|
|
|
105
131
|
locked = false;
|
|
106
132
|
};
|
|
107
133
|
|
|
134
|
+
// SCHEDULER
|
|
108
135
|
const schedule = (file: string) => {
|
|
109
|
-
|
|
136
|
+
const abs = path.resolve(file);
|
|
137
|
+
const virtual = getVirtualFile(abs);
|
|
138
|
+
|
|
139
|
+
queued.set(abs, { real: abs, virtual });
|
|
110
140
|
|
|
111
141
|
if (timer) clearTimeout(timer);
|
|
112
|
-
timer = setTimeout(run,
|
|
142
|
+
timer = setTimeout(run, 300);
|
|
113
143
|
};
|
|
114
144
|
|
|
145
|
+
// WATCHER
|
|
115
146
|
const watcher = chokidar.watch(config.sync.include, {
|
|
116
147
|
ignored: config.sync.exclude,
|
|
117
148
|
ignoreInitial: true,
|
|
@@ -125,9 +156,11 @@ export async function watch() {
|
|
|
125
156
|
|
|
126
157
|
console.log(chalk.gray(`remove: ${file}`));
|
|
127
158
|
|
|
128
|
-
const
|
|
159
|
+
const virtual = getVirtualFile(abs);
|
|
160
|
+
|
|
161
|
+
engine.removeFile(virtual, config);
|
|
129
162
|
|
|
130
|
-
|
|
163
|
+
watchedDeps.delete(abs);
|
|
131
164
|
|
|
132
165
|
const next = engine.getAllEvents();
|
|
133
166
|
|
|
@@ -139,6 +172,12 @@ export async function watch() {
|
|
|
139
172
|
);
|
|
140
173
|
});
|
|
141
174
|
|
|
175
|
+
// WATCH DEPS
|
|
176
|
+
for (const dep of watchedDeps) {
|
|
177
|
+
watcher.add(dep);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// EXIT
|
|
142
181
|
process.on("SIGINT", async () => {
|
|
143
182
|
console.log(chalk.yellow("\nStopping watcher..."));
|
|
144
183
|
await watcher.close();
|
|
@@ -15,9 +15,24 @@ export function findTrackParamIndex(
|
|
|
15
15
|
|
|
16
16
|
function visit(node: ts.Node) {
|
|
17
17
|
if (ts.isCallExpression(node)) {
|
|
18
|
-
|
|
18
|
+
let isTrackCall = false;
|
|
19
19
|
|
|
20
|
-
if (
|
|
20
|
+
if (ts.isPropertyAccessExpression(node.expression)) {
|
|
21
|
+
const method = node.expression.name.text;
|
|
22
|
+
|
|
23
|
+
if (method === "track") {
|
|
24
|
+
isTrackCall = true;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// fallback
|
|
29
|
+
if (ts.isIdentifier(node.expression)) {
|
|
30
|
+
if (node.expression.text === "track") {
|
|
31
|
+
isTrackCall = true;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (isTrackCall) {
|
|
21
36
|
for (const arg of node.arguments) {
|
|
22
37
|
if (ts.isIdentifier(arg)) {
|
|
23
38
|
const idx = paramIndex.get(arg.text);
|
|
@@ -26,6 +41,11 @@ export function findTrackParamIndex(
|
|
|
26
41
|
return;
|
|
27
42
|
}
|
|
28
43
|
}
|
|
44
|
+
|
|
45
|
+
if (ts.isStringLiteral(arg)) {
|
|
46
|
+
result = 0;
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
29
49
|
}
|
|
30
50
|
}
|
|
31
51
|
}
|
|
@@ -39,13 +59,3 @@ export function findTrackParamIndex(
|
|
|
39
59
|
|
|
40
60
|
return result;
|
|
41
61
|
}
|
|
42
|
-
|
|
43
|
-
function getCallName(expr: ts.Expression): string {
|
|
44
|
-
if (ts.isIdentifier(expr)) return expr.text;
|
|
45
|
-
|
|
46
|
-
if (ts.isPropertyAccessExpression(expr)) {
|
|
47
|
-
return expr.name.text;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
return "";
|
|
51
|
-
}
|
package/src/engine/resolver.ts
CHANGED
|
@@ -12,12 +12,12 @@ export function resolveNodeValue(
|
|
|
12
12
|
paramMap?: Map<string, ts.Expression>,
|
|
13
13
|
seen = new Set<ts.Node>()
|
|
14
14
|
): ResolveResult {
|
|
15
|
-
|
|
16
15
|
if (seen.has(node)) {
|
|
17
16
|
return { values: [], dynamic: true };
|
|
18
17
|
}
|
|
19
18
|
seen.add(node);
|
|
20
19
|
|
|
20
|
+
// param mapping
|
|
21
21
|
if (ts.isIdentifier(node) && paramMap?.has(node.text)) {
|
|
22
22
|
return resolveNodeValue(
|
|
23
23
|
paramMap.get(node.text)!,
|
|
@@ -27,54 +27,70 @@ export function resolveNodeValue(
|
|
|
27
27
|
);
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
+
// STRING
|
|
30
31
|
if (ts.isStringLiteral(node)) {
|
|
31
32
|
return { values: [node.text], dynamic: false };
|
|
32
33
|
}
|
|
33
34
|
|
|
35
|
+
// ARRAY
|
|
36
|
+
if (ts.isArrayLiteralExpression(node)) {
|
|
37
|
+
const values: string[] = [];
|
|
38
|
+
|
|
39
|
+
for (const el of node.elements) {
|
|
40
|
+
const res = resolveNodeValue(el, checker, paramMap, seen);
|
|
41
|
+
if (res.values.length) {
|
|
42
|
+
values.push(...res.values);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return { values, dynamic: true };
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// TEMPLATE
|
|
34
50
|
if (ts.isTemplateExpression(node)) {
|
|
35
|
-
let
|
|
51
|
+
let results = [node.head.text];
|
|
36
52
|
|
|
37
53
|
for (const span of node.templateSpans) {
|
|
38
|
-
|
|
39
|
-
|
|
54
|
+
const res = resolveNodeValue(span.expression, checker, paramMap, seen);
|
|
55
|
+
|
|
56
|
+
if (res.values.length) {
|
|
57
|
+
results = results.flatMap(r =>
|
|
58
|
+
res.values.map(v => r + v)
|
|
59
|
+
);
|
|
60
|
+
} else {
|
|
61
|
+
results = results.map(r => r + "*");
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
results = results.map(r => r + span.literal.text);
|
|
40
65
|
}
|
|
41
66
|
|
|
42
|
-
return { values:
|
|
67
|
+
return { values: results, dynamic: true };
|
|
43
68
|
}
|
|
44
69
|
|
|
70
|
+
// CONDITIONAL
|
|
45
71
|
if (ts.isConditionalExpression(node)) {
|
|
46
72
|
const a = resolveNodeValue(node.whenTrue, checker, paramMap, seen);
|
|
47
73
|
const b = resolveNodeValue(node.whenFalse, checker, paramMap, seen);
|
|
48
74
|
|
|
49
75
|
return {
|
|
50
|
-
values: [...new Set([...
|
|
76
|
+
values: [...new Set([...a.values, ...b.values])],
|
|
51
77
|
dynamic: true,
|
|
52
78
|
};
|
|
53
79
|
}
|
|
54
80
|
|
|
81
|
+
// IDENTIFIER
|
|
55
82
|
if (ts.isIdentifier(node)) {
|
|
56
83
|
let symbol = checker.getSymbolAtLocation(node);
|
|
57
84
|
if (!symbol) return { values: [], dynamic: true };
|
|
58
85
|
|
|
59
|
-
|
|
60
|
-
if (ts.isVariableDeclaration(decl) && decl.initializer) {
|
|
61
|
-
return resolveNodeValue(
|
|
62
|
-
decl.initializer,
|
|
63
|
-
checker,
|
|
64
|
-
paramMap,
|
|
65
|
-
seen
|
|
66
|
-
);
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
const original = symbol;
|
|
71
|
-
|
|
86
|
+
// unwrap export
|
|
72
87
|
symbol = resolveExportedSymbol(symbol, checker) ?? symbol;
|
|
73
88
|
|
|
74
|
-
if (symbol
|
|
89
|
+
if (symbol.flags & ts.SymbolFlags.Alias) {
|
|
75
90
|
symbol = checker.getAliasedSymbol(symbol);
|
|
76
91
|
}
|
|
77
92
|
|
|
93
|
+
// inline variable
|
|
78
94
|
for (const decl of symbol.getDeclarations() ?? []) {
|
|
79
95
|
if (ts.isVariableDeclaration(decl) && decl.initializer) {
|
|
80
96
|
return resolveNodeValue(
|
|
@@ -95,14 +111,38 @@ export function resolveNodeValue(
|
|
|
95
111
|
}
|
|
96
112
|
}
|
|
97
113
|
|
|
114
|
+
// UNION TYPES
|
|
115
|
+
const type = checker.getTypeAtLocation(node);
|
|
116
|
+
|
|
117
|
+
if (type.isUnion()) {
|
|
118
|
+
const values: string[] = [];
|
|
119
|
+
|
|
120
|
+
for (const t of type.types) {
|
|
121
|
+
if ((t as any).isStringLiteral?.()) {
|
|
122
|
+
values.push((t as any).value);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if (values.length) {
|
|
127
|
+
return { values, dynamic: false };
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// TYPE FALLBACK (as const)
|
|
132
|
+
if ((type as any).isStringLiteral?.()) {
|
|
133
|
+
return { values: [(type as any).value], dynamic: false };
|
|
134
|
+
}
|
|
135
|
+
|
|
98
136
|
return { values: [], dynamic: true };
|
|
99
137
|
}
|
|
100
138
|
|
|
139
|
+
// PROPERTY ACCESS (obj.a.b)
|
|
101
140
|
if (ts.isPropertyAccessExpression(node)) {
|
|
141
|
+
|
|
102
142
|
const symbol = checker.getSymbolAtLocation(node);
|
|
103
143
|
|
|
104
144
|
if (symbol) {
|
|
105
|
-
let s = symbol;
|
|
145
|
+
let s = resolveExportedSymbol(symbol, checker) ?? symbol;
|
|
106
146
|
|
|
107
147
|
if (s.flags & ts.SymbolFlags.Alias) {
|
|
108
148
|
s = checker.getAliasedSymbol(s);
|
|
@@ -120,6 +160,23 @@ export function resolveNodeValue(
|
|
|
120
160
|
}
|
|
121
161
|
}
|
|
122
162
|
|
|
163
|
+
// constant enum / readonly
|
|
164
|
+
const constantValue = checker.getConstantValue(node as any);
|
|
165
|
+
if (typeof constantValue === "string") {
|
|
166
|
+
return { values: [constantValue], dynamic: false };
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// type fallback
|
|
170
|
+
const type = checker.getTypeAtLocation(node);
|
|
171
|
+
|
|
172
|
+
if ((type as any).isStringLiteral?.()) {
|
|
173
|
+
return {
|
|
174
|
+
values: [(type as any).value],
|
|
175
|
+
dynamic: false,
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// object literal resolve
|
|
123
180
|
if (ts.isIdentifier(node.expression)) {
|
|
124
181
|
const objSymbol = checker.getSymbolAtLocation(node.expression);
|
|
125
182
|
|
|
@@ -152,6 +209,40 @@ export function resolveNodeValue(
|
|
|
152
209
|
return { values: [], dynamic: true };
|
|
153
210
|
}
|
|
154
211
|
|
|
212
|
+
// ARRAY ACCESS (arr[0])
|
|
213
|
+
if (ts.isElementAccessExpression(node)) {
|
|
214
|
+
const obj = resolveNodeValue(node.expression, checker, paramMap, seen);
|
|
215
|
+
|
|
216
|
+
if (!node.argumentExpression) {
|
|
217
|
+
return { values: obj.values, dynamic: true };
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
const index = resolveNodeValue(
|
|
221
|
+
node.argumentExpression,
|
|
222
|
+
checker,
|
|
223
|
+
paramMap,
|
|
224
|
+
seen
|
|
225
|
+
);
|
|
226
|
+
|
|
227
|
+
// if index known
|
|
228
|
+
if (index.values.length === 1) {
|
|
229
|
+
const i = Number(index.values[0]);
|
|
230
|
+
|
|
231
|
+
if (!Number.isNaN(i) && obj.values[i] !== undefined) {
|
|
232
|
+
return {
|
|
233
|
+
values: [obj.values[i]],
|
|
234
|
+
dynamic: false,
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// fallback → dynamic
|
|
240
|
+
return obj.values.length
|
|
241
|
+
? { values: obj.values, dynamic: true }
|
|
242
|
+
: { values: [], dynamic: true };
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// OBJECT
|
|
155
246
|
if (ts.isObjectLiteralExpression(node)) {
|
|
156
247
|
const values: string[] = [];
|
|
157
248
|
|
|
@@ -160,7 +251,7 @@ export function resolveNodeValue(
|
|
|
160
251
|
|
|
161
252
|
const name = prop.name.getText();
|
|
162
253
|
|
|
163
|
-
if (name
|
|
254
|
+
if (["event", "name", "type"].includes(name)) {
|
|
164
255
|
const res = resolveNodeValue(prop.initializer, checker, paramMap, seen);
|
|
165
256
|
values.push(...res.values);
|
|
166
257
|
}
|
|
@@ -169,24 +260,28 @@ export function resolveNodeValue(
|
|
|
169
260
|
return { values, dynamic: true };
|
|
170
261
|
}
|
|
171
262
|
|
|
263
|
+
// BINARY ("btn_" + name)
|
|
172
264
|
if (ts.isBinaryExpression(node)) {
|
|
173
265
|
if (node.operatorToken.kind === ts.SyntaxKind.PlusToken) {
|
|
174
266
|
const left = resolveNodeValue(node.left, checker, paramMap, seen);
|
|
175
267
|
const right = resolveNodeValue(node.right, checker, paramMap, seen);
|
|
176
268
|
|
|
177
|
-
if (left
|
|
178
|
-
return {
|
|
179
|
-
values: left.values.flatMap(l =>
|
|
180
|
-
right.values.length
|
|
181
|
-
? right.values.map(r => l + r)
|
|
182
|
-
: [l + "*"]
|
|
183
|
-
),
|
|
184
|
-
dynamic: true,
|
|
185
|
-
};
|
|
269
|
+
if (!left.values.length) {
|
|
270
|
+
return { values: [], dynamic: true };
|
|
186
271
|
}
|
|
272
|
+
|
|
273
|
+
return {
|
|
274
|
+
values: left.values.flatMap(l =>
|
|
275
|
+
right.values.length
|
|
276
|
+
? right.values.map(r => l + r)
|
|
277
|
+
: [l + "*"]
|
|
278
|
+
),
|
|
279
|
+
dynamic: true,
|
|
280
|
+
};
|
|
187
281
|
}
|
|
188
282
|
}
|
|
189
283
|
|
|
284
|
+
// CALL → dynamic
|
|
190
285
|
if (ts.isCallExpression(node)) {
|
|
191
286
|
return { values: [], dynamic: true };
|
|
192
287
|
}
|
package/src/utils/universal.ts
CHANGED
|
@@ -2,10 +2,17 @@ import path from "path";
|
|
|
2
2
|
import { ParseResult } from "../types";
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
|
-
*
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
5
|
+
* Strip comments (HTML + JS)
|
|
6
|
+
*/
|
|
7
|
+
function stripComments(input: string): string {
|
|
8
|
+
return input
|
|
9
|
+
.replace(/<!--[\s\S]*?-->/g, "")
|
|
10
|
+
.replace(/\/\*[\s\S]*?\*\//g, "")
|
|
11
|
+
.replace(/\/\/.*$/gm, "");
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Extract JS expressions safely
|
|
9
16
|
*/
|
|
10
17
|
function extractJS(input: string): string[] {
|
|
11
18
|
const result: string[] = [];
|
|
@@ -24,7 +31,6 @@ function extractJS(input: string): string[] {
|
|
|
24
31
|
if (escape) escape = false;
|
|
25
32
|
else if (ch === "\\") escape = true;
|
|
26
33
|
else if (ch === inString) inString = null;
|
|
27
|
-
|
|
28
34
|
i++;
|
|
29
35
|
continue;
|
|
30
36
|
}
|
|
@@ -68,7 +74,17 @@ function extractJS(input: string): string[] {
|
|
|
68
74
|
|
|
69
75
|
if (depth === 0) {
|
|
70
76
|
const expr = input.slice(i + 1, j - 1).trim();
|
|
71
|
-
|
|
77
|
+
|
|
78
|
+
if (
|
|
79
|
+
expr &&
|
|
80
|
+
(expr.includes("(") ||
|
|
81
|
+
expr.includes("=") ||
|
|
82
|
+
expr.includes(".") ||
|
|
83
|
+
expr.includes("track"))
|
|
84
|
+
) {
|
|
85
|
+
result.push(expr);
|
|
86
|
+
}
|
|
87
|
+
|
|
72
88
|
i = j;
|
|
73
89
|
continue;
|
|
74
90
|
}
|
|
@@ -95,7 +111,14 @@ function extractJS(input: string): string[] {
|
|
|
95
111
|
j++;
|
|
96
112
|
}
|
|
97
113
|
|
|
98
|
-
if (
|
|
114
|
+
if (
|
|
115
|
+
val.trim() &&
|
|
116
|
+
(val.includes("(") ||
|
|
117
|
+
val.includes("=>") ||
|
|
118
|
+
val.includes("track"))
|
|
119
|
+
) {
|
|
120
|
+
result.push(val.trim());
|
|
121
|
+
}
|
|
99
122
|
|
|
100
123
|
i = j + 1;
|
|
101
124
|
continue;
|
|
@@ -123,6 +146,9 @@ function extractJS(input: string): string[] {
|
|
|
123
146
|
return result;
|
|
124
147
|
}
|
|
125
148
|
|
|
149
|
+
/**
|
|
150
|
+
* Extract loose JS but less noisy
|
|
151
|
+
*/
|
|
126
152
|
function extractLooseJS(input: string): string[] {
|
|
127
153
|
const result: string[] = [];
|
|
128
154
|
|
|
@@ -134,12 +160,13 @@ function extractLooseJS(input: string): string[] {
|
|
|
134
160
|
if (!l) continue;
|
|
135
161
|
if (l.startsWith("<")) continue;
|
|
136
162
|
|
|
163
|
+
// strict filter
|
|
137
164
|
if (
|
|
138
|
-
l.includes("
|
|
139
|
-
l.includes("
|
|
165
|
+
l.includes("track") ||
|
|
166
|
+
l.includes(".track(") ||
|
|
140
167
|
l.includes("=>") ||
|
|
141
|
-
l.
|
|
142
|
-
l.includes("
|
|
168
|
+
l.startsWith("function") ||
|
|
169
|
+
l.includes("(")
|
|
143
170
|
) {
|
|
144
171
|
result.push(l);
|
|
145
172
|
}
|
|
@@ -149,14 +176,8 @@ function extractLooseJS(input: string): string[] {
|
|
|
149
176
|
}
|
|
150
177
|
|
|
151
178
|
/**
|
|
152
|
-
* Universal
|
|
153
|
-
* - Astro
|
|
154
|
-
* - Vue
|
|
155
|
-
* - Svelte
|
|
156
|
-
* - React / JSX
|
|
157
|
-
* - Node / backend
|
|
179
|
+
* Universal parser
|
|
158
180
|
*/
|
|
159
|
-
|
|
160
181
|
export function parseUniversal(
|
|
161
182
|
content: string,
|
|
162
183
|
file: string
|
|
@@ -164,12 +185,14 @@ export function parseUniversal(
|
|
|
164
185
|
const parts: string[] = [];
|
|
165
186
|
const deps: string[] = [];
|
|
166
187
|
|
|
167
|
-
|
|
168
|
-
|
|
188
|
+
const clean = stripComments(content);
|
|
189
|
+
|
|
190
|
+
// Astro frontmatter
|
|
191
|
+
const fm = clean.match(/^---([\s\S]*?)---/);
|
|
169
192
|
if (fm?.[1]) parts.push(fm[1]);
|
|
170
193
|
|
|
171
194
|
// <script src="">
|
|
172
|
-
const scriptSrc =
|
|
195
|
+
const scriptSrc = clean.matchAll(
|
|
173
196
|
/<script\b[^>]*?\bsrc=["'](.+?)["'][^>]*>/g
|
|
174
197
|
);
|
|
175
198
|
|
|
@@ -184,28 +207,33 @@ export function parseUniversal(
|
|
|
184
207
|
}
|
|
185
208
|
|
|
186
209
|
// inline <script>
|
|
187
|
-
const scripts =
|
|
188
|
-
/<script\b[^>]
|
|
210
|
+
const scripts = clean.matchAll(
|
|
211
|
+
/<script\b([^>]*)>([\s\S]*?)<\/script>/g
|
|
189
212
|
);
|
|
190
213
|
|
|
191
214
|
for (const m of scripts) {
|
|
192
|
-
|
|
215
|
+
const attrs = m[1];
|
|
216
|
+
|
|
217
|
+
// skip JSON / data
|
|
218
|
+
if (/type=["']application\/json/.test(attrs)) continue;
|
|
219
|
+
|
|
220
|
+
if (m[2].trim()) {
|
|
221
|
+
parts.push(m[2]);
|
|
222
|
+
}
|
|
193
223
|
}
|
|
194
224
|
|
|
195
|
-
|
|
225
|
+
// remove scripts
|
|
226
|
+
const noScripts = clean.replace(
|
|
196
227
|
/<script[\s\S]*?<\/script>/g,
|
|
197
228
|
""
|
|
198
229
|
);
|
|
199
230
|
|
|
231
|
+
// loose JS
|
|
200
232
|
const loose = extractLooseJS(noScripts);
|
|
201
|
-
|
|
202
|
-
for (const l of loose) {
|
|
203
|
-
parts.push(l);
|
|
204
|
-
}
|
|
233
|
+
parts.push(...loose);
|
|
205
234
|
|
|
206
235
|
// expressions
|
|
207
|
-
const extracted = extractJS(
|
|
208
|
-
|
|
236
|
+
const extracted = extractJS(clean);
|
|
209
237
|
for (const e of extracted) {
|
|
210
238
|
parts.push(e + ";");
|
|
211
239
|
}
|