@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/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 (writeFileIfChanged(fullPath, result)) {
193
- filesChanged++;
194
- (0, logger_1.debug)(`Generated: ${path_1.default.basename(fullPath)}`);
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
- // Generate index.d.ts file with references to all .d.ts files
208
- const indexDtsContent = generateTypeScriptIndex(blobs, normalizedTarget);
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
- let shouldWriteIndex = true;
297
- if (fs_1.default.existsSync(indexFilePath)) {
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 isAutoGenerated = existing.includes('Generated by TSDL');
301
- if (!isAutoGenerated) {
302
- shouldWriteIndex = false;
303
- (0, logger_1.warn)(`Found existing user-managed index.ts at ${indexFilePath}; skipping overwrite.`);
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
- // If we cannot read the file for some reason, be conservative and skip overwriting
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.22.13",
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.3.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, { content: string; sourceFile: ts.SourceFile }>();
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
- if (cacheEntry && cacheEntry.content === blob.raw) {
64
- // Cache hit with same content
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, { content: blob.raw, sourceFile });
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 methodName = declaration.name.text;
920
- const type = declaration.type;
921
-
922
- if (!type || !ts.isFunctionTypeNode(type)) {
966
+
967
+ const varName = declaration.name.text;
968
+ const typeNode = declaration.type;
969
+
970
+ if (!typeNode) {
923
971
  return null;
924
972
  }
925
-
926
- const functionObject = new FunctionObject();
927
- functionObject.declare = new FunctionDeclaration();
928
- functionObject.declare.name = methodName;
929
- functionObject.declare.args = type.parameters.map(param =>
930
- paramsNodeToArguments(param, unionTypeCollector)
931
- );
932
- functionObject.declare.returnType = getParameterType(type.type, unionTypeCollector);
933
-
934
- return functionObject;
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
- const indexContent = _.template(reactIndexTpl)({
304
- components: [],
305
- });
306
- writeFileIfChanged(indexFilePath, indexContent);
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', '--omit=peer'], {
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: reactPackageName,
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