@openwebf/webf 0.22.6 → 0.22.8
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 +57 -6
- package/test/union-types-jsdoc.test.ts +168 -0
- package/dist/analyzer_original.js +0 -467
package/src/react.ts
CHANGED
|
@@ -4,13 +4,16 @@ import path from 'path';
|
|
|
4
4
|
import {ParameterType} from "./analyzer";
|
|
5
5
|
import {ClassObject, FunctionArgumentType, FunctionDeclaration, TypeAliasObject} from "./declaration";
|
|
6
6
|
import {IDLBlob} from "./IDLBlob";
|
|
7
|
-
import {getPointerType, isPointerType} from "./utils";
|
|
7
|
+
import {getPointerType, isPointerType, isUnionType} from "./utils";
|
|
8
8
|
|
|
9
9
|
function readTemplate(name: string) {
|
|
10
10
|
return fs.readFileSync(path.join(__dirname, '../templates/' + name + '.tpl'), {encoding: 'utf-8'});
|
|
11
11
|
}
|
|
12
12
|
|
|
13
13
|
function generateReturnType(type: ParameterType) {
|
|
14
|
+
if (isUnionType(type)) {
|
|
15
|
+
return (type.value as ParameterType[]).map(v => `'${v.value}'`).join(' | ');
|
|
16
|
+
}
|
|
14
17
|
if (isPointerType(type)) {
|
|
15
18
|
const pointerType = getPointerType(type);
|
|
16
19
|
return pointerType;
|
|
@@ -79,6 +82,20 @@ function generateMethodDeclaration(method: FunctionDeclaration) {
|
|
|
79
82
|
return `${methodName}(${args}): ${returnType};`;
|
|
80
83
|
}
|
|
81
84
|
|
|
85
|
+
function generateMethodDeclarationWithDocs(method: FunctionDeclaration, indent: string = ''): string {
|
|
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
|
+
}
|
|
98
|
+
|
|
82
99
|
function toReactEventName(name: string) {
|
|
83
100
|
const eventName = 'on-' + name;
|
|
84
101
|
return _.camelCase(eventName);
|
|
@@ -115,10 +132,14 @@ export function generateReactComponent(blob: IDLBlob, packageName?: string, rela
|
|
|
115
132
|
const events = classObjects.filter(object => {
|
|
116
133
|
return object.name.endsWith('Events');
|
|
117
134
|
});
|
|
135
|
+
const methods = classObjects.filter(object => {
|
|
136
|
+
return object.name.endsWith('Methods');
|
|
137
|
+
});
|
|
118
138
|
|
|
119
139
|
const others = classObjects.filter(object => {
|
|
120
140
|
return !object.name.endsWith('Properties')
|
|
121
|
-
&& !object.name.endsWith('Events')
|
|
141
|
+
&& !object.name.endsWith('Events')
|
|
142
|
+
&& !object.name.endsWith('Methods');
|
|
122
143
|
});
|
|
123
144
|
|
|
124
145
|
// Include type aliases
|
|
@@ -128,6 +149,21 @@ export function generateReactComponent(blob: IDLBlob, packageName?: string, rela
|
|
|
128
149
|
|
|
129
150
|
const dependencies = [
|
|
130
151
|
typeAliasDeclarations,
|
|
152
|
+
// Include Methods interfaces as dependencies
|
|
153
|
+
methods.map(object => {
|
|
154
|
+
const methodDeclarations = object.methods.map(method => {
|
|
155
|
+
return generateMethodDeclarationWithDocs(method, ' ');
|
|
156
|
+
}).join('\n');
|
|
157
|
+
|
|
158
|
+
let interfaceDoc = '';
|
|
159
|
+
if (object.documentation) {
|
|
160
|
+
interfaceDoc = `/**\n${object.documentation.split('\n').map(line => ` * ${line}`).join('\n')}\n */\n`;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
return `${interfaceDoc}interface ${object.name} {
|
|
164
|
+
${methodDeclarations}
|
|
165
|
+
}`;
|
|
166
|
+
}).join('\n\n'),
|
|
131
167
|
others.map(object => {
|
|
132
168
|
const props = object.props.map(prop => {
|
|
133
169
|
if (prop.optional) {
|
|
@@ -146,8 +182,8 @@ interface ${object.name} {
|
|
|
146
182
|
// Generate all components from this file
|
|
147
183
|
const components: string[] = [];
|
|
148
184
|
|
|
149
|
-
// Create a map of component names to their properties and
|
|
150
|
-
const componentMap = new Map<string, { properties?: ClassObject, events?: ClassObject }>();
|
|
185
|
+
// Create a map of component names to their properties, events, and methods
|
|
186
|
+
const componentMap = new Map<string, { properties?: ClassObject, events?: ClassObject, methods?: ClassObject }>();
|
|
151
187
|
|
|
152
188
|
// Process all Properties interfaces
|
|
153
189
|
properties.forEach(prop => {
|
|
@@ -167,6 +203,15 @@ interface ${object.name} {
|
|
|
167
203
|
componentMap.get(componentName)!.events = event;
|
|
168
204
|
});
|
|
169
205
|
|
|
206
|
+
// Process all Methods interfaces
|
|
207
|
+
methods.forEach(method => {
|
|
208
|
+
const componentName = method.name.replace(/Methods$/, '');
|
|
209
|
+
if (!componentMap.has(componentName)) {
|
|
210
|
+
componentMap.set(componentName, {});
|
|
211
|
+
}
|
|
212
|
+
componentMap.get(componentName)!.methods = method;
|
|
213
|
+
});
|
|
214
|
+
|
|
170
215
|
// If we have multiple components, we need to generate a combined file
|
|
171
216
|
const componentEntries = Array.from(componentMap.entries());
|
|
172
217
|
|
|
@@ -202,6 +247,7 @@ interface ${object.name} {
|
|
|
202
247
|
className: className,
|
|
203
248
|
properties: component.properties,
|
|
204
249
|
events: component.events,
|
|
250
|
+
methods: component.methods,
|
|
205
251
|
classObjectDictionary,
|
|
206
252
|
dependencies,
|
|
207
253
|
blob,
|
|
@@ -209,6 +255,7 @@ interface ${object.name} {
|
|
|
209
255
|
toWebFTagName,
|
|
210
256
|
generateReturnType,
|
|
211
257
|
generateMethodDeclaration,
|
|
258
|
+
generateMethodDeclarationWithDocs,
|
|
212
259
|
generateEventHandlerType,
|
|
213
260
|
getEventType,
|
|
214
261
|
});
|
|
@@ -240,6 +287,7 @@ interface ${object.name} {
|
|
|
240
287
|
className: className,
|
|
241
288
|
properties: component.properties,
|
|
242
289
|
events: component.events,
|
|
290
|
+
methods: component.methods,
|
|
243
291
|
classObjectDictionary,
|
|
244
292
|
dependencies: '', // Dependencies will be at the top
|
|
245
293
|
blob,
|
|
@@ -247,6 +295,7 @@ interface ${object.name} {
|
|
|
247
295
|
toWebFTagName,
|
|
248
296
|
generateReturnType,
|
|
249
297
|
generateMethodDeclaration,
|
|
298
|
+
generateMethodDeclarationWithDocs,
|
|
250
299
|
generateEventHandlerType,
|
|
251
300
|
getEventType,
|
|
252
301
|
});
|
|
@@ -7,12 +7,18 @@ export interface <%= className %>Props {
|
|
|
7
7
|
<% _.forEach(properties?.props, function(prop, index) { %>
|
|
8
8
|
<% var propName = _.camelCase(prop.name); %>
|
|
9
9
|
<% var attributeName = _.kebabCase(prop.name); %>
|
|
10
|
+
<% if (prop.documentation) { %>
|
|
11
|
+
/**
|
|
12
|
+
* <%= prop.documentation.split('\n').join('\n * ') %>
|
|
13
|
+
*/
|
|
14
|
+
<% } else { %>
|
|
10
15
|
/**
|
|
11
16
|
* <%= propName %> property
|
|
12
17
|
<% if (prop.optional) { %>
|
|
13
18
|
* @default undefined
|
|
14
19
|
<% } %>
|
|
15
20
|
*/
|
|
21
|
+
<% } %>
|
|
16
22
|
<% if (prop.optional) { %>
|
|
17
23
|
<%= propName %>?: <%= generateReturnType(prop.type) %>;
|
|
18
24
|
<% } else { %>
|
|
@@ -22,9 +28,15 @@ export interface <%= className %>Props {
|
|
|
22
28
|
<% }); %>
|
|
23
29
|
<% _.forEach(events?.props, function(prop, index) { %>
|
|
24
30
|
<% var propName = toReactEventName(prop.name); %>
|
|
31
|
+
<% if (prop.documentation) { %>
|
|
32
|
+
/**
|
|
33
|
+
* <%= prop.documentation.split('\n').join('\n * ') %>
|
|
34
|
+
*/
|
|
35
|
+
<% } else { %>
|
|
25
36
|
/**
|
|
26
37
|
* <%= prop.name %> event handler
|
|
27
38
|
*/
|
|
39
|
+
<% } %>
|
|
28
40
|
<%= propName %>?: (event: <%= getEventType(prop.type) %>) => void;
|
|
29
41
|
|
|
30
42
|
<% }); %>
|
|
@@ -44,24 +56,63 @@ export interface <%= className %>Props {
|
|
|
44
56
|
className?: string;
|
|
45
57
|
}
|
|
46
58
|
|
|
59
|
+
<% if (methods && methods.methods.length > 0) { %>
|
|
60
|
+
/**
|
|
61
|
+
* Element interface with methods accessible via ref
|
|
62
|
+
* @example
|
|
63
|
+
* ```tsx
|
|
64
|
+
* const ref = useRef<<%= className %>Element>(null);
|
|
65
|
+
* // Call methods on the element
|
|
66
|
+
* ref.current?.finishRefresh('success');
|
|
67
|
+
* ```
|
|
68
|
+
*/
|
|
69
|
+
<% } %>
|
|
47
70
|
export interface <%= className %>Element extends WebFElementWithMethods<{
|
|
48
|
-
<% _.forEach(
|
|
49
|
-
|
|
71
|
+
<% _.forEach(methods?.methods, function(method, index) { %>
|
|
72
|
+
<%= generateMethodDeclarationWithDocs(method, ' ') %>
|
|
50
73
|
<% }); %>
|
|
51
74
|
}> {}
|
|
52
75
|
|
|
76
|
+
<% if (properties?.documentation || methods?.documentation || events?.documentation) { %>
|
|
77
|
+
<% const docs = properties?.documentation || methods?.documentation || events?.documentation; %>
|
|
78
|
+
/**
|
|
79
|
+
* <%= docs %>
|
|
80
|
+
*
|
|
81
|
+
* @example
|
|
82
|
+
* ```tsx<% if (methods && methods.methods.length > 0) { %>
|
|
83
|
+
* const ref = useRef<<%= className %>Element>(null);<% } %>
|
|
84
|
+
*
|
|
85
|
+
* <<%= className %><% if (methods && methods.methods.length > 0) { %>
|
|
86
|
+
* ref={ref}<% } %>
|
|
87
|
+
* // Add props here
|
|
88
|
+
* >
|
|
89
|
+
* Content
|
|
90
|
+
* </<%= className %>><% if (methods && methods.methods.length > 0) { %>
|
|
91
|
+
*
|
|
92
|
+
* // Call methods on the element
|
|
93
|
+
* ref.current?.finishRefresh('success');<% } %>
|
|
94
|
+
* ```
|
|
95
|
+
*/
|
|
96
|
+
<% } else { %>
|
|
53
97
|
/**
|
|
54
98
|
* <%= className %> - WebF <%= className %> component
|
|
55
99
|
*
|
|
56
100
|
* @example
|
|
57
|
-
* ```tsx
|
|
58
|
-
* <<%= className %>
|
|
59
|
-
*
|
|
101
|
+
* ```tsx<% if (methods && methods.methods.length > 0) { %>
|
|
102
|
+
* const ref = useRef<<%= className %>Element>(null);<% } %>
|
|
103
|
+
*
|
|
104
|
+
* <<%= className %><% if (methods && methods.methods.length > 0) { %>
|
|
105
|
+
* ref={ref}<% } %>
|
|
106
|
+
* // Add props here
|
|
60
107
|
* >
|
|
61
108
|
* Content
|
|
62
|
-
* </<%= className
|
|
109
|
+
* </<%= className %>><% if (methods && methods.methods.length > 0) { %>
|
|
110
|
+
*
|
|
111
|
+
* // Call methods on the element
|
|
112
|
+
* ref.current?.finishRefresh('success');<% } %>
|
|
63
113
|
* ```
|
|
64
114
|
*/
|
|
115
|
+
<% } %>
|
|
65
116
|
export const <%= className %> = createWebFComponent<<%= className %>Element, <%= className %>Props>({
|
|
66
117
|
tagName: '<%= toWebFTagName(className) %>',
|
|
67
118
|
displayName: '<%= className %>',
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import { analyzer, clearCaches, UnionTypeCollector, ParameterType } from '../src/analyzer';
|
|
2
|
+
import { generateReactComponent } from '../src/react';
|
|
3
|
+
import { IDLBlob } from '../src/IDLBlob';
|
|
4
|
+
import { ClassObject } from '../src/declaration';
|
|
5
|
+
|
|
6
|
+
describe('Union Types and JSDoc Preservation', () => {
|
|
7
|
+
beforeEach(() => {
|
|
8
|
+
clearCaches();
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
test('should generate union types with quoted string literals and preserve JSDoc @default tags', () => {
|
|
12
|
+
// Create test TypeScript content with union types and JSDoc
|
|
13
|
+
const testContent = `
|
|
14
|
+
/**
|
|
15
|
+
* Table cell component
|
|
16
|
+
*/
|
|
17
|
+
interface WebFTableCellProperties {
|
|
18
|
+
/**
|
|
19
|
+
* Text alignment
|
|
20
|
+
* @default "left"
|
|
21
|
+
*/
|
|
22
|
+
align?: 'left' | 'center' | 'right';
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Cell type (header or data)
|
|
26
|
+
* @default "data"
|
|
27
|
+
*/
|
|
28
|
+
type?: 'header' | 'data';
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Column span
|
|
32
|
+
* @default 1
|
|
33
|
+
*/
|
|
34
|
+
colspan?: number;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
interface WebFTableCellEvents {
|
|
38
|
+
/**
|
|
39
|
+
* Cell click event
|
|
40
|
+
*/
|
|
41
|
+
click: CustomEvent<{row: number, column: number, value: any}>;
|
|
42
|
+
}
|
|
43
|
+
`;
|
|
44
|
+
|
|
45
|
+
// Create IDL blob
|
|
46
|
+
const blob = new IDLBlob('test.d.ts', 'dist', 'table_cell', 'implement');
|
|
47
|
+
blob.raw = testContent;
|
|
48
|
+
|
|
49
|
+
// Analyze the content
|
|
50
|
+
const definedPropertyCollector = {
|
|
51
|
+
properties: new Set<string>(),
|
|
52
|
+
files: new Set<string>(),
|
|
53
|
+
interfaces: new Set<string>()
|
|
54
|
+
};
|
|
55
|
+
const unionTypeCollector: UnionTypeCollector = { types: new Set<ParameterType[]>() };
|
|
56
|
+
|
|
57
|
+
analyzer(blob, definedPropertyCollector, unionTypeCollector);
|
|
58
|
+
|
|
59
|
+
// Generate React component
|
|
60
|
+
const reactCode = generateReactComponent(blob);
|
|
61
|
+
|
|
62
|
+
// Test 1: Check that union types are properly quoted
|
|
63
|
+
expect(reactCode).toContain("align?: 'left' | 'center' | 'right';");
|
|
64
|
+
expect(reactCode).toContain("type?: 'header' | 'data';");
|
|
65
|
+
|
|
66
|
+
// Test 2: Check that JSDoc is preserved with @default tags
|
|
67
|
+
expect(reactCode).toMatch(/\*\s+Text alignment[\s\S]*?\*\s+@default "left"/);
|
|
68
|
+
expect(reactCode).toMatch(/\*\s+Cell type \(header or data\)[\s\S]*?\*\s+@default "data"/);
|
|
69
|
+
|
|
70
|
+
// Test 3: Check that the generated interface has the correct structure
|
|
71
|
+
expect(reactCode).toContain('export interface WebFTableCellProps {');
|
|
72
|
+
expect(reactCode).toContain('colspan?: number;');
|
|
73
|
+
|
|
74
|
+
// Test 4: Verify the component is created with correct props
|
|
75
|
+
expect(reactCode).toContain("attributeProps: [");
|
|
76
|
+
expect(reactCode).toContain("'align',");
|
|
77
|
+
expect(reactCode).toContain("'type',");
|
|
78
|
+
expect(reactCode).toContain("'colspan',");
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
test('should handle complex union types', () => {
|
|
82
|
+
const testContent = `
|
|
83
|
+
interface TestProperties {
|
|
84
|
+
/**
|
|
85
|
+
* Size property
|
|
86
|
+
* @default "medium"
|
|
87
|
+
*/
|
|
88
|
+
size?: 'small' | 'medium' | 'large' | 'xl';
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Status
|
|
92
|
+
* @default "pending"
|
|
93
|
+
*/
|
|
94
|
+
status?: 'pending' | 'active' | 'completed' | 'failed';
|
|
95
|
+
}
|
|
96
|
+
`;
|
|
97
|
+
|
|
98
|
+
const blob = new IDLBlob('test.d.ts', 'dist', 'test', 'implement');
|
|
99
|
+
blob.raw = testContent;
|
|
100
|
+
|
|
101
|
+
const definedPropertyCollector = {
|
|
102
|
+
properties: new Set<string>(),
|
|
103
|
+
files: new Set<string>(),
|
|
104
|
+
interfaces: new Set<string>()
|
|
105
|
+
};
|
|
106
|
+
const unionTypeCollector: UnionTypeCollector = { types: new Set<ParameterType[]>() };
|
|
107
|
+
|
|
108
|
+
analyzer(blob, definedPropertyCollector, unionTypeCollector);
|
|
109
|
+
const reactCode = generateReactComponent(blob);
|
|
110
|
+
|
|
111
|
+
// Check complex union types
|
|
112
|
+
expect(reactCode).toContain("size?: 'small' | 'medium' | 'large' | 'xl';");
|
|
113
|
+
expect(reactCode).toContain("status?: 'pending' | 'active' | 'completed' | 'failed';");
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
test('should handle mixed type properties alongside union types', () => {
|
|
117
|
+
const testContent = `
|
|
118
|
+
interface MixedProperties {
|
|
119
|
+
/**
|
|
120
|
+
* String union type
|
|
121
|
+
* @default "auto"
|
|
122
|
+
*/
|
|
123
|
+
mode?: 'auto' | 'manual' | 'disabled';
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Regular string
|
|
127
|
+
*/
|
|
128
|
+
name?: string;
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Number property
|
|
132
|
+
* @default 42
|
|
133
|
+
*/
|
|
134
|
+
count?: number;
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Boolean property
|
|
138
|
+
* @default true
|
|
139
|
+
*/
|
|
140
|
+
enabled?: boolean;
|
|
141
|
+
}
|
|
142
|
+
`;
|
|
143
|
+
|
|
144
|
+
const blob = new IDLBlob('test.d.ts', 'dist', 'mixed', 'implement');
|
|
145
|
+
blob.raw = testContent;
|
|
146
|
+
|
|
147
|
+
const definedPropertyCollector = {
|
|
148
|
+
properties: new Set<string>(),
|
|
149
|
+
files: new Set<string>(),
|
|
150
|
+
interfaces: new Set<string>()
|
|
151
|
+
};
|
|
152
|
+
const unionTypeCollector: UnionTypeCollector = { types: new Set<ParameterType[]>() };
|
|
153
|
+
|
|
154
|
+
analyzer(blob, definedPropertyCollector, unionTypeCollector);
|
|
155
|
+
const reactCode = generateReactComponent(blob);
|
|
156
|
+
|
|
157
|
+
// Check that different types are handled correctly
|
|
158
|
+
expect(reactCode).toContain("mode?: 'auto' | 'manual' | 'disabled';");
|
|
159
|
+
expect(reactCode).toContain("name?: string;");
|
|
160
|
+
expect(reactCode).toContain("count?: number;");
|
|
161
|
+
expect(reactCode).toContain("enabled?: boolean;");
|
|
162
|
+
|
|
163
|
+
// Check JSDoc preservation
|
|
164
|
+
expect(reactCode).toMatch(/\*\s+String union type[\s\S]*?\*\s+@default "auto"/);
|
|
165
|
+
expect(reactCode).toMatch(/\*\s+Number property[\s\S]*?\*\s+@default 42/);
|
|
166
|
+
expect(reactCode).toMatch(/\*\s+Boolean property[\s\S]*?\*\s+@default true/);
|
|
167
|
+
});
|
|
168
|
+
});
|