[web_generator] Add Support for Parsing IDL Files (#377)
* integrated IDL single file parsing and integration testing
* modified interface idl input
* Completed support for Single IDL File Parsing
Fixes #374
* fixed suffix naming
* moved actual output from temp to `.dart_tool`
* made minor fixes
* Add newline to end of files
* Code resolution
* updated integration test suite
* Resolved all standing fixes to the integration test suite
* Update translator.dart
diff --git a/web_generator/bin/update_idl_bindings.dart b/web_generator/bin/update_idl_bindings.dart
index 019b422..1da5464 100644
--- a/web_generator/bin/update_idl_bindings.dart
+++ b/web_generator/bin/update_idl_bindings.dart
@@ -67,12 +67,17 @@
// Run app with `node`.
final generateAll = argResult['generate-all'] as bool;
+ final inputFiles = argResult['input'] as List<String>;
await runProc(
'node',
[
'main.mjs',
'--idl',
- '--output=${p.join(_webPackagePath, 'lib', 'src')}',
+ for (String inputFile in inputFiles) '--input=$inputFile',
+ if (inputFiles.isEmpty)
+ '--output=${p.join(_webPackagePath, 'lib', 'src')}'
+ else
+ '--output=${argResult['output'] as String? ?? p.current}',
if (generateAll) '--generate-all',
],
workingDirectory: bindingsGeneratorPath,
@@ -89,16 +94,17 @@
// delete context file
await contextFile.delete();
- // Update readme.
- final readmeFile =
- File(p.normalize(p.fromUri(Platform.script.resolve('../README.md'))));
+ if (inputFiles.isEmpty) {
+ // Update readme.
+ final readmeFile =
+ File(p.normalize(p.fromUri(Platform.script.resolve('../README.md'))));
- final sourceContent = readmeFile.readAsStringSync();
+ final sourceContent = readmeFile.readAsStringSync();
- final cssVersion = _packageLockVersion(_webRefCss);
- final elementsVersion = _packageLockVersion(_webRefElements);
- final idlVersion = _packageLockVersion(_webRefIdl);
- final versions = '''
+ final cssVersion = _packageLockVersion(_webRefCss);
+ final elementsVersion = _packageLockVersion(_webRefElements);
+ final idlVersion = _packageLockVersion(_webRefIdl);
+ final versions = '''
$_startComment
| Item | Version |
| --- | --: |
@@ -107,15 +113,16 @@
| `$_webRefIdl` | [$idlVersion](https://www.npmjs.com/package/$_webRefIdl/v/$idlVersion) |
''';
- final newContent =
- sourceContent.substring(0, sourceContent.indexOf(_startComment)) +
- versions +
- sourceContent.substring(sourceContent.indexOf(_endComment));
- if (newContent == sourceContent) {
- print(ansi.styleBold.wrap('No update for readme.'));
- } else {
- print(ansi.styleBold.wrap('Updating readme for IDL version $idlVersion'));
- readmeFile.writeAsStringSync(newContent, mode: FileMode.writeOnly);
+ final newContent =
+ sourceContent.substring(0, sourceContent.indexOf(_startComment)) +
+ versions +
+ sourceContent.substring(sourceContent.indexOf(_endComment));
+ if (newContent == sourceContent) {
+ print(ansi.styleBold.wrap('No update for readme.'));
+ } else {
+ print(ansi.styleBold.wrap('Updating readme for IDL version $idlVersion'));
+ readmeFile.writeAsStringSync(newContent, mode: FileMode.writeOnly);
+ }
}
}
@@ -161,23 +168,26 @@
'<!-- END updated by $_scriptPOSIXPath. Do not modify by hand -->';
final _usage = '''
-Global Options:
-${_parser.usage}
+${ansi.styleBold.wrap('WebIDL Gen')}:
+$_thisScript [options]
-${ansi.styleBold.wrap('IDL Command')}: $_thisScript idl [options]
+If no IDL file is provided, defaults to the WebIDL definitions needed for package:web
Usage:
-${_parser.commands['idl']?.usage}
-
-${ansi.styleBold.wrap('Typescript Gen Command')}: $_thisScript dts <.d.ts file> [options]
-
-Usage:
-${_parser.commands['dts']?.usage}''';
+${_parser.usage}''';
final _parser = ArgParser()
..addFlag('help', negatable: false, help: 'Show help information')
..addFlag('update', abbr: 'u', help: 'Update npm dependencies')
..addFlag('compile', defaultsTo: true)
+ ..addOption('output',
+ abbr: 'o',
+ help: 'Output directory where bindings will be generated to '
+ '(defaults to `lib/src` in the web package when no IDL file is provided)')
+ ..addMultiOption('input',
+ abbr: 'i',
+ help: 'The input IDL file(s) to read and generate bindings for. '
+ 'If not provided, the default WebIDL definitions will be used.')
..addFlag('generate-all',
negatable: false,
help: 'Generate bindings for all IDL definitions, including experimental '
diff --git a/web_generator/lib/src/dart_main.dart b/web_generator/lib/src/dart_main.dart
index d5de405..f6943a4 100644
--- a/web_generator/lib/src/dart_main.dart
+++ b/web_generator/lib/src/dart_main.dart
@@ -32,7 +32,10 @@
if (argResult.wasParsed('idl')) {
await generateIDLBindings(
- outputDirectory: argResult['output'] as String,
+ input: (argResult['input'] as List<String>).isEmpty
+ ? null
+ : argResult['input'] as Iterable<String>,
+ output: argResult['output'] as String,
generateAll: argResult['generate-all'] as bool,
languageVersion: Version.parse(languageVersionString),
);
@@ -72,22 +75,46 @@
}
Future<void> generateIDLBindings({
- required String outputDirectory,
+ Iterable<String>? input,
+ required String output,
required bool generateAll,
required Version languageVersion,
}) async {
- const librarySubDir = 'dom';
+ if (input == null) {
+ // parse dom library as normal
+ const librarySubDir = 'dom';
- ensureDirectoryExists('$outputDirectory/$librarySubDir');
+ ensureDirectoryExists('$output/$librarySubDir');
- final bindings = await generateBindings(packageRoot, librarySubDir,
- generateAll: generateAll);
- for (var entry in bindings.entries) {
- final libraryPath = entry.key;
- final library = entry.value;
+ final bindings = await generateBindings(packageRoot, librarySubDir,
+ generateAll: generateAll);
- final contents = _emitLibrary(library, languageVersion).toJS;
- fs.writeFileSync('$outputDirectory/$libraryPath'.toJS, contents);
+ for (var entry in bindings.entries) {
+ final libraryPath = entry.key;
+ final library = entry.value;
+
+ final contents = _emitLibrary(library, languageVersion).toJS;
+ fs.writeFileSync('$output/$libraryPath'.toJS, contents);
+ }
+ } else {
+ // parse individual files
+ ensureDirectoryExists(output);
+
+ final bindings = await generateBindingsForFiles({
+ for (final file in input)
+ file: (fs.readFileSync(
+ file.toJS, JSReadFileOptions(encoding: 'utf-8'.toJS))
+ as JSString)
+ .toDart
+ }, output);
+
+ for (var entry in bindings.entries) {
+ final libraryPath = entry.key;
+ final library = entry.value;
+
+ final contents = _emitLibrary(library, languageVersion).toJS;
+ fs.writeFileSync('$output/$libraryPath'.toJS, contents);
+ }
}
}
diff --git a/web_generator/lib/src/generate_bindings.dart b/web_generator/lib/src/generate_bindings.dart
index 36324f7..33f9361 100644
--- a/web_generator/lib/src/generate_bindings.dart
+++ b/web_generator/lib/src/generate_bindings.dart
@@ -4,6 +4,9 @@
import 'dart:js_interop';
+import 'package:path/path.dart' as p;
+
+import 'js/webidl2.dart' as webidl2;
import 'js/webidl_api.dart' as webidl;
import 'js/webref_css_api.dart';
import 'js/webref_elements_api.dart';
@@ -74,8 +77,8 @@
final cssStyleDeclarations = await _generateCSSStyleDeclarations();
final elementHTMLMap = await _generateElementTagMap();
final translator = Translator(
- packageRoot, librarySubDir, cssStyleDeclarations, elementHTMLMap,
- generateAll: generateAll);
+ librarySubDir, cssStyleDeclarations, elementHTMLMap,
+ generateAll: generateAll, packageRoot: packageRoot);
final array = objectEntries(await idl.parseAll().toDart);
for (var i = 0; i < array.length; i++) {
final entry = array[i] as JSArray<JSAny?>;
@@ -86,3 +89,21 @@
translator.addInterfacesAndNamespaces();
return translator.translate();
}
+
+Future<TranslationResult> generateBindingsForFiles(
+ Map<String, String> fileContents, String output) async {
+ // generate CSS style declarations and element tag map incase they are
+ // needed for the input files.
+ final cssStyleDeclarations = await _generateCSSStyleDeclarations();
+ final elementHTMLMap = await _generateElementTagMap();
+ final translator = Translator(output, cssStyleDeclarations, elementHTMLMap,
+ generateAll: true, generateForWeb: false);
+
+ for (final file in fileContents.entries) {
+ final ast = webidl2.parse(file.value);
+ translator.collect(p.basenameWithoutExtension(file.key), ast);
+ }
+
+ translator.addInterfacesAndNamespaces();
+ return translator.translate();
+}
diff --git a/web_generator/lib/src/js/webidl2.dart b/web_generator/lib/src/js/webidl2.dart
new file mode 100644
index 0000000..78e97de
--- /dev/null
+++ b/web_generator/lib/src/js/webidl2.dart
@@ -0,0 +1,12 @@
+// Copyright (c) 2025, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+@JS('webidl2')
+library;
+
+import 'dart:js_interop';
+
+import 'webidl_api.dart' as idl;
+
+@JS()
+external JSArray<idl.Node> parse(String contents);
diff --git a/web_generator/lib/src/main.mjs b/web_generator/lib/src/main.mjs
index 2355045..c1b7383 100644
--- a/web_generator/lib/src/main.mjs
+++ b/web_generator/lib/src/main.mjs
@@ -8,6 +8,7 @@
import * as css from '@webref/css';
import * as elements from '@webref/elements';
import * as idl from '@webref/idl';
+import * as webidl2 from "webidl2";
import * as ts from 'typescript';
const require = createRequire(import.meta.url);
@@ -19,6 +20,7 @@
globalThis.elements = elements;
globalThis.fs = fs;
globalThis.idl = idl;
+globalThis.webidl2 = webidl2;
globalThis.ts = ts;
globalThis.location = { href: `file://${process.cwd()}/` }
diff --git a/web_generator/lib/src/package-lock.json b/web_generator/lib/src/package-lock.json
index e6d9b3d..a83802b 100644
--- a/web_generator/lib/src/package-lock.json
+++ b/web_generator/lib/src/package-lock.json
@@ -13,10 +13,8 @@
"@webref/css": "^6.11.0",
"@webref/elements": "^2.2.2",
"@webref/idl": "^3.43.1",
- "typescript": "^5.8.3"
- },
- "devDependencies": {
- "webidl2": "^24.2.2"
+ "typescript": "^5.8.3",
+ "webidl2": "^24.4.1"
}
},
"node_modules/@mdn/browser-compat-data": {
diff --git a/web_generator/lib/src/package.json b/web_generator/lib/src/package.json
index 58ac69e..2b77220 100644
--- a/web_generator/lib/src/package.json
+++ b/web_generator/lib/src/package.json
@@ -13,9 +13,7 @@
"@webref/css": "^6.11.0",
"@webref/elements": "^2.2.2",
"@webref/idl": "^3.43.1",
- "typescript": "^5.8.3"
- },
- "devDependencies": {
- "webidl2": "^24.2.2"
+ "typescript": "^5.8.3",
+ "webidl2": "^24.4.1"
}
}
diff --git a/web_generator/lib/src/translator.dart b/web_generator/lib/src/translator.dart
index a47c588..2337a5f 100644
--- a/web_generator/lib/src/translator.dart
+++ b/web_generator/lib/src/translator.dart
@@ -648,10 +648,11 @@
}
class Translator {
- final String packageRoot;
+ final String? packageRoot;
final String _librarySubDir;
final List<String> _cssStyleDeclarations;
final Map<String, Set<String>> _elementTagMap;
+ final bool _generateForWeb;
final _libraries = <String, _Library>{};
final _typeToDeclaration = <String, idl.Node>{};
@@ -668,9 +669,10 @@
/// Singleton so that various helper methods can access info about the AST.
static Translator? instance;
- Translator(this.packageRoot, this._librarySubDir, this._cssStyleDeclarations,
- this._elementTagMap,
- {required bool generateAll}) {
+ Translator(
+ this._librarySubDir, this._cssStyleDeclarations, this._elementTagMap,
+ {this.packageRoot, required bool generateAll, bool generateForWeb = true})
+ : _generateForWeb = generateForWeb {
instance = this;
docProvider = DocProvider.create();
browserCompatData = BrowserCompatData.read(generateAll: generateAll);
@@ -815,7 +817,7 @@
final libraryPath = '$_librarySubDir/${shortName.kebabToSnake}.dart';
assert(!_libraries.containsKey(libraryPath));
- final library = _Library(shortName, '$packageRoot/$libraryPath');
+ final library = _Library(shortName, '${packageRoot ?? '.'}/$libraryPath');
for (var i = 0; i < ast.length; i++) {
library.add(ast[i]);
@@ -1117,8 +1119,7 @@
// from: https://github.com/w3c/webidl2.js/blob/main/README.md#default-and-const-values
final body = switch (constant.valueType) {
'string' => code.literalString((constant.value as JSString).toDart),
- 'boolean' => code.literalBool(
- (constant.value as JSString).toDart.toLowerCase() == 'true'),
+ 'boolean' => code.literalBool((constant.value as JSBoolean).toDart),
'number' =>
code.literalNum(num.parse((constant.value as JSString).toDart)),
'null' => code.literalNull,
@@ -1375,39 +1376,51 @@
];
}
- code.Library _library(_Library library) => code.Library((b) => b
- ..comments.addAll([
- ...licenseHeader,
- '',
- ...mozLicenseHeader,
- ])
- // TODO(https://github.com/dart-lang/sdk/issues/56450): Remove this once
- // this bug has been resolved.
- ..ignoreForFile.addAll([
- 'unintended_html_in_doc_comment',
- ])
- ..generatedByComment = generatedFileDisclaimer
- // TODO(srujzs): This is to address the issue around extension type object
- // literal constructors in https://github.com/dart-lang/sdk/issues/54801.
- // Once this package moves to an SDK version that contains a fix for that,
- // this can be removed.
- ..annotations.addAll(_jsOverride('', alwaysEmit: true))
- ..body.addAll([
- for (final typedef in library.typedefs.where(_usedTypes.contains))
- _typedef(typedef.name, _desugarTypedef(_RawType(typedef.name, false))!),
- for (final callback in library.callbacks.where(_usedTypes.contains))
- _typedef(
- callback.name, _desugarTypedef(_RawType(callback.name, false))!),
- for (final callbackInterface
- in library.callbackInterfaces.where(_usedTypes.contains))
- _typedef(callbackInterface.name,
- _desugarTypedef(_RawType(callbackInterface.name, false))!),
- for (final enum_ in library.enums.where(_usedTypes.contains))
- _typedef(enum_.name, _desugarTypedef(_RawType(enum_.name, false))!),
- for (final interfacelike
- in library.interfacelikes.where(_usedTypes.contains))
- ..._interfacelike(interfacelike),
- ]));
+ code.Library _library(_Library library) => code.Library((b) {
+ if (_generateForWeb) {
+ b.comments.addAll([
+ ...licenseHeader,
+ '',
+ ...mozLicenseHeader,
+ ]);
+ }
+ // TODO(https://github.com/dart-lang/sdk/issues/56450): Remove
+ // this once this bug has been resolved.
+ b
+ ..ignoreForFile.addAll([
+ 'unintended_html_in_doc_comment',
+ 'constant_identifier_names',
+ if (library.interfacelikes
+ .where((i) => i.type == 'namespace')
+ .isNotEmpty)
+ 'non_constant_identifier_names'
+ ])
+ ..generatedByComment = generatedFileDisclaimer
+ // TODO(srujzs): This is to address the issue around extension type
+ // object literal constructors in
+ // https://github.com/dart-lang/sdk/issues/54801.
+ // Once this package moves to an SDK version that contains a fix
+ // for that, this can be removed.
+ ..annotations.addAll(_jsOverride('', alwaysEmit: true))
+ ..body.addAll([
+ for (final typedef in library.typedefs.where(_usedTypes.contains))
+ _typedef(typedef.name,
+ _desugarTypedef(_RawType(typedef.name, false))!),
+ for (final callback in library.callbacks.where(_usedTypes.contains))
+ _typedef(callback.name,
+ _desugarTypedef(_RawType(callback.name, false))!),
+ for (final callbackInterface
+ in library.callbackInterfaces.where(_usedTypes.contains))
+ _typedef(callbackInterface.name,
+ _desugarTypedef(_RawType(callbackInterface.name, false))!),
+ for (final enum_ in library.enums.where(_usedTypes.contains))
+ _typedef(
+ enum_.name, _desugarTypedef(_RawType(enum_.name, false))!),
+ for (final interfacelike
+ in library.interfacelikes.where(_usedTypes.contains))
+ ..._interfacelike(interfacelike),
+ ]);
+ });
code.Library generateRootImport(Iterable<String> files) =>
code.Library((b) => b
@@ -1430,7 +1443,9 @@
}
}
- dartLibraries['dom.dart'] = generateRootImport(dartLibraries.keys);
+ if (_generateForWeb) {
+ dartLibraries['dom.dart'] = generateRootImport(dartLibraries.keys);
+ }
return dartLibraries;
}
diff --git a/web_generator/lib/src/type_aliases.dart b/web_generator/lib/src/type_aliases.dart
index a2bf791..eaa5f33 100644
--- a/web_generator/lib/src/type_aliases.dart
+++ b/web_generator/lib/src/type_aliases.dart
@@ -12,6 +12,7 @@
// Note that this is a special sentinel that doesn't actually exist in the set
// of JS types today (although this might in the future).
'undefined': 'JSUndefined',
+ 'void': 'JSVoid',
'Function': 'JSFunction',
'SharedArrayBuffer': 'JSObject',
@@ -69,5 +70,6 @@
// `Translator._typeReference` for more details.
'JSDouble': 'num',
'JSNumber': 'num',
+ 'JSVoid': 'void',
'JSUndefined': 'void',
};
diff --git a/web_generator/test/integration/idl/callbacks_expected.dart b/web_generator/test/integration/idl/callbacks_expected.dart
new file mode 100644
index 0000000..0021fa2
--- /dev/null
+++ b/web_generator/test/integration/idl/callbacks_expected.dart
@@ -0,0 +1,23 @@
+// Generated from Web IDL definitions.
+
+// ignore_for_file: constant_identifier_names, unintended_html_in_doc_comment
+
+@JS()
+library;
+
+import 'dart:js_interop';
+
+typedef VoidCallback = JSFunction;
+typedef StringCallback = JSFunction;
+typedef Comparator = JSFunction;
+typedef Transformer = JSFunction;
+typedef AsyncOperationCallback = JSFunction;
+extension type AsyncOperations._(JSObject _) implements JSObject {
+ external void performOperation(AsyncOperationCallback whenFinished);
+}
+extension type Processor._(JSObject _) implements JSObject {
+ external void run(VoidCallback onComplete);
+ external void compare(Comparator cmp);
+ external void stringManipulate(String string, StringCallback callback);
+ external void transform(JSAny? data, Transformer transformer);
+}
diff --git a/web_generator/test/integration/idl/callbacks_input.idl b/web_generator/test/integration/idl/callbacks_input.idl
new file mode 100644
index 0000000..b58be8a
--- /dev/null
+++ b/web_generator/test/integration/idl/callbacks_input.idl
@@ -0,0 +1,17 @@
+callback VoidCallback = void ();
+callback StringCallback = void (DOMString result);
+callback Comparator = long (DOMString a, DOMString b);
+callback Transformer = any (any input);
+callback AsyncOperationCallback = undefined (DOMString status);
+
+[Exposed=Window]
+interface AsyncOperations {
+ undefined performOperation(AsyncOperationCallback whenFinished);
+};
+
+interface Processor {
+ void run(VoidCallback onComplete);
+ void compare(Comparator cmp);
+ void stringManipulate(DOMString string, StringCallback callback);
+ void transform(any data, Transformer transformer);
+};
diff --git a/web_generator/test/integration/idl/constructors_expected.dart b/web_generator/test/integration/idl/constructors_expected.dart
new file mode 100644
index 0000000..1c95d0c
--- /dev/null
+++ b/web_generator/test/integration/idl/constructors_expected.dart
@@ -0,0 +1,38 @@
+// Generated from Web IDL definitions.
+
+// ignore_for_file: constant_identifier_names, unintended_html_in_doc_comment
+
+@JS()
+library;
+
+import 'dart:js_interop';
+
+extension type Shape._(JSObject _) implements JSObject {
+ external factory Shape();
+}
+extension type Done._(JSObject _) implements JSObject {}
+extension type Coordinate._(JSObject _) implements JSObject {
+ external int get x;
+ external set x(int value);
+ external int get y;
+ external set y(int value);
+}
+extension type DoneList._(JSObject _) implements JSObject {
+ external factory DoneList(int length);
+
+ external Done item(int index);
+ external int get length;
+}
+extension type Circle._(JSObject _) implements Shape, JSObject {
+ external factory Circle(num radius);
+
+ external static Coordinate triangulate(Circle c1, Circle c2, Circle c3);
+ external static int get triangulationCount;
+ external double get r;
+ external set r(num value);
+ external double get cx;
+ external set cx(num value);
+ external double get cy;
+ external set cy(num value);
+ external double get circumference;
+}
diff --git a/web_generator/test/integration/idl/constructors_input.idl b/web_generator/test/integration/idl/constructors_input.idl
new file mode 100644
index 0000000..dc91471
--- /dev/null
+++ b/web_generator/test/integration/idl/constructors_input.idl
@@ -0,0 +1,32 @@
+interface Shape {
+ constructor();
+};
+
+/// Wordplay around "Node"
+interface Done {
+
+};
+
+interface Coordinate {
+ attribute long x;
+ attribute long y;
+};
+
+[Exposed=Window]
+interface DoneList {
+ constructor(unsigned long length);
+ Done item(unsigned long index);
+ readonly attribute unsigned long length;
+};
+
+[Exposed=Window]
+interface Circle : Shape {
+ constructor(double radius);
+ attribute double r;
+ attribute double cx;
+ attribute double cy;
+ readonly attribute double circumference;
+
+ static readonly attribute long triangulationCount;
+ static Coordinate triangulate(Circle c1, Circle c2, Circle c3);
+};
diff --git a/web_generator/test/integration/idl/dictionaries_expected.dart b/web_generator/test/integration/idl/dictionaries_expected.dart
new file mode 100644
index 0000000..dd9b314
--- /dev/null
+++ b/web_generator/test/integration/idl/dictionaries_expected.dart
@@ -0,0 +1,45 @@
+// Generated from Web IDL definitions.
+
+// ignore_for_file: constant_identifier_names, unintended_html_in_doc_comment
+
+@JS()
+library;
+
+import 'dart:js_interop';
+
+extension type ConfigOptions._(JSObject _) implements JSObject {
+ external factory ConfigOptions({
+ required String endpoint,
+ bool useCache,
+ int timeout,
+ JSArray<JSString>? tags,
+ });
+
+ external String get endpoint;
+ external set endpoint(String value);
+ external bool get useCache;
+ external set useCache(bool value);
+ external int get timeout;
+ external set timeout(int value);
+ external JSArray<JSString>? get tags;
+ external set tags(JSArray<JSString>? value);
+}
+extension type ExtendedOptions._(JSObject _)
+ implements ConfigOptions, JSObject {
+ external factory ExtendedOptions({
+ required String endpoint,
+ bool useCache,
+ int timeout,
+ JSArray<JSString>? tags,
+ String? userToken,
+ String mode,
+ });
+
+ external String? get userToken;
+ external set userToken(String? value);
+ external String get mode;
+ external set mode(String value);
+}
+extension type Configurable._(JSObject _) implements JSObject {
+ external void applySettings([ExtendedOptions options]);
+}
diff --git a/web_generator/test/integration/idl/dictionaries_input.idl b/web_generator/test/integration/idl/dictionaries_input.idl
new file mode 100644
index 0000000..ed73f60
--- /dev/null
+++ b/web_generator/test/integration/idl/dictionaries_input.idl
@@ -0,0 +1,15 @@
+dictionary ConfigOptions {
+ required DOMString endpoint;
+ boolean useCache = true;
+ long timeout = 5000;
+ sequence<DOMString>? tags;
+};
+
+dictionary ExtendedOptions : ConfigOptions {
+ DOMString? userToken;
+ DOMString mode = "default";
+};
+
+interface Configurable {
+ void applySettings(optional ExtendedOptions options);
+};
diff --git a/web_generator/test/integration/idl/enum_expected.dart b/web_generator/test/integration/idl/enum_expected.dart
new file mode 100644
index 0000000..f0dff91
--- /dev/null
+++ b/web_generator/test/integration/idl/enum_expected.dart
@@ -0,0 +1,21 @@
+// Generated from Web IDL definitions.
+
+// ignore_for_file: constant_identifier_names, unintended_html_in_doc_comment
+
+@JS()
+library;
+
+import 'dart:js_interop';
+
+typedef LogLevel = String;
+typedef Direction = String;
+typedef DisplayMode = String;
+extension type Logger._(JSObject _) implements JSObject {
+ external void log(String message, [LogLevel level]);
+ external void logWithDirection(
+ String message, [
+ LogLevel level,
+ Direction dir,
+ ]);
+ external void logWithDisplayMode(String message, DisplayMode display);
+}
diff --git a/web_generator/test/integration/idl/enum_input.idl b/web_generator/test/integration/idl/enum_input.idl
new file mode 100644
index 0000000..5a5e291
--- /dev/null
+++ b/web_generator/test/integration/idl/enum_input.idl
@@ -0,0 +1,26 @@
+enum LogLevel {
+ "debug",
+ "info",
+ "warn",
+ "error"
+};
+
+enum Direction {
+ "up",
+ "down",
+ "left",
+ "right"
+};
+
+enum DisplayMode {
+ "fullscreen",
+ "standalone",
+ "minimal-ui",
+ "browser"
+};
+
+interface Logger {
+ void log(DOMString message, optional LogLevel level = "info");
+ void logWithDirection(DOMString message, optional LogLevel level = "info", optional Direction dir = "up");
+ void logWithDisplayMode(DOMString message, DisplayMode display);
+};
diff --git a/web_generator/test/integration/idl/indexers_expected.dart b/web_generator/test/integration/idl/indexers_expected.dart
new file mode 100644
index 0000000..4ee2c64
--- /dev/null
+++ b/web_generator/test/integration/idl/indexers_expected.dart
@@ -0,0 +1,16 @@
+// Generated from Web IDL definitions.
+
+// ignore_for_file: constant_identifier_names, unintended_html_in_doc_comment
+
+@JS()
+library;
+
+import 'dart:js_interop';
+
+extension type OrderedMap._(JSObject _) implements JSObject {
+ external JSAny? operator [](int index);
+ external void operator []=(int index, JSAny? value);
+ external JSAny? get(String name);
+ external void set(String name, JSAny? value);
+ external int get size;
+}
diff --git a/web_generator/test/integration/idl/indexers_input.idl b/web_generator/test/integration/idl/indexers_input.idl
new file mode 100644
index 0000000..a0145b5
--- /dev/null
+++ b/web_generator/test/integration/idl/indexers_input.idl
@@ -0,0 +1,9 @@
+interface OrderedMap {
+ readonly attribute unsigned long size;
+
+ getter any (unsigned long index);
+ setter undefined (unsigned long index, any value);
+
+ getter any get(DOMString name);
+ setter undefined set(DOMString name, any value);
+};
diff --git a/web_generator/test/integration/idl/interfaces_expected.dart b/web_generator/test/integration/idl/interfaces_expected.dart
new file mode 100644
index 0000000..07d9ec2
--- /dev/null
+++ b/web_generator/test/integration/idl/interfaces_expected.dart
@@ -0,0 +1,61 @@
+// Generated from Web IDL definitions.
+
+// ignore_for_file: constant_identifier_names, unintended_html_in_doc_comment
+
+@JS()
+library;
+
+import 'dart:js_interop';
+
+typedef LogLevel = String;
+extension type ConfigOptions._(JSObject _) implements JSObject {}
+extension type EmptyInterface._(JSObject _) implements JSObject {}
+extension type BaseInterface._(JSObject _) implements JSObject {
+ external void f();
+ external void g();
+}
+extension type Paint._(JSObject _) implements JSObject {}
+extension type SolidColor._(JSObject _) implements Paint, JSObject {
+ external double get red;
+ external set red(num value);
+ external double get green;
+ external set green(num value);
+ external double get blue;
+ external set blue(num value);
+}
+extension type MyInterface._(JSObject _) implements JSObject {
+ external void doSomething();
+ external bool isReady();
+ external JSPromise<JSString> saveData([ConfigOptions config]);
+ external void log(String message, [LogLevel level]);
+ external String operator [](int index);
+ external String get name;
+ external set name(String value);
+ external int get id;
+}
+extension type MySubInterface._(JSObject _) implements JSObject {
+ external void reset();
+ external String get info;
+ external set info(String value);
+}
+extension type MyException._(JSObject _) implements JSObject {
+ external factory MyException([String message, String name]);
+
+ external String get name;
+ external String get message;
+ external int get code;
+}
+extension type ProtocolXError._(JSObject _) implements MyException, JSObject {
+ external factory ProtocolXError(
+ ProtocolXErrorOptions options, [
+ String message,
+ ]);
+
+ external int get errorCode;
+}
+extension type ProtocolXErrorOptions._(JSObject _) implements JSObject {
+ external factory ProtocolXErrorOptions({required int errorCode});
+
+ external int get errorCode;
+ external set errorCode(int value);
+}
diff --git a/web_generator/test/integration/idl/interfaces_input.idl b/web_generator/test/integration/idl/interfaces_input.idl
new file mode 100644
index 0000000..16864a9
--- /dev/null
+++ b/web_generator/test/integration/idl/interfaces_input.idl
@@ -0,0 +1,66 @@
+enum LogLevel {
+ "debug",
+ "info",
+ "warn",
+ "error"
+};
+
+
+interface ConfigOptions {
+
+};
+
+interface EmptyInterface { };
+
+interface BaseInterface {
+ undefined f();
+ undefined g();
+};
+
+interface Paint { };
+
+interface SolidColor : Paint {
+ attribute double red;
+ attribute double green;
+ attribute double blue;
+};
+
+interface MyInterface {
+ attribute DOMString name;
+ readonly attribute unsigned long id;
+
+ void doSomething();
+ boolean isReady();
+
+ Promise<DOMString> saveData(optional ConfigOptions config);
+
+ void log(DOMString message);
+ void log(DOMString message, LogLevel level);
+
+ getter DOMString (unsigned long index);
+};
+
+interface MySubInterface {
+ attribute DOMString info;
+ void reset();
+};
+
+[Exposed=*,
+ Serializable]
+interface MyException { // but see below note about JavaScript binding
+ constructor(optional DOMString message = "", optional DOMString name = "Error");
+ readonly attribute DOMString name;
+ readonly attribute DOMString message;
+ readonly attribute unsigned short code;
+};
+
+[Exposed=Window, Serializable]
+interface ProtocolXError : MyException {
+ constructor(optional DOMString message = "", ProtocolXErrorOptions options);
+
+ readonly attribute unsigned long long errorCode;
+};
+
+dictionary ProtocolXErrorOptions {
+ required [EnforceRange] unsigned long long errorCode;
+};
diff --git a/web_generator/test/integration/idl/methods_expected.dart b/web_generator/test/integration/idl/methods_expected.dart
new file mode 100644
index 0000000..699b495
--- /dev/null
+++ b/web_generator/test/integration/idl/methods_expected.dart
@@ -0,0 +1,34 @@
+// Generated from Web IDL definitions.
+
+// ignore_for_file: constant_identifier_names, unintended_html_in_doc_comment
+
+@JS()
+library;
+
+import 'dart:js_interop';
+
+extension type MyMethodExamples._(JSObject _) implements JSObject {
+ external static bool isValid(String candidate);
+ external void reset();
+ external void configure([bool force, String? label]);
+ external void log(
+ String message, [
+ JSAny? extra1,
+ JSAny? extra2,
+ JSAny? extra3,
+ JSAny? extra4,
+ ]);
+ external void update(JSAny keyOrKeys, [String value]);
+ external JSPromise<JSString> fetchRemoteValue([String? endpoint]);
+ external void init();
+}
+extension type Dimensions._(JSObject _) implements JSObject {
+ external int get width;
+ external set width(int value);
+ external int get height;
+ external set height(int value);
+}
+extension type Button._(JSObject _) implements JSObject {
+ external bool isMouseOver();
+ external void setDimensions(JSAny sizeOrWidth, [int height]);
+}
diff --git a/web_generator/test/integration/idl/methods_input.idl b/web_generator/test/integration/idl/methods_input.idl
new file mode 100644
index 0000000..6e97649
--- /dev/null
+++ b/web_generator/test/integration/idl/methods_input.idl
@@ -0,0 +1,34 @@
+interface MyMethodExamples {
+ void reset();
+
+ void configure(optional boolean force = false, optional DOMString? label = null);
+
+ void log(DOMString message, any... extras);
+
+ void update(DOMString key, DOMString value);
+ void update(sequence<DOMString> keys);
+
+ Promise<DOMString> fetchRemoteValue(optional DOMString? endpoint);
+
+ static boolean isValid(DOMString candidate);
+
+ [Deprecated="Use configure() instead."]
+ void init();
+};
+
+[Exposed=Window]
+interface Dimensions {
+ attribute unsigned long width;
+ attribute unsigned long height;
+};
+
+[Exposed=Window]
+interface Button {
+
+ // An operation that takes no arguments and returns a boolean.
+ boolean isMouseOver();
+
+ // Overloaded operations.
+ undefined setDimensions(Dimensions size);
+ undefined setDimensions(unsigned long width, unsigned long height);
+};
diff --git a/web_generator/test/integration/idl/mixin_interfaces_expected.dart b/web_generator/test/integration/idl/mixin_interfaces_expected.dart
new file mode 100644
index 0000000..fcc2315
--- /dev/null
+++ b/web_generator/test/integration/idl/mixin_interfaces_expected.dart
@@ -0,0 +1,17 @@
+// Generated from Web IDL definitions.
+
+// ignore_for_file: constant_identifier_names, unintended_html_in_doc_comment
+
+@JS()
+library;
+
+import 'dart:js_interop';
+
+extension type Position._(JSObject _) implements JSObject {
+ external void somethingMixedIn();
+ external void moveTo(num newX, num newY);
+ external double get x;
+ external set x(num value);
+ external double get y;
+ external set y(num value);
+}
diff --git a/web_generator/test/integration/idl/mixin_interfaces_input.idl b/web_generator/test/integration/idl/mixin_interfaces_input.idl
new file mode 100644
index 0000000..cf4d22d
--- /dev/null
+++ b/web_generator/test/integration/idl/mixin_interfaces_input.idl
@@ -0,0 +1,23 @@
+interface mixin EventTargetMixin {
+ void addEventListener(DOMString type, EventListener callback, optional boolean capture = false);
+ void removeEventListener(DOMString type, EventListener callback, optional boolean capture = false);
+ boolean dispatchEvent(Event event);
+};
+
+interface mixin PositionMixin {
+ attribute double x;
+ attribute double y;
+ void moveTo(double newX, double newY);
+};
+
+interface Position {};
+
+Position includes PositionMixin;
+
+interface mixin MyMixin {
+ void somethingMixedIn();
+};
+
+partial interface mixin Position {
+ void somethingMixedIn();
+};
diff --git a/web_generator/test/integration/idl/namespaces_expected.dart b/web_generator/test/integration/idl/namespaces_expected.dart
new file mode 100644
index 0000000..520b157
--- /dev/null
+++ b/web_generator/test/integration/idl/namespaces_expected.dart
@@ -0,0 +1,28 @@
+// Generated from Web IDL definitions.
+
+// ignore_for_file: constant_identifier_names, non_constant_identifier_names
+// ignore_for_file: unintended_html_in_doc_comment
+
+@JS()
+library;
+
+import 'dart:js_interop';
+
+typedef Comparator = JSFunction;
+typedef LogLevel = String;
+extension type ConfigOptions._(JSObject _) implements JSObject {}
+@JS()
+external $MyLibrary get MyLibrary;
+@JS('MyLibrary')
+extension type $MyLibrary._(JSObject _) implements JSObject {
+ static const int VERSION_MAJOR = 1;
+
+ static const int VERSION_MINOR = 4;
+
+ external void initialize();
+ external String stringify(JSObject input);
+ external JSPromise<JSAny?> fetchResource(String url);
+ external void log(String message, [LogLevel level]);
+ external ConfigOptions getDefaultConfig();
+ external void forEach(JSArray<JSString> items, Comparator compareFn);
+}
diff --git a/web_generator/test/integration/idl/namespaces_input.idl b/web_generator/test/integration/idl/namespaces_input.idl
new file mode 100644
index 0000000..d8f1929
--- /dev/null
+++ b/web_generator/test/integration/idl/namespaces_input.idl
@@ -0,0 +1,27 @@
+enum LogLevel {
+ "debug",
+ "info",
+ "warn",
+ "error"
+};
+
+callback Comparator = long (DOMString a, DOMString b);
+
+interface ConfigOptions { };
+
+[Exposed=Window]
+namespace MyLibrary {
+ const unsigned short VERSION_MAJOR = 1;
+ const unsigned short VERSION_MINOR = 4;
+
+ void initialize();
+ DOMString stringify(object input);
+ Promise<any> fetchResource(DOMString url);
+
+ void log(DOMString message);
+ void log(DOMString message, LogLevel level);
+
+ ConfigOptions getDefaultConfig();
+
+ void forEach(sequence<DOMString> items, Comparator compareFn);
+};
diff --git a/web_generator/test/integration/idl/properties_expected.dart b/web_generator/test/integration/idl/properties_expected.dart
new file mode 100644
index 0000000..d2f5dca
--- /dev/null
+++ b/web_generator/test/integration/idl/properties_expected.dart
@@ -0,0 +1,29 @@
+// Generated from Web IDL definitions.
+
+// ignore_for_file: constant_identifier_names, unintended_html_in_doc_comment
+
+@JS()
+library;
+
+import 'dart:js_interop';
+
+extension type PropertyTest._(JSObject _) implements JSObject {
+ static const bool DEBUG = false;
+
+ external static int get testCount;
+ external String operator [](JSAny indexOrName);
+ external void operator []=(int index, String value);
+ external String get title;
+ external set title(String value);
+ external int get createdAt;
+ external String? get optionalName;
+ external set optionalName(String? value);
+ external JSArray<JSString> get tags;
+ external set tags(JSArray<JSString> value);
+ external JSUint8Array get binaryData;
+ external set binaryData(JSUint8Array value);
+ external JSAny? get flexibleValue;
+ external set flexibleValue(JSAny? value);
+ external String get testInfo;
+ external set testInfo(String value);
+}
diff --git a/web_generator/test/integration/idl/properties_input.idl b/web_generator/test/integration/idl/properties_input.idl
new file mode 100644
index 0000000..64ebe3a
--- /dev/null
+++ b/web_generator/test/integration/idl/properties_input.idl
@@ -0,0 +1,25 @@
+interface PropertyTest {
+ const boolean DEBUG = false;
+
+ attribute DOMString title;
+
+ static readonly attribute long testCount;
+
+ readonly attribute unsigned long createdAt;
+
+ attribute DOMString? optionalName;
+
+ attribute sequence<DOMString> tags;
+ attribute Uint8Array binaryData;
+
+ attribute (DOMString or long)? flexibleValue;
+
+ getter DOMString (unsigned long index);
+ setter void (unsigned long index, DOMString value);
+
+ getter DOMString (DOMString name);
+
+ deleter boolean (DOMString name);
+
+ stringifier attribute DOMString testInfo;
+};
diff --git a/web_generator/test/integration/idl/typedefs_expected.dart b/web_generator/test/integration/idl/typedefs_expected.dart
new file mode 100644
index 0000000..c3a7e27
--- /dev/null
+++ b/web_generator/test/integration/idl/typedefs_expected.dart
@@ -0,0 +1,16 @@
+// Generated from Web IDL definitions.
+
+// ignore_for_file: constant_identifier_names, unintended_html_in_doc_comment
+
+@JS()
+library;
+
+import 'dart:js_interop';
+
+typedef ArrayBufferView = JSObject;
+typedef BufferSource = JSObject;
+typedef Timestamp = int;
+extension type DataHandler._(JSObject _) implements JSObject {
+ external void setInput(BufferSource data);
+ external Timestamp getLastUpdated();
+}
diff --git a/web_generator/test/integration/idl/typedefs_input.idl b/web_generator/test/integration/idl/typedefs_input.idl
new file mode 100644
index 0000000..11d13f4
--- /dev/null
+++ b/web_generator/test/integration/idl/typedefs_input.idl
@@ -0,0 +1,15 @@
+typedef (Int8Array or Int16Array or Int32Array or
+ Uint8Array or Uint16Array or Uint32Array or Uint8ClampedArray or
+ BigInt64Array or BigUint64Array or
+ Float16Array or Float32Array or Float64Array or DataView) ArrayBufferView;
+
+typedef (ArrayBuffer or ArrayBufferView) BufferSource;
+typedef (DOMString or USVString) StringInput;
+typedef sequence<DOMString> StringList;
+typedef unsigned long long Timestamp;
+
+
+interface DataHandler {
+ void setInput(BufferSource data);
+ Timestamp getLastUpdated();
+};
diff --git a/web_generator/test/integration/idl/types_expected.dart b/web_generator/test/integration/idl/types_expected.dart
new file mode 100644
index 0000000..c582396
--- /dev/null
+++ b/web_generator/test/integration/idl/types_expected.dart
@@ -0,0 +1,18 @@
+// Generated from Web IDL definitions.
+
+// ignore_for_file: constant_identifier_names, unintended_html_in_doc_comment
+
+@JS()
+library;
+
+import 'dart:js_interop';
+
+extension type Global._(JSObject _) implements JSObject {
+ static const bool DEBUG = false;
+
+ static const int LF = 10;
+
+ static const int BIT_MASK = 64512;
+
+ static const double AVOGADRO = 6.022e+23;
+}
diff --git a/web_generator/test/integration/idl/types_input.idl b/web_generator/test/integration/idl/types_input.idl
new file mode 100644
index 0000000..e5cd371
--- /dev/null
+++ b/web_generator/test/integration/idl/types_input.idl
@@ -0,0 +1,6 @@
+interface Global {
+ const boolean DEBUG = false;
+ const octet LF = 10;
+ const unsigned long BIT_MASK = 0x0000fc00;
+ const double AVOGADRO = 6.022e23;
+};
diff --git a/web_generator/test/integration/idl_test.dart b/web_generator/test/integration/idl_test.dart
new file mode 100644
index 0000000..7aa6b9b
--- /dev/null
+++ b/web_generator/test/integration/idl_test.dart
@@ -0,0 +1,72 @@
+@TestOn('vm')
+library;
+
+import 'dart:io';
+
+import 'package:path/path.dart' as p;
+import 'package:test/test.dart';
+import 'package:web_generator/src/cli.dart';
+
+/// Actual test output can be found in `.dart_tool/idl`
+void main() {
+ final bindingsGenPath = p.join('lib', 'src');
+ group('IDL Integration Test', () {
+ final testGenFolder = p.join('test', 'integration', 'idl');
+ final inputDir = Directory(testGenFolder);
+
+ setUpAll(() async {
+ // set up npm
+ await runProc('npm', ['install'], workingDirectory: bindingsGenPath);
+
+ // compile file
+ await compileDartMain(dir: bindingsGenPath);
+ });
+
+ for (final inputFile in inputDir
+ .listSync()
+ .whereType<File>()
+ .where((f) => p.basenameWithoutExtension(f.path).endsWith('_input'))) {
+ final inputFileName = p.basenameWithoutExtension(inputFile.path);
+ final inputName = inputFileName.replaceFirst('_input', '');
+
+ final outputActualPath =
+ p.join('.dart_tool', 'idl', '${inputName}_actual.dart');
+ final outputExpectedPath =
+ p.join(testGenFolder, '${inputName}_expected.dart');
+
+ test(inputName, () async {
+ final inputFilePath = p.relative(inputFile.path, from: bindingsGenPath);
+ final outFilePath = p.relative(outputActualPath, from: bindingsGenPath);
+ // run the entrypoint
+ await runProc(
+ 'node',
+ [
+ 'main.mjs',
+ '--input=$inputFilePath',
+ '--output=${p.dirname(outFilePath)}',
+ '--idl'
+ ],
+ workingDirectory: bindingsGenPath);
+
+ await File(
+ p.join(p.dirname(outputActualPath), '${inputName}_input.dart'))
+ .rename(outputActualPath);
+
+ // read files
+ final expectedOutput = await File(outputExpectedPath).readAsString();
+ final actualOutput = await File(outputActualPath).readAsString();
+
+ expect(actualOutput, expectedOutput);
+ });
+
+ tearDownAll(() {
+ inputDir
+ .listSync()
+ .whereType<File>()
+ .where(
+ (f) => p.basenameWithoutExtension(f.path).endsWith('_actual'))
+ .forEach((f) => f.deleteSync());
+ });
+ }
+ });
+}