@openwebf/webf 0.23.2 → 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/bin/webf.js +1 -0
- package/dist/analyzer.js +65 -1
- package/dist/commands.js +198 -98
- package/dist/dart.js +91 -25
- package/dist/declaration.js +1 -0
- package/dist/generator.js +26 -17
- package/dist/react.js +272 -25
- package/dist/vue.js +74 -11
- package/package.json +1 -1
- package/src/analyzer.ts +58 -2
- package/src/commands.ts +300 -196
- package/src/dart.ts +95 -20
- package/src/declaration.ts +1 -0
- package/src/generator.ts +24 -16
- package/src/react.ts +288 -29
- package/src/vue.ts +85 -13
- package/templates/class.dart.tpl +1 -1
- package/templates/vue.components.d.ts.tpl +2 -0
- package/test/commands.test.ts +82 -2
- package/test/dart-nullable-props.test.ts +58 -0
- package/test/react-consts.test.ts +1 -1
- package/test/react-vue-nullable-props.test.ts +66 -0
- package/test/react.test.ts +46 -4
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
package/src/generator.ts
CHANGED
|
@@ -315,6 +315,8 @@ export async function reactGen({ source, target, exclude, packageName }: Generat
|
|
|
315
315
|
if (writeFileIfChanged(fullPath, result)) {
|
|
316
316
|
filesChanged++;
|
|
317
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'));
|
|
318
320
|
}
|
|
319
321
|
} catch (err) {
|
|
320
322
|
error(`Error generating React component for ${blob.filename}`, err);
|
|
@@ -411,7 +413,7 @@ export async function reactGen({ source, target, exclude, packageName }: Generat
|
|
|
411
413
|
info(`Output directory: ${normalizedTarget}`);
|
|
412
414
|
info('You can now import these components in your React project.');
|
|
413
415
|
|
|
414
|
-
// Aggregate standalone type declarations (consts/enums/type aliases) into a single types.
|
|
416
|
+
// Aggregate standalone type declarations (consts/enums/type aliases) into a single types.ts
|
|
415
417
|
try {
|
|
416
418
|
const consts = blobs.flatMap(b => b.objects.filter(o => o instanceof ConstObject) as ConstObject[]);
|
|
417
419
|
const enums = blobs.flatMap(b => b.objects.filter(o => o instanceof EnumObject) as EnumObject[]);
|
|
@@ -429,7 +431,7 @@ export async function reactGen({ source, target, exclude, packageName }: Generat
|
|
|
429
431
|
.map(c => `export declare const ${c.name}: ${c.type};`)
|
|
430
432
|
.join('\n');
|
|
431
433
|
const enumDecl = enums
|
|
432
|
-
.map(e => `export
|
|
434
|
+
.map(e => `export enum ${e.name} { ${e.members.map(m => m.initializer ? `${m.name} = ${m.initializer}` : `${m.name}`).join(', ')} }`)
|
|
433
435
|
.join('\n');
|
|
434
436
|
const typeAliasDecl = Array.from(typeAliasMap.values())
|
|
435
437
|
.map(t => `export type ${t.name} = ${t.type};`)
|
|
@@ -443,32 +445,38 @@ export async function reactGen({ source, target, exclude, packageName }: Generat
|
|
|
443
445
|
''
|
|
444
446
|
].filter(Boolean).join('\n');
|
|
445
447
|
|
|
446
|
-
const typesPath = path.join(normalizedTarget, 'src', 'types.
|
|
448
|
+
const typesPath = path.join(normalizedTarget, 'src', 'types.ts');
|
|
447
449
|
if (writeFileIfChanged(typesPath, typesContent)) {
|
|
448
450
|
filesChanged++;
|
|
449
|
-
debug(`Generated: src/types.
|
|
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 {}
|
|
450
459
|
}
|
|
451
460
|
|
|
452
|
-
//
|
|
453
|
-
// This avoids bundler resolution errors from importing a .d.ts file.
|
|
461
|
+
// Ensure index.ts re-exports these types so consumers get them on import.
|
|
454
462
|
const indexFilePath = path.join(normalizedTarget, 'src', 'index.ts');
|
|
455
463
|
try {
|
|
464
|
+
let current = '';
|
|
456
465
|
if (fs.existsSync(indexFilePath)) {
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
}
|
|
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`);
|
|
466
474
|
}
|
|
467
475
|
}
|
|
468
476
|
} catch {}
|
|
469
477
|
}
|
|
470
478
|
} catch (e) {
|
|
471
|
-
warn('Failed to generate aggregated React types
|
|
479
|
+
warn('Failed to generate aggregated React types');
|
|
472
480
|
}
|
|
473
481
|
}
|
|
474
482
|
|