@openwebf/webf 0.23.0 → 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 +54 -2
- package/dist/constants.js +242 -0
- package/dist/declaration.js +13 -1
- package/dist/generator.js +71 -12
- package/dist/react.js +11 -0
- package/dist/vue.js +40 -0
- package/package.json +1 -1
- package/src/IDLBlob.ts +2 -2
- package/src/analyzer.ts +85 -27
- package/src/commands.ts +64 -2
- package/src/declaration.ts +15 -0
- package/src/generator.ts +73 -13
- package/src/react.ts +14 -1
- package/src/vue.ts +46 -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/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
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
|
|
|
@@ -310,7 +358,7 @@ function createCommand(target: string, options: { framework: string; packageName
|
|
|
310
358
|
// Leave merge to the codegen step which appends exports safely
|
|
311
359
|
}
|
|
312
360
|
|
|
313
|
-
spawnSync(NPM, ['install'
|
|
361
|
+
spawnSync(NPM, ['install'], {
|
|
314
362
|
cwd: target,
|
|
315
363
|
stdio: 'inherit'
|
|
316
364
|
});
|
|
@@ -602,7 +650,9 @@ async function generateCommand(distPath: string, options: GenerateOptions): Prom
|
|
|
602
650
|
target: resolvedDistPath,
|
|
603
651
|
command,
|
|
604
652
|
exclude: options.exclude,
|
|
605
|
-
packageName
|
|
653
|
+
// Prefer CLI-provided packageName (validated/sanitized above),
|
|
654
|
+
// fallback to detected name from package.json
|
|
655
|
+
packageName: packageName || reactPackageName,
|
|
606
656
|
});
|
|
607
657
|
} else if (framework === 'vue') {
|
|
608
658
|
await vueGen({
|
|
@@ -619,6 +669,18 @@ async function generateCommand(distPath: string, options: GenerateOptions): Prom
|
|
|
619
669
|
if (framework) {
|
|
620
670
|
try {
|
|
621
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
|
+
}
|
|
622
684
|
} catch (error) {
|
|
623
685
|
console.error('\nWarning: Build failed:', error);
|
|
624
686
|
// Don't exit here since generation was successful
|
package/src/declaration.ts
CHANGED
|
@@ -89,3 +89,18 @@ export class TypeAliasObject {
|
|
|
89
89
|
name: string;
|
|
90
90
|
type: string;
|
|
91
91
|
}
|
|
92
|
+
|
|
93
|
+
export class ConstObject {
|
|
94
|
+
name: string;
|
|
95
|
+
type: string;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export class EnumMemberObject {
|
|
99
|
+
name: string;
|
|
100
|
+
initializer?: string;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export class EnumObject {
|
|
104
|
+
name: string;
|
|
105
|
+
members: EnumMemberObject[] = [];
|
|
106
|
+
}
|
package/src/generator.ts
CHANGED
|
@@ -5,7 +5,7 @@ import _ from 'lodash';
|
|
|
5
5
|
import { glob } from 'glob';
|
|
6
6
|
import yaml from 'yaml';
|
|
7
7
|
import { IDLBlob } from './IDLBlob';
|
|
8
|
-
import { ClassObject } from './declaration';
|
|
8
|
+
import { ClassObject, ConstObject, EnumObject, TypeAliasObject } from './declaration';
|
|
9
9
|
import { analyzer, ParameterType, clearCaches } from './analyzer';
|
|
10
10
|
import { generateDartClass } from './dart';
|
|
11
11
|
import { generateReactComponent, generateReactIndex } from './react';
|
|
@@ -205,13 +205,17 @@ export async function dartGen({ source, target, command, exclude }: GenerateOpti
|
|
|
205
205
|
fs.mkdirSync(outputDir, { recursive: true });
|
|
206
206
|
}
|
|
207
207
|
|
|
208
|
-
// Generate Dart file
|
|
208
|
+
// Generate Dart file (skip if empty)
|
|
209
209
|
const genFilePath = path.join(outputDir, _.snakeCase(blob.filename));
|
|
210
210
|
const fullPath = genFilePath + '_bindings_generated.dart';
|
|
211
211
|
|
|
212
|
-
if (
|
|
213
|
-
|
|
214
|
-
|
|
212
|
+
if (result && result.trim().length > 0) {
|
|
213
|
+
if (writeFileIfChanged(fullPath, result)) {
|
|
214
|
+
filesChanged++;
|
|
215
|
+
debug(`Generated: ${path.basename(fullPath)}`);
|
|
216
|
+
}
|
|
217
|
+
} else {
|
|
218
|
+
debug(`Skipped ${path.basename(fullPath)} - empty bindings`);
|
|
215
219
|
}
|
|
216
220
|
|
|
217
221
|
// Copy the original .d.ts file to the output directory
|
|
@@ -225,13 +229,8 @@ export async function dartGen({ source, target, command, exclude }: GenerateOpti
|
|
|
225
229
|
}
|
|
226
230
|
});
|
|
227
231
|
|
|
228
|
-
//
|
|
229
|
-
|
|
230
|
-
const indexDtsPath = path.join(normalizedTarget, 'index.d.ts');
|
|
231
|
-
if (writeFileIfChanged(indexDtsPath, indexDtsContent)) {
|
|
232
|
-
filesChanged++;
|
|
233
|
-
debug('Generated: index.d.ts');
|
|
234
|
-
}
|
|
232
|
+
// Note: We no longer generate a root index.d.ts for Dart codegen
|
|
233
|
+
// as it is not necessary for the codegen workflow.
|
|
235
234
|
|
|
236
235
|
timeEnd('dartGen');
|
|
237
236
|
success(`Dart code generation completed. ${filesChanged} files changed.`);
|
|
@@ -324,6 +323,8 @@ export async function reactGen({ source, target, exclude, packageName }: Generat
|
|
|
324
323
|
|
|
325
324
|
// Generate/merge index file
|
|
326
325
|
const indexFilePath = path.join(normalizedTarget, 'src', 'index.ts');
|
|
326
|
+
// Always build the full index content string for downstream tooling/logging
|
|
327
|
+
const newExports = generateReactIndex(blobs);
|
|
327
328
|
|
|
328
329
|
// Build desired export map: moduleSpecifier -> Set of names
|
|
329
330
|
const desiredExports = new Map<string, Set<string>>();
|
|
@@ -356,7 +357,6 @@ export async function reactGen({ source, target, exclude, packageName }: Generat
|
|
|
356
357
|
|
|
357
358
|
if (!fs.existsSync(indexFilePath)) {
|
|
358
359
|
// No index.ts -> generate fresh file from template
|
|
359
|
-
const newExports = generateReactIndex(blobs);
|
|
360
360
|
if (writeFileIfChanged(indexFilePath, newExports)) {
|
|
361
361
|
filesChanged++;
|
|
362
362
|
debug(`Generated: index.ts`);
|
|
@@ -410,6 +410,66 @@ export async function reactGen({ source, target, exclude, packageName }: Generat
|
|
|
410
410
|
success(`React code generation completed. ${filesChanged} files changed.`);
|
|
411
411
|
info(`Output directory: ${normalizedTarget}`);
|
|
412
412
|
info('You can now import these components in your React project.');
|
|
413
|
+
|
|
414
|
+
// Aggregate standalone type declarations (consts/enums/type aliases) into a single types.d.ts
|
|
415
|
+
try {
|
|
416
|
+
const consts = blobs.flatMap(b => b.objects.filter(o => o instanceof ConstObject) as ConstObject[]);
|
|
417
|
+
const enums = blobs.flatMap(b => b.objects.filter(o => o instanceof EnumObject) as EnumObject[]);
|
|
418
|
+
const typeAliases = blobs.flatMap(b => b.objects.filter(o => o instanceof TypeAliasObject) as TypeAliasObject[]);
|
|
419
|
+
|
|
420
|
+
// Deduplicate by name
|
|
421
|
+
const constMap = new Map<string, ConstObject>();
|
|
422
|
+
consts.forEach(c => { if (!constMap.has(c.name)) constMap.set(c.name, c); });
|
|
423
|
+
const typeAliasMap = new Map<string, TypeAliasObject>();
|
|
424
|
+
typeAliases.forEach(t => { if (!typeAliasMap.has(t.name)) typeAliasMap.set(t.name, t); });
|
|
425
|
+
|
|
426
|
+
const hasAny = constMap.size > 0 || enums.length > 0 || typeAliasMap.size > 0;
|
|
427
|
+
if (hasAny) {
|
|
428
|
+
const constDecl = Array.from(constMap.values())
|
|
429
|
+
.map(c => `export declare const ${c.name}: ${c.type};`)
|
|
430
|
+
.join('\n');
|
|
431
|
+
const enumDecl = enums
|
|
432
|
+
.map(e => `export declare enum ${e.name} { ${e.members.map(m => m.initializer ? `${m.name} = ${m.initializer}` : `${m.name}`).join(', ')} }`)
|
|
433
|
+
.join('\n');
|
|
434
|
+
const typeAliasDecl = Array.from(typeAliasMap.values())
|
|
435
|
+
.map(t => `export type ${t.name} = ${t.type};`)
|
|
436
|
+
.join('\n');
|
|
437
|
+
|
|
438
|
+
const typesContent = [
|
|
439
|
+
'/* Generated by WebF CLI - aggregated type declarations */',
|
|
440
|
+
typeAliasDecl,
|
|
441
|
+
constDecl,
|
|
442
|
+
enumDecl,
|
|
443
|
+
''
|
|
444
|
+
].filter(Boolean).join('\n');
|
|
445
|
+
|
|
446
|
+
const typesPath = path.join(normalizedTarget, 'src', 'types.d.ts');
|
|
447
|
+
if (writeFileIfChanged(typesPath, typesContent)) {
|
|
448
|
+
filesChanged++;
|
|
449
|
+
debug(`Generated: src/types.d.ts`);
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
// Try to help TypeScript pick up additional declarations by adding a reference comment.
|
|
453
|
+
// This avoids bundler resolution errors from importing a .d.ts file.
|
|
454
|
+
const indexFilePath = path.join(normalizedTarget, 'src', 'index.ts');
|
|
455
|
+
try {
|
|
456
|
+
if (fs.existsSync(indexFilePath)) {
|
|
457
|
+
let current = fs.readFileSync(indexFilePath, 'utf-8');
|
|
458
|
+
const refLine = `/// <reference path="./types.d.ts" />`;
|
|
459
|
+
if (!current.includes(refLine)) {
|
|
460
|
+
// Place the reference at the very top, before any code
|
|
461
|
+
const updated = `${refLine}\n${current}`;
|
|
462
|
+
if (writeFileIfChanged(indexFilePath, updated)) {
|
|
463
|
+
filesChanged++;
|
|
464
|
+
debug(`Updated: src/index.ts with reference to types.d.ts`);
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
} catch {}
|
|
469
|
+
}
|
|
470
|
+
} catch (e) {
|
|
471
|
+
warn('Failed to generate aggregated React types.d.ts');
|
|
472
|
+
}
|
|
413
473
|
}
|
|
414
474
|
|
|
415
475
|
export async function vueGen({ source, target, exclude }: GenerateOptions) {
|
package/src/react.ts
CHANGED
|
@@ -2,7 +2,7 @@ import _ from "lodash";
|
|
|
2
2
|
import fs from 'fs';
|
|
3
3
|
import path from 'path';
|
|
4
4
|
import {ParameterType} from "./analyzer";
|
|
5
|
-
import {ClassObject, FunctionArgumentType, FunctionDeclaration, TypeAliasObject} from "./declaration";
|
|
5
|
+
import {ClassObject, FunctionArgumentType, FunctionDeclaration, TypeAliasObject, ConstObject, EnumObject} from "./declaration";
|
|
6
6
|
import {IDLBlob} from "./IDLBlob";
|
|
7
7
|
import {getPointerType, isPointerType, isUnionType} from "./utils";
|
|
8
8
|
|
|
@@ -125,6 +125,8 @@ export function toWebFTagName(className: string): string {
|
|
|
125
125
|
export function generateReactComponent(blob: IDLBlob, packageName?: string, relativeDir?: string) {
|
|
126
126
|
const classObjects = blob.objects.filter(obj => obj instanceof ClassObject) as ClassObject[];
|
|
127
127
|
const typeAliases = blob.objects.filter(obj => obj instanceof TypeAliasObject) as TypeAliasObject[];
|
|
128
|
+
const constObjects = blob.objects.filter(obj => obj instanceof ConstObject) as ConstObject[];
|
|
129
|
+
const enumObjects = blob.objects.filter(obj => obj instanceof EnumObject) as EnumObject[];
|
|
128
130
|
|
|
129
131
|
const classObjectDictionary = Object.fromEntries(
|
|
130
132
|
classObjects.map(object => {
|
|
@@ -153,8 +155,19 @@ export function generateReactComponent(blob: IDLBlob, packageName?: string, rela
|
|
|
153
155
|
return `type ${typeAlias.name} = ${typeAlias.type};`;
|
|
154
156
|
}).join('\n');
|
|
155
157
|
|
|
158
|
+
// Include declare const values as ambient exports for type usage (e.g., unique symbol branding)
|
|
159
|
+
const constDeclarations = constObjects.map(c => `export declare const ${c.name}: ${c.type};`).join('\n');
|
|
160
|
+
|
|
161
|
+
// Include enums
|
|
162
|
+
const enumDeclarations = enumObjects.map(e => {
|
|
163
|
+
const members = e.members.map(m => m.initializer ? `${m.name} = ${m.initializer}` : `${m.name}`).join(', ');
|
|
164
|
+
return `export declare enum ${e.name} { ${members} }`;
|
|
165
|
+
}).join('\n');
|
|
166
|
+
|
|
156
167
|
const dependencies = [
|
|
157
168
|
typeAliasDeclarations,
|
|
169
|
+
constDeclarations,
|
|
170
|
+
enumDeclarations,
|
|
158
171
|
// Include Methods interfaces as dependencies
|
|
159
172
|
methods.map(object => {
|
|
160
173
|
const methodDeclarations = object.methods.map(method => {
|
package/src/vue.ts
CHANGED
|
@@ -2,7 +2,7 @@ import _ from "lodash";
|
|
|
2
2
|
import fs from 'fs';
|
|
3
3
|
import path from 'path';
|
|
4
4
|
import {ParameterType} from "./analyzer";
|
|
5
|
-
import {ClassObject, FunctionArgumentType, FunctionDeclaration} from "./declaration";
|
|
5
|
+
import {ClassObject, FunctionArgumentType, FunctionDeclaration, ConstObject, EnumObject} from "./declaration";
|
|
6
6
|
import {IDLBlob} from "./IDLBlob";
|
|
7
7
|
import {getPointerType, isPointerType} from "./utils";
|
|
8
8
|
|
|
@@ -145,6 +145,17 @@ interface ${object.name} {
|
|
|
145
145
|
return result;
|
|
146
146
|
}
|
|
147
147
|
|
|
148
|
+
function toVueTagName(className: string): string {
|
|
149
|
+
if (className.startsWith('WebF')) {
|
|
150
|
+
const withoutPrefix = className.substring(4);
|
|
151
|
+
return 'web-f-' + _.kebabCase(withoutPrefix);
|
|
152
|
+
} else if (className.startsWith('Flutter')) {
|
|
153
|
+
const withoutPrefix = className.substring(7);
|
|
154
|
+
return 'flutter-' + _.kebabCase(withoutPrefix);
|
|
155
|
+
}
|
|
156
|
+
return _.kebabCase(className);
|
|
157
|
+
}
|
|
158
|
+
|
|
148
159
|
export function generateVueTypings(blobs: IDLBlob[]) {
|
|
149
160
|
const componentNames = blobs.map(blob => {
|
|
150
161
|
const classObjects = blob.objects as ClassObject[];
|
|
@@ -177,13 +188,47 @@ export function generateVueTypings(blobs: IDLBlob[]) {
|
|
|
177
188
|
return component.length > 0;
|
|
178
189
|
}).join('\n\n');
|
|
179
190
|
|
|
191
|
+
// Collect declare consts across blobs and render as exported ambient declarations
|
|
192
|
+
const consts = blobs
|
|
193
|
+
.flatMap(blob => blob.objects)
|
|
194
|
+
.filter(obj => obj instanceof ConstObject) as ConstObject[];
|
|
195
|
+
|
|
196
|
+
// Deduplicate by name keeping first occurrence
|
|
197
|
+
const uniqueConsts = new Map<string, ConstObject>();
|
|
198
|
+
consts.forEach(c => {
|
|
199
|
+
if (!uniqueConsts.has(c.name)) uniqueConsts.set(c.name, c);
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
const constDeclarations = Array.from(uniqueConsts.values())
|
|
203
|
+
.map(c => `export declare const ${c.name}: ${c.type};`)
|
|
204
|
+
.join('\n');
|
|
205
|
+
|
|
206
|
+
// Collect declare enums across blobs
|
|
207
|
+
const enums = blobs
|
|
208
|
+
.flatMap(blob => blob.objects)
|
|
209
|
+
.filter(obj => obj instanceof EnumObject) as EnumObject[];
|
|
210
|
+
|
|
211
|
+
const enumDeclarations = enums.map(e => {
|
|
212
|
+
const members = e.members.map(m => m.initializer ? `${m.name} = ${m.initializer}` : `${m.name}`).join(', ');
|
|
213
|
+
return `export declare enum ${e.name} { ${members} }`;
|
|
214
|
+
}).join('\n');
|
|
215
|
+
|
|
216
|
+
// Build mapping of template tag names to class names for GlobalComponents
|
|
217
|
+
const componentMetas = componentNames.map(className => ({
|
|
218
|
+
className,
|
|
219
|
+
tagName: toVueTagName(className),
|
|
220
|
+
}));
|
|
221
|
+
|
|
180
222
|
const content = _.template(readTemplate('vue.components.d.ts'), {
|
|
181
223
|
interpolate: /<%=([\s\S]+?)%>/g,
|
|
182
224
|
evaluate: /<%([\s\S]+?)%>/g,
|
|
183
225
|
escape: /<%-([\s\S]+?)%>/g
|
|
184
226
|
})({
|
|
185
227
|
componentNames,
|
|
228
|
+
componentMetas,
|
|
186
229
|
components,
|
|
230
|
+
consts: constDeclarations,
|
|
231
|
+
enums: enumDeclarations,
|
|
187
232
|
});
|
|
188
233
|
|
|
189
234
|
return content.split('\n').filter(str => {
|
|
@@ -19,6 +19,9 @@ type VueEventListeners<T extends EventMap> = {
|
|
|
19
19
|
[K in keyof T as `on${Capitalize<string & K>}`]?: (event: T[K]) => any
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
+
<%= consts %>
|
|
23
|
+
<%= enums %>
|
|
24
|
+
|
|
22
25
|
type DefineCustomElement<
|
|
23
26
|
ElementType,
|
|
24
27
|
Events extends EventMap = {},
|
|
@@ -44,10 +47,10 @@ type DefineCustomElement<
|
|
|
44
47
|
|
|
45
48
|
declare module 'vue' {
|
|
46
49
|
interface GlobalComponents {
|
|
47
|
-
<%
|
|
48
|
-
'<%=
|
|
49
|
-
<%=
|
|
50
|
-
<%=
|
|
50
|
+
<% componentMetas.forEach(comp => { %>
|
|
51
|
+
'<%= comp.tagName %>': DefineCustomElement<
|
|
52
|
+
<%= comp.className %>Props,
|
|
53
|
+
<%= comp.className %>Events
|
|
51
54
|
>
|
|
52
55
|
<% }) %>
|
|
53
56
|
}
|