Skip to content
32 changes: 26 additions & 6 deletions web_generator/bin/gen_interop_bindings.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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')}

Expand Down Expand Up @@ -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,
);
Expand All @@ -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',
Expand Down
70 changes: 65 additions & 5 deletions web_generator/lib/src/config.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -49,12 +51,31 @@ abstract interface class Config {
/// 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 {
Expand Down Expand Up @@ -85,12 +106,28 @@ class ConfigImpl implements Config {
@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;
Expand Down Expand Up @@ -127,6 +164,18 @@ class YamlConfig implements Config {
@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,
Expand All @@ -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);
Expand All @@ -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:
Expand All @@ -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<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);
}
}
25 changes: 17 additions & 8 deletions web_generator/lib/src/dart_main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -54,12 +54,15 @@ void main(List<String> args) async {
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);
Expand All @@ -68,7 +71,7 @@ void main(List<String> args) async {

Future<void> generateJSInteropBindings(Config config) async {
// generate
final jsDeclarations = parseDeclarationFiles(config.input);
final jsDeclarations = parseDeclarationFiles(config);

// transform declarations
final manager =
Expand Down Expand Up @@ -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');
84 changes: 72 additions & 12 deletions web_generator/lib/src/interop_gen/parser.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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<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 = [
Expand All @@ -28,6 +80,17 @@ ParserResult parseDeclarationFiles(Iterable<String> files) {
];

// 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) =
Expand All @@ -36,13 +99,10 @@ ParserResult parseDeclarationFiles(Iterable<String> 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);
}
Loading