[interop] Update Configuration to allow TS Compiler Configuration and non-exported decl generation (#443)

* added tsconfig.json support

* formatting and analyzing

* implemented ignoring errors

* add support for generating all declarations

* rewrote ts config parsing using ts functions (required)

Instead of manually marging objects to form compiler options, TS API requires using built-in functions to properly resolve the config into a compiler options objects, with enums, correct types, etc. Functions implemented were `parseJsonConfigFileContent` and `getParsedCommandLineOfConfigFile`

* formatting and doc for previous commit

* allowed for anonymous diagnostics and updated entrypoint

* formatting

* stray comment
diff --git a/web_generator/bin/gen_interop_bindings.dart b/web_generator/bin/gen_interop_bindings.dart
index 862624c..6d2df55 100644
--- a/web_generator/bin/gen_interop_bindings.dart
+++ b/web_generator/bin/gen_interop_bindings.dart
@@ -29,7 +29,7 @@
     return;
   }
 
-  if (argResult.rest.isEmpty) {
+  if (argResult.rest.isEmpty && !argResult.wasParsed('config')) {
     print('''
 ${ansi.lightRed.wrap('At least one argument is needed')}
 
@@ -58,25 +58,37 @@
     await compileDartMain();
   }
 
-  final inputFile = argResult.rest.first;
+  final inputFile = argResult.rest.firstOrNull;
   final outputFile = argResult['output'] as String? ??
-      p.join(p.current, inputFile.replaceAll('.d.ts', '.dart'));
+      p.join(p.current, inputFile?.replaceAll('.d.ts', '.dart'));
   final defaultWebGenConfigPath = p.join(p.current, 'webgen.yaml');
   final configFile = argResult['config'] as String? ??
       (File(defaultWebGenConfigPath).existsSync()
           ? defaultWebGenConfigPath
           : null);
+  final relativeConfigFile = configFile != null
+      ? p.relative(configFile, from: bindingsGeneratorPath)
+      : null;
   final relativeOutputPath =
       p.relative(outputFile, from: bindingsGeneratorPath);
+  final tsConfigPath = argResult['ts-config'] as String?;
+  final tsConfigRelativePath = tsConfigPath != null
+      ? p.relative(tsConfigPath, from: bindingsGeneratorPath)
+      : null;
   // Run app with `node`.
   await runProc(
     'node',
     [
       'main.mjs',
       '--declaration',
-      '--input=${p.relative(inputFile, from: bindingsGeneratorPath)}',
-      '--output=$relativeOutputPath',
-      if (configFile case final config?) '--config=$config'
+      if (argResult.rest.isNotEmpty) ...[
+        '--input=${p.relative(inputFile!, from: bindingsGeneratorPath)}',
+        '--output=$relativeOutputPath',
+      ],
+      if (tsConfigRelativePath case final tsConfig?) '--ts-config=$tsConfig',
+      if (relativeConfigFile case final config?) '--config=$config',
+      if (argResult.wasParsed('ignore-errors')) '--ignore-errors',
+      if (argResult.wasParsed('generate-all')) '--generate-all',
     ],
     workingDirectory: bindingsGeneratorPath,
   );
@@ -100,6 +112,14 @@
   ..addFlag('compile', defaultsTo: true)
   ..addOption('output',
       abbr: 'o', help: 'The output path to generate the Dart interface code')
+  ..addOption('ts-config',
+      help: 'Path to TS Configuration Options File (tsconfig.json) to pass'
+          ' to the parser/transformer')
+  ..addFlag('ignore-errors', help: 'Ignore Generator Errors', negatable: false)
+  ..addFlag('generate-all',
+      help: 'Generate all declarations '
+          '(including private declarations)',
+      negatable: false)
   ..addOption('config',
       hide: true,
       abbr: 'c',
diff --git a/web_generator/lib/src/config.dart b/web_generator/lib/src/config.dart
index 45620d2..a20227a 100644
--- a/web_generator/lib/src/config.dart
+++ b/web_generator/lib/src/config.dart
@@ -2,6 +2,8 @@
 // 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.
 
+import 'dart:convert';
+
 import 'package:dart_style/dart_style.dart';
 import 'package:path/path.dart' as p;
 import 'package:pub_semver/pub_semver.dart';
@@ -49,12 +51,31 @@
   /// If empty, all declarations will be generated by default
   List<String> get includedDeclarations;
 
+  /// An object consisting of TS Configurations from a tsconfig.json file
+  /// used for configuring the TypeScript Program/Compiler
+  Map<String, dynamic>? get tsConfig;
+
+  /// The TS Configuration file (tsconfig.json) if any
+  String? get tsConfigFile;
+
+  /// Whether to ignore source code warnings and errors
+  /// (they will still be printed)
+  bool get ignoreErrors;
+
+  /// Whether to generate code for all declarations, including non-exported
+  /// declarations
+  bool get generateAll;
+
   factory Config(
       {required List<String> input,
       required String output,
       required Version languageVersion,
       FunctionConfig? functions,
-      List<String> includedDeclarations}) = ConfigImpl._;
+      Map<String, dynamic>? tsConfig,
+      List<String> includedDeclarations,
+      bool generateAll,
+      bool ignoreErrors,
+      String? tsConfigFile}) = ConfigImpl._;
 }
 
 class ConfigImpl implements Config {
@@ -85,12 +106,28 @@
   @override
   List<String> includedDeclarations;
 
+  @override
+  Map<String, dynamic>? tsConfig;
+
+  @override
+  String? tsConfigFile;
+
+  @override
+  bool ignoreErrors;
+
+  @override
+  bool generateAll;
+
   ConfigImpl._(
       {required this.input,
       required this.output,
       required this.languageVersion,
       this.functions,
-      this.includedDeclarations = const []});
+      this.tsConfig,
+      this.includedDeclarations = const [],
+      this.ignoreErrors = false,
+      this.generateAll = false,
+      this.tsConfigFile});
 
   @override
   bool get singleFileOutput => input.length == 1;
@@ -127,6 +164,18 @@
   @override
   List<String> includedDeclarations;
 
+  @override
+  Map<String, dynamic>? tsConfig;
+
+  @override
+  String? tsConfigFile;
+
+  @override
+  bool ignoreErrors;
+
+  @override
+  bool generateAll;
+
   YamlConfig._(
       {required this.filename,
       required this.input,
@@ -136,7 +185,11 @@
       this.preamble,
       this.functions,
       this.includedDeclarations = const [],
-      String? languageVersion})
+      this.tsConfig,
+      this.tsConfigFile,
+      String? languageVersion,
+      this.ignoreErrors = false,
+      this.generateAll = false})
       : languageVersion = languageVersion == null
             ? DartFormatter.latestLanguageVersion
             : Version.parse(languageVersion);
@@ -159,6 +212,8 @@
     final allFiles =
         expandGlobs(inputFiles, extension: '.d.ts', cwd: p.dirname(filename));
 
+    final tsConfig = yaml['ts_config'] as YamlMap?;
+
     return YamlConfig._(
         filename: Uri.file(filename),
         input:
@@ -169,12 +224,17 @@
         description: yaml['description'] as String?,
         languageVersion: yaml['language_version'] as String?,
         preamble: yaml['preamble'] as String?,
-        // TODO: Can we consider using `json_serializable`?
+        tsConfig: tsConfig != null
+            ? jsonDecode(jsonEncode(tsConfig)) as Map<String, dynamic>
+            : null,
+        tsConfigFile: yaml['ts_config_file'] as String?,
         functions: FunctionConfig(
             varArgs: (yaml['functions'] as YamlMap?)?['varargs'] as int?),
         includedDeclarations: (yaml['include'] as YamlList?)
                 ?.map<String>((node) => node.toString())
                 .toList() ??
-            []);
+            [],
+        ignoreErrors: yaml['ignore_errors'] as bool? ?? false,
+        generateAll: yaml['generate_all'] as bool? ?? false);
   }
 }
diff --git a/web_generator/lib/src/dart_main.dart b/web_generator/lib/src/dart_main.dart
index a6d65ba..0b4cde2 100644
--- a/web_generator/lib/src/dart_main.dart
+++ b/web_generator/lib/src/dart_main.dart
@@ -54,12 +54,15 @@
         filename: filename,
       );
     } else {
+      final tsConfigFile = argResult['ts-config'] as String?;
       config = Config(
-        input:
-            expandGlobs(argResult['input'] as List<String>, extension: '.d.ts'),
-        output: argResult['output'] as String,
-        languageVersion: Version.parse(languageVersionString),
-      );
+          input: expandGlobs(argResult['input'] as List<String>,
+              extension: '.d.ts'),
+          output: argResult['output'] as String,
+          languageVersion: Version.parse(languageVersionString),
+          tsConfigFile: tsConfigFile,
+          ignoreErrors: argResult.wasParsed('ignore-errors'),
+          generateAll: argResult['generate-all'] as bool);
     }
 
     await generateJSInteropBindings(config);
@@ -68,7 +71,7 @@
 
 Future<void> generateJSInteropBindings(Config config) async {
   // generate
-  final jsDeclarations = parseDeclarationFiles(config.input);
+  final jsDeclarations = parseDeclarationFiles(config);
 
   // transform declarations
   final manager =
@@ -183,10 +186,16 @@
           '(directory for IDL, file for TS Declarations)')
   ..addFlag('generate-all',
       negatable: false,
-      help: '[IDL] Generate bindings for all IDL definitions, '
-          'including experimental and non-standard APIs.')
+      help: 'Generate bindings for all IDL/TS Declaration definitions, '
+          'including experimental and non-standard APIs (IDL) '
+          '/ non-exported APIs (TS Declarations).')
+  ..addOption('ts-config',
+      help: '[TS Declarations] Path to TS Configuration Options File '
+          '(tsconfig.json) to pass to the parser/transformer')
   ..addMultiOption('input',
       abbr: 'i',
       help: '[TS Declarations] The input file to read and generate types for')
+  ..addFlag('ignore-errors',
+      help: '[TS Declarations] Ignore Generator Errors', negatable: false)
   ..addOption('config',
       abbr: 'c', hide: true, valueHelp: '[file].yaml', help: 'Configuration');
diff --git a/web_generator/lib/src/interop_gen/parser.dart b/web_generator/lib/src/interop_gen/parser.dart
index aaf1f86..781ffc3 100644
--- a/web_generator/lib/src/interop_gen/parser.dart
+++ b/web_generator/lib/src/interop_gen/parser.dart
@@ -4,6 +4,9 @@
 
 import 'dart:js_interop';
 
+import 'package:path/path.dart' as p;
+
+import '../config.dart';
 import '../js/node.dart';
 import '../js/typescript.dart' as ts;
 
@@ -14,11 +17,60 @@
   ParserResult({required this.program, required this.files});
 }
 
-/// Parses the given TypeScript declaration [files], provides any diagnostics,
-/// if any, and generates a [ts.TSProgram] for transformation
-ParserResult parseDeclarationFiles(Iterable<String> files) {
-  final program = ts.createProgram(files.jsify() as JSArray<JSString>,
-      ts.TSCompilerOptions(declaration: true));
+/// Parses the given TypeScript declaration files in the [config],
+/// provides diagnostics if any, and generates a [ts.TSProgram]
+/// for transformation.
+///
+/// If a TS Config is passed, this function also produces compiler
+/// options from the TS config file/config object to use alongside the compiler
+ParserResult parseDeclarationFiles(Config config) {
+  final files = config.input;
+  final ignoreErrors = config.ignoreErrors;
+
+  // create host for parsing TS configuration
+  // TODO: @srujzs we can also create our own host
+  //  Do you think we should allow TS handle such functions,
+  //  or we should ourselves
+  final host = ts.sys;
+  var compilerOptions = ts.TSCompilerOptions(declaration: true);
+  if (config.tsConfigFile case final tsConfigFile?) {
+    final parsedCommandLine = ts.getParsedCommandLineOfConfigFile(
+        p.absolute(tsConfigFile),
+        ts.TSCompilerOptions(declaration: true),
+        host);
+
+    if (parsedCommandLine != null) {
+      compilerOptions = parsedCommandLine.options;
+
+      final diagnostics = parsedCommandLine.errors.toDart;
+
+      // handle any diagnostics
+      handleDiagnostics(diagnostics);
+      if (!ignoreErrors && diagnostics.isNotEmpty) {
+        exit(1);
+      }
+    }
+  } else if (config.tsConfig case final tsConfig?
+      when config.filename != null) {
+    final parsedCommandLine = ts.parseJsonConfigFileContent(
+        tsConfig.jsify() as JSObject,
+        host,
+        p.dirname(config.filename!.toFilePath()),
+        ts.TSCompilerOptions(declaration: true));
+
+    compilerOptions = parsedCommandLine.options;
+
+    final diagnostics = parsedCommandLine.errors.toDart;
+
+    // handle any diagnostics
+    handleDiagnostics(diagnostics);
+    if (!ignoreErrors && diagnostics.isNotEmpty) {
+      exit(1);
+    }
+  }
+
+  final program =
+      ts.createProgram(files.jsify() as JSArray<JSString>, compilerOptions);
 
   // get diagnostics
   final diagnostics = [
@@ -28,6 +80,17 @@
   ];
 
   // handle diagnostics
+  handleDiagnostics(diagnostics);
+
+  if (diagnostics.isNotEmpty && !ignoreErrors) {
+    // exit
+    exit(1);
+  }
+
+  return ParserResult(program: program, files: files);
+}
+
+void handleDiagnostics(List<ts.TSDiagnostic> diagnostics) {
   for (final diagnostic in diagnostics) {
     if (diagnostic.file case final diagnosticFile?) {
       final ts.TSLineAndCharacter(line: line, character: char) =
@@ -36,13 +99,10 @@
           ts.flattenDiagnosticMessageText(diagnostic.messageText, '\n');
       printErr('${diagnosticFile.fileName} '
           '(${line.toDartInt + 1},${char.toDartInt + 1}): $message');
+    } else {
+      final message =
+          ts.flattenDiagnosticMessageText(diagnostic.messageText, '\n');
+      printErr('(anonymous): $message');
     }
   }
-
-  if (diagnostics.isNotEmpty) {
-    // exit
-    exit(1);
-  }
-
-  return ParserResult(program: program, files: files);
 }
diff --git a/web_generator/lib/src/interop_gen/transform.dart b/web_generator/lib/src/interop_gen/transform.dart
index 62a0bca..f60bfde 100644
--- a/web_generator/lib/src/interop_gen/transform.dart
+++ b/web_generator/lib/src/interop_gen/transform.dart
@@ -146,8 +146,12 @@
 
   final List<String> filterDeclSet;
 
-  ProgramMap(this.program, List<String> files, {this.filterDeclSet = const []})
+  final bool generateAll;
+
+  ProgramMap(this.program, List<String> files,
+      {this.filterDeclSet = const [], bool? generateAll})
       : typeChecker = program.getTypeChecker(),
+        generateAll = generateAll ?? false,
         files = p.PathSet.of(files);
 
   /// Find the node definition for a given declaration named [declName]
@@ -207,7 +211,7 @@
                 src,
                 file: file,
               ));
-      if (sourceSymbol == null) {
+      if (sourceSymbol == null || generateAll) {
         // fallback to transforming each node
         // TODO: This is a temporary fix to running this with @types/web
         ts.forEachChild(
@@ -254,13 +258,14 @@
   ts.TSTypeChecker get typeChecker => programMap.typeChecker;
 
   TransformerManager(ts.TSProgram program, List<String> inputFiles,
-      {List<String> filterDeclSet = const []})
-      : programMap =
-            ProgramMap(program, inputFiles, filterDeclSet: filterDeclSet);
+      {List<String> filterDeclSet = const [], bool? generateAll})
+      : programMap = ProgramMap(program, inputFiles,
+            filterDeclSet: filterDeclSet, generateAll: generateAll);
 
   TransformerManager.fromParsedResults(ParserResult result, {Config? config})
       : programMap = ProgramMap(result.program, result.files.toList(),
-            filterDeclSet: config?.includedDeclarations ?? []);
+            filterDeclSet: config?.includedDeclarations ?? [],
+            generateAll: config?.generateAll);
 
   TransformResult transform() {
     final outputNodeMap = <String, NodeMap>{};
diff --git a/web_generator/lib/src/interop_gen/transform/transformer.dart b/web_generator/lib/src/interop_gen/transform/transformer.dart
index e2e3634..a026f22 100644
--- a/web_generator/lib/src/interop_gen/transform/transformer.dart
+++ b/web_generator/lib/src/interop_gen/transform/transformer.dart
@@ -82,6 +82,8 @@
   /// Get the current file handled by this transformer
   String get file => (_sourceFile?.fileName ?? _fileName)!;
 
+  bool get generateAll => programMap.generateAll;
+
   Transformer(this.programMap, this._sourceFile,
       {Set<String> exportSet = const {}, String? file})
       : exportSet = exportSet.map((e) => ExportReference(e, as: e)).toSet(),
@@ -1798,7 +1800,7 @@
       // get decls with `export` keyword
       switch (node) {
         case final ExportableDeclaration e:
-          if (e.exported &&
+          if ((e.exported || generateAll) &&
               (filterDeclSet.isEmpty ||
                   filterDeclSetPatterns
                       .any((pattern) => pattern.hasMatch(e.name)))) {
diff --git a/web_generator/lib/src/js/typescript.dart b/web_generator/lib/src/js/typescript.dart
index fd403b9..5e2bfe7 100644
--- a/web_generator/lib/src/js/typescript.dart
+++ b/web_generator/lib/src/js/typescript.dart
@@ -40,9 +40,94 @@
 external String flattenDiagnosticMessageText(JSAny? diag, String newLine,
     [int indent]);
 
+@JS()
+external TSParsedCommandLine? getParsedCommandLineOfConfigFile(
+    String configFileName,
+    TSCompilerOptions? optionsToExtend,
+    TSParseConfigFileHost host);
+
+@JS()
+external TSParsedCommandLine parseJsonConfigFileContent(
+    JSObject json, TSParseConfigFileHost host, String basePath,
+    [TSCompilerOptions existingOptions, String configFileName]);
+
+@JS()
+external TSParseConfigFileHost sys;
+
+@JS('ParsedCommandLine')
+extension type TSParsedCommandLine._(JSObject _) implements JSObject {
+  external TSCompilerOptions options;
+  external JSArray<TSDiagnostic> errors;
+}
+
+@JS('ParseConfigFileHost')
+extension type TSParseConfigFileHost._(JSObject _)
+    implements TSParseConfigHost {
+  external TSParseConfigFileHost({
+    FileExistsFunc fileExists,
+    ReadFileFunc readFile,
+    ReadDirectoryFunc readDirectory,
+    GetCurrentDirectoryFunc getCurrentDirectory,
+    OnUnRecoverableConfigFileDiagnosticFunc onUnRecoverableConfigFileDiagnostic,
+    bool useCaseSensitiveFileNames,
+  });
+
+  external String getCurrentDirectory();
+  @doNotStore
+  external JSAny onUnRecoverableConfigFileDiagnostic(TSDiagnostic diagnostic);
+}
+
+@JS('ParseConfigHost')
+extension type TSParseConfigHost._(JSObject _) implements JSObject {
+  // TODO: This would be a useful place to have the JSFunction generic
+  //  as the given constructor needs the object to be formed via closures/function tearoffs
+  external TSParseConfigHost({
+    FileExistsFunc fileExists,
+    ReadFileFunc readFile,
+    ReadDirectoryFunc readDirectory,
+    bool useCaseSensitiveFileNames,
+  });
+
+  external bool fileExists(String path);
+  external String? readFile(String path);
+  external JSArray<JSString> readDirectory(
+      String rootDir,
+      JSArray<JSString> extensions,
+      JSArray<JSString>? excludes,
+      JSArray<JSString> includes,
+      [int depth]);
+  external bool get useCaseSensitiveFileNames;
+}
+
+extension type FileExistsFunc(JSFunction _) implements JSFunction {
+  external bool call(String path);
+}
+
+extension type ReadFileFunc(JSFunction _) implements JSFunction {
+  external String? call(String path);
+}
+
+extension type ReadDirectoryFunc(JSFunction _) implements JSFunction {
+  external JSArray<JSString> call(String rootDir, JSArray<JSString> extensions,
+      JSArray<JSString>? excludes, JSArray<JSString> includes,
+      [int depth]);
+}
+
+extension type GetCurrentDirectoryFunc(JSFunction _) implements JSFunction {
+  external String call();
+}
+
+extension type OnUnRecoverableConfigFileDiagnosticFunc(JSFunction _)
+    implements JSFunction {
+  @doNotStore
+  external JSAny call(TSDiagnostic diagnostic);
+}
+
 @JS('CompilerOptions')
 extension type TSCompilerOptions._(JSObject _) implements JSObject {
   external TSCompilerOptions({bool? allowJs, bool? declaration});
+  factory TSCompilerOptions.fromJSObject(JSObject object) =>
+      TSCompilerOptions._(object);
   external bool? get allowJs;
   external bool? get declaration;
 }