@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/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';
@@ -14,6 +15,7 @@ interface GenerateOptions {
14
15
  publishToNpm?: boolean;
15
16
  npmRegistry?: string;
16
17
  exclude?: string[];
18
+ dartOnly?: boolean;
17
19
  }
18
20
 
19
21
  interface FlutterPackageMetadata {
@@ -38,12 +40,12 @@ interface FlutterPackageMetadata {
38
40
  function sanitizePackageName(name: string): string {
39
41
  // Remove any leading/trailing whitespace
40
42
  let sanitized = name.trim();
41
-
43
+
42
44
  // Check if it's a scoped package
43
45
  const isScoped = sanitized.startsWith('@');
44
46
  let scope = '';
45
47
  let packageName = sanitized;
46
-
48
+
47
49
  if (isScoped) {
48
50
  const parts = sanitized.split('/');
49
51
  if (parts.length >= 2) {
@@ -54,7 +56,7 @@ function sanitizePackageName(name: string): string {
54
56
  packageName = sanitized.substring(1);
55
57
  }
56
58
  }
57
-
59
+
58
60
  // Sanitize scope if present
59
61
  if (scope) {
60
62
  scope = scope.toLowerCase();
@@ -64,7 +66,7 @@ function sanitizePackageName(name: string): string {
64
66
  scope = '@pkg'; // Default scope if only @ remains
65
67
  }
66
68
  }
67
-
69
+
68
70
  // Sanitize package name part
69
71
  packageName = packageName.toLowerCase();
70
72
  packageName = packageName.replace(/\s+/g, '-');
@@ -73,20 +75,20 @@ function sanitizePackageName(name: string): string {
73
75
  packageName = packageName.replace(/[._]+$/, '');
74
76
  packageName = packageName.replace(/[-_.]{2,}/g, '-');
75
77
  packageName = packageName.replace(/^-+/, '').replace(/-+$/, '');
76
-
78
+
77
79
  // Ensure package name is not empty
78
80
  if (!packageName) {
79
81
  packageName = 'package';
80
82
  }
81
-
83
+
82
84
  // Ensure it starts with a letter or number
83
85
  if (!/^[a-z0-9]/.test(packageName)) {
84
86
  packageName = 'pkg-' + packageName;
85
87
  }
86
-
88
+
87
89
  // Combine scope and package name
88
90
  let result = scope ? `${scope}/${packageName}` : packageName;
89
-
91
+
90
92
  // Truncate to 214 characters (npm limit)
91
93
  if (result.length > 214) {
92
94
  if (scope) {
@@ -100,7 +102,7 @@ function sanitizePackageName(name: string): string {
100
102
  result = result.replace(/[._-]+$/, '');
101
103
  }
102
104
  }
103
-
105
+
104
106
  return result;
105
107
  }
106
108
 
@@ -111,36 +113,36 @@ function isValidNpmPackageName(name: string): boolean {
111
113
  // Check basic rules
112
114
  if (!name || name.length === 0 || name.length > 214) return false;
113
115
  if (name.trim() !== name) return false;
114
-
116
+
115
117
  // Check if it's a scoped package
116
118
  if (name.startsWith('@')) {
117
119
  const parts = name.split('/');
118
120
  if (parts.length !== 2) return false; // Scoped packages must have exactly one /
119
-
121
+
120
122
  const scope = parts[0];
121
123
  const packageName = parts[1];
122
-
124
+
123
125
  // Validate scope
124
126
  if (!/^@[a-z0-9][a-z0-9-]*$/.test(scope)) return false;
125
-
127
+
126
128
  // Validate package name part
127
129
  return isValidNpmPackageName(packageName);
128
130
  }
129
-
131
+
130
132
  // For non-scoped packages
131
133
  if (name !== name.toLowerCase()) return false;
132
134
  if (name.startsWith('.') || name.startsWith('_')) return false;
133
-
135
+
134
136
  // Check for valid characters (letters, numbers, hyphens, underscores, dots)
135
137
  if (!/^[a-z0-9][a-z0-9\-_.]*$/.test(name)) return false;
136
-
138
+
137
139
  // Check for URL-safe characters
138
140
  try {
139
141
  if (encodeURIComponent(name) !== name) return false;
140
142
  } catch {
141
143
  return false;
142
144
  }
143
-
145
+
144
146
  return true;
145
147
  }
146
148
 
@@ -199,15 +201,15 @@ function readFlutterPackageMetadata(packagePath: string): FlutterPackageMetadata
199
201
  console.warn(`Warning: pubspec.yaml not found at ${pubspecPath}. Using default metadata.`);
200
202
  return null;
201
203
  }
202
-
204
+
203
205
  const pubspecContent = fs.readFileSync(pubspecPath, 'utf-8');
204
206
  const pubspec = yaml.parse(pubspecContent);
205
-
207
+
206
208
  // Validate required fields
207
209
  if (!pubspec.name) {
208
210
  console.warn(`Warning: Flutter package name not found in ${pubspecPath}. Using default name.`);
209
211
  }
210
-
212
+
211
213
  return {
212
214
  name: pubspec.name || '',
213
215
  version: pubspec.version || '0.0.1',
@@ -220,42 +222,156 @@ function readFlutterPackageMetadata(packagePath: string): FlutterPackageMetadata
220
222
  }
221
223
  }
222
224
 
225
+ // Copy markdown docs that match .d.ts basenames from source to the built dist folder,
226
+ // and generate an aggregated README.md in the dist directory.
227
+ async function copyMarkdownDocsToDist(params: {
228
+ sourceRoot: string;
229
+ distRoot: string;
230
+ exclude?: string[];
231
+ }): Promise<{ copied: number; skipped: number }> {
232
+ const { sourceRoot, distRoot, exclude } = params;
233
+
234
+ // Ensure dist exists
235
+ if (!fs.existsSync(distRoot)) {
236
+ return { copied: 0, skipped: 0 };
237
+ }
238
+
239
+ // Default ignore patterns similar to generator
240
+ const defaultIgnore = ['**/node_modules/**', '**/dist/**', '**/build/**', '**/example/**'];
241
+ const ignore = exclude && exclude.length ? [...defaultIgnore, ...exclude] : defaultIgnore;
242
+
243
+ // Find all .d.ts files and check for sibling .md files
244
+ const dtsFiles = glob.globSync('**/*.d.ts', { cwd: sourceRoot, ignore });
245
+ let copied = 0;
246
+ let skipped = 0;
247
+ const readmeSections: { title: string; relPath: string; content: string }[] = [];
248
+
249
+ for (const relDts of dtsFiles) {
250
+ if (path.basename(relDts) === 'global.d.ts') {
251
+ continue;
252
+ }
253
+
254
+ const relMd = relDts.replace(/\.d\.ts$/i, '.md');
255
+ const absMd = path.join(sourceRoot, relMd);
256
+ if (!fs.existsSync(absMd)) {
257
+ skipped++;
258
+ continue;
259
+ }
260
+
261
+ let content = '';
262
+ try {
263
+ content = fs.readFileSync(absMd, 'utf-8');
264
+ } catch {
265
+ // If we cannot read the file, still attempt to copy it and skip README aggregation for this entry.
266
+ }
267
+
268
+ // Copy into dist preserving relative path
269
+ const destPath = path.join(distRoot, relMd);
270
+ const destDir = path.dirname(destPath);
271
+ if (!fs.existsSync(destDir)) {
272
+ fs.mkdirSync(destDir, { recursive: true });
273
+ }
274
+ fs.copyFileSync(absMd, destPath);
275
+ copied++;
276
+
277
+ if (content) {
278
+ const base = path.basename(relMd, '.md');
279
+ const title = base
280
+ .split(/[-_]+/)
281
+ .filter(Boolean)
282
+ .map(part => part.charAt(0).toUpperCase() + part.slice(1))
283
+ .join(' ');
284
+ readmeSections.push({
285
+ title: title || base,
286
+ relPath: relMd,
287
+ content
288
+ });
289
+ }
290
+ }
291
+
292
+ // Generate an aggregated README.md inside distRoot so consumers can see component docs easily.
293
+ if (readmeSections.length > 0) {
294
+ const readmePath = path.join(distRoot, 'README.md');
295
+ let existing = '';
296
+ if (fs.existsSync(readmePath)) {
297
+ try {
298
+ existing = fs.readFileSync(readmePath, 'utf-8');
299
+ } catch {
300
+ existing = '';
301
+ }
302
+ }
303
+
304
+ const headerLines: string[] = [
305
+ '# WebF Component Documentation',
306
+ '',
307
+ '> This README is generated from markdown docs co-located with TypeScript definitions in the Flutter package.',
308
+ ''
309
+ ];
310
+
311
+ const sectionBlocks = readmeSections.map(section => {
312
+ const lines: string[] = [];
313
+ lines.push(`## ${section.title}`);
314
+ lines.push('');
315
+ lines.push(`_Source: \`./${section.relPath}\`_`);
316
+ lines.push('');
317
+ lines.push(section.content.trim());
318
+ lines.push('');
319
+ return lines.join('\n');
320
+ }).join('\n');
321
+
322
+ let finalContent: string;
323
+ if (existing && existing.trim().length > 0) {
324
+ finalContent = `${existing.trim()}\n\n---\n\n${headerLines.join('\n')}${sectionBlocks}\n`;
325
+ } else {
326
+ finalContent = `${headerLines.join('\n')}${sectionBlocks}\n`;
327
+ }
328
+
329
+ try {
330
+ fs.writeFileSync(readmePath, finalContent, 'utf-8');
331
+ } catch {
332
+ // If README generation fails, do not affect overall codegen.
333
+ }
334
+ }
335
+
336
+ return { copied, skipped };
337
+ }
338
+
223
339
  function validateTypeScriptEnvironment(projectPath: string): { isValid: boolean; errors: string[] } {
224
340
  const errors: string[] = [];
225
-
341
+
226
342
  // Check for TypeScript configuration
227
343
  const tsConfigPath = path.join(projectPath, 'tsconfig.json');
228
344
  if (!fs.existsSync(tsConfigPath)) {
229
345
  errors.push('Missing tsconfig.json - TypeScript configuration is required for type definitions');
230
346
  }
231
-
347
+
232
348
  // Check for .d.ts files - this is critical
233
349
  const libPath = path.join(projectPath, 'lib');
234
350
  let hasDtsFiles = false;
235
-
351
+
236
352
  if (fs.existsSync(libPath)) {
237
353
  // Check in lib directory
238
- hasDtsFiles = fs.readdirSync(libPath).some(file =>
239
- file.endsWith('.d.ts') ||
240
- (fs.statSync(path.join(libPath, file)).isDirectory() &&
354
+ hasDtsFiles = fs.readdirSync(libPath).some(file =>
355
+ file.endsWith('.d.ts') ||
356
+ (fs.statSync(path.join(libPath, file)).isDirectory() &&
241
357
  fs.readdirSync(path.join(libPath, file)).some(f => f.endsWith('.d.ts')))
242
358
  );
243
359
  }
244
-
360
+
245
361
  // Also check in root directory
246
362
  if (!hasDtsFiles) {
247
- hasDtsFiles = fs.readdirSync(projectPath).some(file =>
248
- file.endsWith('.d.ts') ||
249
- (fs.statSync(path.join(projectPath, file)).isDirectory() &&
250
- file !== 'node_modules' &&
363
+ hasDtsFiles = fs.readdirSync(projectPath).some(file =>
364
+ file.endsWith('.d.ts') ||
365
+ (fs.statSync(path.join(projectPath, file)).isDirectory() &&
366
+ file !== 'node_modules' &&
251
367
  fs.existsSync(path.join(projectPath, file, 'index.d.ts')))
252
368
  );
253
369
  }
254
-
370
+
255
371
  if (!hasDtsFiles) {
256
372
  errors.push('No TypeScript definition files (.d.ts) found in the project - Please create .d.ts files for your components');
257
373
  }
258
-
374
+
259
375
  return {
260
376
  isValid: errors.length === 0,
261
377
  errors
@@ -265,8 +381,8 @@ function validateTypeScriptEnvironment(projectPath: string): { isValid: boolean;
265
381
  function createCommand(target: string, options: { framework: string; packageName: string; metadata?: FlutterPackageMetadata }): void {
266
382
  const { framework, metadata } = options;
267
383
  // Ensure package name is always valid
268
- const packageName = isValidNpmPackageName(options.packageName)
269
- ? options.packageName
384
+ const packageName = isValidNpmPackageName(options.packageName)
385
+ ? options.packageName
270
386
  : sanitizePackageName(options.packageName);
271
387
 
272
388
  if (!fs.existsSync(target)) {
@@ -310,7 +426,8 @@ function createCommand(target: string, options: { framework: string; packageName
310
426
  // Leave merge to the codegen step which appends exports safely
311
427
  }
312
428
 
313
- spawnSync(NPM, ['install', '--omit=peer'], {
429
+ // !no '--omit=peer' here.
430
+ spawnSync(NPM, ['install'], {
314
431
  cwd: target,
315
432
  stdio: 'inherit'
316
433
  });
@@ -350,24 +467,30 @@ async function generateCommand(distPath: string, options: GenerateOptions): Prom
350
467
  // If distPath is not provided or is '.', create a temporary directory
351
468
  let resolvedDistPath: string;
352
469
  let isTempDir = false;
353
-
470
+ const isDartOnly = options.dartOnly;
471
+
354
472
  if (!distPath || distPath === '.') {
355
- // Create a temporary directory for the generated package
356
- const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'webf-typings-'));
357
- resolvedDistPath = tempDir;
358
- isTempDir = true;
359
- console.log(`\nUsing temporary directory: ${tempDir}`);
473
+ if (isDartOnly) {
474
+ // In Dart-only mode we don't need a temporary Node project directory
475
+ resolvedDistPath = path.resolve(distPath || '.');
476
+ } else {
477
+ // Create a temporary directory for the generated package
478
+ const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'webf-typings-'));
479
+ resolvedDistPath = tempDir;
480
+ isTempDir = true;
481
+ console.log(`\nUsing temporary directory: ${tempDir}`);
482
+ }
360
483
  } else {
361
484
  resolvedDistPath = path.resolve(distPath);
362
485
  }
363
-
486
+
364
487
  // First, check if we're in a Flutter package directory when flutter-package-src is not provided
365
488
  if (!options.flutterPackageSrc) {
366
489
  // Check if current directory or parent directories contain pubspec.yaml
367
490
  let currentDir = process.cwd();
368
491
  let foundPubspec = false;
369
492
  let pubspecDir = '';
370
-
493
+
371
494
  // Search up to 3 levels up for pubspec.yaml
372
495
  for (let i = 0; i < 3; i++) {
373
496
  const pubspecPath = path.join(currentDir, 'pubspec.yaml');
@@ -380,141 +503,151 @@ async function generateCommand(distPath: string, options: GenerateOptions): Prom
380
503
  if (parentDir === currentDir) break; // Reached root
381
504
  currentDir = parentDir;
382
505
  }
383
-
506
+
384
507
  if (foundPubspec) {
385
508
  // Use the directory containing pubspec.yaml as the flutter package source
386
509
  options.flutterPackageSrc = pubspecDir;
387
510
  console.log(`\nDetected Flutter package at: ${pubspecDir}`);
388
511
  }
389
512
  }
390
-
391
- // Check if the directory exists and has required files
392
- const packageJsonPath = path.join(resolvedDistPath, 'package.json');
393
- const globalDtsPath = path.join(resolvedDistPath, 'global.d.ts');
394
- const tsConfigPath = path.join(resolvedDistPath, 'tsconfig.json');
395
-
396
- const hasPackageJson = fs.existsSync(packageJsonPath);
397
- const hasGlobalDts = fs.existsSync(globalDtsPath);
398
- const hasTsConfig = fs.existsSync(tsConfigPath);
399
-
400
- // Determine if we need to create a new project
401
- const needsProjectCreation = !hasPackageJson || !hasGlobalDts || !hasTsConfig;
402
-
403
- // Track if this is an existing project (has all required files)
404
- const isExistingProject = hasPackageJson && hasGlobalDts && hasTsConfig;
405
-
513
+
406
514
  let framework = options.framework;
407
515
  let packageName = options.packageName;
408
-
409
- // Validate and sanitize package name if provided
410
- if (packageName && !isValidNpmPackageName(packageName)) {
411
- console.warn(`Warning: Package name "${packageName}" is not valid for npm.`);
412
- const sanitized = sanitizePackageName(packageName);
413
- console.log(`Using sanitized name: "${sanitized}"`);
414
- packageName = sanitized;
415
- }
416
-
417
- if (needsProjectCreation) {
418
- // If project needs creation but options are missing, prompt for them
419
- if (!framework) {
420
- const frameworkAnswer = await inquirer.prompt([{
421
- type: 'list',
422
- name: 'framework',
423
- message: 'Which framework would you like to use?',
424
- choices: ['react', 'vue']
425
- }]);
426
- framework = frameworkAnswer.framework;
427
- }
428
-
429
- // Try to read Flutter package metadata if flutterPackageSrc is provided
430
- let metadata: FlutterPackageMetadata | null = null;
431
- if (options.flutterPackageSrc) {
432
- metadata = readFlutterPackageMetadata(options.flutterPackageSrc);
433
- }
434
-
435
- if (!packageName) {
436
- // Use Flutter package name as default if available, sanitized for npm
437
- const rawDefaultName = metadata?.name || path.basename(resolvedDistPath);
438
- const defaultPackageName = sanitizePackageName(rawDefaultName);
439
-
440
- const packageNameAnswer = await inquirer.prompt([{
441
- type: 'input',
442
- name: 'packageName',
443
- message: 'What is your package name?',
444
- default: defaultPackageName,
445
- validate: (input: string) => {
446
- if (!input || input.trim() === '') {
447
- return 'Package name is required';
448
- }
449
-
450
- // Check if it's valid as-is
451
- if (isValidNpmPackageName(input)) {
452
- return true;
516
+ let isExistingProject = false;
517
+
518
+ if (!isDartOnly) {
519
+ // Check if the directory exists and has required files
520
+ const packageJsonPath = path.join(resolvedDistPath, 'package.json');
521
+ const globalDtsPath = path.join(resolvedDistPath, 'global.d.ts');
522
+ const tsConfigPath = path.join(resolvedDistPath, 'tsconfig.json');
523
+
524
+ const hasPackageJson = fs.existsSync(packageJsonPath);
525
+ const hasGlobalDts = fs.existsSync(globalDtsPath);
526
+ const hasTsConfig = fs.existsSync(tsConfigPath);
527
+
528
+ // Determine if we need to create a new project
529
+ const needsProjectCreation = !hasPackageJson || !hasGlobalDts || !hasTsConfig;
530
+
531
+ // Track if this is an existing project (has all required files)
532
+ isExistingProject = hasPackageJson && hasGlobalDts && hasTsConfig;
533
+
534
+ // Validate and sanitize package name if provided
535
+ if (packageName && !isValidNpmPackageName(packageName)) {
536
+ console.warn(`Warning: Package name "${packageName}" is not valid for npm.`);
537
+ const sanitized = sanitizePackageName(packageName);
538
+ console.log(`Using sanitized name: "${sanitized}"`);
539
+ packageName = sanitized;
540
+ }
541
+
542
+ if (needsProjectCreation) {
543
+ // If project needs creation but options are missing, prompt for them
544
+ if (!framework) {
545
+ const frameworkAnswer = await inquirer.prompt([{
546
+ type: 'list',
547
+ name: 'framework',
548
+ message: 'Which framework would you like to use?',
549
+ choices: ['react', 'vue']
550
+ }]);
551
+ framework = frameworkAnswer.framework;
552
+ }
553
+
554
+ // Try to read Flutter package metadata if flutterPackageSrc is provided
555
+ let metadata: FlutterPackageMetadata | null = null;
556
+ if (options.flutterPackageSrc) {
557
+ metadata = readFlutterPackageMetadata(options.flutterPackageSrc);
558
+ }
559
+
560
+ if (!packageName) {
561
+ // Use Flutter package name as default if available, sanitized for npm
562
+ const rawDefaultName = metadata?.name || path.basename(resolvedDistPath);
563
+ const defaultPackageName = sanitizePackageName(rawDefaultName);
564
+
565
+ const packageNameAnswer = await inquirer.prompt([{
566
+ type: 'input',
567
+ name: 'packageName',
568
+ message: 'What is your package name?',
569
+ default: defaultPackageName,
570
+ validate: (input: string) => {
571
+ if (!input || input.trim() === '') {
572
+ return 'Package name is required';
573
+ }
574
+
575
+ // Check if it's valid as-is
576
+ if (isValidNpmPackageName(input)) {
577
+ return true;
578
+ }
579
+
580
+ // If not valid, show what it would be sanitized to
581
+ const sanitized = sanitizePackageName(input);
582
+ return `Invalid npm package name. Would be sanitized to: "${sanitized}". Please enter a valid name.`;
453
583
  }
454
-
455
- // If not valid, show what it would be sanitized to
456
- const sanitized = sanitizePackageName(input);
457
- return `Invalid npm package name. Would be sanitized to: "${sanitized}". Please enter a valid name.`;
458
- }
459
- }]);
460
- packageName = packageNameAnswer.packageName;
461
- }
462
-
463
- console.log(`\nCreating new ${framework} project in ${resolvedDistPath}...`);
464
- createCommand(resolvedDistPath, {
465
- framework: framework!,
466
- packageName: packageName!,
467
- metadata: metadata || undefined
468
- });
469
- } else {
470
- // Validate existing project structure
471
- if (hasPackageJson) {
472
- try {
473
- const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));
474
-
475
- // Detect framework from existing package.json
476
- if (!framework) {
477
- if (packageJson.dependencies?.react || packageJson.devDependencies?.react) {
478
- framework = 'react';
479
- } else if (packageJson.dependencies?.vue || packageJson.devDependencies?.vue) {
480
- framework = 'vue';
481
- } else {
482
- // If can't detect, prompt for it
483
- const frameworkAnswer = await inquirer.prompt([{
484
- type: 'list',
485
- name: 'framework',
486
- message: 'Which framework are you using?',
487
- choices: ['react', 'vue']
488
- }]);
489
- framework = frameworkAnswer.framework;
584
+ }]);
585
+ packageName = packageNameAnswer.packageName;
586
+ }
587
+
588
+ console.log(`\nCreating new ${framework} project in ${resolvedDistPath}...`);
589
+ createCommand(resolvedDistPath, {
590
+ framework: framework!,
591
+ packageName: packageName!,
592
+ metadata: metadata || undefined
593
+ });
594
+ } else {
595
+ // Validate existing project structure
596
+ if (hasPackageJson) {
597
+ try {
598
+ const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));
599
+
600
+ // Detect framework from existing package.json
601
+ if (!framework) {
602
+ if (packageJson.dependencies?.react || packageJson.devDependencies?.react) {
603
+ framework = 'react';
604
+ } else if (packageJson.dependencies?.vue || packageJson.devDependencies?.vue) {
605
+ framework = 'vue';
606
+ } else {
607
+ // If can't detect, prompt for it
608
+ const frameworkAnswer = await inquirer.prompt([{
609
+ type: 'list',
610
+ name: 'framework',
611
+ message: 'Which framework are you using?',
612
+ choices: ['react', 'vue']
613
+ }]);
614
+ framework = frameworkAnswer.framework;
615
+ }
490
616
  }
617
+
618
+ console.log(`\nDetected existing ${framework} project in ${resolvedDistPath}`);
619
+ } catch (e) {
620
+ console.error('Error reading package.json:', e);
621
+ process.exit(1);
491
622
  }
492
-
493
- console.log(`\nDetected existing ${framework} project in ${resolvedDistPath}`);
494
- } catch (e) {
495
- console.error('Error reading package.json:', e);
496
- process.exit(1);
497
623
  }
498
624
  }
625
+ } else {
626
+ // In Dart-only mode, framework/packageName are unused; ensure framework is not accidentally required later.
627
+ framework = options.framework;
499
628
  }
500
-
629
+
501
630
  // Now proceed with code generation if flutter package source is provided
502
631
  if (!options.flutterPackageSrc) {
503
632
  console.log('\nProject is ready for code generation.');
504
633
  console.log('To generate code, run:');
505
634
  const displayPath = isTempDir ? '<output-dir>' : distPath;
506
- console.log(` webf codegen ${displayPath} --flutter-package-src=<path> --framework=${framework}`);
635
+ if (isDartOnly) {
636
+ console.log(` webf codegen ${displayPath} --flutter-package-src=<path> --dart-only`);
637
+ } else {
638
+ console.log(` webf codegen ${displayPath} --flutter-package-src=<path> --framework=${framework}`);
639
+ }
507
640
  if (isTempDir) {
508
641
  // Clean up temporary directory if we're not using it
509
642
  fs.rmSync(resolvedDistPath, { recursive: true, force: true });
510
643
  }
511
644
  return;
512
645
  }
513
-
646
+
514
647
  // Validate TypeScript environment in the Flutter package
515
648
  console.log(`\nValidating TypeScript environment in ${options.flutterPackageSrc}...`);
516
649
  const validation = validateTypeScriptEnvironment(options.flutterPackageSrc);
517
-
650
+
518
651
  if (!validation.isValid) {
519
652
  // Check specifically for missing tsconfig.json
520
653
  const tsConfigPath = path.join(options.flutterPackageSrc, 'tsconfig.json');
@@ -525,7 +658,7 @@ async function generateCommand(distPath: string, options: GenerateOptions): Prom
525
658
  message: 'No tsconfig.json found. Would you like me to create one for you?',
526
659
  default: true
527
660
  }]);
528
-
661
+
529
662
  if (createTsConfigAnswer.createTsConfig) {
530
663
  // Create a default tsconfig.json
531
664
  const defaultTsConfig = {
@@ -544,10 +677,10 @@ async function generateCommand(distPath: string, options: GenerateOptions): Prom
544
677
  include: ['lib/**/*.d.ts', '**/*.d.ts'],
545
678
  exclude: ['node_modules', 'dist', 'build']
546
679
  };
547
-
680
+
548
681
  fs.writeFileSync(tsConfigPath, JSON.stringify(defaultTsConfig, null, 2), 'utf-8');
549
682
  console.log('āœ… Created tsconfig.json');
550
-
683
+
551
684
  // Re-validate after creating tsconfig
552
685
  const newValidation = validateTypeScriptEnvironment(options.flutterPackageSrc);
553
686
  if (!newValidation.isValid) {
@@ -569,21 +702,40 @@ async function generateCommand(distPath: string, options: GenerateOptions): Prom
569
702
  process.exit(1);
570
703
  }
571
704
  }
572
-
573
- const command = `webf codegen --flutter-package-src=${options.flutterPackageSrc} --framework=${framework} <distPath>`;
574
-
705
+
706
+ const baseCommand = 'webf codegen';
707
+ const flutterPart = options.flutterPackageSrc ? ` --flutter-package-src=${options.flutterPackageSrc}` : '';
708
+ const modePart = isDartOnly
709
+ ? ' --dart-only'
710
+ : (framework ? ` --framework=${framework}` : '');
711
+ const command = `${baseCommand}${flutterPart}${modePart} <distPath>`;
712
+
713
+ if (isDartOnly) {
714
+ console.log(`\nGenerating Dart bindings from ${options.flutterPackageSrc}...`);
715
+
716
+ await dartGen({
717
+ source: options.flutterPackageSrc,
718
+ target: options.flutterPackageSrc,
719
+ command,
720
+ exclude: options.exclude,
721
+ });
722
+
723
+ console.log('\nDart code generation completed successfully!');
724
+ return;
725
+ }
726
+
575
727
  // Auto-initialize typings in the output directory if needed
576
728
  ensureInitialized(resolvedDistPath);
577
-
729
+
578
730
  console.log(`\nGenerating ${framework} code from ${options.flutterPackageSrc}...`);
579
-
731
+
580
732
  await dartGen({
581
733
  source: options.flutterPackageSrc,
582
734
  target: options.flutterPackageSrc,
583
735
  command,
584
736
  exclude: options.exclude,
585
737
  });
586
-
738
+
587
739
  if (framework === 'react') {
588
740
  // Get the package name from package.json if it exists
589
741
  let reactPackageName: string | undefined;
@@ -596,13 +748,15 @@ async function generateCommand(distPath: string, options: GenerateOptions): Prom
596
748
  } catch (e) {
597
749
  // Ignore errors
598
750
  }
599
-
751
+
600
752
  await reactGen({
601
753
  source: options.flutterPackageSrc,
602
754
  target: resolvedDistPath,
603
755
  command,
604
756
  exclude: options.exclude,
605
- packageName: reactPackageName,
757
+ // Prefer CLI-provided packageName (validated/sanitized above),
758
+ // fallback to detected name from package.json
759
+ packageName: packageName || reactPackageName,
606
760
  });
607
761
  } else if (framework === 'vue') {
608
762
  await vueGen({
@@ -612,19 +766,31 @@ async function generateCommand(distPath: string, options: GenerateOptions): Prom
612
766
  exclude: options.exclude,
613
767
  });
614
768
  }
615
-
769
+
616
770
  console.log('\nCode generation completed successfully!');
617
-
771
+
618
772
  // Automatically build the generated package
619
773
  if (framework) {
620
774
  try {
621
775
  await buildPackage(resolvedDistPath);
776
+ // After building React package, copy any matching .md docs next to built JS files
777
+ if (framework === 'react' && options.flutterPackageSrc) {
778
+ const distOut = path.join(resolvedDistPath, 'dist');
779
+ const { copied } = await copyMarkdownDocsToDist({
780
+ sourceRoot: options.flutterPackageSrc,
781
+ distRoot: distOut,
782
+ exclude: options.exclude,
783
+ });
784
+ if (copied > 0) {
785
+ console.log(`šŸ“„ Copied ${copied} markdown docs to dist`);
786
+ }
787
+ }
622
788
  } catch (error) {
623
789
  console.error('\nWarning: Build failed:', error);
624
790
  // Don't exit here since generation was successful
625
791
  }
626
792
  }
627
-
793
+
628
794
  // Handle npm publishing if requested via command line option
629
795
  if (options.publishToNpm && framework) {
630
796
  try {
@@ -641,7 +807,7 @@ async function generateCommand(distPath: string, options: GenerateOptions): Prom
641
807
  message: 'Would you like to publish this package to npm?',
642
808
  default: false
643
809
  }]);
644
-
810
+
645
811
  if (publishAnswer.publish) {
646
812
  // Ask for registry
647
813
  const registryAnswer = await inquirer.prompt([{
@@ -659,10 +825,10 @@ async function generateCommand(distPath: string, options: GenerateOptions): Prom
659
825
  }
660
826
  }
661
827
  }]);
662
-
828
+
663
829
  try {
664
830
  await buildAndPublishPackage(
665
- resolvedDistPath,
831
+ resolvedDistPath,
666
832
  registryAnswer.registry || undefined,
667
833
  isExistingProject
668
834
  );
@@ -672,7 +838,7 @@ async function generateCommand(distPath: string, options: GenerateOptions): Prom
672
838
  }
673
839
  }
674
840
  }
675
-
841
+
676
842
  // If using a temporary directory, remind the user where the files are
677
843
  if (isTempDir) {
678
844
  console.log(`\nšŸ“ Generated files are in: ${resolvedDistPath}`);
@@ -694,19 +860,19 @@ function writeFileIfChanged(filePath: string, content: string) {
694
860
  function ensureInitialized(targetPath: string): void {
695
861
  const globalDtsPath = path.join(targetPath, 'global.d.ts');
696
862
  const tsConfigPath = path.join(targetPath, 'tsconfig.json');
697
-
863
+
698
864
  // Check if initialization files already exist
699
865
  const needsInit = !fs.existsSync(globalDtsPath) || !fs.existsSync(tsConfigPath);
700
-
866
+
701
867
  if (needsInit) {
702
868
  console.log('Initializing WebF typings...');
703
869
  fs.mkdirSync(targetPath, { recursive: true });
704
-
870
+
705
871
  if (!fs.existsSync(globalDtsPath)) {
706
872
  fs.writeFileSync(globalDtsPath, gloabalDts, 'utf-8');
707
873
  console.log('Created global.d.ts');
708
874
  }
709
-
875
+
710
876
  if (!fs.existsSync(tsConfigPath)) {
711
877
  fs.writeFileSync(tsConfigPath, tsConfig, 'utf-8');
712
878
  console.log('Created tsconfig.json');
@@ -716,7 +882,7 @@ function ensureInitialized(targetPath: string): void {
716
882
 
717
883
  async function buildPackage(packagePath: string): Promise<void> {
718
884
  const packageJsonPath = path.join(packagePath, 'package.json');
719
-
885
+
720
886
  if (!fs.existsSync(packageJsonPath)) {
721
887
  // Skip the error in test environment to avoid console warnings
722
888
  if (process.env.NODE_ENV === 'test' || process.env.JEST_WORKER_ID !== undefined) {
@@ -724,34 +890,34 @@ async function buildPackage(packagePath: string): Promise<void> {
724
890
  }
725
891
  throw new Error(`No package.json found in ${packagePath}`);
726
892
  }
727
-
893
+
728
894
  const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));
729
895
  const packageName = packageJson.name;
730
896
  const packageVersion = packageJson.version;
731
-
897
+
732
898
  // Check if node_modules exists
733
899
  const nodeModulesPath = path.join(packagePath, 'node_modules');
734
900
  if (!fs.existsSync(nodeModulesPath)) {
735
901
  console.log(`\nšŸ“¦ Installing dependencies for ${packageName}...`);
736
-
902
+
737
903
  // Check if yarn.lock exists to determine package manager
738
904
  const yarnLockPath = path.join(packagePath, 'yarn.lock');
739
905
  const useYarn = fs.existsSync(yarnLockPath);
740
-
906
+
741
907
  const installCommand = useYarn ? 'yarn' : NPM;
742
908
  const installArgs = useYarn ? [] : ['install'];
743
-
909
+
744
910
  const installResult = spawnSync(installCommand, installArgs, {
745
911
  cwd: packagePath,
746
912
  stdio: 'inherit'
747
913
  });
748
-
914
+
749
915
  if (installResult.status !== 0) {
750
916
  throw new Error('Failed to install dependencies');
751
917
  }
752
918
  console.log('āœ… Dependencies installed successfully!');
753
919
  }
754
-
920
+
755
921
  // Check if package has a build script
756
922
  if (packageJson.scripts?.build) {
757
923
  console.log(`\nBuilding ${packageName}@${packageVersion}...`);
@@ -759,7 +925,7 @@ async function buildPackage(packagePath: string): Promise<void> {
759
925
  cwd: packagePath,
760
926
  stdio: 'inherit'
761
927
  });
762
-
928
+
763
929
  if (buildResult.status !== 0) {
764
930
  throw new Error('Build failed');
765
931
  }
@@ -771,18 +937,18 @@ async function buildPackage(packagePath: string): Promise<void> {
771
937
 
772
938
  async function buildAndPublishPackage(packagePath: string, registry?: string, isExistingProject: boolean = false): Promise<void> {
773
939
  const packageJsonPath = path.join(packagePath, 'package.json');
774
-
940
+
775
941
  if (!fs.existsSync(packageJsonPath)) {
776
942
  throw new Error(`No package.json found in ${packagePath}`);
777
943
  }
778
-
944
+
779
945
  let packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));
780
946
  const packageName = packageJson.name;
781
947
  let packageVersion = packageJson.version;
782
-
948
+
783
949
  // First, ensure dependencies are installed and build the package
784
950
  await buildPackage(packagePath);
785
-
951
+
786
952
  // If this is an existing project, increment the patch version before publishing
787
953
  if (isExistingProject) {
788
954
  console.log(`\nIncrementing version for existing project...`);
@@ -791,18 +957,18 @@ async function buildAndPublishPackage(packagePath: string, registry?: string, is
791
957
  encoding: 'utf-8',
792
958
  stdio: 'pipe'
793
959
  });
794
-
960
+
795
961
  if (versionResult.status !== 0) {
796
962
  console.error('Failed to increment version:', versionResult.stderr);
797
963
  throw new Error('Failed to increment version');
798
964
  }
799
-
965
+
800
966
  // Re-read package.json to get the new version
801
967
  packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));
802
968
  packageVersion = packageJson.version;
803
969
  console.log(`Version updated to ${packageVersion}`);
804
970
  }
805
-
971
+
806
972
  // Set registry if provided
807
973
  if (registry) {
808
974
  console.log(`\nUsing npm registry: ${registry}`);
@@ -810,38 +976,38 @@ async function buildAndPublishPackage(packagePath: string, registry?: string, is
810
976
  cwd: packagePath,
811
977
  stdio: 'inherit'
812
978
  });
813
-
979
+
814
980
  if (setRegistryResult.status !== 0) {
815
981
  throw new Error('Failed to set npm registry');
816
982
  }
817
983
  }
818
-
984
+
819
985
  // Check if user is logged in to npm
820
986
  const whoamiResult = spawnSync(NPM, ['whoami'], {
821
987
  cwd: packagePath,
822
988
  encoding: 'utf-8'
823
989
  });
824
-
990
+
825
991
  if (whoamiResult.status !== 0) {
826
992
  console.error('\nError: You must be logged in to npm to publish packages.');
827
993
  console.error('Please run "npm login" first.');
828
994
  throw new Error('Not logged in to npm');
829
995
  }
830
-
996
+
831
997
  console.log(`\nPublishing ${packageName}@${packageVersion} to npm...`);
832
-
998
+
833
999
  // Publish the package
834
1000
  const publishResult = spawnSync(NPM, ['publish'], {
835
1001
  cwd: packagePath,
836
1002
  stdio: 'inherit'
837
1003
  });
838
-
1004
+
839
1005
  if (publishResult.status !== 0) {
840
1006
  throw new Error('Publish failed');
841
1007
  }
842
-
1008
+
843
1009
  console.log(`\nāœ… Successfully published ${packageName}@${packageVersion}`);
844
-
1010
+
845
1011
  // Reset registry to default if it was changed
846
1012
  if (registry) {
847
1013
  spawnSync(NPM, ['config', 'delete', 'registry'], {