tina4-nodejs 3.13.133 → 3.13.134
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.md +3 -3
- package/README.md +2 -2
- package/package.json +1 -1
- package/packages/cli/dist/bin.js +3181 -3051
- package/packages/cli/src/commands/generate.ts +33 -22
- package/packages/cli/src/commands/lint.ts +77 -111
- package/packages/core/dist/index.js +3090 -2952
- package/packages/core/src/.tina4-metrics.json +15004 -0
- package/packages/core/src/aiClient.ts +199 -161
- package/packages/core/src/dispatchPipeline.ts +65 -67
- package/packages/core/src/docs.ts +52 -544
- package/packages/core/src/docsParser.ts +270 -0
- package/packages/core/src/docsScanner.ts +121 -0
- package/packages/core/src/docsSignatures.ts +165 -0
- package/packages/core/src/index.ts +2 -0
- package/packages/core/src/logger.ts +68 -82
- package/packages/core/src/mcp.ts +32 -60
- package/packages/core/src/messenger.ts +136 -157
- package/packages/core/src/middleware.ts +56 -60
- package/packages/core/src/plan.ts +78 -70
- package/packages/core/src/projectIndex.ts +15 -288
- package/packages/core/src/projectIndexExtractors.ts +126 -0
- package/packages/core/src/projectIndexStorage.ts +122 -0
- package/packages/core/src/push.ts +281 -0
- package/packages/core/src/server.ts +182 -183
- package/packages/frond/dist/index.js +607 -770
- package/packages/frond/src/engine.ts +670 -818
- package/packages/orm/dist/index.js +3100 -2965
- package/packages/orm/src/adapters/mongodb.ts +99 -144
- package/packages/orm/src/baseModel.ts +429 -515
- package/packages/orm/src/fakeData.ts +73 -61
- package/packages/orm/src/migration.ts +96 -126
- package/packages/orm/src/seeder.ts +6 -238
- package/packages/orm/src/seederTable.ts +101 -0
- package/packages/orm/src/seederTypes.ts +14 -0
- package/packages/orm/src/validation.ts +97 -80
- package/types/core/src/aiClient.d.ts +5 -0
- package/types/core/src/docsParser.d.ts +28 -0
- package/types/core/src/docsScanner.d.ts +1 -0
- package/types/core/src/docsSignatures.d.ts +11 -0
- package/types/core/src/index.d.ts +2 -0
- package/types/core/src/messenger.d.ts +8 -0
- package/types/core/src/projectIndexExtractors.d.ts +3 -0
- package/types/core/src/projectIndexStorage.d.ts +13 -0
- package/types/core/src/push.d.ts +45 -0
- package/types/frond/src/engine.d.ts +25 -0
- package/types/orm/src/fakeData.d.ts +3 -0
- package/types/orm/src/seeder.d.ts +3 -89
- package/types/orm/src/seederTable.d.ts +9 -0
- package/types/orm/src/seederTypes.d.ts +16 -0
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
// ── TS regex parser ──────────────────────────────────────────────────
|
|
2
|
+
import { stripStrings } from "./docsScanner.js";
|
|
3
|
+
import { matchMethodSignature, matchTopLevelFunction } from "./docsSignatures.js";
|
|
4
|
+
|
|
5
|
+
export interface ParsedClass {
|
|
6
|
+
name: string;
|
|
7
|
+
line: number;
|
|
8
|
+
doc: string;
|
|
9
|
+
exported: boolean;
|
|
10
|
+
methods: ParsedMethod[];
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface ParsedMethod {
|
|
14
|
+
name: string;
|
|
15
|
+
line: number;
|
|
16
|
+
doc: string;
|
|
17
|
+
signature: string;
|
|
18
|
+
visibility: "public" | "protected" | "private";
|
|
19
|
+
static: boolean;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface ParsedFile {
|
|
23
|
+
classes: ParsedClass[];
|
|
24
|
+
functions: ParsedMethod[];
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const CLASS_RE = /(?:^|\n)([ \t]*)((?:export\s+(?:default\s+)?(?:abstract\s+)?)?class\s+([A-Za-z_$][\w$]*))[\s\S]*?(?=\n[ \t]*(?:export\s+(?:default\s+)?(?:abstract\s+)?class|export\s+function|function|$))/g;
|
|
28
|
+
|
|
29
|
+
interface ClassContext {
|
|
30
|
+
name: string;
|
|
31
|
+
bodyStartDepth: number;
|
|
32
|
+
entry: ParsedClass;
|
|
33
|
+
isExport: boolean;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function lineNumberAt(source: string, offset: number): number {
|
|
37
|
+
let line = 1;
|
|
38
|
+
for (let i = 0; i < offset && i < source.length; i++) {
|
|
39
|
+
if (source.charCodeAt(i) === 10) line++;
|
|
40
|
+
}
|
|
41
|
+
return line;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function readDocComment(stripped: string, source: string, start: number): { doc: string; next: number } | null {
|
|
45
|
+
if (!(stripped[start] === "/" && stripped[start + 1] === "*" && stripped[start + 2] === "*")) return null;
|
|
46
|
+
const end = stripped.indexOf("*/", start + 3);
|
|
47
|
+
return end === -1
|
|
48
|
+
? { doc: "", next: -1 }
|
|
49
|
+
: { doc: source.slice(start, end + 2), next: end + 2 };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function startClass(
|
|
53
|
+
stripped: string,
|
|
54
|
+
source: string,
|
|
55
|
+
lines: string[],
|
|
56
|
+
index: number,
|
|
57
|
+
braceDepth: number,
|
|
58
|
+
pendingDoc: string,
|
|
59
|
+
): { context: ClassContext; next: number } | null {
|
|
60
|
+
if (!isWordBoundary(stripped, index) || !matchKeyword(stripped, index, "class")) return null;
|
|
61
|
+
const after = index + "class".length;
|
|
62
|
+
const nameMatch = /^\s+([A-Za-z_$][\w$]*)/.exec(stripped.slice(after));
|
|
63
|
+
if (!nameMatch) return null;
|
|
64
|
+
const name = nameMatch[1];
|
|
65
|
+
let open = after + nameMatch[0].length;
|
|
66
|
+
while (open < stripped.length && stripped[open] !== "{") open++;
|
|
67
|
+
if (open >= stripped.length) return null;
|
|
68
|
+
const entry: ParsedClass = {
|
|
69
|
+
name,
|
|
70
|
+
line: lineNumberAt(source, index),
|
|
71
|
+
doc: pendingDoc,
|
|
72
|
+
exported: isExportedAt(stripped, lines, lineNumberAt(source, index), name),
|
|
73
|
+
methods: [],
|
|
74
|
+
};
|
|
75
|
+
return {
|
|
76
|
+
context: { name, bodyStartDepth: braceDepth, entry, isExport: entry.exported },
|
|
77
|
+
next: open + 1,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function closeClasses(
|
|
82
|
+
braceDepth: number,
|
|
83
|
+
stack: ClassContext[],
|
|
84
|
+
classes: ParsedClass[],
|
|
85
|
+
): void {
|
|
86
|
+
while (stack.length > 0 && braceDepth <= stack[stack.length - 1].bodyStartDepth) {
|
|
87
|
+
classes.push(stack.pop()!.entry);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function recordClassMethod(
|
|
92
|
+
stripped: string,
|
|
93
|
+
source: string,
|
|
94
|
+
index: number,
|
|
95
|
+
stack: ClassContext[],
|
|
96
|
+
pendingDoc: string,
|
|
97
|
+
): number | null {
|
|
98
|
+
const match = matchMethodSignature(stripped, source, index);
|
|
99
|
+
if (!match) return null;
|
|
100
|
+
const cls = stack[stack.length - 1];
|
|
101
|
+
cls.entry.methods.push({
|
|
102
|
+
name: match.name,
|
|
103
|
+
line: lineNumberAt(source, match.nameStart ?? index),
|
|
104
|
+
doc: pendingDoc,
|
|
105
|
+
signature: match.signature,
|
|
106
|
+
visibility: match.visibility,
|
|
107
|
+
static: match.static,
|
|
108
|
+
});
|
|
109
|
+
return match.endIndex;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function recordTopLevelFunction(
|
|
113
|
+
stripped: string,
|
|
114
|
+
source: string,
|
|
115
|
+
index: number,
|
|
116
|
+
functions: ParsedMethod[],
|
|
117
|
+
pendingDoc: string,
|
|
118
|
+
): number | null {
|
|
119
|
+
const match = matchTopLevelFunction(stripped, source, index);
|
|
120
|
+
if (!match) return null;
|
|
121
|
+
functions.push({
|
|
122
|
+
name: match.name,
|
|
123
|
+
line: lineNumberAt(source, match.nameStart ?? index),
|
|
124
|
+
doc: pendingDoc,
|
|
125
|
+
signature: match.signature,
|
|
126
|
+
visibility: "public",
|
|
127
|
+
static: false,
|
|
128
|
+
});
|
|
129
|
+
return match.endIndex;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Parse a TS source string. Lightweight — finds top-level classes and their
|
|
134
|
+
* public methods, plus top-level exported functions. Captures preceding JSDoc.
|
|
135
|
+
*
|
|
136
|
+
* Strategy: scan token-by-token. We don't need a full AST — we only care
|
|
137
|
+
* about identifying class declarations, brace depth (to find class members),
|
|
138
|
+
* method/function declarations, and JSDoc comments immediately above.
|
|
139
|
+
*/
|
|
140
|
+
export function parseTypeScript(source: string, _debugTag = ""): ParsedFile {
|
|
141
|
+
const classes: ParsedClass[] = [];
|
|
142
|
+
const functions: ParsedMethod[] = [];
|
|
143
|
+
|
|
144
|
+
// Strip line comments and string contents (preserve length for line numbers).
|
|
145
|
+
const stripped = stripStrings(source);
|
|
146
|
+
const lines = source.split(/\r?\n/);
|
|
147
|
+
|
|
148
|
+
let i = 0;
|
|
149
|
+
let pendingDoc = "";
|
|
150
|
+
const len = stripped.length;
|
|
151
|
+
let braceDepth = 0;
|
|
152
|
+
const classStack: ClassContext[] = [];
|
|
153
|
+
|
|
154
|
+
while (i < len) {
|
|
155
|
+
const ch = stripped[i];
|
|
156
|
+
|
|
157
|
+
const doc = readDocComment(stripped, source, i);
|
|
158
|
+
if (doc) {
|
|
159
|
+
if (doc.next === -1) break;
|
|
160
|
+
pendingDoc = doc.doc;
|
|
161
|
+
i = doc.next;
|
|
162
|
+
continue;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Skip line comments (already stripped → '/' followed by '/' won't appear in stripped, but be safe)
|
|
166
|
+
if (ch === "/" && stripped[i + 1] === "/") {
|
|
167
|
+
while (i < len && stripped[i] !== "\n") i++;
|
|
168
|
+
continue;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Brace tracking (only outside strings; strings are zeroed in stripped)
|
|
172
|
+
if (ch === "{") {
|
|
173
|
+
braceDepth++;
|
|
174
|
+
i++;
|
|
175
|
+
continue;
|
|
176
|
+
}
|
|
177
|
+
if (ch === "}") {
|
|
178
|
+
braceDepth--;
|
|
179
|
+
closeClasses(braceDepth, classStack, classes);
|
|
180
|
+
i++;
|
|
181
|
+
continue;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const classStart = startClass(stripped, source, lines, i, braceDepth, pendingDoc);
|
|
185
|
+
if (classStart) {
|
|
186
|
+
classStack.push(classStart.context);
|
|
187
|
+
pendingDoc = "";
|
|
188
|
+
braceDepth++;
|
|
189
|
+
i = classStart.next;
|
|
190
|
+
continue;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// Method or top-level function detection — we only care about either:
|
|
194
|
+
// * methods inside a class body (classStack non-empty AND directly inside class body)
|
|
195
|
+
// * top-level "export function" or "function" declarations
|
|
196
|
+
if (classStack.length > 0
|
|
197
|
+
&& braceDepth === classStack[classStack.length - 1].bodyStartDepth + 1) {
|
|
198
|
+
const next = recordClassMethod(stripped, source, i, classStack, pendingDoc);
|
|
199
|
+
if (next !== null) {
|
|
200
|
+
pendingDoc = "";
|
|
201
|
+
i = next;
|
|
202
|
+
continue;
|
|
203
|
+
}
|
|
204
|
+
} else if (braceDepth === 0) {
|
|
205
|
+
const next = recordTopLevelFunction(stripped, source, i, functions, pendingDoc);
|
|
206
|
+
if (next !== null) {
|
|
207
|
+
pendingDoc = "";
|
|
208
|
+
i = next;
|
|
209
|
+
continue;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// Whitespace doesn't reset pendingDoc — but most other tokens do.
|
|
214
|
+
if (!/\s/.test(ch)) {
|
|
215
|
+
// Non-whitespace, non-doc-comment — only reset doc if it was a long way back.
|
|
216
|
+
// Be conservative: only reset on punctuation that clearly terminates.
|
|
217
|
+
if (ch === ";") {
|
|
218
|
+
pendingDoc = "";
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
i++;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// Any unclosed class (shouldn't happen in valid TS) → flush.
|
|
226
|
+
while (classStack.length > 0) classes.push(classStack.pop()!.entry);
|
|
227
|
+
|
|
228
|
+
return { classes, functions };
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function isWordBoundary(text: string, i: number): boolean {
|
|
232
|
+
if (i === 0) return true;
|
|
233
|
+
const prev = text.charCodeAt(i - 1);
|
|
234
|
+
// Word chars: A-Z a-z 0-9 _ $
|
|
235
|
+
if ((prev >= 65 && prev <= 90) || (prev >= 97 && prev <= 122) || (prev >= 48 && prev <= 57) || prev === 95 || prev === 36) {
|
|
236
|
+
return false;
|
|
237
|
+
}
|
|
238
|
+
return true;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function matchKeyword(text: string, i: number, kw: string): boolean {
|
|
242
|
+
if (text.substr(i, kw.length) !== kw) return false;
|
|
243
|
+
const after = i + kw.length;
|
|
244
|
+
if (after >= text.length) return true;
|
|
245
|
+
const nextCode = text.charCodeAt(after);
|
|
246
|
+
if ((nextCode >= 65 && nextCode <= 90) || (nextCode >= 97 && nextCode <= 122) || (nextCode >= 48 && nextCode <= 57) || nextCode === 95 || nextCode === 36) {
|
|
247
|
+
return false;
|
|
248
|
+
}
|
|
249
|
+
return true;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function isExportedAt(_stripped: string, lines: string[], lineNo: number, name: string): boolean {
|
|
253
|
+
// Walk back up to 8 lines and look for "export class <name>" / "export default class <name>"
|
|
254
|
+
const start = Math.max(0, lineNo - 1);
|
|
255
|
+
const exportClass = "export class " + name;
|
|
256
|
+
const exportDefaultClass = "export default class " + name;
|
|
257
|
+
const exportAbstractClass = "export abstract class " + name;
|
|
258
|
+
const justClass = "class " + name;
|
|
259
|
+
for (let l = start; l >= Math.max(0, start - 8); l--) {
|
|
260
|
+
const ln = lines[l] || "";
|
|
261
|
+
if (ln.includes(exportClass) || ln.includes(exportDefaultClass) || ln.includes(exportAbstractClass)) {
|
|
262
|
+
return true;
|
|
263
|
+
}
|
|
264
|
+
if (ln.includes(justClass)) {
|
|
265
|
+
// declared but not exported
|
|
266
|
+
return /\bexport\b/.test(ln);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
return false;
|
|
270
|
+
}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Replace string literals and template literal contents with spaces of equal
|
|
3
|
+
* length so brace/paren scanning isn't fooled by characters inside strings.
|
|
4
|
+
* Also strips line + block comments. Newlines are preserved so line numbers
|
|
5
|
+
* line up with the original source.
|
|
6
|
+
*/
|
|
7
|
+
const BACKTICK = String.fromCharCode(96);
|
|
8
|
+
|
|
9
|
+
function blankSegment(source: string, start: number, end: number): string {
|
|
10
|
+
return source.slice(start, end).replace(/[^\n]/g, " ");
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function stripBlockComment(source: string, start: number): { text: string; next: number } {
|
|
14
|
+
const end = source.indexOf("*/", start + 2);
|
|
15
|
+
return end === -1
|
|
16
|
+
? { text: blankSegment(source, start, source.length), next: source.length }
|
|
17
|
+
: { text: blankSegment(source, start, end + 2), next: end + 2 };
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function stripLineComment(source: string, start: number): { text: string; next: number } {
|
|
21
|
+
const end = source.indexOf("\n", start);
|
|
22
|
+
const next = end === -1 ? source.length : end;
|
|
23
|
+
return { text: " ".repeat(next - start), next };
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function stripQuoted(source: string, start: number): { text: string; next: number } {
|
|
27
|
+
const out = [source[start]];
|
|
28
|
+
let i = start + 1;
|
|
29
|
+
while (i < source.length) {
|
|
30
|
+
const c = source[i];
|
|
31
|
+
if (c === "\\" && i + 1 < source.length) {
|
|
32
|
+
out.push(" ");
|
|
33
|
+
i += 2;
|
|
34
|
+
continue;
|
|
35
|
+
}
|
|
36
|
+
if (c === source[start]) {
|
|
37
|
+
out.push(c);
|
|
38
|
+
i++;
|
|
39
|
+
break;
|
|
40
|
+
}
|
|
41
|
+
out.push(c === "\n" ? "\n" : " ");
|
|
42
|
+
i++;
|
|
43
|
+
}
|
|
44
|
+
return { text: out.join(""), next: i };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function stripTemplateInterpolation(source: string, start: number): { text: string; next: number } {
|
|
48
|
+
const out = [" "];
|
|
49
|
+
let i = start + 2;
|
|
50
|
+
let depth = 1;
|
|
51
|
+
while (i < source.length && depth > 0) {
|
|
52
|
+
const c = source[i];
|
|
53
|
+
if (c === "{") depth++;
|
|
54
|
+
else if (c === "}") depth--;
|
|
55
|
+
out.push(c === "\n" ? "\n" : " ");
|
|
56
|
+
i++;
|
|
57
|
+
}
|
|
58
|
+
return { text: out.join(""), next: i };
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function stripTemplate(source: string, start: number): { text: string; next: number } {
|
|
62
|
+
const out = [BACKTICK];
|
|
63
|
+
let i = start + 1;
|
|
64
|
+
while (i < source.length) {
|
|
65
|
+
const c = source[i];
|
|
66
|
+
if (c === "\\" && i + 1 < source.length) {
|
|
67
|
+
out.push(" ");
|
|
68
|
+
i += 2;
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
if (c === BACKTICK) {
|
|
72
|
+
out.push(BACKTICK);
|
|
73
|
+
i++;
|
|
74
|
+
break;
|
|
75
|
+
}
|
|
76
|
+
if (c === "$" && source[i + 1] === "{") {
|
|
77
|
+
const interpolation = stripTemplateInterpolation(source, i);
|
|
78
|
+
out.push(interpolation.text);
|
|
79
|
+
i = interpolation.next;
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
out.push(c === "\n" ? "\n" : " ");
|
|
83
|
+
i++;
|
|
84
|
+
}
|
|
85
|
+
return { text: out.join(""), next: i };
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function stripStrings(source: string): string {
|
|
89
|
+
const out: string[] = [];
|
|
90
|
+
let i = 0;
|
|
91
|
+
while (i < source.length) {
|
|
92
|
+
const c = source[i];
|
|
93
|
+
if (c === "/" && source[i + 1] === "*") {
|
|
94
|
+
const part = stripBlockComment(source, i);
|
|
95
|
+
out.push(part.text);
|
|
96
|
+
i = part.next;
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
if (c === "/" && source[i + 1] === "/") {
|
|
100
|
+
const part = stripLineComment(source, i);
|
|
101
|
+
out.push(part.text);
|
|
102
|
+
i = part.next;
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
if (c === '"' || c === "'") {
|
|
106
|
+
const part = stripQuoted(source, i);
|
|
107
|
+
out.push(part.text);
|
|
108
|
+
i = part.next;
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
if (c === BACKTICK) {
|
|
112
|
+
const part = stripTemplate(source, i);
|
|
113
|
+
out.push(part.text);
|
|
114
|
+
i = part.next;
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
out.push(c);
|
|
118
|
+
i++;
|
|
119
|
+
}
|
|
120
|
+
return out.join("");
|
|
121
|
+
}
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
interface MethodMatch {
|
|
2
|
+
name: string;
|
|
3
|
+
signature: string;
|
|
4
|
+
endIndex: number;
|
|
5
|
+
nameStart: number;
|
|
6
|
+
visibility: "public" | "protected" | "private";
|
|
7
|
+
static: boolean;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const METHOD_HEAD_RE =
|
|
11
|
+
/^([ \t]*)((?:public|protected|private|readonly|static|async|abstract|override|\s)*)([A-Za-z_$][\w$]*)\s*[<(]/;
|
|
12
|
+
|
|
13
|
+
export function matchMethodSignature(stripped: string, source: string, i: number): MethodMatch | null {
|
|
14
|
+
// Method must be at start-of-line-ish position.
|
|
15
|
+
if (i > 0) {
|
|
16
|
+
const prev = stripped.charCodeAt(i - 1);
|
|
17
|
+
if (prev !== 10 && prev !== 32 && prev !== 9 && prev !== 123) return null;
|
|
18
|
+
}
|
|
19
|
+
// Take the rest of the current line + a little ahead.
|
|
20
|
+
let lineEnd = stripped.indexOf("\n", i);
|
|
21
|
+
if (lineEnd === -1) lineEnd = stripped.length;
|
|
22
|
+
// Read up to 4 lines for multi-line signatures.
|
|
23
|
+
let chunkEnd = lineEnd;
|
|
24
|
+
for (let extra = 0; extra < 4 && chunkEnd < stripped.length; extra++) {
|
|
25
|
+
const next = stripped.indexOf("\n", chunkEnd + 1);
|
|
26
|
+
if (next === -1) break;
|
|
27
|
+
chunkEnd = next;
|
|
28
|
+
}
|
|
29
|
+
const chunk = stripped.slice(i, chunkEnd + 1);
|
|
30
|
+
const match = METHOD_HEAD_RE.exec(chunk);
|
|
31
|
+
if (!match) return null;
|
|
32
|
+
const modifiers = match[2] || "";
|
|
33
|
+
const name = match[3];
|
|
34
|
+
// Skip reserved words / control-flow that masquerade as method names.
|
|
35
|
+
const reserved = new Set([
|
|
36
|
+
"if", "for", "while", "switch", "return", "do", "try", "catch", "throw",
|
|
37
|
+
"const", "let", "var", "import", "export", "function", "class", "interface",
|
|
38
|
+
"type", "new", "yield", "await", "case", "break", "continue", "else",
|
|
39
|
+
]);
|
|
40
|
+
if (reserved.has(name)) return null;
|
|
41
|
+
|
|
42
|
+
// Determine visibility from modifiers
|
|
43
|
+
let visibility: "public" | "protected" | "private" = "public";
|
|
44
|
+
if (/\bprivate\b/.test(modifiers)) visibility = "private";
|
|
45
|
+
else if (/\bprotected\b/.test(modifiers)) visibility = "protected";
|
|
46
|
+
const isStatic = /\bstatic\b/.test(modifiers);
|
|
47
|
+
|
|
48
|
+
// Capture signature — read from start of "name" up through matching ')' and optional return type.
|
|
49
|
+
const nameStart = i + match[1].length + match[2].length;
|
|
50
|
+
const result = captureSignature(stripped, source, nameStart, name);
|
|
51
|
+
if (!result) return null;
|
|
52
|
+
|
|
53
|
+
return {
|
|
54
|
+
name,
|
|
55
|
+
signature: result.signature,
|
|
56
|
+
endIndex: result.endIndex,
|
|
57
|
+
nameStart,
|
|
58
|
+
visibility,
|
|
59
|
+
static: isStatic,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const FN_HEAD_RE =
|
|
64
|
+
/^((?:export\s+(?:default\s+)?)?(?:async\s+)?function\s+)([A-Za-z_$][\w$]*)\s*[<(]/;
|
|
65
|
+
|
|
66
|
+
export function matchTopLevelFunction(stripped: string, source: string, i: number): MethodMatch | null {
|
|
67
|
+
if (i > 0) {
|
|
68
|
+
const prev = stripped.charCodeAt(i - 1);
|
|
69
|
+
if (prev !== 10 && prev !== 32 && prev !== 9) return null;
|
|
70
|
+
}
|
|
71
|
+
let lineEnd = stripped.indexOf("\n", i);
|
|
72
|
+
if (lineEnd === -1) lineEnd = stripped.length;
|
|
73
|
+
let chunkEnd = lineEnd;
|
|
74
|
+
for (let extra = 0; extra < 4 && chunkEnd < stripped.length; extra++) {
|
|
75
|
+
const next = stripped.indexOf("\n", chunkEnd + 1);
|
|
76
|
+
if (next === -1) break;
|
|
77
|
+
chunkEnd = next;
|
|
78
|
+
}
|
|
79
|
+
const chunk = stripped.slice(i, chunkEnd + 1);
|
|
80
|
+
const match = FN_HEAD_RE.exec(chunk);
|
|
81
|
+
if (!match) return null;
|
|
82
|
+
const name = match[2];
|
|
83
|
+
const nameStart = i + match[1].length;
|
|
84
|
+
const result = captureSignature(stripped, source, nameStart, name);
|
|
85
|
+
if (!result) return null;
|
|
86
|
+
return {
|
|
87
|
+
name,
|
|
88
|
+
signature: result.signature,
|
|
89
|
+
endIndex: result.endIndex,
|
|
90
|
+
nameStart,
|
|
91
|
+
visibility: "public",
|
|
92
|
+
static: false,
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
interface CapturedSig {
|
|
97
|
+
signature: string;
|
|
98
|
+
endIndex: number;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function skipWhitespace(text: string, start: number, spacesOnly = false): number {
|
|
102
|
+
let i = start;
|
|
103
|
+
while (i < text.length && (spacesOnly ? /[ \t]/.test(text[i]) : /\s/.test(text[i]))) i++;
|
|
104
|
+
return i;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function scanBalanced(text: string, start: number, open: string, close: string): number {
|
|
108
|
+
let depth = 0;
|
|
109
|
+
let i = start;
|
|
110
|
+
while (i < text.length) {
|
|
111
|
+
const c = text[i];
|
|
112
|
+
if (c === open) depth++;
|
|
113
|
+
else if (c === close) {
|
|
114
|
+
depth--;
|
|
115
|
+
if (depth === 0) return i + 1;
|
|
116
|
+
}
|
|
117
|
+
i++;
|
|
118
|
+
}
|
|
119
|
+
return i;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function returnTypeStops(stripped: string, index: number, depth: number): boolean {
|
|
123
|
+
const c = stripped[index];
|
|
124
|
+
if (depth === 0 && (c === "{" || c === ";")) return true;
|
|
125
|
+
if (depth !== 0 || c !== "\n") return false;
|
|
126
|
+
const next = skipWhitespace(stripped, index + 1, true);
|
|
127
|
+
return stripped[next] === "{";
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function updateReturnDepth(char: string, depth: number): number {
|
|
131
|
+
if (char === "<" || char === "(" || char === "[") return depth + 1;
|
|
132
|
+
if (char === ">" || char === ")" || char === "]") return depth - 1;
|
|
133
|
+
return depth;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function captureReturnType(stripped: string, source: string, start: number): { endIndex: number; returnType: string } {
|
|
137
|
+
const retStart = skipWhitespace(stripped, start, true);
|
|
138
|
+
if (stripped[retStart] !== ":") return { endIndex: retStart, returnType: "" };
|
|
139
|
+
|
|
140
|
+
let retEnd = retStart + 1;
|
|
141
|
+
let depth = 0;
|
|
142
|
+
while (retEnd < stripped.length) {
|
|
143
|
+
const c = stripped[retEnd];
|
|
144
|
+
if (returnTypeStops(stripped, retEnd, depth)) break;
|
|
145
|
+
depth = updateReturnDepth(c, depth);
|
|
146
|
+
retEnd++;
|
|
147
|
+
}
|
|
148
|
+
return { endIndex: retEnd, returnType: source.slice(retStart, retEnd).trim() };
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function captureSignature(stripped: string, source: string, nameStart: number, name: string): CapturedSig | null {
|
|
152
|
+
let j = skipWhitespace(stripped, nameStart + name.length);
|
|
153
|
+
if (stripped[j] === "<") j = scanBalanced(stripped, j, "<", ">");
|
|
154
|
+
j = skipWhitespace(stripped, j);
|
|
155
|
+
if (stripped[j] !== "(") return null;
|
|
156
|
+
|
|
157
|
+
const parenStart = j;
|
|
158
|
+
j = scanBalanced(stripped, j, "(", ")");
|
|
159
|
+
const parenSegment = source.slice(parenStart, j); // pull from original source for human-readable
|
|
160
|
+
const result = captureReturnType(stripped, source, j);
|
|
161
|
+
const cleanedParens = parenSegment.replace(/\s+/g, " ");
|
|
162
|
+
const sig = name + cleanedParens + (result.returnType ? " " + result.returnType : "");
|
|
163
|
+
return { signature: sig, endIndex: result.endIndex };
|
|
164
|
+
}
|
|
165
|
+
|
|
@@ -109,6 +109,8 @@ export { AI_TOOLS, isInstalled, showMenu, installSelected, installAll, generateC
|
|
|
109
109
|
export type { AiTool } from "./ai.js";
|
|
110
110
|
export { Sso, SSO, SsoError } from "./sso.js";
|
|
111
111
|
export type { SsoOptions } from "./sso.js";
|
|
112
|
+
export { Push, PushError, generateVapidKeys } from "./push.js";
|
|
113
|
+
export type { PushOptions, PushSubscription, PushResult, PushPayload } from "./push.js";
|
|
112
114
|
export { Ai, AiError, AiConfigError, AiHTTPError, AiTimeoutError, AiParseError } from "./aiClient.js";
|
|
113
115
|
export type { ChatResponse, AiMessage, AiChatOptions, AiEmbedOptions, AiEvent, ContentPart, AiMessageContent, AiToolDeclaration, AiToolChoice } from "./aiClient.js";
|
|
114
116
|
export type { ImapMessage, ImapFullMessage, ImapAttachment } from "./messenger.js";
|