@openwebf/webf 0.23.0 → 0.23.7
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/README.md +5 -1
- package/TYPING_GUIDE.md +17 -1
- package/bin/webf.js +1 -0
- package/dist/analyzer.js +135 -17
- package/dist/commands.js +251 -99
- package/dist/constants.js +242 -0
- package/dist/dart.js +91 -25
- package/dist/declaration.js +14 -1
- package/dist/generator.js +80 -12
- package/dist/react.js +281 -23
- package/dist/vue.js +114 -11
- package/package.json +1 -1
- package/src/IDLBlob.ts +2 -2
- package/src/analyzer.ts +142 -28
- package/src/commands.ts +363 -197
- package/src/dart.ts +95 -20
- package/src/declaration.ts +16 -0
- package/src/generator.ts +81 -13
- package/src/react.ts +300 -28
- package/src/vue.ts +131 -14
- package/templates/class.dart.tpl +1 -1
- package/templates/react.package.json.tpl +1 -0
- package/templates/vue.components.d.ts.tpl +9 -4
- package/test/commands.test.ts +82 -2
- package/test/dart-nullable-props.test.ts +58 -0
- package/test/react-consts.test.ts +30 -0
- package/test/react-vue-nullable-props.test.ts +66 -0
- package/test/react.test.ts +46 -4
- package/test/vue.test.ts +34 -2
package/src/dart.ts
CHANGED
|
@@ -2,9 +2,9 @@ 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, PropsDeclaration} from "./declaration";
|
|
5
|
+
import {ClassObject, FunctionArgumentType, FunctionDeclaration, TypeAliasObject, PropsDeclaration, EnumObject} from "./declaration";
|
|
6
6
|
import {IDLBlob} from "./IDLBlob";
|
|
7
|
-
import {getPointerType, isPointerType} from "./utils";
|
|
7
|
+
import {getPointerType, isPointerType, trimNullTypeFromType} from "./utils";
|
|
8
8
|
|
|
9
9
|
function readTemplate(name: string) {
|
|
10
10
|
return fs.readFileSync(path.join(__dirname, '../templates/' + name + '.tpl'), {encoding: 'utf-8'});
|
|
@@ -82,20 +82,59 @@ ${enumValues};
|
|
|
82
82
|
}`
|
|
83
83
|
}
|
|
84
84
|
|
|
85
|
+
function hasNullInUnion(type: ParameterType): boolean {
|
|
86
|
+
if (!Array.isArray(type.value)) return false;
|
|
87
|
+
return type.value.some(t => t.value === FunctionArgumentType.null);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function isBooleanType(type: ParameterType): boolean {
|
|
91
|
+
if (Array.isArray(type.value)) {
|
|
92
|
+
return type.value.some(t => t.value === FunctionArgumentType.boolean);
|
|
93
|
+
}
|
|
94
|
+
return type.value === FunctionArgumentType.boolean;
|
|
95
|
+
}
|
|
96
|
+
|
|
85
97
|
function generateReturnType(type: ParameterType, enumName?: string) {
|
|
86
98
|
// Handle union types first (e.g., 'left' | 'center' | 'right')
|
|
87
99
|
// so we don't incorrectly treat string literal unions as pointer types.
|
|
88
100
|
if (Array.isArray(type.value)) {
|
|
89
|
-
// If we have an enum name, use it
|
|
90
|
-
|
|
101
|
+
// If we have an enum name, always use it (nullable handled separately)
|
|
102
|
+
if (enumName) {
|
|
103
|
+
return enumName;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// If this is a union that includes null and exactly one non-null type,
|
|
107
|
+
// generate the Dart type from the non-null part instead of falling back to String.
|
|
108
|
+
const trimmed = trimNullTypeFromType(type);
|
|
109
|
+
if (!Array.isArray(trimmed.value)) {
|
|
110
|
+
return generateReturnType(trimmed, enumName);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Fallback for complex unions: use String
|
|
114
|
+
return 'String';
|
|
91
115
|
}
|
|
92
116
|
|
|
93
117
|
if (isPointerType(type)) {
|
|
94
118
|
const pointerType = getPointerType(type);
|
|
119
|
+
// Map TS typeof expressions to Dart dynamic
|
|
120
|
+
if (typeof pointerType === 'string' && pointerType.startsWith('typeof ')) {
|
|
121
|
+
return 'dynamic';
|
|
122
|
+
}
|
|
123
|
+
// Map references to known string enums to String in Dart
|
|
124
|
+
if (typeof pointerType === 'string' && EnumObject.globalEnumSet.has(pointerType)) {
|
|
125
|
+
return 'String';
|
|
126
|
+
}
|
|
95
127
|
return pointerType;
|
|
96
128
|
}
|
|
97
129
|
if (type.isArray && typeof type.value === 'object' && !Array.isArray(type.value)) {
|
|
98
|
-
|
|
130
|
+
const elem = getPointerType(type.value);
|
|
131
|
+
if (typeof elem === 'string' && elem.startsWith('typeof ')) {
|
|
132
|
+
return `dynamic[]`;
|
|
133
|
+
}
|
|
134
|
+
if (typeof elem === 'string' && EnumObject.globalEnumSet.has(elem)) {
|
|
135
|
+
return 'String[]';
|
|
136
|
+
}
|
|
137
|
+
return `${elem}[]`;
|
|
99
138
|
}
|
|
100
139
|
|
|
101
140
|
// Handle when type.value is a ParameterType object (nested type)
|
|
@@ -112,7 +151,8 @@ function generateReturnType(type: ParameterType, enumName?: string) {
|
|
|
112
151
|
return 'double';
|
|
113
152
|
}
|
|
114
153
|
case FunctionArgumentType.any: {
|
|
115
|
-
|
|
154
|
+
// Dart doesn't have `any`; use `dynamic`.
|
|
155
|
+
return 'dynamic';
|
|
116
156
|
}
|
|
117
157
|
case FunctionArgumentType.boolean: {
|
|
118
158
|
return 'bool';
|
|
@@ -143,13 +183,23 @@ function generateEventHandlerType(type: ParameterType) {
|
|
|
143
183
|
|
|
144
184
|
function generateAttributeSetter(propName: string, type: ParameterType, enumName?: string): string {
|
|
145
185
|
// Attributes from HTML are always strings, so we need to convert them
|
|
146
|
-
|
|
186
|
+
|
|
187
|
+
const unionHasNull = hasNullInUnion(type);
|
|
188
|
+
|
|
147
189
|
// Handle enum types
|
|
148
190
|
if (enumName && Array.isArray(type.value)) {
|
|
191
|
+
if (unionHasNull) {
|
|
192
|
+
return `${propName} = value == 'null' ? null : ${enumName}.parse(value)`;
|
|
193
|
+
}
|
|
149
194
|
return `${propName} = ${enumName}.parse(value)`;
|
|
150
195
|
}
|
|
151
|
-
|
|
152
|
-
|
|
196
|
+
|
|
197
|
+
const effectiveType: ParameterType = Array.isArray(type.value) && unionHasNull
|
|
198
|
+
? trimNullTypeFromType(type)
|
|
199
|
+
: type;
|
|
200
|
+
|
|
201
|
+
const baseSetter = (() => {
|
|
202
|
+
switch (effectiveType.value) {
|
|
153
203
|
case FunctionArgumentType.boolean:
|
|
154
204
|
return `${propName} = value == 'true' || value == ''`;
|
|
155
205
|
case FunctionArgumentType.int:
|
|
@@ -159,30 +209,43 @@ function generateAttributeSetter(propName: string, type: ParameterType, enumName
|
|
|
159
209
|
default:
|
|
160
210
|
// String and other types can be assigned directly
|
|
161
211
|
return `${propName} = value`;
|
|
212
|
+
}
|
|
213
|
+
})();
|
|
214
|
+
|
|
215
|
+
if (unionHasNull) {
|
|
216
|
+
const assignmentPrefix = `${propName} = `;
|
|
217
|
+
const rhs = baseSetter.startsWith(assignmentPrefix)
|
|
218
|
+
? baseSetter.slice(assignmentPrefix.length)
|
|
219
|
+
: 'value';
|
|
220
|
+
return `${propName} = value == 'null' ? null : (${rhs})`;
|
|
162
221
|
}
|
|
222
|
+
|
|
223
|
+
return baseSetter;
|
|
163
224
|
}
|
|
164
225
|
|
|
165
|
-
function generateAttributeGetter(propName: string, type: ParameterType,
|
|
226
|
+
function generateAttributeGetter(propName: string, type: ParameterType, isNullable: boolean, enumName?: string): string {
|
|
166
227
|
// Handle enum types
|
|
167
228
|
if (enumName && Array.isArray(type.value)) {
|
|
168
|
-
return
|
|
229
|
+
return isNullable ? `${propName}?.value` : `${propName}.value`;
|
|
169
230
|
}
|
|
170
231
|
|
|
171
232
|
// Handle nullable properties - they should return null if the value is null
|
|
172
|
-
if (
|
|
233
|
+
if (isNullable) {
|
|
173
234
|
// For nullable properties, we need to handle null values properly
|
|
174
235
|
return `${propName}?.toString()`;
|
|
175
236
|
}
|
|
176
|
-
// For non-nullable properties
|
|
237
|
+
// For non-nullable properties, always convert to string
|
|
177
238
|
return `${propName}.toString()`;
|
|
178
239
|
}
|
|
179
240
|
|
|
180
241
|
function generateAttributeDeleter(propName: string, type: ParameterType, optional: boolean): string {
|
|
181
242
|
// When deleting an attribute, we should reset it to its default value
|
|
243
|
+
if (isBooleanType(type)) {
|
|
244
|
+
// Booleans (including unions with null) default to false
|
|
245
|
+
return `${propName} = false`;
|
|
246
|
+
}
|
|
247
|
+
|
|
182
248
|
switch (type.value) {
|
|
183
|
-
case FunctionArgumentType.boolean:
|
|
184
|
-
// Booleans default to false
|
|
185
|
-
return `${propName} = false`;
|
|
186
249
|
case FunctionArgumentType.int:
|
|
187
250
|
// Integers default to 0
|
|
188
251
|
return `${propName} = 0`;
|
|
@@ -216,10 +279,21 @@ function generateMethodDeclaration(method: FunctionDeclaration) {
|
|
|
216
279
|
}
|
|
217
280
|
|
|
218
281
|
function shouldMakeNullable(prop: any): boolean {
|
|
219
|
-
|
|
220
|
-
|
|
282
|
+
const type: ParameterType = prop.type;
|
|
283
|
+
|
|
284
|
+
// Boolean properties are only nullable in Dart when explicitly unioned with `null`.
|
|
285
|
+
if (isBooleanType(type)) {
|
|
286
|
+
return hasNullInUnion(type);
|
|
287
|
+
}
|
|
288
|
+
// Dynamic (any) should not use nullable syntax; dynamic already allows null
|
|
289
|
+
if (type.value === FunctionArgumentType.any) {
|
|
221
290
|
return false;
|
|
222
291
|
}
|
|
292
|
+
// Properties with an explicit `null` in their type should be nullable,
|
|
293
|
+
// even if they are not marked optional in TypeScript.
|
|
294
|
+
if (hasNullInUnion(type)) {
|
|
295
|
+
return true;
|
|
296
|
+
}
|
|
223
297
|
// Other optional properties remain nullable
|
|
224
298
|
return prop.optional;
|
|
225
299
|
}
|
|
@@ -317,8 +391,9 @@ interface ${object.name} {
|
|
|
317
391
|
generateAttributeSetter: (propName: string, type: ParameterType) => {
|
|
318
392
|
return generateAttributeSetter(propName, type, enumMap.get(propName));
|
|
319
393
|
},
|
|
320
|
-
generateAttributeGetter: (propName: string, type: ParameterType, optional: boolean) => {
|
|
321
|
-
|
|
394
|
+
generateAttributeGetter: (propName: string, type: ParameterType, optional: boolean, prop?: PropsDeclaration) => {
|
|
395
|
+
const isNullable = prop ? shouldMakeNullable(prop) : optional;
|
|
396
|
+
return generateAttributeGetter(propName, type, isNullable, enumMap.get(propName));
|
|
322
397
|
},
|
|
323
398
|
generateAttributeDeleter,
|
|
324
399
|
shouldMakeNullable,
|
package/src/declaration.ts
CHANGED
|
@@ -89,3 +89,19 @@ 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
|
+
static globalEnumSet: Set<string> = new Set<string>();
|
|
107
|
+
}
|
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.`);
|
|
@@ -316,6 +315,8 @@ export async function reactGen({ source, target, exclude, packageName }: Generat
|
|
|
316
315
|
if (writeFileIfChanged(fullPath, result)) {
|
|
317
316
|
filesChanged++;
|
|
318
317
|
debug(`Generated: ${path.basename(fullPath)}`);
|
|
318
|
+
// Emit a short preview for debugging when WEBF_DEBUG is on
|
|
319
|
+
debug(`Preview (${path.basename(fullPath)}):\n` + result.split('\n').slice(0, 12).join('\n'));
|
|
319
320
|
}
|
|
320
321
|
} catch (err) {
|
|
321
322
|
error(`Error generating React component for ${blob.filename}`, err);
|
|
@@ -324,6 +325,8 @@ export async function reactGen({ source, target, exclude, packageName }: Generat
|
|
|
324
325
|
|
|
325
326
|
// Generate/merge index file
|
|
326
327
|
const indexFilePath = path.join(normalizedTarget, 'src', 'index.ts');
|
|
328
|
+
// Always build the full index content string for downstream tooling/logging
|
|
329
|
+
const newExports = generateReactIndex(blobs);
|
|
327
330
|
|
|
328
331
|
// Build desired export map: moduleSpecifier -> Set of names
|
|
329
332
|
const desiredExports = new Map<string, Set<string>>();
|
|
@@ -356,7 +359,6 @@ export async function reactGen({ source, target, exclude, packageName }: Generat
|
|
|
356
359
|
|
|
357
360
|
if (!fs.existsSync(indexFilePath)) {
|
|
358
361
|
// No index.ts -> generate fresh file from template
|
|
359
|
-
const newExports = generateReactIndex(blobs);
|
|
360
362
|
if (writeFileIfChanged(indexFilePath, newExports)) {
|
|
361
363
|
filesChanged++;
|
|
362
364
|
debug(`Generated: index.ts`);
|
|
@@ -410,6 +412,72 @@ export async function reactGen({ source, target, exclude, packageName }: Generat
|
|
|
410
412
|
success(`React code generation completed. ${filesChanged} files changed.`);
|
|
411
413
|
info(`Output directory: ${normalizedTarget}`);
|
|
412
414
|
info('You can now import these components in your React project.');
|
|
415
|
+
|
|
416
|
+
// Aggregate standalone type declarations (consts/enums/type aliases) into a single types.ts
|
|
417
|
+
try {
|
|
418
|
+
const consts = blobs.flatMap(b => b.objects.filter(o => o instanceof ConstObject) as ConstObject[]);
|
|
419
|
+
const enums = blobs.flatMap(b => b.objects.filter(o => o instanceof EnumObject) as EnumObject[]);
|
|
420
|
+
const typeAliases = blobs.flatMap(b => b.objects.filter(o => o instanceof TypeAliasObject) as TypeAliasObject[]);
|
|
421
|
+
|
|
422
|
+
// Deduplicate by name
|
|
423
|
+
const constMap = new Map<string, ConstObject>();
|
|
424
|
+
consts.forEach(c => { if (!constMap.has(c.name)) constMap.set(c.name, c); });
|
|
425
|
+
const typeAliasMap = new Map<string, TypeAliasObject>();
|
|
426
|
+
typeAliases.forEach(t => { if (!typeAliasMap.has(t.name)) typeAliasMap.set(t.name, t); });
|
|
427
|
+
|
|
428
|
+
const hasAny = constMap.size > 0 || enums.length > 0 || typeAliasMap.size > 0;
|
|
429
|
+
if (hasAny) {
|
|
430
|
+
const constDecl = Array.from(constMap.values())
|
|
431
|
+
.map(c => `export declare const ${c.name}: ${c.type};`)
|
|
432
|
+
.join('\n');
|
|
433
|
+
const enumDecl = enums
|
|
434
|
+
.map(e => `export enum ${e.name} { ${e.members.map(m => m.initializer ? `${m.name} = ${m.initializer}` : `${m.name}`).join(', ')} }`)
|
|
435
|
+
.join('\n');
|
|
436
|
+
const typeAliasDecl = Array.from(typeAliasMap.values())
|
|
437
|
+
.map(t => `export type ${t.name} = ${t.type};`)
|
|
438
|
+
.join('\n');
|
|
439
|
+
|
|
440
|
+
const typesContent = [
|
|
441
|
+
'/* Generated by WebF CLI - aggregated type declarations */',
|
|
442
|
+
typeAliasDecl,
|
|
443
|
+
constDecl,
|
|
444
|
+
enumDecl,
|
|
445
|
+
''
|
|
446
|
+
].filter(Boolean).join('\n');
|
|
447
|
+
|
|
448
|
+
const typesPath = path.join(normalizedTarget, 'src', 'types.ts');
|
|
449
|
+
if (writeFileIfChanged(typesPath, typesContent)) {
|
|
450
|
+
filesChanged++;
|
|
451
|
+
debug(`Generated: src/types.ts`);
|
|
452
|
+
try {
|
|
453
|
+
const constNames = Array.from(constMap.keys());
|
|
454
|
+
const aliasNames = Array.from(typeAliasMap.keys());
|
|
455
|
+
const enumNames = enums.map(e => e.name);
|
|
456
|
+
debug(`[react] Aggregated types - consts: ${constNames.join(', ') || '(none)'}; typeAliases: ${aliasNames.join(', ') || '(none)'}; enums: ${enumNames.join(', ') || '(none)'}\n`);
|
|
457
|
+
debug(`[react] src/types.ts preview:\n` + typesContent.split('\n').slice(0, 20).join('\n'));
|
|
458
|
+
} catch {}
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
// Ensure index.ts re-exports these types so consumers get them on import.
|
|
462
|
+
const indexFilePath = path.join(normalizedTarget, 'src', 'index.ts');
|
|
463
|
+
try {
|
|
464
|
+
let current = '';
|
|
465
|
+
if (fs.existsSync(indexFilePath)) {
|
|
466
|
+
current = fs.readFileSync(indexFilePath, 'utf-8');
|
|
467
|
+
}
|
|
468
|
+
const exportLine = `export * from './types';`;
|
|
469
|
+
if (!current.includes(exportLine)) {
|
|
470
|
+
const updated = current.trim().length ? `${current.trim()}\n${exportLine}\n` : `${exportLine}\n`;
|
|
471
|
+
if (writeFileIfChanged(indexFilePath, updated)) {
|
|
472
|
+
filesChanged++;
|
|
473
|
+
debug(`Updated: src/index.ts to export aggregated types`);
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
} catch {}
|
|
477
|
+
}
|
|
478
|
+
} catch (e) {
|
|
479
|
+
warn('Failed to generate aggregated React types');
|
|
480
|
+
}
|
|
413
481
|
}
|
|
414
482
|
|
|
415
483
|
export async function vueGen({ source, target, exclude }: GenerateOptions) {
|