@portel/photon-core 1.0.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/LICENSE +21 -0
- package/README.md +403 -0
- package/dist/base.d.ts +58 -0
- package/dist/base.d.ts.map +1 -0
- package/dist/base.js +92 -0
- package/dist/base.js.map +1 -0
- package/dist/dependency-manager.d.ts +49 -0
- package/dist/dependency-manager.d.ts.map +1 -0
- package/dist/dependency-manager.js +165 -0
- package/dist/dependency-manager.js.map +1 -0
- package/dist/index.d.ts +39 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +43 -0
- package/dist/index.js.map +1 -0
- package/dist/schema-extractor.d.ts +110 -0
- package/dist/schema-extractor.d.ts.map +1 -0
- package/dist/schema-extractor.js +727 -0
- package/dist/schema-extractor.js.map +1 -0
- package/dist/types.d.ts +109 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +12 -0
- package/dist/types.js.map +1 -0
- package/package.json +58 -0
- package/src/base.ts +105 -0
- package/src/dependency-manager.ts +206 -0
- package/src/index.ts +46 -0
- package/src/schema-extractor.ts +833 -0
- package/src/types.ts +117 -0
|
@@ -0,0 +1,727 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Schema Extractor
|
|
3
|
+
*
|
|
4
|
+
* Extracts JSON schemas from TypeScript method signatures and JSDoc comments
|
|
5
|
+
* Also extracts constructor parameters for config injection
|
|
6
|
+
* Supports Templates (@Template) and Static resources (@Static)
|
|
7
|
+
*
|
|
8
|
+
* Now uses TypeScript's compiler API for robust type parsing
|
|
9
|
+
*/
|
|
10
|
+
import * as fs from 'fs/promises';
|
|
11
|
+
import * as ts from 'typescript';
|
|
12
|
+
/**
|
|
13
|
+
* Extract schemas from a Photon MCP class file
|
|
14
|
+
*/
|
|
15
|
+
export class SchemaExtractor {
|
|
16
|
+
/**
|
|
17
|
+
* Extract method schemas from source code file
|
|
18
|
+
*/
|
|
19
|
+
async extractFromFile(filePath) {
|
|
20
|
+
try {
|
|
21
|
+
const source = await fs.readFile(filePath, 'utf-8');
|
|
22
|
+
return this.extractFromSource(source);
|
|
23
|
+
}
|
|
24
|
+
catch (error) {
|
|
25
|
+
console.error(`Failed to extract schemas from ${filePath}: ${error.message}`);
|
|
26
|
+
return [];
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Extract all metadata (tools, templates, statics) from source code
|
|
31
|
+
*/
|
|
32
|
+
extractAllFromSource(source) {
|
|
33
|
+
const tools = [];
|
|
34
|
+
const templates = [];
|
|
35
|
+
const statics = [];
|
|
36
|
+
try {
|
|
37
|
+
// If source doesn't contain a class declaration, wrap it in one
|
|
38
|
+
let sourceToParse = source;
|
|
39
|
+
if (!source.includes('class ')) {
|
|
40
|
+
sourceToParse = `export default class Temp {\n${source}\n}`;
|
|
41
|
+
}
|
|
42
|
+
// Parse source file into AST
|
|
43
|
+
const sourceFile = ts.createSourceFile('temp.ts', sourceToParse, ts.ScriptTarget.Latest, true);
|
|
44
|
+
// Helper to process a method declaration
|
|
45
|
+
const processMethod = (member) => {
|
|
46
|
+
const methodName = member.name.getText(sourceFile);
|
|
47
|
+
const jsdoc = this.getJSDocComment(member, sourceFile);
|
|
48
|
+
// Extract parameter type information
|
|
49
|
+
const paramsType = this.getFirstParameterType(member, sourceFile);
|
|
50
|
+
if (!paramsType) {
|
|
51
|
+
return; // Skip methods without proper params
|
|
52
|
+
}
|
|
53
|
+
// Build schema from TypeScript type
|
|
54
|
+
const { properties, required } = this.buildSchemaFromType(paramsType, sourceFile);
|
|
55
|
+
// Extract descriptions from JSDoc
|
|
56
|
+
const paramDocs = this.extractParamDocs(jsdoc);
|
|
57
|
+
const paramConstraints = this.extractParamConstraints(jsdoc);
|
|
58
|
+
// Merge descriptions and constraints into properties
|
|
59
|
+
Object.keys(properties).forEach(key => {
|
|
60
|
+
if (paramDocs.has(key)) {
|
|
61
|
+
properties[key].description = paramDocs.get(key);
|
|
62
|
+
}
|
|
63
|
+
// Apply TypeScript-extracted metadata (before JSDoc, so JSDoc can override)
|
|
64
|
+
if (properties[key]._tsReadOnly) {
|
|
65
|
+
properties[key].readOnly = true;
|
|
66
|
+
delete properties[key]._tsReadOnly; // Clean up internal marker
|
|
67
|
+
}
|
|
68
|
+
// Apply JSDoc constraints (takes precedence over TypeScript)
|
|
69
|
+
if (paramConstraints.has(key)) {
|
|
70
|
+
const constraints = paramConstraints.get(key);
|
|
71
|
+
this.applyConstraints(properties[key], constraints);
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
const description = this.extractDescription(jsdoc);
|
|
75
|
+
const inputSchema = {
|
|
76
|
+
type: 'object',
|
|
77
|
+
properties,
|
|
78
|
+
...(required.length > 0 ? { required } : {}),
|
|
79
|
+
};
|
|
80
|
+
// Check if this is a Template
|
|
81
|
+
if (this.hasTemplateTag(jsdoc)) {
|
|
82
|
+
templates.push({
|
|
83
|
+
name: methodName,
|
|
84
|
+
description,
|
|
85
|
+
inputSchema,
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
// Check if this is a Static resource
|
|
89
|
+
else if (this.hasStaticTag(jsdoc)) {
|
|
90
|
+
const uri = this.extractStaticURI(jsdoc) || `static://${methodName}`;
|
|
91
|
+
const mimeType = this.extractMimeType(jsdoc);
|
|
92
|
+
statics.push({
|
|
93
|
+
name: methodName,
|
|
94
|
+
uri,
|
|
95
|
+
description,
|
|
96
|
+
mimeType,
|
|
97
|
+
inputSchema,
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
// Otherwise, it's a regular tool
|
|
101
|
+
else {
|
|
102
|
+
const format = this.extractFormat(jsdoc);
|
|
103
|
+
tools.push({
|
|
104
|
+
name: methodName,
|
|
105
|
+
description,
|
|
106
|
+
inputSchema,
|
|
107
|
+
...(format ? { format } : {}),
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
};
|
|
111
|
+
// Visit all nodes in the AST
|
|
112
|
+
const visit = (node) => {
|
|
113
|
+
// Look for class declarations
|
|
114
|
+
if (ts.isClassDeclaration(node)) {
|
|
115
|
+
node.members.forEach((member) => {
|
|
116
|
+
// Look for async methods
|
|
117
|
+
if (ts.isMethodDeclaration(member) &&
|
|
118
|
+
member.modifiers?.some(m => m.kind === ts.SyntaxKind.AsyncKeyword)) {
|
|
119
|
+
processMethod(member);
|
|
120
|
+
}
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
ts.forEachChild(node, visit);
|
|
124
|
+
};
|
|
125
|
+
visit(sourceFile);
|
|
126
|
+
}
|
|
127
|
+
catch (error) {
|
|
128
|
+
console.error('Failed to parse TypeScript source:', error.message);
|
|
129
|
+
}
|
|
130
|
+
return { tools, templates, statics };
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* Extract schemas from source code string (backward compatibility)
|
|
134
|
+
*/
|
|
135
|
+
extractFromSource(source) {
|
|
136
|
+
return this.extractAllFromSource(source).tools;
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* Get JSDoc comment for a node
|
|
140
|
+
*/
|
|
141
|
+
getJSDocComment(node, sourceFile) {
|
|
142
|
+
// Use TypeScript's JSDoc extraction
|
|
143
|
+
const jsDocs = node.jsDoc;
|
|
144
|
+
if (jsDocs && jsDocs.length > 0) {
|
|
145
|
+
const jsDoc = jsDocs[0];
|
|
146
|
+
const comment = jsDoc.comment;
|
|
147
|
+
// Get full JSDoc text including tags
|
|
148
|
+
const fullText = sourceFile.getFullText();
|
|
149
|
+
const start = jsDoc.pos;
|
|
150
|
+
const end = jsDoc.end;
|
|
151
|
+
const jsDocText = fullText.substring(start, end);
|
|
152
|
+
// Extract content between /** and */
|
|
153
|
+
const match = jsDocText.match(/\/\*\*([\s\S]*?)\*\//);
|
|
154
|
+
return match ? match[1] : '';
|
|
155
|
+
}
|
|
156
|
+
return '';
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* Get the first parameter's type node
|
|
160
|
+
*/
|
|
161
|
+
getFirstParameterType(method, sourceFile) {
|
|
162
|
+
if (method.parameters.length === 0) {
|
|
163
|
+
return undefined;
|
|
164
|
+
}
|
|
165
|
+
const firstParam = method.parameters[0];
|
|
166
|
+
return firstParam.type;
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* Build JSON schema from TypeScript type node
|
|
170
|
+
* Extracts: type, optional, readonly
|
|
171
|
+
*/
|
|
172
|
+
buildSchemaFromType(typeNode, sourceFile) {
|
|
173
|
+
const properties = {};
|
|
174
|
+
const required = [];
|
|
175
|
+
// Handle type literal (object type)
|
|
176
|
+
if (ts.isTypeLiteralNode(typeNode)) {
|
|
177
|
+
typeNode.members.forEach((member) => {
|
|
178
|
+
if (ts.isPropertySignature(member) && member.name) {
|
|
179
|
+
const propName = member.name.getText(sourceFile);
|
|
180
|
+
const isOptional = member.questionToken !== undefined;
|
|
181
|
+
const isReadonly = member.modifiers?.some(m => m.kind === ts.SyntaxKind.ReadonlyKeyword) || false;
|
|
182
|
+
if (!isOptional) {
|
|
183
|
+
required.push(propName);
|
|
184
|
+
}
|
|
185
|
+
if (member.type) {
|
|
186
|
+
properties[propName] = this.typeNodeToSchema(member.type, sourceFile);
|
|
187
|
+
}
|
|
188
|
+
else {
|
|
189
|
+
properties[propName] = { type: 'object' };
|
|
190
|
+
}
|
|
191
|
+
// Add readonly from TypeScript (JSDoc can override)
|
|
192
|
+
if (isReadonly) {
|
|
193
|
+
properties[propName]._tsReadOnly = true;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
return { properties, required };
|
|
199
|
+
}
|
|
200
|
+
/**
|
|
201
|
+
* Convert TypeScript type node to JSON schema
|
|
202
|
+
*/
|
|
203
|
+
typeNodeToSchema(typeNode, sourceFile) {
|
|
204
|
+
const schema = {};
|
|
205
|
+
// Handle union types
|
|
206
|
+
if (ts.isUnionTypeNode(typeNode)) {
|
|
207
|
+
// Check if this is a union of literals that can be converted to enum
|
|
208
|
+
const enumValues = this.extractEnumFromUnion(typeNode, sourceFile);
|
|
209
|
+
if (enumValues) {
|
|
210
|
+
return enumValues;
|
|
211
|
+
}
|
|
212
|
+
// Otherwise use anyOf
|
|
213
|
+
schema.anyOf = typeNode.types.map(t => this.typeNodeToSchema(t, sourceFile));
|
|
214
|
+
return schema;
|
|
215
|
+
}
|
|
216
|
+
// Handle intersection types
|
|
217
|
+
if (ts.isIntersectionTypeNode(typeNode)) {
|
|
218
|
+
schema.allOf = typeNode.types.map(t => this.typeNodeToSchema(t, sourceFile));
|
|
219
|
+
return schema;
|
|
220
|
+
}
|
|
221
|
+
// Handle array types
|
|
222
|
+
if (ts.isArrayTypeNode(typeNode)) {
|
|
223
|
+
schema.type = 'array';
|
|
224
|
+
schema.items = this.typeNodeToSchema(typeNode.elementType, sourceFile);
|
|
225
|
+
return schema;
|
|
226
|
+
}
|
|
227
|
+
// Handle type reference (e.g., Array<string>)
|
|
228
|
+
if (ts.isTypeReferenceNode(typeNode)) {
|
|
229
|
+
const typeName = typeNode.typeName.getText(sourceFile);
|
|
230
|
+
if (typeName === 'Array' && typeNode.typeArguments && typeNode.typeArguments.length > 0) {
|
|
231
|
+
schema.type = 'array';
|
|
232
|
+
schema.items = this.typeNodeToSchema(typeNode.typeArguments[0], sourceFile);
|
|
233
|
+
return schema;
|
|
234
|
+
}
|
|
235
|
+
// For other type references, default to object
|
|
236
|
+
schema.type = 'object';
|
|
237
|
+
return schema;
|
|
238
|
+
}
|
|
239
|
+
// Handle literal types
|
|
240
|
+
if (ts.isLiteralTypeNode(typeNode)) {
|
|
241
|
+
const literal = typeNode.literal;
|
|
242
|
+
if (ts.isStringLiteral(literal)) {
|
|
243
|
+
schema.type = 'string';
|
|
244
|
+
schema.enum = [literal.text];
|
|
245
|
+
return schema;
|
|
246
|
+
}
|
|
247
|
+
if (ts.isNumericLiteral(literal)) {
|
|
248
|
+
schema.type = 'number';
|
|
249
|
+
schema.enum = [parseFloat(literal.text)];
|
|
250
|
+
return schema;
|
|
251
|
+
}
|
|
252
|
+
if (literal.kind === ts.SyntaxKind.TrueKeyword || literal.kind === ts.SyntaxKind.FalseKeyword) {
|
|
253
|
+
schema.type = 'boolean';
|
|
254
|
+
return schema;
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
// Handle tuple types
|
|
258
|
+
if (ts.isTupleTypeNode(typeNode)) {
|
|
259
|
+
schema.type = 'array';
|
|
260
|
+
schema.items = typeNode.elements.map(e => this.typeNodeToSchema(e, sourceFile));
|
|
261
|
+
return schema;
|
|
262
|
+
}
|
|
263
|
+
// Handle type literal (nested object)
|
|
264
|
+
if (ts.isTypeLiteralNode(typeNode)) {
|
|
265
|
+
schema.type = 'object';
|
|
266
|
+
const { properties, required } = this.buildSchemaFromType(typeNode, sourceFile);
|
|
267
|
+
schema.properties = properties;
|
|
268
|
+
if (required.length > 0) {
|
|
269
|
+
schema.required = required;
|
|
270
|
+
}
|
|
271
|
+
return schema;
|
|
272
|
+
}
|
|
273
|
+
// Handle keyword types (string, number, boolean, etc.)
|
|
274
|
+
const typeText = typeNode.getText(sourceFile);
|
|
275
|
+
switch (typeText) {
|
|
276
|
+
case 'string':
|
|
277
|
+
schema.type = 'string';
|
|
278
|
+
break;
|
|
279
|
+
case 'number':
|
|
280
|
+
schema.type = 'number';
|
|
281
|
+
break;
|
|
282
|
+
case 'boolean':
|
|
283
|
+
schema.type = 'boolean';
|
|
284
|
+
break;
|
|
285
|
+
case 'any':
|
|
286
|
+
case 'unknown':
|
|
287
|
+
// No type restriction
|
|
288
|
+
break;
|
|
289
|
+
default:
|
|
290
|
+
// Default to object for complex types
|
|
291
|
+
schema.type = 'object';
|
|
292
|
+
}
|
|
293
|
+
return schema;
|
|
294
|
+
}
|
|
295
|
+
/**
|
|
296
|
+
* Extract enum values from a union of literal types
|
|
297
|
+
* Returns a proper enum schema if all types are literals of the same kind
|
|
298
|
+
* Returns an optimized anyOf schema for mixed unions (e.g., number | '+1' | '-1')
|
|
299
|
+
* Returns null if the union should use standard anyOf processing
|
|
300
|
+
*/
|
|
301
|
+
extractEnumFromUnion(unionNode, sourceFile) {
|
|
302
|
+
const stringLiterals = [];
|
|
303
|
+
const numberLiterals = [];
|
|
304
|
+
const booleanLiterals = [];
|
|
305
|
+
const nonLiteralTypes = [];
|
|
306
|
+
// Categorize union members
|
|
307
|
+
for (const typeNode of unionNode.types) {
|
|
308
|
+
if (ts.isLiteralTypeNode(typeNode)) {
|
|
309
|
+
const literal = typeNode.literal;
|
|
310
|
+
if (ts.isStringLiteral(literal)) {
|
|
311
|
+
stringLiterals.push(literal.text);
|
|
312
|
+
}
|
|
313
|
+
else if (ts.isNumericLiteral(literal)) {
|
|
314
|
+
numberLiterals.push(parseFloat(literal.text));
|
|
315
|
+
}
|
|
316
|
+
else if (literal.kind === ts.SyntaxKind.TrueKeyword) {
|
|
317
|
+
booleanLiterals.push(true);
|
|
318
|
+
}
|
|
319
|
+
else if (literal.kind === ts.SyntaxKind.FalseKeyword) {
|
|
320
|
+
booleanLiterals.push(false);
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
else {
|
|
324
|
+
nonLiteralTypes.push(typeNode);
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
// Case 1: All same-type literals - return simple enum
|
|
328
|
+
if (nonLiteralTypes.length === 0) {
|
|
329
|
+
if (stringLiterals.length > 0 && numberLiterals.length === 0 && booleanLiterals.length === 0) {
|
|
330
|
+
return {
|
|
331
|
+
type: 'string',
|
|
332
|
+
enum: stringLiterals,
|
|
333
|
+
};
|
|
334
|
+
}
|
|
335
|
+
if (numberLiterals.length > 0 && stringLiterals.length === 0 && booleanLiterals.length === 0) {
|
|
336
|
+
return {
|
|
337
|
+
type: 'number',
|
|
338
|
+
enum: numberLiterals,
|
|
339
|
+
};
|
|
340
|
+
}
|
|
341
|
+
if (booleanLiterals.length > 0 && stringLiterals.length === 0 && numberLiterals.length === 0) {
|
|
342
|
+
return {
|
|
343
|
+
type: 'boolean',
|
|
344
|
+
enum: booleanLiterals,
|
|
345
|
+
};
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
// Case 2: Mixed union with literals - create optimized anyOf
|
|
349
|
+
// Example: number | '+1' | '-1' → anyOf: [{ type: number }, { type: string, enum: ['+1', '-1'] }]
|
|
350
|
+
if (nonLiteralTypes.length > 0 && (stringLiterals.length > 0 || numberLiterals.length > 0 || booleanLiterals.length > 0)) {
|
|
351
|
+
const anyOf = [];
|
|
352
|
+
// Add non-literal types
|
|
353
|
+
for (const typeNode of nonLiteralTypes) {
|
|
354
|
+
anyOf.push(this.typeNodeToSchema(typeNode, sourceFile));
|
|
355
|
+
}
|
|
356
|
+
// Add grouped string literals
|
|
357
|
+
if (stringLiterals.length > 0) {
|
|
358
|
+
anyOf.push({
|
|
359
|
+
type: 'string',
|
|
360
|
+
enum: stringLiterals,
|
|
361
|
+
});
|
|
362
|
+
}
|
|
363
|
+
// Add grouped number literals
|
|
364
|
+
if (numberLiterals.length > 0) {
|
|
365
|
+
anyOf.push({
|
|
366
|
+
type: 'number',
|
|
367
|
+
enum: numberLiterals,
|
|
368
|
+
});
|
|
369
|
+
}
|
|
370
|
+
// Add grouped boolean literals
|
|
371
|
+
if (booleanLiterals.length > 0) {
|
|
372
|
+
anyOf.push({
|
|
373
|
+
type: 'boolean',
|
|
374
|
+
enum: booleanLiterals,
|
|
375
|
+
});
|
|
376
|
+
}
|
|
377
|
+
return { anyOf };
|
|
378
|
+
}
|
|
379
|
+
// Case 3: Complex union or mixed literal types - let standard anyOf handle it
|
|
380
|
+
return null;
|
|
381
|
+
}
|
|
382
|
+
/**
|
|
383
|
+
* Extract constructor parameters for config injection
|
|
384
|
+
*/
|
|
385
|
+
extractConstructorParams(source) {
|
|
386
|
+
const params = [];
|
|
387
|
+
try {
|
|
388
|
+
const sourceFile = ts.createSourceFile('temp.ts', source, ts.ScriptTarget.Latest, true);
|
|
389
|
+
const visit = (node) => {
|
|
390
|
+
if (ts.isClassDeclaration(node)) {
|
|
391
|
+
node.members.forEach((member) => {
|
|
392
|
+
if (ts.isConstructorDeclaration(member)) {
|
|
393
|
+
member.parameters.forEach((param) => {
|
|
394
|
+
if (param.name && ts.isIdentifier(param.name)) {
|
|
395
|
+
const name = param.name.getText(sourceFile);
|
|
396
|
+
const type = param.type ? param.type.getText(sourceFile) : 'any';
|
|
397
|
+
const isOptional = param.questionToken !== undefined || param.initializer !== undefined;
|
|
398
|
+
const hasDefault = param.initializer !== undefined;
|
|
399
|
+
let defaultValue = undefined;
|
|
400
|
+
if (param.initializer) {
|
|
401
|
+
defaultValue = this.extractDefaultValue(param.initializer, sourceFile);
|
|
402
|
+
}
|
|
403
|
+
params.push({
|
|
404
|
+
name,
|
|
405
|
+
type,
|
|
406
|
+
isOptional,
|
|
407
|
+
hasDefault,
|
|
408
|
+
defaultValue,
|
|
409
|
+
});
|
|
410
|
+
}
|
|
411
|
+
});
|
|
412
|
+
}
|
|
413
|
+
});
|
|
414
|
+
}
|
|
415
|
+
ts.forEachChild(node, visit);
|
|
416
|
+
};
|
|
417
|
+
visit(sourceFile);
|
|
418
|
+
}
|
|
419
|
+
catch (error) {
|
|
420
|
+
console.error('Failed to extract constructor params:', error.message);
|
|
421
|
+
}
|
|
422
|
+
return params;
|
|
423
|
+
}
|
|
424
|
+
/**
|
|
425
|
+
* Extract default value from initializer
|
|
426
|
+
*/
|
|
427
|
+
extractDefaultValue(initializer, sourceFile) {
|
|
428
|
+
// String literals
|
|
429
|
+
if (ts.isStringLiteral(initializer)) {
|
|
430
|
+
return initializer.text;
|
|
431
|
+
}
|
|
432
|
+
// Numeric literals
|
|
433
|
+
if (ts.isNumericLiteral(initializer)) {
|
|
434
|
+
return parseFloat(initializer.text);
|
|
435
|
+
}
|
|
436
|
+
// Boolean literals
|
|
437
|
+
if (initializer.kind === ts.SyntaxKind.TrueKeyword) {
|
|
438
|
+
return true;
|
|
439
|
+
}
|
|
440
|
+
if (initializer.kind === ts.SyntaxKind.FalseKeyword) {
|
|
441
|
+
return false;
|
|
442
|
+
}
|
|
443
|
+
// For complex expressions (function calls, etc.), return as string
|
|
444
|
+
return initializer.getText(sourceFile);
|
|
445
|
+
}
|
|
446
|
+
/**
|
|
447
|
+
* Extract main description from JSDoc comment
|
|
448
|
+
*/
|
|
449
|
+
extractDescription(jsdocContent) {
|
|
450
|
+
// Split by @param to get only the description part
|
|
451
|
+
const beforeParams = jsdocContent.split(/@param/)[0];
|
|
452
|
+
// Remove leading * from each line and trim
|
|
453
|
+
const lines = beforeParams
|
|
454
|
+
.split('\n')
|
|
455
|
+
.map((line) => line.trim().replace(/^\*\s?/, ''))
|
|
456
|
+
.filter((line) => line && !line.startsWith('@')); // Exclude @tags and empty lines
|
|
457
|
+
// Take only the last meaningful line (the actual method description)
|
|
458
|
+
// This filters out file headers
|
|
459
|
+
const meaningfulLines = lines.filter(line => line.length > 5); // Filter out short lines
|
|
460
|
+
const description = meaningfulLines.length > 0
|
|
461
|
+
? meaningfulLines[meaningfulLines.length - 1]
|
|
462
|
+
: lines.join(' ');
|
|
463
|
+
// Clean up multiple spaces
|
|
464
|
+
return description.replace(/\s+/g, ' ').trim() || 'No description';
|
|
465
|
+
}
|
|
466
|
+
/**
|
|
467
|
+
* Extract parameter descriptions from JSDoc @param tags
|
|
468
|
+
* Also removes constraint tags from descriptions
|
|
469
|
+
*/
|
|
470
|
+
extractParamDocs(jsdocContent) {
|
|
471
|
+
const paramDocs = new Map();
|
|
472
|
+
const paramRegex = /@param\s+(\w+)\s+(.+)/g;
|
|
473
|
+
let match;
|
|
474
|
+
while ((match = paramRegex.exec(jsdocContent)) !== null) {
|
|
475
|
+
const [, paramName, description] = match;
|
|
476
|
+
// Remove all constraint tags from description
|
|
477
|
+
const cleanDesc = description
|
|
478
|
+
.replace(/\{@min\s+[^}]+\}/g, '')
|
|
479
|
+
.replace(/\{@max\s+[^}]+\}/g, '')
|
|
480
|
+
.replace(/\{@pattern\s+[^}]+\}/g, '')
|
|
481
|
+
.replace(/\{@format\s+[^}]+\}/g, '')
|
|
482
|
+
.replace(/\{@default\s+[^}]+\}/g, '')
|
|
483
|
+
.replace(/\{@unique(?:Items)?\s*\}/g, '')
|
|
484
|
+
.replace(/\{@example\s+[^}]+\}/g, '')
|
|
485
|
+
.replace(/\{@multipleOf\s+[^}]+\}/g, '')
|
|
486
|
+
.replace(/\{@deprecated(?:\s+[^}]+)?\}/g, '')
|
|
487
|
+
.replace(/\{@readOnly\s*\}/g, '')
|
|
488
|
+
.replace(/\{@writeOnly\s*\}/g, '')
|
|
489
|
+
.replace(/\s+/g, ' ') // Collapse multiple spaces
|
|
490
|
+
.trim();
|
|
491
|
+
paramDocs.set(paramName, cleanDesc);
|
|
492
|
+
}
|
|
493
|
+
return paramDocs;
|
|
494
|
+
}
|
|
495
|
+
/**
|
|
496
|
+
* Extract parameter constraints from JSDoc @param tags
|
|
497
|
+
* Supports inline tags: {@min}, {@max}, {@pattern}, {@format}, {@default}, {@unique},
|
|
498
|
+
* {@example}, {@multipleOf}, {@deprecated}, {@readOnly}, {@writeOnly}
|
|
499
|
+
*/
|
|
500
|
+
extractParamConstraints(jsdocContent) {
|
|
501
|
+
const constraints = new Map();
|
|
502
|
+
const paramRegex = /@param\s+(\w+)\s+(.+)/g;
|
|
503
|
+
let match;
|
|
504
|
+
while ((match = paramRegex.exec(jsdocContent)) !== null) {
|
|
505
|
+
const [, paramName, description] = match;
|
|
506
|
+
const paramConstraints = {};
|
|
507
|
+
// Extract {@min value} - works for numbers, strings, arrays
|
|
508
|
+
const minMatch = description.match(/\{@min\s+(-?\d+(?:\.\d+)?)\}/);
|
|
509
|
+
if (minMatch) {
|
|
510
|
+
paramConstraints.min = parseFloat(minMatch[1]);
|
|
511
|
+
}
|
|
512
|
+
// Extract {@max value} - works for numbers, strings, arrays
|
|
513
|
+
const maxMatch = description.match(/\{@max\s+(-?\d+(?:\.\d+)?)\}/);
|
|
514
|
+
if (maxMatch) {
|
|
515
|
+
paramConstraints.max = parseFloat(maxMatch[1]);
|
|
516
|
+
}
|
|
517
|
+
// Extract {@pattern regex} - use lookahead to match until tag-closing }
|
|
518
|
+
const patternMatch = description.match(/\{@pattern\s+(.+?)\}(?=\s|$|{@)/);
|
|
519
|
+
if (patternMatch) {
|
|
520
|
+
paramConstraints.pattern = patternMatch[1].trim();
|
|
521
|
+
}
|
|
522
|
+
// Extract {@format formatName} - use lookahead to match until tag-closing }
|
|
523
|
+
const formatMatch = description.match(/\{@format\s+(.+?)\}(?=\s|$|{@)/);
|
|
524
|
+
if (formatMatch) {
|
|
525
|
+
paramConstraints.format = formatMatch[1].trim();
|
|
526
|
+
}
|
|
527
|
+
// Extract {@default value} - use lookahead to match until tag-closing }
|
|
528
|
+
const defaultMatch = description.match(/\{@default\s+(.+?)\}(?=\s|$|{@)/);
|
|
529
|
+
if (defaultMatch) {
|
|
530
|
+
const defaultValue = defaultMatch[1].trim();
|
|
531
|
+
// Try to parse as JSON for numbers, booleans, objects, arrays
|
|
532
|
+
try {
|
|
533
|
+
paramConstraints.default = JSON.parse(defaultValue);
|
|
534
|
+
}
|
|
535
|
+
catch {
|
|
536
|
+
// If not valid JSON, use as string
|
|
537
|
+
paramConstraints.default = defaultValue;
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
// Extract {@unique} or {@uniqueItems} - for arrays
|
|
541
|
+
if (description.match(/\{@unique(?:Items)?\s*\}/)) {
|
|
542
|
+
paramConstraints.unique = true;
|
|
543
|
+
}
|
|
544
|
+
// Extract {@example value} - supports multiple examples, use lookahead
|
|
545
|
+
const exampleMatches = description.matchAll(/\{@example\s+(.+?)\}(?=\s|$|{@)/g);
|
|
546
|
+
const examples = [];
|
|
547
|
+
for (const exampleMatch of exampleMatches) {
|
|
548
|
+
const exampleValue = exampleMatch[1].trim();
|
|
549
|
+
// Try to parse as JSON
|
|
550
|
+
try {
|
|
551
|
+
examples.push(JSON.parse(exampleValue));
|
|
552
|
+
}
|
|
553
|
+
catch {
|
|
554
|
+
// If not valid JSON, use as string
|
|
555
|
+
examples.push(exampleValue);
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
if (examples.length > 0) {
|
|
559
|
+
paramConstraints.examples = examples;
|
|
560
|
+
}
|
|
561
|
+
// Extract {@multipleOf value} - for numbers
|
|
562
|
+
const multipleOfMatch = description.match(/\{@multipleOf\s+(-?\d+(?:\.\d+)?)\}/);
|
|
563
|
+
if (multipleOfMatch) {
|
|
564
|
+
paramConstraints.multipleOf = parseFloat(multipleOfMatch[1]);
|
|
565
|
+
}
|
|
566
|
+
// Extract {@deprecated message} - use lookahead to match until tag-closing }
|
|
567
|
+
const deprecatedMatch = description.match(/\{@deprecated(?:\s+(.+?))?\}(?=\s|$|{@)/);
|
|
568
|
+
if (deprecatedMatch) {
|
|
569
|
+
paramConstraints.deprecated = deprecatedMatch[1]?.trim() || true;
|
|
570
|
+
}
|
|
571
|
+
// Extract {@readOnly} and {@writeOnly} - track which comes last
|
|
572
|
+
// They are mutually exclusive, so last one wins
|
|
573
|
+
const readOnlyMatch = description.match(/\{@readOnly\s*\}/);
|
|
574
|
+
const writeOnlyMatch = description.match(/\{@writeOnly\s*\}/);
|
|
575
|
+
if (readOnlyMatch || writeOnlyMatch) {
|
|
576
|
+
// Find positions to determine which comes last
|
|
577
|
+
const readOnlyPos = readOnlyMatch ? description.indexOf(readOnlyMatch[0]) : -1;
|
|
578
|
+
const writeOnlyPos = writeOnlyMatch ? description.indexOf(writeOnlyMatch[0]) : -1;
|
|
579
|
+
if (readOnlyPos > writeOnlyPos) {
|
|
580
|
+
paramConstraints.readOnly = true;
|
|
581
|
+
paramConstraints.writeOnly = false; // Explicitly clear the other
|
|
582
|
+
}
|
|
583
|
+
else if (writeOnlyPos > readOnlyPos) {
|
|
584
|
+
paramConstraints.writeOnly = true;
|
|
585
|
+
paramConstraints.readOnly = false; // Explicitly clear the other
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
if (Object.keys(paramConstraints).length > 0) {
|
|
589
|
+
constraints.set(paramName, paramConstraints);
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
return constraints;
|
|
593
|
+
}
|
|
594
|
+
/**
|
|
595
|
+
* Apply constraints to a schema property based on type
|
|
596
|
+
* Handles: min/max (contextual), pattern, format, default, unique,
|
|
597
|
+
* examples, multipleOf, deprecated, readOnly, writeOnly
|
|
598
|
+
* Works with both simple types and anyOf schemas
|
|
599
|
+
*/
|
|
600
|
+
applyConstraints(schema, constraints) {
|
|
601
|
+
// Helper to apply constraints to a single schema based on type
|
|
602
|
+
const applyToSchema = (s) => {
|
|
603
|
+
if (s.enum) {
|
|
604
|
+
// Skip enum types for most constraints (but still apply deprecated, examples, etc.)
|
|
605
|
+
if (constraints.examples !== undefined) {
|
|
606
|
+
s.examples = constraints.examples;
|
|
607
|
+
}
|
|
608
|
+
if (constraints.deprecated !== undefined) {
|
|
609
|
+
s.deprecated = constraints.deprecated === true ? true : constraints.deprecated;
|
|
610
|
+
}
|
|
611
|
+
return;
|
|
612
|
+
}
|
|
613
|
+
// Apply min/max based on type
|
|
614
|
+
if (s.type === 'number') {
|
|
615
|
+
if (constraints.min !== undefined) {
|
|
616
|
+
s.minimum = constraints.min;
|
|
617
|
+
}
|
|
618
|
+
if (constraints.max !== undefined) {
|
|
619
|
+
s.maximum = constraints.max;
|
|
620
|
+
}
|
|
621
|
+
if (constraints.multipleOf !== undefined) {
|
|
622
|
+
s.multipleOf = constraints.multipleOf;
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
else if (s.type === 'string') {
|
|
626
|
+
if (constraints.min !== undefined) {
|
|
627
|
+
s.minLength = constraints.min;
|
|
628
|
+
}
|
|
629
|
+
if (constraints.max !== undefined) {
|
|
630
|
+
s.maxLength = constraints.max;
|
|
631
|
+
}
|
|
632
|
+
if (constraints.pattern !== undefined) {
|
|
633
|
+
s.pattern = constraints.pattern;
|
|
634
|
+
}
|
|
635
|
+
if (constraints.format !== undefined) {
|
|
636
|
+
s.format = constraints.format;
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
else if (s.type === 'array') {
|
|
640
|
+
if (constraints.min !== undefined) {
|
|
641
|
+
s.minItems = constraints.min;
|
|
642
|
+
}
|
|
643
|
+
if (constraints.max !== undefined) {
|
|
644
|
+
s.maxItems = constraints.max;
|
|
645
|
+
}
|
|
646
|
+
if (constraints.unique === true) {
|
|
647
|
+
s.uniqueItems = true;
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
// Apply type-agnostic constraints
|
|
651
|
+
if (constraints.default !== undefined) {
|
|
652
|
+
s.default = constraints.default;
|
|
653
|
+
}
|
|
654
|
+
if (constraints.examples !== undefined) {
|
|
655
|
+
s.examples = constraints.examples;
|
|
656
|
+
}
|
|
657
|
+
if (constraints.deprecated !== undefined) {
|
|
658
|
+
s.deprecated = constraints.deprecated === true ? true : constraints.deprecated;
|
|
659
|
+
}
|
|
660
|
+
// readOnly and writeOnly are mutually exclusive
|
|
661
|
+
// JSDoc takes precedence over TypeScript
|
|
662
|
+
if (constraints.readOnly === true) {
|
|
663
|
+
s.readOnly = true;
|
|
664
|
+
delete s.writeOnly; // Clear writeOnly if readOnly is set
|
|
665
|
+
}
|
|
666
|
+
if (constraints.writeOnly === true) {
|
|
667
|
+
s.writeOnly = true;
|
|
668
|
+
delete s.readOnly; // Clear readOnly if writeOnly is set
|
|
669
|
+
}
|
|
670
|
+
};
|
|
671
|
+
// Apply to anyOf schemas or direct schema
|
|
672
|
+
if (schema.anyOf) {
|
|
673
|
+
schema.anyOf.forEach(applyToSchema);
|
|
674
|
+
// For deprecated/examples that apply to the whole property (not individual types)
|
|
675
|
+
// Apply them at the top level too
|
|
676
|
+
if (constraints.deprecated !== undefined) {
|
|
677
|
+
schema.deprecated = constraints.deprecated === true ? true : constraints.deprecated;
|
|
678
|
+
}
|
|
679
|
+
if (constraints.examples !== undefined) {
|
|
680
|
+
schema.examples = constraints.examples;
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
else {
|
|
684
|
+
applyToSchema(schema);
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
/**
|
|
688
|
+
* Check if JSDoc contains @Template tag
|
|
689
|
+
*/
|
|
690
|
+
hasTemplateTag(jsdocContent) {
|
|
691
|
+
return /@Template/i.test(jsdocContent);
|
|
692
|
+
}
|
|
693
|
+
/**
|
|
694
|
+
* Check if JSDoc contains @Static tag
|
|
695
|
+
*/
|
|
696
|
+
hasStaticTag(jsdocContent) {
|
|
697
|
+
return /@Static/i.test(jsdocContent);
|
|
698
|
+
}
|
|
699
|
+
/**
|
|
700
|
+
* Extract URI pattern from @Static tag
|
|
701
|
+
* Example: @Static github://repos/{owner}/{repo}/readme
|
|
702
|
+
*/
|
|
703
|
+
extractStaticURI(jsdocContent) {
|
|
704
|
+
const match = jsdocContent.match(/@Static\s+([\w:\/\{\}\-_.]+)/i);
|
|
705
|
+
return match ? match[1].trim() : null;
|
|
706
|
+
}
|
|
707
|
+
/**
|
|
708
|
+
* Extract format hint from @format tag
|
|
709
|
+
* Example: @format table
|
|
710
|
+
*/
|
|
711
|
+
extractFormat(jsdocContent) {
|
|
712
|
+
const match = jsdocContent.match(/@format\s+(primitive|table|tree|list|none)/i);
|
|
713
|
+
if (match) {
|
|
714
|
+
return match[1].toLowerCase();
|
|
715
|
+
}
|
|
716
|
+
return undefined;
|
|
717
|
+
}
|
|
718
|
+
/**
|
|
719
|
+
* Extract MIME type from @mimeType tag
|
|
720
|
+
* Example: @mimeType text/markdown
|
|
721
|
+
*/
|
|
722
|
+
extractMimeType(jsdocContent) {
|
|
723
|
+
const match = jsdocContent.match(/@mimeType\s+([\w\/\-+.]+)/i);
|
|
724
|
+
return match ? match[1].trim() : undefined;
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
//# sourceMappingURL=schema-extractor.js.map
|