@openwebf/webf 0.22.11 → 0.23.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.
- package/dist/commands.js +11 -5
- package/dist/dart.js +9 -7
- package/dist/generator.js +85 -5
- package/dist/vue.js +17 -2
- package/package.json +3 -2
- package/src/commands.ts +10 -5
- package/src/dart.ts +10 -8
- package/src/generator.ts +82 -6
- package/src/vue.ts +18 -2
- package/templates/class.dart.tpl +1 -1
- 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/tsconfig.json +4 -1
package/dist/commands.js
CHANGED
|
@@ -233,10 +233,16 @@ function createCommand(target, options) {
|
|
|
233
233
|
fs_1.default.mkdirSync(srcDir, { recursive: true });
|
|
234
234
|
}
|
|
235
235
|
const indexFilePath = path_1.default.join(srcDir, 'index.ts');
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
236
|
+
if (!fs_1.default.existsSync(indexFilePath)) {
|
|
237
|
+
const indexContent = lodash_1.default.template(reactIndexTpl)({
|
|
238
|
+
components: [],
|
|
239
|
+
});
|
|
240
|
+
writeFileIfChanged(indexFilePath, indexContent);
|
|
241
|
+
}
|
|
242
|
+
else {
|
|
243
|
+
// Do not overwrite existing index.ts created by the user
|
|
244
|
+
// Leave merge to the codegen step which appends exports safely
|
|
245
|
+
}
|
|
240
246
|
(0, child_process_1.spawnSync)(NPM, ['install', '--omit=peer'], {
|
|
241
247
|
cwd: target,
|
|
242
248
|
stdio: 'inherit'
|
|
@@ -260,7 +266,7 @@ function createCommand(target, options) {
|
|
|
260
266
|
cwd: target,
|
|
261
267
|
stdio: 'inherit'
|
|
262
268
|
});
|
|
263
|
-
(0, child_process_1.spawnSync)(NPM, ['install', '
|
|
269
|
+
(0, child_process_1.spawnSync)(NPM, ['install', 'vue', '-D'], {
|
|
264
270
|
cwd: target,
|
|
265
271
|
stdio: 'inherit'
|
|
266
272
|
});
|
package/dist/dart.js
CHANGED
|
@@ -80,6 +80,12 @@ ${enumValues};
|
|
|
80
80
|
}`;
|
|
81
81
|
}
|
|
82
82
|
function generateReturnType(type, enumName) {
|
|
83
|
+
// Handle union types first (e.g., 'left' | 'center' | 'right')
|
|
84
|
+
// so we don't incorrectly treat string literal unions as pointer types.
|
|
85
|
+
if (Array.isArray(type.value)) {
|
|
86
|
+
// If we have an enum name, use it; otherwise fall back to String
|
|
87
|
+
return enumName || 'String';
|
|
88
|
+
}
|
|
83
89
|
if ((0, utils_1.isPointerType)(type)) {
|
|
84
90
|
const pointerType = (0, utils_1.getPointerType)(type);
|
|
85
91
|
return pointerType;
|
|
@@ -87,11 +93,6 @@ function generateReturnType(type, enumName) {
|
|
|
87
93
|
if (type.isArray && typeof type.value === 'object' && !Array.isArray(type.value)) {
|
|
88
94
|
return `${(0, utils_1.getPointerType)(type.value)}[]`;
|
|
89
95
|
}
|
|
90
|
-
// Handle union types (e.g., 'left' | 'center' | 'right')
|
|
91
|
-
if (Array.isArray(type.value)) {
|
|
92
|
-
// If we have an enum name, use it; otherwise fall back to String
|
|
93
|
-
return enumName || 'String';
|
|
94
|
-
}
|
|
95
96
|
// Handle when type.value is a ParameterType object (nested type)
|
|
96
97
|
if (typeof type.value === 'object' && !Array.isArray(type.value) && type.value !== null) {
|
|
97
98
|
// This might be a nested ParameterType, recurse
|
|
@@ -250,7 +251,7 @@ interface ${object.name} {
|
|
|
250
251
|
}
|
|
251
252
|
// Generate enums for union types
|
|
252
253
|
const enums = [];
|
|
253
|
-
const enumMap = new Map(); // prop name -> enum name
|
|
254
|
+
const enumMap = new Map(); // camelCase prop name -> enum name
|
|
254
255
|
if (componentProperties) {
|
|
255
256
|
for (const prop of componentProperties.props) {
|
|
256
257
|
if (isStringUnionType(prop.type)) {
|
|
@@ -261,7 +262,8 @@ interface ${object.name} {
|
|
|
261
262
|
name: enumName,
|
|
262
263
|
definition: generateDartEnum(enumName, values)
|
|
263
264
|
});
|
|
264
|
-
|
|
265
|
+
// Store by camelCase prop name to match template usage
|
|
266
|
+
enumMap.set(lodash_1.default.camelCase(prop.name), enumName);
|
|
265
267
|
}
|
|
266
268
|
}
|
|
267
269
|
}
|
package/dist/generator.js
CHANGED
|
@@ -29,6 +29,7 @@ const dart_1 = require("./dart");
|
|
|
29
29
|
const react_1 = require("./react");
|
|
30
30
|
const vue_1 = require("./vue");
|
|
31
31
|
const logger_1 = require("./logger");
|
|
32
|
+
const typescript_1 = __importDefault(require("typescript"));
|
|
32
33
|
// Cache for file content to avoid redundant reads
|
|
33
34
|
const fileContentCache = new Map();
|
|
34
35
|
// Cache for generated content to detect changes
|
|
@@ -287,12 +288,91 @@ function reactGen(_a) {
|
|
|
287
288
|
(0, logger_1.error)(`Error generating React component for ${blob.filename}`, err);
|
|
288
289
|
}
|
|
289
290
|
}));
|
|
290
|
-
// Generate index file
|
|
291
|
+
// Generate/merge index file
|
|
291
292
|
const indexFilePath = path_1.default.join(normalizedTarget, 'src', 'index.ts');
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
293
|
+
// Build desired export map: moduleSpecifier -> Set of names
|
|
294
|
+
const desiredExports = new Map();
|
|
295
|
+
const components = blobs.flatMap(blob => {
|
|
296
|
+
const classObjects = blob.objects.filter(obj => obj instanceof declaration_1.ClassObject);
|
|
297
|
+
const properties = classObjects.filter(object => object.name.endsWith('Properties'));
|
|
298
|
+
const events = classObjects.filter(object => object.name.endsWith('Events'));
|
|
299
|
+
const componentMap = new Map();
|
|
300
|
+
properties.forEach(prop => componentMap.set(prop.name.replace(/Properties$/, ''), true));
|
|
301
|
+
events.forEach(evt => componentMap.set(evt.name.replace(/Events$/, ''), true));
|
|
302
|
+
return Array.from(componentMap.keys()).map(className => ({
|
|
303
|
+
className,
|
|
304
|
+
fileName: blob.filename,
|
|
305
|
+
relativeDir: blob.relativeDir,
|
|
306
|
+
}));
|
|
307
|
+
});
|
|
308
|
+
// Deduplicate by className
|
|
309
|
+
const unique = new Map();
|
|
310
|
+
for (const c of components) {
|
|
311
|
+
if (!unique.has(c.className))
|
|
312
|
+
unique.set(c.className, c);
|
|
313
|
+
}
|
|
314
|
+
for (const { className, fileName, relativeDir } of unique.values()) {
|
|
315
|
+
const spec = `./${relativeDir ? `${relativeDir}/` : ''}${fileName}`;
|
|
316
|
+
if (!desiredExports.has(spec))
|
|
317
|
+
desiredExports.set(spec, new Set());
|
|
318
|
+
const set = desiredExports.get(spec);
|
|
319
|
+
set.add(className);
|
|
320
|
+
set.add(`${className}Element`);
|
|
321
|
+
}
|
|
322
|
+
if (!fs_1.default.existsSync(indexFilePath)) {
|
|
323
|
+
// No index.ts -> generate fresh file from template
|
|
324
|
+
const newExports = (0, react_1.generateReactIndex)(blobs);
|
|
325
|
+
if (writeFileIfChanged(indexFilePath, newExports)) {
|
|
326
|
+
filesChanged++;
|
|
327
|
+
(0, logger_1.debug)(`Generated: index.ts`);
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
else {
|
|
331
|
+
// Merge into existing index.ts without removing user code
|
|
332
|
+
try {
|
|
333
|
+
const existing = fs_1.default.readFileSync(indexFilePath, 'utf-8');
|
|
334
|
+
const sourceFile = typescript_1.default.createSourceFile(indexFilePath, existing, typescript_1.default.ScriptTarget.ES2020, true, typescript_1.default.ScriptKind.TS);
|
|
335
|
+
// Track which names already exported per module
|
|
336
|
+
for (const stmt of sourceFile.statements) {
|
|
337
|
+
if (typescript_1.default.isExportDeclaration(stmt) && stmt.exportClause && typescript_1.default.isNamedExports(stmt.exportClause)) {
|
|
338
|
+
const moduleSpecifier = stmt.moduleSpecifier && typescript_1.default.isStringLiteral(stmt.moduleSpecifier)
|
|
339
|
+
? stmt.moduleSpecifier.text
|
|
340
|
+
: undefined;
|
|
341
|
+
if (!moduleSpecifier)
|
|
342
|
+
continue;
|
|
343
|
+
const desired = desiredExports.get(moduleSpecifier);
|
|
344
|
+
if (!desired)
|
|
345
|
+
continue;
|
|
346
|
+
for (const el of stmt.exportClause.elements) {
|
|
347
|
+
const name = el.name.getText(sourceFile);
|
|
348
|
+
if (desired.has(name))
|
|
349
|
+
desired.delete(name);
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
// Prepare new export lines for any remaining names
|
|
354
|
+
const lines = [];
|
|
355
|
+
for (const [spec, names] of desiredExports) {
|
|
356
|
+
const missing = Array.from(names);
|
|
357
|
+
if (missing.length === 0)
|
|
358
|
+
continue;
|
|
359
|
+
const specEscaped = spec.replace(/\\/g, '/');
|
|
360
|
+
lines.push(`export { ${missing.join(', ')} } from "${specEscaped}";`);
|
|
361
|
+
}
|
|
362
|
+
if (lines.length > 0) {
|
|
363
|
+
const appended = (existing.endsWith('\n') ? '' : '\n') + lines.join('\n') + '\n';
|
|
364
|
+
if (writeFileIfChanged(indexFilePath, existing + appended)) {
|
|
365
|
+
filesChanged++;
|
|
366
|
+
(0, logger_1.debug)(`Merged exports into existing index.ts`);
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
else {
|
|
370
|
+
(0, logger_1.debug)(`index.ts is up to date; no merge needed.`);
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
catch (err) {
|
|
374
|
+
(0, logger_1.warn)(`Failed to merge into existing index.ts. Skipping modifications: ${indexFilePath}`);
|
|
375
|
+
}
|
|
296
376
|
}
|
|
297
377
|
(0, logger_1.timeEnd)('reactGen');
|
|
298
378
|
(0, logger_1.success)(`React code generation completed. ${filesChanged} files changed.`);
|
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.
|
|
3
|
+
"version": "0.23.0",
|
|
4
4
|
"description": "Command line tools for WebF",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"bin": {
|
|
@@ -30,6 +30,7 @@
|
|
|
30
30
|
"@types/inquirer": "^8.2.11",
|
|
31
31
|
"@types/jest": "^29.5.12",
|
|
32
32
|
"@types/lodash": "^4.17.17",
|
|
33
|
+
"@types/minimatch": "^5.1.2",
|
|
33
34
|
"@types/node": "^22.15.21",
|
|
34
35
|
"@types/yaml": "^1.9.6",
|
|
35
36
|
"jest": "^29.7.0",
|
|
@@ -39,7 +40,7 @@
|
|
|
39
40
|
"@microsoft/tsdoc": "^0.15.1",
|
|
40
41
|
"@microsoft/tsdoc-config": "^0.17.1",
|
|
41
42
|
"commander": "^14.0.0",
|
|
42
|
-
"glob": "^10.
|
|
43
|
+
"glob": "^10.4.5",
|
|
43
44
|
"inquirer": "^8.2.6",
|
|
44
45
|
"lodash": "^4.17.21",
|
|
45
46
|
"typescript": "^5.8.3",
|
package/src/commands.ts
CHANGED
|
@@ -300,10 +300,15 @@ function createCommand(target: string, options: { framework: string; packageName
|
|
|
300
300
|
}
|
|
301
301
|
|
|
302
302
|
const indexFilePath = path.join(srcDir, 'index.ts');
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
303
|
+
if (!fs.existsSync(indexFilePath)) {
|
|
304
|
+
const indexContent = _.template(reactIndexTpl)({
|
|
305
|
+
components: [],
|
|
306
|
+
});
|
|
307
|
+
writeFileIfChanged(indexFilePath, indexContent);
|
|
308
|
+
} else {
|
|
309
|
+
// Do not overwrite existing index.ts created by the user
|
|
310
|
+
// Leave merge to the codegen step which appends exports safely
|
|
311
|
+
}
|
|
307
312
|
|
|
308
313
|
spawnSync(NPM, ['install', '--omit=peer'], {
|
|
309
314
|
cwd: target,
|
|
@@ -332,7 +337,7 @@ function createCommand(target: string, options: { framework: string; packageName
|
|
|
332
337
|
stdio: 'inherit'
|
|
333
338
|
});
|
|
334
339
|
|
|
335
|
-
spawnSync(NPM, ['install', '
|
|
340
|
+
spawnSync(NPM, ['install', 'vue', '-D'], {
|
|
336
341
|
cwd: target,
|
|
337
342
|
stdio: 'inherit'
|
|
338
343
|
});
|
package/src/dart.ts
CHANGED
|
@@ -83,6 +83,13 @@ ${enumValues};
|
|
|
83
83
|
}
|
|
84
84
|
|
|
85
85
|
function generateReturnType(type: ParameterType, enumName?: string) {
|
|
86
|
+
// Handle union types first (e.g., 'left' | 'center' | 'right')
|
|
87
|
+
// so we don't incorrectly treat string literal unions as pointer types.
|
|
88
|
+
if (Array.isArray(type.value)) {
|
|
89
|
+
// If we have an enum name, use it; otherwise fall back to String
|
|
90
|
+
return enumName || 'String';
|
|
91
|
+
}
|
|
92
|
+
|
|
86
93
|
if (isPointerType(type)) {
|
|
87
94
|
const pointerType = getPointerType(type);
|
|
88
95
|
return pointerType;
|
|
@@ -91,12 +98,6 @@ function generateReturnType(type: ParameterType, enumName?: string) {
|
|
|
91
98
|
return `${getPointerType(type.value)}[]`;
|
|
92
99
|
}
|
|
93
100
|
|
|
94
|
-
// Handle union types (e.g., 'left' | 'center' | 'right')
|
|
95
|
-
if (Array.isArray(type.value)) {
|
|
96
|
-
// If we have an enum name, use it; otherwise fall back to String
|
|
97
|
-
return enumName || 'String';
|
|
98
|
-
}
|
|
99
|
-
|
|
100
101
|
// Handle when type.value is a ParameterType object (nested type)
|
|
101
102
|
if (typeof type.value === 'object' && !Array.isArray(type.value) && type.value !== null) {
|
|
102
103
|
// This might be a nested ParameterType, recurse
|
|
@@ -278,7 +279,7 @@ interface ${object.name} {
|
|
|
278
279
|
|
|
279
280
|
// Generate enums for union types
|
|
280
281
|
const enums: { name: string; definition: string }[] = [];
|
|
281
|
-
const enumMap: Map<string, string> = new Map(); // prop name -> enum name
|
|
282
|
+
const enumMap: Map<string, string> = new Map(); // camelCase prop name -> enum name
|
|
282
283
|
|
|
283
284
|
if (componentProperties) {
|
|
284
285
|
for (const prop of componentProperties.props) {
|
|
@@ -290,7 +291,8 @@ interface ${object.name} {
|
|
|
290
291
|
name: enumName,
|
|
291
292
|
definition: generateDartEnum(enumName, values)
|
|
292
293
|
});
|
|
293
|
-
|
|
294
|
+
// Store by camelCase prop name to match template usage
|
|
295
|
+
enumMap.set(_.camelCase(prop.name), enumName);
|
|
294
296
|
}
|
|
295
297
|
}
|
|
296
298
|
}
|
package/src/generator.ts
CHANGED
|
@@ -11,6 +11,7 @@ import { generateDartClass } from './dart';
|
|
|
11
11
|
import { generateReactComponent, generateReactIndex } from './react';
|
|
12
12
|
import { generateVueTypings } from './vue';
|
|
13
13
|
import { logger, debug, info, success, warn, error, group, progress, time, timeEnd } from './logger';
|
|
14
|
+
import ts from 'typescript';
|
|
14
15
|
|
|
15
16
|
// Cache for file content to avoid redundant reads
|
|
16
17
|
const fileContentCache = new Map<string, string>();
|
|
@@ -321,13 +322,88 @@ export async function reactGen({ source, target, exclude, packageName }: Generat
|
|
|
321
322
|
}
|
|
322
323
|
});
|
|
323
324
|
|
|
324
|
-
// Generate index file
|
|
325
|
+
// Generate/merge index file
|
|
325
326
|
const indexFilePath = path.join(normalizedTarget, 'src', 'index.ts');
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
327
|
+
|
|
328
|
+
// Build desired export map: moduleSpecifier -> Set of names
|
|
329
|
+
const desiredExports = new Map<string, Set<string>>();
|
|
330
|
+
const components = blobs.flatMap(blob => {
|
|
331
|
+
const classObjects = blob.objects.filter(obj => obj instanceof ClassObject) as ClassObject[];
|
|
332
|
+
const properties = classObjects.filter(object => object.name.endsWith('Properties'));
|
|
333
|
+
const events = classObjects.filter(object => object.name.endsWith('Events'));
|
|
334
|
+
const componentMap = new Map<string, boolean>();
|
|
335
|
+
properties.forEach(prop => componentMap.set(prop.name.replace(/Properties$/, ''), true));
|
|
336
|
+
events.forEach(evt => componentMap.set(evt.name.replace(/Events$/, ''), true));
|
|
337
|
+
return Array.from(componentMap.keys()).map(className => ({
|
|
338
|
+
className,
|
|
339
|
+
fileName: blob.filename,
|
|
340
|
+
relativeDir: blob.relativeDir,
|
|
341
|
+
}));
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
// Deduplicate by className
|
|
345
|
+
const unique = new Map<string, { className: string; fileName: string; relativeDir: string }>();
|
|
346
|
+
for (const c of components) {
|
|
347
|
+
if (!unique.has(c.className)) unique.set(c.className, c);
|
|
348
|
+
}
|
|
349
|
+
for (const { className, fileName, relativeDir } of unique.values()) {
|
|
350
|
+
const spec = `./${relativeDir ? `${relativeDir}/` : ''}${fileName}`;
|
|
351
|
+
if (!desiredExports.has(spec)) desiredExports.set(spec, new Set());
|
|
352
|
+
const set = desiredExports.get(spec)!;
|
|
353
|
+
set.add(className);
|
|
354
|
+
set.add(`${className}Element`);
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
if (!fs.existsSync(indexFilePath)) {
|
|
358
|
+
// No index.ts -> generate fresh file from template
|
|
359
|
+
const newExports = generateReactIndex(blobs);
|
|
360
|
+
if (writeFileIfChanged(indexFilePath, newExports)) {
|
|
361
|
+
filesChanged++;
|
|
362
|
+
debug(`Generated: index.ts`);
|
|
363
|
+
}
|
|
364
|
+
} else {
|
|
365
|
+
// Merge into existing index.ts without removing user code
|
|
366
|
+
try {
|
|
367
|
+
const existing = fs.readFileSync(indexFilePath, 'utf-8');
|
|
368
|
+
const sourceFile = ts.createSourceFile(indexFilePath, existing, ts.ScriptTarget.ES2020, true, ts.ScriptKind.TS);
|
|
369
|
+
|
|
370
|
+
// Track which names already exported per module
|
|
371
|
+
for (const stmt of sourceFile.statements) {
|
|
372
|
+
if (ts.isExportDeclaration(stmt) && stmt.exportClause && ts.isNamedExports(stmt.exportClause)) {
|
|
373
|
+
const moduleSpecifier = stmt.moduleSpecifier && ts.isStringLiteral(stmt.moduleSpecifier)
|
|
374
|
+
? stmt.moduleSpecifier.text
|
|
375
|
+
: undefined;
|
|
376
|
+
if (!moduleSpecifier) continue;
|
|
377
|
+
const desired = desiredExports.get(moduleSpecifier);
|
|
378
|
+
if (!desired) continue;
|
|
379
|
+
for (const el of stmt.exportClause.elements) {
|
|
380
|
+
const name = el.name.getText(sourceFile);
|
|
381
|
+
if (desired.has(name)) desired.delete(name);
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
// Prepare new export lines for any remaining names
|
|
387
|
+
const lines: string[] = [];
|
|
388
|
+
for (const [spec, names] of desiredExports) {
|
|
389
|
+
const missing = Array.from(names);
|
|
390
|
+
if (missing.length === 0) continue;
|
|
391
|
+
const specEscaped = spec.replace(/\\/g, '/');
|
|
392
|
+
lines.push(`export { ${missing.join(', ')} } from "${specEscaped}";`);
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
if (lines.length > 0) {
|
|
396
|
+
const appended = (existing.endsWith('\n') ? '' : '\n') + lines.join('\n') + '\n';
|
|
397
|
+
if (writeFileIfChanged(indexFilePath, existing + appended)) {
|
|
398
|
+
filesChanged++;
|
|
399
|
+
debug(`Merged exports into existing index.ts`);
|
|
400
|
+
}
|
|
401
|
+
} else {
|
|
402
|
+
debug(`index.ts is up to date; no merge needed.`);
|
|
403
|
+
}
|
|
404
|
+
} catch (err) {
|
|
405
|
+
warn(`Failed to merge into existing index.ts. Skipping modifications: ${indexFilePath}`);
|
|
406
|
+
}
|
|
331
407
|
}
|
|
332
408
|
|
|
333
409
|
timeEnd('reactGen');
|
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
|
});
|
package/templates/class.dart.tpl
CHANGED
|
@@ -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
|
+
});
|
package/tsconfig.json
CHANGED