@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/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 events
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(properties?.methods, function(method, index) { %>
49
- <%= generateMethodDeclaration(method) %>
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
- * // Add example props here
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
+ });