diff --git a/web_generator/lib/src/ast/base.dart b/web_generator/lib/src/ast/base.dart index bc356b70..6c9bd184 100644 --- a/web_generator/lib/src/ast/base.dart +++ b/web_generator/lib/src/ast/base.dart @@ -3,8 +3,10 @@ // BSD-style license that can be found in the LICENSE file. import 'package:code_builder/code_builder.dart'; +import 'package:path/path.dart' as p; import '../interop_gen/namer.dart'; +import 'declarations.dart'; import 'documentation.dart'; import 'types.dart'; @@ -155,3 +157,36 @@ class ParameterDeclaration { abstract class NamedType extends Type { String get name; } + +/// A reference to a given module +class ModuleReference extends Node { + @override + String? get dartName => + throw Exception('Error calling dartName on ModuleReference'); + + /// The name of the module being referenced + String get name => reference.name; + + /// Where the module is being referenced from, as a relative path + String from; + + /// The full path of the actual file being referenced, if any + String? get actualReference => reference.url; + + /// The module being referenced + ModuleDeclaration reference; + + ModuleReference({required this.from, required this.reference}); + + String get url => p.join(from, '$name.dart'); + + @override + Directive emit([Options? options]) { + return Directive.export( + url, + ); + } + + @override + ID get id => ID(type: 'module-ref', name: url); +} diff --git a/web_generator/lib/src/ast/declarations.dart b/web_generator/lib/src/ast/declarations.dart index 1ede563c..089fe0f0 100644 --- a/web_generator/lib/src/ast/declarations.dart +++ b/web_generator/lib/src/ast/declarations.dart @@ -3,8 +3,13 @@ // BSD-style license that can be found in the LICENSE file. import 'package:code_builder/code_builder.dart'; +import 'package:path/path.dart' as p; +import '../interop_gen/generate.dart'; import '../interop_gen/namer.dart'; +import '../interop_gen/qualified_name.dart'; +import '../interop_gen/transform.dart'; +import '../interop_gen/transform/transformer.dart'; import '../js/typescript.types.dart'; import 'base.dart'; import 'builtin.dart'; @@ -16,16 +21,177 @@ abstract class NestableDeclaration extends NamedDeclaration implements DocumentedDeclaration { NestableDeclaration? get parent; - String get qualifiedName => - parent != null ? '${parent!.qualifiedName}.$name' : name; + String get qualifiedName => parent != null && parent is! ModuleDeclaration + ? '${parent!.qualifiedName}.$name' + : name; - String get completedDartName => parent != null + String get completedDartName => parent != null && parent is! ModuleDeclaration ? '${parent!.completedDartName}_${dartName ?? name}' : (dartName ?? name); } -abstract class ParentDeclaration { +sealed class ParentDeclaration extends NestableDeclaration { + @override + String name; + + @override + String? dartName; + + @override + abstract ParentDeclaration? parent; + Set get nodes; + + final Set namespaceDeclarations; + + final Set topLevelDeclarations; + + final Set nestableDeclarations; + + NodeMap get nodeMap { + final map = NodeMap({}); + for (final decl in [ + ...topLevelDeclarations, + ...namespaceDeclarations, + ...nestableDeclarations + ]) { + final qualifiedName = QualifiedName.raw(decl.id.name); + map[ID( + type: decl.id.type, + name: qualifiedName.length == 1 + ? qualifiedName.asName + : qualifiedName.last.part) + .toString()] = decl; + } + + final dependencies = map.entries + .map((e) => getDependenciesOfDecl(e.value)) + .reduce((value, element) => value..addAll(element)); + + map.addAll(dependencies); + + return map; + } + + ParentDeclaration({ + required this.name, + this.dartName, + this.topLevelDeclarations = const {}, + this.namespaceDeclarations = const {}, + this.nestableDeclarations = const {}, + }); + + ExtensionType _emitObject([covariant DeclarationOptions? options]) { + options?.static = true; + + final (doc, annotations) = generateFromDocumentation(documentation); + // static props and vars + final methods = []; + final fields = []; + + for (final decl in topLevelDeclarations) { + if (decl case final VariableDeclaration variable) { + if (variable.modifier == VariableModifier.$const) { + methods.add(variable.emit(options ?? DeclarationOptions(static: true)) + as Method); + } else { + fields.add(variable.emit(options ?? DeclarationOptions(static: true)) + as Field); + } + } else if (decl case final FunctionDeclaration fn) { + methods.add(fn.emit(options ?? DeclarationOptions(static: true))); + } + } + + // namespace refs + for (final ParentDeclaration( + name: namespaceName, + dartName: namespaceDartName, + ) in namespaceDeclarations) { + methods.add(Method((m) => m + ..name = namespaceDartName ?? namespaceName + ..annotations + .addAll([generateJSAnnotation('$qualifiedName.$namespaceName')]) + ..type = MethodType.getter + ..returns = + refer('${completedDartName}_${namespaceDartName ?? namespaceName}') + ..external = true + ..static = true)); + } + + // class refs + for (final nestable in nestableDeclarations) { + switch (nestable) { + case ClassDeclaration( + name: final className, + dartName: final classDartName, + constructors: final constructors, + typeParameters: final typeParams, + abstract: final abstract + ): + var constr = constructors + .where((c) => c.name == null || c.name == 'unnamed') + .firstOrNull; + + if (constructors.isEmpty && !abstract) { + constr = ConstructorDeclaration.defaultFor(nestable); + } + + // static call to class constructor + if (constr != null) { + options ??= DeclarationOptions(); + + final (requiredParams, optionalParams) = + emitParameters(constr.parameters, options); + + methods.add(Method((m) => m + ..name = classDartName ?? className + ..annotations + .addAll([generateJSAnnotation('$qualifiedName.$className')]) + ..types.addAll( + typeParams.map((t) => t.emit(options?.toTypeOptions()))) + ..requiredParameters.addAll(requiredParams) + ..optionalParameters.addAll(optionalParams) + ..returns = + refer('${completedDartName}_${classDartName ?? className}') + ..lambda = true + ..static = true + ..body = refer(nestable.completedDartName).call( + [ + ...requiredParams.map((p) => refer(p.name)), + if (optionalParams.isNotEmpty) + ...optionalParams.map((p) => refer(p.name)) + ], + {}, + typeParams + .map((t) => t.emit(options?.toTypeOptions())) + .toList()).code)); + } + break; + default: + break; + } + } + + // put them together... + return ExtensionType((eType) => eType + ..docs.addAll([...doc]) + ..annotations.addAll([...annotations]) + ..name = completedDartName + ..annotations.addAll([ + if (parent != null) + generateJSAnnotation(qualifiedName) + else if (dartName != null && dartName != name) + generateJSAnnotation(name) + ]) + ..implements.add(refer('JSObject', 'dart:js_interop')) + ..primaryConstructorName = '_' + ..representationDeclaration = RepresentationDeclaration((rep) => rep + ..name = '_' + ..declaredRepresentationType = refer('JSObject', 'dart:js_interop')) + ..fields.addAll(fields) + ..methods.addAll(methods)); + } } /// A declaration that defines a type (class or interface) @@ -456,14 +622,8 @@ class TypeAliasDeclaration extends NamedDeclaration /// The declaration node for a TypeScript Namespace // TODO: Refactor into shared class when supporting modules -class NamespaceDeclaration extends NestableDeclaration - implements ExportableDeclaration, ParentDeclaration { - @override - String name; - - @override - String? dartName; - +class NamespaceDeclaration extends ParentDeclaration + implements ExportableDeclaration, NestableDeclaration { final ID _id; @override @@ -473,13 +633,7 @@ class NamespaceDeclaration extends NestableDeclaration bool exported; @override - NamespaceDeclaration? parent; - - final Set namespaceDeclarations; - - final Set topLevelDeclarations; - - final Set nestableDeclarations; + ParentDeclaration? parent; @override Set nodes = {}; @@ -488,127 +642,98 @@ class NamespaceDeclaration extends NestableDeclaration Documentation? documentation; NamespaceDeclaration( - {required this.name, + {required super.name, this.exported = true, required ID id, - this.dartName, - this.topLevelDeclarations = const {}, - this.namespaceDeclarations = const {}, - this.nestableDeclarations = const {}, + super.dartName, + super.topLevelDeclarations, + super.namespaceDeclarations, + super.nestableDeclarations, this.documentation}) - : _id = id; + : _id = id, + super(); @override ExtensionType emit([covariant DeclarationOptions? options]) { - options?.static = true; + return super._emitObject(options); + } +} - final (doc, annotations) = generateFromDocumentation(documentation); - // static props and vars - final methods = []; - final fields = []; +/// The declaration node for a TypeScript module +/// +/// ```ts +/// declare module 'some-module' { +/// export function foo(): void; +/// } +/// ``` +// TODO: We can have a ModuleFragmentDeclaration that can expose only +// the parts defined by the given file +// TODO: Module augmentation via overloading, #388 +// TODO: Explicitly state lack of support for ambient non-ts modules +class ModuleDeclaration extends ParentDeclaration + implements NestableDeclaration { + @override + covariant ModuleDeclaration? parent; - for (final decl in topLevelDeclarations) { - if (decl case final VariableDeclaration variable) { - if (variable.modifier == VariableModifier.$const) { - methods.add(variable.emit(options ?? DeclarationOptions(static: true)) - as Method); - } else { - fields.add(variable.emit(options ?? DeclarationOptions(static: true)) - as Field); - } - } else if (decl case final FunctionDeclaration fn) { - methods.add(fn.emit(options ?? DeclarationOptions(static: true))); - } - } + @override + Set nodes = {}; - // namespace refs - for (final NamespaceDeclaration( - name: namespaceName, - dartName: namespaceDartName, - ) in namespaceDeclarations) { - methods.add(Method((m) => m - ..name = namespaceDartName ?? namespaceName - ..annotations - .addAll([generateJSAnnotation('$qualifiedName.$namespaceName')]) - ..type = MethodType.getter - ..returns = - refer('${completedDartName}_${namespaceDartName ?? namespaceName}') - ..external = true - ..static = true)); - } + @override + Documentation? documentation; - // class refs - for (final nestable in nestableDeclarations) { - switch (nestable) { - case ClassDeclaration( - name: final className, - dartName: final classDartName, - constructors: final constructors, - typeParameters: final typeParams, - abstract: final abstract - ): - var constr = constructors - .where((c) => c.name == null || c.name == 'unnamed') - .firstOrNull; + /// This should be late, but because of the way code is structured, we + String? url; - if (constructors.isEmpty && !abstract) { - constr = ConstructorDeclaration.defaultFor(nestable); - } + ModuleDeclaration( + {required super.name, + super.dartName, + this.parent, + super.topLevelDeclarations, + super.namespaceDeclarations, + super.nestableDeclarations, + this.documentation, + this.url}); + + ModuleDeclaration.global( + {super.dartName, + this.parent, + super.topLevelDeclarations, + super.namespaceDeclarations, + super.nestableDeclarations, + this.documentation}) + : super(name: 'global'); - // static call to class constructor - if (constr != null) { - options ??= DeclarationOptions(); + @override + Spec emit([covariant DeclarationOptions? options]) { + // TODO: Add support for annotations above library directive + final (doc, annotations) = generateFromDocumentation(documentation); + return generateLibraryForNodeMap(nodeMap, + preamble: doc.join('\n'), + annotations: [ + ...annotations, + refer('JS', 'dart:js_interop').call([literalString(qualifiedName)]) + ], + extraIgnores: [ + if (name.contains(RegExp(r'-|/'))) 'file_names' + ]); + } - final (requiredParams, optionalParams) = - emitParameters(constr.parameters, options); + @override + ID get id => ID(type: 'module', name: name); - methods.add(Method((m) => m - ..name = classDartName ?? className - ..annotations - .addAll([generateJSAnnotation('$qualifiedName.$className')]) - ..types.addAll( - typeParams.map((t) => t.emit(options?.toTypeOptions()))) - ..requiredParameters.addAll(requiredParams) - ..optionalParameters.addAll(optionalParams) - ..returns = - refer('${completedDartName}_${classDartName ?? className}') - ..lambda = true - ..static = true - ..body = refer(nestable.completedDartName).call( - [ - ...requiredParams.map((p) => refer(p.name)), - if (optionalParams.isNotEmpty) - ...optionalParams.map((p) => refer(p.name)) - ], - {}, - typeParams - .map((t) => t.emit(options?.toTypeOptions())) - .toList()).code)); - } - break; - default: - break; - } + ModuleReference get asReference { + String moduleDirName; + if (url != null) { + moduleDirName = p.basenameWithoutExtension(realPathAsDir(url!)); + } else { + moduleDirName = '_'; } - // put them together... - return ExtensionType((eType) => eType - ..docs.addAll([...doc]) - ..annotations.addAll([...annotations]) - ..name = completedDartName - ..annotations.addAll([ - if (parent != null) - generateJSAnnotation(qualifiedName) - else if (dartName != null && dartName != name) - generateJSAnnotation(name) - ]) - ..implements.add(refer('JSObject', 'dart:js_interop')) - ..primaryConstructorName = '_' - ..representationDeclaration = RepresentationDeclaration((rep) => rep - ..name = '_' - ..declaredRepresentationType = refer('JSObject', 'dart:js_interop')) - ..fields.addAll(fields) - ..methods.addAll(methods)); + return ModuleReference(reference: this, from: moduleDirName); + } + + Spec emitObject([covariant DeclarationOptions? options]) { + return super._emitObject(options); } } diff --git a/web_generator/lib/src/ast/helpers.dart b/web_generator/lib/src/ast/helpers.dart index 4d071e9c..b531f5fe 100644 --- a/web_generator/lib/src/ast/helpers.dart +++ b/web_generator/lib/src/ast/helpers.dart @@ -3,6 +3,7 @@ // BSD-style license that can be found in the LICENSE file. import 'package:code_builder/code_builder.dart'; +import 'package:path/path.dart' as p; import '../formatting.dart'; import '../interop_gen/namer.dart'; @@ -345,3 +346,13 @@ class TupleDeclaration extends NamedDeclaration ])); } } + +String realPathAsDir(String path) { + if (path.endsWith('.d.ts')) { + return path.replaceAll('.d.ts', ''); + } else if (p.extension(path) != '') { + return p.setExtension(path, ''); + } else { + return path; + } +} diff --git a/web_generator/lib/src/dart_main.dart b/web_generator/lib/src/dart_main.dart index 2f5a8ef5..07fd62fb 100644 --- a/web_generator/lib/src/dart_main.dart +++ b/web_generator/lib/src/dart_main.dart @@ -93,12 +93,13 @@ Future generateJSInteropBindings(Config config) async { p.join(configOutput, p.basename(entry.key)).toJS, entry.value.toJS); } } else { - final entry = generatedCodeMap.entries.first; - fs.writeFileSync(configOutput.toJS, entry.value.toJS); + final mainLibrary = generatedCodeMap.entries.first; + fs.writeFileSync(configOutput.toJS, mainLibrary.value.toJS); for (final entry in generatedCodeMap.entries.skip(1)) { - fs.writeFileSync( - p.join(p.dirname(configOutput), p.basename(entry.key)).toJS, - entry.value.toJS); + final outputPath = p.join(p.dirname(configOutput), entry.key); + fs.mkdirSync( + p.dirname(outputPath).toJS, JSMkdirOptions(recursive: true.toJS)); + fs.writeFileSync(outputPath.toJS, entry.value.toJS); } } } diff --git a/web_generator/lib/src/interop_gen/generate.dart b/web_generator/lib/src/interop_gen/generate.dart new file mode 100644 index 00000000..9db9809d --- /dev/null +++ b/web_generator/lib/src/interop_gen/generate.dart @@ -0,0 +1,140 @@ +// 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:convert'; +import 'package:code_builder/code_builder.dart'; +import 'package:dart_style/dart_style.dart'; +import 'package:path/path.dart' as p; +import '../ast/base.dart'; +import '../ast/declarations.dart'; +import '../ast/helpers.dart'; +import '../config.dart'; +import 'transform.dart'; + +void _setGlobalOptions(Config config) { + GlobalOptions.variadicArgsCount = config.functions?.varArgs ?? 4; +} + +typedef ProgramDeclarationMap = Map; + +class TransformResult { + final ProgramDeclarationMap programDeclarationMap; + final ProgramDeclarationMap commonTypes; + final Map moduleDeclarations; + final ModuleDeclaration? globalModule; + final bool multiFileOutput; + + TransformResult(this.programDeclarationMap, + {this.commonTypes = const {}, + this.moduleDeclarations = const {}, + this.globalModule}) + : multiFileOutput = programDeclarationMap.length > 1; + + // TODO(https://github.com/dart-lang/web/issues/388): Handle union of overloads + // (namespaces + functions, multiple interfaces, etc) + Map generate(Config config) { + final formatter = DartFormatter( + languageVersion: DartFormatter.latestShortStyleLanguageVersion); + + _setGlobalOptions(config); + + final moduleFileMap = moduleDeclarations.map((k, v) { + final emitter = + DartEmitter.scoped(useNullSafetySyntax: true, orderDirectives: true); + final lib = v.emit(); + + return MapEntry( + v.url == null + ? v.asReference.url + : p.relative(p.join(realPathAsDir(v.url!), '${v.name}.dart'), + from: p.dirname(v.url!)), + formatter.format('${lib.accept(emitter)}' + .replaceAll('static external', 'external static'))); + }); + if (globalModule != null) { + final emitter = + DartEmitter.scoped(useNullSafetySyntax: true, orderDirectives: true); + final lib = globalModule!.emit(); + moduleFileMap.addAll({ + '_global.dart': formatter.format('${lib.accept(emitter)}' + .replaceAll('static external', 'external static')) + }); + } + + // TODO: If modules reference the given file, then add to map, and combine + // the declarations + final mainFileMap = { + ...programDeclarationMap, + ...commonTypes, + }.map((file, declMap) { + final emitter = + DartEmitter.scoped(useNullSafetySyntax: true, orderDirectives: true); + final lib = generateLibraryForNodeMap(declMap, preamble: config.preamble); + return MapEntry( + file.replaceAll('.d.ts', '.dart'), + formatter.format('${lib.accept(emitter)}' + .replaceAll('static external', 'external static'))); + }); + + return {...mainFileMap, ...moduleFileMap}; + } +} + +Library generateLibraryForNodeMap(NodeMap declMap, + {String? preamble, + List annotations = const [], + List extraIgnores = const []}) { + final specs = declMap.values + .map((d) { + return switch (d) { + final Declaration n => n.emit(), + _ => null, + }; + }) + .nonNulls + .whereType(); + final lib = Library((l) { + // add directives via module references + for (final decl in declMap.values.whereType()) { + l.directives.add(decl.emit()); + } + + l.annotations.addAll(annotations); + + if (preamble case final preamble?) { + l.comments.addAll(const LineSplitter().convert(preamble).map((l) { + if (l.startsWith('//') && !l.startsWith('///')) { + return l.replaceFirst(RegExp(r'^\/\/\s*'), ''); + } + return l; + })); + } + var parentCaseIgnore = false; + var anonymousIgnore = false; + var tupleDecl = false; + + for (final value in declMap.values) { + if (value is TupleDeclaration) tupleDecl = true; + if (value.id.name.contains('Anonymous')) anonymousIgnore = true; + if (value case NestableDeclaration(parent: final _?)) { + parentCaseIgnore = true; + } + } + l + ..ignoreForFile.addAll({ + 'constant_identifier_names', + 'non_constant_identifier_names', + if (parentCaseIgnore) 'camel_case_types', + if (anonymousIgnore) ...[ + 'camel_case_types', + 'library_private_types_in_public_api', + 'unnecessary_parenthesis' + ], + if (tupleDecl) 'unnecessary_parenthesis', + ...extraIgnores + }) + ..body.addAll(specs); + }); + return lib; +} diff --git a/web_generator/lib/src/interop_gen/parser.dart b/web_generator/lib/src/interop_gen/parser.dart index 781ffc37..dd6a9cf2 100644 --- a/web_generator/lib/src/interop_gen/parser.dart +++ b/web_generator/lib/src/interop_gen/parser.dart @@ -7,14 +7,30 @@ import 'dart:js_interop'; import 'package:path/path.dart' as p; import '../config.dart'; +import '../js/filesystem_api.dart'; import '../js/node.dart'; import '../js/typescript.dart' as ts; +class PreProcessResult { + final List modules; + final List referenceLibs; + final List referenceTypes; + + const PreProcessResult( + {this.modules = const [], + this.referenceLibs = const [], + this.referenceTypes = const []}); +} + class ParserResult { ts.TSProgram program; Iterable files; + Map preprocessResult; - ParserResult({required this.program, required this.files}); + ParserResult( + {required this.program, + required this.files, + this.preprocessResult = const {}}); } /// Parses the given TypeScript declaration files in the [config], @@ -25,6 +41,29 @@ class ParserResult { /// options from the TS config file/config object to use alongside the compiler ParserResult parseDeclarationFiles(Config config) { final files = config.input; + // preprocess file + final preProcessResultMap = {}; + + for (final file in files) { + final contents = (fs.readFileSync( + file.toJS, JSReadFileOptions(encoding: 'utf8'.toJS)) as JSString) + .toDart; + final preProcessResult = ts.preProcessFile(contents); + + preProcessResultMap[file] = PreProcessResult( + modules: preProcessResult.ambientExternalModules?.toDart + .map((t) => t.toDart) + .toList() ?? + [], + referenceLibs: preProcessResult.libReferenceDirectives.toDart + .map((ref) => ref.fileName) + .toList(), + referenceTypes: preProcessResult.typeReferenceDirectives.toDart + .map((ref) => ref.fileName) + .toList(), + ); + } + final ignoreErrors = config.ignoreErrors; // create host for parsing TS configuration @@ -87,7 +126,8 @@ ParserResult parseDeclarationFiles(Config config) { exit(1); } - return ParserResult(program: program, files: files); + return ParserResult( + program: program, files: files, preprocessResult: preProcessResultMap); } void handleDiagnostics(List diagnostics) { diff --git a/web_generator/lib/src/interop_gen/transform.dart b/web_generator/lib/src/interop_gen/transform.dart index 87d5d678..8b5990c6 100644 --- a/web_generator/lib/src/interop_gen/transform.dart +++ b/web_generator/lib/src/interop_gen/transform.dart @@ -2,102 +2,25 @@ // 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 'dart:js_interop'; +import 'dart:math'; -import 'package:code_builder/code_builder.dart'; -import 'package:dart_style/dart_style.dart'; +import 'package:collection/collection.dart'; import 'package:meta/meta.dart'; import 'package:path/path.dart' as p; import '../ast/base.dart'; import '../ast/declarations.dart'; -import '../ast/helpers.dart'; import '../config.dart'; import '../js/helpers.dart'; import '../js/typescript.dart' as ts; import '../js/typescript.types.dart'; +import 'generate.dart'; import 'namer.dart'; import 'parser.dart'; import 'qualified_name.dart'; import 'transform/transformer.dart'; -void _setGlobalOptions(Config config) { - GlobalOptions.variadicArgsCount = config.functions?.varArgs ?? 4; -} - -typedef ProgramDeclarationMap = Map; - -class TransformResult { - ProgramDeclarationMap programDeclarationMap; - ProgramDeclarationMap commonTypes; - bool multiFileOutput; - - TransformResult._(this.programDeclarationMap, {this.commonTypes = const {}}) - : multiFileOutput = programDeclarationMap.length > 1; - - // TODO(https://github.com/dart-lang/web/issues/388): Handle union of overloads - // (namespaces + functions, multiple interfaces, etc) - Map generate(Config config) { - final formatter = DartFormatter( - languageVersion: DartFormatter.latestShortStyleLanguageVersion); - - _setGlobalOptions(config); - - return {...programDeclarationMap, ...commonTypes}.map((file, declMap) { - final emitter = - DartEmitter.scoped(useNullSafetySyntax: true, orderDirectives: true); - final specs = declMap.values - .map((d) { - return switch (d) { - final Declaration n => n.emit(), - final Type _ => null, - }; - }) - .nonNulls - .whereType(); - final lib = Library((l) { - if (config.preamble case final preamble?) { - l.comments.addAll(const LineSplitter().convert(preamble).map((l) { - if (l.startsWith('//')) { - return l.replaceFirst(RegExp(r'^\/\/\s*'), ''); - } - return l; - })); - } - var parentCaseIgnore = false; - var anonymousIgnore = false; - var tupleDecl = false; - - for (final value in declMap.values) { - if (value is TupleDeclaration) tupleDecl = true; - if (value.id.name.contains('Anonymous')) anonymousIgnore = true; - if (value case NestableDeclaration(parent: final _?)) { - parentCaseIgnore = true; - } - } - l - ..ignoreForFile.addAll({ - 'constant_identifier_names', - 'non_constant_identifier_names', - if (parentCaseIgnore) 'camel_case_types', - if (anonymousIgnore) ...[ - 'camel_case_types', - 'library_private_types_in_public_api', - 'unnecessary_parenthesis' - ], - if (tupleDecl) 'unnecessary_parenthesis', - }) - ..body.addAll(specs); - }); - return MapEntry( - file.replaceAll('.d.ts', '.dart'), - formatter.format('${lib.accept(emitter)}' - .replaceAll('static external', 'external static'))); - }); - } -} - /// A map of declarations, where the key is the declaration's stringified [ID]. extension type NodeMap._(Map decls) implements Map { @@ -137,6 +60,24 @@ extension type TypeMap._(Map types) implements NodeMap { void add(Type decl) => types[decl.id.toString()] = decl; } +String commonDir(String a, String b) { + final partsA = p.split(p.normalize(a)); + final partsB = p.split(p.normalize(b)); + + final common = []; + final length = min(partsA.length, partsB.length); + + for (var i = 0; i < length; i++) { + if (partsA[i] == partsB[i]) { + common.add(partsA[i]); + } else { + break; + } + } + + return common.isEmpty ? '.' : p.joinAll(common); +} + /// A program map is a map used for handling the context of /// transforming and resolving declarations across files in the project. /// @@ -177,51 +118,185 @@ class ProgramMap { /// The files in the given project final p.PathSet files; + String get basePath => files.length == 1 + ? (p.extension(files.single!) == '' + ? files.single! + : p.dirname(files.single!)) + : files.reduce((prev, next) { + if (prev == null && next == null) { + return null; + } else if (prev == null) { + return p.extension(next!) == '' ? next : p.dirname(next); + } else if (next == null) { + return p.extension(prev) == '' ? prev : p.dirname(prev); + } + return commonDir(prev, next); + })!; + final List filterDeclSet; final bool generateAll; + /// A map of file paths to the modules they define + final p.PathMap> moduleMap; + + /// A map of module names to their respective modules + final NodeMap moduleDeclarations = NodeMap({}); + + /// A reference to the global module declaration, if any + ModuleDeclaration? globalModule; + + bool isDefinedModule(String name) { + return moduleMap.values.any((v) => v.contains(name)); + } + + bool isDefinedModuleInFile(String file, String name) { + return moduleMap[file]?.contains(name) ?? false; + } + + bool isBuiltModule(String name) { + return moduleDeclarations.keys.contains(name); + } + ProgramMap(this.program, List files, - {this.filterDeclSet = const [], bool? generateAll}) + {this.filterDeclSet = const [], + bool? generateAll, + p.PathMap>? moduleMap}) : typeChecker = program.getTypeChecker(), generateAll = generateAll ?? false, - files = p.PathSet.of(files); + files = p.PathSet.of(files), + moduleMap = moduleMap ?? p.PathMap(); + + /// Transforms a given module child declaration by finding the file where it + /// is defined and transforming it /// Find the node definition for a given declaration named [declName] /// or associated with a TypeScript node [node] from the map of files - List? getDeclarationRef(String file, TSNode node, [String? declName]) { - // check - NodeMap nodeMap; - if (_pathMap.containsKey(file)) { - nodeMap = _pathMap[file]!; - } else { - final src = program.getSourceFile(file); - - final transformer = - _activeTransformers.putIfAbsent(file, () => Transformer(this, src)); + /// + /// [context] provides the context of the import, for resolving relative + /// imports and [file] is the file to search in, if not provided, it will + /// search in the global module or the module that contains the file. + List? getDeclarationRef(String file, TSNode node, {String? declName}) { + Transformer? getTransformer( + String filePath, { + ModuleDeclaration? module, + }) { + final src = program.getSourceFile(filePath); + + final transformer = _activeTransformers.putIfAbsent( + filePath, () => Transformer(this, src)); if (!transformer.nodes.contains(node)) { - if (declName case final d? + if (module != null) { + // just transform the node + final transformedDecls = transformer.transformNode(node); + switch (node.kind) { + case TSSyntaxKind.ClassDeclaration || + TSSyntaxKind.InterfaceDeclaration: + final outputDecl = transformedDecls.first as TypeDeclaration; + outputDecl.parent = module; + module.nestableDeclarations.add(outputDecl); + case TSSyntaxKind.EnumDeclaration: + final outputDecl = transformedDecls.first as EnumDeclaration; + outputDecl.parent = module; + module.nestableDeclarations.add(outputDecl); + default: + module.topLevelDeclarations.addAll(transformedDecls); + } + module.nodes.add(node); + } else if (declName case final d? when transformer.nodeMap.findByName(d).isEmpty) { - // find the source file decl - if (src == null) return null; - - final symbol = typeChecker.getSymbolAtLocation(src)!; + // fetch the symbol for the node + // and transform the associated declaration + final symbol = typeChecker.getSymbolAtLocation(src!)!; final exports = symbol.exports?.toDart ?? {}; final targetSymbol = exports[d.toJS]!; - transformer.transform(targetSymbol.getDeclarations()!.toDart.first); } else { transformer.transform(node); } } - nodeMap = transformer.filterAndReturn(); - _activeTransformers[file] = transformer; + return transformer; } + // check + var nodeMap = NodeMap(); final name = declName ?? (node as TSNamedDeclaration).name?.text; + + // search through modules first + if (file == 'global' || file.isEmpty) { + globalModule ??= ModuleDeclaration.global(); + final globalNodeMap = globalModule!.nodeMap; + if ((name != null && globalNodeMap.findByName(name).isEmpty) || + !globalModule!.nodes.contains(node)) { + final fileName = node.getSourceFile().fileName; + final transformer = getTransformer(fileName, module: globalModule); + + if (transformer == null) { + // if no transformer, then we cannot find the node + return null; + } + } + nodeMap = globalModule!.nodeMap; + } + final moduleID = ID(name: file, type: 'module').toString(); + if (moduleDeclarations.containsKey(moduleID)) { + final module = moduleDeclarations[moduleID]!; + if ((name != null && module.nodeMap.findByName(name).isEmpty) || + !module.nodes.contains(node)) { + final fileName = node.getSourceFile().fileName; + final transformer = getTransformer(fileName, module: module); + + moduleDeclarations[moduleID] = module; + + if (transformer == null) { + // if no transformer, then we cannot find the node + // for a specific module, a file must be found + throw Exception( + 'Could not find transformer containing the given module $file'); + } + } + + nodeMap = moduleDeclarations[moduleID]!.nodeMap; + } else if (moduleMap.entries.where((entry) => entry.value.contains(file)) + case final targetModules when targetModules.isNotEmpty) { + // if the file is a module, we need to find the node map for the module + for (final MapEntry(key: moduleFile) in targetModules) { + // ensure module is transformed + if (moduleFile != null) { + final nm = getNodeMap(moduleFile); + final moduleDecl = nm.values + .whereType() + .firstWhereOrNull((m) => m.id.name == moduleFile); + + if (moduleDecl != null) { + nodeMap = moduleDecl.reference.nodeMap; + break; + } + } + } + } else { + final fileWithExt = file.endsWith('.d.ts') + ? file + : '$file.d.ts'; // ensure we have the correct file extension + + // if not, search through files + if (_pathMap.containsKey(fileWithExt)) { + nodeMap = _pathMap[fileWithExt]!; + } else { + final transformer = getTransformer(fileWithExt); + if (transformer == null) { + // if no transformer, then we cannot find the node + return null; + } + + nodeMap = transformer.filterAndReturn(); + _activeTransformers[fileWithExt] = transformer; + } + } + return name == null ? null : nodeMap.findByName(name); } @@ -283,6 +358,11 @@ class ProgramMap { } else { final exportedSymbols = sourceSymbol.exports?.toDart; + // global modules are not captured as exports + final globalModuleDeclarations = src.statements.toDart.where((s) => + s.kind == TSSyntaxKind.ModuleDeclaration && + ((s as TSModuleDeclaration).name as TSIdentifier).text == 'global'); + for (final MapEntry(value: symbol) in exportedSymbols?.entries ?? >[]) { final decls = symbol.getDeclarations()?.toDart ?? []; @@ -296,6 +376,10 @@ class ProgramMap { _activeTransformers[absolutePath]!.transform(decl); } } + + for (final module in globalModuleDeclarations) { + _activeTransformers[absolutePath]!.transform(module); + } } return _activeTransformers[absolutePath]!.filterAndReturn(); @@ -323,7 +407,9 @@ class TransformerManager { TransformerManager.fromParsedResults(ParserResult result, {Config? config}) : programMap = ProgramMap(result.program, result.files.toList(), filterDeclSet: config?.includedDeclarations ?? [], - generateAll: config?.generateAll); + generateAll: config?.generateAll, + moduleMap: p.PathMap.of( + result.preprocessResult.map((k, v) => MapEntry(k, v.modules)))); TransformResult transform() { final outputNodeMap = {}; @@ -333,7 +419,9 @@ class TransformerManager { outputNodeMap[file!] = programMap.getNodeMap(file); } - return TransformResult._(outputNodeMap, - commonTypes: programMap._commonTypes.cast()); + return TransformResult(outputNodeMap, + commonTypes: programMap._commonTypes.cast(), + moduleDeclarations: programMap.moduleDeclarations, + globalModule: programMap.globalModule); } } diff --git a/web_generator/lib/src/interop_gen/transform/transformer.dart b/web_generator/lib/src/interop_gen/transform/transformer.dart index 167ed26e..04b22dac 100644 --- a/web_generator/lib/src/interop_gen/transform/transformer.dart +++ b/web_generator/lib/src/interop_gen/transform/transformer.dart @@ -98,17 +98,31 @@ class Transformer { void transform(TSNode node) { if (nodes.contains(node)) return; - final decls = _transform(node); + final decls = transformNode(node); + // add reference if not global, or an extension of a particular module/file (except this one) + if (decls case [final ModuleDeclaration module] + when (module.name != 'global' && + !programMap.files.contains(p.normalize( + p.join(p.dirname(file), '${module.name}.d.ts')))) || + p.equals( + p.normalize(p.join(p.dirname(file), '${module.name}.d.ts')), + file)) { + programMap.moduleDeclarations.update( + module.id.toString(), (v) => module..url = file, + ifAbsent: () => module..url = file); + // we only need a ref to the module + nodeMap.add(module.asReference); + } nodeMap.addAll({for (final d in decls) d.id.toString(): d}); nodes.add(node); } - List _transform(TSNode node, + List transformNode(TSNode node, {Set? exportSet, UniqueNamer? namer, - NamespaceDeclaration? parent}) { + ParentDeclaration? parent}) { switch (node.kind) { case TSSyntaxKind.ImportDeclaration || TSSyntaxKind.ImportSpecifier: // We do not parse import declarations by default @@ -149,7 +163,12 @@ class Transformer { when (node as TSModuleDeclaration).name.kind == TSSyntaxKind.Identifier && (node.name as TSIdentifier).text != 'global': - return [_transformNamespace(node, namer: namer, parent: parent)]; + return [_transformModule(node, namer: namer, parent: parent)]; + case TSSyntaxKind.ModuleDeclaration: + final moduleDecl = _transformModule(node as TSModuleDeclaration, + namer: namer, parent: parent); + // TODO: return module reference + return [moduleDecl]; default: throw Exception('Unsupported Declaration Kind: ${node.kind}'); } @@ -220,43 +239,90 @@ class Transformer { documentation: _parseAndTransformDocumentation(typealias)); } - /// Transforms a TS Namespace (identified as a [TSModuleDeclaration] with - /// an identifier name that isn't "global") into a Dart Namespace - /// Representation. - NamespaceDeclaration _transformNamespace(TSModuleDeclaration namespace, - {UniqueNamer? namer, NamespaceDeclaration? parent}) { + /// Transforms a TS Namespace + /// (identified as a [TSModuleDeclaration] with + /// an identifier name that isn't "global") or Module into + /// a Dart Namespace or Module Representation respectively. + /// + /// If the module is a global module, it is transformed/augmented into + /// the global [ModuleDeclaration.global] instance. + ParentDeclaration _transformModule(TSModuleDeclaration namespace, + {UniqueNamer? namer, ParentDeclaration? parent}) { namer ??= this.namer; - final namespaceName = (namespace.name as TSIdentifier).text; + final isNamespace = (namespace.name.kind == TSSyntaxKind.Identifier && + (namespace.name as TSIdentifier).text != 'global') || + namespace.name.kind == TSSyntaxKind.QualifiedName; + + final moduleName = (namespace.name as TSIdentifier).text; // get modifiers final modifiers = namespace.modifiers?.toDart ?? []; final isExported = modifiers.any((m) { return m.kind == TSSyntaxKind.ExportKeyword; }); + final isGlobal = !isNamespace && moduleName == 'global'; + + Iterable currentModules; + final ParentDeclaration outputModule; + + if (isNamespace) { + currentModules = parent != null + ? parent.namespaceDeclarations.where((n) => n.name == moduleName) + : nodeMap.findByName(moduleName).whereType(); + final (name: dartName, :id) = currentModules.isEmpty + ? namer.makeUnique(moduleName, isNamespace ? 'namespace' : 'module') + : (name: null, id: null); + outputModule = currentModules.isNotEmpty + ? currentModules.first + : NamespaceDeclaration( + name: moduleName, + dartName: dartName, + id: id!, + exported: isExported, + topLevelDeclarations: {}, + namespaceDeclarations: {}, + nestableDeclarations: {}, + documentation: _parseAndTransformDocumentation(namespace)); + } else if (isGlobal) { + // add to global module + programMap.globalModule ??= ModuleDeclaration.global( + topLevelDeclarations: {}, + namespaceDeclarations: {}, + nestableDeclarations: {}, + ); + + currentModules = [programMap.globalModule!]; + outputModule = programMap.globalModule!; + } else { + currentModules = + nodeMap.findByName(moduleName).whereType(); + if (currentModules.isEmpty) { + currentModules = programMap.moduleDeclarations.values + .where((m) => m.name == moduleName); + } - final currentNamespaces = parent != null - ? parent.namespaceDeclarations.where((n) => n.name == namespaceName) - : nodeMap.findByName(namespaceName).whereType(); - - final (name: dartName, :id) = currentNamespaces.isEmpty - ? namer.makeUnique(namespaceName, 'namespace') - : (name: null, id: null); + final (name: dartName, :id) = currentModules.isEmpty + ? (isNamespace + ? namer.makeUnique( + moduleName, isNamespace ? 'namespace' : 'module') + : (name: moduleName, id: ID(type: 'module', name: moduleName))) + : (name: null, id: null); + + outputModule = currentModules.isNotEmpty + ? currentModules.first + : ModuleDeclaration( + name: moduleName, + dartName: dartName, + topLevelDeclarations: {}, + namespaceDeclarations: {}, + nestableDeclarations: {}, + documentation: _parseAndTransformDocumentation(namespace), + url: file); + } final scopedNamer = ScopedUniqueNamer(); - final outputNamespace = currentNamespaces.isNotEmpty - ? currentNamespaces.first - : NamespaceDeclaration( - name: namespaceName, - dartName: dartName, - id: id!, - exported: isExported, - topLevelDeclarations: {}, - namespaceDeclarations: {}, - nestableDeclarations: {}, - documentation: _parseAndTransformDocumentation(namespace)); - // TODO: We can implement this in classes and interfaces. // however, since namespaces and modules are a thing, // let's keep that in mind @@ -264,18 +330,24 @@ class Transformer { /// allowing cross-references between types and declarations in the /// namespace, including the namespace itself void updateNSInParent() { - if (parent != null) { - if (currentNamespaces.isNotEmpty || - parent.namespaceDeclarations.any((n) => n.name == namespaceName)) { - parent.namespaceDeclarations.remove(currentNamespaces.first); - parent.namespaceDeclarations.add(outputNamespace); + if (isGlobal) { + programMap.globalModule = outputModule as ModuleDeclaration; + } else if (parent != null) { + if (currentModules.isNotEmpty || + parent.namespaceDeclarations.any((n) => n.name == moduleName)) { + parent.namespaceDeclarations.remove(currentModules.first); + parent.namespaceDeclarations.add(outputModule); } else { - outputNamespace.parent = parent; - parent.namespaceDeclarations.add(outputNamespace); + outputModule.parent = parent; + parent.namespaceDeclarations.add(outputModule); } + } else if (!isNamespace) { + programMap.moduleDeclarations.update(outputModule.id.toString(), + (v) => outputModule as ModuleDeclaration, + ifAbsent: () => outputModule as ModuleDeclaration); } else { - nodeMap.update(outputNamespace.id.toString(), (v) => outputNamespace, - ifAbsent: () => outputNamespace); + nodeMap.update(outputModule.id.toString(), (v) => outputModule, + ifAbsent: () => outputModule); } } @@ -295,26 +367,34 @@ class Transformer { } catch (_) { // throws error if no aliased symbol, so ignore } + for (final decl in decls) { // TODO: We could also ignore namespace decls with the same name, as // a single instance should consider such non-necessary - if (outputNamespace.nodes.contains(decl)) continue; + if (outputModule.nodes.contains(decl)) continue; + if (decl.kind == TSSyntaxKind.ExportSpecifier && decls.length > 1) { + continue; + } final outputDecls = - _transform(decl, namer: scopedNamer, parent: outputNamespace); + transformNode(decl, namer: scopedNamer, parent: outputModule); switch (decl.kind) { + case TSSyntaxKind.ModuleDeclaration + when outputDecls.first is ModuleDeclaration && + outputDecls.first.name == 'global': + break; case TSSyntaxKind.ClassDeclaration || TSSyntaxKind.InterfaceDeclaration: final outputDecl = outputDecls.first as TypeDeclaration; - outputDecl.parent = outputNamespace; - outputNamespace.nestableDeclarations.add(outputDecl); + outputDecl.parent = outputModule; + outputModule.nestableDeclarations.add(outputDecl); case TSSyntaxKind.EnumDeclaration: final outputDecl = outputDecls.first as EnumDeclaration; - outputDecl.parent = outputNamespace; - outputNamespace.nestableDeclarations.add(outputDecl); + outputDecl.parent = outputModule; + outputModule.nestableDeclarations.add(outputDecl); default: - outputNamespace.topLevelDeclarations.addAll(outputDecls); + outputModule.topLevelDeclarations.addAll(outputDecls); } - outputNamespace.nodes.add(decl); + outputModule.nodes.add(decl); // update namespace state updateNSInParent(); @@ -326,20 +406,24 @@ class Transformer { when namespaceBody.kind == TSSyntaxKind.ModuleBlock) { for (final statement in (namespaceBody as TSModuleBlock).statements.toDart) { - final outputDecls = _transform(statement, - namer: scopedNamer, parent: outputNamespace); + final outputDecls = transformNode(statement, + namer: scopedNamer, parent: outputModule); switch (statement.kind) { + case TSSyntaxKind.ModuleDeclaration + when outputDecls.first is ModuleDeclaration && + outputDecls.first.name == 'global': + break; case TSSyntaxKind.ClassDeclaration || TSSyntaxKind.InterfaceDeclaration: final outputDecl = outputDecls.first as TypeDeclaration; - outputDecl.parent = outputNamespace; - outputNamespace.nestableDeclarations.add(outputDecl); + outputDecl.parent = outputModule; + outputModule.nestableDeclarations.add(outputDecl); case TSSyntaxKind.EnumDeclaration: final outputDecl = outputDecls.first as EnumDeclaration; - outputDecl.parent = outputNamespace; - outputNamespace.nestableDeclarations.add(outputDecl); + outputDecl.parent = outputModule; + outputModule.nestableDeclarations.add(outputDecl); default: - outputNamespace.topLevelDeclarations.addAll(outputDecls); + outputModule.topLevelDeclarations.addAll(outputDecls); } // update namespace state @@ -347,8 +431,8 @@ class Transformer { } } else if (namespace.body case final namespaceBody?) { // namespace import - _transformNamespace(namespaceBody as TSNamespaceDeclaration, - namer: scopedNamer, parent: outputNamespace); + _transformModule(namespaceBody as TSNamespaceDeclaration, + namer: scopedNamer, parent: outputModule); } } @@ -359,7 +443,7 @@ class Transformer { namer.markUsedSet(scopedNamer); // get the exported symbols from the namespace - return outputNamespace; + return outputModule; } /// Transforms a TS Class or Interface declaration into a node representing @@ -1409,11 +1493,47 @@ class Transformer { /// The referred type may accept [typeArguments], which are passed as well. Type _searchForDeclRecursive( Iterable name, TSSymbol symbol, - {NamespaceDeclaration? parent, + {ParentDeclaration? parent, List? typeArguments, bool isNotTypableDeclaration = false, bool typeArg = false, - bool isNullable = false}) { + bool isNullable = false, + TSImportSpecifer? importSpecifier, + ID? moduleID, + bool global = false}) { + if (global && parent == null) { + final module = programMap.globalModule ??= ModuleDeclaration.global(); + + final searchForDeclRecursive = _searchForDeclRecursive(name, symbol, + typeArguments: typeArguments, + typeArg: typeArg, + parent: module, + isNullable: isNullable, + global: true, + importSpecifier: importSpecifier); + + programMap.globalModule = module; + + return searchForDeclRecursive; + } + if (moduleID != null && parent == null) { + final module = programMap.moduleDeclarations[moduleID.toString()]; + if (module == null) { + throw Exception('Module with ID $moduleID not found in program map'); + } + // fast forward + final searchForDeclRecursive = _searchForDeclRecursive(name, symbol, + typeArguments: typeArguments, + typeArg: typeArg, + parent: module, + isNullable: isNullable, + moduleID: moduleID, + importSpecifier: importSpecifier); + + programMap.moduleDeclarations[moduleID.toString()] = module; + + return searchForDeclRecursive; + } // get name and map final firstName = name.first.part; @@ -1430,8 +1550,13 @@ class Transformer { if (declarationsMatching.isEmpty) { // if not referred type, then check here // transform - final declarations = symbol.getDeclarations()?.toDart ?? []; + var declarations = symbol.getDeclarations()?.toDart ?? []; var firstDecl = declarations.first as TSNamedDeclaration; + if (firstDecl.kind == TSSyntaxKind.ExportSpecifier && + declarations.length > 1) { + declarations = declarations.skip(1).toList(); + firstDecl = declarations.first as TSNamedDeclaration; + } if (firstDecl.kind == TSSyntaxKind.ExportSpecifier) { // in order to prevent recursion, we need to find the source of the @@ -1466,7 +1591,7 @@ class Transformer { : null; // TODO: multi-decls final transformedDecls = - _transform(firstDecl, namer: namer, parent: parent); + transformNode(firstDecl, namer: namer, parent: parent); if (parent != null) { switch (firstDecl.kind) { @@ -1515,11 +1640,42 @@ class Transformer { } } + String? relativeImport; + + if (parent is ModuleDeclaration && parent.name == 'global') { + relativeImport = p.relative( + p.normalize(p.absolute(p.join( + programMap.basePath, + '_global.dart', + ))), + from: p.dirname(file)); + } + + final importSource = importSpecifier?.parent.parent.parent.parent; + if (importSource != null && + importSource.kind == TSSyntaxKind.ModuleBlock) { + // get module name + final module = (importSource as TSModuleBlock).parent; + final moduleName = (module.name as TSIdentifier).text; + final moduleReference = + programMap.moduleDeclarations.findByName(moduleName).first; + + relativeImport = parent is ModuleDeclaration && parent.url != null + ? p.relative( + p.normalize(p.join(p.absolute(realPathAsDir(parent.url ?? '.')), + '${parent.name}.dart')), + from: p.dirname(p.normalize(p.join( + p.absolute(realPathAsDir(moduleReference.url ?? '.')), + '$moduleName.dart')))) + : null; + } + final asReferredType = decl.asReferredType( (typeArguments ?? []) .map((type) => _transformType(type, typeArg: true)) .toList(), - isNullable); + isNullable, + relativeImport); if (asReferredType case ReferredDeclarationType(type: final type) when type is BuiltinType) { @@ -1577,18 +1733,19 @@ class Transformer { final type = typeChecker.getTypeFromTypeNode(node); // from type: if symbol is null, or references an import var symbol = typeChecker.getSymbolAtLocation(typeName); + TSImportSpecifer? importSpecifier; if (symbol == null) { symbol = type?.aliasSymbol ?? type?.symbol; } else if (symbol.getDeclarations()?.toDart ?? [] case [final d] - when d.kind == TSSyntaxKind.ImportSpecifier || - d.kind == TSSyntaxKind.ImportEqualsDeclaration) { + when d.kind == TSSyntaxKind.ImportSpecifier) { + importSpecifier = d as TSImportSpecifer; // prefer using type node ref for such cases // reduces import declaration handling symbol = type?.aliasSymbol ?? type?.symbol; } return _getTypeFromSymbol(symbol, type, typeArguments, - isNotTypableDeclaration, typeArg, isNullable); + isNotTypableDeclaration, typeArg, isNullable, importSpecifier); } /// Given a [TSSymbol] for a given TS node or declaration, and its associated @@ -1612,7 +1769,8 @@ class Transformer { List? typeArguments, bool isNotTypableDeclaration, bool typeArg, - bool isNullable) { + bool isNullable, + [TSImportSpecifer? importSpecifier]) { final declarations = symbol!.getDeclarations()?.toDart ?? []; // get decl qualified name @@ -1622,7 +1780,21 @@ class Transformer { final (fullyQualifiedName, nameImport) = parseTSFullyQualifiedName(tsFullyQualifiedName); - if (nameImport == null) { + if (declarations.first case final node + when node.parent.kind == TSSyntaxKind.ModuleBlock && + fullyQualifiedName.first.part == 'global') { + // global module + final parentNode = node.parent.parent as TSModuleDeclaration; + transformNode(parentNode); + + return _searchForDeclRecursive(fullyQualifiedName.skip(1), symbol, + typeArguments: typeArguments, + typeArg: typeArg, + isNotTypableDeclaration: isNotTypableDeclaration, + isNullable: isNullable, + global: true, + importSpecifier: importSpecifier); + } else if (nameImport == null) { // if import not there, most likely from an import if (type?.isTypeParameter() ?? false) { @@ -1669,10 +1841,15 @@ class Transformer { final namedImport = decl as TSImportSpecifer; final importDecl = namedImport.parent.parent.parent; var importUrl = importDecl.moduleSpecifier.text; - if (!importUrl.endsWith('ts')) importUrl = '$importUrl.d.ts'; - declSource = - p.normalize(p.absolute(p.join(p.dirname(file), importUrl))); + if (programMap.isDefinedModule(importUrl)) { + declSource = importUrl; + } else { + if (!importUrl.endsWith('ts')) importUrl = '$importUrl.d.ts'; + + declSource = + p.normalize(p.absolute(p.join(p.dirname(file), importUrl))); + } mustImport = true; } @@ -1683,12 +1860,15 @@ class Transformer { if ((programMap.files.contains(declSource) && !p.equals(declSource, file)) || mustImport) { + // relative path is null for modules as we cannot resolve the locations + // correctly at transformation time if (programMap.files.contains(declSource) && !p.equals(declSource, file)) { relativePath = p.relative(declSource, from: p.dirname(file)); } final referencedDeclarations = programMap.getDeclarationRef( - declSource, decl, fullyQualifiedName.asName); + declSource, decl, + declName: fullyQualifiedName.asName); firstNode = referencedDeclarations?.whereType().first; } else { @@ -1721,6 +1901,7 @@ class Transformer { .map((type) => _transformType(type, typeArg: true)) .toList(), isNullable, + // TODO: Module Context Apply relativePath?.replaceFirst('.d.ts', '.dart')); if (outputType case ReferredDeclarationType(type: final type) @@ -1735,7 +1916,13 @@ class Transformer { } } else { final filePathWithoutExtension = file.replaceFirst('.d.ts', ''); - if (p.equals(nameImport, filePathWithoutExtension)) { + final nameIDImport = ID(name: nameImport, type: 'module'); + + // if import is equal to this file (defined in the file) + // or module declarations already have this, or the moduleMap + if (p.equals(nameImport, filePathWithoutExtension) || + (programMap.moduleDeclarations.containsKey(nameIDImport.toString()) || + (programMap.moduleMap[file]?.contains(nameImport) ?? false))) { // declared in this file // if import there and this file, handle this file @@ -1752,7 +1939,9 @@ class Transformer { typeArguments: typeArguments, typeArg: typeArg, isNotTypableDeclaration: isNotTypableDeclaration, - isNullable: isNullable); + isNullable: isNullable, + moduleID: nameIDImport, + importSpecifier: importSpecifier); } else { // if import there and not this file, imported from specified file final importUrl = @@ -1761,8 +1950,8 @@ class Transformer { ? p.relative(importUrl, from: p.dirname(file)) : null; final referencedDeclarations = declarations.map((decl) { - return programMap.getDeclarationRef( - importUrl, decl, fullyQualifiedName.asName); + return programMap.getDeclarationRef(nameImport, decl, + declName: fullyQualifiedName.asName); }).reduce((prev, next) => [if (prev != null) ...prev, if (next != null) ...next]); @@ -1802,7 +1991,10 @@ class Transformer { } } } - throw Exception('Could not resolve type for node'); + + throw Exception( + 'Could not resolve type for node ${fullyQualifiedName.asName} ' + 'from ${nameImport ?? '(no import)'} with symbol $symbol'); } /// Get the type of a type node named [typeName] by referencing its @@ -1828,11 +2020,12 @@ class Transformer { /// supported `dart:js_interop` types and related [EnumDeclaration]-like and /// [TypeDeclaration]-like checks Type _getTypeFromDeclaration( - @UnionOf([TSIdentifier, TSQualifiedName]) TSNode typeName, - List? typeArguments, - {bool typeArg = false, - bool isNotTypableDeclaration = false, - bool isNullable = false}) { + @UnionOf([TSIdentifier, TSQualifiedName]) TSNode typeName, + List? typeArguments, { + bool typeArg = false, + bool isNotTypableDeclaration = false, + bool isNullable = false, + }) { // union assertion assert(typeName.kind == TSSyntaxKind.Identifier || typeName.kind == TSSyntaxKind.QualifiedName); @@ -2025,6 +2218,10 @@ class Transformer { filteredDeclarations.add(e); } break; + case ModuleDeclaration(): + break; + case final ModuleReference ref: + filteredDeclarations.add(ref); case Declaration(): // TODO: Handle this case. throw UnimplementedError(); @@ -2037,153 +2234,149 @@ class Transformer { // then filter for dependencies final otherDecls = filteredDeclarations.entries - .map((e) => _getDependenciesOfDecl(e.value)) + .map((e) => getDependenciesOfDecl(e.value)) .reduce((value, element) => value..addAll(element)); filteredDeclarations.addAll(otherDecls); return filteredDeclarations; } +} - /// Given an already filtered declaration [decl], - /// filter out dependencies of [decl] recursively - /// and return them as a declaration map - NodeMap _getDependenciesOfDecl(Node? decl, [NodeMap? context]) { - NodeMap getCallableDependencies(CallableDeclaration callable) { - return NodeMap({ - for (final node in callable.parameters.map((p) => p.type)) - node.id.toString(): node, - for (final node in callable.typeParameters - .map((p) => p.constraint) - .whereType()) - node.id.toString(): node, - callable.returnType.id.toString(): callable.returnType - }); - } +/// Given an already filtered declaration [decl], +/// filter out dependencies of [decl] recursively +/// and return them as a declaration map +NodeMap getDependenciesOfDecl(Node? decl, [NodeMap? context]) { + NodeMap getCallableDependencies(CallableDeclaration callable) { + return NodeMap({ + for (final node in callable.parameters.map((p) => p.type)) + node.id.toString(): node, + for (final node + in callable.typeParameters.map((p) => p.constraint).whereType()) + node.id.toString(): node, + callable.returnType.id.toString(): callable.returnType + }); + } - void updateFilteredDeclsForDecl(Node? decl, NodeMap filteredDeclarations) { - switch (decl) { - case final VariableDeclaration v: - filteredDeclarations.add(v.type); - break; - case final CallableDeclaration f: - filteredDeclarations.addAll(getCallableDependencies(f)); - break; - case final EnumDeclaration _: - break; - case final TypeAliasDeclaration t: - filteredDeclarations.add(t.type); - break; - case final TypeDeclaration t: - for (final con in t.constructors) { - filteredDeclarations.addAll({ - for (final param in con.parameters.map((p) => p.type)) - param.id.toString(): param - }); - } - for (final methods in t.methods) { - filteredDeclarations.addAll(getCallableDependencies(methods)); - } - for (final operators in t.operators) { - filteredDeclarations.addAll(getCallableDependencies(operators)); - } - filteredDeclarations.addAll({ - for (final prop in t.properties - .map((p) => p.type) - .where((p) => p is! BuiltinType)) - prop.id.toString(): prop, - }); - switch (t) { - case ClassDeclaration( - extendedType: final extendedType, - implementedTypes: final implementedTypes - ): - if (extendedType case final ext? when ext is! BuiltinType) { - filteredDeclarations.add(ext); - } - filteredDeclarations.addAll({ - for (final impl - in implementedTypes.where((i) => i is! BuiltinType)) - impl.id.toString(): impl, - }); - break; - case InterfaceDeclaration(extendedTypes: final extendedTypes): - filteredDeclarations.addAll({ - for (final impl - in extendedTypes.where((i) => i is! BuiltinType)) - impl.id.toString(): impl, - }); - break; - } - case NamespaceDeclaration( - topLevelDeclarations: final topLevelDecls, - nestableDeclarations: final typeDecls, - namespaceDeclarations: final namespaceDecls, - ): - for (final tlDecl in [...typeDecls, ...namespaceDecls]) { - filteredDeclarations.add(tlDecl); - updateFilteredDeclsForDecl(tlDecl, filteredDeclarations); - } - for (final topLevelDecl in topLevelDecls) { - updateFilteredDeclsForDecl(topLevelDecl, filteredDeclarations); - } - break; - // TODO: We can make (DeclarationAssociatedType) and use that - // rather than individual type names - case final HomogenousEnumType hu: - filteredDeclarations.add(hu.declaration); - break; - case final TupleType t: - filteredDeclarations.addAll({ - for (final t in t.types.where((t) => t is! BuiltinType)) - t.id.toString(): t - }); - case final UnionType u: - filteredDeclarations.addAll({ - for (final t in u.types.where((t) => t is! BuiltinType)) - t.id.toString(): t - }); - filteredDeclarations.add(u.declaration); - case final DeclarationType d: - filteredDeclarations.add(d.declaration); - break; - case BuiltinType(typeParams: final typeParams) - when typeParams.isNotEmpty: + void updateFilteredDeclsForDecl(Node? decl, NodeMap filteredDeclarations) { + switch (decl) { + case final VariableDeclaration v: + filteredDeclarations.add(v.type); + break; + case final CallableDeclaration f: + filteredDeclarations.addAll(getCallableDependencies(f)); + break; + case final EnumDeclaration _: + break; + case final TypeAliasDeclaration t: + filteredDeclarations.add(t.type); + break; + case final TypeDeclaration t: + for (final con in t.constructors) { filteredDeclarations.addAll({ - for (final t in typeParams.where((t) => t is! BuiltinType)) - t.id.toString(): t + for (final param in con.parameters.map((p) => p.type)) + param.id.toString(): param }); - break; - case final ReferredType r: - if (r.url == null) filteredDeclarations.add(r.declaration); - break; - case BuiltinType() || GenericType(): - break; - default: - print('WARN: The given node type ${decl.runtimeType.toString()} ' - 'is not supported for filtering. Skipping...'); - break; - } + } + for (final methods in t.methods) { + filteredDeclarations.addAll(getCallableDependencies(methods)); + } + for (final operators in t.operators) { + filteredDeclarations.addAll(getCallableDependencies(operators)); + } + filteredDeclarations.addAll({ + for (final prop in t.properties + .map((p) => p.type) + .where((p) => p is! BuiltinType)) + prop.id.toString(): prop, + }); + switch (t) { + case ClassDeclaration( + extendedType: final extendedType, + implementedTypes: final implementedTypes + ): + if (extendedType case final ext? when ext is! BuiltinType) { + filteredDeclarations.add(ext); + } + filteredDeclarations.addAll({ + for (final impl + in implementedTypes.where((i) => i is! BuiltinType)) + impl.id.toString(): impl, + }); + break; + case InterfaceDeclaration(extendedTypes: final extendedTypes): + filteredDeclarations.addAll({ + for (final impl in extendedTypes.where((i) => i is! BuiltinType)) + impl.id.toString(): impl, + }); + break; + } + case NamespaceDeclaration( + topLevelDeclarations: final topLevelDecls, + nestableDeclarations: final typeDecls, + namespaceDeclarations: final namespaceDecls, + ): + for (final tlDecl in [...typeDecls, ...namespaceDecls]) { + filteredDeclarations.add(tlDecl); + updateFilteredDeclsForDecl(tlDecl, filteredDeclarations); + } + for (final topLevelDecl in topLevelDecls) { + updateFilteredDeclsForDecl(topLevelDecl, filteredDeclarations); + } + break; + // TODO: We can make (DeclarationAssociatedType) and use that + // rather than individual type names + case final HomogenousEnumType hu: + filteredDeclarations.add(hu.declaration); + break; + case final TupleType t: + filteredDeclarations.addAll({ + for (final t in t.types.where((t) => t is! BuiltinType)) + t.id.toString(): t + }); + case final UnionType u: + filteredDeclarations.addAll({ + for (final t in u.types.where((t) => t is! BuiltinType)) + t.id.toString(): t + }); + filteredDeclarations.add(u.declaration); + case final DeclarationType d: + filteredDeclarations.add(d.declaration); + break; + case BuiltinType(typeParams: final typeParams) when typeParams.isNotEmpty: + filteredDeclarations.addAll({ + for (final t in typeParams.where((t) => t is! BuiltinType)) + t.id.toString(): t + }); + break; + case final ReferredType r: + if (r.url == null) filteredDeclarations.add(r.declaration); + break; + case BuiltinType() || GenericType(): + break; + default: + print('WARN: The given node type ${decl.runtimeType.toString()} ' + 'is not supported for filtering. Skipping...'); + break; } + } - final filteredDeclarations = NodeMap(); - - updateFilteredDeclsForDecl(decl, filteredDeclarations); + final filteredDeclarations = NodeMap(); - filteredDeclarations - .removeWhere((k, v) => context?.containsKey(k) ?? false); + updateFilteredDeclsForDecl(decl, filteredDeclarations); - if (filteredDeclarations.isNotEmpty) { - final otherDecls = filteredDeclarations.entries - .map((e) => _getDependenciesOfDecl( - e.value, NodeMap({...(context ?? {}), ...filteredDeclarations}))) - .reduce((value, element) => value..addAll(element)); + filteredDeclarations.removeWhere((k, v) => context?.containsKey(k) ?? false); - filteredDeclarations.addAll(otherDecls); - } + if (filteredDeclarations.isNotEmpty) { + final otherDecls = filteredDeclarations.entries + .map((e) => getDependenciesOfDecl( + e.value, NodeMap({...(context ?? {}), ...filteredDeclarations}))) + .reduce((value, element) => value..addAll(element)); - return filteredDeclarations; + filteredDeclarations.addAll(otherDecls); } + + return filteredDeclarations; } ({bool isReadonly, bool isStatic, DeclScope scope}) _parseModifiers( diff --git a/web_generator/lib/src/js/typescript.dart b/web_generator/lib/src/js/typescript.dart index 5e2bfe70..eaf8aced 100644 --- a/web_generator/lib/src/js/typescript.dart +++ b/web_generator/lib/src/js/typescript.dart @@ -51,6 +51,10 @@ external TSParsedCommandLine parseJsonConfigFileContent( JSObject json, TSParseConfigFileHost host, String basePath, [TSCompilerOptions existingOptions, String configFileName]); +@JS() +external TSPreProcessedFileInfo preProcessFile(String sourceText, + [bool readImportFiles, bool detectJavaScriptImports]); + @JS() external TSParseConfigFileHost sys; @@ -123,6 +127,19 @@ extension type OnUnRecoverableConfigFileDiagnosticFunc(JSFunction _) external JSAny call(TSDiagnostic diagnostic); } +@JS('PreProcessedFileInfo') +extension type TSPreProcessedFileInfo._(JSObject _) implements JSObject { + external JSArray referencedFiles; + external JSArray typeReferenceDirectives; + external JSArray libReferenceDirectives; + external JSArray? ambientExternalModules; +} + +@JS('FileReference') +extension type TSFileReference._(JSObject _) implements JSObject { + external String fileName; +} + @JS('CompilerOptions') extension type TSCompilerOptions._(JSObject _) implements JSObject { external TSCompilerOptions({bool? allowJs, bool? declaration}); @@ -198,6 +215,7 @@ extension type TSSourceFile._(JSObject _) implements TSDeclaration { external String fileName; external String? moduleName; external String text; + external TSNodeArray get statements; } extension type TSNodeCallback._(JSObject _) diff --git a/web_generator/test/integration/interop_gen/_global.dart b/web_generator/test/integration/interop_gen/_global.dart new file mode 100644 index 00000000..80230fad --- /dev/null +++ b/web_generator/test/integration/interop_gen/_global.dart @@ -0,0 +1,16 @@ +// ignore_for_file: camel_case_types, constant_identifier_names +// ignore_for_file: non_constant_identifier_names + +@_i1.JS('global') +library; // ignore_for_file: no_leading_underscores_for_library_prefixes + +import 'dart:js_interop' as _i1; + +@_i1.JS('Foo') +extension type Foo._(_i1.JSObject _) implements _i1.JSObject { + external String bar; +} +@_i1.JS('Hanlenle') +extension type Hanlenle._(_i1.JSObject _) implements _i1.JSObject { + external String get palava; +} diff --git a/web_generator/test/integration/interop_gen/global_modules_expected.dart b/web_generator/test/integration/interop_gen/global_modules_expected.dart new file mode 100644 index 00000000..df445ff2 --- /dev/null +++ b/web_generator/test/integration/interop_gen/global_modules_expected.dart @@ -0,0 +1,11 @@ +// 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; + +import '_global.dart' as _i2; + +@_i1.JS() +external _i2.Hanlenle get coco; +@_i1.JS() +external _i2.Foo globalFoo; diff --git a/web_generator/test/integration/interop_gen/global_modules_input.d.ts b/web_generator/test/integration/interop_gen/global_modules_input.d.ts new file mode 100644 index 00000000..7d24dda7 --- /dev/null +++ b/web_generator/test/integration/interop_gen/global_modules_input.d.ts @@ -0,0 +1,15 @@ + +export declare const coco: Hanlenle; +export declare var globalFoo: Foo; + +declare global { + interface Foo { + bar: string; + } +} + +declare global { + interface Hanlenle { + readonly palava: string; + } +} diff --git a/web_generator/test/integration/interop_gen/modules_expected.dart b/web_generator/test/integration/interop_gen/modules_expected.dart new file mode 100644 index 00000000..24849484 --- /dev/null +++ b/web_generator/test/integration/interop_gen/modules_expected.dart @@ -0,0 +1,5 @@ +// ignore_for_file: constant_identifier_names, non_constant_identifier_names + +export 'modules_input/express.dart'; +export 'modules_input/my-lib.dart'; +export 'modules_input/my-lib/extra.dart'; diff --git a/web_generator/test/integration/interop_gen/modules_input.d.ts b/web_generator/test/integration/interop_gen/modules_input.d.ts new file mode 100644 index 00000000..49c4f9ca --- /dev/null +++ b/web_generator/test/integration/interop_gen/modules_input.d.ts @@ -0,0 +1,47 @@ +declare module "my-lib" { + // Named exports + export function greet(name: string): string; + export const version: string; + + // Class export + export class Person { + constructor(name: string, age: number); + name: string; + age: number; + sayHello(): string; + } + + // Interface export + export interface Options { + debug?: boolean; + retries?: number; + } + + // Default export + export default function init(options?: Options): void; +} + + +declare module "my-lib/extra" { + // FIXME: A bug occurs when exporting decls from modules in the same file + import { Options } from "my-lib"; + export function extraFn(options?: Options): void; + + global { + const __APP_VERSION__: string; + + interface Window { + myCustomMethod(): void; + } + + interface Array { + first(): T | undefined; + } + } +} + +declare module "express" { + interface Request { + user?: { id: string; role: string }; + } +} \ No newline at end of file diff --git a/web_generator/test/integration/interop_gen/modules_input/express.dart b/web_generator/test/integration/interop_gen/modules_input/express.dart new file mode 100644 index 00000000..7f81fda2 --- /dev/null +++ b/web_generator/test/integration/interop_gen/modules_input/express.dart @@ -0,0 +1,23 @@ +// ignore_for_file: camel_case_types, constant_identifier_names +// ignore_for_file: library_private_types_in_public_api +// ignore_for_file: non_constant_identifier_names, unnecessary_parenthesis + +@_i1.JS('express') +library; // ignore_for_file: no_leading_underscores_for_library_prefixes + +import 'dart:js_interop' as _i1; + +@_i1.JS('Request') +extension type Request._(_i1.JSObject _) implements _i1.JSObject { + external AnonymousType_1078145? user; +} +extension type AnonymousType_1078145._(_i1.JSObject _) implements _i1.JSObject { + external AnonymousType_1078145({ + String id, + String role, + }); + + external String id; + + external String role; +} diff --git a/web_generator/test/integration/interop_gen/modules_input/my-lib.dart b/web_generator/test/integration/interop_gen/modules_input/my-lib.dart new file mode 100644 index 00000000..443838b7 --- /dev/null +++ b/web_generator/test/integration/interop_gen/modules_input/my-lib.dart @@ -0,0 +1,33 @@ +// ignore_for_file: camel_case_types, constant_identifier_names, file_names +// ignore_for_file: non_constant_identifier_names + +@_i1.JS('my-lib') +library; // ignore_for_file: no_leading_underscores_for_library_prefixes + +import 'dart:js_interop' as _i1; + +@_i1.JS() +external String greet(String name); +@_i1.JS() +external void init([Options? options]); +@_i1.JS() +external String get version; +@_i1.JS('Options') +extension type Options._(_i1.JSObject _) implements _i1.JSObject { + external bool? debug; + + external double? retries; +} +@_i1.JS('Person') +extension type Person._(_i1.JSObject _) implements _i1.JSObject { + external Person( + String name, + num age, + ); + + external String name; + + external double age; + + external String sayHello(); +} diff --git a/web_generator/test/integration/interop_gen/modules_input/my-lib/extra.dart b/web_generator/test/integration/interop_gen/modules_input/my-lib/extra.dart new file mode 100644 index 00000000..21d36e0d --- /dev/null +++ b/web_generator/test/integration/interop_gen/modules_input/my-lib/extra.dart @@ -0,0 +1,12 @@ +// ignore_for_file: constant_identifier_names, file_names +// ignore_for_file: non_constant_identifier_names + +@_i1.JS('my-lib/extra') +library; // ignore_for_file: no_leading_underscores_for_library_prefixes + +import 'dart:js_interop' as _i1; + +import '../my-lib.dart' as _i2; + +@_i1.JS() +external void extraFn([_i2.Options? options]);