Skip to content
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

[JS/TS] Supports direct nested types when using jsOptions #4067

Merged
merged 2 commits into from
Feb 25, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions src/Fable.Cli/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
* [All] Don't scan system packages for plugins (by @MangelMaxime)
* [JS/TS] Fix date formatting when repeating a format token more than the known format (example repeating 'd' more than 4 times) (by @MangelMaxime)
* [Python] Fix date formatting when repeating a format token more than the known format (example repeating 'd' more than 4 times) (by @MangelMaxime)
* [JS/TS] Fix #4010: Supports direct nested types when using `jsOptions` (by @MangelMaxime)

```fs
let opts =
jsOptions<Level1> (fun o ->
o.level2.level3.valueA <- 10
o.level2.level3.valueB <- 20
o.topValueA <- 20
)
```

## 5.0.0-alpha.10 - 2025-02-16

Expand Down
10 changes: 10 additions & 0 deletions src/Fable.Compiler/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
* [All] Don't scan system packages for plugins (by @MangelMaxime)
* [JS/TS] Fix date formatting when repeating a format token more than the known format (example repeating 'd' more than 4 times) (by @MangelMaxime)
* [Python] Fix date formatting when repeating a format token more than the known format (example repeating 'd' more than 4 times) (by @MangelMaxime)
* [JS/TS] Fix #4010: Supports direct nested types when using `jsOptions` (by @MangelMaxime)

```fs
let opts =
jsOptions<Level1> (fun o ->
o.level2.level3.valueA <- 10
o.level2.level3.valueB <- 20
o.topValueA <- 20
)
```

## 5.0.0-alpha.10 - 2025-02-16

Expand Down
95 changes: 88 additions & 7 deletions src/Fable.Transforms/Replacements.fs
Original file line number Diff line number Diff line change
Expand Up @@ -727,6 +727,47 @@ let makeGenericAverager (com: ICompiler) ctx t =
"DivideByInt", divideFn
]

// The code below is not functional because I was not able to make a clean implementation
// of it using functional approach.
// I believe this is not an issue because the code is more readable than all my functional attempts.
type private MakePojoFromLambdaItem =
| Property of string * Expr
| Object of MakePojoFromLambdaContext

and private MakePojoFromLambdaContext(segment: string) =

// We can't use ResizeArray alias here because it also exist in Fable AST
member val Children = Collections.Generic.List<MakePojoFromLambdaItem>()

member val Segment = segment

member this.AddChildAt(segments: string list, property: string * Expr) =
let rec add (path: string list) (newProperty: string * Expr) (current: MakePojoFromLambdaContext) =
match path with
| [] -> current.Children.Add(Property(newProperty))
| head :: tailPath ->
let child =
let existingObject =
current.Children
|> Seq.tryFind (fun c ->
match c with
| Object b -> b.Segment = head
| _ -> false
)

match existingObject with
| Some(Object c) -> c
| None ->
let newChild = MakePojoFromLambdaContext(head)
current.Children.Add(Object(newChild))
newChild
| _ -> failwith "Should not happen, as 'Some' case can only be an Object at this point"

add tailPath newProperty child

add segments property this
this

let makePojoFromLambda com (arg: Expr) =
let rec flattenSequential =
function
Expand All @@ -740,15 +781,55 @@ let makePojoFromLambda com (arg: Expr) =

match arg with
| Lambda(_, lambdaBody, _) ->
(flattenSequential lambdaBody, Some [])
||> List.foldBack (fun statement acc ->
match acc, statement with
| Some acc, Set(_, FieldSet(fieldName), _, value, _) -> objValue (fieldName, value) :: acc |> Some
| _ -> None
)
let flattened = flattenSequential lambdaBody

let rec groupByGetter (acc: MakePojoFromLambdaContext) (body: Expr list) =
match body with
| [] -> acc
| head :: tail ->
match head with
| Set(IdentExpr _, FieldSet(fieldName), _, value, _) ->
let updatedAcc = acc.AddChildAt([], (fieldName, value))
groupByGetter updatedAcc tail

| Set(Get _ as getExpr, FieldSet(fieldName), _, value, _) ->
let rec getGetterSegments (acc: string list) (expr: Expr) =
match expr with
| Get(IdentExpr _, FieldGet(name), _, _) -> name.Name :: acc
| Get(expr, FieldGet(name), _, _) -> getGetterSegments (name.Name :: acc) expr
| _ -> acc

