diff --git a/web_generator/bin/gen_interop_bindings.dart b/web_generator/bin/gen_interop_bindings.dart index 862624cf..6d2df550 100644 --- a/web_generator/bin/gen_interop_bindings.dart +++ b/web_generator/bin/gen_interop_bindings.dart @@ -29,7 +29,7 @@ $_usage'''); 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 @@ $_usage'''); 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 @@ final _parser = ArgParser() ..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 45620d29..a20227ac 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 @@ abstract interface class Config { /// If empty, all declarations will be generated by default List get includedDeclarations; + /// An object consisting of TS Configurations from a tsconfig.json file + /// used for configuring the TypeScript Program/Compiler + Map? 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 input, required String output, required Version languageVersion, FunctionConfig? functions, - List includedDeclarations}) = ConfigImpl._; + Map? tsConfig, + List includedDeclarations, + bool generateAll, + bool ignoreErrors, + String? tsConfigFile}) = ConfigImpl._; } class ConfigImpl implements Config { @@ -85,12 +106,28 @@ class ConfigImpl implements Config { @override List includedDeclarations; + @override + Map? 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 @@ class YamlConfig implements Config { @override List includedDeclarations; + @override + Map? tsConfig; + + @override + String? tsConfigFile; + + @override + bool ignoreErrors; + + @override + bool generateAll; + YamlConfig._( {required this.filename, required this.input, @@ -136,7 +185,11 @@ class YamlConfig implements Config { 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 @@ class YamlConfig implements Config { 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 @@ class YamlConfig implements Config { 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 + : null, + tsConfigFile: yaml['ts_config_file'] as String?, functions: FunctionConfig( varArgs: (yaml['functions'] as YamlMap?)?['varargs'] as int?), includedDeclarations: (yaml['include'] as YamlList?) ?.map((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 a6d65ba0..0b4cde26 100644 --- a/web_generator/lib/src/dart_main.dart +++ b/web_generator/lib/src/dart_main.dart @@ -54,12 +54,15 @@ void main(List args) async { filename: filename, ); } else { + final tsConfigFile = argResult['ts-config'] as String?; config = Config( - input: - expandGlobs(argResult['input'] as List, extension: '.d.ts'), - output: argResult['output'] as String, - languageVersion: Version.parse(languageVersionString), - ); + input: expandGlobs(argResult['input'] as List, + 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 @@ void main(List args) async { Future generateJSInteropBindings(Config config) async { // generate - final jsDeclarations = parseDeclarationFiles(config.input); + final jsDeclarations = parseDeclarationFiles(config); // transform declarations final manager = @@ -183,10 +186,16 @@ final _parser = ArgParser() '(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 aaf1f86c..781ffc37 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 @@ class ParserResult { 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 files) { - final program = ts.createProgram(files.jsify() as JSArray, - 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, compilerOptions); // get diagnostics final diagnostics = [ @@ -28,6 +80,17 @@ ParserResult parseDeclarationFiles(Iterable files) { ]; // handle diagnostics + handleDiagnostics(diagnostics); + + if (diagnostics.isNotEmpty && !ignoreErrors) { + // exit + exit(1); + } + + return ParserResult(program: program, files: files); +} + +void handleDiagnostics(List diagnostics) { for (final diagnostic in diagnostics) { if (diagnostic.file case final diagnosticFile?) { final ts.TSLineAndCharacter(line: line, character: char) = @@ -36,13 +99,10 @@ ParserResult parseDeclarationFiles(Iterable files) { 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 62a0bca5..f60bfde6 100644 --- a/web_generator/lib/src/interop_gen/transform.dart +++ b/web_generator/lib/src/interop_gen/transform.dart @@ -146,8 +146,12 @@ class ProgramMap { final List filterDeclSet; - ProgramMap(this.program, List files, {this.filterDeclSet = const []}) + final bool generateAll; + + ProgramMap(this.program, List 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 @@ class ProgramMap { 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 @@ class TransformerManager { ts.TSTypeChecker get typeChecker => programMap.typeChecker; TransformerManager(ts.TSProgram program, List inputFiles, - {List filterDeclSet = const []}) - : programMap = - ProgramMap(program, inputFiles, filterDeclSet: filterDeclSet); + {List 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 = {}; diff --git a/web_generator/lib/src/interop_gen/transform/transformer.dart b/web_generator/lib/src/interop_gen/transform/transformer.dart index d209f4d8..939af349 100644 --- a/web_generator/lib/src/interop_gen/transform/transformer.dart +++ b/web_generator/lib/src/interop_gen/transform/transformer.dart @@ -81,6 +81,8 @@ class Transformer { /// 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 exportSet = const {}, String? file}) : exportSet = exportSet.map((e) => ExportReference(e, as: e)).toSet(), @@ -1644,7 +1646,7 @@ class Transformer { // 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 fd403b91..5e2bfe70 100644 --- a/web_generator/lib/src/js/typescript.dart +++ b/web_generator/lib/src/js/typescript.dart @@ -40,9 +40,94 @@ external TSLineAndCharacter getLineAndCharacterOfPosition( 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 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 readDirectory( + String rootDir, + JSArray extensions, + JSArray? excludes, + JSArray 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 call(String rootDir, JSArray extensions, + JSArray? excludes, JSArray 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; }