diff --git a/web_generator/lib/src/ast.dart b/web_generator/lib/src/ast.dart new file mode 100644 index 00000000..14a43a64 --- /dev/null +++ b/web_generator/lib/src/ast.dart @@ -0,0 +1,169 @@ +// 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. + +import 'package:code_builder/code_builder.dart'; + +import 'interop_gen/generate.dart'; +import 'interop_gen/namer.dart'; + +sealed class Node { + abstract final String? name; + abstract final ID id; + final String? dartName; + + Node() : dartName = null; +} + +abstract class Declaration extends Node { + @override + abstract final String name; + + Spec emit(); +} + +abstract class NamedDeclaration extends Declaration { + ReferredType asReferredType([List? typeArgs]) => + ReferredType(name: name, declaration: this, typeParams: typeArgs ?? []); +} + +abstract interface class ExportableDeclaration extends Declaration { + /// Whether this declaration is exported. + bool get exported; +} + +abstract class Type extends Node { + Reference emit(); +} + +enum PrimitiveType implements Type { + string('string'), + any('any'), + object('object'), + number('number'), + boolean('boolean'), + undefined('undefined'), + unknown('unknown'); + + const PrimitiveType(this.name); + + @override + final String name; + + @override + ID get id => ID(type: 'type', name: name); + + // TODO(https://github.com/dart-lang/web/pull/386): Configuration options: double and num + @override + Reference emit() { + return switch (this) { + PrimitiveType.string => refer('String'), + PrimitiveType.any => refer('JSAny', 'dart:js_interop'), + PrimitiveType.object => refer('JSObject', 'dart:js_interop'), + PrimitiveType.number => refer('int'), + PrimitiveType.boolean => refer('bool'), + PrimitiveType.undefined => TypeReference((t) => t + ..symbol = 'JSAny' + ..url = 'dart:js_interop' + ..isNullable = true), + PrimitiveType.unknown => TypeReference((t) => t + ..symbol = 'JSAny' + ..url = 'dart:js_interop' + ..isNullable = true) + }; + } + + @override + String? get dartName => null; +} + +// TODO(): Refactor name - not all types can be referred to +// (only specific types) Instead change this +// to represent `typeof` declarations. +// TODO(): Create a shared type for such types that +// can be referred to (i.e namespace, interface, class) +// as a type `ReferrableDeclaration`. +class ReferredType extends Type { + @override + String name; + + @override + ID get id => ID(type: 'type', name: name); + + T declaration; + + List typeParams; + + ReferredType( + {required this.name, + required this.declaration, + this.typeParams = const []}); + + @override + Reference emit() { + // TODO: implement emit + throw UnimplementedError(); + } +} + +// TODO(https://github.com/dart-lang/web/issues/385): Implement Support for UnionType (including implementing `emit`) +class UnionType extends Type { + List types; + + UnionType({required this.types}); + + @override + ID get id => ID(type: 'type', name: types.map((t) => t.id).join('|')); + + @override + Reference emit() { + throw UnimplementedError(); + } + + @override + String? get name => null; +} + +class VariableDeclaration extends NamedDeclaration + implements ExportableDeclaration { + /// The variable modifier, as represented in TypeScript + VariableModifier modifier; + + @override + String name; + + Type type; + + @override + bool exported; + + VariableDeclaration( + {required this.name, + required this.type, + required this.modifier, + required this.exported}); + + @override + ID get id => ID(type: 'var', name: name); + + @override + Spec emit() { + if (modifier == VariableModifier.$const) { + return Method((m) => m + ..name = name + ..type = MethodType.getter + ..annotations.add(generateJSAnnotation()) + ..external = true + ..returns = type.emit()); + } else { + // getter and setter -> single variable + return Field((f) => f + ..external = true + ..name = name + ..type = type.emit() + ..annotations.add(generateJSAnnotation())); + } + } +} + +enum VariableModifier { let, $const, $var } diff --git a/web_generator/lib/src/banned_names.dart b/web_generator/lib/src/banned_names.dart index 19bdbe4c..700b913c 100644 --- a/web_generator/lib/src/banned_names.dart +++ b/web_generator/lib/src/banned_names.dart @@ -2,6 +2,75 @@ // 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. +/// Dart reserved keywords, used for resolving conflict with a name. +/// +/// Source: https://dart.dev/guides/language/language-tour#keywords. +const keywords = { + 'abstract', + 'as', + 'assert', + 'async', + 'await', + 'break', + 'case', + 'catch', + 'class', + 'const', + 'continue', + 'covariant', + 'default', + 'deferred', + 'do', + 'dynamic', + 'else', + 'enum', + 'export', + 'extends', + 'extension', + 'external', + 'factory', + 'false', + 'final', + 'finally', + 'for', + 'Function', + 'get', + 'hide', + 'if', + 'implements', + 'import', + 'in', + 'interface', + 'is', + 'late', + 'library', + 'mixin', + 'new', + 'null', + 'on', + 'operator', + 'part', + 'required', + 'rethrow', + 'return', + 'set', + 'show', + 'static', + 'super', + 'switch', + 'sync', + 'this', + 'throw', + 'true', + 'try', + 'typedef', + 'var', + 'void', + 'while', + 'with', + 'yield', +}; + const bannedNames = { 'assert', 'break', diff --git a/web_generator/lib/src/cli.dart b/web_generator/lib/src/cli.dart index af517b77..b81a69da 100644 --- a/web_generator/lib/src/cli.dart +++ b/web_generator/lib/src/cli.dart @@ -14,7 +14,7 @@ import 'package:path/path.dart' as p; final bindingsGeneratorPath = p.fromUri(Platform.script.resolve('../lib/src')); -Future compileDartMain({String? langVersion}) async { +Future compileDartMain({String? langVersion, String? dir}) async { await runProc( Platform.executable, [ @@ -27,7 +27,7 @@ Future compileDartMain({String? langVersion}) async { '-o', 'dart_main.js', ], - workingDirectory: bindingsGeneratorPath, + workingDirectory: dir ?? bindingsGeneratorPath, ); } diff --git a/web_generator/lib/src/dart_main.dart b/web_generator/lib/src/dart_main.dart index e0e44808..d5de4053 100644 --- a/web_generator/lib/src/dart_main.dart +++ b/web_generator/lib/src/dart_main.dart @@ -7,11 +7,12 @@ import 'dart:js_interop'; import 'package:args/args.dart'; import 'package:code_builder/code_builder.dart' as code; import 'package:dart_style/dart_style.dart'; +import 'package:path/path.dart' as p; import 'package:pub_semver/pub_semver.dart'; -import 'dts/parser.dart'; -import 'dts/transform.dart'; import 'generate_bindings.dart'; +import 'interop_gen/parser.dart'; +import 'interop_gen/transform.dart'; import 'js/filesystem_api.dart'; import 'util.dart'; @@ -44,7 +45,7 @@ void main(List args) async { } } -// TODO(nikeokoronkwo): Add support for configuration +// TODO(https://github.com/dart-lang/web/issues/376): Add support for configuration Future generateJSInteropBindings({ required Iterable inputs, required String output, @@ -54,13 +55,20 @@ Future generateJSInteropBindings({ final jsDeclarations = parseDeclarationFiles(inputs); // transform declarations - final dartDeclarations = transformDeclarations(jsDeclarations); + final dartDeclarations = transform(jsDeclarations); // generate - final generatedCode = dartDeclarations.generate(); - - // write code to file - fs.writeFileSync(output.toJS, generatedCode.toJS); + final generatedCodeMap = dartDeclarations.generate(); + + // write code to file(s) + if (inputs.length == 1) { + final singleEntry = generatedCodeMap.entries.single; + fs.writeFileSync(output.toJS, singleEntry.value.toJS); + } else { + for (final entry in generatedCodeMap.entries) { + fs.writeFileSync(p.join(output, entry.key).toJS, entry.value.toJS); + } + } } Future generateIDLBindings({ diff --git a/web_generator/lib/src/dts/parser.dart b/web_generator/lib/src/dts/parser.dart deleted file mode 100644 index 07c3838f..00000000 --- a/web_generator/lib/src/dts/parser.dart +++ /dev/null @@ -1,6 +0,0 @@ -// 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. - -// TODO(nikeokoronkwo): Implement this function -dynamic parseDeclarationFiles(Iterable files) {} diff --git a/web_generator/lib/src/dts/config.dart b/web_generator/lib/src/interop_gen/config.dart similarity index 100% rename from web_generator/lib/src/dts/config.dart rename to web_generator/lib/src/interop_gen/config.dart diff --git a/web_generator/lib/src/dts/transform.dart b/web_generator/lib/src/interop_gen/generate.dart similarity index 53% rename from web_generator/lib/src/dts/transform.dart rename to web_generator/lib/src/interop_gen/generate.dart index 400e6049..54ce2d3f 100644 --- a/web_generator/lib/src/dts/transform.dart +++ b/web_generator/lib/src/interop_gen/generate.dart @@ -2,12 +2,9 @@ // 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. -class DeclarationMap { - String generate() { - throw UnimplementedError(); - } -} +import 'package:code_builder/code_builder.dart'; -DeclarationMap transformDeclarations(Object? jsDeclarations) { - throw UnimplementedError(); +Expression generateJSAnnotation([String? name]) { + return refer('JS', 'dart:js_interop') + .call([if (name != null) literalString(name)]); } diff --git a/web_generator/lib/src/interop_gen/namer.dart b/web_generator/lib/src/interop_gen/namer.dart new file mode 100644 index 00000000..3725c11e --- /dev/null +++ b/web_generator/lib/src/interop_gen/namer.dart @@ -0,0 +1,37 @@ +// 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. + +class ID { + final String type; + final String name; + final int? index; + + const ID({required this.type, required this.name, this.index}); + + bool get isUnnamed => name == 'unnamed'; + + @override + String toString() => '$type#$name${index != null ? '#$index' : ''}'; +} + +class UniqueNamer { + final Set _usedNames; + + UniqueNamer([Iterable used = const []]) + : _usedNames = used.toSet(); + + static ID parse(String id) { + String? index; + final [type, name, ...ids] = id.split('#'); + if (ids.isEmpty) index = ids.single; + + return ID( + type: type, name: name, index: index == null ? null : int.parse(index)); + } + + /// Adds a [name] to used names. + void markUsed(String name) { + _usedNames.add(name); + } +} diff --git a/web_generator/lib/src/interop_gen/parser.dart b/web_generator/lib/src/interop_gen/parser.dart new file mode 100644 index 00000000..8a65c099 --- /dev/null +++ b/web_generator/lib/src/interop_gen/parser.dart @@ -0,0 +1,21 @@ +// 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. + +import 'dart:js_interop'; + +import '../js/typescript.dart' as ts; + +class ParserResult { + ts.TSProgram program; + Iterable files; + + ParserResult({required this.program, required this.files}); +} + +ParserResult parseDeclarationFiles(Iterable files) { + final program = ts.createProgram(files.jsify() as JSArray, + ts.TSCompilerOptions(declaration: true)); + + 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 new file mode 100644 index 00000000..f6c0a788 --- /dev/null +++ b/web_generator/lib/src/interop_gen/transform.dart @@ -0,0 +1,91 @@ +// 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. + +import 'dart:js_interop'; + +import 'package:code_builder/code_builder.dart'; +import 'package:dart_style/dart_style.dart'; + +import '../ast.dart'; +import '../js/typescript.dart' as ts; +import '../js/typescript.types.dart'; +import 'namer.dart'; +import 'parser.dart'; +import 'transform/transformer.dart'; + +class TransformResult { + ProgramDeclarationMap programMap; + + TransformResult._(this.programMap); + + // TODO(https://github.com/dart-lang/web/issues/388): Handle union of overloads + // (namespaces + functions, multiple interfaces, etc) + Map generate() { + final emitter = DartEmitter.scoped(useNullSafetySyntax: true); + final formatter = + DartFormatter(languageVersion: DartFormatter.latestLanguageVersion); + return programMap.map((file, declMap) { + final specs = declMap.decls.values.map((d) { + return switch (d) { + final Declaration n => n.emit(), + final Type t => t.emit(), + }; + }); + final lib = Library((l) => l..body.addAll(specs)); + return MapEntry(file, formatter.format('${lib.accept(emitter)}')); + }); + } +} + +/// A map of declarations, where the key is the declaration's stringified [ID]. +extension type NodeMap._(Map decls) implements Map { + NodeMap() : decls = {}; + + List findByName(String name) { + return decls.entries + .where((e) => UniqueNamer.parse(e.key).name == name) + .map((e) => e.value) + .toList(); + } + + void add(Node decl) => decls[decl.id.toString()] = decl; +} + +typedef ProgramDeclarationMap = Map; + +TransformResult transform(ParserResult parsedDeclarations) { + final programDeclarationMap = {}; + + for (final file in parsedDeclarations.files) { + if (programDeclarationMap.containsKey(file)) continue; + + transformFile(parsedDeclarations.program, file, programDeclarationMap); + } + + return TransformResult._(programDeclarationMap); +} + +void transformFile(ts.TSProgram program, String file, + Map programDeclarationMap) { + final src = program.getSourceFile(file); + if (src == null) return; + + final typeChecker = program.getTypeChecker(); + + final transformer = Transformer(programDeclarationMap, typeChecker); + + ts.forEachChild( + src, + ((TSNode node) { + // ignore end of file + if (node.kind == TSSyntaxKind.EndOfFileToken) return; + + transformer.transform(node); + }).toJS as ts.TSNodeCallback); + + // filter + final resolvedMap = transformer.filter(); + + programDeclarationMap.addAll({file: resolvedMap}); +} diff --git a/web_generator/lib/src/interop_gen/transform/transformer.dart b/web_generator/lib/src/interop_gen/transform/transformer.dart new file mode 100644 index 00000000..0da0e79d --- /dev/null +++ b/web_generator/lib/src/interop_gen/transform/transformer.dart @@ -0,0 +1,215 @@ +// 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. + +import 'dart:js_interop'; +import '../../ast.dart'; +import '../../js/typescript.dart' as ts; +import '../../js/typescript.types.dart'; +import '../namer.dart'; +import '../transform.dart'; + +class Transformer { + /// A set of already resolved TS Nodes + final Set nodes = {}; + + /// A map of declarations + final NodeMap nodeMap = NodeMap(); + + /// The type checker for the given program + final ts.TSTypeChecker typeChecker; + + /// A set of declarations to export + final Set exportSet; + + /// namer, for giving elements unique names + final UniqueNamer namer; + + final ProgramDeclarationMap programMap; + + Transformer(this.programMap, this.typeChecker, + {Iterable exportSet = const []}) + : exportSet = exportSet.toSet(), + namer = UniqueNamer(); + + void transform(TSNode node) { + if (nodes.contains(node)) return; + + switch (node.kind) { + case TSSyntaxKind.VariableStatement: + final decs = _transformVariable(node as TSVariableStatement); + nodeMap.addAll({for (final d in decs) d.id.toString(): d}); + default: + final Declaration decl = switch (node.kind) { + _ => throw Exception('Unsupported Declaration Kind: ${node.kind}') + }; + // ignore: dead_code This line will not be dead in future decl additions + nodeMap.add(decl); + } + + nodes.add(node); + } + + NodeMap filter() { + final filteredDeclarations = NodeMap(); + + // filter out for export declarations + nodeMap.forEach((id, node) { + if (exportSet.contains(node.name)) { + filteredDeclarations[id] = node; + } + + // get decls with `export` keyword + switch (node) { + case final ExportableDeclaration e: + if (e.exported) { + filteredDeclarations.add(e); + } + break; + case final PrimitiveType _: + // primitive types are generated by default + break; + case Type(): + // TODO: Handle this case. + throw UnimplementedError(); + case Declaration(): + // TODO: Handle this case. + throw UnimplementedError(); + } + }); + + // then filter for dependencies + final otherDecls = filteredDeclarations.entries + .map((e) => _getDependenciesOfDecl(e.value)) + .reduce((value, element) => value..addAll(element)); + + return filteredDeclarations..addAll(otherDecls); + } + + /// Given an already filtered declaration [decl], + /// filter out dependencies of [decl] recursively + /// and return them as a declaration map + NodeMap _getDependenciesOfDecl([Node? decl]) { + final filteredDeclarations = NodeMap(); + + switch (decl) { + case final VariableDeclaration v: + if (v.type is! PrimitiveType) filteredDeclarations.add(v.type); + break; + case final UnionType u: + filteredDeclarations.addAll({ + for (final t in u.types.where((t) => t is! PrimitiveType)) + t.id.toString(): t + }); + break; + case final PrimitiveType _: + // primitive types are generated by default + break; + default: + print('WARN: The given node type ${decl.runtimeType.toString()} ' + 'is not supported for filtering. Skipping...'); + break; + } + + if (filteredDeclarations.isNotEmpty) { + final otherDecls = filteredDeclarations.entries + .map((e) => _getDependenciesOfDecl(e.value)) + .reduce((value, element) => value..addAll(element)); + + filteredDeclarations.addAll(otherDecls); + } + + return filteredDeclarations; + } + + List _transformVariable(TSVariableStatement variable) { + // get the modifier of the declaration + final modifiers = variable.modifiers.toDart; + final isExported = modifiers.any((m) { + return m.kind == TSSyntaxKind.ExportKeyword; + }); + + var modifier = VariableModifier.$var; + + if ((variable.declarationList.flags & TSNodeFlags.Const) != 0) { + modifier = VariableModifier.$const; + } else if ((variable.declarationList.flags & TSNodeFlags.Let) != 0) { + modifier = VariableModifier.let; + } + + return variable.declarationList.declarations.toDart.map((d) { + namer.markUsed(d.name.text); + return VariableDeclaration( + name: d.name.text, + type: d.type == null ? PrimitiveType.any : _transformType(d.type!), + modifier: modifier, + exported: isExported); + }).toList(); + } + + TSNode? _getDeclarationByName(TSIdentifier name) { + final symbol = typeChecker.getSymbolAtLocation(name); + + final declarations = symbol?.getDeclarations(); + // TODO(https://github.com/dart-lang/web/issues/387): Some declarations may not be defined on file, + // and may be from an import statement + // We should be able to handle these + return declarations?.toDart.first; + } + + /// Parses the type + /// + /// TODO(https://github.com/dart-lang/web/issues/384): Add support for literals (i.e individual booleans and `null`) + /// TODO(https://github.com/dart-lang/web/issues/383): Add support for `typeof` types + Type _transformType(TSTypeNode type) { + if (type.kind == TSSyntaxKind.UnionType) { + final unionType = type as TSUnionTypeNode; + // parse union type + return UnionType( + types: unionType.types.toDart.map(_transformType).toList()); + } + + if (type.kind == TSSyntaxKind.TypeReference) { + // reference type + final refType = type as TSTypeReferenceNode; + + final name = refType.typeName.text; + final typeArguments = refType.typeArguments?.toDart; + + var declarationsMatching = nodeMap.findByName(name); + if (declarationsMatching.isEmpty) { + // TODO: In the case of overloading, should/shouldn't we handle more than one declaration? + final declaration = _getDeclarationByName(refType.typeName); + + if (declaration == null) { + throw Exception('Found no declaration matching $name'); + } + + transform(declaration); + + declarationsMatching = nodeMap.findByName(name); + } + + // TODO: In the case of overloading, should/shouldn't we handle more than one declaration? + final firstNode = + declarationsMatching.whereType().first; + + return firstNode.asReferredType( + (typeArguments ?? []).map(_transformType).toList(), + ); + } + + // check for its kind + return switch (type.kind) { + TSSyntaxKind.StringKeyword => PrimitiveType.string, + TSSyntaxKind.AnyKeyword => PrimitiveType.any, + TSSyntaxKind.ObjectKeyword => PrimitiveType.object, + TSSyntaxKind.NumberKeyword => PrimitiveType.number, + TSSyntaxKind.UndefinedKeyword => PrimitiveType.undefined, + TSSyntaxKind.UnknownKeyword => PrimitiveType.unknown, + TSSyntaxKind.BooleanKeyword => PrimitiveType.boolean, + _ => throw UnsupportedError( + 'The given type with kind ${type.kind} is not supported yet') + }; + } +} diff --git a/web_generator/lib/src/js/typescript.dart b/web_generator/lib/src/js/typescript.dart new file mode 100644 index 00000000..bc9a73d8 --- /dev/null +++ b/web_generator/lib/src/js/typescript.dart @@ -0,0 +1,52 @@ +@JS('ts') +library; + +import 'dart:js_interop'; + +import 'typescript.types.dart'; + +@JS() +external TSProgram createProgram( + JSArray files, TSCompilerOptions options); + +@JS() +external TSSourceFile createSourceFile( + String filename, + String contents, +); + +@JS() +external void forEachChild( + TSNode node, TSNodeCallback cbNode, + [TSNodeArrayCallback? cdNodes]); + +@JS('CompilerOptions') +extension type TSCompilerOptions._(JSObject _) implements JSObject { + external TSCompilerOptions({bool? allowJs, bool? declaration}); + external bool? get allowJs; + external bool? get declaration; +} + +@JS('Program') +extension type TSProgram._(JSObject _) implements JSObject { + external TSSourceFile? getSourceFile(String file); + external TSTypeChecker getTypeChecker(); +} + +@JS('TypeChecker') +extension type TSTypeChecker._(JSObject _) implements JSObject { + external TSSymbol? getSymbolAtLocation(TSNode node); +} + +@JS('SourceFile') +extension type TSSourceFile._(JSObject _) implements TSNode {} + +extension type TSNodeCallback._(JSObject _) + implements JSObject { + external T? call(TSNode node); +} + +extension type TSNodeArrayCallback._(JSObject _) + implements JSObject { + external T? call(TSNodeArray nodes); +} diff --git a/web_generator/lib/src/js/typescript.types.dart b/web_generator/lib/src/js/typescript.types.dart new file mode 100644 index 00000000..e6d31d39 --- /dev/null +++ b/web_generator/lib/src/js/typescript.types.dart @@ -0,0 +1,117 @@ +// ignore_for_file: constant_identifier_names + +@JS('ts') +library; + +import 'dart:js_interop'; + +import 'package:meta/meta.dart'; + +extension type const TSSyntaxKind._(num _) { + /// To be ignored + static const TSSyntaxKind EndOfFileToken = TSSyntaxKind._(1); + + /// declarations + static const TSSyntaxKind ClassDeclaration = TSSyntaxKind._(263); + static const TSSyntaxKind VariableStatement = TSSyntaxKind._(243); + static const TSSyntaxKind VariableDeclaration = TSSyntaxKind._(260); + static const TSSyntaxKind InterfaceDeclaration = TSSyntaxKind._(264); + static const TSSyntaxKind FunctionDeclaration = TSSyntaxKind._(262); + static const TSSyntaxKind ExportDeclaration = TSSyntaxKind._(278); + + /// keywords + static const TSSyntaxKind ExportKeyword = TSSyntaxKind._(95); + static const TSSyntaxKind DeclareKeyword = TSSyntaxKind._(138); + static const TSSyntaxKind ExtendsKeyword = TSSyntaxKind._(96); + static const TSSyntaxKind ImplementsKeyword = TSSyntaxKind._(119); + + // types that are keywords + static const TSSyntaxKind StringKeyword = TSSyntaxKind._(154); + static const TSSyntaxKind NumberKeyword = TSSyntaxKind._(150); + static const TSSyntaxKind BooleanKeyword = TSSyntaxKind._(136); + static const TSSyntaxKind ObjectKeyword = TSSyntaxKind._(151); + static const TSSyntaxKind AnyKeyword = TSSyntaxKind._(133); + static const TSSyntaxKind UndefinedKeyword = TSSyntaxKind._(157); + static const TSSyntaxKind SetKeyword = TSSyntaxKind._(153); + static const TSSyntaxKind UnknownKeyword = TSSyntaxKind._(159); + + // types + static const TSSyntaxKind UnionType = TSSyntaxKind._(192); + static const TSSyntaxKind TypeReference = TSSyntaxKind._(183); + + /// Other + static const TSSyntaxKind Identifier = TSSyntaxKind._(80); + static const TSSyntaxKind TypeParameter = TSSyntaxKind._(168); + static const TSSyntaxKind HeritageClause = TSSyntaxKind._(298); + static const TSSyntaxKind ExpressionWithTypeArguments = TSSyntaxKind._(233); +} + +extension type const TSNodeFlags._(int _) implements int { + static const TSNodeFlags None = TSNodeFlags._(0); + static const TSNodeFlags Let = TSNodeFlags._(1); + static const TSNodeFlags Const = TSNodeFlags._(2); +} + +@JS('Node') +extension type TSNode._(JSObject _) implements JSObject { + external TSSyntaxKind get kind; + external TSNode get parent; + external TSNodeFlags get flags; +} + +@JS('TypeNode') +extension type TSTypeNode._(JSObject _) implements TSNode {} + +@JS('UnionTypeNode') +extension type TSUnionTypeNode._(JSObject _) implements TSTypeNode { + @redeclare + TSSyntaxKind get kind => TSSyntaxKind.UnionType; + external TSNodeArray get types; +} + +@JS('TypeReferenceNode') +extension type TSTypeReferenceNode._(JSObject _) implements TSTypeNode { + @redeclare + TSSyntaxKind get kind => TSSyntaxKind.TypeReference; + + external TSIdentifier get typeName; + external TSNodeArray? get typeArguments; +} + +@JS('Declaration') +extension type TSDeclaration._(JSObject _) implements TSNode {} + +@JS('Statement') +extension type TSStatement._(JSObject _) implements TSNode {} + +@JS('Identifier') +extension type TSIdentifier._(JSObject _) implements TSDeclaration { + external String get text; +} + +@JS('VariableDeclaration') +extension type TSVariableStatement._(JSObject _) implements TSStatement { + external TSVariableDeclarationList get declarationList; + external TSNodeArray get modifiers; +} + +@JS('VariableDeclaration') +extension type TSVariableDeclaration._(JSObject _) implements TSDeclaration { + external TSIdentifier get name; + external TSTypeNode? get type; +} + +@JS('VariableDeclarationList') +extension type TSVariableDeclarationList._(JSObject _) implements TSNode { + external TSNodeArray get declarations; +} + +@JS('NodeArray') +extension type TSNodeArray._(JSArray _) + implements JSArray {} + +@JS('Symbol') +extension type TSSymbol._(JSObject _) implements JSObject { + external String get name; + external JSArray? getDeclarations(); +} diff --git a/web_generator/lib/src/js/typescript_extensions.dart b/web_generator/lib/src/js/typescript_extensions.dart new file mode 100644 index 00000000..603d62d8 --- /dev/null +++ b/web_generator/lib/src/js/typescript_extensions.dart @@ -0,0 +1,14 @@ +import 'typescript.types.dart'; + +extension Names on TSSyntaxKind { + String get name { + return switch (this) { + TSSyntaxKind.DeclareKeyword => 'declare', + TSSyntaxKind.ExportKeyword => 'export', + TSSyntaxKind.ExtendsKeyword => 'extends', + TSSyntaxKind.ImplementsKeyword => 'implements', + TSSyntaxKind.VariableDeclaration => 'variable', + _ => throw UnsupportedError('The keyword is not supported at the moment') + }; + } +} diff --git a/web_generator/pubspec.yaml b/web_generator/pubspec.yaml index 1e43459e..1c384c76 100644 --- a/web_generator/pubspec.yaml +++ b/web_generator/pubspec.yaml @@ -16,6 +16,7 @@ dependencies: dart_flutter_team_lints: ^3.0.0 dart_style: ^3.0.0 io: ^1.0.4 + meta: ^1.17.0 package_config: ^2.1.1 path: ^1.8.3 pub_semver: ^2.1.5 diff --git a/web_generator/test/integration/interop_gen/variables_expected.dart b/web_generator/test/integration/interop_gen/variables_expected.dart new file mode 100644 index 00000000..2609c67d --- /dev/null +++ b/web_generator/test/integration/interop_gen/variables_expected.dart @@ -0,0 +1,31 @@ +// ignore_for_file: no_leading_underscores_for_library_prefixes +import 'dart:js_interop' as _i1; + +@_i1.JS() +external int counter; +@_i1.JS() +external String get appName; +@_i1.JS() +external int globalCounter; +@_i1.JS() +external _i1.JSObject globalObject; +@_i1.JS() +external bool counted; +@_i1.JS() +external String username; +@_i1.JS() +external int get foo; +@_i1.JS() +external int get bar; +@_i1.JS() +external int free; +@_i1.JS() +external int dom; +@_i1.JS() +external String fred; +@_i1.JS() +external String doctor; +@_i1.JS() +external _i1.JSAny something; +@_i1.JS() +external _i1.JSAny? get maybeValue; diff --git a/web_generator/test/integration/interop_gen/variables_input.d.ts b/web_generator/test/integration/interop_gen/variables_input.d.ts new file mode 100644 index 00000000..933449bb --- /dev/null +++ b/web_generator/test/integration/interop_gen/variables_input.d.ts @@ -0,0 +1,11 @@ +export declare let counter: number; +export declare const appName: string; +export declare var globalCounter: number; +export declare var globalObject: object; +export declare var counted: boolean; +export declare let username: string; +export declare const foo: number, bar: number; +export declare let free: number, dom: number; +export declare let fred: string, doctor: string; +export declare let something: any; +export declare const maybeValue: unknown; diff --git a/web_generator/test/integration/interop_gen_test.dart b/web_generator/test/integration/interop_gen_test.dart new file mode 100644 index 00000000..c0e7ec81 --- /dev/null +++ b/web_generator/test/integration/interop_gen_test.dart @@ -0,0 +1,66 @@ +// 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. + +@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('Interop Gen Integration Test', () { + final testGenFolder = p.join('test', 'integration', 'interop_gen'); + final inputDir = Directory(testGenFolder); + final outputDir = Directory(p.join('.dart_tool', 'interop_gen')); + + setUpAll(() async { + // set up npm + await runProc('npm', ['install'], workingDirectory: bindingsGenPath); + + // compile file + await compileDartMain(dir: bindingsGenPath); + + await outputDir.create(recursive: true); + }); + + for (final inputFile in inputDir + .listSync() + .whereType() + .where((f) => p.basenameWithoutExtension(f.path).contains('_input'))) { + final inputFileName = p.basenameWithoutExtension(inputFile.path); + final inputName = inputFileName.replaceFirst('_input.d', ''); + + final outputActualPath = + p.join('.dart_tool', 'interop_gen', '${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=$outFilePath', + '--declaration' + ], + workingDirectory: bindingsGenPath); + + // read files + final expectedOutput = await File(outputExpectedPath).readAsString(); + final actualOutput = await File(outputActualPath).readAsString(); + + expect(actualOutput, expectedOutput); + }); + } + }); +}