@utopia-ai/cli 0.1.0
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/.claude/settings.json +1 -0
- package/.claude/settings.local.json +38 -0
- package/bin/utopia.js +20 -0
- package/package.json +46 -0
- package/python/README.md +34 -0
- package/python/instrumenter/instrument.py +1148 -0
- package/python/pyproject.toml +32 -0
- package/python/setup.py +27 -0
- package/python/utopia_runtime/__init__.py +30 -0
- package/python/utopia_runtime/__pycache__/__init__.cpython-313.pyc +0 -0
- package/python/utopia_runtime/__pycache__/client.cpython-313.pyc +0 -0
- package/python/utopia_runtime/__pycache__/probe.cpython-313.pyc +0 -0
- package/python/utopia_runtime/client.py +31 -0
- package/python/utopia_runtime/probe.py +446 -0
- package/python/utopia_runtime.egg-info/PKG-INFO +59 -0
- package/python/utopia_runtime.egg-info/SOURCES.txt +10 -0
- package/python/utopia_runtime.egg-info/dependency_links.txt +1 -0
- package/python/utopia_runtime.egg-info/top_level.txt +1 -0
- package/scripts/publish-npm.sh +14 -0
- package/scripts/publish-pypi.sh +17 -0
- package/src/cli/commands/codex.ts +193 -0
- package/src/cli/commands/context.ts +188 -0
- package/src/cli/commands/destruct.ts +237 -0
- package/src/cli/commands/easter-eggs.ts +203 -0
- package/src/cli/commands/init.ts +505 -0
- package/src/cli/commands/instrument.ts +962 -0
- package/src/cli/commands/mcp.ts +16 -0
- package/src/cli/commands/serve.ts +194 -0
- package/src/cli/commands/status.ts +304 -0
- package/src/cli/commands/validate.ts +328 -0
- package/src/cli/index.ts +37 -0
- package/src/cli/utils/config.ts +54 -0
- package/src/graph/index.ts +687 -0
- package/src/instrumenter/javascript.ts +1798 -0
- package/src/mcp/index.ts +886 -0
- package/src/runtime/js/index.ts +518 -0
- package/src/runtime/js/package-lock.json +30 -0
- package/src/runtime/js/package.json +30 -0
- package/src/runtime/js/tsconfig.json +16 -0
- package/src/server/db/index.ts +26 -0
- package/src/server/db/schema.ts +45 -0
- package/src/server/index.ts +79 -0
- package/src/server/middleware/auth.ts +74 -0
- package/src/server/routes/admin.ts +36 -0
- package/src/server/routes/graph.ts +358 -0
- package/src/server/routes/probes.ts +286 -0
- package/src/types.ts +147 -0
- package/src/utopia-mode/index.ts +206 -0
- package/tsconfig.json +19 -0
|
@@ -0,0 +1,1798 @@
|
|
|
1
|
+
// Utopia JS/TS AST Instrumenter
|
|
2
|
+
// Uses Babel to parse, traverse, and transform JS/TS source files,
|
|
3
|
+
// injecting lightweight probes for error tracking, database monitoring,
|
|
4
|
+
// API call tracing, infrastructure reporting, and function profiling.
|
|
5
|
+
|
|
6
|
+
import * as parser from '@babel/parser';
|
|
7
|
+
import _traverse from '@babel/traverse';
|
|
8
|
+
import _generate from '@babel/generator';
|
|
9
|
+
import * as t from '@babel/types';
|
|
10
|
+
import { readFile, writeFile } from 'node:fs/promises';
|
|
11
|
+
import { resolve, relative, extname, basename, dirname, join } from 'node:path';
|
|
12
|
+
import { readdir, stat } from 'node:fs/promises';
|
|
13
|
+
|
|
14
|
+
// Handle default export interop for CommonJS/ESM compatibility
|
|
15
|
+
const traverse = (_traverse as any).default || _traverse;
|
|
16
|
+
const generate = (_generate as any).default || _generate;
|
|
17
|
+
|
|
18
|
+
// ---------------------------------------------------------------------------
|
|
19
|
+
// Types
|
|
20
|
+
// ---------------------------------------------------------------------------
|
|
21
|
+
|
|
22
|
+
interface InstrumentOptions {
|
|
23
|
+
probeTypes: ('error' | 'database' | 'api' | 'infra' | 'function')[];
|
|
24
|
+
utopiaMode: boolean;
|
|
25
|
+
dryRun: boolean;
|
|
26
|
+
entryPoints?: string[];
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
interface InstrumentResult {
|
|
30
|
+
file: string;
|
|
31
|
+
probesAdded: { type: string; line: number; functionName: string }[];
|
|
32
|
+
success: boolean;
|
|
33
|
+
error?: string;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
interface ValidationResult {
|
|
37
|
+
valid: boolean;
|
|
38
|
+
errors: string[];
|
|
39
|
+
warnings: string[];
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// ---------------------------------------------------------------------------
|
|
43
|
+
// Constants
|
|
44
|
+
// ---------------------------------------------------------------------------
|
|
45
|
+
|
|
46
|
+
const SKIP_DIRS = new Set([
|
|
47
|
+
'node_modules',
|
|
48
|
+
'dist',
|
|
49
|
+
'build',
|
|
50
|
+
'.next',
|
|
51
|
+
'__tests__',
|
|
52
|
+
'__mocks__',
|
|
53
|
+
'.git',
|
|
54
|
+
'.utopia',
|
|
55
|
+
'coverage',
|
|
56
|
+
]);
|
|
57
|
+
|
|
58
|
+
const VALID_EXTENSIONS = new Set(['.js', '.ts', '.jsx', '.tsx']);
|
|
59
|
+
|
|
60
|
+
const ENTRY_POINT_BASENAMES = new Set([
|
|
61
|
+
'index.ts',
|
|
62
|
+
'index.js',
|
|
63
|
+
'index.tsx',
|
|
64
|
+
'index.jsx',
|
|
65
|
+
'main.ts',
|
|
66
|
+
'main.js',
|
|
67
|
+
'server.ts',
|
|
68
|
+
'server.js',
|
|
69
|
+
'app.ts',
|
|
70
|
+
'app.js',
|
|
71
|
+
]);
|
|
72
|
+
|
|
73
|
+
const PRISMA_METHODS = new Set([
|
|
74
|
+
'findMany',
|
|
75
|
+
'findUnique',
|
|
76
|
+
'findFirst',
|
|
77
|
+
'findUniqueOrThrow',
|
|
78
|
+
'findFirstOrThrow',
|
|
79
|
+
'create',
|
|
80
|
+
'createMany',
|
|
81
|
+
'update',
|
|
82
|
+
'updateMany',
|
|
83
|
+
'upsert',
|
|
84
|
+
'delete',
|
|
85
|
+
'deleteMany',
|
|
86
|
+
'count',
|
|
87
|
+
'aggregate',
|
|
88
|
+
'groupBy',
|
|
89
|
+
]);
|
|
90
|
+
|
|
91
|
+
const MONGOOSE_METHODS = new Set([
|
|
92
|
+
'find',
|
|
93
|
+
'findOne',
|
|
94
|
+
'findById',
|
|
95
|
+
'findOneAndUpdate',
|
|
96
|
+
'findOneAndDelete',
|
|
97
|
+
'findOneAndReplace',
|
|
98
|
+
'findByIdAndUpdate',
|
|
99
|
+
'findByIdAndDelete',
|
|
100
|
+
'create',
|
|
101
|
+
'insertMany',
|
|
102
|
+
'updateOne',
|
|
103
|
+
'updateMany',
|
|
104
|
+
'deleteOne',
|
|
105
|
+
'deleteMany',
|
|
106
|
+
'countDocuments',
|
|
107
|
+
'estimatedDocumentCount',
|
|
108
|
+
'aggregate',
|
|
109
|
+
]);
|
|
110
|
+
|
|
111
|
+
const KNEX_QUERY_METHODS = new Set([
|
|
112
|
+
'select',
|
|
113
|
+
'insert',
|
|
114
|
+
'update',
|
|
115
|
+
'delete',
|
|
116
|
+
'del',
|
|
117
|
+
'where',
|
|
118
|
+
'from',
|
|
119
|
+
'into',
|
|
120
|
+
'raw',
|
|
121
|
+
]);
|
|
122
|
+
|
|
123
|
+
const BABEL_PLUGINS: parser.ParserPlugin[] = [
|
|
124
|
+
'typescript',
|
|
125
|
+
'jsx',
|
|
126
|
+
'decorators-legacy',
|
|
127
|
+
'classProperties',
|
|
128
|
+
'optionalChaining',
|
|
129
|
+
'nullishCoalescingOperator',
|
|
130
|
+
'dynamicImport',
|
|
131
|
+
'exportDefaultFrom',
|
|
132
|
+
'importMeta',
|
|
133
|
+
];
|
|
134
|
+
|
|
135
|
+
// ---------------------------------------------------------------------------
|
|
136
|
+
// Helpers
|
|
137
|
+
// ---------------------------------------------------------------------------
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Detect Babel parser plugins to use based on file extension.
|
|
141
|
+
*/
|
|
142
|
+
function getParserPlugins(filePath: string): parser.ParserPlugin[] {
|
|
143
|
+
const ext = extname(filePath).toLowerCase();
|
|
144
|
+
// All files get the full plugin set; Babel is fine with unused plugins
|
|
145
|
+
return [...BABEL_PLUGINS];
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Determine the source type for the parser.
|
|
150
|
+
*/
|
|
151
|
+
function getSourceType(filePath: string): 'module' | 'script' {
|
|
152
|
+
// Default to module for all TS/modern JS
|
|
153
|
+
return 'module';
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Get the enclosing function name from a Babel traversal path.
|
|
158
|
+
*/
|
|
159
|
+
function getEnclosingFunctionName(path: any): string {
|
|
160
|
+
let current = path;
|
|
161
|
+
while (current) {
|
|
162
|
+
if (current.isFunctionDeclaration() && current.node.id) {
|
|
163
|
+
return current.node.id.name;
|
|
164
|
+
}
|
|
165
|
+
if (current.isFunctionExpression() && current.node.id) {
|
|
166
|
+
return current.node.id.name;
|
|
167
|
+
}
|
|
168
|
+
if (current.isClassMethod() || current.isObjectMethod()) {
|
|
169
|
+
const key = current.node.key;
|
|
170
|
+
if (t.isIdentifier(key)) return key.name;
|
|
171
|
+
if (t.isStringLiteral(key)) return key.value;
|
|
172
|
+
}
|
|
173
|
+
if (
|
|
174
|
+
current.isVariableDeclarator &&
|
|
175
|
+
current.isVariableDeclarator() &&
|
|
176
|
+
t.isIdentifier(current.node.id)
|
|
177
|
+
) {
|
|
178
|
+
return current.node.id.name;
|
|
179
|
+
}
|
|
180
|
+
// Arrow function assigned to variable
|
|
181
|
+
if (
|
|
182
|
+
current.parentPath &&
|
|
183
|
+
current.parentPath.isVariableDeclarator() &&
|
|
184
|
+
t.isIdentifier(current.parentPath.node.id)
|
|
185
|
+
) {
|
|
186
|
+
return current.parentPath.node.id.name;
|
|
187
|
+
}
|
|
188
|
+
// Object property
|
|
189
|
+
if (
|
|
190
|
+
current.parentPath &&
|
|
191
|
+
current.parentPath.isObjectProperty() &&
|
|
192
|
+
t.isIdentifier(current.parentPath.node.key)
|
|
193
|
+
) {
|
|
194
|
+
return current.parentPath.node.key.name;
|
|
195
|
+
}
|
|
196
|
+
// Export default
|
|
197
|
+
if (current.parentPath && current.parentPath.isExportDefaultDeclaration()) {
|
|
198
|
+
return 'default';
|
|
199
|
+
}
|
|
200
|
+
current = current.parentPath;
|
|
201
|
+
}
|
|
202
|
+
return '<anonymous>';
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Given a CallExpression path, get enclosing function name by walking up.
|
|
207
|
+
*/
|
|
208
|
+
function getEnclosingFnName(path: any): string {
|
|
209
|
+
let current = path.parentPath;
|
|
210
|
+
while (current) {
|
|
211
|
+
if (current.isFunctionDeclaration() && current.node.id) {
|
|
212
|
+
return current.node.id.name;
|
|
213
|
+
}
|
|
214
|
+
if (current.isFunctionExpression() && current.node.id) {
|
|
215
|
+
return current.node.id.name;
|
|
216
|
+
}
|
|
217
|
+
if (current.isArrowFunctionExpression()) {
|
|
218
|
+
// Check if assigned to a variable
|
|
219
|
+
if (
|
|
220
|
+
current.parentPath &&
|
|
221
|
+
current.parentPath.isVariableDeclarator() &&
|
|
222
|
+
t.isIdentifier(current.parentPath.node.id)
|
|
223
|
+
) {
|
|
224
|
+
return current.parentPath.node.id.name;
|
|
225
|
+
}
|
|
226
|
+
if (
|
|
227
|
+
current.parentPath &&
|
|
228
|
+
current.parentPath.isObjectProperty() &&
|
|
229
|
+
t.isIdentifier(current.parentPath.node.key)
|
|
230
|
+
) {
|
|
231
|
+
return current.parentPath.node.key.name;
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
if (current.isClassMethod() || current.isObjectMethod()) {
|
|
235
|
+
const key = current.node.key;
|
|
236
|
+
if (t.isIdentifier(key)) return key.name;
|
|
237
|
+
if (t.isStringLiteral(key)) return key.value;
|
|
238
|
+
}
|
|
239
|
+
current = current.parentPath;
|
|
240
|
+
}
|
|
241
|
+
return '<anonymous>';
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Read a specific line from source text. Returns empty string if out of range.
|
|
246
|
+
*/
|
|
247
|
+
function getSourceLine(source: string, lineNumber: number): string {
|
|
248
|
+
const lines = source.split('\n');
|
|
249
|
+
if (lineNumber < 1 || lineNumber > lines.length) return '';
|
|
250
|
+
return lines[lineNumber - 1].trim();
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* Get the function name from a function-like path node.
|
|
255
|
+
*/
|
|
256
|
+
function getFunctionName(path: any): string {
|
|
257
|
+
const node = path.node;
|
|
258
|
+
|
|
259
|
+
// FunctionDeclaration
|
|
260
|
+
if (t.isFunctionDeclaration(node) && node.id) {
|
|
261
|
+
return node.id.name;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// ClassMethod / ObjectMethod
|
|
265
|
+
if (t.isClassMethod(node) || t.isObjectMethod(node)) {
|
|
266
|
+
if (t.isIdentifier(node.key)) return node.key.name;
|
|
267
|
+
if (t.isStringLiteral(node.key)) return node.key.value;
|
|
268
|
+
return '<computed>';
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// FunctionExpression with name
|
|
272
|
+
if (t.isFunctionExpression(node) && node.id) {
|
|
273
|
+
return node.id.name;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// Variable assignment: const foo = () => {} or const foo = function() {}
|
|
277
|
+
if (
|
|
278
|
+
path.parentPath &&
|
|
279
|
+
path.parentPath.isVariableDeclarator() &&
|
|
280
|
+
t.isIdentifier(path.parentPath.node.id)
|
|
281
|
+
) {
|
|
282
|
+
return path.parentPath.node.id.name;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// Object property: { foo: () => {} }
|
|
286
|
+
if (
|
|
287
|
+
path.parentPath &&
|
|
288
|
+
path.parentPath.isObjectProperty() &&
|
|
289
|
+
t.isIdentifier(path.parentPath.node.key)
|
|
290
|
+
) {
|
|
291
|
+
return path.parentPath.node.key.name;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// Export default
|
|
295
|
+
if (path.parentPath && path.parentPath.isExportDefaultDeclaration()) {
|
|
296
|
+
return 'default';
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// Assignment expression: module.exports = function() {}
|
|
300
|
+
if (
|
|
301
|
+
path.parentPath &&
|
|
302
|
+
path.parentPath.isAssignmentExpression() &&
|
|
303
|
+
t.isMemberExpression(path.parentPath.node.left)
|
|
304
|
+
) {
|
|
305
|
+
const left = path.parentPath.node.left;
|
|
306
|
+
if (t.isIdentifier(left.property)) return left.property.name;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
return '<anonymous>';
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* Check if a function body is already wrapped in a utopia try/catch.
|
|
314
|
+
*/
|
|
315
|
+
function isAlreadyWrapped(body: t.BlockStatement): boolean {
|
|
316
|
+
if (body.body.length === 0) return false;
|
|
317
|
+
const first = body.body[0];
|
|
318
|
+
if (!t.isTryStatement(first)) return false;
|
|
319
|
+
// Check if there is a leading comment indicating utopia instrumentation
|
|
320
|
+
const leadingComments = first.leadingComments;
|
|
321
|
+
if (leadingComments && leadingComments.some((c: t.Comment) => c.value.includes('utopia:probe'))) {
|
|
322
|
+
return true;
|
|
323
|
+
}
|
|
324
|
+
// Also check the try block covers the whole body
|
|
325
|
+
if (body.body.length === 1 && t.isTryStatement(first)) {
|
|
326
|
+
const catchClause = first.handler;
|
|
327
|
+
if (catchClause && catchClause.param && t.isIdentifier(catchClause.param)) {
|
|
328
|
+
if (catchClause.param.name === '__utopia_err') return true;
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
return false;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
/**
|
|
335
|
+
* Check if a call expression is already wrapped in a utopia probe IIFE.
|
|
336
|
+
*/
|
|
337
|
+
function isInsideUtopiaIIFE(path: any): boolean {
|
|
338
|
+
let current = path.parentPath;
|
|
339
|
+
let depth = 0;
|
|
340
|
+
while (current && depth < 10) {
|
|
341
|
+
if (current.isCallExpression()) {
|
|
342
|
+
const callee = current.node.callee;
|
|
343
|
+
// Check for IIFE pattern: (async () => { ... })()
|
|
344
|
+
if (
|
|
345
|
+
t.isArrowFunctionExpression(callee) ||
|
|
346
|
+
t.isFunctionExpression(callee)
|
|
347
|
+
) {
|
|
348
|
+
// Check for utopia variable names in the body
|
|
349
|
+
const body = callee.body;
|
|
350
|
+
if (t.isBlockStatement(body)) {
|
|
351
|
+
const bodySource = body.body.some(
|
|
352
|
+
(stmt: t.Statement) =>
|
|
353
|
+
t.isVariableDeclaration(stmt) &&
|
|
354
|
+
stmt.declarations.some(
|
|
355
|
+
(d: t.VariableDeclarator) =>
|
|
356
|
+
t.isIdentifier(d.id) &&
|
|
357
|
+
(d.id.name.startsWith('__utopia_db_') ||
|
|
358
|
+
d.id.name.startsWith('__utopia_api_'))
|
|
359
|
+
)
|
|
360
|
+
);
|
|
361
|
+
if (bodySource) return true;
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
current = current.parentPath;
|
|
366
|
+
depth++;
|
|
367
|
+
}
|
|
368
|
+
return false;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
/**
|
|
372
|
+
* Convert a CallExpression node to a readable string representation.
|
|
373
|
+
*/
|
|
374
|
+
function callExpressionToString(node: t.CallExpression): string {
|
|
375
|
+
try {
|
|
376
|
+
const result = generate(node, { concise: true });
|
|
377
|
+
// Truncate very long call strings
|
|
378
|
+
const str = result.code;
|
|
379
|
+
if (str.length > 200) return str.slice(0, 200) + '...';
|
|
380
|
+
return str;
|
|
381
|
+
} catch {
|
|
382
|
+
return '<unknown call>';
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
// ---------------------------------------------------------------------------
|
|
387
|
+
// Detection helpers for DB / API patterns
|
|
388
|
+
// ---------------------------------------------------------------------------
|
|
389
|
+
|
|
390
|
+
interface DetectedDbCall {
|
|
391
|
+
library: string;
|
|
392
|
+
operation: string;
|
|
393
|
+
table: string;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
/**
|
|
397
|
+
* Detect if a CallExpression is a database operation. Returns info or null.
|
|
398
|
+
*/
|
|
399
|
+
function detectDbCall(node: t.CallExpression): DetectedDbCall | null {
|
|
400
|
+
const callee = node.callee;
|
|
401
|
+
|
|
402
|
+
// Pattern: prisma.<model>.<method>()
|
|
403
|
+
// e.g. prisma.user.findMany()
|
|
404
|
+
if (t.isMemberExpression(callee) && t.isIdentifier(callee.property)) {
|
|
405
|
+
const method = callee.property.name;
|
|
406
|
+
if (PRISMA_METHODS.has(method)) {
|
|
407
|
+
const obj = callee.object;
|
|
408
|
+
if (t.isMemberExpression(obj) && t.isIdentifier(obj.property) && t.isIdentifier(obj.object)) {
|
|
409
|
+
if (obj.object.name === 'prisma' || obj.object.name === 'db') {
|
|
410
|
+
return { library: 'prisma', operation: method, table: obj.property.name };
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
// Pattern: db.query(...), pool.query(...), connection.query(...)
|
|
417
|
+
if (t.isMemberExpression(callee) && t.isIdentifier(callee.property)) {
|
|
418
|
+
const method = callee.property.name;
|
|
419
|
+
if (method === 'query' || method === 'execute') {
|
|
420
|
+
const obj = callee.object;
|
|
421
|
+
if (t.isIdentifier(obj)) {
|
|
422
|
+
const name = obj.name.toLowerCase();
|
|
423
|
+
if (
|
|
424
|
+
name === 'db' ||
|
|
425
|
+
name === 'pool' ||
|
|
426
|
+
name === 'connection' ||
|
|
427
|
+
name === 'client' ||
|
|
428
|
+
name === 'conn' ||
|
|
429
|
+
name === 'database' ||
|
|
430
|
+
name === 'pg' ||
|
|
431
|
+
name === 'mysql'
|
|
432
|
+
) {
|
|
433
|
+
return { library: 'sql', operation: method, table: '<query>' };
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
// Pattern: Model.find(...), Model.findOne(...), Model.create(...) (Mongoose)
|
|
440
|
+
if (t.isMemberExpression(callee) && t.isIdentifier(callee.property)) {
|
|
441
|
+
const method = callee.property.name;
|
|
442
|
+
if (MONGOOSE_METHODS.has(method)) {
|
|
443
|
+
const obj = callee.object;
|
|
444
|
+
if (t.isIdentifier(obj)) {
|
|
445
|
+
// Mongoose models are typically PascalCase
|
|
446
|
+
const name = obj.name;
|
|
447
|
+
if (name.length > 0 && name[0] === name[0].toUpperCase() && name[0] !== name[0].toLowerCase()) {
|
|
448
|
+
return { library: 'mongoose', operation: method, table: name };
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
// Pattern: knex('table').<method>() or knex.select()/knex.insert() etc.
|
|
455
|
+
if (t.isMemberExpression(callee) && t.isIdentifier(callee.property)) {
|
|
456
|
+
const method = callee.property.name;
|
|
457
|
+
if (KNEX_QUERY_METHODS.has(method)) {
|
|
458
|
+
const obj = callee.object;
|
|
459
|
+
// knex('table').select()
|
|
460
|
+
if (t.isCallExpression(obj) && t.isIdentifier(obj.callee)) {
|
|
461
|
+
if (obj.callee.name === 'knex') {
|
|
462
|
+
let table = '<table>';
|
|
463
|
+
if (obj.arguments.length > 0 && t.isStringLiteral(obj.arguments[0])) {
|
|
464
|
+
table = obj.arguments[0].value;
|
|
465
|
+
}
|
|
466
|
+
return { library: 'knex', operation: method, table };
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
// knex.select()
|
|
470
|
+
if (t.isIdentifier(obj) && obj.name === 'knex') {
|
|
471
|
+
return { library: 'knex', operation: method, table: '<query>' };
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
return null;
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
interface DetectedApiCall {
|
|
480
|
+
library: string;
|
|
481
|
+
method: string;
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
/**
|
|
485
|
+
* Detect if a CallExpression is an HTTP API call. Returns info or null.
|
|
486
|
+
*/
|
|
487
|
+
function detectApiCall(node: t.CallExpression): DetectedApiCall | null {
|
|
488
|
+
const callee = node.callee;
|
|
489
|
+
|
|
490
|
+
// Pattern: fetch(url, opts)
|
|
491
|
+
if (t.isIdentifier(callee) && callee.name === 'fetch') {
|
|
492
|
+
return { library: 'fetch', method: 'GET' };
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
// Pattern: axios.get(), axios.post(), axios.put(), axios.delete(), axios.patch()
|
|
496
|
+
if (t.isMemberExpression(callee) && t.isIdentifier(callee.property) && t.isIdentifier(callee.object)) {
|
|
497
|
+
if (callee.object.name === 'axios') {
|
|
498
|
+
const method = callee.property.name.toUpperCase();
|
|
499
|
+
if (['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'HEAD', 'OPTIONS', 'REQUEST'].includes(method)) {
|
|
500
|
+
return { library: 'axios', method: method === 'REQUEST' ? 'UNKNOWN' : method };
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
// Pattern: axios(config)
|
|
506
|
+
if (t.isIdentifier(callee) && callee.name === 'axios') {
|
|
507
|
+
return { library: 'axios', method: 'UNKNOWN' };
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
return null;
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
// ---------------------------------------------------------------------------
|
|
514
|
+
// AST builder helpers
|
|
515
|
+
// ---------------------------------------------------------------------------
|
|
516
|
+
|
|
517
|
+
/**
|
|
518
|
+
* Build the utopia:probe comment node.
|
|
519
|
+
*/
|
|
520
|
+
function makeProbeComment(): t.CommentLine {
|
|
521
|
+
return { type: 'CommentLine', value: ' utopia:probe' } as t.CommentLine;
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
/**
|
|
525
|
+
* Add a leading comment to a node.
|
|
526
|
+
*/
|
|
527
|
+
function addProbeComment(node: t.Node): void {
|
|
528
|
+
if (!node.leadingComments) {
|
|
529
|
+
node.leadingComments = [];
|
|
530
|
+
}
|
|
531
|
+
node.leadingComments.push(makeProbeComment());
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
/**
|
|
535
|
+
* Build the error probe try/catch wrapper.
|
|
536
|
+
*/
|
|
537
|
+
function buildErrorTryCatch(
|
|
538
|
+
originalBody: t.Statement[],
|
|
539
|
+
filePath: string,
|
|
540
|
+
line: number,
|
|
541
|
+
functionName: string,
|
|
542
|
+
codeLine: string
|
|
543
|
+
): t.TryStatement {
|
|
544
|
+
const catchParam = t.identifier('__utopia_err');
|
|
545
|
+
|
|
546
|
+
const reportCall = t.expressionStatement(
|
|
547
|
+
t.callExpression(
|
|
548
|
+
t.memberExpression(t.identifier('__utopia'), t.identifier('reportError')),
|
|
549
|
+
[
|
|
550
|
+
t.objectExpression([
|
|
551
|
+
t.objectProperty(t.identifier('file'), t.stringLiteral(filePath)),
|
|
552
|
+
t.objectProperty(t.identifier('line'), t.numericLiteral(line)),
|
|
553
|
+
t.objectProperty(t.identifier('functionName'), t.stringLiteral(functionName)),
|
|
554
|
+
t.objectProperty(
|
|
555
|
+
t.identifier('errorType'),
|
|
556
|
+
t.logicalExpression(
|
|
557
|
+
'||',
|
|
558
|
+
t.optionalMemberExpression(
|
|
559
|
+
t.optionalMemberExpression(
|
|
560
|
+
t.identifier('__utopia_err'),
|
|
561
|
+
t.identifier('constructor'),
|
|
562
|
+
false,
|
|
563
|
+
true
|
|
564
|
+
),
|
|
565
|
+
t.identifier('name'),
|
|
566
|
+
false,
|
|
567
|
+
true
|
|
568
|
+
),
|
|
569
|
+
t.stringLiteral('Error')
|
|
570
|
+
)
|
|
571
|
+
),
|
|
572
|
+
t.objectProperty(
|
|
573
|
+
t.identifier('message'),
|
|
574
|
+
t.logicalExpression(
|
|
575
|
+
'||',
|
|
576
|
+
t.optionalMemberExpression(
|
|
577
|
+
t.identifier('__utopia_err'),
|
|
578
|
+
t.identifier('message'),
|
|
579
|
+
false,
|
|
580
|
+
true
|
|
581
|
+
),
|
|
582
|
+
t.callExpression(t.identifier('String'), [t.identifier('__utopia_err')])
|
|
583
|
+
)
|
|
584
|
+
),
|
|
585
|
+
t.objectProperty(
|
|
586
|
+
t.identifier('stack'),
|
|
587
|
+
t.logicalExpression(
|
|
588
|
+
'||',
|
|
589
|
+
t.optionalMemberExpression(
|
|
590
|
+
t.identifier('__utopia_err'),
|
|
591
|
+
t.identifier('stack'),
|
|
592
|
+
false,
|
|
593
|
+
true
|
|
594
|
+
),
|
|
595
|
+
t.stringLiteral('')
|
|
596
|
+
)
|
|
597
|
+
),
|
|
598
|
+
t.objectProperty(t.identifier('inputData'), t.objectExpression([])),
|
|
599
|
+
t.objectProperty(t.identifier('codeLine'), t.stringLiteral(codeLine)),
|
|
600
|
+
]),
|
|
601
|
+
]
|
|
602
|
+
)
|
|
603
|
+
);
|
|
604
|
+
|
|
605
|
+
const rethrow = t.throwStatement(t.identifier('__utopia_err'));
|
|
606
|
+
|
|
607
|
+
const catchBlock = t.blockStatement([reportCall, rethrow]);
|
|
608
|
+
const catchClause = t.catchClause(catchParam, catchBlock);
|
|
609
|
+
|
|
610
|
+
const tryBlock = t.blockStatement([...originalBody]);
|
|
611
|
+
const tryStatement = t.tryStatement(tryBlock, catchClause);
|
|
612
|
+
|
|
613
|
+
addProbeComment(tryStatement);
|
|
614
|
+
return tryStatement;
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
/**
|
|
618
|
+
* Build a database probe wrapper IIFE for a call expression.
|
|
619
|
+
*/
|
|
620
|
+
function buildDbProbeIIFE(
|
|
621
|
+
originalCall: t.Expression,
|
|
622
|
+
filePath: string,
|
|
623
|
+
line: number,
|
|
624
|
+
functionName: string,
|
|
625
|
+
operation: string,
|
|
626
|
+
callString: string,
|
|
627
|
+
table: string,
|
|
628
|
+
library: string
|
|
629
|
+
): t.CallExpression {
|
|
630
|
+
const startVar = t.variableDeclaration('const', [
|
|
631
|
+
t.variableDeclarator(
|
|
632
|
+
t.identifier('__utopia_db_start'),
|
|
633
|
+
t.callExpression(t.memberExpression(t.identifier('Date'), t.identifier('now')), [])
|
|
634
|
+
),
|
|
635
|
+
]);
|
|
636
|
+
|
|
637
|
+
const resultVar = t.variableDeclaration('const', [
|
|
638
|
+
t.variableDeclarator(
|
|
639
|
+
t.identifier('__utopia_db_result'),
|
|
640
|
+
t.awaitExpression(originalCall)
|
|
641
|
+
),
|
|
642
|
+
]);
|
|
643
|
+
|
|
644
|
+
const durationExpr = t.binaryExpression(
|
|
645
|
+
'-',
|
|
646
|
+
t.callExpression(t.memberExpression(t.identifier('Date'), t.identifier('now')), []),
|
|
647
|
+
t.identifier('__utopia_db_start')
|
|
648
|
+
);
|
|
649
|
+
|
|
650
|
+
const successReport = t.expressionStatement(
|
|
651
|
+
t.callExpression(
|
|
652
|
+
t.memberExpression(t.identifier('__utopia'), t.identifier('reportDb')),
|
|
653
|
+
[
|
|
654
|
+
t.objectExpression([
|
|
655
|
+
t.objectProperty(t.identifier('file'), t.stringLiteral(filePath)),
|
|
656
|
+
t.objectProperty(t.identifier('line'), t.numericLiteral(line)),
|
|
657
|
+
t.objectProperty(t.identifier('functionName'), t.stringLiteral(functionName)),
|
|
658
|
+
t.objectProperty(t.identifier('operation'), t.stringLiteral(operation)),
|
|
659
|
+
t.objectProperty(t.identifier('query'), t.stringLiteral(callString)),
|
|
660
|
+
t.objectProperty(t.identifier('table'), t.stringLiteral(table)),
|
|
661
|
+
t.objectProperty(t.identifier('duration'), durationExpr),
|
|
662
|
+
t.objectProperty(
|
|
663
|
+
t.identifier('rowCount'),
|
|
664
|
+
t.conditionalExpression(
|
|
665
|
+
t.callExpression(
|
|
666
|
+
t.memberExpression(t.identifier('Array'), t.identifier('isArray')),
|
|
667
|
+
[t.identifier('__utopia_db_result')]
|
|
668
|
+
),
|
|
669
|
+
t.memberExpression(t.identifier('__utopia_db_result'), t.identifier('length')),
|
|
670
|
+
t.identifier('undefined')
|
|
671
|
+
)
|
|
672
|
+
),
|
|
673
|
+
t.objectProperty(
|
|
674
|
+
t.identifier('connectionInfo'),
|
|
675
|
+
t.objectExpression([
|
|
676
|
+
t.objectProperty(t.identifier('type'), t.stringLiteral(library)),
|
|
677
|
+
])
|
|
678
|
+
),
|
|
679
|
+
]),
|
|
680
|
+
]
|
|
681
|
+
)
|
|
682
|
+
);
|
|
683
|
+
|
|
684
|
+
const returnResult = t.returnStatement(t.identifier('__utopia_db_result'));
|
|
685
|
+
|
|
686
|
+
const catchParam = t.identifier('__utopia_db_err');
|
|
687
|
+
|
|
688
|
+
const errorDurationExpr = t.binaryExpression(
|
|
689
|
+
'-',
|
|
690
|
+
t.callExpression(t.memberExpression(t.identifier('Date'), t.identifier('now')), []),
|
|
691
|
+
t.identifier('__utopia_db_start')
|
|
692
|
+
);
|
|
693
|
+
|
|
694
|
+
const errorReport = t.expressionStatement(
|
|
695
|
+
t.callExpression(
|
|
696
|
+
t.memberExpression(t.identifier('__utopia'), t.identifier('reportDb')),
|
|
697
|
+
[
|
|
698
|
+
t.objectExpression([
|
|
699
|
+
t.objectProperty(t.identifier('file'), t.stringLiteral(filePath)),
|
|
700
|
+
t.objectProperty(t.identifier('line'), t.numericLiteral(line)),
|
|
701
|
+
t.objectProperty(t.identifier('functionName'), t.stringLiteral(functionName)),
|
|
702
|
+
t.objectProperty(t.identifier('operation'), t.stringLiteral(operation)),
|
|
703
|
+
t.objectProperty(t.identifier('query'), t.stringLiteral(callString)),
|
|
704
|
+
t.objectProperty(t.identifier('duration'), errorDurationExpr),
|
|
705
|
+
t.objectProperty(
|
|
706
|
+
t.identifier('connectionInfo'),
|
|
707
|
+
t.objectExpression([
|
|
708
|
+
t.objectProperty(t.identifier('type'), t.stringLiteral(library)),
|
|
709
|
+
])
|
|
710
|
+
),
|
|
711
|
+
t.objectProperty(
|
|
712
|
+
t.identifier('error'),
|
|
713
|
+
t.optionalMemberExpression(
|
|
714
|
+
t.identifier('__utopia_db_err'),
|
|
715
|
+
t.identifier('message'),
|
|
716
|
+
false,
|
|
717
|
+
true
|
|
718
|
+
)
|
|
719
|
+
),
|
|
720
|
+
]),
|
|
721
|
+
]
|
|
722
|
+
)
|
|
723
|
+
);
|
|
724
|
+
|
|
725
|
+
const rethrow = t.throwStatement(t.identifier('__utopia_db_err'));
|
|
726
|
+
|
|
727
|
+
const tryStatement = t.tryStatement(
|
|
728
|
+
t.blockStatement([resultVar, successReport, returnResult]),
|
|
729
|
+
t.catchClause(catchParam, t.blockStatement([errorReport, rethrow]))
|
|
730
|
+
);
|
|
731
|
+
|
|
732
|
+
const arrowBody = t.blockStatement([startVar, tryStatement]);
|
|
733
|
+
const arrow = t.arrowFunctionExpression([], arrowBody, true);
|
|
734
|
+
|
|
735
|
+
const iife = t.callExpression(arrow, []);
|
|
736
|
+
const awaitedIife = t.awaitExpression(iife);
|
|
737
|
+
|
|
738
|
+
// We return the call expression (the IIFE call), the await is handled by placement
|
|
739
|
+
return iife;
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
/**
|
|
743
|
+
* Build an API probe wrapper IIFE for a call expression.
|
|
744
|
+
*/
|
|
745
|
+
function buildApiProbeIIFE(
|
|
746
|
+
originalCall: t.Expression,
|
|
747
|
+
filePath: string,
|
|
748
|
+
line: number,
|
|
749
|
+
functionName: string,
|
|
750
|
+
method: string,
|
|
751
|
+
library: string
|
|
752
|
+
): t.CallExpression {
|
|
753
|
+
const startVar = t.variableDeclaration('const', [
|
|
754
|
+
t.variableDeclarator(
|
|
755
|
+
t.identifier('__utopia_api_start'),
|
|
756
|
+
t.callExpression(t.memberExpression(t.identifier('Date'), t.identifier('now')), [])
|
|
757
|
+
),
|
|
758
|
+
]);
|
|
759
|
+
|
|
760
|
+
const resultVar = t.variableDeclaration('const', [
|
|
761
|
+
t.variableDeclarator(
|
|
762
|
+
t.identifier('__utopia_api_result'),
|
|
763
|
+
t.awaitExpression(originalCall)
|
|
764
|
+
),
|
|
765
|
+
]);
|
|
766
|
+
|
|
767
|
+
const durationExpr = t.binaryExpression(
|
|
768
|
+
'-',
|
|
769
|
+
t.callExpression(t.memberExpression(t.identifier('Date'), t.identifier('now')), []),
|
|
770
|
+
t.identifier('__utopia_api_start')
|
|
771
|
+
);
|
|
772
|
+
|
|
773
|
+
// Build URL extraction depending on library
|
|
774
|
+
let urlExpr: t.Expression;
|
|
775
|
+
let methodExpr: t.Expression;
|
|
776
|
+
let statusCodeExpr: t.Expression;
|
|
777
|
+
|
|
778
|
+
if (library === 'fetch') {
|
|
779
|
+
// For fetch: first arg is the URL, response has .status
|
|
780
|
+
urlExpr = t.callExpression(t.identifier('String'), [
|
|
781
|
+
t.logicalExpression(
|
|
782
|
+
'||',
|
|
783
|
+
t.optionalMemberExpression(
|
|
784
|
+
t.identifier('__utopia_api_result'),
|
|
785
|
+
t.identifier('url'),
|
|
786
|
+
false,
|
|
787
|
+
true
|
|
788
|
+
),
|
|
789
|
+
t.stringLiteral('')
|
|
790
|
+
),
|
|
791
|
+
]);
|
|
792
|
+
statusCodeExpr = t.logicalExpression(
|
|
793
|
+
'||',
|
|
794
|
+
t.optionalMemberExpression(
|
|
795
|
+
t.identifier('__utopia_api_result'),
|
|
796
|
+
t.identifier('status'),
|
|
797
|
+
false,
|
|
798
|
+
true
|
|
799
|
+
),
|
|
800
|
+
t.numericLiteral(0)
|
|
801
|
+
);
|
|
802
|
+
methodExpr = t.stringLiteral(method);
|
|
803
|
+
} else {
|
|
804
|
+
// For axios: response has .status, .config.url, .config.method
|
|
805
|
+
urlExpr = t.logicalExpression(
|
|
806
|
+
'||',
|
|
807
|
+
t.optionalMemberExpression(
|
|
808
|
+
t.optionalMemberExpression(
|
|
809
|
+
t.identifier('__utopia_api_result'),
|
|
810
|
+
t.identifier('config'),
|
|
811
|
+
false,
|
|
812
|
+
true
|
|
813
|
+
),
|
|
814
|
+
t.identifier('url'),
|
|
815
|
+
false,
|
|
816
|
+
true
|
|
817
|
+
),
|
|
818
|
+
t.stringLiteral('')
|
|
819
|
+
);
|
|
820
|
+
statusCodeExpr = t.logicalExpression(
|
|
821
|
+
'||',
|
|
822
|
+
t.optionalMemberExpression(
|
|
823
|
+
t.identifier('__utopia_api_result'),
|
|
824
|
+
t.identifier('status'),
|
|
825
|
+
false,
|
|
826
|
+
true
|
|
827
|
+
),
|
|
828
|
+
t.numericLiteral(0)
|
|
829
|
+
);
|
|
830
|
+
methodExpr = t.logicalExpression(
|
|
831
|
+
'||',
|
|
832
|
+
t.optionalMemberExpression(
|
|
833
|
+
t.optionalMemberExpression(
|
|
834
|
+
t.identifier('__utopia_api_result'),
|
|
835
|
+
t.identifier('config'),
|
|
836
|
+
false,
|
|
837
|
+
true
|
|
838
|
+
),
|
|
839
|
+
t.identifier('method'),
|
|
840
|
+
false,
|
|
841
|
+
true
|
|
842
|
+
),
|
|
843
|
+
t.stringLiteral(method)
|
|
844
|
+
);
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
const successReport = t.expressionStatement(
|
|
848
|
+
t.callExpression(
|
|
849
|
+
t.memberExpression(t.identifier('__utopia'), t.identifier('reportApi')),
|
|
850
|
+
[
|
|
851
|
+
t.objectExpression([
|
|
852
|
+
t.objectProperty(t.identifier('file'), t.stringLiteral(filePath)),
|
|
853
|
+
t.objectProperty(t.identifier('line'), t.numericLiteral(line)),
|
|
854
|
+
t.objectProperty(t.identifier('functionName'), t.stringLiteral(functionName)),
|
|
855
|
+
t.objectProperty(t.identifier('method'), methodExpr),
|
|
856
|
+
t.objectProperty(t.identifier('url'), urlExpr),
|
|
857
|
+
t.objectProperty(t.identifier('statusCode'), statusCodeExpr),
|
|
858
|
+
t.objectProperty(t.identifier('duration'), durationExpr),
|
|
859
|
+
]),
|
|
860
|
+
]
|
|
861
|
+
)
|
|
862
|
+
);
|
|
863
|
+
|
|
864
|
+
const returnResult = t.returnStatement(t.identifier('__utopia_api_result'));
|
|
865
|
+
|
|
866
|
+
const catchParam = t.identifier('__utopia_api_err');
|
|
867
|
+
|
|
868
|
+
const errorDurationExpr = t.binaryExpression(
|
|
869
|
+
'-',
|
|
870
|
+
t.callExpression(t.memberExpression(t.identifier('Date'), t.identifier('now')), []),
|
|
871
|
+
t.identifier('__utopia_api_start')
|
|
872
|
+
);
|
|
873
|
+
|
|
874
|
+
const errorReport = t.expressionStatement(
|
|
875
|
+
t.callExpression(
|
|
876
|
+
t.memberExpression(t.identifier('__utopia'), t.identifier('reportApi')),
|
|
877
|
+
[
|
|
878
|
+
t.objectExpression([
|
|
879
|
+
t.objectProperty(t.identifier('file'), t.stringLiteral(filePath)),
|
|
880
|
+
t.objectProperty(t.identifier('line'), t.numericLiteral(line)),
|
|
881
|
+
t.objectProperty(t.identifier('functionName'), t.stringLiteral(functionName)),
|
|
882
|
+
t.objectProperty(t.identifier('method'), t.stringLiteral(method)),
|
|
883
|
+
t.objectProperty(t.identifier('url'), t.stringLiteral('')),
|
|
884
|
+
t.objectProperty(t.identifier('statusCode'), t.numericLiteral(0)),
|
|
885
|
+
t.objectProperty(t.identifier('duration'), errorDurationExpr),
|
|
886
|
+
t.objectProperty(
|
|
887
|
+
t.identifier('error'),
|
|
888
|
+
t.optionalMemberExpression(
|
|
889
|
+
t.identifier('__utopia_api_err'),
|
|
890
|
+
t.identifier('message'),
|
|
891
|
+
false,
|
|
892
|
+
true
|
|
893
|
+
)
|
|
894
|
+
),
|
|
895
|
+
]),
|
|
896
|
+
]
|
|
897
|
+
)
|
|
898
|
+
);
|
|
899
|
+
|
|
900
|
+
const rethrow = t.throwStatement(t.identifier('__utopia_api_err'));
|
|
901
|
+
|
|
902
|
+
const tryStatement = t.tryStatement(
|
|
903
|
+
t.blockStatement([resultVar, successReport, returnResult]),
|
|
904
|
+
t.catchClause(catchParam, t.blockStatement([errorReport, rethrow]))
|
|
905
|
+
);
|
|
906
|
+
|
|
907
|
+
const arrowBody = t.blockStatement([startVar, tryStatement]);
|
|
908
|
+
const arrow = t.arrowFunctionExpression([], arrowBody, true);
|
|
909
|
+
|
|
910
|
+
return t.callExpression(arrow, []);
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
/**
|
|
914
|
+
* Build the infra probe report statement for entry point files.
|
|
915
|
+
*/
|
|
916
|
+
function buildInfraProbeStatement(filePath: string): t.ExpressionStatement {
|
|
917
|
+
// Build: process.env.AWS_REGION ? 'aws' : process.env.GOOGLE_CLOUD_PROJECT ? 'gcp' : process.env.VERCEL ? 'vercel' : 'other'
|
|
918
|
+
const envAccess = (key: string) =>
|
|
919
|
+
t.memberExpression(
|
|
920
|
+
t.memberExpression(t.identifier('process'), t.identifier('env')),
|
|
921
|
+
t.identifier(key)
|
|
922
|
+
);
|
|
923
|
+
|
|
924
|
+
const providerExpr = t.conditionalExpression(
|
|
925
|
+
envAccess('AWS_REGION'),
|
|
926
|
+
t.stringLiteral('aws'),
|
|
927
|
+
t.conditionalExpression(
|
|
928
|
+
envAccess('GOOGLE_CLOUD_PROJECT'),
|
|
929
|
+
t.stringLiteral('gcp'),
|
|
930
|
+
t.conditionalExpression(
|
|
931
|
+
envAccess('VERCEL'),
|
|
932
|
+
t.stringLiteral('vercel'),
|
|
933
|
+
t.stringLiteral('other')
|
|
934
|
+
)
|
|
935
|
+
)
|
|
936
|
+
);
|
|
937
|
+
|
|
938
|
+
const regionExpr = t.logicalExpression(
|
|
939
|
+
'||',
|
|
940
|
+
t.logicalExpression(
|
|
941
|
+
'||',
|
|
942
|
+
t.logicalExpression(
|
|
943
|
+
'||',
|
|
944
|
+
envAccess('AWS_REGION'),
|
|
945
|
+
envAccess('GOOGLE_CLOUD_REGION')
|
|
946
|
+
),
|
|
947
|
+
envAccess('VERCEL_REGION')
|
|
948
|
+
),
|
|
949
|
+
t.identifier('undefined')
|
|
950
|
+
);
|
|
951
|
+
|
|
952
|
+
const serviceTypeExpr = t.conditionalExpression(
|
|
953
|
+
envAccess('AWS_LAMBDA_FUNCTION_NAME'),
|
|
954
|
+
t.stringLiteral('lambda'),
|
|
955
|
+
t.conditionalExpression(
|
|
956
|
+
envAccess('K_SERVICE'),
|
|
957
|
+
t.stringLiteral('cloud-run'),
|
|
958
|
+
t.conditionalExpression(
|
|
959
|
+
envAccess('VERCEL'),
|
|
960
|
+
t.stringLiteral('vercel'),
|
|
961
|
+
t.identifier('undefined')
|
|
962
|
+
)
|
|
963
|
+
)
|
|
964
|
+
);
|
|
965
|
+
|
|
966
|
+
const instanceIdExpr = t.logicalExpression(
|
|
967
|
+
'||',
|
|
968
|
+
envAccess('HOSTNAME'),
|
|
969
|
+
t.identifier('undefined')
|
|
970
|
+
);
|
|
971
|
+
|
|
972
|
+
// Filter env vars to exclude secrets:
|
|
973
|
+
// Object.fromEntries(Object.entries(process.env).filter(([k]) => !k.includes('KEY') && ...))
|
|
974
|
+
const filterExpr = t.callExpression(
|
|
975
|
+
t.memberExpression(t.identifier('Object'), t.identifier('fromEntries')),
|
|
976
|
+
[
|
|
977
|
+
t.callExpression(
|
|
978
|
+
t.memberExpression(
|
|
979
|
+
t.callExpression(
|
|
980
|
+
t.memberExpression(t.identifier('Object'), t.identifier('entries')),
|
|
981
|
+
[t.memberExpression(t.identifier('process'), t.identifier('env'))]
|
|
982
|
+
),
|
|
983
|
+
t.identifier('filter')
|
|
984
|
+
),
|
|
985
|
+
[
|
|
986
|
+
t.arrowFunctionExpression(
|
|
987
|
+
[t.arrayPattern([t.identifier('k')])],
|
|
988
|
+
t.logicalExpression(
|
|
989
|
+
'&&',
|
|
990
|
+
t.logicalExpression(
|
|
991
|
+
'&&',
|
|
992
|
+
t.logicalExpression(
|
|
993
|
+
'&&',
|
|
994
|
+
t.unaryExpression(
|
|
995
|
+
'!',
|
|
996
|
+
t.callExpression(
|
|
997
|
+
t.memberExpression(t.identifier('k'), t.identifier('includes')),
|
|
998
|
+
[t.stringLiteral('KEY')]
|
|
999
|
+
)
|
|
1000
|
+
),
|
|
1001
|
+
t.unaryExpression(
|
|
1002
|
+
'!',
|
|
1003
|
+
t.callExpression(
|
|
1004
|
+
t.memberExpression(t.identifier('k'), t.identifier('includes')),
|
|
1005
|
+
[t.stringLiteral('SECRET')]
|
|
1006
|
+
)
|
|
1007
|
+
)
|
|
1008
|
+
),
|
|
1009
|
+
t.unaryExpression(
|
|
1010
|
+
'!',
|
|
1011
|
+
t.callExpression(
|
|
1012
|
+
t.memberExpression(t.identifier('k'), t.identifier('includes')),
|
|
1013
|
+
[t.stringLiteral('TOKEN')]
|
|
1014
|
+
)
|
|
1015
|
+
)
|
|
1016
|
+
),
|
|
1017
|
+
t.unaryExpression(
|
|
1018
|
+
'!',
|
|
1019
|
+
t.callExpression(
|
|
1020
|
+
t.memberExpression(t.identifier('k'), t.identifier('includes')),
|
|
1021
|
+
[t.stringLiteral('PASSWORD')]
|
|
1022
|
+
)
|
|
1023
|
+
)
|
|
1024
|
+
)
|
|
1025
|
+
),
|
|
1026
|
+
]
|
|
1027
|
+
),
|
|
1028
|
+
]
|
|
1029
|
+
);
|
|
1030
|
+
|
|
1031
|
+
// process.memoryUsage?.()?.heapUsed || 0
|
|
1032
|
+
const memoryExpr = t.logicalExpression(
|
|
1033
|
+
'||',
|
|
1034
|
+
t.optionalMemberExpression(
|
|
1035
|
+
t.optionalCallExpression(
|
|
1036
|
+
t.optionalMemberExpression(
|
|
1037
|
+
t.identifier('process'),
|
|
1038
|
+
t.identifier('memoryUsage'),
|
|
1039
|
+
false,
|
|
1040
|
+
true
|
|
1041
|
+
),
|
|
1042
|
+
[],
|
|
1043
|
+
true
|
|
1044
|
+
),
|
|
1045
|
+
t.identifier('heapUsed'),
|
|
1046
|
+
false,
|
|
1047
|
+
true
|
|
1048
|
+
),
|
|
1049
|
+
t.numericLiteral(0)
|
|
1050
|
+
);
|
|
1051
|
+
|
|
1052
|
+
const reportCall = t.expressionStatement(
|
|
1053
|
+
t.callExpression(
|
|
1054
|
+
t.memberExpression(t.identifier('__utopia'), t.identifier('reportInfra')),
|
|
1055
|
+
[
|
|
1056
|
+
t.objectExpression([
|
|
1057
|
+
t.objectProperty(t.identifier('file'), t.stringLiteral(filePath)),
|
|
1058
|
+
t.objectProperty(t.identifier('line'), t.numericLiteral(1)),
|
|
1059
|
+
t.objectProperty(t.identifier('provider'), providerExpr),
|
|
1060
|
+
t.objectProperty(t.identifier('region'), regionExpr),
|
|
1061
|
+
t.objectProperty(t.identifier('serviceType'), serviceTypeExpr),
|
|
1062
|
+
t.objectProperty(t.identifier('instanceId'), instanceIdExpr),
|
|
1063
|
+
t.objectProperty(t.identifier('envVars'), filterExpr),
|
|
1064
|
+
t.objectProperty(t.identifier('memoryUsage'), memoryExpr),
|
|
1065
|
+
]),
|
|
1066
|
+
]
|
|
1067
|
+
)
|
|
1068
|
+
);
|
|
1069
|
+
|
|
1070
|
+
addProbeComment(reportCall);
|
|
1071
|
+
return reportCall;
|
|
1072
|
+
}
|
|
1073
|
+
|
|
1074
|
+
/**
|
|
1075
|
+
* Build a function probe wrapper for Utopia mode.
|
|
1076
|
+
* Wraps function body with timing + arg capture + reportFunction call.
|
|
1077
|
+
* For Utopia mode, also calls reportLlmContext with function source context.
|
|
1078
|
+
*/
|
|
1079
|
+
function buildFunctionProbeWrapper(
|
|
1080
|
+
originalBody: t.Statement[],
|
|
1081
|
+
filePath: string,
|
|
1082
|
+
line: number,
|
|
1083
|
+
functionName: string,
|
|
1084
|
+
paramNames: string[],
|
|
1085
|
+
utopiaMode: boolean
|
|
1086
|
+
): t.Statement[] {
|
|
1087
|
+
// const __utopia_fn_start = Date.now();
|
|
1088
|
+
const startDecl = t.variableDeclaration('const', [
|
|
1089
|
+
t.variableDeclarator(
|
|
1090
|
+
t.identifier('__utopia_fn_start'),
|
|
1091
|
+
t.callExpression(
|
|
1092
|
+
t.memberExpression(t.identifier('Date'), t.identifier('now')),
|
|
1093
|
+
[]
|
|
1094
|
+
)
|
|
1095
|
+
),
|
|
1096
|
+
]);
|
|
1097
|
+
addProbeComment(startDecl);
|
|
1098
|
+
|
|
1099
|
+
// Build args capture: { param1: param1, param2: param2 }
|
|
1100
|
+
const argsCapture = t.arrayExpression(
|
|
1101
|
+
paramNames.map(name => {
|
|
1102
|
+
try {
|
|
1103
|
+
return t.identifier(name);
|
|
1104
|
+
} catch {
|
|
1105
|
+
return t.stringLiteral(`<${name}>`);
|
|
1106
|
+
}
|
|
1107
|
+
})
|
|
1108
|
+
);
|
|
1109
|
+
|
|
1110
|
+
// __utopia.reportFunction({ file, line, functionName, args, duration, callStack })
|
|
1111
|
+
const reportFnCall = t.expressionStatement(
|
|
1112
|
+
t.callExpression(
|
|
1113
|
+
t.memberExpression(t.identifier('__utopia'), t.identifier('reportFunction')),
|
|
1114
|
+
[
|
|
1115
|
+
t.objectExpression([
|
|
1116
|
+
t.objectProperty(t.identifier('file'), t.stringLiteral(filePath)),
|
|
1117
|
+
t.objectProperty(t.identifier('line'), t.numericLiteral(line)),
|
|
1118
|
+
t.objectProperty(t.identifier('functionName'), t.stringLiteral(functionName)),
|
|
1119
|
+
t.objectProperty(t.identifier('args'), argsCapture),
|
|
1120
|
+
t.objectProperty(
|
|
1121
|
+
t.identifier('returnValue'),
|
|
1122
|
+
t.identifier('__utopia_fn_result')
|
|
1123
|
+
),
|
|
1124
|
+
t.objectProperty(
|
|
1125
|
+
t.identifier('duration'),
|
|
1126
|
+
t.binaryExpression(
|
|
1127
|
+
'-',
|
|
1128
|
+
t.callExpression(
|
|
1129
|
+
t.memberExpression(t.identifier('Date'), t.identifier('now')),
|
|
1130
|
+
[]
|
|
1131
|
+
),
|
|
1132
|
+
t.identifier('__utopia_fn_start')
|
|
1133
|
+
)
|
|
1134
|
+
),
|
|
1135
|
+
t.objectProperty(
|
|
1136
|
+
t.identifier('callStack'),
|
|
1137
|
+
t.callExpression(
|
|
1138
|
+
t.memberExpression(
|
|
1139
|
+
t.logicalExpression(
|
|
1140
|
+
'||',
|
|
1141
|
+
t.optionalMemberExpression(
|
|
1142
|
+
t.newExpression(t.identifier('Error'), []),
|
|
1143
|
+
t.identifier('stack'),
|
|
1144
|
+
false,
|
|
1145
|
+
true
|
|
1146
|
+
),
|
|
1147
|
+
t.stringLiteral('')
|
|
1148
|
+
),
|
|
1149
|
+
t.identifier('split')
|
|
1150
|
+
),
|
|
1151
|
+
[t.stringLiteral('\n')]
|
|
1152
|
+
)
|
|
1153
|
+
),
|
|
1154
|
+
]),
|
|
1155
|
+
]
|
|
1156
|
+
)
|
|
1157
|
+
);
|
|
1158
|
+
|
|
1159
|
+
// Build the utopia mode LLM context call if enabled
|
|
1160
|
+
const llmReportStmts: t.Statement[] = [];
|
|
1161
|
+
if (utopiaMode) {
|
|
1162
|
+
// __utopia.reportLlmContext({ file, line, functionName, context: JSON.stringify({ functionName, args, returnValue, duration }) })
|
|
1163
|
+
const llmCall = t.expressionStatement(
|
|
1164
|
+
t.callExpression(
|
|
1165
|
+
t.memberExpression(t.identifier('__utopia'), t.identifier('reportLlmContext')),
|
|
1166
|
+
[
|
|
1167
|
+
t.objectExpression([
|
|
1168
|
+
t.objectProperty(t.identifier('file'), t.stringLiteral(filePath)),
|
|
1169
|
+
t.objectProperty(t.identifier('line'), t.numericLiteral(line)),
|
|
1170
|
+
t.objectProperty(t.identifier('functionName'), t.stringLiteral(functionName)),
|
|
1171
|
+
t.objectProperty(
|
|
1172
|
+
t.identifier('context'),
|
|
1173
|
+
t.callExpression(
|
|
1174
|
+
t.memberExpression(t.identifier('JSON'), t.identifier('stringify')),
|
|
1175
|
+
[
|
|
1176
|
+
t.objectExpression([
|
|
1177
|
+
t.objectProperty(t.identifier('function'), t.stringLiteral(functionName)),
|
|
1178
|
+
t.objectProperty(t.identifier('file'), t.stringLiteral(filePath)),
|
|
1179
|
+
t.objectProperty(t.identifier('args'), argsCapture),
|
|
1180
|
+
t.objectProperty(t.identifier('returnValue'), t.identifier('__utopia_fn_result')),
|
|
1181
|
+
t.objectProperty(
|
|
1182
|
+
t.identifier('duration'),
|
|
1183
|
+
t.binaryExpression(
|
|
1184
|
+
'-',
|
|
1185
|
+
t.callExpression(
|
|
1186
|
+
t.memberExpression(t.identifier('Date'), t.identifier('now')),
|
|
1187
|
+
[]
|
|
1188
|
+
),
|
|
1189
|
+
t.identifier('__utopia_fn_start')
|
|
1190
|
+
)
|
|
1191
|
+
),
|
|
1192
|
+
]),
|
|
1193
|
+
]
|
|
1194
|
+
)
|
|
1195
|
+
),
|
|
1196
|
+
]),
|
|
1197
|
+
]
|
|
1198
|
+
)
|
|
1199
|
+
);
|
|
1200
|
+
llmReportStmts.push(llmCall);
|
|
1201
|
+
}
|
|
1202
|
+
|
|
1203
|
+
// let __utopia_fn_result;
|
|
1204
|
+
const resultDecl = t.variableDeclaration('let', [
|
|
1205
|
+
t.variableDeclarator(t.identifier('__utopia_fn_result'), t.identifier('undefined')),
|
|
1206
|
+
]);
|
|
1207
|
+
|
|
1208
|
+
// try { <original body with result capture> } finally { report }
|
|
1209
|
+
// We need to capture the return value. Wrap in try/finally:
|
|
1210
|
+
// try { __utopia_fn_result = (() => { <original body> })(); } finally { reportFunction(); }
|
|
1211
|
+
// Simpler approach: just wrap and report in finally block
|
|
1212
|
+
const finallyBlock = t.blockStatement([reportFnCall, ...llmReportStmts]);
|
|
1213
|
+
|
|
1214
|
+
const tryStatement = t.tryStatement(
|
|
1215
|
+
t.blockStatement([...originalBody]),
|
|
1216
|
+
null,
|
|
1217
|
+
finallyBlock
|
|
1218
|
+
);
|
|
1219
|
+
|
|
1220
|
+
return [startDecl, resultDecl, tryStatement];
|
|
1221
|
+
}
|
|
1222
|
+
|
|
1223
|
+
/**
|
|
1224
|
+
* Build the utopia runtime import declaration.
|
|
1225
|
+
*/
|
|
1226
|
+
function buildUtopiaImport(): t.ImportDeclaration {
|
|
1227
|
+
return t.importDeclaration(
|
|
1228
|
+
[t.importSpecifier(t.identifier('__utopia'), t.identifier('__utopia'))],
|
|
1229
|
+
t.stringLiteral('utopia-runtime')
|
|
1230
|
+
);
|
|
1231
|
+
}
|
|
1232
|
+
|
|
1233
|
+
/**
|
|
1234
|
+
* Check if the AST already has an import from 'utopia-runtime'.
|
|
1235
|
+
*/
|
|
1236
|
+
function hasUtopiaImport(ast: t.File): boolean {
|
|
1237
|
+
for (const node of ast.program.body) {
|
|
1238
|
+
if (
|
|
1239
|
+
t.isImportDeclaration(node) &&
|
|
1240
|
+
node.source.value === 'utopia-runtime'
|
|
1241
|
+
) {
|
|
1242
|
+
return true;
|
|
1243
|
+
}
|
|
1244
|
+
}
|
|
1245
|
+
return false;
|
|
1246
|
+
}
|
|
1247
|
+
|
|
1248
|
+
// ---------------------------------------------------------------------------
|
|
1249
|
+
// Core instrumenter
|
|
1250
|
+
// ---------------------------------------------------------------------------
|
|
1251
|
+
|
|
1252
|
+
/**
|
|
1253
|
+
* Instrument a single JavaScript/TypeScript file.
|
|
1254
|
+
*/
|
|
1255
|
+
export async function instrumentFile(
|
|
1256
|
+
filePath: string,
|
|
1257
|
+
options: InstrumentOptions
|
|
1258
|
+
): Promise<InstrumentResult> {
|
|
1259
|
+
const absolutePath = resolve(filePath);
|
|
1260
|
+
const probesAdded: { type: string; line: number; functionName: string }[] = [];
|
|
1261
|
+
|
|
1262
|
+
try {
|
|
1263
|
+
const source = await readFile(absolutePath, 'utf-8');
|
|
1264
|
+
const relPath = relative(process.cwd(), absolutePath);
|
|
1265
|
+
const plugins = getParserPlugins(absolutePath);
|
|
1266
|
+
|
|
1267
|
+
let ast: t.File;
|
|
1268
|
+
try {
|
|
1269
|
+
ast = parser.parse(source, {
|
|
1270
|
+
sourceType: getSourceType(absolutePath),
|
|
1271
|
+
plugins,
|
|
1272
|
+
errorRecovery: true,
|
|
1273
|
+
});
|
|
1274
|
+
} catch (parseError: any) {
|
|
1275
|
+
return {
|
|
1276
|
+
file: relPath,
|
|
1277
|
+
probesAdded: [],
|
|
1278
|
+
success: false,
|
|
1279
|
+
error: `Parse error: ${parseError.message}`,
|
|
1280
|
+
};
|
|
1281
|
+
}
|
|
1282
|
+
|
|
1283
|
+
const shouldInstrument = {
|
|
1284
|
+
error: options.probeTypes.includes('error'),
|
|
1285
|
+
database: options.probeTypes.includes('database'),
|
|
1286
|
+
api: options.probeTypes.includes('api'),
|
|
1287
|
+
infra: options.probeTypes.includes('infra'),
|
|
1288
|
+
function: options.probeTypes.includes('function'),
|
|
1289
|
+
};
|
|
1290
|
+
|
|
1291
|
+
const isEntry = isEntryPoint(absolutePath, options.entryPoints);
|
|
1292
|
+
|
|
1293
|
+
// -----------------------------------------------------------------------
|
|
1294
|
+
// Traverse the AST and inject probes
|
|
1295
|
+
// -----------------------------------------------------------------------
|
|
1296
|
+
|
|
1297
|
+
traverse(ast, {
|
|
1298
|
+
// ---- Error probes: wrap function bodies in try/catch ----
|
|
1299
|
+
'FunctionDeclaration|ArrowFunctionExpression|FunctionExpression|ClassMethod'(
|
|
1300
|
+
path: any
|
|
1301
|
+
) {
|
|
1302
|
+
if (!shouldInstrument.error) return;
|
|
1303
|
+
|
|
1304
|
+
const node = path.node;
|
|
1305
|
+
let body: t.BlockStatement | null = null;
|
|
1306
|
+
|
|
1307
|
+
if (t.isArrowFunctionExpression(node)) {
|
|
1308
|
+
if (t.isBlockStatement(node.body)) {
|
|
1309
|
+
body = node.body;
|
|
1310
|
+
} else {
|
|
1311
|
+
// Expression body arrow: () => expr
|
|
1312
|
+
// Convert to block: () => { return expr; }
|
|
1313
|
+
const returnStmt = t.returnStatement(node.body as t.Expression);
|
|
1314
|
+
body = t.blockStatement([returnStmt]);
|
|
1315
|
+
node.body = body;
|
|
1316
|
+
}
|
|
1317
|
+
} else if (
|
|
1318
|
+
t.isFunctionDeclaration(node) ||
|
|
1319
|
+
t.isFunctionExpression(node) ||
|
|
1320
|
+
t.isClassMethod(node)
|
|
1321
|
+
) {
|
|
1322
|
+
body = node.body;
|
|
1323
|
+
}
|
|
1324
|
+
|
|
1325
|
+
if (!body || body.body.length === 0) return;
|
|
1326
|
+
if (isAlreadyWrapped(body)) return;
|
|
1327
|
+
|
|
1328
|
+
const fnName = getFunctionName(path);
|
|
1329
|
+
const line = node.loc?.start?.line ?? 0;
|
|
1330
|
+
const codeLine = getSourceLine(source, line);
|
|
1331
|
+
|
|
1332
|
+
const tryCatch = buildErrorTryCatch(
|
|
1333
|
+
[...body.body],
|
|
1334
|
+
relPath,
|
|
1335
|
+
line,
|
|
1336
|
+
fnName,
|
|
1337
|
+
codeLine
|
|
1338
|
+
);
|
|
1339
|
+
|
|
1340
|
+
body.body = [tryCatch];
|
|
1341
|
+
|
|
1342
|
+
probesAdded.push({ type: 'error', line, functionName: fnName });
|
|
1343
|
+
},
|
|
1344
|
+
|
|
1345
|
+
// ---- Database and API probes: wrap call expressions ----
|
|
1346
|
+
CallExpression(path: any) {
|
|
1347
|
+
if (!shouldInstrument.database && !shouldInstrument.api) return;
|
|
1348
|
+
|
|
1349
|
+
const node = path.node as t.CallExpression;
|
|
1350
|
+
const line = node.loc?.start?.line ?? 0;
|
|
1351
|
+
|
|
1352
|
+
// Skip if already inside a utopia IIFE
|
|
1353
|
+
if (isInsideUtopiaIIFE(path)) return;
|
|
1354
|
+
|
|
1355
|
+
// ---- Database probes ----
|
|
1356
|
+
if (shouldInstrument.database) {
|
|
1357
|
+
const dbInfo = detectDbCall(node);
|
|
1358
|
+
if (dbInfo) {
|
|
1359
|
+
const fnName = getEnclosingFnName(path);
|
|
1360
|
+
const callStr = callExpressionToString(node);
|
|
1361
|
+
|
|
1362
|
+
const iife = buildDbProbeIIFE(
|
|
1363
|
+
t.cloneNode(node, true),
|
|
1364
|
+
relPath,
|
|
1365
|
+
line,
|
|
1366
|
+
fnName,
|
|
1367
|
+
dbInfo.operation,
|
|
1368
|
+
callStr,
|
|
1369
|
+
dbInfo.table,
|
|
1370
|
+
dbInfo.library
|
|
1371
|
+
);
|
|
1372
|
+
|
|
1373
|
+
// Replace the call expression with an awaited IIFE
|
|
1374
|
+
path.replaceWith(t.awaitExpression(iife));
|
|
1375
|
+
path.skip(); // Don't re-traverse the replacement
|
|
1376
|
+
|
|
1377
|
+
probesAdded.push({
|
|
1378
|
+
type: 'database',
|
|
1379
|
+
line,
|
|
1380
|
+
functionName: fnName,
|
|
1381
|
+
});
|
|
1382
|
+
return;
|
|
1383
|
+
}
|
|
1384
|
+
}
|
|
1385
|
+
|
|
1386
|
+
// ---- API probes ----
|
|
1387
|
+
if (shouldInstrument.api) {
|
|
1388
|
+
const apiInfo = detectApiCall(node);
|
|
1389
|
+
if (apiInfo) {
|
|
1390
|
+
const fnName = getEnclosingFnName(path);
|
|
1391
|
+
|
|
1392
|
+
const iife = buildApiProbeIIFE(
|
|
1393
|
+
t.cloneNode(node, true),
|
|
1394
|
+
relPath,
|
|
1395
|
+
line,
|
|
1396
|
+
fnName,
|
|
1397
|
+
apiInfo.method,
|
|
1398
|
+
apiInfo.library
|
|
1399
|
+
);
|
|
1400
|
+
|
|
1401
|
+
path.replaceWith(t.awaitExpression(iife));
|
|
1402
|
+
path.skip();
|
|
1403
|
+
|
|
1404
|
+
probesAdded.push({
|
|
1405
|
+
type: 'api',
|
|
1406
|
+
line,
|
|
1407
|
+
functionName: fnName,
|
|
1408
|
+
});
|
|
1409
|
+
return;
|
|
1410
|
+
}
|
|
1411
|
+
}
|
|
1412
|
+
},
|
|
1413
|
+
});
|
|
1414
|
+
|
|
1415
|
+
// ---- Function probes (Utopia mode): wrap functions with timing + reporting ----
|
|
1416
|
+
if (shouldInstrument.function) {
|
|
1417
|
+
traverse(ast, {
|
|
1418
|
+
'FunctionDeclaration|FunctionExpression|ArrowFunctionExpression|ClassMethod'(
|
|
1419
|
+
path: any
|
|
1420
|
+
) {
|
|
1421
|
+
const node = path.node;
|
|
1422
|
+
let body: t.BlockStatement | null = null;
|
|
1423
|
+
|
|
1424
|
+
if (t.isArrowFunctionExpression(node)) {
|
|
1425
|
+
if (t.isBlockStatement(node.body)) {
|
|
1426
|
+
body = node.body;
|
|
1427
|
+
} else {
|
|
1428
|
+
const returnStmt = t.returnStatement(node.body as t.Expression);
|
|
1429
|
+
body = t.blockStatement([returnStmt]);
|
|
1430
|
+
node.body = body;
|
|
1431
|
+
}
|
|
1432
|
+
} else if (
|
|
1433
|
+
t.isFunctionDeclaration(node) ||
|
|
1434
|
+
t.isFunctionExpression(node) ||
|
|
1435
|
+
t.isClassMethod(node)
|
|
1436
|
+
) {
|
|
1437
|
+
body = node.body;
|
|
1438
|
+
}
|
|
1439
|
+
|
|
1440
|
+
if (!body || body.body.length === 0) return;
|
|
1441
|
+
|
|
1442
|
+
// Skip if already has function probe (check for __utopia_fn_start)
|
|
1443
|
+
const hasProbe = body.body.some(
|
|
1444
|
+
(s: t.Statement) =>
|
|
1445
|
+
t.isVariableDeclaration(s) &&
|
|
1446
|
+
s.declarations.some(
|
|
1447
|
+
(d: t.VariableDeclarator) =>
|
|
1448
|
+
t.isIdentifier(d.id) && d.id.name === '__utopia_fn_start'
|
|
1449
|
+
)
|
|
1450
|
+
);
|
|
1451
|
+
if (hasProbe) return;
|
|
1452
|
+
|
|
1453
|
+
// Only instrument "interesting" functions for now:
|
|
1454
|
+
// - DB call handlers, API handlers, exported functions, named functions > 3 statements
|
|
1455
|
+
const fnName = getFunctionName(path);
|
|
1456
|
+
if (!fnName || fnName === '<anonymous>') return;
|
|
1457
|
+
if (body.body.length < 2) return; // Skip trivial functions
|
|
1458
|
+
|
|
1459
|
+
const line = node.loc?.start?.line ?? 0;
|
|
1460
|
+
|
|
1461
|
+
// Extract parameter names
|
|
1462
|
+
const params: string[] = (node.params || [])
|
|
1463
|
+
.map((p: t.Node) => {
|
|
1464
|
+
if (t.isIdentifier(p)) return p.name;
|
|
1465
|
+
if (t.isAssignmentPattern(p) && t.isIdentifier(p.left)) return p.left.name;
|
|
1466
|
+
if (t.isRestElement(p) && t.isIdentifier(p.argument)) return p.argument.name;
|
|
1467
|
+
return null;
|
|
1468
|
+
})
|
|
1469
|
+
.filter(Boolean) as string[];
|
|
1470
|
+
|
|
1471
|
+
const wrappedBody = buildFunctionProbeWrapper(
|
|
1472
|
+
[...body.body],
|
|
1473
|
+
relPath,
|
|
1474
|
+
line,
|
|
1475
|
+
fnName,
|
|
1476
|
+
params,
|
|
1477
|
+
options.utopiaMode
|
|
1478
|
+
);
|
|
1479
|
+
|
|
1480
|
+
body.body = wrappedBody;
|
|
1481
|
+
probesAdded.push({ type: 'function', line, functionName: fnName });
|
|
1482
|
+
},
|
|
1483
|
+
});
|
|
1484
|
+
}
|
|
1485
|
+
|
|
1486
|
+
// ---- Infra probe: add at module level for entry points ----
|
|
1487
|
+
if (shouldInstrument.infra && isEntry) {
|
|
1488
|
+
const infraStmt = buildInfraProbeStatement(relPath);
|
|
1489
|
+
|
|
1490
|
+
// Insert after all imports
|
|
1491
|
+
let insertIndex = 0;
|
|
1492
|
+
for (let i = 0; i < ast.program.body.length; i++) {
|
|
1493
|
+
const stmt = ast.program.body[i];
|
|
1494
|
+
if (t.isImportDeclaration(stmt)) {
|
|
1495
|
+
insertIndex = i + 1;
|
|
1496
|
+
}
|
|
1497
|
+
}
|
|
1498
|
+
|
|
1499
|
+
ast.program.body.splice(insertIndex, 0, infraStmt);
|
|
1500
|
+
probesAdded.push({ type: 'infra', line: 1, functionName: '<module>' });
|
|
1501
|
+
}
|
|
1502
|
+
|
|
1503
|
+
// ---- Add utopia runtime import if probes were added ----
|
|
1504
|
+
if (probesAdded.length > 0 && !hasUtopiaImport(ast)) {
|
|
1505
|
+
const importDecl = buildUtopiaImport();
|
|
1506
|
+
// Insert at very top (before other imports)
|
|
1507
|
+
ast.program.body.unshift(importDecl);
|
|
1508
|
+
}
|
|
1509
|
+
|
|
1510
|
+
// ---- Generate output ----
|
|
1511
|
+
const output = generate(ast, {
|
|
1512
|
+
retainLines: true,
|
|
1513
|
+
comments: true,
|
|
1514
|
+
});
|
|
1515
|
+
|
|
1516
|
+
if (!options.dryRun) {
|
|
1517
|
+
await writeFile(absolutePath, output.code, 'utf-8');
|
|
1518
|
+
}
|
|
1519
|
+
|
|
1520
|
+
return {
|
|
1521
|
+
file: relPath,
|
|
1522
|
+
probesAdded,
|
|
1523
|
+
success: true,
|
|
1524
|
+
};
|
|
1525
|
+
} catch (error: any) {
|
|
1526
|
+
return {
|
|
1527
|
+
file: relative(process.cwd(), absolutePath),
|
|
1528
|
+
probesAdded: [],
|
|
1529
|
+
success: false,
|
|
1530
|
+
error: error.message,
|
|
1531
|
+
};
|
|
1532
|
+
}
|
|
1533
|
+
}
|
|
1534
|
+
|
|
1535
|
+
// ---------------------------------------------------------------------------
|
|
1536
|
+
// Directory instrumenter
|
|
1537
|
+
// ---------------------------------------------------------------------------
|
|
1538
|
+
|
|
1539
|
+
/**
|
|
1540
|
+
* Recursively collect all instrumentable files in a directory.
|
|
1541
|
+
*/
|
|
1542
|
+
async function collectFiles(dirPath: string): Promise<string[]> {
|
|
1543
|
+
const results: string[] = [];
|
|
1544
|
+
|
|
1545
|
+
async function walk(dir: string): Promise<void> {
|
|
1546
|
+
let entries: string[];
|
|
1547
|
+
try {
|
|
1548
|
+
entries = await readdir(dir);
|
|
1549
|
+
} catch {
|
|
1550
|
+
return;
|
|
1551
|
+
}
|
|
1552
|
+
|
|
1553
|
+
for (const entry of entries) {
|
|
1554
|
+
const fullPath = join(dir, entry);
|
|
1555
|
+
|
|
1556
|
+
// Skip hidden directories and known non-source dirs
|
|
1557
|
+
if (entry.startsWith('.') || SKIP_DIRS.has(entry)) continue;
|
|
1558
|
+
|
|
1559
|
+
let stats;
|
|
1560
|
+
try {
|
|
1561
|
+
stats = await stat(fullPath);
|
|
1562
|
+
} catch {
|
|
1563
|
+
continue;
|
|
1564
|
+
}
|
|
1565
|
+
|
|
1566
|
+
if (stats.isDirectory()) {
|
|
1567
|
+
await walk(fullPath);
|
|
1568
|
+
} else if (stats.isFile()) {
|
|
1569
|
+
const ext = extname(entry).toLowerCase();
|
|
1570
|
+
if (!VALID_EXTENSIONS.has(ext)) continue;
|
|
1571
|
+
|
|
1572
|
+
// Skip test files, spec files, and declaration files
|
|
1573
|
+
const name = entry.toLowerCase();
|
|
1574
|
+
if (name.endsWith('.test.ts') || name.endsWith('.test.js')) continue;
|
|
1575
|
+
if (name.endsWith('.test.tsx') || name.endsWith('.test.jsx')) continue;
|
|
1576
|
+
if (name.endsWith('.spec.ts') || name.endsWith('.spec.js')) continue;
|
|
1577
|
+
if (name.endsWith('.spec.tsx') || name.endsWith('.spec.jsx')) continue;
|
|
1578
|
+
if (name.endsWith('.d.ts')) continue;
|
|
1579
|
+
|
|
1580
|
+
results.push(fullPath);
|
|
1581
|
+
}
|
|
1582
|
+
}
|
|
1583
|
+
}
|
|
1584
|
+
|
|
1585
|
+
await walk(dirPath);
|
|
1586
|
+
return results;
|
|
1587
|
+
}
|
|
1588
|
+
|
|
1589
|
+
/**
|
|
1590
|
+
* Instrument all eligible files in a directory recursively.
|
|
1591
|
+
*/
|
|
1592
|
+
export async function instrumentDirectory(
|
|
1593
|
+
dirPath: string,
|
|
1594
|
+
options: InstrumentOptions
|
|
1595
|
+
): Promise<InstrumentResult[]> {
|
|
1596
|
+
const absoluteDir = resolve(dirPath);
|
|
1597
|
+
const files = await collectFiles(absoluteDir);
|
|
1598
|
+
const results: InstrumentResult[] = [];
|
|
1599
|
+
|
|
1600
|
+
for (const file of files) {
|
|
1601
|
+
const result = await instrumentFile(file, options);
|
|
1602
|
+
results.push(result);
|
|
1603
|
+
}
|
|
1604
|
+
|
|
1605
|
+
return results;
|
|
1606
|
+
}
|
|
1607
|
+
|
|
1608
|
+
// ---------------------------------------------------------------------------
|
|
1609
|
+
// Validation
|
|
1610
|
+
// ---------------------------------------------------------------------------
|
|
1611
|
+
|
|
1612
|
+
/**
|
|
1613
|
+
* Validate that instrumentation was applied correctly to a file.
|
|
1614
|
+
* Parses the file, checks for syntax errors, and verifies probe markers.
|
|
1615
|
+
*/
|
|
1616
|
+
export async function validateInstrumentation(
|
|
1617
|
+
filePath: string
|
|
1618
|
+
): Promise<ValidationResult> {
|
|
1619
|
+
const absolutePath = resolve(filePath);
|
|
1620
|
+
const errors: string[] = [];
|
|
1621
|
+
const warnings: string[] = [];
|
|
1622
|
+
|
|
1623
|
+
try {
|
|
1624
|
+
const source = await readFile(absolutePath, 'utf-8');
|
|
1625
|
+
const plugins = getParserPlugins(absolutePath);
|
|
1626
|
+
|
|
1627
|
+
let ast: t.File;
|
|
1628
|
+
try {
|
|
1629
|
+
ast = parser.parse(source, {
|
|
1630
|
+
sourceType: getSourceType(absolutePath),
|
|
1631
|
+
plugins,
|
|
1632
|
+
errorRecovery: true,
|
|
1633
|
+
});
|
|
1634
|
+
} catch (parseError: any) {
|
|
1635
|
+
return {
|
|
1636
|
+
valid: false,
|
|
1637
|
+
errors: [`Syntax error after instrumentation: ${parseError.message}`],
|
|
1638
|
+
warnings: [],
|
|
1639
|
+
};
|
|
1640
|
+
}
|
|
1641
|
+
|
|
1642
|
+
// Check for parser errors stored in the AST
|
|
1643
|
+
const astAny = ast as any;
|
|
1644
|
+
if (astAny.errors && astAny.errors.length > 0) {
|
|
1645
|
+
for (const err of astAny.errors) {
|
|
1646
|
+
errors.push(`Parser error: ${err?.message || String(err)}`);
|
|
1647
|
+
}
|
|
1648
|
+
}
|
|
1649
|
+
|
|
1650
|
+
// Check that utopia:probe comments exist and are followed by valid code
|
|
1651
|
+
let probeCommentCount = 0;
|
|
1652
|
+
let validProbeCount = 0;
|
|
1653
|
+
|
|
1654
|
+
traverse(ast, {
|
|
1655
|
+
enter(path: any) {
|
|
1656
|
+
const node = path.node;
|
|
1657
|
+
if (!node.leadingComments) return;
|
|
1658
|
+
|
|
1659
|
+
for (const comment of node.leadingComments) {
|
|
1660
|
+
if (comment.value.includes('utopia:probe')) {
|
|
1661
|
+
probeCommentCount++;
|
|
1662
|
+
|
|
1663
|
+
// Verify the node after the comment is a valid probe construct
|
|
1664
|
+
if (t.isTryStatement(node)) {
|
|
1665
|
+
// Error probe: check catch param is __utopia_err
|
|
1666
|
+
if (
|
|
1667
|
+
node.handler &&
|
|
1668
|
+
node.handler.param &&
|
|
1669
|
+
t.isIdentifier(node.handler.param) &&
|
|
1670
|
+
node.handler.param.name === '__utopia_err'
|
|
1671
|
+
) {
|
|
1672
|
+
validProbeCount++;
|
|
1673
|
+
} else {
|
|
1674
|
+
warnings.push(
|
|
1675
|
+
`Probe comment at line ${node.loc?.start?.line ?? '?'} followed by try/catch without expected __utopia_err parameter`
|
|
1676
|
+
);
|
|
1677
|
+
}
|
|
1678
|
+
} else if (t.isExpressionStatement(node)) {
|
|
1679
|
+
// Could be infra probe or other report call
|
|
1680
|
+
const expr = node.expression;
|
|
1681
|
+
if (
|
|
1682
|
+
t.isCallExpression(expr) &&
|
|
1683
|
+
t.isMemberExpression(expr.callee) &&
|
|
1684
|
+
t.isIdentifier(expr.callee.object) &&
|
|
1685
|
+
expr.callee.object.name === '__utopia'
|
|
1686
|
+
) {
|
|
1687
|
+
validProbeCount++;
|
|
1688
|
+
} else {
|
|
1689
|
+
warnings.push(
|
|
1690
|
+
`Probe comment at line ${node.loc?.start?.line ?? '?'} not followed by expected __utopia call`
|
|
1691
|
+
);
|
|
1692
|
+
}
|
|
1693
|
+
} else {
|
|
1694
|
+
warnings.push(
|
|
1695
|
+
`Probe comment at line ${node.loc?.start?.line ?? '?'} followed by unexpected node type: ${node.type}`
|
|
1696
|
+
);
|
|
1697
|
+
}
|
|
1698
|
+
}
|
|
1699
|
+
}
|
|
1700
|
+
},
|
|
1701
|
+
});
|
|
1702
|
+
|
|
1703
|
+
if (probeCommentCount === 0) {
|
|
1704
|
+
warnings.push('No utopia:probe markers found in file');
|
|
1705
|
+
}
|
|
1706
|
+
|
|
1707
|
+
// Verify utopia-runtime import exists if probes are present
|
|
1708
|
+
if (probeCommentCount > 0 && !hasUtopiaImport(ast)) {
|
|
1709
|
+
errors.push(
|
|
1710
|
+
'File has utopia:probe markers but is missing "utopia-runtime" import'
|
|
1711
|
+
);
|
|
1712
|
+
}
|
|
1713
|
+
|
|
1714
|
+
// Check for unmatched __utopia references (should all be guarded by import)
|
|
1715
|
+
let hasUtopiaRef = false;
|
|
1716
|
+
traverse(ast, {
|
|
1717
|
+
Identifier(path: any) {
|
|
1718
|
+
if (path.node.name === '__utopia') {
|
|
1719
|
+
hasUtopiaRef = true;
|
|
1720
|
+
path.stop();
|
|
1721
|
+
}
|
|
1722
|
+
},
|
|
1723
|
+
});
|
|
1724
|
+
|
|
1725
|
+
if (hasUtopiaRef && !hasUtopiaImport(ast)) {
|
|
1726
|
+
errors.push(
|
|
1727
|
+
'File references __utopia but does not import from utopia-runtime'
|
|
1728
|
+
);
|
|
1729
|
+
}
|
|
1730
|
+
|
|
1731
|
+
return {
|
|
1732
|
+
valid: errors.length === 0,
|
|
1733
|
+
errors,
|
|
1734
|
+
warnings,
|
|
1735
|
+
};
|
|
1736
|
+
} catch (error: any) {
|
|
1737
|
+
return {
|
|
1738
|
+
valid: false,
|
|
1739
|
+
errors: [`Validation failed: ${error.message}`],
|
|
1740
|
+
warnings: [],
|
|
1741
|
+
};
|
|
1742
|
+
}
|
|
1743
|
+
}
|
|
1744
|
+
|
|
1745
|
+
// ---------------------------------------------------------------------------
|
|
1746
|
+
// Entry point detection
|
|
1747
|
+
// ---------------------------------------------------------------------------
|
|
1748
|
+
|
|
1749
|
+
/**
|
|
1750
|
+
* Determine if a file is considered an entry point.
|
|
1751
|
+
*/
|
|
1752
|
+
export function isEntryPoint(
|
|
1753
|
+
filePath: string,
|
|
1754
|
+
customEntryPoints?: string[]
|
|
1755
|
+
): boolean {
|
|
1756
|
+
const absolutePath = resolve(filePath);
|
|
1757
|
+
const base = basename(absolutePath);
|
|
1758
|
+
const normalizedPath = absolutePath.replace(/\\/g, '/');
|
|
1759
|
+
|
|
1760
|
+
// Check against well-known entry point basenames
|
|
1761
|
+
if (ENTRY_POINT_BASENAMES.has(base)) {
|
|
1762
|
+
return true;
|
|
1763
|
+
}
|
|
1764
|
+
|
|
1765
|
+
// Check Next.js API route patterns
|
|
1766
|
+
if (
|
|
1767
|
+
normalizedPath.includes('/pages/api/') ||
|
|
1768
|
+
normalizedPath.includes('/src/pages/api/') ||
|
|
1769
|
+
normalizedPath.includes('/app/api/')
|
|
1770
|
+
) {
|
|
1771
|
+
return true;
|
|
1772
|
+
}
|
|
1773
|
+
|
|
1774
|
+
// Check custom entry points
|
|
1775
|
+
if (customEntryPoints) {
|
|
1776
|
+
for (const pattern of customEntryPoints) {
|
|
1777
|
+
const resolvedPattern = resolve(pattern);
|
|
1778
|
+
if (absolutePath === resolvedPattern) {
|
|
1779
|
+
return true;
|
|
1780
|
+
}
|
|
1781
|
+
// Support glob-like suffix matching: if pattern ends with **, treat as directory prefix
|
|
1782
|
+
if (pattern.endsWith('**')) {
|
|
1783
|
+
const prefix = resolve(pattern.slice(0, -2));
|
|
1784
|
+
if (absolutePath.startsWith(prefix)) {
|
|
1785
|
+
return true;
|
|
1786
|
+
}
|
|
1787
|
+
}
|
|
1788
|
+
}
|
|
1789
|
+
}
|
|
1790
|
+
|
|
1791
|
+
return false;
|
|
1792
|
+
}
|
|
1793
|
+
|
|
1794
|
+
// ---------------------------------------------------------------------------
|
|
1795
|
+
// Exports
|
|
1796
|
+
// ---------------------------------------------------------------------------
|
|
1797
|
+
|
|
1798
|
+
export type { InstrumentOptions, InstrumentResult, ValidationResult };
|