@openwebf/webf 0.23.10 → 0.24.1

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/agents.ts ADDED
@@ -0,0 +1,267 @@
1
+ import fs from 'fs';
2
+ import os from 'os';
3
+ import path from 'path';
4
+ import yaml from 'yaml';
5
+
6
+ type SkillInfo = {
7
+ directoryName: string;
8
+ name: string;
9
+ description?: string;
10
+ skillFileRelativePath: string;
11
+ referenceRelativePaths: string[];
12
+ };
13
+
14
+ type CopyStats = {
15
+ filesWritten: number;
16
+ filesUnchanged: number;
17
+ backupsCreated: number;
18
+ };
19
+
20
+ const WEBF_AGENTS_BLOCK_START = '<!-- webf-agents:init start -->';
21
+ const WEBF_AGENTS_BLOCK_END = '<!-- webf-agents:init end -->';
22
+
23
+ function ensureDirSync(dirPath: string) {
24
+ fs.mkdirSync(dirPath, { recursive: true });
25
+ }
26
+
27
+ function readFileIfExists(filePath: string): Buffer | null {
28
+ try {
29
+ return fs.readFileSync(filePath);
30
+ } catch (error: any) {
31
+ if (error?.code === 'ENOENT') return null;
32
+ throw error;
33
+ }
34
+ }
35
+
36
+ function backupFileSync(filePath: string) {
37
+ const timestamp = new Date()
38
+ .toISOString()
39
+ .replace(/[:.]/g, '')
40
+ .replace('T', '')
41
+ .replace('Z', 'Z');
42
+ const backupPath = `${filePath}.bak.${timestamp}`;
43
+ fs.copyFileSync(filePath, backupPath);
44
+ return backupPath;
45
+ }
46
+
47
+ function copyFileWithBackupSync(srcPath: string, destPath: string) {
48
+ const src = fs.readFileSync(srcPath);
49
+ const dest = readFileIfExists(destPath);
50
+
51
+ if (dest && Buffer.compare(src, dest) === 0) return { changed: false, backupPath: null as string | null };
52
+
53
+ ensureDirSync(path.dirname(destPath));
54
+ let backupPath: string | null = null;
55
+ if (dest) {
56
+ backupPath = backupFileSync(destPath);
57
+ }
58
+ fs.writeFileSync(destPath, src);
59
+ return { changed: true, backupPath };
60
+ }
61
+
62
+ function copyDirRecursiveSync(srcDir: string, destDir: string, stats: CopyStats) {
63
+ ensureDirSync(destDir);
64
+ const entries = fs.readdirSync(srcDir, { withFileTypes: true });
65
+ for (const entry of entries) {
66
+ const srcPath = path.join(srcDir, entry.name);
67
+ const destPath = path.join(destDir, entry.name);
68
+ if (entry.isDirectory()) {
69
+ copyDirRecursiveSync(srcPath, destPath, stats);
70
+ continue;
71
+ }
72
+ if (entry.isFile()) {
73
+ const { changed, backupPath } = copyFileWithBackupSync(srcPath, destPath);
74
+ if (changed) stats.filesWritten += 1;
75
+ else stats.filesUnchanged += 1;
76
+ if (backupPath) stats.backupsCreated += 1;
77
+ }
78
+ }
79
+ }
80
+
81
+ function listFilesRecursiveSync(dirPath: string): string[] {
82
+ const out: string[] = [];
83
+ const entries = fs.readdirSync(dirPath, { withFileTypes: true });
84
+ for (const entry of entries) {
85
+ const entryPath = path.join(dirPath, entry.name);
86
+ if (entry.isDirectory()) {
87
+ out.push(...listFilesRecursiveSync(entryPath));
88
+ continue;
89
+ }
90
+ if (entry.isFile()) out.push(entryPath);
91
+ }
92
+ return out;
93
+ }
94
+
95
+ function toPosixRelativePath(fromDir: string, absolutePath: string) {
96
+ const rel = path.relative(fromDir, absolutePath);
97
+ return rel.split(path.sep).join('/');
98
+ }
99
+
100
+ function parseSkillFrontmatter(skillMd: string): { name?: string; description?: string } {
101
+ const trimmed = skillMd.trimStart();
102
+ if (!trimmed.startsWith('---')) return {};
103
+ const endIndex = trimmed.indexOf('\n---', 3);
104
+ if (endIndex === -1) return {};
105
+ const fm = trimmed.slice(3, endIndex).trim();
106
+ try {
107
+ const parsed = yaml.parse(fm) ?? {};
108
+ return { name: parsed?.name, description: parsed?.description };
109
+ } catch {
110
+ return {};
111
+ }
112
+ }
113
+
114
+ function updateOrAppendMarkedBlock(existing: string, newBlock: string): { content: string; action: 'replaced' | 'appended' } {
115
+ const startIndex = existing.indexOf(WEBF_AGENTS_BLOCK_START);
116
+ const endIndex = existing.indexOf(WEBF_AGENTS_BLOCK_END);
117
+
118
+ if (startIndex !== -1 && endIndex !== -1 && endIndex > startIndex) {
119
+ const before = existing.slice(0, startIndex).trimEnd();
120
+ const after = existing.slice(endIndex + WEBF_AGENTS_BLOCK_END.length).trimStart();
121
+ const next = [before, newBlock.trim(), after].filter(Boolean).join('\n\n');
122
+ return { content: next + '\n', action: 'replaced' };
123
+ }
124
+
125
+ const content = existing.trimEnd();
126
+ return { content: (content ? content + '\n\n' : '') + newBlock.trim() + '\n', action: 'appended' };
127
+ }
128
+
129
+ function buildClaudeBlock(sourcePackageName: string, sourcePackageVersion: string, skills: SkillInfo[]) {
130
+ const lines: string[] = [];
131
+ lines.push(WEBF_AGENTS_BLOCK_START);
132
+ lines.push('## WebF Claude Code Skills');
133
+ lines.push('');
134
+ lines.push(`Source: \`${sourcePackageName}@${sourcePackageVersion}\``);
135
+ lines.push('');
136
+ lines.push('### Skills');
137
+ for (const skill of skills) {
138
+ const desc = skill.description ? ` — ${skill.description}` : '';
139
+ lines.push(`- \`${skill.name}\`${desc} (\`${skill.skillFileRelativePath}\`)`);
140
+ }
141
+
142
+ const anyReferences = skills.some(s => s.referenceRelativePaths.length > 0);
143
+ if (anyReferences) {
144
+ lines.push('');
145
+ lines.push('### References');
146
+ for (const skill of skills) {
147
+ if (skill.referenceRelativePaths.length === 0) continue;
148
+ const refs = skill.referenceRelativePaths.map(r => `\`${r}\``).join(', ');
149
+ lines.push(`- \`${skill.name}\`: ${refs}`);
150
+ }
151
+ }
152
+
153
+ lines.push(WEBF_AGENTS_BLOCK_END);
154
+ return lines.join('\n');
155
+ }
156
+
157
+ function resolveSkillsPackageRoot() {
158
+ const packageJsonPath = require.resolve('@openwebf/claude-code-skills/package.json');
159
+ const packageRoot = path.dirname(packageJsonPath);
160
+ const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));
161
+ return {
162
+ packageRoot,
163
+ packageName: packageJson?.name ?? '@openwebf/claude-code-skills',
164
+ packageVersion: packageJson?.version ?? 'unknown',
165
+ };
166
+ }
167
+
168
+ function listSkillDirectories(skillsPackageRoot: string) {
169
+ const entries = fs.readdirSync(skillsPackageRoot, { withFileTypes: true });
170
+ const dirs = entries
171
+ .filter(e => e.isDirectory())
172
+ .map(e => e.name)
173
+ .filter(name => fs.existsSync(path.join(skillsPackageRoot, name, 'SKILL.md')))
174
+ .sort();
175
+ return dirs;
176
+ }
177
+
178
+ function collectSkillInfo(projectRoot: string, skillsDir: string, skillDirectoryName: string): SkillInfo {
179
+ const skillDirAbs = path.join(skillsDir, skillDirectoryName);
180
+ const skillMdAbs = path.join(skillDirAbs, 'SKILL.md');
181
+ const skillMd = fs.readFileSync(skillMdAbs, 'utf-8');
182
+ const fm = parseSkillFrontmatter(skillMd);
183
+
184
+ const referenceRelativePaths: string[] = [];
185
+ const files = listFilesRecursiveSync(skillDirAbs);
186
+ for (const fileAbs of files) {
187
+ if (path.basename(fileAbs) === 'SKILL.md') continue;
188
+ referenceRelativePaths.push(toPosixRelativePath(projectRoot, fileAbs));
189
+ }
190
+ referenceRelativePaths.sort();
191
+
192
+ return {
193
+ directoryName: skillDirectoryName,
194
+ name: fm.name ?? skillDirectoryName,
195
+ description: fm.description,
196
+ skillFileRelativePath: toPosixRelativePath(projectRoot, skillMdAbs),
197
+ referenceRelativePaths,
198
+ };
199
+ }
200
+
201
+ async function agentsInitCommand(projectDir: string): Promise<void> {
202
+ const startedAt = Date.now();
203
+ const resolvedProjectDir = path.resolve(process.cwd(), projectDir || '.');
204
+ const claudeMdPath = path.join(resolvedProjectDir, 'CLAUDE.md');
205
+ const claudeDir = path.join(resolvedProjectDir, '.claude');
206
+ const projectSkillsDir = path.join(claudeDir, 'skills');
207
+
208
+ const hasClaudeMd = fs.existsSync(claudeMdPath);
209
+ const hasClaudeDir = fs.existsSync(claudeDir);
210
+
211
+ const isNewProject = !hasClaudeMd && !hasClaudeDir;
212
+
213
+ console.log('webf agents init');
214
+ console.log(`Project: ${resolvedProjectDir}`);
215
+ if (isNewProject) {
216
+ console.log('Detected: no CLAUDE.md and no .claude/ (new project)');
217
+ } else {
218
+ console.log(`Detected: CLAUDE.md=${hasClaudeMd ? 'yes' : 'no'}, .claude/=${hasClaudeDir ? 'yes' : 'no'} (existing project)`);
219
+ }
220
+
221
+ const { packageRoot, packageName, packageVersion } = resolveSkillsPackageRoot();
222
+ const skillDirectories = listSkillDirectories(packageRoot);
223
+
224
+ if (skillDirectories.length === 0) {
225
+ throw new Error(`No skills found in ${packageName} (resolved at ${packageRoot}).`);
226
+ }
227
+
228
+ console.log(`Skills source: ${packageName}@${packageVersion}`);
229
+ console.log(`Skills destination: ${toPosixRelativePath(resolvedProjectDir, projectSkillsDir)}`);
230
+
231
+ ensureDirSync(projectSkillsDir);
232
+
233
+ const copyStats: CopyStats = { filesWritten: 0, filesUnchanged: 0, backupsCreated: 0 };
234
+ for (const skillDirName of skillDirectories) {
235
+ const srcSkillDir = path.join(packageRoot, skillDirName);
236
+ const destSkillDir = path.join(projectSkillsDir, skillDirName);
237
+
238
+ copyDirRecursiveSync(srcSkillDir, destSkillDir, copyStats);
239
+ }
240
+
241
+ const installedSkills = skillDirectories.map(skillDirName =>
242
+ collectSkillInfo(resolvedProjectDir, projectSkillsDir, skillDirName)
243
+ );
244
+
245
+ const block = buildClaudeBlock(packageName, packageVersion, installedSkills);
246
+
247
+ if (isNewProject) {
248
+ const initial = ['# Claude Code', '', block, ''].join('\n');
249
+ fs.writeFileSync(claudeMdPath, initial, 'utf-8');
250
+ console.log(`Created ${toPosixRelativePath(resolvedProjectDir, claudeMdPath)}`);
251
+ } else {
252
+ const existing = readFileIfExists(claudeMdPath)?.toString('utf-8') ?? '';
253
+ const { content, action } = updateOrAppendMarkedBlock(existing, block);
254
+ fs.writeFileSync(claudeMdPath, content, 'utf-8');
255
+ console.log(`${action === 'replaced' ? 'Updated' : 'Appended'} WebF skills block in ${toPosixRelativePath(resolvedProjectDir, claudeMdPath)}`);
256
+ }
257
+
258
+ const versionFilePath = path.join(claudeDir, 'webf-claude-code-skills.version');
259
+ fs.writeFileSync(versionFilePath, `${packageName}@${packageVersion}${os.EOL}`, 'utf-8');
260
+ console.log(`Wrote ${toPosixRelativePath(resolvedProjectDir, versionFilePath)}`);
261
+
262
+ console.log(
263
+ `Installed ${installedSkills.length} skills (${copyStats.filesWritten} files written, ${copyStats.filesUnchanged} unchanged, ${copyStats.backupsCreated} backups) in ${Date.now() - startedAt}ms`
264
+ );
265
+ }
266
+
267
+ export { agentsInitCommand };
package/src/commands.ts CHANGED
@@ -4,10 +4,12 @@ import path from 'path';
4
4
  import os from 'os';
