@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/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,12 +28,23 @@ 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
|
<% }); %>
|
|
43
|
+
/**
|
|
44
|
+
* HTML id attribute
|
|
45
|
+
*/
|
|
46
|
+
id?: string;
|
|
47
|
+
|
|
31
48
|
/**
|
|
32
49
|
* Additional CSS styles
|
|
33
50
|
*/
|
|
@@ -44,24 +61,63 @@ export interface <%= className %>Props {
|
|
|
44
61
|
className?: string;
|
|
45
62
|
}
|
|
46
63
|
|
|
64
|
+
<% if (methods && methods.methods.length > 0) { %>
|
|
65
|
+
/**
|
|
66
|
+
* Element interface with methods accessible via ref
|
|
67
|
+
* @example
|
|
68
|
+
* ```tsx
|
|
69
|
+
* const ref = useRef<<%= className %>Element>(null);
|
|
70
|
+
* // Call methods on the element
|
|
71
|
+
* ref.current?.finishRefresh('success');
|
|
72
|
+
* ```
|
|
73
|
+
*/
|
|
74
|
+
<% } %>
|
|
47
75
|
export interface <%= className %>Element extends WebFElementWithMethods<{
|
|
48
|
-
<% _.forEach(
|
|
49
|
-
|
|
76
|
+
<% _.forEach(methods?.methods, function(method, index) { %>
|
|
77
|
+
<%= generateMethodDeclarationWithDocs(method, ' ') %>
|
|
50
78
|
<% }); %>
|
|
51
79
|
}> {}
|
|
52
80
|
|
|
81
|
+
<% if (properties?.documentation || methods?.documentation || events?.documentation) { %>
|
|
82
|
+
<% const docs = properties?.documentation || methods?.documentation || events?.documentation; %>
|
|
83
|
+
/**
|
|
84
|
+
* <%= docs %>
|
|
85
|
+
*
|
|
86
|
+
* @example
|
|
87
|
+
* ```tsx<% if (methods && methods.methods.length > 0) { %>
|
|
88
|
+
* const ref = useRef<<%= className %>Element>(null);<% } %>
|
|
89
|
+
*
|
|
90
|
+
* <<%= className %><% if (methods && methods.methods.length > 0) { %>
|
|
91
|
+
* ref={ref}<% } %>
|
|
92
|
+
* // Add props here
|
|
93
|
+
* >
|
|
94
|
+
* Content
|
|
95
|
+
* </<%= className %>><% if (methods && methods.methods.length > 0) { %>
|
|
96
|
+
*
|
|
97
|
+
* // Call methods on the element
|
|
98
|
+
* ref.current?.finishRefresh('success');<% } %>
|
|
99
|
+
* ```
|
|
100
|
+
*/
|
|
101
|
+
<% } else { %>
|
|
53
102
|
/**
|
|
54
103
|
* <%= className %> - WebF <%= className %> component
|
|
55
104
|
*
|
|
56
105
|
* @example
|
|
57
|
-
* ```tsx
|
|
58
|
-
* <<%= className %>
|
|
59
|
-
*
|
|
106
|
+
* ```tsx<% if (methods && methods.methods.length > 0) { %>
|
|
107
|
+
* const ref = useRef<<%= className %>Element>(null);<% } %>
|
|
108
|
+
*
|
|
109
|
+
* <<%= className %><% if (methods && methods.methods.length > 0) { %>
|
|
110
|
+
* ref={ref}<% } %>
|
|
111
|
+
* // Add props here
|
|
60
112
|
* >
|
|
61
113
|
* Content
|
|
62
|
-
* </<%= className
|
|
114
|
+
* </<%= className %>><% if (methods && methods.methods.length > 0) { %>
|
|
115
|
+
*
|
|
116
|
+
* // Call methods on the element
|
|
117
|
+
* ref.current?.finishRefresh('success');<% } %>
|
|
63
118
|
* ```
|
|
64
119
|
*/
|
|
120
|
+
<% } %>
|
|
65
121
|
export const <%= className %> = createWebFComponent<<%= className %>Element, <%= className %>Props>({
|
|
66
122
|
tagName: '<%= toWebFTagName(className) %>',
|
|
67
123
|
displayName: '<%= className %>',
|
package/test/react.test.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { generateReactComponent } from '../src/react';
|
|
2
2
|
import { IDLBlob } from '../src/IDLBlob';
|
|
3
|
-
import { ClassObject, ClassObjectKind } from '../src/declaration';
|
|
3
|
+
import { ClassObject, ClassObjectKind, PropsDeclaration } from '../src/declaration';
|
|
4
4
|
|
|
5
5
|
// Import the toWebFTagName function for testing
|
|
6
6
|
import { toWebFTagName } from '../src/react';
|
|
@@ -130,5 +130,66 @@ describe('React Generator', () => {
|
|
|
130
130
|
// From src/lib/src/html/shimmer to src/utils: ../../../../utils
|
|
131
131
|
expect(result).toContain('import { createWebFComponent, WebFElementWithMethods } from "../../../../utils/createWebFComponent"');
|
|
132
132
|
});
|
|
133
|
+
|
|
134
|
+
it('should include standard HTML props (id, className, style) in component interface', () => {
|
|
135
|
+
const blob = new IDLBlob('/test/source', '/test/target', 'TestComponent', 'test', '');
|
|
136
|
+
|
|
137
|
+
const properties = new ClassObject();
|
|
138
|
+
properties.name = 'TestComponentProperties';
|
|
139
|
+
properties.kind = ClassObjectKind.interface;
|
|
140
|
+
blob.objects = [properties];
|
|
141
|
+
|
|
142
|
+
const result = generateReactComponent(blob);
|
|
143
|
+
|
|
144
|
+
// Should include standard HTML props
|
|
145
|
+
expect(result).toContain('id?: string;');
|
|
146
|
+
expect(result).toContain('style?: React.CSSProperties;');
|
|
147
|
+
expect(result).toContain('children?: React.ReactNode;');
|
|
148
|
+
expect(result).toContain('className?: string;');
|
|
149
|
+
|
|
150
|
+
// Props should have proper JSDoc comments
|
|
151
|
+
expect(result).toMatch(/\/\*\*\s*\n\s*\*\s*HTML id attribute\s*\n\s*\*\/\s*\n\s*id\?: string;/);
|
|
152
|
+
expect(result).toMatch(/\/\*\*\s*\n\s*\*\s*Additional CSS styles\s*\n\s*\*\/\s*\n\s*style\?: React\.CSSProperties;/);
|
|
153
|
+
expect(result).toMatch(/\/\*\*\s*\n\s*\*\s*Children elements\s*\n\s*\*\/\s*\n\s*children\?: React\.ReactNode;/);
|
|
154
|
+
expect(result).toMatch(/\/\*\*\s*\n\s*\*\s*Additional CSS class names\s*\n\s*\*\/\s*\n\s*className\?: string;/);
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
it('should include standard HTML props even when component has custom properties', () => {
|
|
158
|
+
const blob = new IDLBlob('/test/source', '/test/target', 'TestComponent', 'test', '');
|
|
159
|
+
|
|
160
|
+
const properties = new ClassObject();
|
|
161
|
+
properties.name = 'TestComponentProperties';
|
|
162
|
+
properties.kind = ClassObjectKind.interface;
|
|
163
|
+
const titleProp = new PropsDeclaration();
|
|
164
|
+
titleProp.name = 'title';
|
|
165
|
+
titleProp.type = { value: 'dom_string' };
|
|
166
|
+
titleProp.optional = false;
|
|
167
|
+
titleProp.documentation = 'The component title';
|
|
168
|
+
titleProp.readonly = false;
|
|
169
|
+
titleProp.typeMode = {};
|
|
170
|
+
|
|
171
|
+
const disabledProp = new PropsDeclaration();
|
|
172
|
+
disabledProp.name = 'disabled';
|
|
173
|
+
disabledProp.type = { value: 'boolean' };
|
|
174
|
+
disabledProp.optional = true;
|
|
175
|
+
disabledProp.documentation = 'Whether the component is disabled';
|
|
176
|
+
disabledProp.readonly = false;
|
|
177
|
+
disabledProp.typeMode = {};
|
|
178
|
+
|
|
179
|
+
properties.props = [titleProp, disabledProp];
|
|
180
|
+
blob.objects = [properties];
|
|
181
|
+
|
|
182
|
+
const result = generateReactComponent(blob);
|
|
183
|
+
|
|
184
|
+
// Should include custom props (dom_string is not converted in raw output)
|
|
185
|
+
expect(result).toContain('title: dom_string;');
|
|
186
|
+
expect(result).toContain('disabled?: boolean;');
|
|
187
|
+
|
|
188
|
+
// And still include standard HTML props
|
|
189
|
+
expect(result).toContain('id?: string;');
|
|
190
|
+
expect(result).toContain('style?: React.CSSProperties;');
|
|
191
|
+
expect(result).toContain('children?: React.ReactNode;');
|
|
192
|
+
expect(result).toContain('className?: string;');
|
|
193
|
+
});
|
|
133
194
|
});
|
|
134
195
|
});
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
import { generateReactComponent } from '../src/react';
|
|
2
|
+
import { generateVueTypings } from '../src/vue';
|
|
3
|
+
import { IDLBlob } from '../src/IDLBlob';
|
|
4
|
+
import { ClassObject, ClassObjectKind, PropsDeclaration } from '../src/declaration';
|
|
5
|
+
|
|
6
|
+
describe('Standard HTML Props Generation', () => {
|
|
7
|
+
describe('React Components', () => {
|
|
8
|
+
it('should generate id prop in the correct position within the interface', () => {
|
|
9
|
+
const blob = new IDLBlob('/test/source', '/test/target', 'TestButton', 'test', '');
|
|
10
|
+
|
|
11
|
+
const properties = new ClassObject();
|
|
12
|
+
properties.name = 'TestButtonProperties';
|
|
13
|
+
properties.kind = ClassObjectKind.interface;
|
|
14
|
+
const labelProp = new PropsDeclaration();
|
|
15
|
+
labelProp.name = 'label';
|
|
16
|
+
labelProp.type = { value: 'dom_string' };
|
|
17
|
+
labelProp.optional = false;
|
|
18
|
+
labelProp.readonly = false;
|
|
19
|
+
labelProp.typeMode = {};
|
|
20
|
+
|
|
21
|
+
const variantProp = new PropsDeclaration();
|
|
22
|
+
variantProp.name = 'variant';
|
|
23
|
+
variantProp.type = { value: 'dom_string' };
|
|
24
|
+
variantProp.optional = true;
|
|
25
|
+
variantProp.readonly = false;
|
|
26
|
+
variantProp.typeMode = {};
|
|
27
|
+
|
|
28
|
+
properties.props = [labelProp, variantProp];
|
|
29
|
+
|
|
30
|
+
const events = new ClassObject();
|
|
31
|
+
events.name = 'TestButtonEvents';
|
|
32
|
+
events.kind = ClassObjectKind.interface;
|
|
33
|
+
const clickProp = new PropsDeclaration();
|
|
34
|
+
clickProp.name = 'click';
|
|
35
|
+
clickProp.type = { value: 'Event', isArray: false };
|
|
36
|
+
clickProp.optional = true;
|
|
37
|
+
clickProp.readonly = false;
|
|
38
|
+
clickProp.typeMode = {};
|
|
39
|
+
|
|
40
|
+
events.props = [clickProp];
|
|
41
|
+
|
|
42
|
+
blob.objects = [properties, events];
|
|
43
|
+
|
|
44
|
+
const result = generateReactComponent(blob);
|
|
45
|
+
|
|
46
|
+
// Verify the props interface structure - extract full content including newlines
|
|
47
|
+
const propsStart = result.indexOf('export interface TestButtonProps {');
|
|
48
|
+
const propsEnd = result.indexOf('}', propsStart) + 1;
|
|
49
|
+
const propsContent = result.substring(propsStart, propsEnd);
|
|
50
|
+
|
|
51
|
+
// Verify order: custom props, event handlers, then standard HTML props
|
|
52
|
+
const labelIndex = propsContent.indexOf('label: dom_string;');
|
|
53
|
+
const variantIndex = propsContent.indexOf('variant?: dom_string;');
|
|
54
|
+
const onClickIndex = propsContent.indexOf('onClick?: (event: Event) => void;');
|
|
55
|
+
const idIndex = propsContent.indexOf('id?: string;');
|
|
56
|
+
const styleIndex = propsContent.indexOf('style?: React.CSSProperties;');
|
|
57
|
+
const childrenIndex = propsContent.indexOf('children?: React.ReactNode;');
|
|
58
|
+
const classNameIndex = propsContent.indexOf('className?: string;');
|
|
59
|
+
|
|
60
|
+
// All props should exist
|
|
61
|
+
expect(labelIndex).toBeGreaterThan(-1);
|
|
62
|
+
expect(variantIndex).toBeGreaterThan(-1);
|
|
63
|
+
expect(onClickIndex).toBeGreaterThan(-1);
|
|
64
|
+
expect(idIndex).toBeGreaterThan(-1);
|
|
65
|
+
expect(styleIndex).toBeGreaterThan(-1);
|
|
66
|
+
expect(childrenIndex).toBeGreaterThan(-1);
|
|
67
|
+
expect(classNameIndex).toBeGreaterThan(-1);
|
|
68
|
+
|
|
69
|
+
// Verify order
|
|
70
|
+
expect(labelIndex).toBeLessThan(variantIndex);
|
|
71
|
+
expect(variantIndex).toBeLessThan(onClickIndex);
|
|
72
|
+
expect(onClickIndex).toBeLessThan(idIndex);
|
|
73
|
+
expect(idIndex).toBeLessThan(styleIndex);
|
|
74
|
+
expect(styleIndex).toBeLessThan(childrenIndex);
|
|
75
|
+
expect(childrenIndex).toBeLessThan(classNameIndex);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it('should properly type the standard props', () => {
|
|
79
|
+
const blob = new IDLBlob('/test/source', '/test/target', 'SimpleComponent', 'test', '');
|
|
80
|
+
|
|
81
|
+
const properties = new ClassObject();
|
|
82
|
+
properties.name = 'SimpleComponentProperties';
|
|
83
|
+
properties.kind = ClassObjectKind.interface;
|
|
84
|
+
blob.objects = [properties];
|
|
85
|
+
|
|
86
|
+
const result = generateReactComponent(blob);
|
|
87
|
+
|
|
88
|
+
// Verify exact type definitions
|
|
89
|
+
expect(result).toMatch(/id\?: string;/);
|
|
90
|
+
expect(result).toMatch(/style\?: React\.CSSProperties;/);
|
|
91
|
+
expect(result).toMatch(/children\?: React\.ReactNode;/);
|
|
92
|
+
expect(result).toMatch(/className\?: string;/);
|
|
93
|
+
});
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
describe('Vue Components', () => {
|
|
97
|
+
it('should generate standard HTML props with correct Vue naming conventions', () => {
|
|
98
|
+
const blob = new IDLBlob('/test/source', '/test/target', 'VueButton', 'test', '');
|
|
99
|
+
|
|
100
|
+
const properties = new ClassObject();
|
|
101
|
+
properties.name = 'VueButtonProperties';
|
|
102
|
+
properties.kind = ClassObjectKind.interface;
|
|
103
|
+
const labelProp = new PropsDeclaration();
|
|
104
|
+
labelProp.name = 'label';
|
|
105
|
+
labelProp.type = { value: 'dom_string' };
|
|
106
|
+
labelProp.optional = false;
|
|
107
|
+
labelProp.readonly = false;
|
|
108
|
+
labelProp.typeMode = {};
|
|
109
|
+
|
|
110
|
+
const isDisabledProp = new PropsDeclaration();
|
|
111
|
+
isDisabledProp.name = 'isDisabled';
|
|
112
|
+
isDisabledProp.type = { value: 'boolean' };
|
|
113
|
+
isDisabledProp.optional = true;
|
|
114
|
+
isDisabledProp.readonly = false;
|
|
115
|
+
isDisabledProp.typeMode = {};
|
|
116
|
+
|
|
117
|
+
properties.props = [labelProp, isDisabledProp];
|
|
118
|
+
|
|
119
|
+
blob.objects = [properties];
|
|
120
|
+
|
|
121
|
+
const result = generateVueTypings([blob]);
|
|
122
|
+
|
|
123
|
+
// Verify Props type includes custom and standard props - extract full content
|
|
124
|
+
const propsStart = result.indexOf('export type VueButtonProps = {');
|
|
125
|
+
const propsEnd = result.indexOf('}', propsStart) + 1;
|
|
126
|
+
const propsContent = result.substring(propsStart, propsEnd);
|
|
127
|
+
|
|
128
|
+
// Custom props should be kebab-case (dom_string is not converted)
|
|
129
|
+
expect(propsContent).toContain("'label': dom_string;");
|
|
130
|
+
expect(propsContent).toContain("'is-disabled'?: boolean;");
|
|
131
|
+
|
|
132
|
+
// Standard HTML props
|
|
133
|
+
expect(propsContent).toContain("'id'?: string;");
|
|
134
|
+
expect(propsContent).toContain("'class'?: string;");
|
|
135
|
+
expect(propsContent).toContain("'style'?: string | Record<string, any>;");
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it('should handle Vue style prop with both string and object types', () => {
|
|
139
|
+
const blob = new IDLBlob('/test/source', '/test/target', 'StyledComponent', 'test', '');
|
|
140
|
+
|
|
141
|
+
const properties = new ClassObject();
|
|
142
|
+
properties.name = 'StyledComponentProperties';
|
|
143
|
+
properties.kind = ClassObjectKind.interface;
|
|
144
|
+
blob.objects = [properties];
|
|
145
|
+
|
|
146
|
+
const result = generateVueTypings([blob]);
|
|
147
|
+
|
|
148
|
+
// Vue style prop should accept both string and object
|
|
149
|
+
expect(result).toMatch(/'style'\?: string \| Record<string, any>;/);
|
|
150
|
+
});
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
describe('Cross-framework consistency', () => {
|
|
154
|
+
it('should generate equivalent props for both React and Vue', () => {
|
|
155
|
+
const blob = new IDLBlob('/test/source', '/test/target', 'CrossFrameworkComponent', 'test', '');
|
|
156
|
+
|
|
157
|
+
const properties = new ClassObject();
|
|
158
|
+
properties.name = 'CrossFrameworkComponentProperties';
|
|
159
|
+
properties.kind = ClassObjectKind.interface;
|
|
160
|
+
const titleProp = new PropsDeclaration();
|
|
161
|
+
titleProp.name = 'title';
|
|
162
|
+
titleProp.type = { value: 'dom_string' };
|
|
163
|
+
titleProp.optional = false;
|
|
164
|
+
titleProp.readonly = false;
|
|
165
|
+
titleProp.typeMode = {};
|
|
166
|
+
|
|
167
|
+
properties.props = [titleProp];
|
|
168
|
+
blob.objects = [properties];
|
|
169
|
+
|
|
170
|
+
const reactResult = generateReactComponent(blob);
|
|
171
|
+
const vueResult = generateVueTypings([blob]);
|
|
172
|
+
|
|
173
|
+
// Both should have id prop
|
|
174
|
+
expect(reactResult).toContain('id?: string;');
|
|
175
|
+
expect(vueResult).toContain("'id'?: string;");
|
|
176
|
+
|
|
177
|
+
// Both should have style prop (with appropriate types)
|
|
178
|
+
expect(reactResult).toContain('style?: React.CSSProperties;');
|
|
179
|
+
expect(vueResult).toContain("'style'?: string | Record<string, any>;");
|
|
180
|
+
|
|
181
|
+
// React has className, Vue has class
|
|
182
|
+
expect(reactResult).toContain('className?: string;');
|
|
183
|
+
expect(vueResult).toContain("'class'?: string;");
|
|
184
|
+
|
|
185
|
+
// React has children, Vue uses slots (not in props)
|
|
186
|
+
expect(reactResult).toContain('children?: React.ReactNode;');
|
|
187
|
+
expect(vueResult).not.toContain('children');
|
|
188
|
+
});
|
|
189
|
+
});
|
|
190
|
+
});
|
|
@@ -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
|
+
});
|