[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;
}