diff --git a/web_generator/lib/src/interop_gen/transform/transformer.dart b/web_generator/lib/src/interop_gen/transform/transformer.dart index 6431293c..6d918a78 100644 --- a/web_generator/lib/src/interop_gen/transform/transformer.dart +++ b/web_generator/lib/src/interop_gen/transform/transformer.dart @@ -156,6 +156,12 @@ class Transformer { return [ _transformClassOrInterface(node as TSObjectDeclaration, namer: namer), ]; + case TSSyntaxKind.ImportEqualsDeclaration + when (node as TSImportEqualsDeclaration).moduleReference.kind == + TSSyntaxKind.ExternalModuleReference: + return [ + _transformImportEqualsAsNamespace(node, namer: namer, parent: parent), + ]; case TSSyntaxKind.ImportEqualsDeclaration when (node as TSImportEqualsDeclaration).moduleReference.kind != TSSyntaxKind.ExternalModuleReference: @@ -221,7 +227,6 @@ class Transformer { } } - // TODO(): Support `import = require` declarations, https://github.com/dart-lang/web/issues/438 TypeAliasDeclaration _transformImportEqualsDeclarationAsTypeAlias( TSImportEqualsDeclaration typealias, { UniqueNamer? namer, @@ -248,6 +253,175 @@ class Transformer { ); } + /// Transforms an import equals declaration with an external module reference + /// (e.g., `import foo = require("bar")`) into a namespace declaration. + /// This normalizes CommonJS-style imports into the same representation as + /// ES6 namespace imports (`import * as foo from "bar"`). + NamespaceDeclaration _transformImportEqualsAsNamespace( + TSImportEqualsDeclaration importEquals, { + UniqueNamer? namer, + NamespaceDeclaration? parent, + }) { + namer ??= this.namer; + + final namespaceName = importEquals.name.text; + final moduleRef = importEquals.moduleReference as TSExternalModuleReference; + + // Validate that the expression is a string literal + if (moduleRef.expression.kind != TSSyntaxKind.StringLiteral) { + print( + 'WARN: Unsupported import = require() with non-string expression: ' + '${moduleRef.expression.kind}', + ); + // Return empty namespace as fallback + final (:id, name: dartName) = namer.makeUnique( + namespaceName, + 'namespace', + ); + return NamespaceDeclaration( + name: namespaceName, + dartName: dartName, + id: id, + exported: false, + topLevelDeclarations: {}, + namespaceDeclarations: {}, + nestableDeclarations: {}, + ); + } + + // get modifiers + final modifiers = importEquals.modifiers?.toDart ?? []; + final isExported = modifiers.any((m) { + return m.kind == TSSyntaxKind.ExportKeyword; + }); + + 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 scopedNamer = ScopedUniqueNamer(); + + final outputNamespace = currentNamespaces.isNotEmpty + ? currentNamespaces.first + : NamespaceDeclaration( + name: namespaceName, + dartName: dartName, + id: id!, + exported: isExported, + topLevelDeclarations: {}, + namespaceDeclarations: {}, + nestableDeclarations: {}, + documentation: _parseAndTransformDocumentation(importEquals), + ); + + /// Updates the state of the given declaration, + /// 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); + } else { + outputNamespace.parent = parent; + parent.namespaceDeclarations.add(outputNamespace); + } + } else { + nodeMap.update( + outputNamespace.id.toString(), + (v) => outputNamespace, + ifAbsent: () => outputNamespace, + ); + } + } + + void transformDeclAndAppendParent( + NamespaceDeclaration outputNamespace, + TSNode decl, + ) { + if (outputNamespace.nodes.contains(decl)) return; + if (decl.kind == TSSyntaxKind.EnumMember) { + final tsEnum = (decl as TSEnumMember).parent; + // parse whole enum + final transformedEnum = _transformEnum(tsEnum, namer: namer); + + // add enum + if (parent != null) { + parent.nestableDeclarations.add(transformedEnum); + parent.nodes.add(tsEnum); + } else { + nodes.add(tsEnum); + nodeMap.add(transformedEnum); + } + + // add all members to namespace + outputNamespace.nodes.addAll(tsEnum.members.toDart); + } else { + final outputDecls = transformAndReturn( + decl, + namer: scopedNamer, + parent: outputNamespace, + ); + switch (decl.kind) { + case TSSyntaxKind.ClassDeclaration || + TSSyntaxKind.InterfaceDeclaration: + final outputDecl = outputDecls.single as TypeDeclaration; + outputDecl.parent = outputNamespace; + outputNamespace.nestableDeclarations.add(outputDecl); + case TSSyntaxKind.EnumDeclaration: + final outputDecl = outputDecls.single as EnumDeclaration; + outputDecl.parent = outputNamespace; + outputNamespace.nestableDeclarations.add(outputDecl); + case TSSyntaxKind.TypeAliasDeclaration: + final outputDecl = outputDecls.single as TypeAliasDeclaration; + outputDecl.parent = outputNamespace; + outputNamespace.nestableDeclarations.add(outputDecl); + default: + outputNamespace.topLevelDeclarations.addAll(outputDecls); + } + outputNamespace.nodes.add(decl); + } + + // update namespace state + updateNSInParent(); + } + + // preload nodemap + updateNSInParent(); + + // Resolve the symbol at the import name location to get the module's export + final symbol = typeChecker.getSymbolAtLocation(importEquals.name); + final exports = symbol?.exports?.toDart; + + if (exports case final exportedMap?) { + for (final symbol in exportedMap.values) { + final decls = symbol.getDeclarations()?.toDart ?? []; + try { + final aliasedSymbol = typeChecker.getAliasedSymbol(symbol); + decls.addAll(aliasedSymbol.getDeclarations()?.toDart ?? []); + } catch (_) { + // throws error if no aliased symbol, so ignore + } + for (final decl in decls) { + transformDeclAndAppendParent(outputNamespace, decl); + } + } + } + + // final update on namespace state + updateNSInParent(); + + // index names + namer.markUsedSet(scopedNamer); + + return outputNamespace; + } + /// Transforms a TS Namespace (identified as a [TSModuleDeclaration] with /// an identifier name that isn't "global") into a Dart Namespace /// Representation. diff --git a/web_generator/test/integration/interop_gen/import_equals_test_expected.dart b/web_generator/test/integration/interop_gen/import_equals_test_expected.dart new file mode 100644 index 00000000..d2fa63ca --- /dev/null +++ b/web_generator/test/integration/interop_gen/import_equals_test_expected.dart @@ -0,0 +1,69 @@ +// 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 'package:meta/meta.dart' as _i2; + +@_i1.JS() +external Vector2D get v2d; +@_i1.JS() +external Vector3D get v3d; +extension type Vector2D._(_i1.JSObject _) implements Vector { + external Vector2D(num x, num y); + + external double x; + + external double y; + + external Vector2D unit(); + @_i2.redeclare + external double get magnitude; + @_i2.redeclare + external double get directionAngle; + external Point2D moveFrom(Point2D point); + external static Vector2D from(num magnitude, num at); + external static Vector2D fromPoints(Point2D start, Point2D end); +} +extension type Point2D._(_i1.JSObject _) implements _i1.JSObject { + external double x; + + external double y; +} +extension type Vector._(_i1.JSObject _) implements _i1.JSObject { + external double get magnitude; + external double get directionAngle; +} +extension type Vector3D._(_i1.JSObject _) implements Vector { + external Vector3D(num x, num y, num z); + + external double x; + + external double y; + + external double z; + + external Vector3D unit(); + @_i2.redeclare + external double get magnitude; + external DirectionAngles get directionAngles; + @_i2.redeclare + external double get directionAngle; + external Point3D moveFrom(Point3D point); + external static Vector3D from(num magnitude, DirectionAngles at); + external static Vector3D fromPoints(Point3D start, Point3D end); +} +extension type DirectionAngles._(_i1.JSObject _) implements _i1.JSObject { + external double alpha; + + external double beta; + + external double gamma; +} +extension type Point3D._(_i1.JSObject _) implements _i1.JSObject { + external double x; + + external double y; + + external double z; +} diff --git a/web_generator/test/integration/interop_gen/import_equals_test_input.d.ts b/web_generator/test/integration/interop_gen/import_equals_test_input.d.ts new file mode 100644 index 00000000..ad7b5270 --- /dev/null +++ b/web_generator/test/integration/interop_gen/import_equals_test_input.d.ts @@ -0,0 +1,10 @@ +// Test file for import = require() support +// Tests that import = require() declarations are properly transformed +// into namespace imports + +import Vector = require("./classes_input"); + +// Declare a variable using the imported namespace +// The Vector namespace should contain Vector2D, Vector3D, etc. +export declare const v2d: Vector.Vector2D; +export declare const v3d: Vector.Vector3D;