@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 +98 -4
- package/dist/commands.js +1 -1
- package/dist/generator.js +16 -47
- package/dist/react.js +12 -1
- package/dist/vue.js +17 -2
- package/package.json +1 -1
- package/src/analyzer.ts +101 -5
- package/src/commands.ts +1 -1
- package/src/generator.ts +19 -53
- package/src/react.ts +15 -1
- package/src/vue.ts +18 -2
- package/templates/vue.components.d.ts.tpl +8 -3
- package/templates/vue.package.json.tpl +7 -1
- package/test/commands.test.ts +2 -2
- package/test/generator.test.ts +30 -1
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
|
-
|
|
71
|
-
|
|
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', '
|
|
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
|
|
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
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
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
|
-
|
|
339
|
-
|
|
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
|
-
|
|
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
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
|
-
|
|
61
|
-
|
|
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', '
|
|
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
|
|
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
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
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
|
-
}
|
|
378
|
-
|
|
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
|
-
}
|
|
381
|
-
|
|
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
|
-
'<%=
|
|
48
|
+
'<%= name %>': DefineCustomElement<
|
|
44
49
|
<%= name %>Props,
|
|
45
50
|
<%= name %>Events
|
|
46
51
|
>
|
package/test/commands.test.ts
CHANGED
|
@@ -284,10 +284,10 @@ describe('Commands', () => {
|
|
|
284
284
|
{ cwd: target, stdio: 'inherit' }
|
|
285
285
|
);
|
|
286
286
|
|
|
287
|
-
// Should install Vue
|
|
287
|
+
// Should install Vue 3 as dev dependency
|
|
288
288
|
expect(mockSpawnSync).toHaveBeenCalledWith(
|
|
289
289
|
expect.stringMatching(/npm(\.cmd)?/),
|
|
290
|
-
['install', '
|
|
290
|
+
['install', 'vue', '-D'],
|
|
291
291
|
{ cwd: target, stdio: 'inherit' }
|
|
292
292
|
);
|
|
293
293
|
});
|
package/test/generator.test.ts
CHANGED
|
@@ -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
|
+
});
|