@openwebf/webf 0.22.10 → 0.22.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/analyzer.js CHANGED
@@ -66,11 +66,17 @@ const TYPE_REFERENCE_MAP = {
66
66
  };
67
67
  function analyzer(blob, definedPropertyCollector, unionTypeCollector) {
68
68
  try {
69
- // Check cache first
70
- let sourceFile = sourceFileCache.get(blob.source);
71
- if (!sourceFile) {
69
+ // Check cache first - consider both file path and content
70
+ const cacheEntry = sourceFileCache.get(blob.source);
71
+ let sourceFile;
72
+ if (cacheEntry && cacheEntry.content === blob.raw) {
73
+ // Cache hit with same content
74
+ sourceFile = cacheEntry.sourceFile;
75
+ }
76
+ else {
77
+ // Cache miss or content changed - parse and update cache
72
78
  sourceFile = typescript_1.default.createSourceFile(blob.source, blob.raw, typescript_1.ScriptTarget.ES2020);
73
- sourceFileCache.set(blob.source, sourceFile);
79
+ sourceFileCache.set(blob.source, { content: blob.raw, sourceFile });
74
80
  }
75
81
  blob.objects = sourceFile.statements
76
82
  .map(statement => {
@@ -292,6 +298,8 @@ function getParameterBaseType(type, mode) {
292
298
  if (mode)
293
299
  mode.staticMethod = true;
294
300
  return handleGenericWrapper(typeReference, mode);
301
+ case 'CustomEvent':
302
+ return handleCustomEventType(typeReference);
295
303
  default:
296
304
  if (identifier.includes('SupportAsync')) {
297
305
  return handleSupportAsyncType(identifier, typeReference, mode);
@@ -344,6 +352,92 @@ function handleGenericWrapper(typeReference, mode) {
344
352
  const argument = typeReference.typeArguments[0];
345
353
  return getParameterBaseType(argument, mode);
346
354
  }
355
+ function handleCustomEventType(typeReference) {
356
+ // Handle CustomEvent<T> by returning the full type with generic parameter
357
+ if (!typeReference.typeArguments || !typeReference.typeArguments[0]) {
358
+ return 'CustomEvent';
359
+ }
360
+ const argument = typeReference.typeArguments[0];
361
+ let genericType;
362
+ if (typescript_1.default.isTypeReferenceNode(argument) && typescript_1.default.isIdentifier(argument.typeName)) {
363
+ const typeName = argument.typeName.text;
364
+ // Check if it's a mapped type reference like 'int' or 'double'
365
+ const mappedType = TYPE_REFERENCE_MAP[typeName];
366
+ if (mappedType !== undefined) {
367
+ switch (mappedType) {
368
+ case declaration_1.FunctionArgumentType.boolean:
369
+ genericType = 'boolean';
370
+ break;
371
+ case declaration_1.FunctionArgumentType.dom_string:
372
+ genericType = 'string';
373
+ break;
374
+ case declaration_1.FunctionArgumentType.double:
375
+ case declaration_1.FunctionArgumentType.int:
376
+ genericType = 'number';
377
+ break;
378
+ case declaration_1.FunctionArgumentType.any:
379
+ genericType = 'any';
380
+ break;
381
+ case declaration_1.FunctionArgumentType.void:
382
+ genericType = 'void';
383
+ break;
384
+ case declaration_1.FunctionArgumentType.function:
385
+ genericType = 'Function';
386
+ break;
387
+ case declaration_1.FunctionArgumentType.promise:
388
+ genericType = 'Promise<any>';
389
+ break;
390
+ default:
391
+ genericType = typeName;
392
+ }
393
+ }
394
+ else {
395
+ // For other type references, use the type name directly
396
+ genericType = typeName;
397
+ }
398
+ }
399
+ else if (typescript_1.default.isLiteralTypeNode(argument) && typescript_1.default.isStringLiteral(argument.literal)) {
400
+ genericType = argument.literal.text;
401
+ }
402
+ else {
403
+ // Handle basic types (boolean, string, number, etc.)
404
+ const basicType = BASIC_TYPE_MAP[argument.kind];
405
+ if (basicType !== undefined) {
406
+ switch (basicType) {
407
+ case declaration_1.FunctionArgumentType.boolean:
408
+ genericType = 'boolean';
409
+ break;
410
+ case declaration_1.FunctionArgumentType.dom_string:
411
+ genericType = 'string';
412
+ break;
413
+ case declaration_1.FunctionArgumentType.double:
414
+ case declaration_1.FunctionArgumentType.int:
415
+ genericType = 'number';
416
+ break;
417
+ case declaration_1.FunctionArgumentType.any:
418
+ genericType = 'any';
419
+ break;
420
+ case declaration_1.FunctionArgumentType.void:
421
+ genericType = 'void';
422
+ break;
423
+ case declaration_1.FunctionArgumentType.null:
424
+ genericType = 'null';
425
+ break;
426
+ case declaration_1.FunctionArgumentType.undefined:
427
+ genericType = 'undefined';
428
+ break;
429
+ default:
430
+ genericType = 'any';
431
+ }
432
+ }
433
+ else {
434
+ // For truly complex types, fallback to 'any' to avoid errors
435
+ console.warn('Complex generic type in CustomEvent, using any');
436
+ genericType = 'any';
437
+ }
438
+ }
439
+ return `CustomEvent<${genericType}>`;
440
+ }
347
441
  function handleSupportAsyncType(identifier, typeReference, mode) {
348
442
  if (mode) {
349
443
  mode.supportAsync = true;
package/dist/commands.js CHANGED
@@ -260,7 +260,7 @@ function createCommand(target, options) {
260
260
  cwd: target,
261
261
  stdio: 'inherit'
262
262
  });
263
- (0, child_process_1.spawnSync)(NPM, ['install', '@types/vue', '-D'], {
263
+ (0, child_process_1.spawnSync)(NPM, ['install', 'vue', '-D'], {
264
264
  cwd: target,
265
265
  stdio: 'inherit'
266
266
  });
package/dist/generator.js CHANGED
@@ -287,60 +287,29 @@ function reactGen(_a) {
287
287
  (0, logger_1.error)(`Error generating React component for ${blob.filename}`, err);
288
288
  }
289
289
  }));
290
- // Generate or update index file
290
+ // Generate index file
291
+ // Avoid overriding a user-managed index.ts. Only write when:
292
+ // - index.ts does not exist, or
293
+ // - it contains the auto-generated marker from our template
291
294
  const indexFilePath = path_1.default.join(normalizedTarget, 'src', 'index.ts');
292
295
  const newExports = (0, react_1.generateReactIndex)(blobs);
296
+ let shouldWriteIndex = true;
293
297
  if (fs_1.default.existsSync(indexFilePath)) {
294
- // Read existing index file
295
- const existingContent = fs_1.default.readFileSync(indexFilePath, 'utf-8');
296
- const existingLines = existingContent.split('\n');
297
- // Parse new exports
298
- const newExportLines = newExports.split('\n').filter(line => line.startsWith('export '));
299
- // Find which exports are missing
300
- const missingExports = [];
301
- for (const newExport of newExportLines) {
302
- // Extract the export statement to check if it exists
303
- const exportMatch = newExport.match(/export\s*{\s*([^}]+)\s*}\s*from\s*["']([^"']+)["']/);
304
- if (exportMatch) {
305
- const [, exportNames, modulePath] = exportMatch;
306
- const exportedItems = exportNames.split(',').map(s => s.trim());
307
- // Check if this exact export exists
308
- const exists = existingLines.some(line => {
309
- if (!line.startsWith('export '))
310
- return false;
311
- const lineMatch = line.match(/export\s*{\s*([^}]+)\s*}\s*from\s*["']([^"']+)["']/);
312
- if (!lineMatch)
313
- return false;
314
- const [, lineExportNames, lineModulePath] = lineMatch;
315
- const lineExportedItems = lineExportNames.split(',').map(s => s.trim());
316
- // Check if same module and same exports
317
- return lineModulePath === modulePath &&
318
- exportedItems.every(item => lineExportedItems.includes(item)) &&
319
- lineExportedItems.every(item => exportedItems.includes(item));
320
- });
321
- if (!exists) {
322
- missingExports.push(newExport);
323
- }
324
- }
325
- }
326
- // If there are missing exports, append them
327
- if (missingExports.length > 0) {
328
- let updatedContent = existingContent.trimRight();
329
- if (!updatedContent.endsWith('\n')) {
330
- updatedContent += '\n';
331
- }
332
- updatedContent += missingExports.join('\n') + '\n';
333
- if (writeFileIfChanged(indexFilePath, updatedContent)) {
334
- filesChanged++;
335
- (0, logger_1.debug)(`Updated: index.ts (added ${missingExports.length} exports)`);
298
+ try {
299
+ const existing = fs_1.default.readFileSync(indexFilePath, 'utf-8');
300
+ const isAutoGenerated = existing.includes('Generated by TSDL');
301
+ if (!isAutoGenerated) {
302
+ shouldWriteIndex = false;
303
+ (0, logger_1.warn)(`Found existing user-managed index.ts at ${indexFilePath}; skipping overwrite.`);
336
304
  }
337
305
  }
338
- else {
339
- (0, logger_1.debug)(`Skipped: index.ts (all exports already exist)`);
306
+ catch (err) {
307
+ // If we cannot read the file for some reason, be conservative and skip overwriting
308
+ shouldWriteIndex = false;
309
+ (0, logger_1.warn)(`Unable to read existing index.ts; skipping overwrite: ${indexFilePath}`);
340
310
  }
341
311
  }
342
- else {
343
- // File doesn't exist, create it
312
+ if (shouldWriteIndex) {
344
313
  if (writeFileIfChanged(indexFilePath, newExports)) {
345
314
  filesChanged++;
346
315
  (0, logger_1.debug)(`Generated: index.ts`);
package/dist/react.js CHANGED
@@ -63,6 +63,10 @@ function getEventType(type) {
63
63
  return 'Event';
64
64
  }
65
65
  const pointerType = (0, utils_1.getPointerType)(type);
66
+ // Handle CustomEvent with generic parameter
67
+ if (pointerType.startsWith('CustomEvent<') && pointerType.endsWith('>')) {
68
+ return pointerType;
69
+ }
66
70
  if (pointerType === 'CustomEvent') {
67
71
  return 'CustomEvent';
68
72
  }
@@ -319,8 +323,15 @@ function generateReactIndex(blobs) {
319
323
  }).filter(component => {
320
324
  return component.className.length > 0;
321
325
  });
326
+ // Deduplicate components by className, keeping the first occurrence
327
+ const deduplicatedComponents = new Map();
328
+ components.forEach(component => {
329
+ if (!deduplicatedComponents.has(component.className)) {
330
+ deduplicatedComponents.set(component.className, component);
331
+ }
332
+ });
322
333
  const content = lodash_1.default.template(readTemplate('react.index.ts'))({
323
- components,
334
+ components: Array.from(deduplicatedComponents.values()),
324
335
  });
325
336
  return content.split('\n').filter(str => {
326
337
  return str.trim().length > 0;
package/dist/vue.js CHANGED
@@ -51,6 +51,10 @@ function generateEventHandlerType(type) {
51
51
  if (pointerType === 'CustomEvent') {
52
52
  return 'CustomEvent';
53
53
  }
54
+ // Handle generic types like CustomEvent<T>
55
+ if (pointerType.startsWith('CustomEvent<')) {
56
+ return pointerType;
57
+ }
54
58
  throw new Error('Unknown event type: ' + pointerType);
55
59
  }
56
60
  function generateMethodDeclaration(method) {
@@ -65,6 +69,10 @@ function generateMethodDeclaration(method) {
65
69
  }
66
70
  function generateVueComponent(blob) {
67
71
  const classObjects = blob.objects;
72
+ // Skip if no class objects
73
+ if (!classObjects || classObjects.length === 0) {
74
+ return '';
75
+ }
68
76
  const classObjectDictionary = Object.fromEntries(classObjects.map(object => {
69
77
  return [object.name, object];
70
78
  }));
@@ -79,6 +87,9 @@ function generateVueComponent(blob) {
79
87
  && !object.name.endsWith('Events');
80
88
  });
81
89
  const dependencies = others.map(object => {
90
+ if (!object || !object.props) {
91
+ return '';
92
+ }
82
93
  const props = object.props.map(prop => {
83
94
  if (prop.optional) {
84
95
  return `${prop.name}?: ${generateReturnType(prop.type)};`;
@@ -89,7 +100,7 @@ function generateVueComponent(blob) {
89
100
  interface ${object.name} {
90
101
  ${props}
91
102
  }`;
92
- }).join('\n\n');
103
+ }).filter(dep => dep.trim() !== '').join('\n\n');
93
104
  const componentProperties = properties.length > 0 ? properties[0] : undefined;
94
105
  const componentEvents = events.length > 0 ? events[0] : undefined;
95
106
  const className = (() => {
@@ -149,7 +160,11 @@ function generateVueTypings(blobs) {
149
160
  }).filter(component => {
150
161
  return component.length > 0;
151
162
  }).join('\n\n');
152
- const content = lodash_1.default.template(readTemplate('vue.components.d.ts'))({
163
+ const content = lodash_1.default.template(readTemplate('vue.components.d.ts'), {
164
+ interpolate: /<%=([\s\S]+?)%>/g,
165
+ evaluate: /<%([\s\S]+?)%>/g,
166
+ escape: /<%-([\s\S]+?)%>/g
167
+ })({
153
168
  componentNames,
154
169
  components,
155
170
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openwebf/webf",
3
- "version": "0.22.10",
3
+ "version": "0.22.13",
4
4
  "description": "Command line tools for WebF",
5
5
  "main": "index.js",
6
6
  "bin": {
package/src/analyzer.ts CHANGED
@@ -26,7 +26,7 @@ export interface UnionTypeCollector {
26
26
  }
27
27
 
28
28
  // Cache for parsed source files to avoid re-parsing
29
- const sourceFileCache = new Map<string, ts.SourceFile>();
29
+ const sourceFileCache = new Map<string, { content: string; sourceFile: ts.SourceFile }>();
30
30
 
31
31
  // Cache for type conversions to avoid redundant processing
32
32
  const typeConversionCache = new Map<string, ParameterType>();
@@ -56,11 +56,17 @@ const TYPE_REFERENCE_MAP: Record<string, FunctionArgumentType> = {
56
56
 
57
57
  export function analyzer(blob: IDLBlob, definedPropertyCollector: DefinedPropertyCollector, unionTypeCollector: UnionTypeCollector) {
58
58
  try {
59
- // Check cache first
60
- let sourceFile = sourceFileCache.get(blob.source);
61
- if (!sourceFile) {
59
+ // Check cache first - consider both file path and content
60
+ const cacheEntry = sourceFileCache.get(blob.source);
61
+ let sourceFile: ts.SourceFile;
62
+
63
+ if (cacheEntry && cacheEntry.content === blob.raw) {
64
+ // Cache hit with same content
65
+ sourceFile = cacheEntry.sourceFile;
66
+ } else {
67
+ // Cache miss or content changed - parse and update cache
62
68
  sourceFile = ts.createSourceFile(blob.source, blob.raw, ScriptTarget.ES2020);
63
- sourceFileCache.set(blob.source, sourceFile);
69
+ sourceFileCache.set(blob.source, { content: blob.raw, sourceFile });
64
70
  }
65
71
 
66
72
  blob.objects = sourceFile.statements
@@ -324,6 +330,9 @@ function getParameterBaseType(type: ts.TypeNode, mode?: ParameterMode): Paramete
324
330
  if (mode) mode.staticMethod = true;
325
331
  return handleGenericWrapper(typeReference, mode);
326
332
 
333
+ case 'CustomEvent':
334
+ return handleCustomEventType(typeReference);
335
+
327
336
  default:
328
337
  if (identifier.includes('SupportAsync')) {
329
338
  return handleSupportAsyncType(identifier, typeReference, mode);
@@ -386,6 +395,93 @@ function handleGenericWrapper(typeReference: ts.TypeReferenceNode, mode?: Parame
386
395
  return getParameterBaseType(argument, mode);
387
396
  }
388
397
 
398
+ function handleCustomEventType(typeReference: ts.TypeReferenceNode): ParameterBaseType {
399
+ // Handle CustomEvent<T> by returning the full type with generic parameter
400
+ if (!typeReference.typeArguments || !typeReference.typeArguments[0]) {
401
+ return 'CustomEvent';
402
+ }
403
+
404
+ const argument = typeReference.typeArguments[0];
405
+ let genericType: string;
406
+
407
+ if (ts.isTypeReferenceNode(argument) && ts.isIdentifier(argument.typeName)) {
408
+ const typeName = argument.typeName.text;
409
+
410
+ // Check if it's a mapped type reference like 'int' or 'double'
411
+ const mappedType = TYPE_REFERENCE_MAP[typeName];
412
+ if (mappedType !== undefined) {
413
+ switch (mappedType) {
414
+ case FunctionArgumentType.boolean:
415
+ genericType = 'boolean';
416
+ break;
417
+ case FunctionArgumentType.dom_string:
418
+ genericType = 'string';
419
+ break;
420
+ case FunctionArgumentType.double:
421
+ case FunctionArgumentType.int:
422
+ genericType = 'number';
423
+ break;
424
+ case FunctionArgumentType.any:
425
+ genericType = 'any';
426
+ break;
427
+ case FunctionArgumentType.void:
428
+ genericType = 'void';
429
+ break;
430
+ case FunctionArgumentType.function:
431
+ genericType = 'Function';
432
+ break;
433
+ case FunctionArgumentType.promise:
434
+ genericType = 'Promise<any>';
435
+ break;
436
+ default:
437
+ genericType = typeName;
438
+ }
439
+ } else {
440
+ // For other type references, use the type name directly
441
+ genericType = typeName;
442
+ }
443
+ } else if (ts.isLiteralTypeNode(argument) && ts.isStringLiteral(argument.literal)) {
444
+ genericType = argument.literal.text;
445
+ } else {
446
+ // Handle basic types (boolean, string, number, etc.)
447
+ const basicType = BASIC_TYPE_MAP[argument.kind];
448
+ if (basicType !== undefined) {
449
+ switch (basicType) {
450
+ case FunctionArgumentType.boolean:
451
+ genericType = 'boolean';
452
+ break;
453
+ case FunctionArgumentType.dom_string:
454
+ genericType = 'string';
455
+ break;
456
+ case FunctionArgumentType.double:
457
+ case FunctionArgumentType.int:
458
+ genericType = 'number';
459
+ break;
460
+ case FunctionArgumentType.any:
461
+ genericType = 'any';
462
+ break;
463
+ case FunctionArgumentType.void:
464
+ genericType = 'void';
465
+ break;
466
+ case FunctionArgumentType.null:
467
+ genericType = 'null';
468
+ break;
469
+ case FunctionArgumentType.undefined:
470
+ genericType = 'undefined';
471
+ break;
472
+ default:
473
+ genericType = 'any';
474
+ }
475
+ } else {
476
+ // For truly complex types, fallback to 'any' to avoid errors
477
+ console.warn('Complex generic type in CustomEvent, using any');
478
+ genericType = 'any';
479
+ }
480
+ }
481
+
482
+ return `CustomEvent<${genericType}>`;
483
+ }
484
+
389
485
  function handleSupportAsyncType(identifier: string, typeReference: ts.TypeReferenceNode, mode?: ParameterMode): ParameterBaseType {
390
486
  if (mode) {
391
487
  mode.supportAsync = true;
package/src/commands.ts CHANGED
@@ -332,7 +332,7 @@ function createCommand(target: string, options: { framework: string; packageName
332
332
  stdio: 'inherit'
333
333
  });
334
334
 
335
- spawnSync(NPM, ['install', '@types/vue', '-D'], {
335
+ spawnSync(NPM, ['install', 'vue', '-D'], {
336
336
  cwd: target,
337
337
  stdio: 'inherit'
338
338
  });
package/src/generator.ts CHANGED
@@ -321,64 +321,30 @@ export async function reactGen({ source, target, exclude, packageName }: Generat
321
321
  }
322
322
  });
323
323
 
324
- // Generate or update index file
324
+ // Generate index file
325
+ // Avoid overriding a user-managed index.ts. Only write when:
326
+ // - index.ts does not exist, or
327
+ // - it contains the auto-generated marker from our template
325
328
  const indexFilePath = path.join(normalizedTarget, 'src', 'index.ts');
326
329
  const newExports = generateReactIndex(blobs);
327
-
330
+
331
+ let shouldWriteIndex = true;
328
332
  if (fs.existsSync(indexFilePath)) {
329
- // Read existing index file
330
- const existingContent = fs.readFileSync(indexFilePath, 'utf-8');
331
- const existingLines = existingContent.split('\n');
332
-
333
- // Parse new exports
334
- const newExportLines = newExports.split('\n').filter(line => line.startsWith('export '));
335
-
336
- // Find which exports are missing
337
- const missingExports: string[] = [];
338
- for (const newExport of newExportLines) {
339
- // Extract the export statement to check if it exists
340
- const exportMatch = newExport.match(/export\s*{\s*([^}]+)\s*}\s*from\s*["']([^"']+)["']/);
341
- if (exportMatch) {
342
- const [, exportNames, modulePath] = exportMatch;
343
- const exportedItems = exportNames.split(',').map(s => s.trim());
344
-
345
- // Check if this exact export exists
346
- const exists = existingLines.some(line => {
347
- if (!line.startsWith('export ')) return false;
348
- const lineMatch = line.match(/export\s*{\s*([^}]+)\s*}\s*from\s*["']([^"']+)["']/);
349
- if (!lineMatch) return false;
350
- const [, lineExportNames, lineModulePath] = lineMatch;
351
- const lineExportedItems = lineExportNames.split(',').map(s => s.trim());
352
-
353
- // Check if same module and same exports
354
- return lineModulePath === modulePath &&
355
- exportedItems.every(item => lineExportedItems.includes(item)) &&
356
- lineExportedItems.every(item => exportedItems.includes(item));
357
- });
358
-
359
- if (!exists) {
360
- missingExports.push(newExport);
361
- }
362
- }
363
- }
364
-
365
- // If there are missing exports, append them
366
- if (missingExports.length > 0) {
367
- let updatedContent = existingContent.trimRight();
368
- if (!updatedContent.endsWith('\n')) {
369
- updatedContent += '\n';
370
- }
371
- updatedContent += missingExports.join('\n') + '\n';
372
-
373
- if (writeFileIfChanged(indexFilePath, updatedContent)) {
374
- filesChanged++;
375
- debug(`Updated: index.ts (added ${missingExports.length} exports)`);
333
+ try {
334
+ const existing = fs.readFileSync(indexFilePath, 'utf-8');
335
+ const isAutoGenerated = existing.includes('Generated by TSDL');
336
+ if (!isAutoGenerated) {
337
+ shouldWriteIndex = false;
338
+ warn(`Found existing user-managed index.ts at ${indexFilePath}; skipping overwrite.`);
376
339
  }
377
- } else {
378
- debug(`Skipped: index.ts (all exports already exist)`);
340
+ } catch (err) {
341
+ // If we cannot read the file for some reason, be conservative and skip overwriting
342
+ shouldWriteIndex = false;
343
+ warn(`Unable to read existing index.ts; skipping overwrite: ${indexFilePath}`);
379
344
  }
380
- } else {
381
- // File doesn't exist, create it
345
+ }
346
+
347
+ if (shouldWriteIndex) {
382
348
  if (writeFileIfChanged(indexFilePath, newExports)) {
383
349
  filesChanged++;
384
350
  debug(`Generated: index.ts`);
package/src/react.ts CHANGED
@@ -61,6 +61,12 @@ function getEventType(type: ParameterType) {
61
61
  return 'Event';
62
62
  }
63
63
  const pointerType = getPointerType(type);
64
+
65
+ // Handle CustomEvent with generic parameter
66
+ if (pointerType.startsWith('CustomEvent<') && pointerType.endsWith('>')) {
67
+ return pointerType;
68
+ }
69
+
64
70
  if (pointerType === 'CustomEvent') {
65
71
  return 'CustomEvent';
66
72
  }
@@ -360,8 +366,16 @@ export function generateReactIndex(blobs: IDLBlob[]) {
360
366
  return component.className.length > 0;
361
367
  });
362
368
 
369
+ // Deduplicate components by className, keeping the first occurrence
370
+ const deduplicatedComponents = new Map<string, { className: string; fileName: string; relativeDir: string }>();
371
+ components.forEach(component => {
372
+ if (!deduplicatedComponents.has(component.className)) {
373
+ deduplicatedComponents.set(component.className, component);
374
+ }
375
+ });
376
+
363
377
  const content = _.template(readTemplate('react.index.ts'))({
364
- components,
378
+ components: Array.from(deduplicatedComponents.values()),
365
379
  });
366
380
 
367
381
  return content.split('\n').filter(str => {
package/src/vue.ts CHANGED
@@ -50,6 +50,10 @@ function generateEventHandlerType(type: ParameterType) {
50
50
  if (pointerType === 'CustomEvent') {
51
51
  return 'CustomEvent';
52
52
  }
53
+ // Handle generic types like CustomEvent<T>
54
+ if (pointerType.startsWith('CustomEvent<')) {
55
+ return pointerType;
56
+ }
53
57
  throw new Error('Unknown event type: ' + pointerType);
54
58
  }
55
59
 
@@ -66,6 +70,11 @@ function generateMethodDeclaration(method: FunctionDeclaration) {
66
70
 
67
71
  function generateVueComponent(blob: IDLBlob) {
68
72
  const classObjects = blob.objects as ClassObject[];
73
+
74
+ // Skip if no class objects
75
+ if (!classObjects || classObjects.length === 0) {
76
+ return '';
77
+ }
69
78
  const classObjectDictionary = Object.fromEntries(
70
79
  classObjects.map(object => {
71
80
  return [object.name, object];
@@ -85,6 +94,9 @@ function generateVueComponent(blob: IDLBlob) {
85
94
  });
86
95
 
87
96
  const dependencies = others.map(object => {
97
+ if (!object || !object.props) {
98
+ return '';
99
+ }
88
100
  const props = object.props.map(prop => {
89
101
  if (prop.optional) {
90
102
  return `${prop.name}?: ${generateReturnType(prop.type)};`;
@@ -96,7 +108,7 @@ function generateVueComponent(blob: IDLBlob) {
96
108
  interface ${object.name} {
97
109
  ${props}
98
110
  }`;
99
- }).join('\n\n');
111
+ }).filter(dep => dep.trim() !== '').join('\n\n');
100
112
 
101
113
  const componentProperties = properties.length > 0 ? properties[0] : undefined;
102
114
  const componentEvents = events.length > 0 ? events[0] : undefined;
@@ -165,7 +177,11 @@ export function generateVueTypings(blobs: IDLBlob[]) {
165
177
  return component.length > 0;
166
178
  }).join('\n\n');
167
179
 
168
- const content = _.template(readTemplate('vue.components.d.ts'))({
180
+ const content = _.template(readTemplate('vue.components.d.ts'), {
181
+ interpolate: /<%=([\s\S]+?)%>/g,
182
+ evaluate: /<%([\s\S]+?)%>/g,
183
+ escape: /<%-([\s\S]+?)%>/g
184
+ })({
169
185
  componentNames,
170
186
  components,
171
187
  });
@@ -14,18 +14,23 @@ type VueEmit<T extends EventMap> = EmitFn<{
14
14
  [K in keyof T]: (event: T[K]) => void
15
15
  }>
16
16
 
17
+ // Vue 3 event listener properties for template usage
18
+ type VueEventListeners<T extends EventMap> = {
19
+ [K in keyof T as `on${Capitalize<string & K>}`]?: (event: T[K]) => any
20
+ }
21
+
17
22
  type DefineCustomElement<
18
23
  ElementType,
19
24
  Events extends EventMap = {},
20
25
  SelectedAttributes extends keyof ElementType = keyof ElementType
21
- > = new () => ElementType & {
26
+ > = new () => ElementType & VueEventListeners<Events> & {
22
27
  // Use $props to define the properties exposed to template type checking. Vue
23
28
  // specifically reads prop definitions from the `$props` type. Note that we
24
29
  // combine the element's props with the global HTML props and Vue's special
25
30
  // props.
26
31
  /** @deprecated Do not use the $props property on a Custom Element ref,
27
32
  this is for template prop types only. */
28
- $props: Partial<Pick<ElementType, SelectedAttributes>> & PublicProps
33
+ $props: Partial<Pick<ElementType, SelectedAttributes>> & PublicProps & VueEventListeners<Events>
29
34
 
30
35
  // Use $emit to specifically define event types. Vue specifically reads event
31
36
  // types from the `$emit` type. Note that `$emit` expects a particular format
@@ -40,7 +45,7 @@ type DefineCustomElement<
40
45
  declare module 'vue' {
41
46
  interface GlobalComponents {
42
47
  <% componentNames.forEach(name => { %>
43
- '<%= _.kebabCase(name) %>': DefineCustomElement<
48
+ '<%= name %>': DefineCustomElement<
44
49
  <%= name %>Props,
45
50
  <%= name %>Events
46
51
  >
@@ -7,5 +7,11 @@
7
7
  "files": ["index.d.ts"],
8
8
  "keywords": [],
9
9
  "author": "",
10
- "license": "ISC"
10
+ "license": "ISC",
11
+ "peerDependencies": {
12
+ "vue": "^3.0.0"
13
+ },
14
+ "devDependencies": {
15
+ "vue": "^3.0.0"
16
+ }
11
17
  }
@@ -284,10 +284,10 @@ describe('Commands', () => {
284
284
  { cwd: target, stdio: 'inherit' }
285
285
  );
286
286
 
287
- // Should install Vue types as dev dependency
287
+ // Should install Vue 3 as dev dependency
288
288
  expect(mockSpawnSync).toHaveBeenCalledWith(
289
289
  expect.stringMatching(/npm(\.cmd)?/),
290
- ['install', '@types/vue', '-D'],
290
+ ['install', 'vue', '-D'],
291
291
  { cwd: target, stdio: 'inherit' }
292
292
  );
293
293
  });
@@ -336,6 +336,12 @@ describe('Generator', () => {
336
336
  'export { Test, TestElement } from "./lib/src/html/test";\n' +
337
337
  'export { Component, ComponentElement } from "./lib/src/html/component";'
338
338
  );
339
+ // Ensure index.ts does not exist so it will be generated
340
+ mockFs.existsSync.mockImplementation((p: any) => {
341
+ const s = p.toString();
342
+ if (s.includes(path.join('/test/target', 'src', 'index.ts'))) return false;
343
+ return true;
344
+ });
339
345
 
340
346
  await reactGen({
341
347
  source: '/test/source',
@@ -357,6 +363,29 @@ describe('Generator', () => {
357
363
  expect(indexCall![1]).toContain('export { Test, TestElement }');
358
364
  expect(indexCall![1]).toContain('export { Component, ComponentElement }');
359
365
  });
366
+
367
+ it('should not overwrite user-managed index.ts', async () => {
368
+ // Existing index.ts that does not contain auto-generated marker
369
+ mockFs.existsSync.mockImplementation((p: any) => {
370
+ const s = p.toString();
371
+ if (s.includes(path.join('/test/target', 'src', 'index.ts'))) return true;
372
+ return true;
373
+ });
374
+ mockFs.readFileSync.mockImplementation((p: any) => {
375
+ const s = p.toString();
376
+ if (s.includes(path.join('/test/target', 'src', 'index.ts'))) return '// custom index file';
377
+ return 'test content';
378
+ });
379
+
380
+ await reactGen({
381
+ source: '/test/source',
382
+ target: '/test/target',
383
+ command: 'test command'
384
+ });
385
+
386
+ const indexWrite = mockFs.writeFileSync.mock.calls.find(call => call[0].toString().includes('index.ts'));
387
+ expect(indexWrite).toBeUndefined();
388
+ });
360
389
  });
361
390
 
362
391
  describe('vueGen', () => {
@@ -478,4 +507,4 @@ describe('Generator', () => {
478
507
  expect(mockAnalyzer.clearCaches).toHaveBeenCalled();
479
508
  });
480
509
  });
481
- });
510
+ });