diff --git a/src/Fable.Cli/CHANGELOG.md b/src/Fable.Cli/CHANGELOG.md
index c77b49eb7..730960e5d 100644
--- a/src/Fable.Cli/CHANGELOG.md
+++ b/src/Fable.Cli/CHANGELOG.md
@@ -12,6 +12,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
* [Python] Print root module and module function comments (by @alfonsogarciacaro)
* [Rust] Add support for module comments (by @ncave)
* [Rust] Add support for null strings (by @ncave)
+* [TS/JS] `Pojo` attribute support (by @alfonsogarciacaro)
### Fixed
diff --git a/src/Fable.Compiler/CHANGELOG.md b/src/Fable.Compiler/CHANGELOG.md
index de78c5f4e..c1974a58a 100644
--- a/src/Fable.Compiler/CHANGELOG.md
+++ b/src/Fable.Compiler/CHANGELOG.md
@@ -12,6 +12,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
* [Python] - Print root module and module function comments (by @alfonsogarciacaro)
* [Rust] Add support for module comments (by @ncave)
* [Rust] Add support for null strings (by @ncave)
+* [TS/JS] `Pojo` attribute support (by @alfonsogarciacaro)
### Fixed
diff --git a/src/Fable.Core/CHANGELOG.md b/src/Fable.Core/CHANGELOG.md
index 8aa8ac0b3..c2bb5eaf9 100644
--- a/src/Fable.Core/CHANGELOG.md
+++ b/src/Fable.Core/CHANGELOG.md
@@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## Unreleased
+### Added
+
+* [TS/JS] Add `Pojo` attribute (by @alfonsogarciacaro)
+
## 4.3.0 - 2024-01-25
### Added
diff --git a/src/Fable.Core/Fable.Core.JS.fs b/src/Fable.Core/Fable.Core.JS.fs
index 90e383dbf..238b917bb 100644
--- a/src/Fable.Core/Fable.Core.JS.fs
+++ b/src/Fable.Core/Fable.Core.JS.fs
@@ -48,6 +48,32 @@ module JSX =
let nothing: Element = nativeOnly
module JS =
+ ///
+ /// Constructor calls to class decorated with this attribute will be compiled as JS pojos defined by the constructor arguments.
+ /// The class declaration will be erased, but in Typescript an interface will be generated.
+ ///
+ ///
+ ///
+ /// [<Pojo>]
+ /// type Person(name: string, ?age: int) =
+ /// member val name = name
+ /// member val age = age
+ ///
+ /// let p = Person("Alice")
+ ///
+ /// // Typescript
+ /// export interface Person {
+ /// name: string,
+ /// age?: number
+ /// }
+ ///
+ /// export const p: Person = { name: "Alice" }
+ ///
+ ///
+ []
+ type PojoAttribute() =
+ inherit Attribute()
+
/// Used to remove the arguments of a surrounding function immediately calling a function decorated with this argument.
/// This is convenient to represent JS patterns when a function is actually loaded lazily with a dynamic import.
type RemoveSurroundingArgsAttribute() =
@@ -109,17 +135,6 @@ module JS =
/// that is representable as a Number value, which is approximately:
/// 2.2204460492503130808472633361816 x 10−16.
abstract EPSILON: float
- ///
- /// Returns true if passed value is finite.
- /// Unlike the global isFinite, Number.isFinite doesn't forcibly convert the parameter to a
- /// number. Only finite values of the type number, result in true.
- ///
- /// A numeric value.
- abstract isFinite: number: obj -> bool
- /// Returns true if the value passed is an integer, false otherwise.
- /// A numeric value.
- abstract isInteger: number: obj -> bool
- ///
/// Returns a Boolean value that indicates whether a value is the reserved value NaN (not a
/// number). Unlike the global isNaN(), Number.isNaN() doesn't forcefully convert the parameter
/// to a number. Only values of the type number, that are also NaN, result in true.
diff --git a/src/Fable.Transforms/Dart/Fable2Dart.fs b/src/Fable.Transforms/Dart/Fable2Dart.fs
index ed68b48ee..d0fe7a064 100644
--- a/src/Fable.Transforms/Dart/Fable2Dart.fs
+++ b/src/Fable.Transforms/Dart/Fable2Dart.fs
@@ -461,8 +461,8 @@ module Util =
|> Option.map (splitNamedArgs args)
|> function
| None -> args, []
- | Some(args, []) -> args, []
- | Some(args, namedArgs) ->
+ | Some(args, None) -> args, []
+ | Some(args, Some namedArgs) ->
args,
namedArgs
|> List.choose (fun (p, v) ->
diff --git a/src/Fable.Transforms/FSharp2Fable.Util.fs b/src/Fable.Transforms/FSharp2Fable.Util.fs
index c59c84472..bed17301c 100644
--- a/src/Fable.Transforms/FSharp2Fable.Util.fs
+++ b/src/Fable.Transforms/FSharp2Fable.Util.fs
@@ -272,8 +272,6 @@ type FsMemberFunctionOrValue(m: FSharpMemberOrFunctionOrValue) =
member _.Attributes = m.Attributes |> Seq.map (fun x -> FsAtt(x) :> Fable.Attribute)
member _.CurriedParameterGroups =
- let mutable i = -1
-
let namedParamsIndex =
m.Attributes
|> Helpers.tryFindAttrib Atts.paramObject
@@ -282,6 +280,13 @@ type FsMemberFunctionOrValue(m: FSharpMemberOrFunctionOrValue) =
| Some(_, (:? int as index)) -> index
| _ -> 0
)
+ |> Option.orElseWith (fun () ->
+ m.DeclaringEntity
+ |> Option.bind (fun ent -> ent.Attributes |> Helpers.tryFindAttrib Atts.pojoDefinedByConsArgs)
+ |> Option.map (fun _ -> 0)
+ )
+
+ let mutable i = -1
m.CurriedParameterGroups
|> Seq.mapToList (
@@ -821,46 +826,6 @@ module Helpers =
// [] 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 ->
- // Empty constructors are considered valid as it allows to simplify unwraping
- // complex Union types
- //
- // []
- // []
- // type ClassWithUnion private () =
- // []
- // new (stringOrNumber : string) = ClassWithUnion()
- // []
- // new (stringOrNumber : int) = ClassWithUnion()
- //
- // Without this trick when we have a lot of U2, U3, etc. to map it is really difficult
- // or verbose to craft the correct F# class. By using, an empty constructor we can
- // "bypass" the F# type system.
- memb.CurriedParameterGroups |> List.concat |> List.isEmpty
- || memb.Attributes
- |> Seq.exists (fun att -> att.Entity.FullName = Atts.paramObject)
- )
-
- isGlobalType && areAllConstructorsParamObject
-
let tryPickAttrib attFullNames (attributes: FSharpAttribute seq) =
let attFullNames = Map attFullNames
@@ -2046,25 +2011,10 @@ module Util =
let tryGlobalOrImportedAttributes (com: Compiler) (entRef: Fable.EntityRef) (attributes: Fable.Attribute seq) =
let globalRef customName =
- let name =
- // Custom name has precedence
- match customName with
- | Some name -> name
- | None ->
- let entity = com.GetEntity(entRef)
-
- // If we are generating TypeScript, and the entity is an object class pattern
- // we need to use the compiled name, replacing '`' with '$' to mimic
- // how Fable generates the compiled name for generic types
- // I was not able to find where this is done in Fable, so I am doing it manually here
- if com.Options.Language = TypeScript && isParamObjectClassPattern entity then
- entity.CompiledName.Replace("`", "$")
- // Otherwise, we use the display name as `Global` is often used to describe external API
- // and we want to keep the original name
- else
- entRef.DisplayName
-
- name |> makeTypedIdent Fable.Any |> Fable.IdentExpr |> Some
+ defaultArg customName entRef.DisplayName
+ |> makeTypedIdent Fable.Any
+ |> Fable.IdentExpr
+ |> Some
match attributes with
| _ when entRef.FullName.StartsWith("Fable.Core.JS.", StringComparison.Ordinal) -> globalRef None
@@ -2146,6 +2096,12 @@ module Util =
| _ -> false
))
+ let isPojoDefinedByConsArgsEntity (entity: Fable.Entity) =
+ entity |> hasAttribute Atts.pojoDefinedByConsArgs
+
+ let isPojoDefinedByConsArgsFSharpEntity (ent: FSharpEntity) =
+ ent.Attributes |> hasAttrib Atts.pojoDefinedByConsArgs
+
let isEmittedOrImportedMember (memb: FSharpMemberOrFunctionOrValue) =
memb.Attributes
|> Seq.exists (fun att ->
@@ -2405,7 +2361,12 @@ module Util =
// Don't mangle interfaces by default (for better interop) unless they have Mangle attribute
| _ when ent.IsInterface -> tryMangleAttribute ent.Attributes |> Option.defaultValue false
// Mangle members from abstract classes unless they are global/imported or with explicitly attached members
- | _ -> not (isGlobalOrImportedFSharpEntity ent || isAttachMembersEntity com ent)
+ | _ ->
+ not (
+ isGlobalOrImportedFSharpEntity ent
+ || isAttachMembersEntity com ent
+ || isPojoDefinedByConsArgsFSharpEntity ent
+ )
let getMangledAbstractMemberName (ent: FSharpEntity) memberName overloadHash =
// TODO: Error if entity doesn't have fullName?
@@ -2681,8 +2642,12 @@ module Util =
let moduleOrClassExpr =
match tryGlobalOrImportedFSharpEntity com e with
| Some expr -> Some expr
- // AttachMembers classes behave the same as global/imported classes
- | None when not (com.Options.Language = Rust) && isAttachMembersEntity com e ->
+ // AttachMembers/Pojo classes behave
+ // the same as global/imported classes
+ | None when
+ com.Options.Language <> Rust
+ && (isAttachMembersEntity com e || isPojoDefinedByConsArgsFSharpEntity e)
+ ->
FsEnt.Ref e |> entityIdent com |> Some
| None -> None
diff --git a/src/Fable.Transforms/FSharp2Fable.fs b/src/Fable.Transforms/FSharp2Fable.fs
index 4cf8eec40..d8e07c19e 100644
--- a/src/Fable.Transforms/FSharp2Fable.fs
+++ b/src/Fable.Transforms/FSharp2Fable.fs
@@ -1486,7 +1486,7 @@ let private isIgnoredNonAttachedMember (memb: FSharpMemberOrFunctionOrValue) =
)
|| (
match memb.DeclaringEntity with
- | Some ent -> isGlobalOrImportedFSharpEntity ent
+ | Some ent -> isGlobalOrImportedFSharpEntity ent || isPojoDefinedByConsArgsFSharpEntity ent
| None -> false
)
@@ -2044,8 +2044,7 @@ let rec private transformDeclarations (com: FableCompiler) ctx fsDecls =
if
(isErasedOrStringEnumEntity ent && Compiler.Language <> TypeScript)
- || (isGlobalOrImportedEntity ent
- && (not (Compiler.Language = TypeScript && isParamObjectClassPattern ent)))
+ || isGlobalOrImportedEntity ent
then
[]
else
diff --git a/src/Fable.Transforms/Fable2Babel.fs b/src/Fable.Transforms/Fable2Babel.fs
index fc1bba82c..b67c31271 100644
--- a/src/Fable.Transforms/Fable2Babel.fs
+++ b/src/Fable.Transforms/Fable2Babel.fs
@@ -1878,7 +1878,7 @@ module Util =
Expression.newExpression (classExpr, [||])
- let transformCallArgs
+ let transformCallArgsWithNamedArgs
(com: IBabelCompiler)
ctx
(callInfo: Fable.CallInfo)
@@ -1906,26 +1906,12 @@ module Util =
paramsInfo
|> Option.map (splitNamedArgs args)
|> function
- | None -> args, None
- | Some(args, []) ->
- // Detect if the method has a ParamObject attribute
- // If yes and no argument is passed, pass an empty object
- // See https://github.com/fable-compiler/Fable/issues/3480
- match callInfo.MemberRef with
- | Some(Fable.MemberRef(_, info)) ->
- let hasParamObjectAttribute =
- info.AttributeFullNames
- |> List.tryFind (fun attr -> attr = Atts.paramObject)
- |> Option.isSome
-
- if hasParamObjectAttribute then
- args, Some(makeJsObject [])
- else
- args, None
- | _ ->
- // Here detect empty named args
- args, None
- | Some(args, namedArgs) ->
+ | None
+ | Some(_, None) -> args, None
+ // If there are named arguments but none is passed, pass an empty object
+ // See https://github.com/fable-compiler/Fable/issues/3480
+ | Some(args, Some []) -> args, Some(makeJsObject [])
+ | Some(args, Some namedArgs) ->
let objArg =
namedArgs
|> List.choose (fun (p, v) ->
@@ -1958,9 +1944,12 @@ module Util =
else
List.map (fun e -> com.TransformAsExpr(ctx, e)) args
- match objArg with
- | None -> args
- | Some objArg -> args @ [ objArg ]
+ args, objArg
+
+ let transformCallArgs com ctx callInfo memberInfo =
+ match transformCallArgsWithNamedArgs com ctx callInfo memberInfo with
+ | args, None -> args
+ | args, Some objArg -> args @ [ objArg ]
let resolveExpr t strategy babelExpr : Statement =
match strategy with
@@ -2146,16 +2135,37 @@ module Util =
transformJsxCall com ctx callee callInfo.Args memberInfo
| memberInfo ->
let callee = com.TransformAsExpr(ctx, callee)
- let args = transformCallArgs com ctx callInfo memberInfo
+
+ let nonNamedArgs, namedArgs =
+ transformCallArgsWithNamedArgs com ctx callInfo memberInfo
+
+ let args = nonNamedArgs @ Option.toList namedArgs
match callInfo.ThisArg with
| None when List.contains "new" callInfo.Tags ->
- let typeParamInst =
- match typ with
- | Fable.DeclaredType(_entRef, genArgs) -> makeTypeParamInstantiationIfTypeScript com ctx genArgs
+ let pojo =
+ match nonNamedArgs, namedArgs, memberInfo with
+ | [], Some namedArgs, Some memberInfo ->
+ match memberInfo.DeclaringEntity |> Option.bind com.TryGetEntity with
+ | Some e when FSharp2Fable.Util.isPojoDefinedByConsArgsEntity e -> Some namedArgs
+ | _ -> None
| _ -> None
- Expression.newExpression (callee, List.toArray args, ?typeArguments = typeParamInst, ?loc = range)
+ match pojo with
+ | Some pojo -> pojo
+ | None ->
+ let typeParamInst =
+ match typ with
+ | Fable.DeclaredType(_entRef, genArgs) ->
+ makeTypeParamInstantiationIfTypeScript com ctx genArgs
+ | _ -> None
+
+ Expression.newExpression (
+ callee,
+ List.toArray args,
+ ?typeArguments = typeParamInst,
+ ?loc = range
+ )
| None -> callFunction com ctx range callee callInfo.GenericArgs args
| Some(TransformExpr com ctx thisArg) ->
callFunction com ctx range callee callInfo.GenericArgs (thisArg :: args)
@@ -3793,7 +3803,7 @@ module Util =
declareType com ctx ent decl.Name args body baseExpr classMembers
- let transformParamObjectClassPatternToInterface
+ let transformPojoDefinedByConsArgsToInterface
(com: IBabelCompiler)
ctx
(ent: Fable.Entity)
@@ -4224,11 +4234,11 @@ module Util =
else
[]
| ent ->
- if
- Compiler.Language = TypeScript
- && FSharp2Fable.Helpers.isParamObjectClassPattern ent
- then
- transformParamObjectClassPatternToInterface com ctx ent decl
+ if FSharp2Fable.Util.isPojoDefinedByConsArgsEntity ent then
+ if Compiler.Language = TypeScript then
+ transformPojoDefinedByConsArgsToInterface com ctx ent decl
+ else
+ []
else
let classMembers =
diff --git a/src/Fable.Transforms/Python/Fable2Python.fs b/src/Fable.Transforms/Python/Fable2Python.fs
index a21d570e8..3552724e3 100644
--- a/src/Fable.Transforms/Python/Fable2Python.fs
+++ b/src/Fable.Transforms/Python/Fable2Python.fs
@@ -1972,8 +1972,8 @@ module Util =
|> Option.map (splitNamedArgs args)
|> function
| None -> args, None, []
- | Some(args, []) -> args, None, []
- | Some(args, namedArgs) ->
+ | Some(args, None) -> args, None, []
+ | Some(args, Some namedArgs) ->
let objArg, stmts =
namedArgs
|> List.choose (fun (p, v) ->
diff --git a/src/Fable.Transforms/Transforms.Util.fs b/src/Fable.Transforms/Transforms.Util.fs
index db6ab8ab6..8ea7729fc 100644
--- a/src/Fable.Transforms/Transforms.Util.fs
+++ b/src/Fable.Transforms/Transforms.Util.fs
@@ -67,6 +67,9 @@ module Atts =
[]
let global_ = "Fable.Core.GlobalAttribute" // typeof.FullName
+ []
+ let pojoDefinedByConsArgs = "Fable.Core.JS.PojoAttribute" // typeof.FullName
+
[]
let emit = "Fable.Core.Emit"
@@ -1278,12 +1281,12 @@ module AST =
let splitNamedArgs (args: Expr list) (info: ParamsInfo) =
match info.NamedIndex with
- | None -> args, []
- | Some index when index > args.Length || index > info.Parameters.Length -> args, []
+ | None -> args, None
+ | Some index when index > args.Length || index > info.Parameters.Length -> args, Some []
| Some index ->
let args, namedArgs = List.splitAt index args
let namedParams = List.skip index info.Parameters |> List.truncate namedArgs.Length
- args, List.zipSafe namedParams namedArgs
+ args, List.zipSafe namedParams namedArgs |> Some
/// Used to compare arg idents of a lambda wrapping a function call
let argEquals (argIdents: Ident list) argExprs =
diff --git a/src/fable-metadata/lib/Fable.Core.dll b/src/fable-metadata/lib/Fable.Core.dll
index 27bb345d1..3a5f6d42f 100644
Binary files a/src/fable-metadata/lib/Fable.Core.dll and b/src/fable-metadata/lib/Fable.Core.dll differ
diff --git a/tests/Js/Main/JsInteropTests.fs b/tests/Js/Main/JsInteropTests.fs
index 2ca052b9a..2412db10a 100644
--- a/tests/Js/Main/JsInteropTests.fs
+++ b/tests/Js/Main/JsInteropTests.fs
@@ -316,55 +316,36 @@ module TaggedUnion =
| [] Baz of Baz
#if FABLE_COMPILER
-module ParamObjectClassPattern =
+module PojoDefinedByConsArgs =
+ open Fable.Core.JS
- open Fable.Core.JsInterop
+ []
+ type Person( name : string ) =
+ member val name = name
- []
- []
- type Person
- []
- ( name : string ) =
-
- member val name: string = jsNative with get, set
-
- []
- []
- type User
- []
- ( id: int, name: string, ?age: int ) =
+ []
+ type User( id: int, name: string, ?age: int ) =
inherit Person(name)
- member val id: int = jsNative with get, set
- member val age: int = jsNative with get, set
- member val name: string = jsNative with get, set
-
- []
- []
- type BaseBag<'T>
- () =
+ member val id = id
+ member val age = age with get, set
+ member val name = name
- []
+ []
+ type BaseBag<'T>() =
new ( bag : 'T) = BaseBag()
+ member val bag: 'T = jsNative
- member val bag: 'T = jsNative with get, set
-
- []
- []
- type UserBag<'ExtraData>
- private () =
+ []
+ type UserBag<'ExtraData> private () =
inherit BaseBag()
-
- []
new ( bag : int, data: 'ExtraData, ?userId : int) = UserBag()
- []
new ( bag : int, data : 'ExtraData, ?userId : Guid) = UserBag()
-
member val bag: int = jsNative with get, set
- member val data: 'ExtraData = jsNative with get, set
- member val userId: U2 option = jsNative with get, set
+ member val data: 'ExtraData = jsNative
+ member val userId: U2 option = jsNative
let tests =
- testList "ParamObjectClassPattern" [
+ testList "PojoDefinedByConsArgs" [
testCase "does create a POJO" <| fun _ ->
let user = User(1, "John")
@@ -377,7 +358,7 @@ module ParamObjectClassPattern =
equal user userObj
- testCase "ParamObject supports downcasting" <| fun _ ->
+ testCase "PojoDefinedByConsArgs supports downcasting" <| fun _ ->
let user = User(1, "John")
let person = user :> Person
@@ -389,12 +370,18 @@ module ParamObjectClassPattern =
equal "Kaladin" directDowncast.name
#endif
- testCase "ParamObject works with generics" <| fun _ ->
+ testCase "PojoDefinedByConsArgs works with generics" <| fun _ ->
let userBag = UserBag(42, "data", 1)
equal 42 userBag.bag
equal "data" userBag.data
equal (Some (U2.Case1 1)) userBag.userId
+
+ testCase "PojoDefinedByConsArgs can mutate members" <| fun _ ->
+ let user = User(1, "John")
+ user.age |> equal None
+ user.age <- Some 42
+ user.age |> equal (Some 42)
]
#endif
@@ -906,6 +893,6 @@ let tests =
validatePassword x |> equal "np"
#if FABLE_COMPILER
- ParamObjectClassPattern.tests
+ PojoDefinedByConsArgs.tests
#endif
]