[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());
+      });
+    }
+  });
+}