@openwebf/webf 0.23.7 → 0.24.0

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,7 +3,9 @@ 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
+ import { generateModuleArtifacts } from './module';
7
+ import { getPackageTypesFileFromDir, isPackageTypesReady, readJsonFile } from './peerDeps';
8
+ import { globSync } from 'glob';
7
9
  import _ from 'lodash';
8
10
  import inquirer from 'inquirer';
9
11
  import yaml from 'yaml';
@@ -164,6 +166,20 @@ const gitignore = fs.readFileSync(
164
166
  'utf-8'
165
167
  );
166
168
 
169
+ const modulePackageJson = fs.readFileSync(
170
+ path.resolve(__dirname, '../templates/module.package.json.tpl'),
171
+ 'utf-8'
172
+ );
173
+
174
+ const moduleTsConfig = fs.readFileSync(
175
+ path.resolve(__dirname, '../templates/module.tsconfig.json.tpl'),
176
+ 'utf-8'
177
+ );
178
+
179
+ const moduleTsUpConfig = fs.readFileSync(
180
+ path.resolve(__dirname, '../templates/module.tsup.config.ts.tpl'),
181
+ 'utf-8'
182
+ );
167
183
  const reactPackageJson = fs.readFileSync(
168
184
  path.resolve(__dirname, '../templates/react.package.json.tpl'),
169
185
  'utf-8'
@@ -222,6 +238,40 @@ function readFlutterPackageMetadata(packagePath: string): FlutterPackageMetadata
222
238
  }
223
239
  }
224
240
 
241
+ function copyReadmeToPackageRoot(params: {
242
+ sourceRoot: string;
243
+ targetRoot: string;
244
+ }): { copied: boolean; sourcePath?: string; targetPath: string } {
245
+ const { sourceRoot, targetRoot } = params;
246
+ const targetPath = path.join(targetRoot, 'README.md');
247
+
248
+ if (fs.existsSync(targetPath)) {
249
+ return { copied: false, targetPath };
250
+ }
251
+
252
+ const candidateNames = ['README.md', 'Readme.md', 'readme.md'];
253
+ let sourcePath: string | null = null;
254
+ for (const candidate of candidateNames) {
255
+ const abs = path.join(sourceRoot, candidate);
256
+ if (fs.existsSync(abs)) {
257
+ sourcePath = abs;
258
+ break;
259
+ }
260
+ }
261
+
262
+ if (!sourcePath) {
263
+ return { copied: false, targetPath };
264
+ }
265
+
266
+ try {
267
+ const content = fs.readFileSync(sourcePath, 'utf-8');
268
+ writeFileIfChanged(targetPath, content);
269
+ return { copied: true, sourcePath, targetPath };
270
+ } catch {
271
+ return { copied: false, targetPath };
272
+ }
273
+ }
274
+
225
275
  // Copy markdown docs that match .d.ts basenames from source to the built dist folder,
226
276
  // and generate an aggregated README.md in the dist directory.