5
5
  import { dartGen, reactGen, vueGen } from './generator';
6
6
  import { generateModuleArtifacts } from './module';
7
+ import { getPackageTypesFileFromDir, isPackageTypesReady, readJsonFile } from './peerDeps';
7
8
  import { globSync } from 'glob';
8
9
  import _ from 'lodash';
9
10
  import inquirer from 'inquirer';
10
11
  import yaml from 'yaml';
12
+ import { agentsInitCommand } from './agents';
11
13
 
12
14
  interface GenerateOptions {
13
15
  flutterPackageSrc?: string;
@@ -441,7 +443,8 @@ function createCommand(target: string, options: { framework: string; packageName
441
443
  // Leave merge to the codegen step which appends exports safely
442
444
  }
443
445
 
444
- spawnSync(NPM, ['install'], {
446
+ // Ensure devDependencies are installed even if the user's shell has NODE_ENV=production.
447
+ spawnSync(NPM, ['install', '--production=false'], {
445
448
  cwd: target,
446
449
  stdio: 'inherit'
447
450
  });
@@ -463,12 +466,8 @@ function createCommand(target: string, options: { framework: string; packageName
463
466
  const gitignoreContent = _.template(gitignore)({});
464
467
  writeFileIfChanged(gitignorePath, gitignoreContent);
465
468
 
466
- spawnSync(NPM, ['install', '@openwebf/webf-enterprise-typings'], {
467
- cwd: target,
468
- stdio: 'inherit'
469
- });
470
-
471
- spawnSync(NPM, ['install', 'vue', '-D'], {
469
+ // Ensure devDependencies are installed even if the user's shell has NODE_ENV=production.
470
+ spawnSync(NPM, ['install', '--production=false'], {
472
471
  cwd: target,
473
472
  stdio: 'inherit'
474
473
  });
@@ -1179,6 +1178,101 @@ async function buildPackage(packagePath: string): Promise<void> {
1179
1178
  const packageName = packageJson.name;
1180
1179
  const packageVersion = packageJson.version;
1181
1180
 
1181
+ function getInstalledPackageJsonPath(pkgName: string): string {
1182
+ const parts = pkgName.split('/');
1183
+ return path.join(packagePath, 'node_modules', ...parts, 'package.json');
1184
+ }
1185
+
1186
+ function getInstalledPackageDir(pkgName: string): string {
1187
+ const parts = pkgName.split('/');
1188
+ return path.join(packagePath, 'node_modules', ...parts);
1189
+ }
1190
+
1191
+ function findUp(startDir: string, relativePathToFind: string): string | null {
1192
+ let dir = path.resolve(startDir);
1193
+ while (true) {
1194
+ const candidate = path.join(dir, relativePathToFind);
1195
+ if (fs.existsSync(candidate)) return candidate;
1196
+ const parent = path.dirname(dir);
1197
+ if (parent === dir) return null;
1198
+ dir = parent;
1199
+ }
1200
+ }
1201
+
1202
+ function ensurePeerDependencyAvailableForBuild(peerName: string): void {
1203
+ const installedPkgJson = getInstalledPackageJsonPath(peerName);
1204
+ if (fs.existsSync(installedPkgJson)) return;
1205
+
1206
+ const peerRange = packageJson.peerDependencies?.[peerName];
1207
+ const localMap: Record<string, string> = {
1208
+ '@openwebf/react-core-ui': path.join('packages', 'react-core-ui'),
1209
+ '@openwebf/vue-core-ui': path.join('packages', 'vue-core-ui'),
1210
+ };
1211
+
1212
+ let installSpec: string | null = null;
1213
+
1214
+ const localRel = localMap[peerName];
1215
+ if (localRel) {
1216
+ const localPath = findUp(process.cwd(), localRel);
1217
+ if (localPath) {
1218
+ if (!isPackageTypesReady(localPath)) {
1219
+ const localPkgJsonPath = path.join(localPath, 'package.json');
1220
+ if (fs.existsSync(localPkgJsonPath)) {
1221
+ const localPkgJson = readJsonFile(localPkgJsonPath);
1222
+ if (localPkgJson.scripts?.build) {
1223
+ if (process.env.WEBF_CODEGEN_BUILD_LOCAL_PEERS !== '1') {
1224
+ console.warn(
1225
+ `\n⚠️ Local ${peerName} found at ${localPath} but type declarations are missing; falling back to registry install.`
1226
+ );
1227
+ } else {
1228
+ console.log(
1229
+ `\n🔧 Local ${peerName} found at ${localPath} but build artifacts are missing; building it for DTS...`
1230
+ );
1231
+ const buildLocalResult = spawnSync(NPM, ['run', 'build'], {
1232
+ cwd: localPath,
1233
+ stdio: 'inherit'
1234
+ });
1235
+ if (buildLocalResult.status === 0) {
1236
+ if (isPackageTypesReady(localPath)) {
1237
+ installSpec = localPath;
1238
+ } else {
1239
+ console.warn(
1240
+ `\n⚠️ Built local ${peerName} but type declarations are still missing; falling back to registry install.`
1241
+ );
1242
+ }
1243
+ } else {
1244
+ console.warn(`\n⚠️ Failed to build local ${peerName}; falling back to registry install.`);
1245
+ }
1246
+ }
1247
+ }
1248
+ }
1249
+ } else {
1250
+ installSpec = localPath;
1251
+ }
1252
+ }
1253
+ }
1254
+
1255
+ if (!installSpec) {
1256
+ installSpec = peerRange ? `${peerName}@${peerRange}` : peerName;
1257
+ }
1258
+
1259
+ console.log(`\n📦 Installing peer dependency for build: ${peerName}...`);
1260
+ const installResult = spawnSync(NPM, ['install', '--no-save', installSpec], {
1261
+ cwd: packagePath,
1262
+ stdio: 'inherit'
1263
+ });
1264
+ if (installResult.status !== 0) {
1265
+ throw new Error(`Failed to install peer dependency for build: ${peerName}`);
1266
+ }
1267
+
1268
+ const installedTypesFile = getPackageTypesFileFromDir(getInstalledPackageDir(peerName));
1269
+ if (installedTypesFile && !fs.existsSync(installedTypesFile)) {
1270
+ throw new Error(
1271
+ `Peer dependency ${peerName} was installed but type declarations were not found at ${installedTypesFile}`
1272
+ );
1273
+ }
1274
+ }
1275
+
1182
1276
  // Check if node_modules exists
1183
1277
  const nodeModulesPath = path.join(packagePath, 'node_modules');
1184
1278
  if (!fs.existsSync(nodeModulesPath)) {
@@ -1204,6 +1298,14 @@ async function buildPackage(packagePath: string): Promise<void> {
1204
1298
 
1205
1299
  // Check if package has a build script
1206
1300
  if (packageJson.scripts?.build) {
1301
+ // DTS build needs peer deps present locally to resolve types (even though they are not bundled).
1302
+ if (packageJson.peerDependencies?.['@openwebf/react-core-ui']) {
1303
+ ensurePeerDependencyAvailableForBuild('@openwebf/react-core-ui');
1304
+ }
1305
+ if (packageJson.peerDependencies?.['@openwebf/vue-core-ui']) {
1306
+ ensurePeerDependencyAvailableForBuild('@openwebf/vue-core-ui');
1307
+ }
1308
+
1207
1309
  console.log(`\nBuilding ${packageName}@${packageVersion}...`);
1208
1310
  const buildResult = spawnSync(NPM, ['run', 'build'], {
1209
1311
  cwd: packagePath,
@@ -1300,4 +1402,4 @@ async function buildAndPublishPackage(packagePath: string, registry?: string, is
1300
1402
  }
1301
1403
  }
1302
1404
 
1303
- export { generateCommand, generateModuleCommand };
1405
+ export { generateCommand, generateModuleCommand, agentsInitCommand };
package/src/generator.ts CHANGED
@@ -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) {
package/src/module.ts CHANGED
@@ -335,14 +335,14 @@ function mapTsParamTypeToDart(
335
335
  return { dartType: 'dynamic', isByteData: false };
336
336
  }
337
337
 
338
- function mapTsPropertyTypeToDart(type: ts.TypeNode): string {
338
+ function mapTsPropertyTypeToDart(type: ts.TypeNode, optional: boolean): string {
339
339
  switch (type.kind) {
340
340
  case ts.SyntaxKind.StringKeyword:
341
- return 'String?';
341
+ return optional ? 'String?' : 'String';
342
342
  case ts.SyntaxKind.NumberKeyword:
343
- return 'num?';
343
+ return optional ? 'num?' : 'num';
344
344
  case ts.SyntaxKind.BooleanKeyword:
345
- return 'bool?';
345
+ return optional ? 'bool?' : 'bool';
346
346
  default:
347
347
  return 'dynamic';
348
348
  }
@@ -380,16 +380,17 @@ function buildDartBindings(def: ModuleDefinition, command: string): string {
380
380
 
381
381
  for (const iface of optionInterfaces) {
382
382
  const name = iface.name.text;
383
- const propInfos: { fieldName: string; key: string; dartType: string }[] = [];
383
+ const propInfos: { fieldName: string; key: string; dartType: string; optional: boolean }[] = [];
384
384
 
385
385
  for (const member of iface.members) {
386
386
  if (!ts.isPropertySignature(member) || !member.name) continue;
387
387
 
388
388
  const key = member.name.getText(def.sourceFile).replace(/['"]/g, '');
389
389
  const fieldName = key;
390
- const dartType = member.type ? mapTsPropertyTypeToDart(member.type) : 'dynamic';
390
+ const optional = !!member.questionToken;
391
+ const dartType = member.type ? mapTsPropertyTypeToDart(member.type, optional) : 'dynamic';
391
392
 
392
- propInfos.push({ fieldName, key, dartType });
393
+ propInfos.push({ fieldName, key, dartType, optional });
393
394
  }
394
395
 
395
396
  lines.push(`class ${name} {`);
@@ -398,23 +399,50 @@ function buildDartBindings(def: ModuleDefinition, command: string): string {
398
399
  }
399
400
  lines.push('');
400
401
 
401
- const ctorParams = propInfos.map(p => `this.${p.fieldName}`).join(', ');
402
+ const ctorParams = propInfos.map(p => {
403
+ if (p.optional || p.dartType === 'dynamic') {
404
+ return `this.${p.fieldName}`;
405
+ }
406
+ return `required this.${p.fieldName}`;
407
+ }).join(', ');
402
408
  lines.push(` const ${name}({${ctorParams}});`);
403
409
  lines.push('');
404
410
 
405
411
  lines.push(` factory ${name}.fromMap(Map<String, dynamic> map) {`);
406
412
  lines.push(` return ${name}(`);
407
413
  for (const prop of propInfos) {
408
- if (prop.dartType.startsWith('String')) {
409
- lines.push(` ${prop.fieldName}: map['${prop.key}']?.toString(),`);
410
- } else if (prop.dartType.startsWith('bool')) {
411
- lines.push(
412
- ` ${prop.fieldName}: map['${prop.key}'] is bool ? map['${prop.key}'] as bool : null,`
413
- );
414
- } else if (prop.dartType.startsWith('num')) {
415
- lines.push(
416
- ` ${prop.fieldName}: map['${prop.key}'] is num ? map['${prop.key}'] as num : null,`
417
- );
414
+ const isString = prop.dartType.startsWith('String');
415
+ const isBool = prop.dartType.startsWith('bool');
416
+ const isNum = prop.dartType.startsWith('num');
417
+
418
+ if (isString) {
419
+ if (prop.optional) {
420
+ lines.push(` ${prop.fieldName}: map['${prop.key}']?.toString(),`);
421
+ } else {
422
+ lines.push(
423
+ ` ${prop.fieldName}: map['${prop.key}']?.toString() ?? '',`
424
+ );
425
+ }
426
+ } else if (isBool) {
427
+ if (prop.optional) {
428
+ lines.push(
429
+ ` ${prop.fieldName}: map['${prop.key}'] is bool ? map['${prop.key}'] as bool : null,`
430
+ );
431
+ } else {
432
+ lines.push(
433
+ ` ${prop.fieldName}: map['${prop.key}'] is bool ? map['${prop.key}'] as bool : false,`
434
+ );
435
+ }
436
+ } else if (isNum) {
437
+ if (prop.optional) {
438
+ lines.push(
439
+ ` ${prop.fieldName}: map['${prop.key}'] is num ? map['${prop.key}'] as num : null,`
440
+ );
441
+ } else {
442
+ lines.push(
443
+ ` ${prop.fieldName}: map['${prop.key}'] is num ? map['${prop.key}'] as num : 0,`
444
+ );
445
+ }
418
446
  } else {
419
447
  lines.push(` ${prop.fieldName}: map['${prop.key}'],`);
420
448
  }
@@ -426,9 +454,13 @@ function buildDartBindings(def: ModuleDefinition, command: string): string {
426
454
  lines.push(' Map<String, dynamic> toMap() {');
427
455
  lines.push(' final map = <String, dynamic>{};');
428
456
  for (const prop of propInfos) {
429
- lines.push(
430
- ` if (${prop.fieldName} != null) { map['${prop.key}'] = ${prop.fieldName}; }`
431
- );
457
+ if (!prop.optional && (prop.dartType === 'String' || prop.dartType === 'bool' || prop.dartType === 'num')) {
458
+ lines.push(` map['${prop.key}'] = ${prop.fieldName};`);
459
+ } else {
460
+ lines.push(
461
+ ` if (${prop.fieldName} != null) { map['${prop.key}'] = ${prop.fieldName}; }`
462
+ );
463
+ }
432
464
  }
433
465
  lines.push(' return map;');
434
466
  lines.push(' }');
@@ -0,0 +1,21 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+
4
+ export function readJsonFile(jsonPath: string): any {
5
+ return JSON.parse(fs.readFileSync(jsonPath, 'utf-8'));
6
+ }
7
+
8
+ export function getPackageTypesFileFromDir(pkgDir: string): string | null {
9
+ const pkgJsonPath = path.join(pkgDir, 'package.json');
10
+ if (!fs.existsSync(pkgJsonPath)) return null;
11
+ const pkgJson = readJsonFile(pkgJsonPath);
12
+ const typesRel: string | undefined = pkgJson.types;
13
+ if (!typesRel) return null;
14
+ return path.join(pkgDir, typesRel);
15
+ }
16
+
17
+ export function isPackageTypesReady(pkgDir: string): boolean {
18
+ const typesFile = getPackageTypesFileFromDir(pkgDir);
19
+ return typesFile ? fs.existsSync(typesFile) : true;
20
+ }
21
+