@openwebf/webf 0.1.0

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.
Files changed (60) hide show
  1. package/CLAUDE.md +206 -0
  2. package/README-zhCN.md +256 -0
  3. package/README.md +232 -0
  4. package/bin/webf.js +25 -0
  5. package/coverage/clover.xml +1295 -0
  6. package/coverage/coverage-final.json +12 -0
  7. package/coverage/lcov-report/IDLBlob.ts.html +142 -0
  8. package/coverage/lcov-report/analyzer.ts.html +2158 -0
  9. package/coverage/lcov-report/analyzer_original.ts.html +1450 -0
  10. package/coverage/lcov-report/base.css +224 -0
  11. package/coverage/lcov-report/block-navigation.js +87 -0
  12. package/coverage/lcov-report/commands.ts.html +700 -0
  13. package/coverage/lcov-report/dart.ts.html +490 -0
  14. package/coverage/lcov-report/declaration.ts.html +337 -0
  15. package/coverage/lcov-report/favicon.png +0 -0
  16. package/coverage/lcov-report/generator.ts.html +1171 -0
  17. package/coverage/lcov-report/index.html +266 -0
  18. package/coverage/lcov-report/logger.ts.html +424 -0
  19. package/coverage/lcov-report/prettify.css +1 -0
  20. package/coverage/lcov-report/prettify.js +2 -0
  21. package/coverage/lcov-report/react.ts.html +619 -0
  22. package/coverage/lcov-report/sort-arrow-sprite.png +0 -0
  23. package/coverage/lcov-report/sorter.js +196 -0
  24. package/coverage/lcov-report/utils.ts.html +466 -0
  25. package/coverage/lcov-report/vue.ts.html +613 -0
  26. package/coverage/lcov.info +2149 -0
  27. package/global.d.ts +2 -0
  28. package/jest.config.js +24 -0
  29. package/package.json +36 -0
  30. package/src/IDLBlob.ts +20 -0
  31. package/src/analyzer.ts +692 -0
  32. package/src/commands.ts +645 -0
  33. package/src/dart.ts +170 -0
  34. package/src/declaration.ts +84 -0
  35. package/src/generator.ts +454 -0
  36. package/src/logger.ts +114 -0
  37. package/src/react.ts +186 -0
  38. package/src/utils.ts +127 -0
  39. package/src/vue.ts +176 -0
  40. package/templates/class.dart.tpl +86 -0
  41. package/templates/gitignore.tpl +2 -0
  42. package/templates/react.component.tsx.tpl +53 -0
  43. package/templates/react.createComponent.tpl +286 -0
  44. package/templates/react.index.ts.tpl +8 -0
  45. package/templates/react.package.json.tpl +26 -0
  46. package/templates/react.tsconfig.json.tpl +16 -0
  47. package/templates/react.tsup.config.ts.tpl +10 -0
  48. package/templates/tsconfig.json.tpl +8 -0
  49. package/templates/vue.component.partial.tpl +31 -0
  50. package/templates/vue.components.d.ts.tpl +49 -0
  51. package/templates/vue.package.json.tpl +11 -0
  52. package/templates/vue.tsconfig.json.tpl +15 -0
  53. package/test/IDLBlob.test.ts +75 -0
  54. package/test/analyzer.test.ts +370 -0
  55. package/test/commands.test.ts +1253 -0
  56. package/test/generator.test.ts +460 -0
  57. package/test/logger.test.ts +215 -0
  58. package/test/react.test.ts +49 -0
  59. package/test/utils.test.ts +316 -0
  60. package/tsconfig.json +30 -0