227
277
  async function copyMarkdownDocsToDist(params: {
@@ -241,7 +291,7 @@ async function copyMarkdownDocsToDist(params: {
241
291
  const ignore = exclude && exclude.length ? [...defaultIgnore, ...exclude] : defaultIgnore;
242
292
 
243
293
  // Find all .d.ts files and check for sibling .md files
244
- const dtsFiles = glob.globSync('**/*.d.ts', { cwd: sourceRoot, ignore });
294
+ const dtsFiles = globSync('**/*.d.ts', { cwd: sourceRoot, ignore });
245
295
  let copied = 0;
246
296
  let skipped = 0;
247
297
  const readmeSections: { title: string; relPath: string; content: string }[] = [];
@@ -426,8 +476,8 @@ function createCommand(target: string, options: { framework: string; packageName
426
476
  // Leave merge to the codegen step which appends exports safely
427
477
  }
428
478
 
429
- // !no '--omit=peer' here.
430
- spawnSync(NPM, ['install'], {
479
+ // Ensure devDependencies are installed even if the user's shell has NODE_ENV=production.
480
+ spawnSync(NPM, ['install', '--production=false'], {
431
481
  cwd: target,
432
482
  stdio: 'inherit'
433
483
  });
@@ -449,12 +499,8 @@ function createCommand(target: string, options: { framework: string; packageName
449
499
  const gitignoreContent = _.template(gitignore)({});
450
500
  writeFileIfChanged(gitignorePath, gitignoreContent);
451
501
 
452
- spawnSync(NPM, ['install', '@openwebf/webf-enterprise-typings'], {
453
- cwd: target,
454
- stdio: 'inherit'
455
- });
456
-
457
- spawnSync(NPM, ['install', 'vue', '-D'], {
502
+ // Ensure devDependencies are installed even if the user's shell has NODE_ENV=production.
503
+ spawnSync(NPM, ['install', '--production=false'], {
458
504
  cwd: target,
459
505
  stdio: 'inherit'
460
506
  });
@@ -463,6 +509,46 @@ function createCommand(target: string, options: { framework: string; packageName
463
509
  console.log(`WebF ${framework} package created at: ${target}`);
464
510
  }
465
511
 
512
+ function createModuleProject(target: string, options: { packageName: string; metadata?: FlutterPackageMetadata; skipGitignore?: boolean }): void {
513
+ const { metadata, skipGitignore } = options;
514
+ const packageName = isValidNpmPackageName(options.packageName)
515
+ ? options.packageName
516
+ : sanitizePackageName(options.packageName);
517
+
518
+ if (!fs.existsSync(target)) {
519
+ fs.mkdirSync(target, { recursive: true });
520
+ }
521
+
522
+ const packageJsonPath = path.join(target, 'package.json');
523
+ const packageJsonContent = _.template(modulePackageJson)({
524
+ packageName,
525
+ version: metadata?.version || '0.0.1',
526
+ description: metadata?.description || '',
527
+ });
528
+ writeFileIfChanged(packageJsonPath, packageJsonContent);
529
+
530
+ const tsConfigPath = path.join(target, 'tsconfig.json');
531
+ const tsConfigContent = _.template(moduleTsConfig)({});
532
+ writeFileIfChanged(tsConfigPath, tsConfigContent);
533
+
534
+ const tsupConfigPath = path.join(target, 'tsup.config.ts');
535
+ const tsupConfigContent = _.template(moduleTsUpConfig)({});
536
+ writeFileIfChanged(tsupConfigPath, tsupConfigContent);
537
+
538
+ if (!skipGitignore) {
539
+ const gitignorePath = path.join(target, '.gitignore');
540
+ const gitignoreContent = _.template(gitignore)({});
541
+ writeFileIfChanged(gitignorePath, gitignoreContent);
542
+ }
543
+
544
+ const srcDir = path.join(target, 'src');
545
+ if (!fs.existsSync(srcDir)) {
546
+ fs.mkdirSync(srcDir, { recursive: true });
547
+ }
548
+
549
+ console.log(`WebF module package scaffold created at: ${target}`);
550
+ }
551
+
466
552
  async function generateCommand(distPath: string, options: GenerateOptions): Promise<void> {
467
553
  // If distPath is not provided or is '.', create a temporary directory
468
554
  let resolvedDistPath: string;
@@ -727,6 +813,17 @@ async function generateCommand(distPath: string, options: GenerateOptions): Prom
727
813
  // Auto-initialize typings in the output directory if needed
728
814
  ensureInitialized(resolvedDistPath);
729
815
 
816
+ // Copy README.md from the source Flutter package into the npm package root (so `npm publish` includes it).
817
+ if (options.flutterPackageSrc) {
818
+ const { copied } = copyReadmeToPackageRoot({
819
+ sourceRoot: options.flutterPackageSrc,
820
+ targetRoot: resolvedDistPath,
821
+ });
822
+ if (copied) {
823
+ console.log('📄 Copied README.md to package root');
824
+ }
825
+ }
826
+
730
827
  console.log(`\nGenerating ${framework} code from ${options.flutterPackageSrc}...`);
731
828
 
732
829
  await dartGen({
@@ -846,6 +943,236 @@ async function generateCommand(distPath: string, options: GenerateOptions): Prom
846
943
  }
847
944
  }
848
945
 
946
+ async function generateModuleCommand(distPath: string, options: GenerateOptions): Promise<void> {
947
+ let resolvedDistPath: string;
948
+ let isTempDir = false;
949
+ if (!distPath || distPath === '.') {
950
+ const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'webf-module-'));
951
+ resolvedDistPath = tempDir;
952
+ isTempDir = true;
953
+ console.log(`\nUsing temporary directory for module package: ${tempDir}`);
954
+ } else {
955
+ resolvedDistPath = path.resolve(distPath);
956
+ }
957
+
958
+ // Detect Flutter package root if not provided
959
+ if (!options.flutterPackageSrc) {
960
+ let currentDir = process.cwd();
961
+ let foundPubspec = false;
962
+ let pubspecDir = '';
963
+
964
+ for (let i = 0; i < 3; i++) {
965
+ const pubspecPath = path.join(currentDir, 'pubspec.yaml');
966
+ if (fs.existsSync(pubspecPath)) {
967
+ foundPubspec = true;
968
+ pubspecDir = currentDir;
969
+ break;
970
+ }
971
+ const parentDir = path.dirname(currentDir);
972
+ if (parentDir === currentDir) break;
973
+ currentDir = parentDir;
974
+ }
975
+
976
+ if (!foundPubspec) {
977
+ console.error('Could not find pubspec.yaml. Please provide --flutter-package-src.');
978
+ process.exit(1);
979
+ }
980
+
981
+ options.flutterPackageSrc = pubspecDir;
982
+ console.log(`Detected Flutter package at: ${pubspecDir}`);
983
+ }
984
+
985
+ const flutterPackageSrc = path.resolve(options.flutterPackageSrc);
986
+
987
+ // Validate TS environment in the Flutter package
988
+ console.log(`\nValidating TypeScript environment in ${flutterPackageSrc}...`);
989
+ let validation = validateTypeScriptEnvironment(flutterPackageSrc);
990
+ if (!validation.isValid) {
991
+ const tsConfigPath = path.join(flutterPackageSrc, 'tsconfig.json');
992
+ if (!fs.existsSync(tsConfigPath)) {
993
+ const defaultTsConfig = {
994
+ compilerOptions: {
995
+ target: 'ES2020',
996
+ module: 'commonjs',
997
+ lib: ['ES2020'],
998
+ declaration: true,
999
+ strict: true,
1000
+ esModuleInterop: true,
1001
+ skipLibCheck: true,
1002
+ forceConsistentCasingInFileNames: true,
1003
+ resolveJsonModule: true,
1004
+ moduleResolution: 'node',
1005
+ },
1006
+ include: ['lib/**/*.d.ts', '**/*.d.ts'],
1007
+ exclude: ['node_modules', 'dist', 'build'],
1008
+ };
1009
+
1010
+ fs.writeFileSync(tsConfigPath, JSON.stringify(defaultTsConfig, null, 2), 'utf-8');
1011
+ console.log('✅ Created tsconfig.json for module package');
1012
+
1013
+ validation = validateTypeScriptEnvironment(flutterPackageSrc);
1014
+ }
1015
+
1016
+ if (!validation.isValid) {
1017
+ console.error('\n❌ TypeScript environment validation failed:');
1018
+ validation.errors.forEach(err => console.error(` - ${err}`));
1019
+ console.error('\nPlease fix the above issues before running `webf module-codegen` again.');
1020
+ process.exit(1);
1021
+ }
1022
+ }
1023
+
1024
+ // Read Flutter metadata for package.json
1025
+ const metadata = readFlutterPackageMetadata(flutterPackageSrc);
1026
+
1027
+ // Determine package name
1028
+ let packageName = options.packageName;
1029
+ if (packageName && !isValidNpmPackageName(packageName)) {
1030
+ console.warn(`Warning: Package name "${packageName}" is not valid for npm.`);
1031
+ const sanitized = sanitizePackageName(packageName);
1032
+ console.log(`Using sanitized name: "${sanitized}"`);
1033
+ packageName = sanitized;
1034
+ }
1035
+
1036
+ if (!packageName) {
1037
+ const rawDefaultName = metadata?.name
1038
+ ? `@openwebf/${metadata.name.replace(/^webf_/, 'webf-')}`
1039
+ : '@openwebf/webf-module';
1040
+
1041
+ const defaultPackageName = isValidNpmPackageName(rawDefaultName)
1042
+ ? rawDefaultName
1043
+ : sanitizePackageName(rawDefaultName);
1044
+
1045
+ const packageNameAnswer = await inquirer.prompt([{
1046
+ type: 'input',
1047
+ name: 'packageName',
1048
+ message: 'What is your npm package name for this module?',
1049
+ default: defaultPackageName,
1050
+ validate: (input: string) => {
1051
+ if (!input || input.trim() === '') {
1052
+ return 'Package name is required';
1053
+ }
1054
+
1055
+ if (isValidNpmPackageName(input)) {
1056
+ return true;
1057
+ }
1058
+
1059
+ const sanitized = sanitizePackageName(input);
1060
+ return `Invalid npm package name. Would be sanitized to: "${sanitized}". Please enter a valid name.`;
1061
+ }
1062
+ }]);
1063
+ packageName = packageNameAnswer.packageName;
1064
+ }
1065
+
1066
+ // Prevent npm scaffolding (package.json, tsup.config.ts, etc.) from being written into
1067
+ // the Flutter package itself. Force users to choose a separate output directory.
1068
+ if (resolvedDistPath === flutterPackageSrc) {
1069
+ console.error('\n❌ Output directory must not be the Flutter package root.');
1070
+ console.error('Please choose a separate directory for the generated npm package, for example:');
1071
+ console.error(' webf module-codegen ../packages/webf-share --flutter-package-src=../webf_modules/share');
1072
+ process.exit(1);
1073
+ }
1074
+
1075
+ // Scaffold npm project for the module
1076
+ if (!packageName) {
1077
+ throw new Error('Package name could not be resolved for module package.');
1078
+ }
1079
+ createModuleProject(resolvedDistPath, {
1080
+ packageName,
1081
+ metadata: metadata || undefined,
1082
+ });
1083
+
1084
+ // Locate module interface file (*.module.d.ts)
1085
+ const defaultIgnore = ['**/node_modules/**', '**/dist/**', '**/build/**', '**/example/**'];
1086
+ const ignore = options.exclude && options.exclude.length
1087
+ ? [...defaultIgnore, ...options.exclude]
1088
+ : defaultIgnore;
1089
+
1090
+ const candidates = globSync('**/*.module.d.ts', {
1091
+ cwd: flutterPackageSrc,
1092
+ ignore,
1093
+ });
1094
+
1095
+ if (candidates.length === 0) {
1096
+ console.error(
1097
+ `\n❌ No module interface files (*.module.d.ts) found under ${flutterPackageSrc}.`
1098
+ );
1099
+ console.error('Please add a TypeScript interface file describing your module API.');
1100
+ process.exit(1);
1101
+ }
1102
+
1103
+ const moduleInterfaceRel = candidates[0];
1104
+ const moduleInterfacePath = path.join(flutterPackageSrc, moduleInterfaceRel);
1105
+
1106
+ const command = `webf module-codegen --flutter-package-src=${flutterPackageSrc} <distPath>`;
1107
+
1108
+ console.log(`\nGenerating module npm package and Dart bindings from ${moduleInterfaceRel}...`);
1109
+
1110
+ generateModuleArtifacts({
1111
+ moduleInterfacePath,
1112
+ npmTargetDir: resolvedDistPath,
1113
+ flutterPackageDir: flutterPackageSrc,
1114
+ command,
1115
+ });
1116
+
1117
+ console.log('\nModule code generation completed successfully!');
1118
+
1119
+ try {
1120
+ await buildPackage(resolvedDistPath);
1121
+ } catch (error) {
1122
+ console.error('\nWarning: Build failed:', error);
1123
+ }
1124
+
1125
+ if (options.publishToNpm) {
1126
+ try {
1127
+ await buildAndPublishPackage(resolvedDistPath, options.npmRegistry, false);
1128
+ } catch (error) {
1129
+ console.error('\nError during npm publish:', error);
1130
+ process.exit(1);
1131
+ }
1132
+ } else {
1133
+ const publishAnswer = await inquirer.prompt([{
1134
+ type: 'confirm',
1135
+ name: 'publish',
1136
+ message: 'Would you like to publish this module package to npm?',
1137
+ default: false
1138
+ }]);
1139
+
1140
+ if (publishAnswer.publish) {
1141
+ const registryAnswer = await inquirer.prompt([{
1142
+ type: 'input',
1143
+ name: 'registry',
1144
+ message: 'NPM registry URL (leave empty for default npm registry):',
1145
+ default: '',
1146
+ validate: (input: string) => {
1147
+ if (!input) return true;
1148
+ try {
1149
+ new URL(input);
1150
+ return true;
1151
+ } catch {
1152
+ return 'Please enter a valid URL';
1153
+ }
1154
+ }
1155
+ }]);
1156
+
1157
+ try {
1158
+ await buildAndPublishPackage(
1159
+ resolvedDistPath,
1160
+ registryAnswer.registry || undefined,
1161
+ false
1162
+ );
1163
+ } catch (error) {
1164
+ console.error('\nError during npm publish:', error);
1165
+ // Don't exit here since generation was successful
1166
+ }
1167
+ }
1168
+ }
1169
+
1170
+ if (isTempDir) {
1171
+ console.log(`\n📁 Generated module npm package is in: ${resolvedDistPath}`);
1172
+ console.log('💡 To use it, copy this directory to your packages folder or publish it directly.');
1173
+ }
1174
+ }
1175
+
849
1176
  function writeFileIfChanged(filePath: string, content: string) {
850
1177
  if (fs.existsSync(filePath)) {
851
1178
  const oldContent = fs.readFileSync(filePath, 'utf-8')
@@ -895,6 +1222,101 @@ async function buildPackage(packagePath: string): Promise<void> {
895
1222
  const packageName = packageJson.name;
896
1223
  const packageVersion = packageJson.version;
897
1224
 
1225
+ function getInstalledPackageJsonPath(pkgName: string): string {
1226
+ const parts = pkgName.split('/');
1227
+ return path.join(packagePath, 'node_modules', ...parts, 'package.json');
1228
+ }
1229
+
1230
+ function getInstalledPackageDir(pkgName: string): string {
1231
+ const parts = pkgName.split('/');
1232
+ return path.join(packagePath, 'node_modules', ...parts);
1233
+ }
1234
+
1235
+ function findUp(startDir: string, relativePathToFind: string): string | null {
1236
+ let dir = path.resolve(startDir);
1237
+ while (true) {
1238
+ const candidate = path.join(dir, relativePathToFind);
1239
+ if (fs.existsSync(candidate)) return candidate;
1240
+ const parent = path.dirname(dir);
1241
+ if (parent === dir) return null;
1242
+ dir = parent;
1243
+ }
1244
+ }
1245
+
1246
+ function ensurePeerDependencyAvailableForBuild(peerName: string): void {
1247
+ const installedPkgJson = getInstalledPackageJsonPath(peerName);
1248
+ if (fs.existsSync(installedPkgJson)) return;
1249
+
1250
+ const peerRange = packageJson.peerDependencies?.[peerName];
1251
+ const localMap: Record<string, string> = {
1252
+ '@openwebf/react-core-ui': path.join('packages', 'react-core-ui'),
1253
+ '@openwebf/vue-core-ui': path.join('packages', 'vue-core-ui'),
1254
+ };
1255
+
1256
+ let installSpec: string | null = null;
1257
+
1258
+ const localRel = localMap[peerName];
1259
+ if (localRel) {
1260
+ const localPath = findUp(process.cwd(), localRel);
1261
+ if (localPath) {
1262
+ if (!isPackageTypesReady(localPath)) {
1263
+ const localPkgJsonPath = path.join(localPath, 'package.json');
1264
+ if (fs.existsSync(localPkgJsonPath)) {
1265
+ const localPkgJson = readJsonFile(localPkgJsonPath);
1266
+ if (localPkgJson.scripts?.build) {
1267
+ if (process.env.WEBF_CODEGEN_BUILD_LOCAL_PEERS !== '1') {
1268
+ console.warn(
1269
+ `\n⚠️ Local ${peerName} found at ${localPath} but type declarations are missing; falling back to registry install.`
1270
+ );
1271
+ } else {
1272
+ console.log(
1273
+ `\n🔧 Local ${peerName} found at ${localPath} but build artifacts are missing; building it for DTS...`
1274
+ );
1275
+ const buildLocalResult = spawnSync(NPM, ['run', 'build'], {
1276
+ cwd: localPath,
1277
+ stdio: 'inherit'
1278
+ });
1279
+ if (buildLocalResult.status === 0) {
1280
+ if (isPackageTypesReady(localPath)) {
1281
+ installSpec = localPath;
1282
+ } else {
1283
+ console.warn(
1284
+ `\n⚠️ Built local ${peerName} but type declarations are still missing; falling back to registry install.`
1285
+ );
1286
+ }
1287
+ } else {
1288
+ console.warn(`\n⚠️ Failed to build local ${peerName}; falling back to registry install.`);
1289
+ }
1290
+ }
1291
+ }
1292
+ }
1293
+ } else {
1294
+ installSpec = localPath;
1295
+ }
1296
+ }
1297
+ }
1298
+
1299
+ if (!installSpec) {
1300
+ installSpec = peerRange ? `${peerName}@${peerRange}` : peerName;
1301
+ }
1302
+
1303
+ console.log(`\n📦 Installing peer dependency for build: ${peerName}...`);
1304
+ const installResult = spawnSync(NPM, ['install', '--no-save', installSpec], {
1305
+ cwd: packagePath,
1306
+ stdio: 'inherit'
1307
+ });
1308
+ if (installResult.status !== 0) {
1309
+ throw new Error(`Failed to install peer dependency for build: ${peerName}`);
1310
+ }
1311
+
1312
+ const installedTypesFile = getPackageTypesFileFromDir(getInstalledPackageDir(peerName));
1313
+ if (installedTypesFile && !fs.existsSync(installedTypesFile)) {
1314
+ throw new Error(
1315
+ `Peer dependency ${peerName} was installed but type declarations were not found at ${installedTypesFile}`
1316
+ );
1317
+ }
1318
+ }
1319
+
898
1320
  // Check if node_modules exists
899
1321
  const nodeModulesPath = path.join(packagePath, 'node_modules');
900
1322
  if (!fs.existsSync(nodeModulesPath)) {
@@ -920,6 +1342,14 @@ async function buildPackage(packagePath: string): Promise<void> {
920
1342
 
921
1343
  // Check if package has a build script
922
1344
  if (packageJson.scripts?.build) {
1345
+ // DTS build needs peer deps present locally to resolve types (even though they are not bundled).
1346
+ if (packageJson.peerDependencies?.['@openwebf/react-core-ui']) {
1347
+ ensurePeerDependencyAvailableForBuild('@openwebf/react-core-ui');
1348
+ }
1349
+ if (packageJson.peerDependencies?.['@openwebf/vue-core-ui']) {
1350
+ ensurePeerDependencyAvailableForBuild('@openwebf/vue-core-ui');
1351
+ }
1352
+
923
1353
  console.log(`\nBuilding ${packageName}@${packageVersion}...`);
924
1354
  const buildResult = spawnSync(NPM, ['run', 'build'], {
925
1355
  cwd: packagePath,
@@ -1016,4 +1446,4 @@ async function buildAndPublishPackage(packagePath: string, registry?: string, is
1016
1446
  }
1017
1447
  }
1018
1448
 
1019
- export { generateCommand };
1449
+ export { generateCommand, generateModuleCommand };
package/src/generator.ts CHANGED
@@ -2,7 +2,7 @@ import path from 'path';
2
2
  import fs from 'fs';
3
3
  import process from 'process';
4
4
  import _ from 'lodash';
5
- import { glob } from 'glob';
5
+ import { globSync } from 'glob';
6
6
  import yaml from 'yaml';
7
7
  import { IDLBlob } from './IDLBlob';
8
8
  import { ClassObject, ConstObject, EnumObject, TypeAliasObject } from './declaration';
@@ -19,7 +19,7 @@ const fileContentCache = new Map<string, string>();
19
19
  // Cache for generated content to detect changes
20
20
  const generatedContentCache = new Map<string, string>();
21
21
 
22
- function writeFileIfChanged(filePath: string, content: string): boolean {
22
+ export function writeFileIfChanged(filePath: string, content: string): boolean {
23
23
  // Check if content has changed by comparing with cache
24
24
  const cachedContent = generatedContentCache.get(filePath);
25
25
  if (cachedContent === content) {
@@ -107,7 +107,7 @@ function getTypeFiles(source: string, excludePatterns?: string[]): string[] {
107
107
  const defaultIgnore = ['**/node_modules/**', '**/dist/**', '**/build/**', '**/example/**'];
108
108
  const ignore = excludePatterns ? [...defaultIgnore, ...excludePatterns] : defaultIgnore;
109
109
 
110
- const files = glob.globSync("**/*.d.ts", {
110
+ const files = globSync("**/*.d.ts", {
111
111
  cwd: source,
112
112
  ignore: ignore
113
113
  });
@@ -407,13 +407,9 @@ export async function reactGen({ source, target, exclude, packageName }: Generat
407
407
  warn(`Failed to merge into existing index.ts. Skipping modifications: ${indexFilePath}`);
408
408
  }
409
409
  }
410
-
411
- timeEnd('reactGen');
412
- success(`React code generation completed. ${filesChanged} files changed.`);
413
- info(`Output directory: ${normalizedTarget}`);
414
- info('You can now import these components in your React project.');
415
410
 
416
- // Aggregate standalone type declarations (consts/enums/type aliases) into a single types.ts
411
+ // Always generate src/types.ts so generated components can safely import it.
412
+ // When there are no standalone declarations, emit an empty module (`export {};`).
417
413
  try {
418
414
  const consts = blobs.flatMap(b => b.objects.filter(o => o instanceof ConstObject) as ConstObject[]);
419
415
  const enums = blobs.flatMap(b => b.objects.filter(o => o instanceof EnumObject) as EnumObject[]);
@@ -426,40 +422,40 @@ export async function reactGen({ source, target, exclude, packageName }: Generat
426
422
  typeAliases.forEach(t => { if (!typeAliasMap.has(t.name)) typeAliasMap.set(t.name, t); });
427
423
 
428
424
  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');
425
+ const constDecl = Array.from(constMap.values())
426
+ .map(c => `export declare const ${c.name}: ${c.type};`)
427
+ .join('\n');
428
+ const enumDecl = enums
429
+ .map(e => `export enum ${e.name} { ${e.members.map(m => m.initializer ? `${m.name} = ${m.initializer}` : `${m.name}`).join(', ')} }`)
430
+ .join('\n');
431
+ const typeAliasDecl = Array.from(typeAliasMap.values())
432
+ .map(t => `export type ${t.name} = ${t.type};`)
433
+ .join('\n');
439
434
 
440
- const typesContent = [
441
- '/* Generated by WebF CLI - aggregated type declarations */',
442
- typeAliasDecl,
443
- constDecl,
444
- enumDecl,
445
- ''
446
- ].filter(Boolean).join('\n');
435
+ const typesContent = [
436
+ '/* Generated by WebF CLI - aggregated type declarations */',
437
+ hasAny ? typeAliasDecl : '',
438
+ hasAny ? constDecl : '',
439
+ hasAny ? enumDecl : '',
440
+ hasAny ? '' : 'export {};',
441
+ ''
442
+ ].filter(Boolean).join('\n');
447
443
 
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
- }
444
+ const typesPath = path.join(normalizedTarget, 'src', 'types.ts');
445
+ if (writeFileIfChanged(typesPath, typesContent)) {
446
+ filesChanged++;
447
+ debug(`Generated: src/types.ts`);
448
+ try {
449
+ const constNames = Array.from(constMap.keys());
450
+ const aliasNames = Array.from(typeAliasMap.keys());
451
+ const enumNames = enums.map(e => e.name);
452
+ debug(`[react] Aggregated types - consts: ${constNames.join(', ') || '(none)'}; typeAliases: ${aliasNames.join(', ') || '(none)'}; enums: ${enumNames.join(', ') || '(none)'}\n`);
453
+ debug(`[react] src/types.ts preview:\n` + typesContent.split('\n').slice(0, 20).join('\n'));
454
+ } catch {}
455
+ }
460
456
 
461
- // Ensure index.ts re-exports these types so consumers get them on import.
462
- const indexFilePath = path.join(normalizedTarget, 'src', 'index.ts');
457
+ // Only re-export from index.ts when there are actual declarations to surface.
458
+ if (hasAny) {
463
459
  try {
464
460
  let current = '';
465
461
  if (fs.existsSync(indexFilePath)) {
@@ -478,6 +474,11 @@ export async function reactGen({ source, target, exclude, packageName }: Generat
478
474
  } catch (e) {
479
475
  warn('Failed to generate aggregated React types');
480
476
  }
477
+
478
+ timeEnd('reactGen');
479
+ success(`React code generation completed. ${filesChanged} files changed.`);
480
+ info(`Output directory: ${normalizedTarget}`);
481
+ info('You can now import these components in your React project.');
481
482
  }
482
483
 
483
484
  export async function vueGen({ source, target, exclude }: GenerateOptions) {