@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/README.md +8 -0
- package/bin/webf.js +9 -1
- package/dist/agents.js +245 -0
- package/dist/commands.js +101 -7
- package/dist/generator.js +37 -36
- package/dist/module.js +43 -14
- package/dist/peerDeps.js +27 -0
- package/dist/react.js +10 -18
- package/dist/vue.js +138 -132
- package/package.json +2 -1
- package/src/agents.ts +267 -0
- package/src/commands.ts +110 -8
- package/src/generator.ts +38 -37
- package/src/module.ts +53 -21
- package/src/peerDeps.ts +21 -0
- package/src/react.ts +10 -18
- package/src/vue.ts +157 -142
- package/templates/react.component.tsx.tpl +2 -2
- package/templates/react.index.ts.tpl +2 -1
- package/templates/react.package.json.tpl +3 -4
- package/templates/react.tsconfig.json.tpl +8 -1
- package/templates/react.tsup.config.ts.tpl +1 -1
- package/templates/vue.component.partial.tpl +4 -4
- package/templates/vue.components.d.ts.tpl +24 -9
- package/templates/vue.package.json.tpl +3 -1
- package/test/agents-init.test.ts +80 -0
- package/test/commands.test.ts +5 -12
- package/test/generator.test.ts +17 -10
- package/test/peerDeps.test.ts +30 -0
- package/test/react-consts.test.ts +9 -3
- package/test/standard-props.test.ts +14 -14
- package/test/templates.test.ts +17 -0
- package/test/vue.test.ts +36 -11
- package/dist/constants.js +0 -242
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
|
-
|
|
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
|
-
|
|
467
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
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
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
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
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
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
|
-
|
|
462
|
-
|
|
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
|
|
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 =>
|
|
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
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
)
|
|
414
|
-
}
|
|
415
|
-
|
|
416
|
-
|
|
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
|
-
|
|
430
|
-
`
|
|
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(' }');
|
package/src/peerDeps.ts
ADDED
|
@@ -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
|
+
|