te.js 2.1.0 → 2.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +197 -196
- package/auto-docs/analysis/handler-analyzer.js +58 -58
- package/auto-docs/analysis/source-resolver.js +101 -101
- package/auto-docs/constants.js +37 -37
- package/auto-docs/docs-llm/index.js +7 -7
- package/auto-docs/docs-llm/prompts.js +222 -222
- package/auto-docs/docs-llm/provider.js +132 -132
- package/auto-docs/index.js +146 -146
- package/auto-docs/openapi/endpoint-processor.js +277 -277
- package/auto-docs/openapi/generator.js +107 -107
- package/auto-docs/openapi/level3.js +131 -131
- package/auto-docs/openapi/spec-builders.js +244 -244
- package/auto-docs/ui/docs-ui.js +186 -186
- package/auto-docs/utils/logger.js +17 -17
- package/auto-docs/utils/strip-usage.js +10 -10
- package/cli/docs-command.js +315 -315
- package/cli/fly-command.js +71 -71
- package/cli/index.js +56 -56
- package/database/index.js +165 -165
- package/database/mongodb.js +146 -146
- package/database/redis.js +201 -201
- package/docs/README.md +36 -36
- package/docs/ammo.md +362 -362
- package/docs/api-reference.md +490 -490
- package/docs/auto-docs.md +216 -216
- package/docs/cli.md +152 -152
- package/docs/configuration.md +275 -275
- package/docs/database.md +390 -390
- package/docs/error-handling.md +438 -438
- package/docs/file-uploads.md +333 -333
- package/docs/getting-started.md +214 -214
- package/docs/middleware.md +355 -355
- package/docs/rate-limiting.md +393 -393
- package/docs/routing.md +302 -302
- package/package.json +62 -62
- package/rate-limit/algorithms/fixed-window.js +141 -141
- package/rate-limit/algorithms/sliding-window.js +147 -147
- package/rate-limit/algorithms/token-bucket.js +115 -115
- package/rate-limit/base.js +165 -165
- package/rate-limit/index.js +147 -147
- package/rate-limit/storage/base.js +104 -104
- package/rate-limit/storage/memory.js +101 -101
- package/rate-limit/storage/redis.js +88 -88
- package/server/ammo/body-parser.js +220 -220
- package/server/ammo/dispatch-helper.js +103 -103
- package/server/ammo/enhancer.js +57 -57
- package/server/ammo.js +454 -415
- package/server/endpoint.js +97 -74
- package/server/error.js +9 -9
- package/server/errors/code-context.js +125 -125
- package/server/errors/llm-error-service.js +140 -140
- package/server/files/helper.js +33 -33
- package/server/files/uploader.js +143 -143
- package/server/handler.js +158 -119
- package/server/target.js +185 -175
- package/server/targets/middleware-validator.js +22 -22
- package/server/targets/path-validator.js +21 -21
- package/server/targets/registry.js +160 -160
- package/server/targets/shoot-validator.js +21 -21
- package/te.js +428 -402
- package/utils/auto-register.js +17 -17
- package/utils/configuration.js +64 -64
- package/utils/errors-llm-config.js +84 -84
- package/utils/request-logger.js +43 -43
- package/utils/status-codes.js +82 -82
- package/utils/tejas-entrypoint-html.js +18 -18
package/server/endpoint.js
CHANGED
|
@@ -1,74 +1,97 @@
|
|
|
1
|
-
import isMiddlewareValid from './targets/middleware-validator.js';
|
|
2
|
-
import { isPathValid, standardizePath } from './targets/path-validator.js';
|
|
3
|
-
import isShootValid from './targets/shoot-validator.js';
|
|
4
|
-
|
|
5
|
-
class Endpoint {
|
|
6
|
-
constructor() {
|
|
7
|
-
this.path = '';
|
|
8
|
-
this.middlewares = [];
|
|
9
|
-
this.handler = null;
|
|
10
|
-
this.metadata = null;
|
|
11
|
-
/**
|
|
12
|
-
this.
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
if (
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
1
|
+
import isMiddlewareValid from './targets/middleware-validator.js';
|
|
2
|
+
import { isPathValid, standardizePath } from './targets/path-validator.js';
|
|
3
|
+
import isShootValid from './targets/shoot-validator.js';
|
|
4
|
+
|
|
5
|
+
class Endpoint {
|
|
6
|
+
constructor() {
|
|
7
|
+
this.path = '';
|
|
8
|
+
this.middlewares = [];
|
|
9
|
+
this.handler = null;
|
|
10
|
+
this.metadata = null;
|
|
11
|
+
/** Allowed HTTP methods (e.g. ['GET', 'POST']). null = method-agnostic. */
|
|
12
|
+
this.methods = null;
|
|
13
|
+
/** Source group (e.g. target file id) for grouping in docs. Set by loader before register(). */
|
|
14
|
+
this.group = null;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
setPath(base, path) {
|
|
18
|
+
const standardizedBase = standardizePath(base);
|
|
19
|
+
const standardizedPath = standardizePath(path);
|
|
20
|
+
|
|
21
|
+
let fullPath = `${standardizedBase}${standardizedPath}`;
|
|
22
|
+
if (fullPath.length === 0) fullPath = '/';
|
|
23
|
+
|
|
24
|
+
if (!isPathValid(fullPath)) return this;
|
|
25
|
+
|
|
26
|
+
this.path = fullPath;
|
|
27
|
+
return this;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
setMiddlewares(middlewares) {
|
|
31
|
+
const validMiddlewares = middlewares.filter(isMiddlewareValid);
|
|
32
|
+
if (validMiddlewares.length === 0) return this;
|
|
33
|
+
|
|
34
|
+
this.middlewares = this.middlewares.concat(validMiddlewares);
|
|
35
|
+
return this;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
setHandler(handler) {
|
|
39
|
+
if (!isShootValid(handler)) return this;
|
|
40
|
+
this.handler = handler;
|
|
41
|
+
|
|
42
|
+
return this;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
setMetadata(metadata) {
|
|
46
|
+
this.metadata = metadata ?? null;
|
|
47
|
+
return this;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* @param {string[]|null} methods - Allowed HTTP methods (e.g. ['GET', 'POST']). null = method-agnostic.
|
|
52
|
+
*/
|
|
53
|
+
setMethods(methods) {
|
|
54
|
+
if (methods == null) {
|
|
55
|
+
this.methods = null;
|
|
56
|
+
return this;
|
|
57
|
+
}
|
|
58
|
+
const arr = Array.isArray(methods) ? methods : [methods];
|
|
59
|
+
let normalized = arr.map((m) => String(m).toUpperCase()).filter(Boolean);
|
|
60
|
+
if (normalized.includes('GET') && !normalized.includes('HEAD')) {
|
|
61
|
+
normalized = [...normalized, 'HEAD'];
|
|
62
|
+
}
|
|
63
|
+
this.methods = normalized.length > 0 ? normalized : null;
|
|
64
|
+
return this;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
setGroup(group) {
|
|
68
|
+
this.group = group ?? null;
|
|
69
|
+
return this;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
getPath() {
|
|
73
|
+
return this.path;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
getMiddlewares() {
|
|
77
|
+
return this.middlewares;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
getHandler() {
|
|
81
|
+
return this.handler;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
getMetadata() {
|
|
85
|
+
return this.metadata;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
getMethods() {
|
|
89
|
+
return this.methods;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
getGroup() {
|
|
93
|
+
return this.group;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export default Endpoint;
|
package/server/error.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
class TejError extends Error {
|
|
2
|
-
constructor(code, message) {
|
|
3
|
-
super(message);
|
|
4
|
-
this.name = this.constructor.name;
|
|
5
|
-
this.code = code;
|
|
6
|
-
}
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
export default TejError;
|
|
1
|
+
class TejError extends Error {
|
|
2
|
+
constructor(code, message) {
|
|
3
|
+
super(message);
|
|
4
|
+
this.name = this.constructor.name;
|
|
5
|
+
this.code = code;
|
|
6
|
+
}
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export default TejError;
|
|
@@ -1,125 +1,125 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Capture code context from the call stack: surrounding source with line numbers,
|
|
3
|
-
* including upstream callers and downstream code. Used by LLM error inference.
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import { readFile } from 'node:fs/promises';
|
|
7
|
-
import { fileURLToPath } from 'node:url';
|
|
8
|
-
import path from 'node:path';
|
|
9
|
-
|
|
10
|
-
/** Path segments that identify te.js internals (excluded from "user" stack frames). */
|
|
11
|
-
const INTERNAL_PATTERNS = [
|
|
12
|
-
'server/ammo.js',
|
|
13
|
-
'server/handler.js',
|
|
14
|
-
'server/errors/llm-error-service.js',
|
|
15
|
-
'server/errors/code-context.js',
|
|
16
|
-
'node_modules',
|
|
17
|
-
];
|
|
18
|
-
|
|
19
|
-
const LINES_ABOVE = 25;
|
|
20
|
-
const LINES_BELOW = 25;
|
|
21
|
-
const MAX_FRAMES = 6;
|
|
22
|
-
|
|
23
|
-
/**
|
|
24
|
-
* Parse a single stack frame line to extract file path, line, and column.
|
|
25
|
-
* Handles "at fn (file:///path:line:col)" and "at file:///path:line:col" and "at /path:line:col".
|
|
26
|
-
* @param {string} line
|
|
27
|
-
* @returns {{ filePath: string, line: number, column: number } | null}
|
|
28
|
-
*/
|
|
29
|
-
function parseStackFrame(line) {
|
|
30
|
-
const trimmed = line.trim();
|
|
31
|
-
if (!trimmed.startsWith('at ')) return null;
|
|
32
|
-
// Last occurrence of :number:number is line:column (path may contain colons on Windows/file URL)
|
|
33
|
-
const match = trimmed.match(/:(\d+):(\d+)\s*\)?\s*$/);
|
|
34
|
-
if (!match) return null;
|
|
35
|
-
const lineNum = parseInt(match[1], 10);
|
|
36
|
-
const colNum = parseInt(match[2], 10);
|
|
37
|
-
const before = trimmed.slice(0, trimmed.lastIndexOf(':' + match[1] + ':' + match[2]));
|
|
38
|
-
// Strip "at ... (" or "at " prefix to get path
|
|
39
|
-
let filePath = before.replace(/^\s*at\s+(?:.*?\s+\()?/, '').replace(/\)?\s*$/, '').trim();
|
|
40
|
-
if (filePath.startsWith('file://')) {
|
|
41
|
-
try {
|
|
42
|
-
filePath = fileURLToPath(filePath);
|
|
43
|
-
} catch {
|
|
44
|
-
return null;
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
if (!filePath || lineNum <= 0) return null;
|
|
48
|
-
return { filePath, line: lineNum, column: colNum };
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
/**
|
|
52
|
-
* Return true if this file path is internal (te.js / node_modules) and should be skipped for user context.
|
|
53
|
-
* @param {string} filePath - Absolute or relative path
|
|
54
|
-
*/
|
|
55
|
-
function isInternalFrame(filePath) {
|
|
56
|
-
const normalized = path.normalize(filePath).replace(/\\/g, '/');
|
|
57
|
-
return INTERNAL_PATTERNS.some((p) => normalized.includes(p));
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
/**
|
|
61
|
-
* Read source file and return lines [lineNum - LINES_ABOVE, lineNum + LINES_BELOW] with line numbers.
|
|
62
|
-
* @param {string} filePath - Absolute path
|
|
63
|
-
* @param {number} lineNum - Center line (1-based)
|
|
64
|
-
* @returns {Promise<{ file: string, line: number, snippet: string } | null>}
|
|
65
|
-
*/
|
|
66
|
-
async function readSnippet(filePath, lineNum) {
|
|
67
|
-
let content;
|
|
68
|
-
try {
|
|
69
|
-
content = await readFile(filePath, 'utf-8');
|
|
70
|
-
} catch {
|
|
71
|
-
return null;
|
|
72
|
-
}
|
|
73
|
-
const lines = content.split(/\r?\n/);
|
|
74
|
-
const start = Math.max(0, lineNum - 1 - LINES_ABOVE);
|
|
75
|
-
const end = Math.min(lines.length, lineNum + LINES_BELOW);
|
|
76
|
-
const snippet = lines
|
|
77
|
-
.slice(start, end)
|
|
78
|
-
.map((text, i) => {
|
|
79
|
-
const num = start + i + 1;
|
|
80
|
-
const marker = num === lineNum ? ' →' : ' ';
|
|
81
|
-
return `${String(num).padStart(4)}${marker} ${text}`;
|
|
82
|
-
})
|
|
83
|
-
.join('\n');
|
|
84
|
-
|
|
85
|
-
return {
|
|
86
|
-
file: filePath,
|
|
87
|
-
line: lineNum,
|
|
88
|
-
snippet,
|
|
89
|
-
};
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
/**
|
|
93
|
-
* Capture code context from the current call stack: parse stack, filter to user frames,
|
|
94
|
-
* and read surrounding source (with line numbers) for each frame. First frame is the
|
|
95
|
-
* throw site; remaining frames are upstream callers. Each snippet includes lines
|
|
96
|
-
* above and below (downstream in the same function).
|
|
97
|
-
*
|
|
98
|
-
* @param {string} [stack] - Stack string (e.g. new Error().stack). If omitted, captures current stack.
|
|
99
|
-
* @param {{ maxFrames?: number, linesAround?: number }} [options]
|
|
100
|
-
* @returns {Promise<{ snippets: Array<{ file: string, line: number, snippet: string }> }>}
|
|
101
|
-
*/
|
|
102
|
-
export async function captureCodeContext(stack, options = {}) {
|
|
103
|
-
const stackStr = typeof stack === 'string' && stack ? stack : new Error().stack;
|
|
104
|
-
if (!stackStr) return { snippets: [] };
|
|
105
|
-
|
|
106
|
-
const maxFrames = options.maxFrames ?? MAX_FRAMES;
|
|
107
|
-
const lines = stackStr.split('\n');
|
|
108
|
-
const frames = [];
|
|
109
|
-
|
|
110
|
-
for (const line of lines) {
|
|
111
|
-
const parsed = parseStackFrame(line);
|
|
112
|
-
if (!parsed) continue;
|
|
113
|
-
if (isInternalFrame(parsed.filePath)) continue;
|
|
114
|
-
frames.push(parsed);
|
|
115
|
-
if (frames.length >= maxFrames) break;
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
const snippets = [];
|
|
119
|
-
for (const { filePath, line } of frames) {
|
|
120
|
-
const one = await readSnippet(filePath, line);
|
|
121
|
-
if (one) snippets.push(one);
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
return { snippets };
|
|
125
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Capture code context from the call stack: surrounding source with line numbers,
|
|
3
|
+
* including upstream callers and downstream code. Used by LLM error inference.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { readFile } from 'node:fs/promises';
|
|
7
|
+
import { fileURLToPath } from 'node:url';
|
|
8
|
+
import path from 'node:path';
|
|
9
|
+
|
|
10
|
+
/** Path segments that identify te.js internals (excluded from "user" stack frames). */
|
|
11
|
+
const INTERNAL_PATTERNS = [
|
|
12
|
+
'server/ammo.js',
|
|
13
|
+
'server/handler.js',
|
|
14
|
+
'server/errors/llm-error-service.js',
|
|
15
|
+
'server/errors/code-context.js',
|
|
16
|
+
'node_modules',
|
|
17
|
+
];
|
|
18
|
+
|
|
19
|
+
const LINES_ABOVE = 25;
|
|
20
|
+
const LINES_BELOW = 25;
|
|
21
|
+
const MAX_FRAMES = 6;
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Parse a single stack frame line to extract file path, line, and column.
|
|
25
|
+
* Handles "at fn (file:///path:line:col)" and "at file:///path:line:col" and "at /path:line:col".
|
|
26
|
+
* @param {string} line
|
|
27
|
+
* @returns {{ filePath: string, line: number, column: number } | null}
|
|
28
|
+
*/
|
|
29
|
+
function parseStackFrame(line) {
|
|
30
|
+
const trimmed = line.trim();
|
|
31
|
+
if (!trimmed.startsWith('at ')) return null;
|
|
32
|
+
// Last occurrence of :number:number is line:column (path may contain colons on Windows/file URL)
|
|
33
|
+
const match = trimmed.match(/:(\d+):(\d+)\s*\)?\s*$/);
|
|
34
|
+
if (!match) return null;
|
|
35
|
+
const lineNum = parseInt(match[1], 10);
|
|
36
|
+
const colNum = parseInt(match[2], 10);
|
|
37
|
+
const before = trimmed.slice(0, trimmed.lastIndexOf(':' + match[1] + ':' + match[2]));
|
|
38
|
+
// Strip "at ... (" or "at " prefix to get path
|
|
39
|
+
let filePath = before.replace(/^\s*at\s+(?:.*?\s+\()?/, '').replace(/\)?\s*$/, '').trim();
|
|
40
|
+
if (filePath.startsWith('file://')) {
|
|
41
|
+
try {
|
|
42
|
+
filePath = fileURLToPath(filePath);
|
|
43
|
+
} catch {
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
if (!filePath || lineNum <= 0) return null;
|
|
48
|
+
return { filePath, line: lineNum, column: colNum };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Return true if this file path is internal (te.js / node_modules) and should be skipped for user context.
|
|
53
|
+
* @param {string} filePath - Absolute or relative path
|
|
54
|
+
*/
|
|
55
|
+
function isInternalFrame(filePath) {
|
|
56
|
+
const normalized = path.normalize(filePath).replace(/\\/g, '/');
|
|
57
|
+
return INTERNAL_PATTERNS.some((p) => normalized.includes(p));
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Read source file and return lines [lineNum - LINES_ABOVE, lineNum + LINES_BELOW] with line numbers.
|
|
62
|
+
* @param {string} filePath - Absolute path
|
|
63
|
+
* @param {number} lineNum - Center line (1-based)
|
|
64
|
+
* @returns {Promise<{ file: string, line: number, snippet: string } | null>}
|
|
65
|
+
*/
|
|
66
|
+
async function readSnippet(filePath, lineNum) {
|
|
67
|
+
let content;
|
|
68
|
+
try {
|
|
69
|
+
content = await readFile(filePath, 'utf-8');
|
|
70
|
+
} catch {
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
const lines = content.split(/\r?\n/);
|
|
74
|
+
const start = Math.max(0, lineNum - 1 - LINES_ABOVE);
|
|
75
|
+
const end = Math.min(lines.length, lineNum + LINES_BELOW);
|
|
76
|
+
const snippet = lines
|
|
77
|
+
.slice(start, end)
|
|
78
|
+
.map((text, i) => {
|
|
79
|
+
const num = start + i + 1;
|
|
80
|
+
const marker = num === lineNum ? ' →' : ' ';
|
|
81
|
+
return `${String(num).padStart(4)}${marker} ${text}`;
|
|
82
|
+
})
|
|
83
|
+
.join('\n');
|
|
84
|
+
|
|
85
|
+
return {
|
|
86
|
+
file: filePath,
|
|
87
|
+
line: lineNum,
|
|
88
|
+
snippet,
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Capture code context from the current call stack: parse stack, filter to user frames,
|
|
94
|
+
* and read surrounding source (with line numbers) for each frame. First frame is the
|
|
95
|
+
* throw site; remaining frames are upstream callers. Each snippet includes lines
|
|
96
|
+
* above and below (downstream in the same function).
|
|
97
|
+
*
|
|
98
|
+
* @param {string} [stack] - Stack string (e.g. new Error().stack). If omitted, captures current stack.
|
|
99
|
+
* @param {{ maxFrames?: number, linesAround?: number }} [options]
|
|
100
|
+
* @returns {Promise<{ snippets: Array<{ file: string, line: number, snippet: string }> }>}
|
|
101
|
+
*/
|
|
102
|
+
export async function captureCodeContext(stack, options = {}) {
|
|
103
|
+
const stackStr = typeof stack === 'string' && stack ? stack : new Error().stack;
|
|
104
|
+
if (!stackStr) return { snippets: [] };
|
|
105
|
+
|
|
106
|
+
const maxFrames = options.maxFrames ?? MAX_FRAMES;
|
|
107
|
+
const lines = stackStr.split('\n');
|
|
108
|
+
const frames = [];
|
|
109
|
+
|
|
110
|
+
for (const line of lines) {
|
|
111
|
+
const parsed = parseStackFrame(line);
|
|
112
|
+
if (!parsed) continue;
|
|
113
|
+
if (isInternalFrame(parsed.filePath)) continue;
|
|
114
|
+
frames.push(parsed);
|
|
115
|
+
if (frames.length >= maxFrames) break;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const snippets = [];
|
|
119
|
+
for (const { filePath, line } of frames) {
|
|
120
|
+
const one = await readSnippet(filePath, line);
|
|
121
|
+
if (one) snippets.push(one);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return { snippets };
|
|
125
|
+
}
|