@openwebf/webf 0.22.6 → 0.22.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/analyzer.js +117 -17
- package/dist/react.js +47 -2
- package/package.json +3 -1
- package/src/analyzer.ts +145 -10
- package/src/declaration.ts +2 -0
- package/src/react.ts +53 -4
- package/templates/react.component.tsx.tpl +62 -6
- package/templates/vue.component.partial.tpl +3 -0
- package/test/react.test.ts +62 -1
- package/test/standard-props.test.ts +190 -0
- package/test/union-types-jsdoc.test.ts +168 -0
- package/test/vue.test.ts +157 -0
- package/dist/analyzer_original.js +0 -467
package/dist/analyzer.js
CHANGED
|
@@ -37,12 +37,15 @@ exports.analyzer = analyzer;
|
|
|
37
37
|
exports.buildClassRelationship = buildClassRelationship;
|
|
38
38
|
exports.clearCaches = clearCaches;
|
|
39
39
|
const typescript_1 = __importStar(require("typescript"));
|
|
40
|
+
const tsdoc_1 = require("@microsoft/tsdoc");
|
|
40
41
|
const declaration_1 = require("./declaration");
|
|
41
42
|
const utils_1 = require("./utils");
|
|
42
43
|
// Cache for parsed source files to avoid re-parsing
|
|
43
44
|
const sourceFileCache = new Map();
|
|
44
45
|
// Cache for type conversions to avoid redundant processing
|
|
45
46
|
const typeConversionCache = new Map();
|
|
47
|
+
// TSDoc parser instance
|
|
48
|
+
const tsdocParser = new tsdoc_1.TSDocParser();
|
|
46
49
|
// Type mapping constants for better performance
|
|
47
50
|
const BASIC_TYPE_MAP = {
|
|
48
51
|
[typescript_1.default.SyntaxKind.StringKeyword]: declaration_1.FunctionArgumentType.dom_string,
|
|
@@ -72,7 +75,7 @@ function analyzer(blob, definedPropertyCollector, unionTypeCollector) {
|
|
|
72
75
|
blob.objects = sourceFile.statements
|
|
73
76
|
.map(statement => {
|
|
74
77
|
try {
|
|
75
|
-
return walkProgram(blob, statement, definedPropertyCollector, unionTypeCollector);
|
|
78
|
+
return walkProgram(blob, statement, sourceFile, definedPropertyCollector, unionTypeCollector);
|
|
76
79
|
}
|
|
77
80
|
catch (error) {
|
|
78
81
|
console.error(`Error processing statement in ${blob.source}:`, error);
|
|
@@ -106,6 +109,93 @@ function getInterfaceName(statement) {
|
|
|
106
109
|
}
|
|
107
110
|
return statement.name.escapedText;
|
|
108
111
|
}
|
|
112
|
+
function getJSDocComment(node, sourceFile) {
|
|
113
|
+
const sourceText = sourceFile.getFullText();
|
|
114
|
+
const nodeStart = node.getFullStart();
|
|
115
|
+
const nodePos = node.getStart(sourceFile);
|
|
116
|
+
// Get the text between full start and actual start (includes leading trivia)
|
|
117
|
+
const leadingText = sourceText.substring(nodeStart, nodePos);
|
|
118
|
+
// Find JSDoc comment in the leading text
|
|
119
|
+
const jsDocMatch = leadingText.match(/\/\*\*([\s\S]*?)\*\/\s*$/);
|
|
120
|
+
if (!jsDocMatch)
|
|
121
|
+
return undefined;
|
|
122
|
+
// Extract the full JSDoc comment including delimiters
|
|
123
|
+
const commentText = jsDocMatch[0];
|
|
124
|
+
const commentStartPos = nodeStart + leadingText.lastIndexOf(commentText);
|
|
125
|
+
// Create a TextRange for the comment
|
|
126
|
+
const textRange = tsdoc_1.TextRange.fromStringRange(sourceText, commentStartPos, commentStartPos + commentText.length);
|
|
127
|
+
// Parse the JSDoc comment using TSDoc
|
|
128
|
+
const parserContext = tsdocParser.parseRange(textRange);
|
|
129
|
+
const docComment = parserContext.docComment;
|
|
130
|
+
// For now, always use the raw comment to preserve all tags including @default
|
|
131
|
+
// TSDoc parser doesn't handle @default tags properly out of the box
|
|
132
|
+
// Fallback to raw comment if TSDoc parsing fails
|
|
133
|
+
const comment = jsDocMatch[1]
|
|
134
|
+
.split('\n')
|
|
135
|
+
.map(line => line.replace(/^\s*\*\s?/, ''))
|
|
136
|
+
.join('\n')
|
|
137
|
+
.trim();
|
|
138
|
+
return comment || undefined;
|
|
139
|
+
}
|
|
140
|
+
// Helper function to render TSDoc nodes to string
|
|
141
|
+
function renderDocNodes(nodes) {
|
|
142
|
+
return nodes.map(node => {
|
|
143
|
+
if (node.kind === 'PlainText') {
|
|
144
|
+
return node.text;
|
|
145
|
+
}
|
|
146
|
+
else if (node.kind === 'SoftBreak') {
|
|
147
|
+
return '\n';
|
|
148
|
+
}
|
|
149
|
+
else if (node.kind === 'Paragraph') {
|
|
150
|
+
return renderDocNodes(node.nodes);
|
|
151
|
+
}
|
|
152
|
+
return '';
|
|
153
|
+
}).join('').trim();
|
|
154
|
+
}
|
|
155
|
+
// Special function to get the first JSDoc comment in a file for the first interface
|
|
156
|
+
function getFirstInterfaceJSDoc(statement, sourceFile) {
|
|
157
|
+
// Find all interfaces in the file
|
|
158
|
+
const interfaces = [];
|
|
159
|
+
typescript_1.default.forEachChild(sourceFile, child => {
|
|
160
|
+
if (typescript_1.default.isInterfaceDeclaration(child)) {
|
|
161
|
+
interfaces.push(child);
|
|
162
|
+
}
|
|
163
|
+
});
|
|
164
|
+
// If this is the first interface, check for a file-level JSDoc
|
|
165
|
+
if (interfaces.length > 0 && interfaces[0] === statement) {
|
|
166
|
+
const sourceText = sourceFile.getFullText();
|
|
167
|
+
const firstInterfacePos = statement.getFullStart();
|
|
168
|
+
// Get all text before the first interface
|
|
169
|
+
const textBeforeInterface = sourceText.substring(0, firstInterfacePos);
|
|
170
|
+
// Find the last JSDoc comment before the interface
|
|
171
|
+
const jsDocMatches = textBeforeInterface.match(/\/\*\*([\s\S]*?)\*\//g);
|
|
172
|
+
if (jsDocMatches && jsDocMatches.length > 0) {
|
|
173
|
+
const lastJsDoc = jsDocMatches[jsDocMatches.length - 1];
|
|
174
|
+
const commentStartPos = textBeforeInterface.lastIndexOf(lastJsDoc);
|
|
175
|
+
// Create a TextRange for the comment
|
|
176
|
+
const textRange = tsdoc_1.TextRange.fromStringRange(sourceText, commentStartPos, commentStartPos + lastJsDoc.length);
|
|
177
|
+
// Parse the JSDoc comment using TSDoc
|
|
178
|
+
const parserContext = tsdocParser.parseRange(textRange);
|
|
179
|
+
const docComment = parserContext.docComment;
|
|
180
|
+
// Extract the parsed content
|
|
181
|
+
if (docComment.summarySection) {
|
|
182
|
+
const summary = renderDocNodes(docComment.summarySection.nodes);
|
|
183
|
+
if (summary)
|
|
184
|
+
return summary;
|
|
185
|
+
}
|
|
186
|
+
// Fallback to raw comment
|
|
187
|
+
const comment = lastJsDoc
|
|
188
|
+
.replace(/^\/\*\*/, '')
|
|
189
|
+
.replace(/\*\/$/, '')
|
|
190
|
+
.split('\n')
|
|
191
|
+
.map(line => line.replace(/^\s*\*\s?/, ''))
|
|
192
|
+
.join('\n')
|
|
193
|
+
.trim();
|
|
194
|
+
return comment || undefined;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
return undefined;
|
|
198
|
+
}
|
|
109
199
|
function getHeritageType(heritage) {
|
|
110
200
|
if (!heritage.types.length)
|
|
111
201
|
return null;
|
|
@@ -210,11 +300,15 @@ function getParameterBaseType(type, mode) {
|
|
|
210
300
|
}
|
|
211
301
|
}
|
|
212
302
|
if (type.kind === typescript_1.default.SyntaxKind.LiteralType) {
|
|
213
|
-
// Handle literal types
|
|
303
|
+
// Handle literal types
|
|
214
304
|
const literalType = type;
|
|
215
305
|
if (literalType.literal.kind === typescript_1.default.SyntaxKind.NullKeyword) {
|
|
216
306
|
return declaration_1.FunctionArgumentType.null;
|
|
217
307
|
}
|
|
308
|
+
if (literalType.literal.kind === typescript_1.default.SyntaxKind.StringLiteral) {
|
|
309
|
+
// Return the string literal value itself
|
|
310
|
+
return literalType.literal.text;
|
|
311
|
+
}
|
|
218
312
|
return declaration_1.FunctionArgumentType.any;
|
|
219
313
|
}
|
|
220
314
|
return declaration_1.FunctionArgumentType.any;
|
|
@@ -379,10 +473,10 @@ function isParamsReadOnly(m) {
|
|
|
379
473
|
return false;
|
|
380
474
|
return m.modifiers.some(k => k.kind === typescript_1.default.SyntaxKind.ReadonlyKeyword);
|
|
381
475
|
}
|
|
382
|
-
function walkProgram(blob, statement, definedPropertyCollector, unionTypeCollector) {
|
|
476
|
+
function walkProgram(blob, statement, sourceFile, definedPropertyCollector, unionTypeCollector) {
|
|
383
477
|
switch (statement.kind) {
|
|
384
478
|
case typescript_1.default.SyntaxKind.InterfaceDeclaration:
|
|
385
|
-
return processInterfaceDeclaration(statement, blob, definedPropertyCollector, unionTypeCollector);
|
|
479
|
+
return processInterfaceDeclaration(statement, blob, sourceFile, definedPropertyCollector, unionTypeCollector);
|
|
386
480
|
case typescript_1.default.SyntaxKind.VariableStatement:
|
|
387
481
|
return processVariableStatement(statement, unionTypeCollector);
|
|
388
482
|
case typescript_1.default.SyntaxKind.TypeAliasDeclaration:
|
|
@@ -399,9 +493,13 @@ function processTypeAliasDeclaration(statement, blob) {
|
|
|
399
493
|
typeAlias.type = printer.printNode(typescript_1.default.EmitHint.Unspecified, statement.type, statement.getSourceFile());
|
|
400
494
|
return typeAlias;
|
|
401
495
|
}
|
|
402
|
-
function processInterfaceDeclaration(statement, blob, definedPropertyCollector, unionTypeCollector) {
|
|
496
|
+
function processInterfaceDeclaration(statement, blob, sourceFile, definedPropertyCollector, unionTypeCollector) {
|
|
403
497
|
const interfaceName = statement.name.escapedText.toString();
|
|
404
498
|
const obj = new declaration_1.ClassObject();
|
|
499
|
+
// Capture JSDoc comment for the interface
|
|
500
|
+
const directComment = getJSDocComment(statement, sourceFile);
|
|
501
|
+
const fileComment = getFirstInterfaceJSDoc(statement, sourceFile);
|
|
502
|
+
obj.documentation = directComment || fileComment;
|
|
405
503
|
// Process heritage clauses
|
|
406
504
|
if (statement.heritageClauses) {
|
|
407
505
|
const heritage = statement.heritageClauses[0];
|
|
@@ -420,40 +518,41 @@ function processInterfaceDeclaration(statement, blob, definedPropertyCollector,
|
|
|
420
518
|
}
|
|
421
519
|
// Process members in batches for better performance
|
|
422
520
|
const members = Array.from(statement.members);
|
|
423
|
-
processMembersBatch(members, obj, definedPropertyCollector, unionTypeCollector);
|
|
521
|
+
processMembersBatch(members, obj, sourceFile, definedPropertyCollector, unionTypeCollector);
|
|
424
522
|
declaration_1.ClassObject.globalClassMap[interfaceName] = obj;
|
|
425
523
|
return obj;
|
|
426
524
|
}
|
|
427
|
-
function processMembersBatch(members, obj, definedPropertyCollector, unionTypeCollector) {
|
|
525
|
+
function processMembersBatch(members, obj, sourceFile, definedPropertyCollector, unionTypeCollector) {
|
|
428
526
|
for (const member of members) {
|
|
429
527
|
try {
|
|
430
|
-
processMember(member, obj, definedPropertyCollector, unionTypeCollector);
|
|
528
|
+
processMember(member, obj, sourceFile, definedPropertyCollector, unionTypeCollector);
|
|
431
529
|
}
|
|
432
530
|
catch (error) {
|
|
433
531
|
console.error(`Error processing member:`, error);
|
|
434
532
|
}
|
|
435
533
|
}
|
|
436
534
|
}
|
|
437
|
-
function processMember(member, obj, definedPropertyCollector, unionTypeCollector) {
|
|
535
|
+
function processMember(member, obj, sourceFile, definedPropertyCollector, unionTypeCollector) {
|
|
438
536
|
switch (member.kind) {
|
|
439
537
|
case typescript_1.default.SyntaxKind.PropertySignature:
|
|
440
|
-
processPropertySignature(member, obj, definedPropertyCollector, unionTypeCollector);
|
|
538
|
+
processPropertySignature(member, obj, sourceFile, definedPropertyCollector, unionTypeCollector);
|
|
441
539
|
break;
|
|
442
540
|
case typescript_1.default.SyntaxKind.MethodSignature:
|
|
443
|
-
processMethodSignature(member, obj, unionTypeCollector);
|
|
541
|
+
processMethodSignature(member, obj, sourceFile, unionTypeCollector);
|
|
444
542
|
break;
|
|
445
543
|
case typescript_1.default.SyntaxKind.IndexSignature:
|
|
446
|
-
processIndexSignature(member, obj, unionTypeCollector);
|
|
544
|
+
processIndexSignature(member, obj, sourceFile, unionTypeCollector);
|
|
447
545
|
break;
|
|
448
546
|
case typescript_1.default.SyntaxKind.ConstructSignature:
|
|
449
|
-
processConstructSignature(member, obj, unionTypeCollector);
|
|
547
|
+
processConstructSignature(member, obj, sourceFile, unionTypeCollector);
|
|
450
548
|
break;
|
|
451
549
|
}
|
|
452
550
|
}
|
|
453
|
-
function processPropertySignature(member, obj, definedPropertyCollector, unionTypeCollector) {
|
|
551
|
+
function processPropertySignature(member, obj, sourceFile, definedPropertyCollector, unionTypeCollector) {
|
|
454
552
|
const prop = new declaration_1.PropsDeclaration();
|
|
455
553
|
prop.name = getPropName(member.name, prop);
|
|
456
554
|
prop.readonly = isParamsReadOnly(member);
|
|
555
|
+
prop.documentation = getJSDocComment(member, sourceFile);
|
|
457
556
|
definedPropertyCollector.properties.add(prop.name);
|
|
458
557
|
if (!member.type) {
|
|
459
558
|
console.warn(`Property ${prop.name} has no type annotation`);
|
|
@@ -491,10 +590,11 @@ function createAsyncProperty(prop, mode) {
|
|
|
491
590
|
};
|
|
492
591
|
return asyncProp;
|
|
493
592
|
}
|
|
494
|
-
function processMethodSignature(member, obj, unionTypeCollector) {
|
|
593
|
+
function processMethodSignature(member, obj, sourceFile, unionTypeCollector) {
|
|
495
594
|
var _a;
|
|
496
595
|
const f = new declaration_1.FunctionDeclaration();
|
|
497
596
|
f.name = getPropName(member.name);
|
|
597
|
+
f.documentation = getJSDocComment(member, sourceFile);
|
|
498
598
|
f.args = member.parameters.map(params => paramsNodeToArguments(params, unionTypeCollector));
|
|
499
599
|
if (member.type) {
|
|
500
600
|
const mode = new declaration_1.ParameterMode();
|
|
@@ -531,7 +631,7 @@ function createAsyncMethod(member, originalFunc, unionTypeCollector) {
|
|
|
531
631
|
};
|
|
532
632
|
return asyncFunc;
|
|
533
633
|
}
|
|
534
|
-
function processIndexSignature(member, obj, unionTypeCollector) {
|
|
634
|
+
function processIndexSignature(member, obj, sourceFile, unionTypeCollector) {
|
|
535
635
|
const prop = new declaration_1.IndexedPropertyDeclaration();
|
|
536
636
|
const modifier = member.modifiers;
|
|
537
637
|
prop.readonly = !!(modifier && modifier[0].kind === typescript_1.default.SyntaxKind.ReadonlyKeyword);
|
|
@@ -544,7 +644,7 @@ function processIndexSignature(member, obj, unionTypeCollector) {
|
|
|
544
644
|
prop.typeMode = mode;
|
|
545
645
|
obj.indexedProp = prop;
|
|
546
646
|
}
|
|
547
|
-
function processConstructSignature(member, obj, unionTypeCollector) {
|
|
647
|
+
function processConstructSignature(member, obj, sourceFile, unionTypeCollector) {
|
|
548
648
|
const c = new declaration_1.FunctionDeclaration();
|
|
549
649
|
c.name = 'constructor';
|
|
550
650
|
c.args = member.parameters.map(params => paramsNodeToArguments(params, unionTypeCollector));
|
package/dist/react.js
CHANGED
|
@@ -15,6 +15,9 @@ function readTemplate(name) {
|
|
|
15
15
|
return fs_1.default.readFileSync(path_1.default.join(__dirname, '../templates/' + name + '.tpl'), { encoding: 'utf-8' });
|
|
16
16
|
}
|
|
17
17
|
function generateReturnType(type) {
|
|
18
|
+
if ((0, utils_1.isUnionType)(type)) {
|
|
19
|
+
return type.value.map(v => `'${v.value}'`).join(' | ');
|
|
20
|
+
}
|
|
18
21
|
if ((0, utils_1.isPointerType)(type)) {
|
|
19
22
|
const pointerType = (0, utils_1.getPointerType)(type);
|
|
20
23
|
return pointerType;
|
|
@@ -79,6 +82,19 @@ function generateMethodDeclaration(method) {
|
|
|
79
82
|
var returnType = generateReturnType(method.returnType);
|
|
80
83
|
return `${methodName}(${args}): ${returnType};`;
|
|
81
84
|
}
|
|
85
|
+
function generateMethodDeclarationWithDocs(method, indent = '') {
|
|
86
|
+
let result = '';
|
|
87
|
+
if (method.documentation) {
|
|
88
|
+
result += `${indent}/**\n`;
|
|
89
|
+
const docLines = method.documentation.split('\n');
|
|
90
|
+
docLines.forEach(line => {
|
|
91
|
+
result += `${indent} * ${line}\n`;
|
|
92
|
+
});
|
|
93
|
+
result += `${indent} */\n`;
|
|
94
|
+
}
|
|
95
|
+
result += `${indent}${generateMethodDeclaration(method)}`;
|
|
96
|
+
return result;
|
|
97
|
+
}
|
|
82
98
|
function toReactEventName(name) {
|
|
83
99
|
const eventName = 'on-' + name;
|
|
84
100
|
return lodash_1.default.camelCase(eventName);
|
|
@@ -110,9 +126,13 @@ function generateReactComponent(blob, packageName, relativeDir) {
|
|
|
110
126
|
const events = classObjects.filter(object => {
|
|
111
127
|
return object.name.endsWith('Events');
|
|
112
128
|
});
|
|
129
|
+
const methods = classObjects.filter(object => {
|
|
130
|
+
return object.name.endsWith('Methods');
|
|
131
|
+
});
|
|
113
132
|
const others = classObjects.filter(object => {
|
|
114
133
|
return !object.name.endsWith('Properties')
|
|
115
|
-
&& !object.name.endsWith('Events')
|
|
134
|
+
&& !object.name.endsWith('Events')
|
|
135
|
+
&& !object.name.endsWith('Methods');
|
|
116
136
|
});
|
|
117
137
|
// Include type aliases
|
|
118
138
|
const typeAliasDeclarations = typeAliases.map(typeAlias => {
|
|
@@ -120,6 +140,19 @@ function generateReactComponent(blob, packageName, relativeDir) {
|
|
|
120
140
|
}).join('\n');
|
|
121
141
|
const dependencies = [
|
|
122
142
|
typeAliasDeclarations,
|
|
143
|
+
// Include Methods interfaces as dependencies
|
|
144
|
+
methods.map(object => {
|
|
145
|
+
const methodDeclarations = object.methods.map(method => {
|
|
146
|
+
return generateMethodDeclarationWithDocs(method, ' ');
|
|
147
|
+
}).join('\n');
|
|
148
|
+
let interfaceDoc = '';
|
|
149
|
+
if (object.documentation) {
|
|
150
|
+
interfaceDoc = `/**\n${object.documentation.split('\n').map(line => ` * ${line}`).join('\n')}\n */\n`;
|
|
151
|
+
}
|
|
152
|
+
return `${interfaceDoc}interface ${object.name} {
|
|
153
|
+
${methodDeclarations}
|
|
154
|
+
}`;
|
|
155
|
+
}).join('\n\n'),
|
|
123
156
|
others.map(object => {
|
|
124
157
|
const props = object.props.map(prop => {
|
|
125
158
|
if (prop.optional) {
|
|
@@ -135,7 +168,7 @@ interface ${object.name} {
|
|
|
135
168
|
].filter(Boolean).join('\n\n');
|
|
136
169
|
// Generate all components from this file
|
|
137
170
|
const components = [];
|
|
138
|
-
// Create a map of component names to their properties and
|
|
171
|
+
// Create a map of component names to their properties, events, and methods
|
|
139
172
|
const componentMap = new Map();
|
|
140
173
|
// Process all Properties interfaces
|
|
141
174
|
properties.forEach(prop => {
|
|
@@ -153,6 +186,14 @@ interface ${object.name} {
|
|
|
153
186
|
}
|
|
154
187
|
componentMap.get(componentName).events = event;
|
|
155
188
|
});
|
|
189
|
+
// Process all Methods interfaces
|
|
190
|
+
methods.forEach(method => {
|
|
191
|
+
const componentName = method.name.replace(/Methods$/, '');
|
|
192
|
+
if (!componentMap.has(componentName)) {
|
|
193
|
+
componentMap.set(componentName, {});
|
|
194
|
+
}
|
|
195
|
+
componentMap.get(componentName).methods = method;
|
|
196
|
+
});
|
|
156
197
|
// If we have multiple components, we need to generate a combined file
|
|
157
198
|
const componentEntries = Array.from(componentMap.entries());
|
|
158
199
|
if (componentEntries.length === 0) {
|
|
@@ -180,6 +221,7 @@ interface ${object.name} {
|
|
|
180
221
|
className: className,
|
|
181
222
|
properties: component.properties,
|
|
182
223
|
events: component.events,
|
|
224
|
+
methods: component.methods,
|
|
183
225
|
classObjectDictionary,
|
|
184
226
|
dependencies,
|
|
185
227
|
blob,
|
|
@@ -187,6 +229,7 @@ interface ${object.name} {
|
|
|
187
229
|
toWebFTagName,
|
|
188
230
|
generateReturnType,
|
|
189
231
|
generateMethodDeclaration,
|
|
232
|
+
generateMethodDeclarationWithDocs,
|
|
190
233
|
generateEventHandlerType,
|
|
191
234
|
getEventType,
|
|
192
235
|
});
|
|
@@ -214,6 +257,7 @@ interface ${object.name} {
|
|
|
214
257
|
className: className,
|
|
215
258
|
properties: component.properties,
|
|
216
259
|
events: component.events,
|
|
260
|
+
methods: component.methods,
|
|
217
261
|
classObjectDictionary,
|
|
218
262
|
dependencies: '', // Dependencies will be at the top
|
|
219
263
|
blob,
|
|
@@ -221,6 +265,7 @@ interface ${object.name} {
|
|
|
221
265
|
toWebFTagName,
|
|
222
266
|
generateReturnType,
|
|
223
267
|
generateMethodDeclaration,
|
|
268
|
+
generateMethodDeclarationWithDocs,
|
|
224
269
|
generateEventHandlerType,
|
|
225
270
|
getEventType,
|
|
226
271
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@openwebf/webf",
|
|
3
|
-
"version": "0.22.
|
|
3
|
+
"version": "0.22.9",
|
|
4
4
|
"description": "Command line tools for WebF",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"bin": {
|
|
@@ -36,6 +36,8 @@
|
|
|
36
36
|
"ts-jest": "^29.1.2"
|
|
37
37
|
},
|
|
38
38
|
"dependencies": {
|
|
39
|
+
"@microsoft/tsdoc": "^0.15.1",
|
|
40
|
+
"@microsoft/tsdoc-config": "^0.17.1",
|
|
39
41
|
"commander": "^14.0.0",
|
|
40
42
|
"glob": "^10.3.10",
|
|
41
43
|
"inquirer": "^8.2.6",
|
package/src/analyzer.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import ts, {HeritageClause, ScriptTarget, VariableStatement} from 'typescript';
|
|
2
|
+
import { TSDocParser, TextRange, DocComment } from '@microsoft/tsdoc';
|
|
2
3
|
import {IDLBlob} from './IDLBlob';
|
|
3
4
|
import {
|
|
4
5
|
ClassObject,
|
|
@@ -30,6 +31,9 @@ const sourceFileCache = new Map<string, ts.SourceFile>();
|
|
|
30
31
|
// Cache for type conversions to avoid redundant processing
|
|
31
32
|
const typeConversionCache = new Map<string, ParameterType>();
|
|
32
33
|
|
|
34
|
+
// TSDoc parser instance
|
|
35
|
+
const tsdocParser = new TSDocParser();
|
|
36
|
+
|
|
33
37
|
// Type mapping constants for better performance
|
|
34
38
|
const BASIC_TYPE_MAP: Partial<Record<ts.SyntaxKind, FunctionArgumentType>> = {
|
|
35
39
|
[ts.SyntaxKind.StringKeyword]: FunctionArgumentType.dom_string,
|
|
@@ -62,7 +66,7 @@ export function analyzer(blob: IDLBlob, definedPropertyCollector: DefinedPropert
|
|
|
62
66
|
blob.objects = sourceFile.statements
|
|
63
67
|
.map(statement => {
|
|
64
68
|
try {
|
|
65
|
-
return walkProgram(blob, statement, definedPropertyCollector, unionTypeCollector);
|
|
69
|
+
return walkProgram(blob, statement, sourceFile, definedPropertyCollector, unionTypeCollector);
|
|
66
70
|
} catch (error) {
|
|
67
71
|
console.error(`Error processing statement in ${blob.source}:`, error);
|
|
68
72
|
return null;
|
|
@@ -98,6 +102,118 @@ function getInterfaceName(statement: ts.Statement): string {
|
|
|
98
102
|
return statement.name.escapedText as string;
|
|
99
103
|
}
|
|
100
104
|
|
|
105
|
+
function getJSDocComment(node: ts.Node, sourceFile: ts.SourceFile): string | undefined {
|
|
106
|
+
|
|
107
|
+
const sourceText = sourceFile.getFullText();
|
|
108
|
+
const nodeStart = node.getFullStart();
|
|
109
|
+
const nodePos = node.getStart(sourceFile);
|
|
110
|
+
|
|
111
|
+
// Get the text between full start and actual start (includes leading trivia)
|
|
112
|
+
const leadingText = sourceText.substring(nodeStart, nodePos);
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
// Find JSDoc comment in the leading text
|
|
116
|
+
const jsDocMatch = leadingText.match(/\/\*\*([\s\S]*?)\*\/\s*$/);
|
|
117
|
+
if (!jsDocMatch) return undefined;
|
|
118
|
+
|
|
119
|
+
// Extract the full JSDoc comment including delimiters
|
|
120
|
+
const commentText = jsDocMatch[0];
|
|
121
|
+
const commentStartPos = nodeStart + leadingText.lastIndexOf(commentText);
|
|
122
|
+
|
|
123
|
+
// Create a TextRange for the comment
|
|
124
|
+
const textRange = TextRange.fromStringRange(
|
|
125
|
+
sourceText,
|
|
126
|
+
commentStartPos,
|
|
127
|
+
commentStartPos + commentText.length
|
|
128
|
+
);
|
|
129
|
+
|
|
130
|
+
// Parse the JSDoc comment using TSDoc
|
|
131
|
+
const parserContext = tsdocParser.parseRange(textRange);
|
|
132
|
+
const docComment = parserContext.docComment;
|
|
133
|
+
|
|
134
|
+
// For now, always use the raw comment to preserve all tags including @default
|
|
135
|
+
// TSDoc parser doesn't handle @default tags properly out of the box
|
|
136
|
+
|
|
137
|
+
// Fallback to raw comment if TSDoc parsing fails
|
|
138
|
+
const comment = jsDocMatch[1]
|
|
139
|
+
.split('\n')
|
|
140
|
+
.map(line => line.replace(/^\s*\*\s?/, ''))
|
|
141
|
+
.join('\n')
|
|
142
|
+
.trim();
|
|
143
|
+
return comment || undefined;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Helper function to render TSDoc nodes to string
|
|
147
|
+
function renderDocNodes(nodes: ReadonlyArray<any>): string {
|
|
148
|
+
return nodes.map(node => {
|
|
149
|
+
if (node.kind === 'PlainText') {
|
|
150
|
+
return node.text;
|
|
151
|
+
} else if (node.kind === 'SoftBreak') {
|
|
152
|
+
return '\n';
|
|
153
|
+
} else if (node.kind === 'Paragraph') {
|
|
154
|
+
return renderDocNodes(node.nodes);
|
|
155
|
+
}
|
|
156
|
+
return '';
|
|
157
|
+
}).join('').trim();
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Special function to get the first JSDoc comment in a file for the first interface
|
|
161
|
+
function getFirstInterfaceJSDoc(statement: ts.InterfaceDeclaration, sourceFile: ts.SourceFile): string | undefined {
|
|
162
|
+
|
|
163
|
+
// Find all interfaces in the file
|
|
164
|
+
const interfaces: ts.InterfaceDeclaration[] = [];
|
|
165
|
+
ts.forEachChild(sourceFile, child => {
|
|
166
|
+
if (ts.isInterfaceDeclaration(child)) {
|
|
167
|
+
interfaces.push(child);
|
|
168
|
+
}
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
// If this is the first interface, check for a file-level JSDoc
|
|
172
|
+
if (interfaces.length > 0 && interfaces[0] === statement) {
|
|
173
|
+
const sourceText = sourceFile.getFullText();
|
|
174
|
+
const firstInterfacePos = statement.getFullStart();
|
|
175
|
+
|
|
176
|
+
// Get all text before the first interface
|
|
177
|
+
const textBeforeInterface = sourceText.substring(0, firstInterfacePos);
|
|
178
|
+
|
|
179
|
+
// Find the last JSDoc comment before the interface
|
|
180
|
+
const jsDocMatches = textBeforeInterface.match(/\/\*\*([\s\S]*?)\*\//g);
|
|
181
|
+
if (jsDocMatches && jsDocMatches.length > 0) {
|
|
182
|
+
const lastJsDoc = jsDocMatches[jsDocMatches.length - 1];
|
|
183
|
+
const commentStartPos = textBeforeInterface.lastIndexOf(lastJsDoc);
|
|
184
|
+
|
|
185
|
+
// Create a TextRange for the comment
|
|
186
|
+
const textRange = TextRange.fromStringRange(
|
|
187
|
+
sourceText,
|
|
188
|
+
commentStartPos,
|
|
189
|
+
commentStartPos + lastJsDoc.length
|
|
190
|
+
);
|
|
191
|
+
|
|
192
|
+
// Parse the JSDoc comment using TSDoc
|
|
193
|
+
const parserContext = tsdocParser.parseRange(textRange);
|
|
194
|
+
const docComment = parserContext.docComment;
|
|
195
|
+
|
|
196
|
+
// Extract the parsed content
|
|
197
|
+
if (docComment.summarySection) {
|
|
198
|
+
const summary = renderDocNodes(docComment.summarySection.nodes);
|
|
199
|
+
if (summary) return summary;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// Fallback to raw comment
|
|
203
|
+
const comment = lastJsDoc
|
|
204
|
+
.replace(/^\/\*\*/, '')
|
|
205
|
+
.replace(/\*\/$/, '')
|
|
206
|
+
.split('\n')
|
|
207
|
+
.map(line => line.replace(/^\s*\*\s?/, ''))
|
|
208
|
+
.join('\n')
|
|
209
|
+
.trim();
|
|
210
|
+
return comment || undefined;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
return undefined;
|
|
215
|
+
}
|
|
216
|
+
|
|
101
217
|
function getHeritageType(heritage: HeritageClause): string | null {
|
|
102
218
|
if (!heritage.types.length) return null;
|
|
103
219
|
|
|
@@ -217,11 +333,15 @@ function getParameterBaseType(type: ts.TypeNode, mode?: ParameterMode): Paramete
|
|
|
217
333
|
}
|
|
218
334
|
|
|
219
335
|
if (type.kind === ts.SyntaxKind.LiteralType) {
|
|
220
|
-
// Handle literal types
|
|
336
|
+
// Handle literal types
|
|
221
337
|
const literalType = type as ts.LiteralTypeNode;
|
|
222
338
|
if (literalType.literal.kind === ts.SyntaxKind.NullKeyword) {
|
|
223
339
|
return FunctionArgumentType.null;
|
|
224
340
|
}
|
|
341
|
+
if (literalType.literal.kind === ts.SyntaxKind.StringLiteral) {
|
|
342
|
+
// Return the string literal value itself
|
|
343
|
+
return (literalType.literal as ts.StringLiteral).text;
|
|
344
|
+
}
|
|
225
345
|
return FunctionArgumentType.any;
|
|
226
346
|
}
|
|
227
347
|
|
|
@@ -411,10 +531,10 @@ function isParamsReadOnly(m: ts.PropertySignature): boolean {
|
|
|
411
531
|
return m.modifiers.some(k => k.kind === ts.SyntaxKind.ReadonlyKeyword);
|
|
412
532
|
}
|
|
413
533
|
|
|
414
|
-
function walkProgram(blob: IDLBlob, statement: ts.Statement, definedPropertyCollector: DefinedPropertyCollector, unionTypeCollector: UnionTypeCollector) {
|
|
534
|
+
function walkProgram(blob: IDLBlob, statement: ts.Statement, sourceFile: ts.SourceFile, definedPropertyCollector: DefinedPropertyCollector, unionTypeCollector: UnionTypeCollector) {
|
|
415
535
|
switch(statement.kind) {
|
|
416
536
|
case ts.SyntaxKind.InterfaceDeclaration:
|
|
417
|
-
return processInterfaceDeclaration(statement as ts.InterfaceDeclaration, blob, definedPropertyCollector, unionTypeCollector);
|
|
537
|
+
return processInterfaceDeclaration(statement as ts.InterfaceDeclaration, blob, sourceFile, definedPropertyCollector, unionTypeCollector);
|
|
418
538
|
|
|
419
539
|
case ts.SyntaxKind.VariableStatement:
|
|
420
540
|
return processVariableStatement(statement as VariableStatement, unionTypeCollector);
|
|
@@ -444,12 +564,19 @@ function processTypeAliasDeclaration(
|
|
|
444
564
|
function processInterfaceDeclaration(
|
|
445
565
|
statement: ts.InterfaceDeclaration,
|
|
446
566
|
blob: IDLBlob,
|
|
567
|
+
sourceFile: ts.SourceFile,
|
|
447
568
|
definedPropertyCollector: DefinedPropertyCollector,
|
|
448
569
|
unionTypeCollector: UnionTypeCollector
|
|
449
570
|
): ClassObject | null {
|
|
450
571
|
const interfaceName = statement.name.escapedText.toString();
|
|
451
572
|
const obj = new ClassObject();
|
|
452
573
|
|
|
574
|
+
// Capture JSDoc comment for the interface
|
|
575
|
+
const directComment = getJSDocComment(statement, sourceFile);
|
|
576
|
+
const fileComment = getFirstInterfaceJSDoc(statement, sourceFile);
|
|
577
|
+
obj.documentation = directComment || fileComment;
|
|
578
|
+
|
|
579
|
+
|
|
453
580
|
// Process heritage clauses
|
|
454
581
|
if (statement.heritageClauses) {
|
|
455
582
|
const heritage = statement.heritageClauses[0];
|
|
@@ -470,7 +597,7 @@ function processInterfaceDeclaration(
|
|
|
470
597
|
|
|
471
598
|
// Process members in batches for better performance
|
|
472
599
|
const members = Array.from(statement.members);
|
|
473
|
-
processMembersBatch(members, obj, definedPropertyCollector, unionTypeCollector);
|
|
600
|
+
processMembersBatch(members, obj, sourceFile, definedPropertyCollector, unionTypeCollector);
|
|
474
601
|
|
|
475
602
|
ClassObject.globalClassMap[interfaceName] = obj;
|
|
476
603
|
|
|
@@ -480,12 +607,13 @@ function processInterfaceDeclaration(
|
|
|
480
607
|
function processMembersBatch(
|
|
481
608
|
members: ts.TypeElement[],
|
|
482
609
|
obj: ClassObject,
|
|
610
|
+
sourceFile: ts.SourceFile,
|
|
483
611
|
definedPropertyCollector: DefinedPropertyCollector,
|
|
484
612
|
unionTypeCollector: UnionTypeCollector
|
|
485
613
|
): void {
|
|
486
614
|
for (const member of members) {
|
|
487
615
|
try {
|
|
488
|
-
processMember(member, obj, definedPropertyCollector, unionTypeCollector);
|
|
616
|
+
processMember(member, obj, sourceFile, definedPropertyCollector, unionTypeCollector);
|
|
489
617
|
} catch (error) {
|
|
490
618
|
console.error(`Error processing member:`, error);
|
|
491
619
|
}
|
|
@@ -495,24 +623,25 @@ function processMembersBatch(
|
|
|
495
623
|
function processMember(
|
|
496
624
|
member: ts.TypeElement,
|
|
497
625
|
obj: ClassObject,
|
|
626
|
+
sourceFile: ts.SourceFile,
|
|
498
627
|
definedPropertyCollector: DefinedPropertyCollector,
|
|
499
628
|
unionTypeCollector: UnionTypeCollector
|
|
500
629
|
): void {
|
|
501
630
|
switch(member.kind) {
|
|
502
631
|
case ts.SyntaxKind.PropertySignature:
|
|
503
|
-
processPropertySignature(member as ts.PropertySignature, obj, definedPropertyCollector, unionTypeCollector);
|
|
632
|
+
processPropertySignature(member as ts.PropertySignature, obj, sourceFile, definedPropertyCollector, unionTypeCollector);
|
|
504
633
|
break;
|
|
505
634
|
|
|
506
635
|
case ts.SyntaxKind.MethodSignature:
|
|
507
|
-
processMethodSignature(member as ts.MethodSignature, obj, unionTypeCollector);
|
|
636
|
+
processMethodSignature(member as ts.MethodSignature, obj, sourceFile, unionTypeCollector);
|
|
508
637
|
break;
|
|
509
638
|
|
|
510
639
|
case ts.SyntaxKind.IndexSignature:
|
|
511
|
-
processIndexSignature(member as ts.IndexSignatureDeclaration, obj, unionTypeCollector);
|
|
640
|
+
processIndexSignature(member as ts.IndexSignatureDeclaration, obj, sourceFile, unionTypeCollector);
|
|
512
641
|
break;
|
|
513
642
|
|
|
514
643
|
case ts.SyntaxKind.ConstructSignature:
|
|
515
|
-
processConstructSignature(member as ts.ConstructSignatureDeclaration, obj, unionTypeCollector);
|
|
644
|
+
processConstructSignature(member as ts.ConstructSignatureDeclaration, obj, sourceFile, unionTypeCollector);
|
|
516
645
|
break;
|
|
517
646
|
}
|
|
518
647
|
}
|
|
@@ -520,12 +649,14 @@ function processMember(
|
|
|
520
649
|
function processPropertySignature(
|
|
521
650
|
member: ts.PropertySignature,
|
|
522
651
|
obj: ClassObject,
|
|
652
|
+
sourceFile: ts.SourceFile,
|
|
523
653
|
definedPropertyCollector: DefinedPropertyCollector,
|
|
524
654
|
unionTypeCollector: UnionTypeCollector
|
|
525
655
|
): void {
|
|
526
656
|
const prop = new PropsDeclaration();
|
|
527
657
|
prop.name = getPropName(member.name!, prop);
|
|
528
658
|
prop.readonly = isParamsReadOnly(member);
|
|
659
|
+
prop.documentation = getJSDocComment(member, sourceFile);
|
|
529
660
|
|
|
530
661
|
definedPropertyCollector.properties.add(prop.name);
|
|
531
662
|
|
|
@@ -577,10 +708,12 @@ function createAsyncProperty(prop: PropsDeclaration, mode: ParameterMode): Props
|
|
|
577
708
|
function processMethodSignature(
|
|
578
709
|
member: ts.MethodSignature,
|
|
579
710
|
obj: ClassObject,
|
|
711
|
+
sourceFile: ts.SourceFile,
|
|
580
712
|
unionTypeCollector: UnionTypeCollector
|
|
581
713
|
): void {
|
|
582
714
|
const f = new FunctionDeclaration();
|
|
583
715
|
f.name = getPropName(member.name!);
|
|
716
|
+
f.documentation = getJSDocComment(member, sourceFile);
|
|
584
717
|
f.args = member.parameters.map(params =>
|
|
585
718
|
paramsNodeToArguments(params, unionTypeCollector)
|
|
586
719
|
);
|
|
@@ -639,6 +772,7 @@ function createAsyncMethod(
|
|
|
639
772
|
function processIndexSignature(
|
|
640
773
|
member: ts.IndexSignatureDeclaration,
|
|
641
774
|
obj: ClassObject,
|
|
775
|
+
sourceFile: ts.SourceFile,
|
|
642
776
|
unionTypeCollector: UnionTypeCollector
|
|
643
777
|
): void {
|
|
644
778
|
const prop = new IndexedPropertyDeclaration();
|
|
@@ -659,6 +793,7 @@ function processIndexSignature(
|
|
|
659
793
|
function processConstructSignature(
|
|
660
794
|
member: ts.ConstructSignatureDeclaration,
|
|
661
795
|
obj: ClassObject,
|
|
796
|
+
sourceFile: ts.SourceFile,
|
|
662
797
|
unionTypeCollector: UnionTypeCollector
|
|
663
798
|
): void {
|
|
664
799
|
const c = new FunctionDeclaration();
|
package/src/declaration.ts
CHANGED
|
@@ -45,6 +45,7 @@ export class PropsDeclaration {
|
|
|
45
45
|
isSymbol?: boolean;
|
|
46
46
|
readonly: boolean;
|
|
47
47
|
optional: boolean;
|
|
48
|
+
documentation?: string;
|
|
48
49
|
}
|
|
49
50
|
|
|
50
51
|
export class IndexedPropertyDeclaration extends PropsDeclaration {
|
|
@@ -77,6 +78,7 @@ export class ClassObject {
|
|
|
77
78
|
staticMethods: FunctionDeclaration[] = [];
|
|
78
79
|
construct?: FunctionDeclaration;
|
|
79
80
|
kind: ClassObjectKind = ClassObjectKind.interface;
|
|
81
|
+
documentation?: string;
|
|
80
82
|
}
|
|
81
83
|
|
|
82
84
|
export class FunctionObject {
|