-
Notifications
You must be signed in to change notification settings - Fork 47
Add support for TypeScript import = require() in web interop generator #511
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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<NamespaceDeclaration>(); | ||
|
|
||
| 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), | ||
| ); | ||
|
Comment on lines
+302
to
+319
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The final (name: dartName, id: id) = currentNamespaces.isEmpty
? namer.makeUnique(namespaceName, 'namespace')
: (name: currentNamespaces.first.dartName, id: currentNamespaces.first.id);
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, | ||
| ); | ||
| } | ||
| } | ||
|
Comment on lines
+324
to
+341
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The condition void updateNSInParent() {
if (parent != null) {
final existingParentNamespace = parent.namespaceDeclarations.firstWhereOrNull((n) => n.name == namespaceName);
if (existingParentNamespace != null) {
parent.namespaceDeclarations.remove(existingParentNamespace);
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 | ||
| } | ||
|
Comment on lines
+407
to
+409
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Catching all exceptions with } on JSError catch (e) {
// Assuming JSError is the specific error thrown when no aliased symbol exists.
// If other errors can occur, they should be handled differently or rethrown.
if (!e.message.contains('No aliased symbol')) {
rethrow;
}
} |
||
| 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. | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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; | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The current implementation logs a warning and returns an empty
NamespaceDeclarationwhenmoduleRef.expression.kindis notTSSyntaxKind.StringLiteral. While this prevents a crash, returning an empty namespace might lead to silent failures or unexpected behavior downstream if the generated Dart code relies on a non-empty namespace. Consider throwing anUnsupportedErroriferrorIfUnsupportedis true, or providing a more explicit fallback that clearly indicates the namespace is invalid or empty, rather than silently creating an empty one.