@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/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; otherwise fall back to String
90
- return enumName || 'String';
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
- return `${getPointerType(type.value)}[]`;
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
- return 'any';
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
- switch (type.value) {
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, optional: boolean, enumName?: string): string {
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 optional ? `${propName}?.value` : `${propName}.value`;
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 (optional && type.value !== FunctionArgumentType.boolean) {
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 (including booleans), always convert to string
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
- // Boolean properties should never be nullable in Dart, even if optional in TypeScript
220
- if (prop.type.value === FunctionArgumentType.boolean) {
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
- return generateAttributeGetter(propName, type, optional, enumMap.get(propName));
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,
@@ -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 (writeFileIfChanged(fullPath, result)) {
213
- filesChanged++;
214
- debug(`Generated: ${path.basename(fullPath)}`);
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
- // Generate index.d.ts file with references to all .d.ts files
229
- const indexDtsContent = generateTypeScriptIndex(blobs, normalizedTarget);
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) {