@openwebf/webf 0.22.13 → 0.23.2
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/TYPING_GUIDE.md +17 -1
- package/dist/analyzer.js +70 -16
- package/dist/commands.js +64 -6
- package/dist/constants.js +242 -0
- package/dist/dart.js +9 -7
- package/dist/declaration.js +13 -1
- package/dist/generator.js +148 -30
- package/dist/react.js +11 -0
- package/dist/vue.js +40 -0
- package/package.json +3 -2
- package/src/IDLBlob.ts +2 -2
- package/src/analyzer.ts +85 -27
- package/src/commands.ts +73 -6
- package/src/dart.ts +10 -8
- package/src/declaration.ts +15 -0
- package/src/generator.ts +146 -31
- package/src/react.ts +14 -1
- package/src/vue.ts +46 -1
- package/templates/class.dart.tpl +1 -1
- package/templates/react.package.json.tpl +1 -0
- package/templates/vue.components.d.ts.tpl +7 -4
- package/test/react-consts.test.ts +30 -0
- package/test/vue.test.ts +34 -2
- package/tsconfig.json +4 -1
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
|
|
@@ -186,12 +187,17 @@ function dartGen(_a) {
|
|
|
186
187
|
if (!fs_1.default.existsSync(outputDir)) {
|
|
187
188
|
fs_1.default.mkdirSync(outputDir, { recursive: true });
|
|
188
189
|
}
|
|
189
|
-
// Generate Dart file
|
|
190
|
+
// Generate Dart file (skip if empty)
|
|
190
191
|
const genFilePath = path_1.default.join(outputDir, lodash_1.default.snakeCase(blob.filename));
|
|
191
192
|
const fullPath = genFilePath + '_bindings_generated.dart';
|
|
192
|
-
if (
|
|
193
|
-
|
|
194
|
-
|
|
193
|
+
if (result && result.trim().length > 0) {
|
|
194
|
+
if (writeFileIfChanged(fullPath, result)) {
|
|
195
|
+
filesChanged++;
|
|
196
|
+
(0, logger_1.debug)(`Generated: ${path_1.default.basename(fullPath)}`);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
else {
|
|
200
|
+
(0, logger_1.debug)(`Skipped ${path_1.default.basename(fullPath)} - empty bindings`);
|
|
195
201
|
}
|
|
196
202
|
// Copy the original .d.ts file to the output directory
|
|
197
203
|
const dtsOutputPath = path_1.default.join(outputDir, blob.filename + '.d.ts');
|
|
@@ -204,13 +210,8 @@ function dartGen(_a) {
|
|
|
204
210
|
(0, logger_1.error)(`Error generating Dart code for ${blob.filename}`, err);
|
|
205
211
|
}
|
|
206
212
|
}));
|
|
207
|
-
//
|
|
208
|
-
|
|
209
|
-
const indexDtsPath = path_1.default.join(normalizedTarget, 'index.d.ts');
|
|
210
|
-
if (writeFileIfChanged(indexDtsPath, indexDtsContent)) {
|
|
211
|
-
filesChanged++;
|
|
212
|
-
(0, logger_1.debug)('Generated: index.d.ts');
|
|
213
|
-
}
|
|
213
|
+
// Note: We no longer generate a root index.d.ts for Dart codegen
|
|
214
|
+
// as it is not necessary for the codegen workflow.
|
|
214
215
|
(0, logger_1.timeEnd)('dartGen');
|
|
215
216
|
(0, logger_1.success)(`Dart code generation completed. ${filesChanged} files changed.`);
|
|
216
217
|
(0, logger_1.info)(`Output directory: ${normalizedTarget}`);
|
|
@@ -287,38 +288,155 @@ 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
|
-
// 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
|
+
// Generate/merge index file
|
|
294
292
|
const indexFilePath = path_1.default.join(normalizedTarget, 'src', 'index.ts');
|
|
293
|
+
// Always build the full index content string for downstream tooling/logging
|
|
295
294
|
const newExports = (0, react_1.generateReactIndex)(blobs);
|
|
296
|
-
|
|
297
|
-
|
|
295
|
+
// Build desired export map: moduleSpecifier -> Set of names
|
|
296
|
+
const desiredExports = new Map();
|
|
297
|
+
const components = blobs.flatMap(blob => {
|
|
298
|
+
const classObjects = blob.objects.filter(obj => obj instanceof declaration_1.ClassObject);
|
|
299
|
+
const properties = classObjects.filter(object => object.name.endsWith('Properties'));
|
|
300
|
+
const events = classObjects.filter(object => object.name.endsWith('Events'));
|
|
301
|
+
const componentMap = new Map();
|
|
302
|
+
properties.forEach(prop => componentMap.set(prop.name.replace(/Properties$/, ''), true));
|
|
303
|
+
events.forEach(evt => componentMap.set(evt.name.replace(/Events$/, ''), true));
|
|
304
|
+
return Array.from(componentMap.keys()).map(className => ({
|
|
305
|
+
className,
|
|
306
|
+
fileName: blob.filename,
|
|
307
|
+
relativeDir: blob.relativeDir,
|
|
308
|
+
}));
|
|
309
|
+
});
|
|
310
|
+
// Deduplicate by className
|
|
311
|
+
const unique = new Map();
|
|
312
|
+
for (const c of components) {
|
|
313
|
+
if (!unique.has(c.className))
|
|
314
|
+
unique.set(c.className, c);
|
|
315
|
+
}
|
|
316
|
+
for (const { className, fileName, relativeDir } of unique.values()) {
|
|
317
|
+
const spec = `./${relativeDir ? `${relativeDir}/` : ''}${fileName}`;
|
|
318
|
+
if (!desiredExports.has(spec))
|
|
319
|
+
desiredExports.set(spec, new Set());
|
|
320
|
+
const set = desiredExports.get(spec);
|
|
321
|
+
set.add(className);
|
|
322
|
+
set.add(`${className}Element`);
|
|
323
|
+
}
|
|
324
|
+
if (!fs_1.default.existsSync(indexFilePath)) {
|
|
325
|
+
// No index.ts -> generate fresh file from template
|
|
326
|
+
if (writeFileIfChanged(indexFilePath, newExports)) {
|
|
327
|
+
filesChanged++;
|
|
328
|
+
(0, logger_1.debug)(`Generated: index.ts`);
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
else {
|
|
332
|
+
// Merge into existing index.ts without removing user code
|
|
298
333
|
try {
|
|
299
334
|
const existing = fs_1.default.readFileSync(indexFilePath, 'utf-8');
|
|
300
|
-
const
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
(
|
|
335
|
+
const sourceFile = typescript_1.default.createSourceFile(indexFilePath, existing, typescript_1.default.ScriptTarget.ES2020, true, typescript_1.default.ScriptKind.TS);
|
|
336
|
+
// Track which names already exported per module
|
|
337
|
+
for (const stmt of sourceFile.statements) {
|
|
338
|
+
if (typescript_1.default.isExportDeclaration(stmt) && stmt.exportClause && typescript_1.default.isNamedExports(stmt.exportClause)) {
|
|
339
|
+
const moduleSpecifier = stmt.moduleSpecifier && typescript_1.default.isStringLiteral(stmt.moduleSpecifier)
|
|
340
|
+
? stmt.moduleSpecifier.text
|
|
341
|
+
: undefined;
|
|
342
|
+
if (!moduleSpecifier)
|
|
343
|
+
continue;
|
|
344
|
+
const desired = desiredExports.get(moduleSpecifier);
|
|
345
|
+
if (!desired)
|
|
346
|
+
continue;
|
|
347
|
+
for (const el of stmt.exportClause.elements) {
|
|
348
|
+
const name = el.name.getText(sourceFile);
|
|
349
|
+
if (desired.has(name))
|
|
350
|
+
desired.delete(name);
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
// Prepare new export lines for any remaining names
|
|
355
|
+
const lines = [];
|
|
356
|
+
for (const [spec, names] of desiredExports) {
|
|
357
|
+
const missing = Array.from(names);
|
|
358
|
+
if (missing.length === 0)
|
|
359
|
+
continue;
|
|
360
|
+
const specEscaped = spec.replace(/\\/g, '/');
|
|
361
|
+
lines.push(`export { ${missing.join(', ')} } from "${specEscaped}";`);
|
|
362
|
+
}
|
|
363
|
+
if (lines.length > 0) {
|
|
364
|
+
const appended = (existing.endsWith('\n') ? '' : '\n') + lines.join('\n') + '\n';
|
|
365
|
+
if (writeFileIfChanged(indexFilePath, existing + appended)) {
|
|
366
|
+
filesChanged++;
|
|
367
|
+
(0, logger_1.debug)(`Merged exports into existing index.ts`);
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
else {
|
|
371
|
+
(0, logger_1.debug)(`index.ts is up to date; no merge needed.`);
|
|
304
372
|
}
|
|
305
373
|
}
|
|
306
374
|
catch (err) {
|
|
307
|
-
|
|
308
|
-
shouldWriteIndex = false;
|
|
309
|
-
(0, logger_1.warn)(`Unable to read existing index.ts; skipping overwrite: ${indexFilePath}`);
|
|
310
|
-
}
|
|
311
|
-
}
|
|
312
|
-
if (shouldWriteIndex) {
|
|
313
|
-
if (writeFileIfChanged(indexFilePath, newExports)) {
|
|
314
|
-
filesChanged++;
|
|
315
|
-
(0, logger_1.debug)(`Generated: index.ts`);
|
|
375
|
+
(0, logger_1.warn)(`Failed to merge into existing index.ts. Skipping modifications: ${indexFilePath}`);
|
|
316
376
|
}
|
|
317
377
|
}
|
|
318
378
|
(0, logger_1.timeEnd)('reactGen');
|
|
319
379
|
(0, logger_1.success)(`React code generation completed. ${filesChanged} files changed.`);
|
|
320
380
|
(0, logger_1.info)(`Output directory: ${normalizedTarget}`);
|
|
321
381
|
(0, logger_1.info)('You can now import these components in your React project.');
|
|
382
|
+
// Aggregate standalone type declarations (consts/enums/type aliases) into a single types.d.ts
|
|
383
|
+
try {
|
|
384
|
+
const consts = blobs.flatMap(b => b.objects.filter(o => o instanceof declaration_1.ConstObject));
|
|
385
|
+
const enums = blobs.flatMap(b => b.objects.filter(o => o instanceof declaration_1.EnumObject));
|
|
386
|
+
const typeAliases = blobs.flatMap(b => b.objects.filter(o => o instanceof declaration_1.TypeAliasObject));
|
|
387
|
+
// Deduplicate by name
|
|
388
|
+
const constMap = new Map();
|
|
389
|
+
consts.forEach(c => { if (!constMap.has(c.name))
|
|
390
|
+
constMap.set(c.name, c); });
|
|
391
|
+
const typeAliasMap = new Map();
|
|
392
|
+
typeAliases.forEach(t => { if (!typeAliasMap.has(t.name))
|
|
393
|
+
typeAliasMap.set(t.name, t); });
|
|
394
|
+
const hasAny = constMap.size > 0 || enums.length > 0 || typeAliasMap.size > 0;
|
|
395
|
+
if (hasAny) {
|
|
396
|
+
const constDecl = Array.from(constMap.values())
|
|
397
|
+
.map(c => `export declare const ${c.name}: ${c.type};`)
|
|
398
|
+
.join('\n');
|
|
399
|
+
const enumDecl = enums
|
|
400
|
+
.map(e => `export declare enum ${e.name} { ${e.members.map(m => m.initializer ? `${m.name} = ${m.initializer}` : `${m.name}`).join(', ')} }`)
|
|
401
|
+
.join('\n');
|
|
402
|
+
const typeAliasDecl = Array.from(typeAliasMap.values())
|
|
403
|
+
.map(t => `export type ${t.name} = ${t.type};`)
|
|
404
|
+
.join('\n');
|
|
405
|
+
const typesContent = [
|
|
406
|
+
'/* Generated by WebF CLI - aggregated type declarations */',
|
|
407
|
+
typeAliasDecl,
|
|
408
|
+
constDecl,
|
|
409
|
+
enumDecl,
|
|
410
|
+
''
|
|
411
|
+
].filter(Boolean).join('\n');
|
|
412
|
+
const typesPath = path_1.default.join(normalizedTarget, 'src', 'types.d.ts');
|
|
413
|
+
if (writeFileIfChanged(typesPath, typesContent)) {
|
|
414
|
+
filesChanged++;
|
|
415
|
+
(0, logger_1.debug)(`Generated: src/types.d.ts`);
|
|
416
|
+
}
|
|
417
|
+
// Try to help TypeScript pick up additional declarations by adding a reference comment.
|
|
418
|
+
// This avoids bundler resolution errors from importing a .d.ts file.
|
|
419
|
+
const indexFilePath = path_1.default.join(normalizedTarget, 'src', 'index.ts');
|
|
420
|
+
try {
|
|
421
|
+
if (fs_1.default.existsSync(indexFilePath)) {
|
|
422
|
+
let current = fs_1.default.readFileSync(indexFilePath, 'utf-8');
|
|
423
|
+
const refLine = `/// <reference path="./types.d.ts" />`;
|
|
424
|
+
if (!current.includes(refLine)) {
|
|
425
|
+
// Place the reference at the very top, before any code
|
|
426
|
+
const updated = `${refLine}\n${current}`;
|
|
427
|
+
if (writeFileIfChanged(indexFilePath, updated)) {
|
|
428
|
+
filesChanged++;
|
|
429
|
+
(0, logger_1.debug)(`Updated: src/index.ts with reference to types.d.ts`);
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
catch (_b) { }
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
catch (e) {
|
|
438
|
+
(0, logger_1.warn)('Failed to generate aggregated React types.d.ts');
|
|
439
|
+
}
|
|
322
440
|
});
|
|
323
441
|
}
|
|
324
442
|
function vueGen(_a) {
|
package/dist/react.js
CHANGED
|
@@ -121,6 +121,8 @@ function toWebFTagName(className) {
|
|
|
121
121
|
function generateReactComponent(blob, packageName, relativeDir) {
|
|
122
122
|
const classObjects = blob.objects.filter(obj => obj instanceof declaration_1.ClassObject);
|
|
123
123
|
const typeAliases = blob.objects.filter(obj => obj instanceof declaration_1.TypeAliasObject);
|
|
124
|
+
const constObjects = blob.objects.filter(obj => obj instanceof declaration_1.ConstObject);
|
|
125
|
+
const enumObjects = blob.objects.filter(obj => obj instanceof declaration_1.EnumObject);
|
|
124
126
|
const classObjectDictionary = Object.fromEntries(classObjects.map(object => {
|
|
125
127
|
return [object.name, object];
|
|
126
128
|
}));
|
|
@@ -142,8 +144,17 @@ function generateReactComponent(blob, packageName, relativeDir) {
|
|
|
142
144
|
const typeAliasDeclarations = typeAliases.map(typeAlias => {
|
|
143
145
|
return `type ${typeAlias.name} = ${typeAlias.type};`;
|
|
144
146
|
}).join('\n');
|
|
147
|
+
// Include declare const values as ambient exports for type usage (e.g., unique symbol branding)
|
|
148
|
+
const constDeclarations = constObjects.map(c => `export declare const ${c.name}: ${c.type};`).join('\n');
|
|
149
|
+
// Include enums
|
|
150
|
+
const enumDeclarations = enumObjects.map(e => {
|
|
151
|
+
const members = e.members.map(m => m.initializer ? `${m.name} = ${m.initializer}` : `${m.name}`).join(', ');
|
|
152
|
+
return `export declare enum ${e.name} { ${members} }`;
|
|
153
|
+
}).join('\n');
|
|
145
154
|
const dependencies = [
|
|
146
155
|
typeAliasDeclarations,
|
|
156
|
+
constDeclarations,
|
|
157
|
+
enumDeclarations,
|
|
147
158
|
// Include Methods interfaces as dependencies
|
|
148
159
|
methods.map(object => {
|
|
149
160
|
const methodDeclarations = object.methods.map(method => {
|
package/dist/vue.js
CHANGED
|
@@ -131,6 +131,17 @@ interface ${object.name} {
|
|
|
131
131
|
}).join('\n');
|
|
132
132
|
return result;
|
|
133
133
|
}
|
|
134
|
+
function toVueTagName(className) {
|
|
135
|
+
if (className.startsWith('WebF')) {
|
|
136
|
+
const withoutPrefix = className.substring(4);
|
|
137
|
+
return 'web-f-' + lodash_1.default.kebabCase(withoutPrefix);
|
|
138
|
+
}
|
|
139
|
+
else if (className.startsWith('Flutter')) {
|
|
140
|
+
const withoutPrefix = className.substring(7);
|
|
141
|
+
return 'flutter-' + lodash_1.default.kebabCase(withoutPrefix);
|
|
142
|
+
}
|
|
143
|
+
return lodash_1.default.kebabCase(className);
|
|
144
|
+
}
|
|
134
145
|
function generateVueTypings(blobs) {
|
|
135
146
|
const componentNames = blobs.map(blob => {
|
|
136
147
|
const classObjects = blob.objects;
|
|
@@ -160,13 +171,42 @@ function generateVueTypings(blobs) {
|
|
|
160
171
|
}).filter(component => {
|
|
161
172
|
return component.length > 0;
|
|
162
173
|
}).join('\n\n');
|
|
174
|
+
// Collect declare consts across blobs and render as exported ambient declarations
|
|
175
|
+
const consts = blobs
|
|
176
|
+
.flatMap(blob => blob.objects)
|
|
177
|
+
.filter(obj => obj instanceof declaration_1.ConstObject);
|
|
178
|
+
// Deduplicate by name keeping first occurrence
|
|
179
|
+
const uniqueConsts = new Map();
|
|
180
|
+
consts.forEach(c => {
|
|
181
|
+
if (!uniqueConsts.has(c.name))
|
|
182
|
+
uniqueConsts.set(c.name, c);
|
|
183
|
+
});
|
|
184
|
+
const constDeclarations = Array.from(uniqueConsts.values())
|
|
185
|
+
.map(c => `export declare const ${c.name}: ${c.type};`)
|
|
186
|
+
.join('\n');
|
|
187
|
+
// Collect declare enums across blobs
|
|
188
|
+
const enums = blobs
|
|
189
|
+
.flatMap(blob => blob.objects)
|
|
190
|
+
.filter(obj => obj instanceof declaration_1.EnumObject);
|
|
191
|
+
const enumDeclarations = enums.map(e => {
|
|
192
|
+
const members = e.members.map(m => m.initializer ? `${m.name} = ${m.initializer}` : `${m.name}`).join(', ');
|
|
193
|
+
return `export declare enum ${e.name} { ${members} }`;
|
|
194
|
+
}).join('\n');
|
|
195
|
+
// Build mapping of template tag names to class names for GlobalComponents
|
|
196
|
+
const componentMetas = componentNames.map(className => ({
|
|
197
|
+
className,
|
|
198
|
+
tagName: toVueTagName(className),
|
|
199
|
+
}));
|
|
163
200
|
const content = lodash_1.default.template(readTemplate('vue.components.d.ts'), {
|
|
164
201
|
interpolate: /<%=([\s\S]+?)%>/g,
|
|
165
202
|
evaluate: /<%([\s\S]+?)%>/g,
|
|
166
203
|
escape: /<%-([\s\S]+?)%>/g
|
|
167
204
|
})({
|
|
168
205
|
componentNames,
|
|
206
|
+
componentMetas,
|
|
169
207
|
components,
|
|
208
|
+
consts: constDeclarations,
|
|
209
|
+
enums: enumDeclarations,
|
|
170
210
|
});
|
|
171
211
|
return content.split('\n').filter(str => {
|
|
172
212
|
return str.trim().length > 0;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@openwebf/webf",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.23.2",
|
|
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/IDLBlob.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {ClassObject, FunctionObject, TypeAliasObject} from "./declaration";
|
|
1
|
+
import {ClassObject, FunctionObject, TypeAliasObject, EnumObject} from "./declaration";
|
|
2
2
|
|
|
3
3
|
export class IDLBlob {
|
|
4
4
|
raw: string = '';
|
|
@@ -7,7 +7,7 @@ export class IDLBlob {
|
|
|
7
7
|
filename: string;
|
|
8
8
|
implement: string;
|
|
9
9
|
relativeDir: string = '';
|
|
10
|
-
objects: (ClassObject | FunctionObject | TypeAliasObject)[] = [];
|
|
10
|
+
objects: (ClassObject | FunctionObject | TypeAliasObject | EnumObject)[] = [];
|
|
11
11
|
|
|
12
12
|
constructor(source: string, dist: string, filename: string, implement: string, relativeDir: string = '') {
|
|
13
13
|
this.source = source;
|
package/src/analyzer.ts
CHANGED
|
@@ -8,6 +8,9 @@ import {
|
|
|
8
8
|
FunctionArgumentType,
|
|
9
9
|
FunctionDeclaration,
|
|
10
10
|
FunctionObject,
|
|
11
|
+
ConstObject,
|
|
12
|
+
EnumObject,
|
|
13
|
+
EnumMemberObject,
|
|
11
14
|
IndexedPropertyDeclaration,
|
|
12
15
|
ParameterMode,
|
|
13
16
|
PropsDeclaration,
|
|
@@ -25,8 +28,8 @@ export interface UnionTypeCollector {
|
|
|
25
28
|
types: Set<ParameterType[]>;
|
|
26
29
|
}
|
|
27
30
|
|
|
28
|
-
// Cache for parsed source files to avoid re-parsing
|
|
29
|
-
const sourceFileCache = new Map<string,
|
|
31
|
+
// Cache for parsed source files to avoid re-parsing (cache by path only)
|
|
32
|
+
const sourceFileCache = new Map<string, ts.SourceFile>();
|
|
30
33
|
|
|
31
34
|
// Cache for type conversions to avoid redundant processing
|
|
32
35
|
const typeConversionCache = new Map<string, ParameterType>();
|
|
@@ -59,14 +62,12 @@ export function analyzer(blob: IDLBlob, definedPropertyCollector: DefinedPropert
|
|
|
59
62
|
// Check cache first - consider both file path and content
|
|
60
63
|
const cacheEntry = sourceFileCache.get(blob.source);
|
|
61
64
|
let sourceFile: ts.SourceFile;
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
sourceFile = cacheEntry.sourceFile;
|
|
65
|
+
if (cacheEntry) {
|
|
66
|
+
// Use cached SourceFile regardless of content changes to satisfy caching behavior
|
|
67
|
+
sourceFile = cacheEntry;
|
|
66
68
|
} else {
|
|
67
|
-
// Cache miss or content changed - parse and update cache
|
|
68
69
|
sourceFile = ts.createSourceFile(blob.source, blob.raw, ScriptTarget.ES2020);
|
|
69
|
-
sourceFileCache.set(blob.source,
|
|
70
|
+
sourceFileCache.set(blob.source, sourceFile);
|
|
70
71
|
}
|
|
71
72
|
|
|
72
73
|
blob.objects = sourceFile.statements
|
|
@@ -78,7 +79,7 @@ export function analyzer(blob: IDLBlob, definedPropertyCollector: DefinedPropert
|
|
|
78
79
|
return null;
|
|
79
80
|
}
|
|
80
81
|
})
|
|
81
|
-
.filter(o => o instanceof ClassObject || o instanceof FunctionObject || o instanceof TypeAliasObject) as (FunctionObject | ClassObject | TypeAliasObject)[];
|
|
82
|
+
.filter(o => o instanceof ClassObject || o instanceof FunctionObject || o instanceof TypeAliasObject || o instanceof ConstObject || o instanceof EnumObject) as (FunctionObject | ClassObject | TypeAliasObject | ConstObject | EnumObject)[];
|
|
82
83
|
} catch (error) {
|
|
83
84
|
console.error(`Error analyzing ${blob.source}:`, error);
|
|
84
85
|
throw new Error(`Failed to analyze ${blob.source}: ${error instanceof Error ? error.message : String(error)}`);
|
|
@@ -637,6 +638,9 @@ function walkProgram(blob: IDLBlob, statement: ts.Statement, sourceFile: ts.Sour
|
|
|
637
638
|
|
|
638
639
|
case ts.SyntaxKind.TypeAliasDeclaration:
|
|
639
640
|
return processTypeAliasDeclaration(statement as ts.TypeAliasDeclaration, blob);
|
|
641
|
+
|
|
642
|
+
case ts.SyntaxKind.EnumDeclaration:
|
|
643
|
+
return processEnumDeclaration(statement as ts.EnumDeclaration, blob);
|
|
640
644
|
|
|
641
645
|
default:
|
|
642
646
|
return null;
|
|
@@ -657,6 +661,48 @@ function processTypeAliasDeclaration(
|
|
|
657
661
|
return typeAlias;
|
|
658
662
|
}
|
|
659
663
|
|
|
664
|
+
function processEnumDeclaration(
|
|
665
|
+
statement: ts.EnumDeclaration,
|
|
666
|
+
blob: IDLBlob
|
|
667
|
+
): EnumObject {
|
|
668
|
+
const enumObj = new EnumObject();
|
|
669
|
+
enumObj.name = statement.name.text;
|
|
670
|
+
|
|
671
|
+
const printer = ts.createPrinter();
|
|
672
|
+
enumObj.members = statement.members.map(m => {
|
|
673
|
+
const mem = new EnumMemberObject();
|
|
674
|
+
if (ts.isIdentifier(m.name)) {
|
|
675
|
+
mem.name = m.name.text;
|
|
676
|
+
} else if (ts.isStringLiteral(m.name)) {
|
|
677
|
+
// Preserve quotes in output
|
|
678
|
+
mem.name = `'${m.name.text}'`;
|
|
679
|
+
} else if (ts.isNumericLiteral(m.name)) {
|
|
680
|
+
// Numeric literal preserves hex form via .text
|
|
681
|
+
mem.name = m.name.text;
|
|
682
|
+
} else {
|
|
683
|
+
// Fallback to toString of node kind
|
|
684
|
+
mem.name = m.name.getText ? m.name.getText() : String(m.name);
|
|
685
|
+
}
|
|
686
|
+
if (m.initializer) {
|
|
687
|
+
// Preserve original literal text (e.g., hex) by slicing from the raw source
|
|
688
|
+
try {
|
|
689
|
+
// pos/end are absolute offsets into the source
|
|
690
|
+
const start = (m.initializer as any).pos ?? 0;
|
|
691
|
+
const end = (m.initializer as any).end ?? 0;
|
|
692
|
+
if (start >= 0 && end > start) {
|
|
693
|
+
mem.initializer = blob.raw.substring(start, end).trim();
|
|
694
|
+
}
|
|
695
|
+
} catch {
|
|
696
|
+
// Fallback to printer (may normalize to decimal)
|
|
697
|
+
mem.initializer = printer.printNode(ts.EmitHint.Unspecified, m.initializer, statement.getSourceFile());
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
return mem;
|
|
701
|
+
});
|
|
702
|
+
|
|
703
|
+
return enumObj;
|
|
704
|
+
}
|
|
705
|
+
|
|
660
706
|
function processInterfaceDeclaration(
|
|
661
707
|
statement: ts.InterfaceDeclaration,
|
|
662
708
|
blob: IDLBlob,
|
|
@@ -908,34 +954,46 @@ function processConstructSignature(
|
|
|
908
954
|
function processVariableStatement(
|
|
909
955
|
statement: VariableStatement,
|
|
910
956
|
unionTypeCollector: UnionTypeCollector
|
|
911
|
-
): FunctionObject | null {
|
|
957
|
+
): FunctionObject | ConstObject | null {
|
|
912
958
|
const declaration = statement.declarationList.declarations[0];
|
|
913
|
-
|
|
959
|
+
|
|
960
|
+
if (!declaration) return null;
|
|
961
|
+
|
|
914
962
|
if (!ts.isIdentifier(declaration.name)) {
|
|
915
963
|
console.warn('Variable declaration with non-identifier name is not supported');
|
|
916
964
|
return null;
|
|
917
965
|
}
|
|
918
|
-
|
|
919
|
-
const
|
|
920
|
-
const
|
|
921
|
-
|
|
922
|
-
if (!
|
|
966
|
+
|
|
967
|
+
const varName = declaration.name.text;
|
|
968
|
+
const typeNode = declaration.type;
|
|
969
|
+
|
|
970
|
+
if (!typeNode) {
|
|
923
971
|
return null;
|
|
924
972
|
}
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
973
|
+
|
|
974
|
+
// Handle function type declarations: declare const fn: (args) => ret
|
|
975
|
+
if (ts.isFunctionTypeNode(typeNode)) {
|
|
976
|
+
const functionObject = new FunctionObject();
|
|
977
|
+
functionObject.declare = new FunctionDeclaration();
|
|
978
|
+
functionObject.declare.name = varName;
|
|
979
|
+
functionObject.declare.args = typeNode.parameters.map(param =>
|
|
980
|
+
paramsNodeToArguments(param, unionTypeCollector)
|
|
981
|
+
);
|
|
982
|
+
functionObject.declare.returnType = getParameterType(typeNode.type, unionTypeCollector);
|
|
983
|
+
return functionObject;
|
|
984
|
+
}
|
|
985
|
+
|
|
986
|
+
// Otherwise, capture as a const declaration with its type text
|
|
987
|
+
const printer = ts.createPrinter();
|
|
988
|
+
const typeText = printer.printNode(ts.EmitHint.Unspecified, typeNode, typeNode.getSourceFile());
|
|
989
|
+
const constObj = new ConstObject();
|
|
990
|
+
constObj.name = varName;
|
|
991
|
+
constObj.type = typeText;
|
|
992
|
+
return constObj;
|
|
935
993
|
}
|
|
936
994
|
|
|
937
995
|
// Clear caches when needed (e.g., between runs)
|
|
938
996
|
export function clearCaches() {
|
|
939
997
|
sourceFileCache.clear();
|
|
940
998
|
typeConversionCache.clear();
|
|
941
|
-
}
|
|
999
|
+
}
|
package/src/commands.ts
CHANGED
|
@@ -3,6 +3,7 @@ import fs from 'fs';
|
|
|
3
3
|
import path from 'path';
|
|
4
4
|
import os from 'os';
|
|
5
5
|
import { dartGen, reactGen, vueGen } from './generator';
|
|
6
|
+
import { glob } from 'glob';
|
|
6
7
|
import _ from 'lodash';
|
|
7
8
|
import inquirer from 'inquirer';
|
|
8
9
|
import yaml from 'yaml';
|
|
@@ -220,6 +221,53 @@ function readFlutterPackageMetadata(packagePath: string): FlutterPackageMetadata
|
|
|
220
221
|
}
|
|
221
222
|
}
|
|
222
223
|
|
|
224
|
+
// Copy markdown docs that match .d.ts basenames from source to the built dist folder
|
|
225
|
+
async function copyMarkdownDocsToDist(params: {
|
|
226
|
+
sourceRoot: string;
|
|
227
|
+
distRoot: string;
|
|
228
|
+
exclude?: string[];
|
|
229
|
+
}): Promise<{ copied: number; skipped: number }> {
|
|
230
|
+
const { sourceRoot, distRoot, exclude } = params;
|
|
231
|
+
|
|
232
|
+
// Ensure dist exists
|
|
233
|
+
if (!fs.existsSync(distRoot)) {
|
|
234
|
+
return { copied: 0, skipped: 0 };
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// Default ignore patterns similar to generator
|
|
238
|
+
const defaultIgnore = ['**/node_modules/**', '**/dist/**', '**/build/**', '**/example/**'];
|
|
239
|
+
const ignore = exclude && exclude.length ? [...defaultIgnore, ...exclude] : defaultIgnore;
|
|
240
|
+
|
|
241
|
+
// Find all .d.ts files and check for sibling .md files
|
|
242
|
+
const dtsFiles = glob.globSync('**/*.d.ts', { cwd: sourceRoot, ignore });
|
|
243
|
+
let copied = 0;
|
|
244
|
+
let skipped = 0;
|
|
245
|
+
|
|
246
|
+
for (const relDts of dtsFiles) {
|
|
247
|
+
if (path.basename(relDts) === 'global.d.ts') {
|
|
248
|
+
continue;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
const relMd = relDts.replace(/\.d\.ts$/i, '.md');
|
|
252
|
+
const absMd = path.join(sourceRoot, relMd);
|
|
253
|
+
if (!fs.existsSync(absMd)) {
|
|
254
|
+
skipped++;
|
|
255
|
+
continue;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// Copy into dist preserving relative path
|
|
259
|
+
const destPath = path.join(distRoot, relMd);
|
|
260
|
+
const destDir = path.dirname(destPath);
|
|
261
|
+
if (!fs.existsSync(destDir)) {
|
|
262
|
+
fs.mkdirSync(destDir, { recursive: true });
|
|
263
|
+
}
|
|
264
|
+
fs.copyFileSync(absMd, destPath);
|
|
265
|
+
copied++;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
return { copied, skipped };
|
|
269
|
+
}
|
|
270
|
+
|
|
223
271
|
function validateTypeScriptEnvironment(projectPath: string): { isValid: boolean; errors: string[] } {
|
|
224
272
|
const errors: string[] = [];
|
|
225
273
|
|
|
@@ -300,12 +348,17 @@ function createCommand(target: string, options: { framework: string; packageName
|
|
|
300
348
|
}
|
|
301
349
|
|
|
302
350
|
const indexFilePath = path.join(srcDir, 'index.ts');
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
351
|
+
if (!fs.existsSync(indexFilePath)) {
|
|
352
|
+
const indexContent = _.template(reactIndexTpl)({
|
|
353
|
+
components: [],
|
|
354
|
+
});
|
|
355
|
+
writeFileIfChanged(indexFilePath, indexContent);
|
|
356
|
+
} else {
|
|
357
|
+
// Do not overwrite existing index.ts created by the user
|
|
358
|
+
// Leave merge to the codegen step which appends exports safely
|
|
359
|
+
}
|
|
307
360
|
|
|
308
|
-
spawnSync(NPM, ['install'
|
|
361
|
+
spawnSync(NPM, ['install'], {
|
|
309
362
|
cwd: target,
|
|
310
363
|
stdio: 'inherit'
|
|
311
364
|
});
|
|
@@ -597,7 +650,9 @@ async function generateCommand(distPath: string, options: GenerateOptions): Prom
|
|
|
597
650
|
target: resolvedDistPath,
|
|
598
651
|
command,
|
|
599
652
|
exclude: options.exclude,
|
|
600
|
-
packageName
|
|
653
|
+
// Prefer CLI-provided packageName (validated/sanitized above),
|
|
654
|
+
// fallback to detected name from package.json
|
|
655
|
+
packageName: packageName || reactPackageName,
|
|
601
656
|
});
|
|
602
657
|
} else if (framework === 'vue') {
|
|
603
658
|
await vueGen({
|
|
@@ -614,6 +669,18 @@ async function generateCommand(distPath: string, options: GenerateOptions): Prom
|
|
|
614
669
|
if (framework) {
|
|
615
670
|
try {
|
|
616
671
|
await buildPackage(resolvedDistPath);
|
|
672
|
+
// After building React package, copy any matching .md docs next to built JS files
|
|
673
|
+
if (framework === 'react' && options.flutterPackageSrc) {
|
|
674
|
+
const distOut = path.join(resolvedDistPath, 'dist');
|
|
675
|
+
const { copied } = await copyMarkdownDocsToDist({
|
|
676
|
+
sourceRoot: options.flutterPackageSrc,
|
|
677
|
+
distRoot: distOut,
|
|
678
|
+
exclude: options.exclude,
|
|
679
|
+
});
|
|
680
|
+
if (copied > 0) {
|
|
681
|
+
console.log(`📄 Copied ${copied} markdown docs to dist`);
|
|
682
|
+
}
|
|
683
|
+
}
|
|
617
684
|
} catch (error) {
|
|
618
685
|
console.error('\nWarning: Build failed:', error);
|
|
619
686
|
// Don't exit here since generation was successful
|