let updatedAcc =
let getterSegments = getGetterSegments [] getExpr
acc.AddChildAt(getterSegments, (fieldName, value))

// This is a nested property
groupByGetter updatedAcc tail
| _ -> groupByGetter acc tail

let root = groupByGetter (MakePojoFromLambdaContext("/")) flattened

let rec mapToExpression (node: MakePojoFromLambdaItem) =
match node with
| Property(name, value) -> objValue (name, value)
| Object b ->
let mappedChildren = b.Children |> Seq.map mapToExpression |> Seq.toList
objValue (b.Segment, ObjectExpr(mappedChildren, Any, None))

// Note: If the user mix nested getter and jsOptions then the last one will bein effect
// We could try to generate a warning/error in this case but it seems complicated for little gain
if root.Children.Count = 0 then
None
else
root.Children |> Seq.map mapToExpression |> Seq.toList |> Some
| _ -> None
|> Option.map (fun members -> ObjectExpr(members, typ, None))
|> Option.defaultWith (fun () -> Helper.LibCall(com, "Util", "jsOptions", typ, [ arg ], ?genArgs = genArgs))
|> Option.defaultWith (fun () ->
// TODO: Do we want to support nested getters here too?
// This could be complex because here the user can mix any code
// so it can be difficult to detect the pattern we want
Helper.LibCall(com, "Util", "jsOptions", typ, [ arg ], ?genArgs = genArgs)
)

let makePojo (com: Compiler) caseRule keyValueList =
let makeObjMember caseRule name values =
Expand Down
2 changes: 1 addition & 1 deletion src/fable-library-ts/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

* [JS/TS] Fix escaping of `{` and `}` in FormattableString (#3890) (by @roboz0r)
* [JS/TS] Fix `uri.Host` to return the host name without the port (by @MangelMaxime)
* [JS/TS] Fix TypeScript compilation by resolving type of `jsOptions` (#3894) (by @ManngelMaxime)
* [JS/TS] Fix TypeScript compilation by resolving type of `jsOptions` (#3894) (by @MangelMaxime)

## 1.4.3 - 2024-09-04

Expand Down
54 changes: 54 additions & 0 deletions tests/Js/Main/JsInteropTests.fs
Original file line number Diff line number Diff line change
Expand Up @@ -248,6 +248,25 @@ type JsOptions =
abstract foo: string with get, set
abstract bar: int with get, set

module JsOptions =

[<AllowNullLiteral>]
type Level3 =
abstract member valueA: int with get, set
abstract member valueB: int with get, set

[<AllowNullLiteral>]
type Level2 =
abstract member level3: Level3 with get, set
abstract member valueC: int with get, set

[<AllowNullLiteral>]
type Level1 =
abstract member level2: Level2 with get, set
abstract member topValueA: int with get, set
abstract member topValueB: int with get, set


[<Fable.Core.AttachMembers>]
type ClassWithAttachments(v, ?sign) =
static let secretSauce = "wasabi"
Expand Down Expand Up @@ -843,6 +862,41 @@ let tests =
opts.foo |> equal "foo"
opts.bar |> equal 5


testCase "jsOptions works with nested getters" <| fun () ->
let options =
jsOptions<JsOptions.Level1>(fun o ->
o.topValueA <- 20
o.level2.level3.valueA <- 10
o.level2.level3.valueB <- 20
o.level2.valueC <- 44
o.topValueB <- 30
)

options.topValueA |> equal 20
options.topValueB |> equal 30
options.level2.valueC |> equal 44
options.level2.level3.valueA |> equal 10
options.level2.level3.valueB |> equal 20

testCase "jsOptions works with mix of nested getter and nested jsOptions" <| fun () ->
let options =
jsOptions<JsOptions.Level1>(fun o ->
o.topValueA <- 20
o.level2.level3.valueA <- 10
o.level2.level3.valueB <- 20
o.topValueB <- 30
o.level2 <- jsOptions<JsOptions.Level2> (fun o ->
o.level3.valueA <- 17
o.level3.valueB <- 20
)
)

options.topValueA |> equal 20
options.topValueB |> equal 30
options.level2.level3.valueA |> equal 17
options.level2.level3.valueB |> equal 20

testCase "Stringifying a JS object works" <| fun () ->
let fooOptional = importMember "./js/1foo.js"
string fooOptional |> equal "much foo, much awesome"
Expand Down
Loading