@openwebf/webf 0.23.10 → 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/dist/vue.js CHANGED
@@ -8,7 +8,6 @@ const lodash_1 = __importDefault(require("lodash"));
8
8
  const fs_1 = __importDefault(require("fs"));
9
9
  const path_1 = __importDefault(require("path"));
10
10
  const declaration_1 = require("./declaration");
11
- const logger_1 = require("./logger");
12
11
  const utils_1 = require("./utils");
13
12
  function readTemplate(name) {
14
13
  return fs_1.default.readFileSync(path_1.default.join(__dirname, '../templates/' + name + '.tpl'), { encoding: 'utf-8' });
@@ -50,19 +49,18 @@ function generateReturnType(type) {
50
49
  return 'any';
51
50
  if (typeof pointerType === 'string' && pointerType.startsWith('typeof ')) {
52
51
  const ident = pointerType.substring('typeof '.length).trim();
53
- return `typeof __webfTypes.${ident}`;
52
+ return `typeof ${ident}`;
54
53
  }
55
54
  return pointerType;
56
55
  }
57
56
  if (type.isArray && typeof type.value === 'object' && !Array.isArray(type.value)) {
58
- const elemType = (0, utils_1.getPointerType)(type.value);
59
- if (elemType === 'Type')
57
+ const elemType = generateReturnType(type.value);
58
+ if (!elemType)
60
59
  return 'any[]';
61
- if (typeof elemType === 'string' && elemType.startsWith('typeof ')) {
62
- const ident = elemType.substring('typeof '.length).trim();
63
- return `(typeof __webfTypes.${ident})[]`;
60
+ if (/^[A-Za-z_][A-Za-z0-9_]*(?:\\.[A-Za-z_][A-Za-z0-9_]*)*$/.test(elemType)) {
61
+ return `${elemType}[]`;
64
62
  }
65
- return `${elemType}[]`;
63
+ return `(${elemType})[]`;
66
64
  }
67
65
  switch (type.value) {
68
66
  case declaration_1.FunctionArgumentType.int:
@@ -111,125 +109,111 @@ function generateMethodDeclaration(method) {
111
109
  var returnType = generateReturnType(method.returnType);
112
110
  return `${methodName}(${args}): ${returnType};`;
113
111
  }
114
- function generateVueComponent(blob) {
115
- const classObjects = blob.objects;
116
- // Skip if no class objects
117
- if (!classObjects || classObjects.length === 0) {
118
- return '';
119
- }
120
- const classObjectDictionary = Object.fromEntries(classObjects.map(object => {
121
- return [object.name, object];
122
- }));
123
- const properties = classObjects.filter(object => {
124
- return object.name.endsWith('Properties');
112
+ function getVueComponentSpecs(blob) {
113
+ const classObjects = blob.objects.filter(obj => obj instanceof declaration_1.ClassObject);
114
+ if (classObjects.length === 0)
115
+ return [];
116
+ const properties = classObjects.filter(object => object.name.endsWith('Properties'));
117
+ const events = classObjects.filter(object => object.name.endsWith('Events'));
118
+ const methods = classObjects.filter(object => object.name.endsWith('Methods'));
119
+ const componentMap = new Map();
120
+ properties.forEach(prop => {
121
+ const className = prop.name.replace(/Properties$/, '');
122
+ if (!componentMap.has(className))
123
+ componentMap.set(className, { className });
124
+ componentMap.get(className).properties = prop;
125
125
  });
126
- const events = classObjects.filter(object => {
127
- return object.name.endsWith('Events');
126
+ events.forEach(ev => {
127
+ const className = ev.name.replace(/Events$/, '');
128
+ if (!componentMap.has(className))
129
+ componentMap.set(className, { className });
130
+ componentMap.get(className).events = ev;
128
131
  });
129
- const others = classObjects.filter(object => {
130
- return !object.name.endsWith('Properties')
131
- && !object.name.endsWith('Events');
132
+ methods.forEach(m => {
133
+ const className = m.name.replace(/Methods$/, '');
134
+ if (!componentMap.has(className))
135
+ componentMap.set(className, { className });
136
+ componentMap.get(className).methods = m;
132
137
  });
133
- const dependencies = others.map(object => {
134
- if (!object || !object.props || object.props.length === 0) {
135
- return '';
136
- }
137
- const interfaceLines = [];
138
- if (object.documentation && object.documentation.trim().length > 0) {
139
- interfaceLines.push('/**');
140
- object.documentation.split('\n').forEach(line => {
141
- interfaceLines.push(` * ${line}`);
138
+ return Array.from(componentMap.values())
139
+ .filter(spec => spec.className.trim().length > 0)
140
+ .sort((a, b) => a.className.localeCompare(b.className));
141
+ }
142
+ function renderSupportingInterface(object) {
143
+ const hasProps = !!object.props && object.props.length > 0;
144
+ const hasMethods = !!object.methods && object.methods.length > 0;
145
+ if (!hasProps && !hasMethods) {
146
+ return '';
147
+ }
148
+ const interfaceLines = [];
149
+ if (object.documentation && object.documentation.trim().length > 0) {
150
+ interfaceLines.push('/**');
151
+ object.documentation.split('\n').forEach(line => {
152
+ interfaceLines.push(` * ${line}`);
153
+ });
154
+ interfaceLines.push(' */');
155
+ }
156
+ interfaceLines.push(`export interface ${object.name} {`);
157
+ const propLines = (object.props || []).map(prop => {
158
+ const lines = [];
159
+ if (prop.documentation && prop.documentation.trim().length > 0) {
160
+ lines.push(' /**');
161
+ prop.documentation.split('\n').forEach(line => {
162
+ lines.push(` * ${line}`);
142
163
  });
143
- interfaceLines.push(' */');
164
+ lines.push(' */');
144
165
  }
145
- interfaceLines.push(`interface ${object.name} {`);
146
- const propLines = object.props.map(prop => {
147
- const lines = [];
148
- if (prop.documentation && prop.documentation.trim().length > 0) {
149
- lines.push(' /**');
150
- prop.documentation.split('\n').forEach(line => {
151
- lines.push(` * ${line}`);
152
- });
153
- lines.push(' */');
154
- }
155
- const optionalToken = prop.optional ? '?' : '';
156
- lines.push(` ${prop.name}${optionalToken}: ${generateReturnType(prop.type)};`);
157
- return lines.join('\n');
166
+ const optionalToken = prop.optional ? '?' : '';
167
+ lines.push(` ${prop.name}${optionalToken}: ${generateReturnType(prop.type)};`);
168
+ return lines.join('\n');
169
+ });
170
+ interfaceLines.push(propLines.join('\n'));
171
+ if (object.methods && object.methods.length > 0) {
172
+ const methodLines = object.methods.map(method => {
173
+ return ` ${generateMethodDeclaration(method)}`;
158
174
  });
159
- interfaceLines.push(propLines.join('\n'));
160
- interfaceLines.push('}');
161
- return interfaceLines.join('\n');
162
- }).filter(dep => dep.trim() !== '').join('\n\n');
163
- const componentProperties = properties.length > 0 ? properties[0] : undefined;
164
- const componentEvents = events.length > 0 ? events[0] : undefined;
165
- const className = (() => {
166
- if (componentProperties) {
167
- return componentProperties.name.replace(/Properties$/, '');
168
- }
169
- if (componentEvents) {
170
- return componentEvents.name.replace(/Events$/, '');
171
- }
172
- return '';
173
- })();
174
- if (!className) {
175
- return '';
175
+ interfaceLines.push(methodLines.join('\n'));
176
176
  }
177
- const content = lodash_1.default.template(readTemplate('vue.component.partial'))({
178
- className: className,
179
- properties: componentProperties,
180
- events: componentEvents,
181
- classObjectDictionary,
182
- dependencies,
183
- blob,
184
- generateReturnType,
185
- generateMethodDeclaration,
186
- generateEventHandlerType,
187
- });
188
- const result = content.split('\n').filter(str => {
189
- return str.trim().length > 0;
190
- }).join('\n');
191
- return result;
177
+ interfaceLines.push('}');
178
+ return interfaceLines.join('\n');
192
179
  }
193
- function toVueTagName(className) {
180
+ function toVueTagName(rawClassName) {
181
+ const className = rawClassName.trim();
194
182
  if (className.startsWith('WebF')) {
195
183
  const withoutPrefix = className.substring(4);
196
- return 'web-f-' + lodash_1.default.kebabCase(withoutPrefix);
184
+ const suffix = lodash_1.default.kebabCase(withoutPrefix);
185
+ return suffix.length > 0 ? 'webf-' + suffix : 'webf';
197
186
  }
198
- else if (className.startsWith('Flutter')) {
187
+ if (className.startsWith('Flutter')) {
199
188
  const withoutPrefix = className.substring(7);
200
- return 'flutter-' + lodash_1.default.kebabCase(withoutPrefix);
189
+ const suffix = lodash_1.default.kebabCase(withoutPrefix);
190
+ return suffix.length > 0 ? 'flutter-' + suffix : 'flutter';
201
191
  }
202
- return lodash_1.default.kebabCase(className);
192
+ const kebab = lodash_1.default.kebabCase(className);
193
+ return kebab.replace(/^web-f-/, 'webf-');
203
194
  }
204
195
  function generateVueTypings(blobs) {
205
- const componentNames = blobs.map(blob => {
206
- const classObjects = blob.objects;
207
- const properties = classObjects.filter(object => {
208
- return object.name.endsWith('Properties');
209
- });
210
- const events = classObjects.filter(object => {
211
- return object.name.endsWith('Events');
212
- });
213
- const componentProperties = properties.length > 0 ? properties[0] : undefined;
214
- const componentEvents = events.length > 0 ? events[0] : undefined;
215
- const className = (() => {
216
- if (componentProperties) {
217
- return componentProperties.name.replace(/Properties$/, '');
218
- }
219
- if (componentEvents) {
220
- return componentEvents.name.replace(/Events$/, '');
221
- }
222
- return '';
223
- })();
224
- return className;
225
- }).filter(name => {
226
- return name.length > 0;
196
+ const componentSpecMap = new Map();
197
+ blobs
198
+ .flatMap(blob => getVueComponentSpecs(blob))
199
+ .forEach(spec => {
200
+ if (!componentSpecMap.has(spec.className))
201
+ componentSpecMap.set(spec.className, spec);
227
202
  });
228
- const components = blobs.map(blob => {
229
- return generateVueComponent(blob);
230
- }).filter(component => {
231
- return component.length > 0;
232
- }).join('\n\n');
203
+ const componentSpecs = Array.from(componentSpecMap.values()).sort((a, b) => a.className.localeCompare(b.className));
204
+ const componentNames = componentSpecs.map(spec => spec.className);
205
+ const components = componentSpecs.map(spec => {
206
+ const content = lodash_1.default.template(readTemplate('vue.component.partial'))({
207
+ className: spec.className,
208
+ properties: spec.properties,
209
+ events: spec.events,
210
+ methods: spec.methods,
211
+ generateReturnType,
212
+ generateMethodDeclaration,
213
+ generateEventHandlerType,
214
+ });
215
+ return content.split('\n').filter(str => str.trim().length > 0).join('\n');
216
+ }).filter(Boolean).join('\n\n');
233
217
  // Collect declare consts across blobs and render as exported ambient declarations
234
218
  const consts = blobs
235
219
  .flatMap(blob => blob.objects)
@@ -241,34 +225,55 @@ function generateVueTypings(blobs) {
241
225
  uniqueConsts.set(c.name, c);
242
226
  });
243
227
  const constDeclarations = Array.from(uniqueConsts.values())
228
+ .sort((a, b) => a.name.localeCompare(b.name))
244
229
  .map(c => `export declare const ${c.name}: ${c.type};`)
245
230
  .join('\n');
246
231
  // Collect declare enums across blobs
247
232
  const enums = blobs
248
233
  .flatMap(blob => blob.objects)
249
234
  .filter(obj => obj instanceof declaration_1.EnumObject);
250
- const enumDeclarations = enums.map(e => {
235
+ const uniqueEnums = new Map();
236
+ enums.forEach(e => {
237
+ if (!uniqueEnums.has(e.name))
238
+ uniqueEnums.set(e.name, e);
239
+ });
240
+ const enumDeclarations = Array.from(uniqueEnums.values())
241
+ .sort((a, b) => a.name.localeCompare(b.name))
242
+ .map(e => {
251
243
  const members = e.members.map(m => m.initializer ? `${m.name} = ${m.initializer}` : `${m.name}`).join(', ');
252
244
  return `export declare enum ${e.name} { ${members} }`;
253
- }).join('\n');
254
- // Compute relative import path from the generated typings file (index.d.ts at dist root)
255
- // to the aggregated React types module (src/types.ts) when present.
256
- let typesImportPath = './src/types';
257
- try {
258
- if (blobs.length > 0) {
259
- const distRoot = blobs[0].dist;
260
- const typingsDir = distRoot; // index.d.ts is written directly under distRoot
261
- const typesFilePath = path_1.default.join(distRoot, 'src', 'types');
262
- const rel = path_1.default.relative(typingsDir, typesFilePath).replace(/\\/g, '/');
263
- typesImportPath = rel.startsWith('.') ? rel : `./${rel}`;
264
- }
265
- }
266
- catch (_a) {
267
- typesImportPath = './src/types';
268
- }
269
- // Always import the types namespace to support typeof references
270
- const typesImport = `import * as __webfTypes from '${typesImportPath}';`;
271
- (0, logger_1.debug)(`[vue] Generating typings; importing types from ${typesImportPath}`);
245
+ })
246
+ .join('\n');
247
+ // Collect type aliases across blobs and render as exported declarations.
248
+ const typeAliases = blobs
249
+ .flatMap(blob => blob.objects)
250
+ .filter(obj => obj instanceof declaration_1.TypeAliasObject);
251
+ const uniqueTypeAliases = new Map();
252
+ typeAliases.forEach(t => {
253
+ if (!uniqueTypeAliases.has(t.name))
254
+ uniqueTypeAliases.set(t.name, t);
255
+ });
256
+ const typeAliasDeclarations = Array.from(uniqueTypeAliases.values())
257
+ .sort((a, b) => a.name.localeCompare(b.name))
258
+ .map(t => `export type ${t.name} = ${t.type};`)
259
+ .join('\n');
260
+ // Collect supporting interfaces (non-component interfaces) so referenced types exist.
261
+ const supportingInterfaces = blobs
262
+ .flatMap(blob => blob.objects)
263
+ .filter(obj => obj instanceof declaration_1.ClassObject);
264
+ const supporting = supportingInterfaces.filter(obj => {
265
+ return !obj.name.endsWith('Properties') && !obj.name.endsWith('Events') && !obj.name.endsWith('Methods');
266
+ });
267
+ const uniqueSupporting = new Map();
268
+ supporting.forEach(obj => {
269
+ if (!uniqueSupporting.has(obj.name))
270
+ uniqueSupporting.set(obj.name, obj);
271
+ });
272
+ const supportingDeclarations = Array.from(uniqueSupporting.values())
273
+ .sort((a, b) => a.name.localeCompare(b.name))
274
+ .map(obj => renderSupportingInterface(obj))
275
+ .filter(Boolean)
276
+ .join('\n\n');
272
277
  // Build mapping of template tag names to class names for GlobalComponents
273
278
  const componentMetas = componentNames.map(className => ({
274
279
  className,
@@ -284,7 +289,8 @@ function generateVueTypings(blobs) {
284
289
  components,
285
290
  consts: constDeclarations,
286
291
  enums: enumDeclarations,
287
- typesImport,
292
+ typeAliases: typeAliasDeclarations,
293
+ dependencies: supportingDeclarations,
288
294
  });
289
295
  return content.split('\n').filter(str => {
290
296
  return str.trim().length > 0;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openwebf/webf",
3
- "version": "0.23.10",
3
+ "version": "0.24.0",
4
4
  "description": "Command line tools for WebF",
5
5
  "main": "index.js",
6
6
  "bin": {
package/src/commands.ts CHANGED
@@ -4,6 +4,7 @@ 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';
@@ -237,6 +238,40 @@ function readFlutterPackageMetadata(packagePath: string): FlutterPackageMetadata
237
238
  }
238
239
  }
239
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
+
240
275
  // Copy markdown docs that match .d.ts basenames from source to the built dist folder,
241
276
  // and generate an aggregated README.md in the dist directory.
242
277
  async function copyMarkdownDocsToDist(params: {
@@ -441,7 +476,8 @@ function createCommand(target: string, options: { framework: string; packageName
441
476
  // Leave merge to the codegen step which appends exports safely
442
477
  }
443
478
 
444
- spawnSync(NPM, ['install'], {
479
+ // Ensure devDependencies are installed even if the user's shell has NODE_ENV=production.
480
+ spawnSync(NPM, ['install', '--production=false'], {
445
481
  cwd: target,
446
482
  stdio: 'inherit'
447
483
  });
@@ -463,12 +499,8 @@ function createCommand(target: string, options: { framework: string; packageName
463
499
  const gitignoreContent = _.template(gitignore)({});
464
500
  writeFileIfChanged(gitignorePath, gitignoreContent);
465
501
 
466
- spawnSync(NPM, ['install', '@openwebf/webf-enterprise-typings'], {
467
- cwd: target,
468
- stdio: 'inherit'
469
- });
470
-
471
- 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'], {
472
504
  cwd: target,
473
505
  stdio: 'inherit'
474
506
  });
@@ -781,6 +813,17 @@ async function generateCommand(distPath: string, options: GenerateOptions): Prom
781
813
  // Auto-initialize typings in the output directory if needed
782
814
  ensureInitialized(resolvedDistPath);
783
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
+
784
827
  console.log(`\nGenerating ${framework} code from ${options.flutterPackageSrc}...`);
785
828
 
786
829
  await dartGen({
@@ -1179,6 +1222,101 @@ async function buildPackage(packagePath: string): Promise<void> {
1179
1222
  const packageName = packageJson.name;
1180
1223
  const packageVersion = packageJson.version;
1181
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
+
1182
1320
  // Check if node_modules exists
1183
1321
  const nodeModulesPath = path.join(packagePath, 'node_modules');
1184
1322
  if (!fs.existsSync(nodeModulesPath)) {
@@ -1204,6 +1342,14 @@ async function buildPackage(packagePath: string): Promise<void> {
1204
1342
 
1205
1343
  // Check if package has a build script
1206
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
+
1207
1353
  console.log(`\nBuilding ${packageName}@${packageVersion}...`);
1208
1354
  const buildResult = spawnSync(NPM, ['run', 'build'], {
1209
1355
  cwd: packagePath,
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) {