@openwebf/webf 0.22.0 → 0.22.3

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.
@@ -20,7 +20,6 @@ mockFs.readFileSync = jest.fn().mockImplementation((filePath: any) => {
20
20
  if (pathStr.includes('react.package.json.tpl')) return '<%= packageName %> <%= version %> <%= description %>';
21
21
  if (pathStr.includes('react.tsconfig.json.tpl')) return 'react tsconfig';
22
22
  if (pathStr.includes('react.tsup.config.ts.tpl')) return 'tsup config';
23
- if (pathStr.includes('react.createComponent.tpl')) return 'create component';
24
23
  if (pathStr.includes('react.index.ts.tpl')) return 'index template';
25
24
  if (pathStr.includes('vue.package.json.tpl')) return '<%= packageName %> <%= version %> <%= description %>';
26
25
  if (pathStr.includes('vue.tsconfig.json.tpl')) return 'vue tsconfig';
@@ -127,10 +126,6 @@ describe('Commands', () => {
127
126
  path.join(path.resolve(target), 'src'),
128
127
  { recursive: true }
129
128
  );
130
- expect(mockFs.mkdirSync).toHaveBeenCalledWith(
131
- path.join(path.resolve(target), 'src', 'utils'),
132
- { recursive: true }
133
- );
134
129
  });
135
130
 
136
131
  it('should prompt for framework and package name when missing', async () => {
@@ -72,7 +72,7 @@ describe('Generator', () => {
72
72
 
73
73
  mockDartGenerator.generateDartClass.mockReturnValue('dart code');
74
74
  mockReactGenerator.generateReactComponent.mockReturnValue('react component');
75
- mockReactGenerator.generateReactIndex.mockReturnValue('export * from "./test"');
75
+ mockReactGenerator.generateReactIndex.mockReturnValue('export { Component, ComponentElement } from "./lib/src/html/component";');
76
76
  mockVueGenerator.generateVueTypings.mockReturnValue('vue typings');
77
77
  });
78
78
 
@@ -86,7 +86,7 @@ describe('Generator', () => {
86
86
 
87
87
  expect(mockGlob.globSync).toHaveBeenCalledWith('**/*.d.ts', {
88
88
  cwd: '/test/source',
89
- ignore: ['**/node_modules/**', '**/dist/**', '**/build/**']
89
+ ignore: ['**/node_modules/**', '**/dist/**', '**/build/**', '**/example/**']
90
90
  });
91
91
 
92
92
  expect(mockAnalyzer.analyzer).toHaveBeenCalledTimes(2); // For each file
@@ -103,7 +103,7 @@ describe('Generator', () => {
103
103
 
104
104
  expect(mockGlob.globSync).toHaveBeenCalledWith('**/*.d.ts', {
105
105
  cwd: expect.stringContaining('relative/source'),
106
- ignore: ['**/node_modules/**', '**/dist/**', '**/build/**']
106
+ ignore: ['**/node_modules/**', '**/dist/**', '**/build/**', '**/example/**']
107
107
  });
108
108
  });
109
109
 
@@ -270,6 +270,20 @@ describe('Generator', () => {
270
270
  'utf-8'
271
271
  );
272
272
  });
273
+
274
+ it('should handle custom exclude patterns', async () => {
275
+ await dartGen({
276
+ source: '/test/source',
277
+ target: '/test/target',
278
+ command: 'test command',
279
+ exclude: ['**/test/**', '**/docs/**']
280
+ });
281
+
282
+ expect(mockGlob.globSync).toHaveBeenCalledWith('**/*.d.ts', {
283
+ cwd: '/test/source',
284
+ ignore: ['**/node_modules/**', '**/dist/**', '**/build/**', '**/example/**', '**/test/**', '**/docs/**']
285
+ });
286
+ });
273
287
  });
274
288
 
275
289
  describe('reactGen', () => {
@@ -317,6 +331,12 @@ describe('Generator', () => {
317
331
  });
318
332
 
319
333
  it('should generate index file', async () => {
334
+ // Update the mock to return proper format with export statements
335
+ mockReactGenerator.generateReactIndex.mockReturnValue(
336
+ 'export { Test, TestElement } from "./lib/src/html/test";\n' +
337
+ 'export { Component, ComponentElement } from "./lib/src/html/component";'
338
+ );
339
+
320
340
  await reactGen({
321
341
  source: '/test/source',
322
342
  target: '/test/target',
@@ -330,11 +350,12 @@ describe('Generator', () => {
330
350
  ])
331
351
  );
332
352
 
333
- expect(mockFs.writeFileSync).toHaveBeenCalledWith(
334
- expect.stringContaining('index.ts'),
335
- 'export * from "./test"',
336
- 'utf-8'
337
- );
353
+ // Check that index.ts was written (the content check is done by looking at the mock return value)
354
+ const writeCalls = mockFs.writeFileSync.mock.calls;
355
+ const indexCall = writeCalls.find(call => call[0].toString().includes('index.ts'));
356
+ expect(indexCall).toBeDefined();
357
+ expect(indexCall![1]).toContain('export { Test, TestElement }');
358
+ expect(indexCall![1]).toContain('export { Component, ComponentElement }');
338
359
  });
339
360
  });
340
361
 
@@ -0,0 +1,231 @@
1
+ // Mock fs module before importing commands
2
+ jest.mock('fs');
3
+
4
+ // Import the functions we want to test (we'll need to export them first)
5
+ // For now, let's just define them here for testing
6
+ function sanitizePackageName(name: string): string {
7
+ // Remove any leading/trailing whitespace
8
+ let sanitized = name.trim();
9
+
10
+ // Check if it's a scoped package
11
+ const isScoped = sanitized.startsWith('@');
12
+ let scope = '';
13
+ let packageName = sanitized;
14
+
15
+ if (isScoped) {
16
+ const parts = sanitized.split('/');
17
+ if (parts.length >= 2) {
18
+ scope = parts[0];
19
+ packageName = parts.slice(1).join('/');
20
+ } else {
21
+ // Invalid scoped package, treat as regular
22
+ packageName = sanitized.substring(1);
23
+ }
24
+ }
25
+
26
+ // Sanitize scope if present
27
+ if (scope) {
28
+ scope = scope.toLowerCase();
29
+ // Remove invalid characters from scope (keep only @ and alphanumeric/hyphen)
30
+ scope = scope.replace(/[^@a-z0-9-]/g, '');
31
+ if (scope === '@') {
32
+ scope = '@pkg'; // Default scope if only @ remains
33
+ }
34
+ }
35
+
36
+ // Sanitize package name part
37
+ packageName = packageName.toLowerCase();
38
+ packageName = packageName.replace(/\s+/g, '-');
39
+ packageName = packageName.replace(/[^a-z0-9\-_.]/g, '');
40
+ packageName = packageName.replace(/^[._]+/, '');
41
+ packageName = packageName.replace(/[._]+$/, '');
42
+ packageName = packageName.replace(/[-_.]{2,}/g, '-');
43
+ packageName = packageName.replace(/^-+/, '').replace(/-+$/, '');
44
+
45
+ // Ensure package name is not empty
46
+ if (!packageName) {
47
+ packageName = 'package';
48
+ }
49
+
50
+ // Ensure it starts with a letter or number
51
+ if (!/^[a-z0-9]/.test(packageName)) {
52
+ packageName = 'pkg-' + packageName;
53
+ }
54
+
55
+ // Combine scope and package name
56
+ let result = scope ? `${scope}/${packageName}` : packageName;
57
+
58
+ // Truncate to 214 characters (npm limit)
59
+ if (result.length > 214) {
60
+ if (scope) {
61
+ // Try to preserve scope
62
+ const maxPackageLength = 214 - scope.length - 1; // -1 for the /
63
+ packageName = packageName.substring(0, maxPackageLength);
64
+ packageName = packageName.replace(/[._-]+$/, '');
65
+ result = `${scope}/${packageName}`;
66
+ } else {
67
+ result = result.substring(0, 214);
68
+ result = result.replace(/[._-]+$/, '');
69
+ }
70
+ }
71
+
72
+ return result;
73
+ }
74
+
75
+ function isValidNpmPackageName(name: string): boolean {
76
+ // Check basic rules
77
+ if (!name || name.length === 0 || name.length > 214) return false;
78
+ if (name.trim() !== name) return false;
79
+
80
+ // Check if it's a scoped package
81
+ if (name.startsWith('@')) {
82
+ const parts = name.split('/');
83
+ if (parts.length !== 2) return false; // Scoped packages must have exactly one /
84
+
85
+ const scope = parts[0];
86
+ const packageName = parts[1];
87
+
88
+ // Validate scope
89
+ if (!/^@[a-z0-9][a-z0-9-]*$/.test(scope)) return false;
90
+
91
+ // Validate package name part
92
+ return isValidNpmPackageName(packageName);
93
+ }
94
+
95
+ // For non-scoped packages
96
+ if (name !== name.toLowerCase()) return false;
97
+ if (name.startsWith('.') || name.startsWith('_')) return false;
98
+
99
+ // Check for valid characters (letters, numbers, hyphens, underscores, dots)
100
+ if (!/^[a-z0-9][a-z0-9\-_.]*$/.test(name)) return false;
101
+
102
+ // Check for URL-safe characters
103
+ try {
104
+ if (encodeURIComponent(name) !== name) return false;
105
+ } catch {
106
+ return false;
107
+ }
108
+
109
+ return true;
110
+ }
111
+
112
+ describe('npm package name sanitization', () => {
113
+ describe('sanitizePackageName', () => {
114
+ it('should convert uppercase to lowercase', () => {
115
+ expect(sanitizePackageName('MyPackage')).toBe('mypackage');
116
+ expect(sanitizePackageName('HELLO-WORLD')).toBe('hello-world');
117
+ });
118
+
119
+ it('should replace spaces with hyphens', () => {
120
+ expect(sanitizePackageName('my package')).toBe('my-package');
121
+ expect(sanitizePackageName('hello world app')).toBe('hello-world-app');
122
+ });
123
+
124
+ it('should remove invalid characters', () => {
125
+ expect(sanitizePackageName('my@package!')).toBe('mypackage');
126
+ expect(sanitizePackageName('hello#world$')).toBe('helloworld');
127
+ expect(sanitizePackageName('test(1)_package')).toBe('test1_package');
128
+ });
129
+
130
+ it('should remove leading dots and underscores', () => {
131
+ expect(sanitizePackageName('.package')).toBe('package');
132
+ expect(sanitizePackageName('_package')).toBe('package');
133
+ expect(sanitizePackageName('...package')).toBe('package');
134
+ });
135
+
136
+ it('should remove trailing dots and underscores', () => {
137
+ expect(sanitizePackageName('package.')).toBe('package');
138
+ expect(sanitizePackageName('package_')).toBe('package');
139
+ expect(sanitizePackageName('package...')).toBe('package');
140
+ });
141
+
142
+ it('should handle consecutive special characters', () => {
143
+ expect(sanitizePackageName('my--package')).toBe('my-package');
144
+ expect(sanitizePackageName('hello___world')).toBe('hello-world');
145
+ expect(sanitizePackageName('test...app')).toBe('test-app');
146
+ });
147
+
148
+ it('should ensure package starts with letter or number', () => {
149
+ expect(sanitizePackageName('-package')).toBe('package');
150
+ expect(sanitizePackageName('--package')).toBe('package');
151
+ expect(sanitizePackageName('_test')).toBe('test');
152
+ expect(sanitizePackageName('.app')).toBe('app');
153
+ expect(sanitizePackageName('---')).toBe('package'); // all special chars removed, falls back to default
154
+ expect(sanitizePackageName('-')).toBe('package'); // single hyphen removed, falls back to default
155
+ });
156
+
157
+ it('should handle empty or invalid input', () => {
158
+ expect(sanitizePackageName('')).toBe('package');
159
+ expect(sanitizePackageName(' ')).toBe('package');
160
+ expect(sanitizePackageName('___')).toBe('package');
161
+ });
162
+
163
+ it('should truncate long names', () => {
164
+ const longName = 'a'.repeat(220);
165
+ const result = sanitizePackageName(longName);
166
+ expect(result.length).toBeLessThanOrEqual(214);
167
+ expect(result).toBe('a'.repeat(214));
168
+ });
169
+
170
+ it('should handle Flutter package names', () => {
171
+ expect(sanitizePackageName('webf_cupertino_ui')).toBe('webf_cupertino_ui');
172
+ expect(sanitizePackageName('WebF Cupertino UI')).toBe('webf-cupertino-ui');
173
+ expect(sanitizePackageName('flutter_package_name')).toBe('flutter_package_name');
174
+ });
175
+
176
+ it('should handle scoped packages', () => {
177
+ expect(sanitizePackageName('@openwebf/react-cupertino-ui')).toBe('@openwebf/react-cupertino-ui');
178
+ expect(sanitizePackageName('@OpenWebF/React-Cupertino-UI')).toBe('@openwebf/react-cupertino-ui');
179
+ expect(sanitizePackageName('@my-org/my-package')).toBe('@my-org/my-package');
180
+ expect(sanitizePackageName(' @scope/package ')).toBe('@scope/package');
181
+ expect(sanitizePackageName('@SCOPE/PACKAGE')).toBe('@scope/package');
182
+ expect(sanitizePackageName('@/package')).toBe('@pkg/package'); // Invalid scope
183
+ expect(sanitizePackageName('@123/package')).toBe('@123/package'); // Numeric scope is valid
184
+ expect(sanitizePackageName('@scope/my package')).toBe('@scope/my-package');
185
+ expect(sanitizePackageName('@scope/.package')).toBe('@scope/package');
186
+ expect(sanitizePackageName('@scope/_package')).toBe('@scope/package');
187
+ });
188
+ });
189
+
190
+ describe('isValidNpmPackageName', () => {
191
+ it('should accept valid package names', () => {
192
+ expect(isValidNpmPackageName('mypackage')).toBe(true);
193
+ expect(isValidNpmPackageName('my-package')).toBe(true);
194
+ expect(isValidNpmPackageName('my_package')).toBe(true);
195
+ expect(isValidNpmPackageName('my.package')).toBe(true);
196
+ expect(isValidNpmPackageName('package123')).toBe(true);
197
+ expect(isValidNpmPackageName('@openwebf/react-cupertino-ui')).toBe(true);
198
+ expect(isValidNpmPackageName('@scope/package')).toBe(true);
199
+ expect(isValidNpmPackageName('@my-org/my-package')).toBe(true);
200
+ expect(isValidNpmPackageName('@123/package')).toBe(true);
201
+ });
202
+
203
+ it('should reject invalid package names', () => {
204
+ expect(isValidNpmPackageName('MyPackage')).toBe(false); // uppercase
205
+ expect(isValidNpmPackageName('my package')).toBe(false); // space
206
+ expect(isValidNpmPackageName('.package')).toBe(false); // starts with dot
207
+ expect(isValidNpmPackageName('_package')).toBe(false); // starts with underscore
208
+ expect(isValidNpmPackageName('my@package')).toBe(false); // special char in wrong position
209
+ expect(isValidNpmPackageName('')).toBe(false); // empty
210
+ expect(isValidNpmPackageName(' package ')).toBe(false); // leading/trailing space
211
+ expect(isValidNpmPackageName('@')).toBe(false); // just @
212
+ expect(isValidNpmPackageName('@/')).toBe(false); // missing both parts
213
+ expect(isValidNpmPackageName('@scope')).toBe(false); // missing package name
214
+ expect(isValidNpmPackageName('@/package')).toBe(false); // missing scope name
215
+ expect(isValidNpmPackageName('@Scope/package')).toBe(false); // uppercase in scope
216
+ expect(isValidNpmPackageName('@scope/Package')).toBe(false); // uppercase in package
217
+ expect(isValidNpmPackageName('@scope/.package')).toBe(false); // package starts with dot
218
+ expect(isValidNpmPackageName('@scope/_package')).toBe(false); // package starts with underscore
219
+ });
220
+
221
+ it('should reject names that are too long', () => {
222
+ const longName = 'a'.repeat(215);
223
+ expect(isValidNpmPackageName(longName)).toBe(false);
224
+ });
225
+
226
+ it('should reject non-URL-safe names', () => {
227
+ expect(isValidNpmPackageName('my package')).toBe(false);
228
+ expect(isValidNpmPackageName('package?name')).toBe(false);
229
+ });
230
+ });
231
+ });
@@ -2,9 +2,32 @@ import { generateReactComponent } from '../src/react';
2
2
  import { IDLBlob } from '../src/IDLBlob';
3
3
  import { ClassObject, ClassObjectKind } from '../src/declaration';
4
4
 
5
+ // Import the toWebFTagName function for testing
6
+ import { toWebFTagName } from '../src/react';
7
+
5
8
  describe('React Generator', () => {
9
+ describe('toWebFTagName', () => {
10
+ it('should convert WebF prefixed components correctly', () => {
11
+ expect(toWebFTagName('WebFTable')).toBe('webf-table');
12
+ expect(toWebFTagName('WebFTableCell')).toBe('webf-table-cell');
13
+ expect(toWebFTagName('WebFListView')).toBe('webf-list-view');
14
+ expect(toWebFTagName('WebFTouchArea')).toBe('webf-touch-area');
15
+ });
16
+
17
+ it('should convert Flutter prefixed components correctly', () => {
18
+ expect(toWebFTagName('FlutterShimmer')).toBe('flutter-shimmer');
19
+ expect(toWebFTagName('FlutterShimmerText')).toBe('flutter-shimmer-text');
20
+ expect(toWebFTagName('FlutterShimmerAvatar')).toBe('flutter-shimmer-avatar');
21
+ });
22
+
23
+ it('should handle components without special prefixes', () => {
24
+ expect(toWebFTagName('CustomComponent')).toBe('custom-component');
25
+ expect(toWebFTagName('MyElement')).toBe('my-element');
26
+ });
27
+ });
28
+
6
29
  describe('generateReactComponent', () => {
7
- it('should use correct import path for createComponent in subdirectories', () => {
30
+ it('should import createWebFComponent from @openwebf/react-core-ui', () => {
8
31
  const blob = new IDLBlob('/test/source', '/test/target', 'TestComponent', 'test', 'components/ui');
9
32
 
10
33
  const properties = new ClassObject();
@@ -14,11 +37,11 @@ describe('React Generator', () => {
14
37
 
15
38
  const result = generateReactComponent(blob);
16
39
 
17
- // Component in components/ui/ needs to go up 2 levels
18
- expect(result).toContain('import { createComponent } from "../../utils/createComponent"');
40
+ // Should import from npm package by default
41
+ expect(result).toContain('import { createWebFComponent, WebFElementWithMethods } from "@openwebf/react-core-ui"');
19
42
  });
20
43
 
21
- it('should use correct import path for createComponent in root directory', () => {
44
+ it('should generate component using createWebFComponent', () => {
22
45
  const blob = new IDLBlob('/test/source', '/test/target', 'TestComponent', 'test', '');
23
46
 
24
47
  const properties = new ClassObject();
@@ -28,11 +51,13 @@ describe('React Generator', () => {
28
51
 
29
52
  const result = generateReactComponent(blob);
30
53
 
31
- // Component in root needs simple relative path
32
- expect(result).toContain('import { createComponent } from "./utils/createComponent"');
54
+ // Should use createWebFComponent
55
+ expect(result).toContain('export const TestComponent = createWebFComponent<TestComponentElement, TestComponentProps>({');
56
+ expect(result).toContain('tagName: \'test-component\'');
57
+ expect(result).toContain('displayName: \'TestComponent\'');
33
58
  });
34
59
 
35
- it('should use correct import path for single level subdirectory', () => {
60
+ it('should generate proper TypeScript interfaces', () => {
36
61
  const blob = new IDLBlob('/test/source', '/test/target', 'TestComponent', 'test', 'widgets');
37
62
 
38
63
  const properties = new ClassObject();
@@ -42,8 +67,68 @@ describe('React Generator', () => {
42
67
 
43
68
  const result = generateReactComponent(blob);
44
69
 
45
- // Component in widgets/ needs to go up 1 level
46
- expect(result).toContain('import { createComponent } from "../utils/createComponent"');
70
+ // Should have proper interfaces
71
+ expect(result).toContain('export interface TestComponentProps {');
72
+ expect(result).toContain('export interface TestComponentElement extends WebFElementWithMethods<{');
73
+ });
74
+
75
+ it('should generate correct tagName for WebF components', () => {
76
+ const blob = new IDLBlob('/test/source', '/test/target', 'WebFTableCell', 'test', '');
77
+
78
+ const properties = new ClassObject();
79
+ properties.name = 'WebFTableCellProperties';
80
+ properties.kind = ClassObjectKind.interface;
81
+ blob.objects = [properties];
82
+
83
+ const result = generateReactComponent(blob);
84
+
85
+ // Should generate correct tagName
86
+ expect(result).toContain('tagName: \'webf-table-cell\'');
87
+ expect(result).not.toContain('tagName: \'web-f-table-cell\'');
88
+ });
89
+
90
+ it('should generate correct tagName for Flutter components', () => {
91
+ const blob = new IDLBlob('/test/source', '/test/target', 'FlutterShimmerText', 'test', '');
92
+
93
+ const properties = new ClassObject();
94
+ properties.name = 'FlutterShimmerTextProperties';
95
+ properties.kind = ClassObjectKind.interface;
96
+ blob.objects = [properties];
97
+
98
+ const result = generateReactComponent(blob);
99
+
100
+ // Should generate correct tagName
101
+ expect(result).toContain('tagName: \'flutter-shimmer-text\'');
102
+ });
103
+
104
+ it('should use relative import for @openwebf/react-core-ui package', () => {
105
+ const blob = new IDLBlob('/test/source', '/test/target', 'TestComponent', 'test', 'lib/src/html');
106
+
107
+ const properties = new ClassObject();
108
+ properties.name = 'TestComponentProperties';
109
+ properties.kind = ClassObjectKind.interface;
110
+ blob.objects = [properties];
111
+
112
+ const result = generateReactComponent(blob, '@openwebf/react-core-ui', 'lib/src/html');
113
+
114
+ // Should use relative import for react-core-ui package itself
115
+ // From src/lib/src/html to src/utils: ../../../utils
116
+ expect(result).toContain('import { createWebFComponent, WebFElementWithMethods } from "../../../utils/createWebFComponent"');
117
+ });
118
+
119
+ it('should use relative import for nested directories in @openwebf/react-core-ui', () => {
120
+ const blob = new IDLBlob('/test/source', '/test/target', 'TestComponent', 'test', 'lib/src/html/shimmer');
121
+
122
+ const properties = new ClassObject();
123
+ properties.name = 'TestComponentProperties';
124
+ properties.kind = ClassObjectKind.interface;
125
+ blob.objects = [properties];
126
+
127
+ const result = generateReactComponent(blob, '@openwebf/react-core-ui', 'lib/src/html/shimmer');
128
+
129
+ // Should use relative import with correct depth
130
+ // From src/lib/src/html/shimmer to src/utils: ../../../../utils
131
+ expect(result).toContain('import { createWebFComponent, WebFElementWithMethods } from "../../../../utils/createWebFComponent"');
47
132
  });
48
133
  });
49
134
  });