@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/README.md +28 -0
- package/bin/webf.js +12 -1
- package/dist/commands.js +375 -9
- package/dist/generator.js +39 -37
- package/dist/module.js +487 -0
- package/dist/peerDeps.js +27 -0
- package/dist/react.js +10 -18
- package/dist/vue.js +138 -117
- package/package.json +2 -2
- package/src/commands.ts +441 -11
- package/src/generator.ts +41 -40
- package/src/module.ts +632 -0
- package/src/peerDeps.ts +21 -0
- package/src/react.ts +10 -18
- package/src/vue.ts +158 -128
- package/templates/module.package.json.tpl +36 -0
- package/templates/module.tsconfig.json.tpl +25 -0
- package/templates/module.tsup.config.ts.tpl +13 -0
- package/templates/react.component.tsx.tpl +2 -2
- package/templates/react.index.ts.tpl +2 -1
- package/templates/react.package.json.tpl +4 -5
- 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 +4 -2
- package/test/commands.test.ts +86 -19
- package/test/generator.test.ts +33 -24
- 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/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 {
|
|
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 =
|
|
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
|
-
//
|
|
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
|
-
|
|
453
|
-
|
|
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 {
|
|
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 =
|
|
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
|
-
//
|
|
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) {
|