From 931f6d4b53f5aafd6d78123afdd3107ad97537cf Mon Sep 17 00:00:00 2001 From: Maxime Mangel Date: Wed, 8 Jan 2025 22:23:30 +0100 Subject: [PATCH 1/3] [TS] Emit `interface` declaration when encountering a `ParamObject` pattern --- src/Fable.Transforms/FSharp2Fable.Util.fs | 29 +++++++ src/Fable.Transforms/FSharp2Fable.fs | 3 +- src/Fable.Transforms/Fable2Babel.fs | 97 +++++++++++++++++------ 3 files changed, 102 insertions(+), 27 deletions(-) diff --git a/src/Fable.Transforms/FSharp2Fable.Util.fs b/src/Fable.Transforms/FSharp2Fable.Util.fs index 6ed4956ab..e973470cd 100644 --- a/src/Fable.Transforms/FSharp2Fable.Util.fs +++ b/src/Fable.Transforms/FSharp2Fable.Util.fs @@ -796,6 +796,35 @@ module Helpers = | None -> false ) + // When compiling to TypeScript, we want to captuer classes that use the + // [] attribute on the type and[] on the constructors + // so we can transform it into an interface + + /// + /// Check if the entity is decorated with the Global attribute + /// and all its constructors are decorated with ParamObject attribute. + /// + /// This is used to identify classes that should be transformed into interfaces. + /// + /// + /// + /// true if the entity is a global type with all constructors as param objects, + /// false otherwise. + /// + let isParamObjectClassPattern (entity: Fable.Entity) = + let isGlobalType = + entity.Attributes |> Seq.exists (fun att -> att.Entity.FullName = Atts.global_) + + let areAllConstructorsParamObject = + entity.MembersFunctionsAndValues + |> Seq.filter _.IsConstructor + |> Seq.forall (fun memb -> + memb.Attributes + |> Seq.exists (fun att -> att.Entity.FullName = Atts.paramObject) + ) + + isGlobalType && areAllConstructorsParamObject + let tryPickAttrib attFullNames (attributes: FSharpAttribute seq) = let attFullNames = Map attFullNames diff --git a/src/Fable.Transforms/FSharp2Fable.fs b/src/Fable.Transforms/FSharp2Fable.fs index 2e36160cf..65de58b23 100644 --- a/src/Fable.Transforms/FSharp2Fable.fs +++ b/src/Fable.Transforms/FSharp2Fable.fs @@ -2044,7 +2044,8 @@ let rec private transformDeclarations (com: FableCompiler) ctx fsDecls = if (isErasedOrStringEnumEntity ent && Compiler.Language <> TypeScript) - || isGlobalOrImportedEntity ent + || (isGlobalOrImportedEntity ent + && (Compiler.Language = TypeScript && not (isParamObjectClassPattern ent))) then [] else diff --git a/src/Fable.Transforms/Fable2Babel.fs b/src/Fable.Transforms/Fable2Babel.fs index a532501b0..61001a3f2 100644 --- a/src/Fable.Transforms/Fable2Babel.fs +++ b/src/Fable.Transforms/Fable2Babel.fs @@ -3782,6 +3782,44 @@ module Util = declareType com ctx ent entName args body baseExpr classMembers + let transformParamObjectClassPatternToInterface + (com: IBabelCompiler) + ctx + (classDecl: Fable.ClassDecl) + (ent: Fable.Entity) + = + let members = + ent.MembersFunctionsAndValues + |> Seq.filter _.IsConstructor + |> Seq.map (fun constructor -> + constructor.CurriedParameterGroups + |> List.concat + |> List.mapi (fun index arg -> + let name = defaultArg arg.Name $"arg{index}" + + let typeAnnotation = + if arg.IsOptional then + unwrapOptionalType arg.Type + else + arg.Type + |> FableTransforms.uncurryType + |> makeTypeAnnotation com ctx + + AbstractMember.abstractProperty ( + name |> Identifier.identifier |> Expression.Identifier, + typeAnnotation, + isOptional = arg.IsOptional + ) + ) + ) + |> Seq.concat + |> Seq.toArray + + Declaration.interfaceDeclaration (Identifier.identifier classDecl.Name, members, [||]) + |> asModuleDeclaration ent.IsPublic + |> List.singleton + + let transformClassWithPrimaryConstructor (com: IBabelCompiler) ctx @@ -4124,33 +4162,40 @@ module Util = else [] | ent -> - let classMembers = - decl.AttachedMembers - |> List.toArray - |> Array.collect (fun memb -> - withCurrentScope ctx memb.UsedNames - <| fun ctx -> - memb.ImplementedSignatureRef - |> Option.bind (com.TryGetMember) - |> Option.orElseWith (fun () -> com.TryGetMember(memb.MemberRef)) - |> function - | None -> [||] - | Some info -> - if not memb.IsMangled && (info.IsGetter || info.IsSetter) then - transformAttachedProperty com ctx ent info memb - else - transformAttachedMethod com ctx ent info memb - ) + if + Compiler.Language = TypeScript + && FSharp2Fable.Helpers.isParamObjectClassPattern ent + then + transformParamObjectClassPatternToInterface com ctx decl ent + else - match decl.Constructor with - | Some cons -> - withCurrentScope ctx cons.UsedNames - <| fun ctx -> transformClassWithPrimaryConstructor com ctx ent decl classMembers cons - | None -> - if ent.IsFSharpUnion then - transformUnion com ctx ent decl.Name classMembers - else - transformClassWithCompilerGeneratedConstructor com ctx ent decl.Name classMembers + let classMembers = + decl.AttachedMembers + |> List.toArray + |> Array.collect (fun memb -> + withCurrentScope ctx memb.UsedNames + <| fun ctx -> + memb.ImplementedSignatureRef + |> Option.bind (com.TryGetMember) + |> Option.orElseWith (fun () -> com.TryGetMember(memb.MemberRef)) + |> function + | None -> [||] + | Some info -> + if not memb.IsMangled && (info.IsGetter || info.IsSetter) then + transformAttachedProperty com ctx ent info memb + else + transformAttachedMethod com ctx ent info memb + ) + + match decl.Constructor with + | Some cons -> + withCurrentScope ctx cons.UsedNames + <| fun ctx -> transformClassWithPrimaryConstructor com ctx ent decl classMembers cons + | None -> + if ent.IsFSharpUnion then + transformUnion com ctx ent decl.Name classMembers + else + transformClassWithCompilerGeneratedConstructor com ctx ent decl.Name classMembers let transformImports (imports: Import seq) : ModuleDeclaration list = let statefulImports = ResizeArray() From 652bb024cb781184e38435e707525b182f53f348 Mon Sep 17 00:00:00 2001 From: Maxime Mangel Date: Wed, 8 Jan 2025 22:47:34 +0100 Subject: [PATCH 2/3] Try fix condition --- src/Fable.Transforms/FSharp2Fable.fs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Fable.Transforms/FSharp2Fable.fs b/src/Fable.Transforms/FSharp2Fable.fs index 65de58b23..3fbbbf650 100644 --- a/src/Fable.Transforms/FSharp2Fable.fs +++ b/src/Fable.Transforms/FSharp2Fable.fs @@ -2045,7 +2045,7 @@ let rec private transformDeclarations (com: FableCompiler) ctx fsDecls = if (isErasedOrStringEnumEntity ent && Compiler.Language <> TypeScript) || (isGlobalOrImportedEntity ent - && (Compiler.Language = TypeScript && not (isParamObjectClassPattern ent))) + && (not (Compiler.Language = TypeScript && isParamObjectClassPattern ent))) then [] else From 9e6291508b110bb419830ef258cff646927d16d6 Mon Sep 17 00:00:00 2001 From: Maxime Mangel Date: Wed, 8 Jan 2025 23:03:53 +0100 Subject: [PATCH 3/3] Forward generics information --- src/Fable.Transforms/Fable2Babel.fs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/Fable.Transforms/Fable2Babel.fs b/src/Fable.Transforms/Fable2Babel.fs index 61001a3f2..da2815176 100644 --- a/src/Fable.Transforms/Fable2Babel.fs +++ b/src/Fable.Transforms/Fable2Babel.fs @@ -3815,7 +3815,13 @@ module Util = |> Seq.concat |> Seq.toArray - Declaration.interfaceDeclaration (Identifier.identifier classDecl.Name, members, [||]) + + let typeParameters = + ent.GenericParameters + |> List.map (fun g -> Fable.GenericParam(g.Name, g.IsMeasure, g.Constraints)) + |> makeTypeParamDecl com ctx + + Declaration.interfaceDeclaration (Identifier.identifier classDecl.Name, members, [||], typeParameters) |> asModuleDeclaration ent.IsPublic |> List.singleton