package/src/dart.ts ADDED
@@ -0,0 +1,170 @@
1
+ import _ from "lodash";
2
+ import fs from 'fs';
3
+ import path from 'path';
4
+ import {ParameterType} from "./analyzer";
5
+ import {ClassObject, FunctionArgumentType, FunctionDeclaration} from "./declaration";
6
+ import {IDLBlob} from "./IDLBlob";
7
+ import {getPointerType, isPointerType} from "./utils";
8
+
9
+ function readTemplate(name: string) {
10
+ return fs.readFileSync(path.join(__dirname, '../templates/' + name + '.tpl'), {encoding: 'utf-8'});
11
+ }
12
+
13
+ function generateReturnType(type: ParameterType) {
14
+ if (isPointerType(type)) {
15
+ const pointerType = getPointerType(type);
16
+ return pointerType;
17
+ }
18
+ if (type.isArray && typeof type.value === 'object' && !Array.isArray(type.value)) {
19
+ return `${getPointerType(type.value)}[]`;
20
+ }
21
+ switch (type.value) {
22
+ case FunctionArgumentType.int: {
23
+ return 'int';
24
+ }
25
+ case FunctionArgumentType.double: {
26
+ return 'double';
27
+ }
28
+ case FunctionArgumentType.any: {
29
+ return 'any';
30
+ }
31
+ case FunctionArgumentType.boolean: {
32
+ return 'bool';
33
+ }
34
+ case FunctionArgumentType.dom_string: {
35
+ return 'String';
36
+ }
37
+ case FunctionArgumentType.void:
38
+ return 'void';
39
+ default:
40
+ return 'void';
41
+ }
42
+ }
43
+
44
+ function generateEventHandlerType(type: ParameterType) {
45
+ if (!isPointerType(type)) {
46
+ throw new Error('Event type must be an instance of Event');
47
+ }
48
+ const pointerType = getPointerType(type);
49
+ if (pointerType === 'Event') {
50
+ return `EventHandler`;
51
+ }
52
+ if (pointerType === 'CustomEvent') {
53
+ return `EventHandler<CustomEvent>`;
54
+ }
55
+ throw new Error('Unknown event type: ' + pointerType);
56
+ }
57
+
58
+ function generateAttributeSetter(propName: string, type: ParameterType): string {
59
+ // Attributes from HTML are always strings, so we need to convert them
60
+ switch (type.value) {
61
+ case FunctionArgumentType.boolean:
62
+ return `${propName} = value == 'true' || value == ''`;
63
+ case FunctionArgumentType.int:
64
+ return `${propName} = int.tryParse(value) ?? 0`;
65
+ case FunctionArgumentType.double:
66
+ return `${propName} = double.tryParse(value) ?? 0.0`;
67
+ default:
68
+ // String and other types can be assigned directly
69
+ return `${propName} = value`;
70
+ }
71
+ }
72
+
73
+ function generateAttributeGetter(propName: string, type: ParameterType, optional: boolean): string {
74
+ // For non-nullable types, we might need to handle null values
75
+ if (type.value === FunctionArgumentType.boolean && optional) {
76
+ // For optional booleans that are non-nullable in Dart, default to false
77
+ return `${propName}.toString()`;
78
+ }
79
+ return `${propName}.toString()`;
80
+ }
81
+
82
+ function generateMethodDeclaration(method: FunctionDeclaration) {
83
+ var methodName = method.name;
84
+ var args = method.args.map(arg => {
85
+ var argName = arg.name;
86
+ var argType = generateReturnType(arg.type);
87
+ return `${argName}: ${argType}`;
88
+ }).join(', ');
89
+ var returnType = generateReturnType(method.returnType);
90
+ return `${methodName}(${args}): ${returnType};`;
91
+ }
92
+
93
+ function shouldMakeNullable(prop: any): boolean {
94
+ // Boolean properties should never be nullable in Dart, even if optional in TypeScript
95
+ if (prop.type.value === FunctionArgumentType.boolean) {
96
+ return false;
97
+ }
98
+ // Other optional properties remain nullable
99
+ return prop.optional;
100
+ }
101
+
102
+ export function generateDartClass(blob: IDLBlob, command: string): string {
103
+ const classObjects = blob.objects as ClassObject[];
104
+ const classObjectDictionary = Object.fromEntries(
105
+ classObjects.map(object => {
106
+ return [object.name, object];
107
+ })
108
+ );
109
+
110
+ const properties = classObjects.filter(object => {
111
+ return object.name.endsWith('Properties');
112
+ });
113
+ const events = classObjects.filter(object => {
114
+ return object.name.endsWith('Events');
115
+ });
116
+
117
+ const others = classObjects.filter(object => {
118
+ return !object.name.endsWith('Properties')
119
+ && !object.name.endsWith('Events');
120
+ });
121
+
122
+ const dependencies = others.map(object => {
123
+ const props = object.props.map(prop => {
124
+ if (prop.optional) {
125
+ return `${prop.name}?: ${generateReturnType(prop.type)};`;
126
+ }
127
+ return `${prop.name}: ${generateReturnType(prop.type)};`;
128
+ }).join('\n ');
129
+
130
+ return `
131
+ interface ${object.name} {
132
+ ${props}
133
+ }`;
134
+ }).join('\n\n');
135
+
136
+ const componentProperties = properties.length > 0 ? properties[0] : undefined;
137
+ const componentEvents = events.length > 0 ? events[0] : undefined;
138
+ const className = (() => {
139
+ if (componentProperties) {
140
+ return componentProperties.name.replace(/Properties$/, '');
141
+ }
142
+ if (componentEvents) {
143
+ return componentEvents.name.replace(/Events$/, '');
144
+ }
145
+ return '';
146
+ })();
147
+
148
+ if (!className) {
149
+ return '';
150
+ }
151
+
152
+ const content = _.template(readTemplate('class.dart'))({
153
+ className: className,
154
+ properties: componentProperties,
155
+ events: componentEvents,
156
+ classObjectDictionary,
157
+ dependencies,
158
+ blob,
159
+ generateReturnType,
160
+ generateMethodDeclaration,
161
+ generateEventHandlerType,
162
+ generateAttributeSetter,
163
+ shouldMakeNullable,
164
+ command,
165
+ });
166
+
167
+ return content.split('\n').filter(str => {
168
+ return str.trim().length > 0;
169
+ }).join('\n');
170
+ }
@@ -0,0 +1,84 @@
1
+ import {ParameterBaseType, ParameterType} from "./analyzer";
2
+
3
+ export enum FunctionArgumentType {
4
+ // Basic types
5
+ dom_string,
6
+ object,
7
+ promise,
8
+ int,
9
+ double,
10
+ boolean,
11
+ function,
12
+ void,
13
+ any,
14
+ null,
15
+ undefined,
16
+ array,
17
+ js_array_proto_methods,
18
+ }
19
+
20
+ export class FunctionArguments {
21
+ name: string;
22
+ type: ParameterType;
23
+ isDotDotDot: boolean;
24
+ isSymbolKey: boolean;
25
+ typeMode: ParameterMode;
26
+ required: boolean;
27
+ }
28
+
29
+ export class ParameterMode {
30
+ newObject?: boolean;
31
+ dartImpl?: boolean;
32
+ layoutDependent?: boolean;
33
+ static?: boolean;
34
+ supportAsync?: boolean;
35
+ supportAsyncManual?: boolean;
36
+ supportAsyncArrayValue?: boolean;
37
+ staticMethod?: boolean;
38
+ }
39
+
40
+ export class PropsDeclaration {
41
+ type: ParameterType;
42
+ typeMode: ParameterMode;
43
+ async_type?: ParameterType;
44
+ name: string;
45
+ isSymbol?: boolean;
46
+ readonly: boolean;
47
+ optional: boolean;
48
+ }
49
+
50
+ export class IndexedPropertyDeclaration extends PropsDeclaration {
51
+ indexKeyType: 'string' | 'number';
52
+ }
53
+
54
+ export class FunctionDeclaration extends PropsDeclaration {
55
+ args: FunctionArguments[] = [];
56
+ returnType: ParameterType;
57
+ async_returnType?: ParameterType;
58
+ returnTypeMode?: ParameterMode;
59
+ }
60
+
61
+ export enum ClassObjectKind {
62
+ interface,
63
+ dictionary,
64
+ mixin
65
+ }
66
+
67
+ export class ClassObject {
68
+ static globalClassMap: {[key: string]: ClassObject} = Object.create(null);
69
+ static globalClassRelationMap: {[key: string]: string[]} = Object.create(null);
70
+ name: string;
71
+ parent: string;
72
+ mixinParent: string[];
73
+ props: PropsDeclaration[] = [];
74
+ inheritedProps?: PropsDeclaration[] = [];
75
+ indexedProp?: IndexedPropertyDeclaration;
76
+ methods: FunctionDeclaration[] = [];
77
+ staticMethods: FunctionDeclaration[] = [];
78
+ construct?: FunctionDeclaration;
79
+ kind: ClassObjectKind = ClassObjectKind.interface;
80
+ }
81
+
82
+ export class FunctionObject {
83
+ declare: FunctionDeclaration
84
+ }
@@ -0,0 +1,454 @@
1
+ import path from 'path';
2
+ import fs from 'fs';
3
+ import process from 'process';
4
+ import _ from 'lodash';
5
+ import { glob } from 'glob';
6
+ import yaml from 'yaml';
7
+ import { IDLBlob } from './IDLBlob';
8
+ import { ClassObject } from './declaration';
9
+ import { analyzer, ParameterType, clearCaches } from './analyzer';
10
+ import { generateDartClass } from './dart';
11
+ import { generateReactComponent, generateReactIndex } from './react';
12
+ import { generateVueTypings } from './vue';
13
+ import { logger, debug, info, success, warn, error, group, progress, time, timeEnd } from './logger';
14
+
15
+ // Cache for file content to avoid redundant reads
16
+ const fileContentCache = new Map<string, string>();
17
+
18
+ // Cache for generated content to detect changes
19
+ const generatedContentCache = new Map<string, string>();
20
+
21
+ function writeFileIfChanged(filePath: string, content: string): boolean {
22
+ // Check if content has changed by comparing with cache
23
+ const cachedContent = generatedContentCache.get(filePath);
24
+ if (cachedContent === content) {
25
+ return false; // No change
26
+ }
27
+
28
+ // Check if file exists and has same content
29
+ if (fs.existsSync(filePath)) {
30
+ const existingContent = fileContentCache.get(filePath) || fs.readFileSync(filePath, 'utf-8');
31
+ fileContentCache.set(filePath, existingContent);
32
+
33
+ if (existingContent === content) {
34
+ generatedContentCache.set(filePath, content);
35
+ return false; // No change
36
+ }
37
+ }
38
+
39
+ // Create directory if it doesn't exist
40
+ const dir = path.dirname(filePath);
41
+ if (!fs.existsSync(dir)) {
42
+ fs.mkdirSync(dir, { recursive: true });
43
+ }
44
+
45
+ // Write file and update caches
46
+ fs.writeFileSync(filePath, content, 'utf-8');
47
+ fileContentCache.set(filePath, content);
48
+ generatedContentCache.set(filePath, content);
49
+
50
+ return true; // File was changed
51
+ }
52
+
53
+ class DefinedPropertyCollector {
54
+ properties = new Set<string>();
55
+ files = new Set<string>();
56
+ interfaces = new Set<string>();
57
+ }
58
+
59
+ class UnionTypeCollector {
60
+ types = new Set<ParameterType[]>()
61
+ }
62
+
63
+ interface GenerateOptions {
64
+ source: string;
65
+ target: string;
66
+ command: string;
67
+ }
68
+
69
+ // Batch processing for file operations
70
+ async function processFilesInBatch<T>(
71
+ items: T[],
72
+ batchSize: number,
73
+ processor: (item: T) => Promise<void> | void
74
+ ): Promise<void> {
75
+ for (let i = 0; i < items.length; i += batchSize) {
76
+ const batch = items.slice(i, i + batchSize);
77
+ await Promise.all(batch.map(item => processor(item)));
78
+ }
79
+ }
80
+
81
+ function validatePaths(source: string, target: string): { source: string; target: string } {
82
+ if (!source) {
83
+ throw new Error('Source path is required');
84
+ }
85
+
86
+ if (!target) {
87
+ throw new Error('Target path is required');
88
+ }
89
+
90
+ // Normalize paths
91
+ const normalizedSource = path.isAbsolute(source) ? source : path.join(process.cwd(), source);
92
+ const normalizedTarget = path.isAbsolute(target) ? target : path.join(process.cwd(), target);
93
+
94
+ // Validate source exists
95
+ if (!fs.existsSync(normalizedSource)) {
96
+ throw new Error(`Source path does not exist: ${normalizedSource}`);
97
+ }
98
+
99
+ return { source: normalizedSource, target: normalizedTarget };
100
+ }
101
+
102
+ function getTypeFiles(source: string): string[] {
103
+ try {
104
+ const files = glob.globSync("**/*.d.ts", {
105
+ cwd: source,
106
+ ignore: ['**/node_modules/**', '**/dist/**', '**/build/**']
107
+ });
108
+
109
+ return files.filter(file => !file.includes('global.d.ts'));
110
+ } catch (err) {
111
+ error(`Error scanning for type files in ${source}`, err);
112
+ throw new Error(`Failed to scan type files: ${err instanceof Error ? err.message : String(err)}`);
113
+ }
114
+ }
115
+
116
+ function createBlobs(typeFiles: string[], source: string, target: string): IDLBlob[] {
117
+ return typeFiles.map(file => {
118
+ const filename = path.basename(file, '.d.ts');
119
+ const implement = file.replace(path.join(__dirname, '../../'), '').replace('.d.ts', '');
120
+ // Store the relative directory path for maintaining structure
121
+ const relativeDir = path.dirname(file);
122
+ const blob = new IDLBlob(path.join(source, file), target, filename, implement, relativeDir);
123
+
124
+ // Pre-cache file content
125
+ if (!fileContentCache.has(blob.source)) {
126
+ try {
127
+ const content = fs.readFileSync(blob.source, 'utf-8');
128
+ fileContentCache.set(blob.source, content);
129
+ blob.raw = content;
130
+ } catch (err) {
131
+ error(`Error reading file ${blob.source}`, err);
132
+ throw err;
133
+ }
134
+ } else {
135
+ blob.raw = fileContentCache.get(blob.source)!;
136
+ }
137
+
138
+ return blob;
139
+ });
140
+ }
141
+
142
+ export async function dartGen({ source, target, command }: GenerateOptions) {
143
+ group('Dart Code Generation');
144
+ time('dartGen');
145
+
146
+ const { source: normalizedSource, target: normalizedTarget } = validatePaths(source, target);
147
+
148
+ const definedPropertyCollector = new DefinedPropertyCollector();
149
+ const unionTypeCollector = new UnionTypeCollector();
150
+
151
+ // Clear analyzer caches for fresh run
152
+ if (typeof clearCaches === 'function') {
153
+ clearCaches();
154
+ }
155
+
156
+ // Get type files
157
+ const typeFiles = getTypeFiles(normalizedSource);
158
+ info(`Found ${typeFiles.length} type definition files`);
159
+
160
+ if (typeFiles.length === 0) {
161
+ warn('No type definition files found');
162
+ timeEnd('dartGen');
163
+ return;
164
+ }
165
+
166
+ // Create blobs
167
+ const blobs = createBlobs(typeFiles, normalizedSource, normalizedTarget);
168
+
169
+ // Reset global class map
170
+ ClassObject.globalClassMap = Object.create(null);
171
+
172
+ // Analyze all files first
173
+ info('Analyzing type definitions...');
174
+ let analyzed = 0;
175
+ for (const blob of blobs) {
176
+ try {
177
+ analyzer(blob, definedPropertyCollector, unionTypeCollector);
178
+ analyzed++;
179
+ progress(analyzed, blobs.length, `Analyzing ${blob.filename}`);
180
+ } catch (err) {
181
+ error(`Error analyzing ${blob.filename}`, err);
182
+ // Continue with other files
183
+ }
184
+ }
185
+
186
+ // Generate Dart code and copy .d.ts files
187
+ info('\nGenerating Dart classes...');
188
+ let filesChanged = 0;
189
+
190
+ await processFilesInBatch(blobs, 5, async (blob) => {
191
+ try {
192
+ const result = generateDartClass(blob, command);
193
+ blob.dist = normalizedTarget;
194
+
195
+ // Maintain the same directory structure as the .d.ts file
196
+ const outputDir = path.join(blob.dist, blob.relativeDir);
197
+ // Ensure the directory exists
198
+ if (!fs.existsSync(outputDir)) {
199
+ fs.mkdirSync(outputDir, { recursive: true });
200
+ }
201
+
202
+ // Generate Dart file
203
+ const genFilePath = path.join(outputDir, _.snakeCase(blob.filename));
204
+ const fullPath = genFilePath + '_bindings_generated.dart';
205
+
206
+ if (writeFileIfChanged(fullPath, result)) {
207
+ filesChanged++;
208
+ debug(`Generated: ${path.basename(fullPath)}`);
209
+ }
210
+
211
+ // Copy the original .d.ts file to the output directory
212
+ const dtsOutputPath = path.join(outputDir, blob.filename + '.d.ts');
213
+ if (writeFileIfChanged(dtsOutputPath, blob.raw)) {
214
+ filesChanged++;
215
+ debug(`Copied: ${path.basename(dtsOutputPath)}`);
216
+ }
217
+ } catch (err) {
218
+ error(`Error generating Dart code for ${blob.filename}`, err);
219
+ }
220
+ });
221
+
222
+ // Generate index.d.ts file with references to all .d.ts files
223
+ const indexDtsContent = generateTypeScriptIndex(blobs, normalizedTarget);
224
+ const indexDtsPath = path.join(normalizedTarget, 'index.d.ts');
225
+ if (writeFileIfChanged(indexDtsPath, indexDtsContent)) {
226
+ filesChanged++;
227
+ debug('Generated: index.d.ts');
228
+ }
229
+
230
+ timeEnd('dartGen');
231
+ success(`Dart code generation completed. ${filesChanged} files changed.`);
232
+ info(`Output directory: ${normalizedTarget}`);
233
+ }
234
+
235
+ export async function reactGen({ source, target }: GenerateOptions) {
236
+ group('React Code Generation');
237
+ time('reactGen');
238
+
239
+ const { source: normalizedSource, target: normalizedTarget } = validatePaths(source, target);
240
+
241
+ const definedPropertyCollector = new DefinedPropertyCollector();
242
+ const unionTypeCollector = new UnionTypeCollector();
243
+
244
+ // Clear analyzer caches for fresh run
245
+ if (typeof clearCaches === 'function') {
246
+ clearCaches();
247
+ }
248
+
249
+ // Get type files
250
+ const typeFiles = getTypeFiles(normalizedSource);
251
+ info(`Found ${typeFiles.length} type definition files`);
252
+
253
+ if (typeFiles.length === 0) {
254
+ warn('No type definition files found');
255
+ timeEnd('reactGen');
256
+ return;
257
+ }
258
+
259
+ // Create blobs
260
+ const blobs = createBlobs(typeFiles, normalizedSource, normalizedTarget);
261
+
262
+ // Reset global class map
263
+ ClassObject.globalClassMap = Object.create(null);
264
+
265
+ // Analyze all files first
266
+ info('Analyzing type definitions...');
267
+ let analyzed = 0;
268
+ for (const blob of blobs) {
269
+ try {
270
+ analyzer(blob, definedPropertyCollector, unionTypeCollector);
271
+ analyzed++;
272
+ progress(analyzed, blobs.length, `Analyzing ${blob.filename}`);
273
+ } catch (err) {
274
+ error(`Error analyzing ${blob.filename}`, err);
275
+ // Continue with other files
276
+ }
277
+ }
278
+
279
+ // Ensure src directory exists
280
+ const srcDir = path.join(normalizedTarget, 'src');
281
+ if (!fs.existsSync(srcDir)) {
282
+ fs.mkdirSync(srcDir, { recursive: true });
283
+ }
284
+
285
+ // Generate React components
286
+ info('\nGenerating React components...');
287
+ let filesChanged = 0;
288
+
289
+ await processFilesInBatch(blobs, 5, async (blob) => {
290
+ try {
291
+ const result = generateReactComponent(blob);
292
+
293
+ // Maintain the same directory structure as the .d.ts file
294
+ const outputDir = path.join(normalizedTarget, 'src', blob.relativeDir);
295
+ // Ensure the directory exists
296
+ if (!fs.existsSync(outputDir)) {
297
+ fs.mkdirSync(outputDir, { recursive: true });
298
+ }
299
+
300
+ const genFilePath = path.join(outputDir, blob.filename);
301
+ const fullPath = genFilePath + '.tsx';
302
+
303
+ if (writeFileIfChanged(fullPath, result)) {
304
+ filesChanged++;
305
+ debug(`Generated: ${path.basename(fullPath)}`);
306
+ }
307
+ } catch (err) {
308
+ error(`Error generating React component for ${blob.filename}`, err);
309
+ }
310
+ });
311
+
312
+ // Generate index file
313
+ const indexContent = generateReactIndex(blobs);
314
+ const indexFilePath = path.join(normalizedTarget, 'src', 'index.ts');
315
+ if (writeFileIfChanged(indexFilePath, indexContent)) {
316
+ filesChanged++;
317
+ debug(`Generated: index.ts`);
318
+ }
319
+
320
+ timeEnd('reactGen');
321
+ success(`React code generation completed. ${filesChanged} files changed.`);
322
+ info(`Output directory: ${normalizedTarget}`);
323
+ info('You can now import these components in your React project.');
324
+ }
325
+
326
+ export async function vueGen({ source, target }: GenerateOptions) {
327
+ group('Vue Typings Generation');
328
+ time('vueGen');
329
+
330
+ const { source: normalizedSource, target: normalizedTarget } = validatePaths(source, target);
331
+
332
+ const definedPropertyCollector = new DefinedPropertyCollector();
333
+ const unionTypeCollector = new UnionTypeCollector();
334
+
335
+ // Clear analyzer caches for fresh run
336
+ if (typeof clearCaches === 'function') {
337
+ clearCaches();
338
+ }
339
+
340
+ // Get type files
341
+ const typeFiles = getTypeFiles(normalizedSource);
342
+ info(`Found ${typeFiles.length} type definition files`);
343
+
344
+ if (typeFiles.length === 0) {
345
+ warn('No type definition files found');
346
+ timeEnd('vueGen');
347
+ return;
348
+ }
349
+
350
+ // Create blobs
351
+ const blobs = createBlobs(typeFiles, normalizedSource, normalizedTarget);
352
+
353
+ // Reset global class map
354
+ ClassObject.globalClassMap = Object.create(null);
355
+
356
+ // Analyze all files first
357
+ info('Analyzing type definitions...');
358
+ let analyzed = 0;
359
+ for (const blob of blobs) {
360
+ try {
361
+ analyzer(blob, definedPropertyCollector, unionTypeCollector);
362
+ analyzed++;
363
+ progress(analyzed, blobs.length, `Analyzing ${blob.filename}`);
364
+ } catch (err) {
365
+ error(`Error analyzing ${blob.filename}`, err);
366
+ // Continue with other files
367
+ }
368
+ }
369
+
370
+ // Generate Vue typings
371
+ info('\nGenerating Vue typings...');
372
+ const typingsContent = generateVueTypings(blobs);
373
+ const typingsFilePath = path.join(normalizedTarget, 'index.d.ts');
374
+
375
+ let filesChanged = 0;
376
+ if (writeFileIfChanged(typingsFilePath, typingsContent)) {
377
+ filesChanged++;
378
+ debug(`Generated: index.d.ts`);
379
+ }
380
+
381
+ timeEnd('vueGen');
382
+ success(`Vue typings generation completed. ${filesChanged} files changed.`);
383
+ info(`Output directory: ${normalizedTarget}`);
384
+ info('You can now import these types in your Vue project.');
385
+ }
386
+
387
+ function generateTypeScriptIndex(blobs: IDLBlob[], targetPath: string): string {
388
+ const references: string[] = ['/// <reference path="./global.d.ts" />'];
389
+ const exports: string[] = [];
390
+
391
+ // Group blobs by directory to maintain structure
392
+ const filesByDir = new Map<string, IDLBlob[]>();
393
+
394
+ blobs.forEach(blob => {
395
+ const dir = blob.relativeDir || '.';
396
+ if (!filesByDir.has(dir)) {
397
+ filesByDir.set(dir, []);
398
+ }
399
+ filesByDir.get(dir)!.push(blob);
400
+ });
401
+
402
+ // Sort directories and files for consistent output
403
+ const sortedDirs = Array.from(filesByDir.keys()).sort();
404
+
405
+ sortedDirs.forEach(dir => {
406
+ const dirBlobs = filesByDir.get(dir)!.sort((a, b) => a.filename.localeCompare(b.filename));
407
+
408
+ dirBlobs.forEach(blob => {
409
+ const relativePath = dir === '.' ? blob.filename : path.join(dir, blob.filename);
410
+ references.push(`/// <reference path="./${relativePath}.d.ts" />`);
411
+ exports.push(`export * from './${relativePath}';`);
412
+ });
413
+ });
414
+
415
+ // Get package name from pubspec.yaml if available
416
+ let packageName = 'WebF Package';
417
+ let packageDescription = 'TypeScript Definitions';
418
+
419
+ try {
420
+ const pubspecPath = path.join(targetPath, 'pubspec.yaml');
421
+ if (fs.existsSync(pubspecPath)) {
422
+ const pubspecContent = fs.readFileSync(pubspecPath, 'utf-8');
423
+ const pubspec = yaml.parse(pubspecContent);
424
+ if (pubspec.name) {
425
+ packageName = pubspec.name.replace(/_/g, ' ').replace(/\b\w/g, (l: string) => l.toUpperCase());
426
+ }
427
+ if (pubspec.description) {
428
+ packageDescription = pubspec.description;
429
+ }
430
+ }
431
+ } catch (err) {
432
+ // Ignore errors, use defaults
433
+ }
434
+
435
+ return `${references.join('\n')}
436
+
437
+ /**
438
+ * ${packageName} - TypeScript Definitions
439
+ *
440
+ * ${packageDescription}
441
+ */
442
+
443
+ ${exports.join('\n')}
444
+ `;
445
+ }
446
+
447
+ // Clear all caches (useful for watch mode or between runs)
448
+ export function clearAllCaches() {
449
+ fileContentCache.clear();
450
+ generatedContentCache.clear();
451
+ if (typeof clearCaches === 'function') {
452
+ clearCaches(); // Clear analyzer caches
453
+ }
454
+ }