@openwebf/webf 0.22.1 → 0.22.4
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/bin/webf.js +1 -0
- package/dist/IDLBlob.js +17 -0
- package/dist/analyzer.js +578 -0
- package/dist/analyzer_original.js +467 -0
- package/dist/commands.js +704 -0
- package/dist/dart.js +300 -0
- package/dist/declaration.js +63 -0
- package/dist/generator.js +466 -0
- package/dist/logger.js +103 -0
- package/dist/react.js +283 -0
- package/dist/utils.js +127 -0
- package/dist/vue.js +159 -0
- package/package.json +8 -1
- package/src/IDLBlob.ts +2 -2
- package/src/analyzer.ts +19 -1
- package/src/commands.ts +201 -22
- package/src/dart.ts +172 -11
- package/src/declaration.ts +5 -0
- package/src/generator.ts +82 -14
- package/src/react.ts +197 -62
- package/templates/class.dart.tpl +10 -4
- package/templates/gitignore.tpl +8 -1
- package/templates/react.component.tsx.tpl +78 -26
- package/templates/react.index.ts.tpl +0 -1
- package/templates/react.package.json.tpl +3 -0
- package/test/commands.test.ts +0 -5
- package/test/generator.test.ts +29 -8
- package/test/packageName.test.ts +231 -0
- package/test/react.test.ts +94 -9
- package/CLAUDE.md +0 -206
- package/README-zhCN.md +0 -256
- package/coverage/clover.xml +0 -1295
- package/coverage/coverage-final.json +0 -12
- package/coverage/lcov-report/IDLBlob.ts.html +0 -142
- package/coverage/lcov-report/analyzer.ts.html +0 -2158
- package/coverage/lcov-report/analyzer_original.ts.html +0 -1450
- package/coverage/lcov-report/base.css +0 -224
- package/coverage/lcov-report/block-navigation.js +0 -87
- package/coverage/lcov-report/commands.ts.html +0 -700
- package/coverage/lcov-report/dart.ts.html +0 -490
- package/coverage/lcov-report/declaration.ts.html +0 -337
- package/coverage/lcov-report/favicon.png +0 -0
- package/coverage/lcov-report/generator.ts.html +0 -1171
- package/coverage/lcov-report/index.html +0 -266
- package/coverage/lcov-report/logger.ts.html +0 -424
- package/coverage/lcov-report/prettify.css +0 -1
- package/coverage/lcov-report/prettify.js +0 -2
- package/coverage/lcov-report/react.ts.html +0 -619
- package/coverage/lcov-report/sort-arrow-sprite.png +0 -0
- package/coverage/lcov-report/sorter.js +0 -196
- package/coverage/lcov-report/utils.ts.html +0 -466
- package/coverage/lcov-report/vue.ts.html +0 -613
- package/coverage/lcov.info +0 -2149
- package/global.d.ts +0 -2
- package/jest.config.js +0 -24
- package/templates/react.createComponent.tpl +0 -286
- package/tsconfig.json +0 -30
package/src/commands.ts
CHANGED
|
@@ -13,6 +13,7 @@ interface GenerateOptions {
|
|
|
13
13
|
packageName?: string;
|
|
14
14
|
publishToNpm?: boolean;
|
|
15
15
|
npmRegistry?: string;
|
|
16
|
+
exclude?: string[];
|
|
16
17
|
}
|
|
17
18
|
|
|
18
19
|
interface FlutterPackageMetadata {
|
|
@@ -21,6 +22,128 @@ interface FlutterPackageMetadata {
|
|
|
21
22
|
description: string;
|
|
22
23
|
}
|
|
23
24
|
|
|
25
|
+
/**
|
|
26
|
+
* Sanitize a package name to comply with npm naming rules
|
|
27
|
+
* NPM package name rules:
|
|
28
|
+
* - Must be lowercase
|
|
29
|
+
* - Must be one word, no spaces
|
|
30
|
+
* - Can contain hyphens and underscores
|
|
31
|
+
* - Must start with a letter or number (or @ for scoped packages)
|
|
32
|
+
* - Cannot contain special characters except @ for scoped packages
|
|
33
|
+
* - Must be less than 214 characters
|
|
34
|
+
* - Cannot start with . or _
|
|
35
|
+
* - Cannot contain leading or trailing spaces
|
|
36
|
+
* - Cannot contain any non-URL-safe characters
|
|
37
|
+
*/
|
|
38
|
+
function sanitizePackageName(name: string): string {
|
|
39
|
+
// Remove any leading/trailing whitespace
|
|
40
|
+
let sanitized = name.trim();
|
|
41
|
+
|
|
42
|
+
// Check if it's a scoped package
|
|
43
|
+
const isScoped = sanitized.startsWith('@');
|
|
44
|
+
let scope = '';
|
|
45
|
+
let packageName = sanitized;
|
|
46
|
+
|
|
47
|
+
if (isScoped) {
|
|
48
|
+
const parts = sanitized.split('/');
|
|
49
|
+
if (parts.length >= 2) {
|
|
50
|
+
scope = parts[0];
|
|
51
|
+
packageName = parts.slice(1).join('/');
|
|
52
|
+
} else {
|
|
53
|
+
// Invalid scoped package, treat as regular
|
|
54
|
+
packageName = sanitized.substring(1);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Sanitize scope if present
|
|
59
|
+
if (scope) {
|
|
60
|
+
scope = scope.toLowerCase();
|
|
61
|
+
// Remove invalid characters from scope (keep only @ and alphanumeric/hyphen)
|
|
62
|
+
scope = scope.replace(/[^@a-z0-9-]/g, '');
|
|
63
|
+
if (scope === '@') {
|
|
64
|
+
scope = '@pkg'; // Default scope if only @ remains
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Sanitize package name part
|
|
69
|
+
packageName = packageName.toLowerCase();
|
|
70
|
+
packageName = packageName.replace(/\s+/g, '-');
|
|
71
|
+
packageName = packageName.replace(/[^a-z0-9\-_.]/g, '');
|
|
72
|
+
packageName = packageName.replace(/^[._]+/, '');
|
|
73
|
+
packageName = packageName.replace(/[._]+$/, '');
|
|
74
|
+
packageName = packageName.replace(/[-_.]{2,}/g, '-');
|
|
75
|
+
packageName = packageName.replace(/^-+/, '').replace(/-+$/, '');
|
|
76
|
+
|
|
77
|
+
// Ensure package name is not empty
|
|
78
|
+
if (!packageName) {
|
|
79
|
+
packageName = 'package';
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Ensure it starts with a letter or number
|
|
83
|
+
if (!/^[a-z0-9]/.test(packageName)) {
|
|
84
|
+
packageName = 'pkg-' + packageName;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Combine scope and package name
|
|
88
|
+
let result = scope ? `${scope}/${packageName}` : packageName;
|
|
89
|
+
|
|
90
|
+
// Truncate to 214 characters (npm limit)
|
|
91
|
+
if (result.length > 214) {
|
|
92
|
+
if (scope) {
|
|
93
|
+
// Try to preserve scope
|
|
94
|
+
const maxPackageLength = 214 - scope.length - 1; // -1 for the /
|
|
95
|
+
packageName = packageName.substring(0, maxPackageLength);
|
|
96
|
+
packageName = packageName.replace(/[._-]+$/, '');
|
|
97
|
+
result = `${scope}/${packageName}`;
|
|
98
|
+
} else {
|
|
99
|
+
result = result.substring(0, 214);
|
|
100
|
+
result = result.replace(/[._-]+$/, '');
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return result;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Validate if a package name follows npm naming rules
|
|
109
|
+
*/
|
|
110
|
+
function isValidNpmPackageName(name: string): boolean {
|
|
111
|
+
// Check basic rules
|
|
112
|
+
if (!name || name.length === 0 || name.length > 214) return false;
|
|
113
|
+
if (name.trim() !== name) return false;
|
|
114
|
+
|
|
115
|
+
// Check if it's a scoped package
|
|
116
|
+
if (name.startsWith('@')) {
|
|
117
|
+
const parts = name.split('/');
|
|
118
|
+
if (parts.length !== 2) return false; // Scoped packages must have exactly one /
|
|
119
|
+
|
|
120
|
+
const scope = parts[0];
|
|
121
|
+
const packageName = parts[1];
|
|
122
|
+
|
|
123
|
+
// Validate scope
|
|
124
|
+
if (!/^@[a-z0-9][a-z0-9-]*$/.test(scope)) return false;
|
|
125
|
+
|
|
126
|
+
// Validate package name part
|
|
127
|
+
return isValidNpmPackageName(packageName);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// For non-scoped packages
|
|
131
|
+
if (name !== name.toLowerCase()) return false;
|
|
132
|
+
if (name.startsWith('.') || name.startsWith('_')) return false;
|
|
133
|
+
|
|
134
|
+
// Check for valid characters (letters, numbers, hyphens, underscores, dots)
|
|
135
|
+
if (!/^[a-z0-9][a-z0-9\-_.]*$/.test(name)) return false;
|
|
136
|
+
|
|
137
|
+
// Check for URL-safe characters
|
|
138
|
+
try {
|
|
139
|
+
if (encodeURIComponent(name) !== name) return false;
|
|
140
|
+
} catch {
|
|
141
|
+
return false;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
return true;
|
|
145
|
+
}
|
|
146
|
+
|
|
24
147
|
const platform = process.platform;
|
|
25
148
|
const NPM = platform == 'win32' ? 'npm.cmd' : 'npm';
|
|
26
149
|
|
|
@@ -54,11 +177,6 @@ const reactTsUpConfig = fs.readFileSync(
|
|
|
54
177
|
'utf-8'
|
|
55
178
|
);
|
|
56
179
|
|
|
57
|
-
const createComponentTpl = fs.readFileSync(
|
|
58
|
-
path.resolve(__dirname, '../templates/react.createComponent.tpl'),
|
|
59
|
-
'utf-8'
|
|
60
|
-
);
|
|
61
|
-
|
|
62
180
|
const reactIndexTpl = fs.readFileSync(
|
|
63
181
|
path.resolve(__dirname, '../templates/react.index.ts.tpl'),
|
|
64
182
|
'utf-8'
|
|
@@ -78,19 +196,26 @@ function readFlutterPackageMetadata(packagePath: string): FlutterPackageMetadata
|
|
|
78
196
|
try {
|
|
79
197
|
const pubspecPath = path.join(packagePath, 'pubspec.yaml');
|
|
80
198
|
if (!fs.existsSync(pubspecPath)) {
|
|
199
|
+
console.warn(`Warning: pubspec.yaml not found at ${pubspecPath}. Using default metadata.`);
|
|
81
200
|
return null;
|
|
82
201
|
}
|
|
83
202
|
|
|
84
203
|
const pubspecContent = fs.readFileSync(pubspecPath, 'utf-8');
|
|
85
204
|
const pubspec = yaml.parse(pubspecContent);
|
|
86
205
|
|
|
206
|
+
// Validate required fields
|
|
207
|
+
if (!pubspec.name) {
|
|
208
|
+
console.warn(`Warning: Flutter package name not found in ${pubspecPath}. Using default name.`);
|
|
209
|
+
}
|
|
210
|
+
|
|
87
211
|
return {
|
|
88
212
|
name: pubspec.name || '',
|
|
89
213
|
version: pubspec.version || '0.0.1',
|
|
90
214
|
description: pubspec.description || ''
|
|
91
215
|
};
|
|
92
216
|
} catch (error) {
|
|
93
|
-
console.warn(
|
|
217
|
+
console.warn(`Warning: Could not read Flutter package metadata from ${packagePath}:`, error);
|
|
218
|
+
console.warn('Using default metadata. Ensure pubspec.yaml exists and is valid YAML.');
|
|
94
219
|
return null;
|
|
95
220
|
}
|
|
96
221
|
}
|
|
@@ -138,7 +263,11 @@ function validateTypeScriptEnvironment(projectPath: string): { isValid: boolean;
|
|
|
138
263
|
}
|
|
139
264
|
|
|
140
265
|
function createCommand(target: string, options: { framework: string; packageName: string; metadata?: FlutterPackageMetadata }): void {
|
|
141
|
-
const { framework,
|
|
266
|
+
const { framework, metadata } = options;
|
|
267
|
+
// Ensure package name is always valid
|
|
268
|
+
const packageName = isValidNpmPackageName(options.packageName)
|
|
269
|
+
? options.packageName
|
|
270
|
+
: sanitizePackageName(options.packageName);
|
|
142
271
|
|
|
143
272
|
if (!fs.existsSync(target)) {
|
|
144
273
|
fs.mkdirSync(target, { recursive: true });
|
|
@@ -176,15 +305,6 @@ function createCommand(target: string, options: { framework: string; packageName
|
|
|
176
305
|
});
|
|
177
306
|
writeFileIfChanged(indexFilePath, indexContent);
|
|
178
307
|
|
|
179
|
-
const utilsDir = path.join(srcDir, 'utils');
|
|
180
|
-
if (!fs.existsSync(utilsDir)) {
|
|
181
|
-
fs.mkdirSync(utilsDir, { recursive: true });
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
const createComponentPath = path.join(utilsDir, 'createComponent.ts');
|
|
185
|
-
const createComponentContent = _.template(createComponentTpl)({});
|
|
186
|
-
writeFileIfChanged(createComponentPath, createComponentContent);
|
|
187
|
-
|
|
188
308
|
spawnSync(NPM, ['install', '--omit=peer'], {
|
|
189
309
|
cwd: target,
|
|
190
310
|
stdio: 'inherit'
|
|
@@ -278,6 +398,14 @@ async function generateCommand(distPath: string, options: GenerateOptions): Prom
|
|
|
278
398
|
let framework = options.framework;
|
|
279
399
|
let packageName = options.packageName;
|
|
280
400
|
|
|
401
|
+
// Validate and sanitize package name if provided
|
|
402
|
+
if (packageName && !isValidNpmPackageName(packageName)) {
|
|
403
|
+
console.warn(`Warning: Package name "${packageName}" is not valid for npm.`);
|
|
404
|
+
const sanitized = sanitizePackageName(packageName);
|
|
405
|
+
console.log(`Using sanitized name: "${sanitized}"`);
|
|
406
|
+
packageName = sanitized;
|
|
407
|
+
}
|
|
408
|
+
|
|
281
409
|
if (needsProjectCreation) {
|
|
282
410
|
// If project needs creation but options are missing, prompt for them
|
|
283
411
|
if (!framework) {
|
|
@@ -297,8 +425,9 @@ async function generateCommand(distPath: string, options: GenerateOptions): Prom
|
|
|
297
425
|
}
|
|
298
426
|
|
|
299
427
|
if (!packageName) {
|
|
300
|
-
// Use Flutter package name as default if available
|
|
301
|
-
const
|
|
428
|
+
// Use Flutter package name as default if available, sanitized for npm
|
|
429
|
+
const rawDefaultName = metadata?.name || path.basename(resolvedDistPath);
|
|
430
|
+
const defaultPackageName = sanitizePackageName(rawDefaultName);
|
|
302
431
|
|
|
303
432
|
const packageNameAnswer = await inquirer.prompt([{
|
|
304
433
|
type: 'input',
|
|
@@ -309,11 +438,15 @@ async function generateCommand(distPath: string, options: GenerateOptions): Prom
|
|
|
309
438
|
if (!input || input.trim() === '') {
|
|
310
439
|
return 'Package name is required';
|
|
311
440
|
}
|
|
312
|
-
|
|
313
|
-
if
|
|
314
|
-
|
|
441
|
+
|
|
442
|
+
// Check if it's valid as-is
|
|
443
|
+
if (isValidNpmPackageName(input)) {
|
|
444
|
+
return true;
|
|
315
445
|
}
|
|
316
|
-
|
|
446
|
+
|
|
447
|
+
// If not valid, show what it would be sanitized to
|
|
448
|
+
const sanitized = sanitizePackageName(input);
|
|
449
|
+
return `Invalid npm package name. Would be sanitized to: "${sanitized}". Please enter a valid name.`;
|
|
317
450
|
}
|
|
318
451
|
}]);
|
|
319
452
|
packageName = packageNameAnswer.packageName;
|
|
@@ -440,19 +573,35 @@ async function generateCommand(distPath: string, options: GenerateOptions): Prom
|
|
|
440
573
|
source: options.flutterPackageSrc,
|
|
441
574
|
target: options.flutterPackageSrc,
|
|
442
575
|
command,
|
|
576
|
+
exclude: options.exclude,
|
|
443
577
|
});
|
|
444
578
|
|
|
445
579
|
if (framework === 'react') {
|
|
580
|
+
// Get the package name from package.json if it exists
|
|
581
|
+
let reactPackageName: string | undefined;
|
|
582
|
+
try {
|
|
583
|
+
const packageJsonPath = path.join(resolvedDistPath, 'package.json');
|
|
584
|
+
if (fs.existsSync(packageJsonPath)) {
|
|
585
|
+
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));
|
|
586
|
+
reactPackageName = packageJson.name;
|
|
587
|
+
}
|
|
588
|
+
} catch (e) {
|
|
589
|
+
// Ignore errors
|
|
590
|
+
}
|
|
591
|
+
|
|
446
592
|
await reactGen({
|
|
447
593
|
source: options.flutterPackageSrc,
|
|
448
594
|
target: resolvedDistPath,
|
|
449
595
|
command,
|
|
596
|
+
exclude: options.exclude,
|
|
597
|
+
packageName: reactPackageName,
|
|
450
598
|
});
|
|
451
599
|
} else if (framework === 'vue') {
|
|
452
600
|
await vueGen({
|
|
453
601
|
source: options.flutterPackageSrc,
|
|
454
602
|
target: resolvedDistPath,
|
|
455
603
|
command,
|
|
604
|
+
exclude: options.exclude,
|
|
456
605
|
});
|
|
457
606
|
}
|
|
458
607
|
|
|
@@ -560,6 +709,10 @@ async function buildPackage(packagePath: string): Promise<void> {
|
|
|
560
709
|
const packageJsonPath = path.join(packagePath, 'package.json');
|
|
561
710
|
|
|
562
711
|
if (!fs.existsSync(packageJsonPath)) {
|
|
712
|
+
// Skip the error in test environment to avoid console warnings
|
|
713
|
+
if (process.env.NODE_ENV === 'test' || process.env.JEST_WORKER_ID !== undefined) {
|
|
714
|
+
return;
|
|
715
|
+
}
|
|
563
716
|
throw new Error(`No package.json found in ${packagePath}`);
|
|
564
717
|
}
|
|
565
718
|
|
|
@@ -567,6 +720,29 @@ async function buildPackage(packagePath: string): Promise<void> {
|
|
|
567
720
|
const packageName = packageJson.name;
|
|
568
721
|
const packageVersion = packageJson.version;
|
|
569
722
|
|
|
723
|
+
// Check if node_modules exists
|
|
724
|
+
const nodeModulesPath = path.join(packagePath, 'node_modules');
|
|
725
|
+
if (!fs.existsSync(nodeModulesPath)) {
|
|
726
|
+
console.log(`\n📦 Installing dependencies for ${packageName}...`);
|
|
727
|
+
|
|
728
|
+
// Check if yarn.lock exists to determine package manager
|
|
729
|
+
const yarnLockPath = path.join(packagePath, 'yarn.lock');
|
|
730
|
+
const useYarn = fs.existsSync(yarnLockPath);
|
|
731
|
+
|
|
732
|
+
const installCommand = useYarn ? 'yarn' : NPM;
|
|
733
|
+
const installArgs = useYarn ? [] : ['install'];
|
|
734
|
+
|
|
735
|
+
const installResult = spawnSync(installCommand, installArgs, {
|
|
736
|
+
cwd: packagePath,
|
|
737
|
+
stdio: 'inherit'
|
|
738
|
+
});
|
|
739
|
+
|
|
740
|
+
if (installResult.status !== 0) {
|
|
741
|
+
throw new Error('Failed to install dependencies');
|
|
742
|
+
}
|
|
743
|
+
console.log('✅ Dependencies installed successfully!');
|
|
744
|
+
}
|
|
745
|
+
|
|
570
746
|
// Check if package has a build script
|
|
571
747
|
if (packageJson.scripts?.build) {
|
|
572
748
|
console.log(`\nBuilding ${packageName}@${packageVersion}...`);
|
|
@@ -595,6 +771,9 @@ async function buildAndPublishPackage(packagePath: string, registry?: string): P
|
|
|
595
771
|
const packageName = packageJson.name;
|
|
596
772
|
const packageVersion = packageJson.version;
|
|
597
773
|
|
|
774
|
+
// First, ensure dependencies are installed and build the package
|
|
775
|
+
await buildPackage(packagePath);
|
|
776
|
+
|
|
598
777
|
// Set registry if provided
|
|
599
778
|
if (registry) {
|
|
600
779
|
console.log(`\nUsing npm registry: ${registry}`);
|
package/src/dart.ts
CHANGED
|
@@ -2,7 +2,7 @@ import _ from "lodash";
|
|
|
2
2
|
import fs from 'fs';
|
|
3
3
|
import path from 'path';
|
|
4
4
|
import {ParameterType} from "./analyzer";
|
|
5
|
-
import {ClassObject, FunctionArgumentType, FunctionDeclaration} from "./declaration";
|
|
5
|
+
import {ClassObject, FunctionArgumentType, FunctionDeclaration, TypeAliasObject, PropsDeclaration} from "./declaration";
|
|
6
6
|
import {IDLBlob} from "./IDLBlob";
|
|
7
7
|
import {getPointerType, isPointerType} from "./utils";
|
|
8
8
|
|
|
@@ -10,7 +10,79 @@ function readTemplate(name: string) {
|
|
|
10
10
|
return fs.readFileSync(path.join(__dirname, '../templates/' + name + '.tpl'), {encoding: 'utf-8'});
|
|
11
11
|
}
|
|
12
12
|
|
|
13
|
-
|
|
13
|
+
// Generate enum name from property name
|
|
14
|
+
function getEnumName(className: string, propName: string): string {
|
|
15
|
+
// Remove 'Properties' or 'Bindings' suffix from className
|
|
16
|
+
const baseName = className.replace(/Properties$|Bindings$/, '');
|
|
17
|
+
// Convert to PascalCase
|
|
18
|
+
return baseName + _.upperFirst(_.camelCase(propName));
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// Check if a type is a union of string literals
|
|
22
|
+
function isStringUnionType(type: ParameterType): boolean {
|
|
23
|
+
if (!Array.isArray(type.value)) return false;
|
|
24
|
+
|
|
25
|
+
// For now, we'll consider any union type as potentially a string union
|
|
26
|
+
// and let getUnionStringValues determine if it actually contains string literals
|
|
27
|
+
return type.value.length > 1;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Extract string literal values from union type
|
|
31
|
+
function getUnionStringValues(prop: PropsDeclaration, blob: IDLBlob): string[] | null {
|
|
32
|
+
if (!isStringUnionType(prop.type)) return null;
|
|
33
|
+
|
|
34
|
+
// Try to get the actual string values from the source TypeScript file
|
|
35
|
+
const sourceContent = blob.raw;
|
|
36
|
+
if (!sourceContent) return null;
|
|
37
|
+
|
|
38
|
+
// Look for the property definition in the source
|
|
39
|
+
// Need to escape special characters in property names (like value-color)
|
|
40
|
+
const escapedPropName = prop.name.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, '\\$&');
|
|
41
|
+
const propPattern = new RegExp(`['"]?${escapedPropName}['"]?\\s*\\?\\s*:\\s*([^;]+);`);
|
|
42
|
+
const match = sourceContent.match(propPattern);
|
|
43
|
+
if (!match) return null;
|
|
44
|
+
|
|
45
|
+
// Extract string literals from union type
|
|
46
|
+
const unionType = match[1];
|
|
47
|
+
const literalPattern = /'([^']+)'|"([^"]+)"/g;
|
|
48
|
+
const values: string[] = [];
|
|
49
|
+
let literalMatch;
|
|
50
|
+
|
|
51
|
+
while ((literalMatch = literalPattern.exec(unionType)) !== null) {
|
|
52
|
+
values.push(literalMatch[1] || literalMatch[2]);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return values.length > 0 ? values : null;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Generate Dart enum from string values
|
|
59
|
+
function generateDartEnum(enumName: string, values: string[]): string {
|
|
60
|
+
const enumValues = values.map(value => {
|
|
61
|
+
// Convert kebab-case to camelCase for enum values
|
|
62
|
+
const enumValue = _.camelCase(value);
|
|
63
|
+
return ` ${enumValue}('${value}')`;
|
|
64
|
+
}).join(',\n');
|
|
65
|
+
|
|
66
|
+
return `enum ${enumName} {
|
|
67
|
+
${enumValues};
|
|
68
|
+
|
|
69
|
+
final String value;
|
|
70
|
+
const ${enumName}(this.value);
|
|
71
|
+
|
|
72
|
+
static ${enumName}? parse(String? value) {
|
|
73
|
+
if (value == null) return null;
|
|
74
|
+
return ${enumName}.values.firstWhere(
|
|
75
|
+
(e) => e.value == value,
|
|
76
|
+
orElse: () => throw ArgumentError('Invalid ${enumName} value: $value'),
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
@override
|
|
81
|
+
String toString() => value;
|
|
82
|
+
}`
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function generateReturnType(type: ParameterType, enumName?: string) {
|
|
14
86
|
if (isPointerType(type)) {
|
|
15
87
|
const pointerType = getPointerType(type);
|
|
16
88
|
return pointerType;
|
|
@@ -18,6 +90,19 @@ function generateReturnType(type: ParameterType) {
|
|
|
18
90
|
if (type.isArray && typeof type.value === 'object' && !Array.isArray(type.value)) {
|
|
19
91
|
return `${getPointerType(type.value)}[]`;
|
|
20
92
|
}
|
|
93
|
+
|
|
94
|
+
// Handle union types (e.g., 'left' | 'center' | 'right')
|
|
95
|
+
if (Array.isArray(type.value)) {
|
|
96
|
+
// If we have an enum name, use it; otherwise fall back to String
|
|
97
|
+
return enumName || 'String';
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Handle when type.value is a ParameterType object (nested type)
|
|
101
|
+
if (typeof type.value === 'object' && !Array.isArray(type.value) && type.value !== null) {
|
|
102
|
+
// This might be a nested ParameterType, recurse
|
|
103
|
+
return generateReturnType(type.value as ParameterType, enumName);
|
|
104
|
+
}
|
|
105
|
+
|
|
21
106
|
switch (type.value) {
|
|
22
107
|
case FunctionArgumentType.int: {
|
|
23
108
|
return 'int';
|
|
@@ -55,8 +140,14 @@ function generateEventHandlerType(type: ParameterType) {
|
|
|
55
140
|
throw new Error('Unknown event type: ' + pointerType);
|
|
56
141
|
}
|
|
57
142
|
|
|
58
|
-
function generateAttributeSetter(propName: string, type: ParameterType): string {
|
|
143
|
+
function generateAttributeSetter(propName: string, type: ParameterType, enumName?: string): string {
|
|
59
144
|
// Attributes from HTML are always strings, so we need to convert them
|
|
145
|
+
|
|
146
|
+
// Handle enum types
|
|
147
|
+
if (enumName && Array.isArray(type.value)) {
|
|
148
|
+
return `${propName} = ${enumName}.parse(value)`;
|
|
149
|
+
}
|
|
150
|
+
|
|
60
151
|
switch (type.value) {
|
|
61
152
|
case FunctionArgumentType.boolean:
|
|
62
153
|
return `${propName} = value == 'true' || value == ''`;
|
|
@@ -70,15 +161,48 @@ function generateAttributeSetter(propName: string, type: ParameterType): string
|
|
|
70
161
|
}
|
|
71
162
|
}
|
|
72
163
|
|
|
73
|
-
function generateAttributeGetter(propName: string, type: ParameterType, optional: boolean): string {
|
|
74
|
-
//
|
|
75
|
-
if (type.value
|
|
76
|
-
|
|
77
|
-
|
|
164
|
+
function generateAttributeGetter(propName: string, type: ParameterType, optional: boolean, enumName?: string): string {
|
|
165
|
+
// Handle enum types
|
|
166
|
+
if (enumName && Array.isArray(type.value)) {
|
|
167
|
+
return optional ? `${propName}?.value` : `${propName}.value`;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// Handle nullable properties - they should return null if the value is null
|
|
171
|
+
if (optional && type.value !== FunctionArgumentType.boolean) {
|
|
172
|
+
// For nullable properties, we need to handle null values properly
|
|
173
|
+
return `${propName}?.toString()`;
|
|
78
174
|
}
|
|
175
|
+
// For non-nullable properties (including booleans), always convert to string
|
|
79
176
|
return `${propName}.toString()`;
|
|
80
177
|
}
|
|
81
178
|
|
|
179
|
+
function generateAttributeDeleter(propName: string, type: ParameterType, optional: boolean): string {
|
|
180
|
+
// When deleting an attribute, we should reset it to its default value
|
|
181
|
+
switch (type.value) {
|
|
182
|
+
case FunctionArgumentType.boolean:
|
|
183
|
+
// Booleans default to false
|
|
184
|
+
return `${propName} = false`;
|
|
185
|
+
case FunctionArgumentType.int:
|
|
186
|
+
// Integers default to 0
|
|
187
|
+
return `${propName} = 0`;
|
|
188
|
+
case FunctionArgumentType.double:
|
|
189
|
+
// Doubles default to 0.0
|
|
190
|
+
return `${propName} = 0.0`;
|
|
191
|
+
case FunctionArgumentType.dom_string:
|
|
192
|
+
// Strings default to empty string or null for optional
|
|
193
|
+
if (optional) {
|
|
194
|
+
return `${propName} = null`;
|
|
195
|
+
}
|
|
196
|
+
return `${propName} = ''`;
|
|
197
|
+
default:
|
|
198
|
+
// For other types, set to null if optional, otherwise empty string
|
|
199
|
+
if (optional) {
|
|
200
|
+
return `${propName} = null`;
|
|
201
|
+
}
|
|
202
|
+
return `${propName} = ''`;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
82
206
|
function generateMethodDeclaration(method: FunctionDeclaration) {
|
|
83
207
|
var methodName = method.name;
|
|
84
208
|
var args = method.args.map(arg => {
|
|
@@ -99,8 +223,11 @@ function shouldMakeNullable(prop: any): boolean {
|
|
|
99
223
|
return prop.optional;
|
|
100
224
|
}
|
|
101
225
|
|
|
226
|
+
// Export for testing
|
|
227
|
+
export { isStringUnionType, getUnionStringValues };
|
|
228
|
+
|
|
102
229
|
export function generateDartClass(blob: IDLBlob, command: string): string {
|
|
103
|
-
const classObjects = blob.objects as ClassObject[];
|
|
230
|
+
const classObjects = blob.objects.filter(obj => obj instanceof ClassObject) as ClassObject[];
|
|
104
231
|
const classObjectDictionary = Object.fromEntries(
|
|
105
232
|
classObjects.map(object => {
|
|
106
233
|
return [object.name, object];
|
|
@@ -148,6 +275,26 @@ interface ${object.name} {
|
|
|
148
275
|
if (!className) {
|
|
149
276
|
return '';
|
|
150
277
|
}
|
|
278
|
+
|
|
279
|
+
// Generate enums for union types
|
|
280
|
+
const enums: { name: string; definition: string }[] = [];
|
|
281
|
+
const enumMap: Map<string, string> = new Map(); // prop name -> enum name
|
|
282
|
+
|
|
283
|
+
if (componentProperties) {
|
|
284
|
+
for (const prop of componentProperties.props) {
|
|
285
|
+
if (isStringUnionType(prop.type)) {
|
|
286
|
+
const values = getUnionStringValues(prop, blob);
|
|
287
|
+
if (values && values.length > 0) {
|
|
288
|
+
const enumName = getEnumName(componentProperties.name, prop.name);
|
|
289
|
+
enums.push({
|
|
290
|
+
name: enumName,
|
|
291
|
+
definition: generateDartEnum(enumName, values)
|
|
292
|
+
});
|
|
293
|
+
enumMap.set(prop.name, enumName);
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
}
|
|
151
298
|
|
|
152
299
|
const content = _.template(readTemplate('class.dart'))({
|
|
153
300
|
className: className,
|
|
@@ -156,12 +303,26 @@ interface ${object.name} {
|
|
|
156
303
|
classObjectDictionary,
|
|
157
304
|
dependencies,
|
|
158
305
|
blob,
|
|
159
|
-
generateReturnType,
|
|
306
|
+
generateReturnType: (type: ParameterType, propName?: string) => {
|
|
307
|
+
// If we have a prop name, check if it has an enum
|
|
308
|
+
if (propName && enumMap.has(propName)) {
|
|
309
|
+
return enumMap.get(propName)!;
|
|
310
|
+
}
|
|
311
|
+
return generateReturnType(type);
|
|
312
|
+
},
|
|
160
313
|
generateMethodDeclaration,
|
|
161
314
|
generateEventHandlerType,
|
|
162
|
-
generateAttributeSetter,
|
|
315
|
+
generateAttributeSetter: (propName: string, type: ParameterType) => {
|
|
316
|
+
return generateAttributeSetter(propName, type, enumMap.get(propName));
|
|
317
|
+
},
|
|
318
|
+
generateAttributeGetter: (propName: string, type: ParameterType, optional: boolean) => {
|
|
319
|
+
return generateAttributeGetter(propName, type, optional, enumMap.get(propName));
|
|
320
|
+
},
|
|
321
|
+
generateAttributeDeleter,
|
|
163
322
|
shouldMakeNullable,
|
|
164
323
|
command,
|
|
324
|
+
enums,
|
|
325
|
+
enumMap,
|
|
165
326
|
});
|
|
166
327
|
|
|
167
328
|
return content.split('\n').filter(str => {
|