@openwebf/webf 0.22.1 → 0.22.4
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/bin/webf.js +1 -0
- package/dist/IDLBlob.js +17 -0
- package/dist/analyzer.js +578 -0
- package/dist/analyzer_original.js +467 -0
- package/dist/commands.js +704 -0
- package/dist/dart.js +300 -0
- package/dist/declaration.js +63 -0
- package/dist/generator.js +466 -0
- package/dist/logger.js +103 -0
- package/dist/react.js +283 -0
- package/dist/utils.js +127 -0
- package/dist/vue.js +159 -0
- package/package.json +8 -1
- package/src/IDLBlob.ts +2 -2
- package/src/analyzer.ts +19 -1
- package/src/commands.ts +201 -22
- package/src/dart.ts +172 -11
- package/src/declaration.ts +5 -0
- package/src/generator.ts +82 -14
- package/src/react.ts +197 -62
- package/templates/class.dart.tpl +10 -4
- package/templates/gitignore.tpl +8 -1
- package/templates/react.component.tsx.tpl +78 -26
- package/templates/react.index.ts.tpl +0 -1
- package/templates/react.package.json.tpl +3 -0
- package/test/commands.test.ts +0 -5
- package/test/generator.test.ts +29 -8
- package/test/packageName.test.ts +231 -0
- package/test/react.test.ts +94 -9
- package/CLAUDE.md +0 -206
- package/README-zhCN.md +0 -256
- package/coverage/clover.xml +0 -1295
- package/coverage/coverage-final.json +0 -12
- package/coverage/lcov-report/IDLBlob.ts.html +0 -142
- package/coverage/lcov-report/analyzer.ts.html +0 -2158
- package/coverage/lcov-report/analyzer_original.ts.html +0 -1450
- package/coverage/lcov-report/base.css +0 -224
- package/coverage/lcov-report/block-navigation.js +0 -87
- package/coverage/lcov-report/commands.ts.html +0 -700
- package/coverage/lcov-report/dart.ts.html +0 -490
- package/coverage/lcov-report/declaration.ts.html +0 -337
- package/coverage/lcov-report/favicon.png +0 -0
- package/coverage/lcov-report/generator.ts.html +0 -1171
- package/coverage/lcov-report/index.html +0 -266
- package/coverage/lcov-report/logger.ts.html +0 -424
- package/coverage/lcov-report/prettify.css +0 -1
- package/coverage/lcov-report/prettify.js +0 -2
- package/coverage/lcov-report/react.ts.html +0 -619
- package/coverage/lcov-report/sort-arrow-sprite.png +0 -0
- package/coverage/lcov-report/sorter.js +0 -196
- package/coverage/lcov-report/utils.ts.html +0 -466
- package/coverage/lcov-report/vue.ts.html +0 -613
- package/coverage/lcov.info +0 -2149
- package/global.d.ts +0 -2
- package/jest.config.js +0 -24
- package/templates/react.createComponent.tpl +0 -286
- package/tsconfig.json +0 -30
package/test/commands.test.ts
CHANGED
|
@@ -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 () => {
|
package/test/generator.test.ts
CHANGED
|
@@ -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
|
|
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
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
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
|
+
});
|
package/test/react.test.ts
CHANGED
|
@@ -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
|
|
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
|
-
//
|
|
18
|
-
expect(result).toContain('import {
|
|
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
|
|
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
|
-
//
|
|
32
|
-
expect(result).toContain('
|
|
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
|
|
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
|
-
//
|
|
46
|
-
expect(result).toContain('
|
|
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
|
});
|
package/CLAUDE.md
DELETED
|
@@ -1,206 +0,0 @@
|
|
|
1
|
-
# WebF CLI Development Guide
|
|
2
|
-
|
|
3
|
-
## Overview
|
|
4
|
-
The WebF CLI is a code generation tool that creates type-safe bindings between Flutter/Dart and JavaScript frameworks (React, Vue). It analyzes TypeScript definition files and generates corresponding Dart classes and JavaScript/TypeScript components.
|
|
5
|
-
|
|
6
|
-
## Architecture
|
|
7
|
-
|
|
8
|
-
### Core Components
|
|
9
|
-
- `analyzer.ts` - TypeScript AST analysis with multi-level caching
|
|
10
|
-
- `generator.ts` - Orchestrates code generation for Dart, React, and Vue
|
|
11
|
-
- `dart.ts` - Dart code generation from TypeScript definitions
|
|
12
|
-
- `react.ts` - React component generation
|
|
13
|
-
- `vue.ts` - Vue component generation
|
|
14
|
-
- `commands.ts` - CLI command implementations
|
|
15
|
-
- `logger.ts` - Logging utility without external dependencies
|
|
16
|
-
|
|
17
|
-
### Key Features
|
|
18
|
-
- Multi-level caching for performance optimization
|
|
19
|
-
- Parallel file processing
|
|
20
|
-
- Type-safe attribute handling with automatic conversions
|
|
21
|
-
- Comprehensive error handling and recovery
|
|
22
|
-
|
|
23
|
-
## Code Generation Patterns
|
|
24
|
-
|
|
25
|
-
### TypeScript to Dart Type Mapping
|
|
26
|
-
```typescript
|
|
27
|
-
// Boolean attributes are always non-nullable in Dart
|
|
28
|
-
interface Props {
|
|
29
|
-
open?: boolean; // Generates: bool get open;
|
|
30
|
-
title?: string; // Generates: String? get title;
|
|
31
|
-
}
|
|
32
|
-
```
|
|
33
|
-
|
|
34
|
-
### Attribute Type Conversion
|
|
35
|
-
HTML attributes are always strings, so the generator includes automatic type conversion:
|
|
36
|
-
- Boolean: `value == 'true' || value == ''`
|
|
37
|
-
- Integer: `int.tryParse(value) ?? 0`
|
|
38
|
-
- Double: `double.tryParse(value) ?? 0.0`
|
|
39
|
-
- String: Direct assignment
|
|
40
|
-
|
|
41
|
-
## Performance Optimizations
|
|
42
|
-
|
|
43
|
-
### Caching Strategy
|
|
44
|
-
1. **Source File Cache**: Parsed TypeScript AST files are cached
|
|
45
|
-
2. **Type Conversion Cache**: Frequently used type conversions are cached
|
|
46
|
-
3. **File Content Cache**: File contents are cached to detect changes
|
|
47
|
-
|
|
48
|
-
### Batch Processing
|
|
49
|
-
Files are processed in batches for optimal parallelism:
|
|
50
|
-
```typescript
|
|
51
|
-
await processFilesInBatch(items, batchSize, processor);
|
|
52
|
-
```
|
|
53
|
-
|
|
54
|
-
## Testing Guidelines
|
|
55
|
-
|
|
56
|
-
### Test Structure
|
|
57
|
-
- Unit tests for all core modules
|
|
58
|
-
- Mock file system operations before module imports
|
|
59
|
-
- Test coverage threshold: 70%
|
|
60
|
-
|
|
61
|
-
### Running Tests
|
|
62
|
-
```bash
|
|
63
|
-
npm test # Run all tests
|
|
64
|
-
npm test -- test/analyzer.test.ts # Run specific test
|
|
65
|
-
npm run test:coverage # Run with coverage report
|
|
66
|
-
```
|
|
67
|
-
|
|
68
|
-
### Mock Patterns
|
|
69
|
-
For modules that read files at load time:
|
|
70
|
-
```typescript
|
|
71
|
-
jest.mock('fs');
|
|
72
|
-
import fs from 'fs';
|
|
73
|
-
const mockFs = fs as jest.Mocked<typeof fs>;
|
|
74
|
-
mockFs.readFileSync = jest.fn().mockImplementation((path) => {
|
|
75
|
-
// Return appropriate content
|
|
76
|
-
});
|
|
77
|
-
// Now import the module
|
|
78
|
-
import { moduleUnderTest } from './module';
|
|
79
|
-
```
|
|
80
|
-
|
|
81
|
-
## CLI Usage
|
|
82
|
-
|
|
83
|
-
### Commands
|
|
84
|
-
```bash
|
|
85
|
-
# Generate code from TypeScript definitions (auto-creates project if needed)
|
|
86
|
-
webf codegen <output-dir> --flutter-package-src=<path> [--framework=react|vue] [--package-name=<name>] [--publish-to-npm] [--npm-registry=<url>]
|
|
87
|
-
|
|
88
|
-
# Create a new project without code generation
|
|
89
|
-
webf codegen <output-dir> [--framework=react|vue] [--package-name=<name>]
|
|
90
|
-
|
|
91
|
-
# Generate and publish to npm
|
|
92
|
-
webf codegen <output-dir> --flutter-package-src=<path> --publish-to-npm
|
|
93
|
-
|
|
94
|
-
# Generate and publish to custom registry
|
|
95
|
-
webf codegen <output-dir> --flutter-package-src=<path> --publish-to-npm --npm-registry=https://custom.registry.com/
|
|
96
|
-
```
|
|
97
|
-
|
|
98
|
-
### Auto-creation Behavior
|
|
99
|
-
The `generate` command now automatically detects if a project needs to be created:
|
|
100
|
-
- If required files (package.json, global.d.ts, tsconfig.json) are missing, it will create a new project
|
|
101
|
-
- If framework or package name are not provided, it will prompt interactively
|
|
102
|
-
- If an existing project is detected, it will use the existing configuration
|
|
103
|
-
- Framework can be auto-detected from existing package.json dependencies
|
|
104
|
-
|
|
105
|
-
### Metadata Synchronization
|
|
106
|
-
When creating typing projects, the CLI automatically synchronizes metadata from the Flutter package:
|
|
107
|
-
- Reads `pubspec.yaml` from the Flutter package source directory
|
|
108
|
-
- Extracts version and description information
|
|
109
|
-
- Applies this metadata to the generated `package.json` files
|
|
110
|
-
- Ensures typing packages match the same version as the Flutter package
|
|
111
|
-
|
|
112
|
-
### Automatic Build
|
|
113
|
-
After code generation, the CLI automatically runs `npm run build` if a build script is present in the package.json. This ensures the generated package is immediately ready for use or publishing. The build process:
|
|
114
|
-
- Checks for the presence of a `build` script in package.json
|
|
115
|
-
- Runs the build command if available
|
|
116
|
-
- Continues successfully even if the build fails (with a warning)
|
|
117
|
-
- Provides clear console output about the build status
|
|
118
|
-
|
|
119
|
-
### NPM Publishing
|
|
120
|
-
The CLI supports automatic npm publishing with the following features:
|
|
121
|
-
- **--publish-to-npm**: Automatically publishes the generated package to npm (build is run automatically)
|
|
122
|
-
- **--npm-registry**: Specify a custom npm registry URL (defaults to https://registry.npmjs.org/)
|
|
123
|
-
- **Interactive publishing**: If not using the --publish-to-npm flag, the CLI will ask if you want to publish after generation
|
|
124
|
-
- **Registry configuration**: When choosing to publish interactively, you can specify a custom registry URL
|
|
125
|
-
- Checks if user is logged in before attempting to publish
|
|
126
|
-
- Temporarily sets and resets registry configuration when custom registry is used
|
|
127
|
-
|
|
128
|
-
Requirements for publishing:
|
|
129
|
-
- Must be logged in to npm (`npm login`)
|
|
130
|
-
- Package must have a valid package.json
|
|
131
|
-
- Package will be built automatically before publishing (if build script exists)
|
|
132
|
-
|
|
133
|
-
Publishing workflow:
|
|
134
|
-
1. If `--publish-to-npm` is not specified, CLI prompts after successful generation
|
|
135
|
-
2. If user chooses to publish, CLI asks for registry URL (optional)
|
|
136
|
-
3. Validates npm login status
|
|
137
|
-
4. Publishes to specified registry (no need to build separately)
|
|
138
|
-
|
|
139
|
-
### Output Directory Behavior
|
|
140
|
-
- Dart files are generated in the Flutter package source directory
|
|
141
|
-
- React/Vue files are generated in the specified output directory
|
|
142
|
-
- If no output directory is specified, a temporary directory is created
|
|
143
|
-
- Temporary directories are created in the system temp folder with prefix `webf-typings-`
|
|
144
|
-
- When using temporary directories, the path is displayed at the end of generation
|
|
145
|
-
- Paths can be absolute or relative to current working directory
|
|
146
|
-
|
|
147
|
-
### Generated Files Structure
|
|
148
|
-
When running `dartGen`:
|
|
149
|
-
- Dart binding files are generated with `_bindings_generated.dart` suffix
|
|
150
|
-
- Original `.d.ts` files are copied to the output directory maintaining their structure
|
|
151
|
-
- An `index.d.ts` file is generated with:
|
|
152
|
-
- TypeScript triple-slash references to all `.d.ts` files
|
|
153
|
-
- ES module exports for all type definitions
|
|
154
|
-
- Package metadata from `pubspec.yaml` in documentation comments
|
|
155
|
-
- Directory structure from source is preserved in the output
|
|
156
|
-
|
|
157
|
-
## Development Workflow
|
|
158
|
-
|
|
159
|
-
### Adding New Features
|
|
160
|
-
1. Update TypeScript interfaces/types
|
|
161
|
-
2. Implement feature with tests
|
|
162
|
-
3. Update templates if needed
|
|
163
|
-
4. Ensure all tests pass
|
|
164
|
-
5. Update this documentation
|
|
165
|
-
|
|
166
|
-
### Debugging
|
|
167
|
-
Use the logger for debugging:
|
|
168
|
-
```typescript
|
|
169
|
-
import { debug, info, warn, error } from './logger';
|
|
170
|
-
debug('Processing file:', filename);
|
|
171
|
-
```
|
|
172
|
-
|
|
173
|
-
### Template Modification
|
|
174
|
-
Templates are in `/templates/*.tpl`. When modifying:
|
|
175
|
-
1. Update the template file
|
|
176
|
-
2. Update the corresponding generator function
|
|
177
|
-
3. Ensure generated code follows style guidelines
|
|
178
|
-
|
|
179
|
-
## Common Issues and Solutions
|
|
180
|
-
|
|
181
|
-
### Issue: Boolean attributes treated as strings
|
|
182
|
-
**Solution**: Use `generateAttributeSetter` which handles type conversion
|
|
183
|
-
|
|
184
|
-
### Issue: Null type handling
|
|
185
|
-
**Solution**: Check for literal types containing null:
|
|
186
|
-
```typescript
|
|
187
|
-
if (type.kind === ts.SyntaxKind.LiteralType) {
|
|
188
|
-
const literalType = type as ts.LiteralTypeNode;
|
|
189
|
-
if (literalType.literal.kind === ts.SyntaxKind.NullKeyword) {
|
|
190
|
-
return FunctionArgumentType.null;
|
|
191
|
-
}
|
|
192
|
-
}
|
|
193
|
-
```
|
|
194
|
-
|
|
195
|
-
### Issue: File changes not detected
|
|
196
|
-
**Solution**: Clear caches before generation:
|
|
197
|
-
```typescript
|
|
198
|
-
clearCaches();
|
|
199
|
-
```
|
|
200
|
-
|
|
201
|
-
## Code Style
|
|
202
|
-
- Use async/await for asynchronous operations
|
|
203
|
-
- Implement proper error handling with try-catch
|
|
204
|
-
- Add descriptive error messages
|
|
205
|
-
- Use TypeScript strict mode
|
|
206
|
-
- Follow existing patterns in the codebase
|