diff --git a/web_generator/lib/src/ast/declarations.dart b/web_generator/lib/src/ast/declarations.dart index 1c6bc012..a57a129a 100644 --- a/web_generator/lib/src/ast/declarations.dart +++ b/web_generator/lib/src/ast/declarations.dart @@ -232,3 +232,40 @@ class EnumMember { String? dartName; } + +class TypeAliasDeclaration extends NamedDeclaration + implements ExportableDeclaration { + @override + final String name; + + final List typeParameters; + + final Type type; + + @override + final String? dartName; + + @override + bool exported; + + @override + ID get id => ID(type: 'typealias', name: name); + + TypeAliasDeclaration( + {required this.name, + this.typeParameters = const [], + required this.type, + required this.exported}) + : dartName = null; + + @override + TypeDef emit([DeclarationOptions? options]) { + options ??= DeclarationOptions(); + + return TypeDef((t) => t + ..name = name + ..types + .addAll(typeParameters.map((t) => t.emit(options?.toTypeOptions()))) + ..definition = type.emit(options?.toTypeOptions())); + } +} diff --git a/web_generator/lib/src/ast/helpers.dart b/web_generator/lib/src/ast/helpers.dart index e987ccda..354f2083 100644 --- a/web_generator/lib/src/ast/helpers.dart +++ b/web_generator/lib/src/ast/helpers.dart @@ -7,6 +7,7 @@ import 'package:code_builder/code_builder.dart'; import 'base.dart'; import 'builtin.dart'; import 'declarations.dart'; +import 'types.dart'; BuiltinType? getSupportedType(String name, [List typeParams = const []]) { final type = switch (name) { @@ -38,6 +39,15 @@ Type getJSTypeAlternative(Type type) { if (primitiveType == null) return BuiltinType.anyType; return BuiltinType.primitiveType(primitiveType, shouldEmitJsType: true); + } else if (type is ReferredType) { + switch (type.declaration) { + case TypeAliasDeclaration(type: final t): + case EnumDeclaration(baseType: final t): + final jsTypeT = getJSTypeAlternative(t); + return jsTypeT == t ? type : jsTypeT; + default: + return type; + } } return type; } diff --git a/web_generator/lib/src/interop_gen/transform.dart b/web_generator/lib/src/interop_gen/transform.dart index 08009ced..b1a46421 100644 --- a/web_generator/lib/src/interop_gen/transform.dart +++ b/web_generator/lib/src/interop_gen/transform.dart @@ -23,8 +23,8 @@ class TransformResult { // (namespaces + functions, multiple interfaces, etc) Map generate() { final emitter = DartEmitter.scoped(useNullSafetySyntax: true); - final formatter = - DartFormatter(languageVersion: DartFormatter.latestLanguageVersion); + final formatter = DartFormatter( + languageVersion: DartFormatter.latestShortStyleLanguageVersion); return programMap.map((file, declMap) { final specs = declMap.decls.values.map((d) { return switch (d) { diff --git a/web_generator/lib/src/interop_gen/transform/transformer.dart b/web_generator/lib/src/interop_gen/transform/transformer.dart index cd121358..73050449 100644 --- a/web_generator/lib/src/interop_gen/transform/transformer.dart +++ b/web_generator/lib/src/interop_gen/transform/transformer.dart @@ -49,6 +49,8 @@ class Transformer { _transformFunction(node as TSFunctionDeclaration), TSSyntaxKind.EnumDeclaration => _transformEnum(node as TSEnumDeclaration), + TSSyntaxKind.TypeAliasDeclaration => + _transformTypeAlias(node as TSTypeAliasDeclaration), _ => throw Exception('Unsupported Declaration Kind: ${node.kind}') }; // ignore: dead_code This line will not be dead in future decl additions @@ -177,6 +179,30 @@ class Transformer { return declarations?.toDart.first; } + TypeAliasDeclaration _transformTypeAlias(TSTypeAliasDeclaration typealias) { + final name = typealias.name.text; + + final modifiers = typealias.modifiers?.toDart; + final isExported = modifiers?.any((m) { + return m.kind == TSSyntaxKind.ExportKeyword; + }) ?? + false; + + final typeParams = typealias.typeParameters?.toDart; + + final type = typealias.type; + + return TypeAliasDeclaration( + name: name, + // TODO: Can we find a way not to make the types be JS types + // by default if possible. Leaving this for now, + // so that using such typealiases in generics does not break + type: _transformType(type), + typeParameters: + typeParams?.map(_transformTypeParamDeclaration).toList() ?? [], + exported: isExported); + } + FunctionDeclaration _transformFunction(TSFunctionDeclaration function) { final name = function.name.text; @@ -228,18 +254,20 @@ class Transformer { GenericType _transformTypeParamDeclaration( TSTypeParameterDeclaration typeParam) { + final constraint = typeParam.constraint == null + ? BuiltinType.anyType + : _transformType(typeParam.constraint!, typeArg: true); return GenericType( name: typeParam.name.text, - constraint: typeParam.constraint == null - ? BuiltinType.anyType - : _transformType(typeParam.constraint!)); + constraint: getJSTypeAlternative(constraint)); } /// 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, {bool parameter = false}) { + Type _transformType(TSTypeNode type, + {bool parameter = false, bool typeArg = false}) { switch (type.kind) { case TSSyntaxKind.TypeReference: final refType = type as TSTypeReferenceNode; @@ -255,7 +283,10 @@ class Transformer { // for this, and adding support for "supported declarations" // (also a better name for that) final supportedType = getSupportedType( - name, (typeArguments ?? []).map(_transformType).toList()); + name, + (typeArguments ?? []) + .map((type) => _transformType(type, typeArg: true)) + .toList()); if (supportedType != null) { return supportedType; } @@ -280,8 +311,19 @@ class Transformer { final firstNode = declarationsMatching.whereType().first; + // For Typealiases, we can either return the type itself + // or the JS Alternative (if its underlying type isn't a JS type) + switch (firstNode) { + case TypeAliasDeclaration(type: final t): + case EnumDeclaration(baseType: final t): + final jsType = getJSTypeAlternative(t); + if (jsType != t && typeArg) return jsType; + } + return firstNode.asReferredType( - (typeArguments ?? []).map(_transformType).toList(), + (typeArguments ?? []) + .map((type) => _transformType(type, typeArg: true)) + .toList(), ); // TODO: Union types are also anonymous by design // Unless we are making typedefs for them, we should @@ -381,7 +423,8 @@ class Transformer { 'The given type with kind ${type.kind} is not supported yet') }; - return BuiltinType.primitiveType(primitiveType); + return BuiltinType.primitiveType(primitiveType, + shouldEmitJsType: typeArg ? true : null); } } @@ -445,6 +488,9 @@ class Transformer { break; case final EnumDeclaration _: break; + case final TypeAliasDeclaration t: + if (decl.type is! BuiltinType) filteredDeclarations.add(t.type); + break; // TODO: We can make (DeclarationAssociatedType) and use that // rather than individual type names case final HomogenousEnumType hu: diff --git a/web_generator/lib/src/js/typescript.types.dart b/web_generator/lib/src/js/typescript.types.dart index 25bda5ce..579eed4c 100644 --- a/web_generator/lib/src/js/typescript.types.dart +++ b/web_generator/lib/src/js/typescript.types.dart @@ -24,6 +24,7 @@ extension type const TSSyntaxKind._(num _) { static const TSSyntaxKind InterfaceDeclaration = TSSyntaxKind._(264); static const TSSyntaxKind FunctionDeclaration = TSSyntaxKind._(262); static const TSSyntaxKind ExportDeclaration = TSSyntaxKind._(278); + static const TSSyntaxKind TypeAliasDeclaration = TSSyntaxKind._(265); static const TSSyntaxKind Parameter = TSSyntaxKind._(169); static const TSSyntaxKind EnumDeclaration = TSSyntaxKind._(266); @@ -178,6 +179,15 @@ extension type TSFunctionDeclaration._(JSObject _) implements TSDeclaration { external TSNodeArray get modifiers; } +@JS('TypeAliasDeclaration') +extension type TSTypeAliasDeclaration._(JSObject _) + implements TSDeclaration, TSStatement { + external TSNodeArray? get modifiers; + external TSNodeArray? get typeParameters; + external TSIdentifier get name; + external TSTypeNode get type; +} + @JS('ParameterDeclaration') extension type TSParameterDeclaration._(JSObject _) implements TSDeclaration { external TSNode get name; diff --git a/web_generator/test/integration/interop_gen/enum_expected.dart b/web_generator/test/integration/interop_gen/enum_expected.dart index 57f30934..dc775e93 100644 --- a/web_generator/test/integration/interop_gen/enum_expected.dart +++ b/web_generator/test/integration/interop_gen/enum_expected.dart @@ -104,7 +104,10 @@ extension type const Permissions._(int _) { static const Permissions All = Permissions._(7); } @_i1.JS() -external bool hasPermission(Permissions perm, Permissions flag); +external bool hasPermission( + Permissions perm, + Permissions flag, +); @_i1.JS() external Permissions get userPermissions; @_i1.JS() diff --git a/web_generator/test/integration/interop_gen/functions_expected.dart b/web_generator/test/integration/interop_gen/functions_expected.dart index ce36fa8b..f615bb69 100644 --- a/web_generator/test/integration/interop_gen/functions_expected.dart +++ b/web_generator/test/integration/interop_gen/functions_expected.dart @@ -13,7 +13,10 @@ external void logMessages( _i1.JSArray<_i1.JSString> messages4, ]); @_i1.JS() -external _i1.JSPromise delay(num ms, [U? returnValue]); +external _i1.JSPromise delay( + num ms, [ + U? returnValue, +]); @_i1.JS() external _i1.JSArray<_i1.JSNumber> toArray(num a); @_i1.JS() @@ -21,11 +24,18 @@ external double square(num a); @_i1.JS() external double pow(num a); @_i1.JS('pow') -external double pow$1(num a, num power); +external double pow$1( + num a, + num power, +); @_i1.JS('toArray') external _i1.JSArray<_i1.JSString> toArray$1(String a); @_i1.JS() -external _i1.JSObject createUser(String name, [num? age, String? role]); +external _i1.JSObject createUser( + String name, [ + num? age, + String? role, +]); @_i1.JS() external T firstElement(_i1.JSArray arr); @_i1.JS() @@ -38,8 +48,7 @@ external T identity(T value); external void someFunction(_i1.JSArray arr); @_i1.JS('someFunction') external B someFunction$1( - _i1.JSArray arr, -); + _i1.JSArray arr); @_i1.JS() external T logTuple>( T args, [ diff --git a/web_generator/test/integration/interop_gen/typealias_expected.dart b/web_generator/test/integration/interop_gen/typealias_expected.dart new file mode 100644 index 00000000..61cf2f2c --- /dev/null +++ b/web_generator/test/integration/interop_gen/typealias_expected.dart @@ -0,0 +1,76 @@ +// ignore_for_file: constant_identifier_names, non_constant_identifier_names + +// ignore_for_file: no_leading_underscores_for_library_prefixes +import 'dart:js_interop' as _i1; + +typedef Username = String; +typedef Age = double; +typedef IsActive = bool; +typedef Tags = _i1.JSArray<_i1.JSString>; +typedef List = _i1.JSArray; +typedef Box = _i1.JSArray<_i1.JSArray>; +typedef PromisedArray> + = _i1.JSPromise; +typedef Shape2D = String; +typedef PrismFromShape2D = _i1.JSArray; +typedef Logger = LoggerType; +typedef Direction = AnonymousUnion; +typedef Method = AnonymousUnion$1; +@_i1.JS() +external LoggerContainer<_i1.JSNumber> get loggerContainers; +@_i1.JS() +external Logger myLogger; +@_i1.JS() +external Method get requestMethod; +@_i1.JS() +external Username get username; +@_i1.JS() +external Age get age; +@_i1.JS() +external _i1.JSArray get tagArray; +@_i1.JS() +external List<_i1.JSString> get users; +@_i1.JS() +external Box<_i1.JSNumber> get matrix; +@_i1.JS() +external PrismFromShape2D<_i1.JSString> makePrism(Shape2D shape); +@_i1.JS('makePrism') +external PrismFromShape2D makePrism$1(S shape); +@_i1.JS() +external PromisedArray<_i1.JSString, _i1.JSArray<_i1.JSString>> fetchNames(); +@_i1.JS() +external String isUserActive(IsActive status); +extension type const LoggerType._(int _) { + static const LoggerType Noop = LoggerType._(0); + + static const LoggerType Stdout = LoggerType._(1); + + static const LoggerType Stderr = LoggerType._(2); + + static const LoggerType File = LoggerType._(3); + + static const LoggerType Other = LoggerType._(4); +} +extension type const AnonymousUnion._(String _) { + static const AnonymousUnion N = AnonymousUnion._('N'); + + static const AnonymousUnion S = AnonymousUnion._('S'); + + static const AnonymousUnion E = AnonymousUnion._('E'); + + static const AnonymousUnion W = AnonymousUnion._('W'); +} +extension type const AnonymousUnion$1._(String _) { + static const AnonymousUnion$1 GET = AnonymousUnion$1._('GET'); + + static const AnonymousUnion$1 POST = AnonymousUnion$1._('POST'); + + static const AnonymousUnion$1 PUT = AnonymousUnion$1._('PUT'); + + static const AnonymousUnion$1 DELETE = AnonymousUnion$1._('DELETE'); + + static const AnonymousUnion$1 PATCH = AnonymousUnion$1._('PATCH'); + + static const AnonymousUnion$1 OPTIONS = AnonymousUnion$1._('OPTIONS'); +} +typedef LoggerContainer = _i1.JSArray; diff --git a/web_generator/test/integration/interop_gen/typealias_input.d.ts b/web_generator/test/integration/interop_gen/typealias_input.d.ts new file mode 100644 index 00000000..27fccba2 --- /dev/null +++ b/web_generator/test/integration/interop_gen/typealias_input.d.ts @@ -0,0 +1,32 @@ +declare enum LoggerType { + Noop = 0, + Stdout = 1, + Stderr = 2, + File = 3, + Other = 4 +} +export type Username = string; +export type Age = number; +export type IsActive = boolean; +export type Tags = string[]; +export type List = T[]; +export type Box = Array>; +export type PromisedArray> = Promise; +export type Shape2D = string; +export type PrismFromShape2D = Array; +export type Logger = LoggerType; +export type Direction = "N" | "S" | "E" | "W"; +export type Method = "GET" | "POST" | "PUT" | "DELETE" | "PATCH" | "OPTIONS"; +type LoggerContainer = N[]; +export declare const loggerContainers: LoggerContainer; +export declare let myLogger: Logger; +export declare const requestMethod: Method; +export declare const username: Username; +export declare const age: Age; +export declare const tagArray: Tags[]; +export declare const users: List; +export declare const matrix: Box; +export declare function makePrism(shape: Shape2D): PrismFromShape2D; +export declare function makePrism(shape: S): PrismFromShape2D; +export declare function fetchNames(): PromisedArray; +export declare function isUserActive(status: IsActive): string; diff --git a/web_generator/test/ts_bindings_test.dart b/web_generator/test/ts_bindings_test.dart deleted file mode 100644 index 0f9506d2..00000000 --- a/web_generator/test/ts_bindings_test.dart +++ /dev/null @@ -1,77 +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. - -@TestOn('vm') -@Tags(['node']) -@Skip('https://github.com/dart-lang/web/issues/372') -library; - -import 'dart:io'; - -import 'package:path/path.dart' as p; -import 'package:test/test.dart'; -import 'package:web_generator/src/cli.dart'; - -void main() { - final testGenFolder = p.join('test', 'gen'); - final testGenDTSFiles = p.join(testGenFolder, 'input'); - - group('Web Generator TS Bindings Integration Test', () { - final inputDir = Directory(testGenDTSFiles); - - setUpAll(() async { - // set up npm - await runProc('npm', ['install'], - workingDirectory: bindingsGeneratorPath, detached: true); - - // compile file - await runProc( - Platform.executable, - [ - 'compile', - 'js', - '--enable-asserts', - '--server-mode', - 'dart_main.dart', - '-o', - 'dart_main.js', - ], - workingDirectory: bindingsGeneratorPath, - detached: true); - }); - - for (final inputFile in inputDir.listSync().whereType()) { - final inputFileName = p.basenameWithoutExtension(inputFile.path); - - final outputActualPath = - p.join('test', 'gen', 'expected', '${inputFileName}_actual.dart'); - final outputExpectedPath = - p.join('test', 'gen', 'expected', '${inputFileName}_expected.dart'); - - test(inputFileName, () async { - final inputFilePath = - p.relative(inputFile.path, from: bindingsGeneratorPath); - final outFilePath = - p.relative(outputActualPath, from: bindingsGeneratorPath); - // run the entrypoint - await runProc( - 'node', - [ - 'main.mjs', - '--input=$inputFilePath', - '--output=$outFilePath', - '--declaration' - ], - workingDirectory: bindingsGeneratorPath, - detached: true); - - // read files - final expectedOutput = await File(outputExpectedPath).readAsString(); - final actualOutput = await File(outputActualPath).readAsString(); - - expect(actualOutput, expectedOutput); - }); - } - }); -}