Skip to content
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
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -229,3 +229,6 @@ tests/**/*.actual
# Rust
target/
Cargo.lock

# This file is copied as part of the Restore task
tests/React/Components.Copied.fs
6 changes: 5 additions & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,5 +32,9 @@
"temp"
],
"python.testing.unittestEnabled": false,
"python.testing.pytestEnabled": true
"python.testing.pytestEnabled": true,
"files.associations": {
"*.jsx.actual": "javascript",
"*.jsx.expected": "javascript"
}
}
18 changes: 15 additions & 3 deletions src/Fable.Build/Test/JavaScript.fs
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,23 @@ let private testReact (isWatch: bool) =
if isWatch then
Async.Parallel
[
Command.WatchFableAsync(CmdLine.appendRaw "--noCache", workingDirectory = workingDirectory)
Command.WatchFableAsync(
CmdLine.appendRaw "watch"
>> CmdLine.appendRaw "--noCache"
// There seems to be some strange console Log writting
>> CmdLine.appendRaw "--verbose"
>> CmdLine.appendRaw "--runWatch"
>> CmdLine.appendRaw "npx jest",
workingDirectory = workingDirectory
)
|> Async.AwaitTask

Command.RunAsync("npx", "jest --watch", workingDirectory = workingDirectory)
|> Async.AwaitTask
// Running both command in the same shell don't seems to be working as expected.

// For now, we expect the user to use `./build.sh test javascript --react-only --watch`
// and `npx jest --watch` in a second terminal
// Command.RunAsync("npx", "jest --watch", workingDirectory = workingDirectory)
// |> Async.AwaitTask
]
|> Async.RunSynchronously
|> ignore
Expand Down
1 change: 1 addition & 0 deletions src/Fable.Cli/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
* [Python] Fixed error when type contains multiple generic type parameters (#3986) (by @dbrattli)
* [Python] Fixed import path handling for libraries (#4088) (by @dbrattli)
* [Python] Reenable type aliasing for imports with name "*" (by @freymauer)
* [JS/TS] Optimise JSX output in order to avoid F# list CEs to surface in it (by @MangelMaxime)

### Removed

Expand Down
1 change: 1 addition & 0 deletions src/Fable.Compiler/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

* [Python] Fixed import path handling for libraries (#4088) (by @dbrattli)
* [Python] Reenable type aliasing for imports with name "*" (by @freymauer)
* [JS/TS] Optimise JSX output in order to avoid F# list CEs to surface in it (by @MangelMaxime)

## 5.0.0-alpha.12 - 2025-03-14

Expand Down
111 changes: 111 additions & 0 deletions src/Fable.Transforms/Fable2Babel.fs
Original file line number Diff line number Diff line change
Expand Up @@ -2034,6 +2034,111 @@ module Util =
None
)

module Jsx =

(***

For JSX, we want to rewrite the default output in order to remove all the code coming from list CEs

By default, this code

```fs
Html.div
[
yield! [
Html.div "Test 1"
Html.div "Test 2"
]
]
```

generates something like

```jsx
<div>
{toList(delay(() => [<div>
Test 1
</div>, <div>
Test 2
</div>]))}
</div>;
```

but thanks to the optimisation done below we get

```jsx
<div>
<div>
Test 1
</div>
<div>
Test 2
</div>
</div>
```

Initial implementation of this optimiser comes from https://github.com/shayanhabibi/Partas.Solid/blob/master/Partas.Solid.FablePlugin/Plugin.fs

***)

// Check if the provided expression is equal to the expected identiferText (as a string)
let rec (|IdentifierIs|_|) (identifierText: string) expression =
match expression with
| Expression.Identifier(Identifier(currentCallerText, _)) when identifierText = currentCallerText -> Some()
| _ -> None

// Make it easy to check if we are calling the expected function
and (|CalledExpression|_|) (callerText: string) value =
match value with
| CallExpression(IdentifierIs callerText, UnrollerFromArray exprs, _, _) -> Some exprs
| _ -> None

and (|UnrollerFromSingleton|) (expr: Expression) : Expression list =
[ expr ]
|> function
| Unroller exprs -> exprs

and (|UnrollerFromArray|) (arrayExpr: Expression array) : Expression list =
arrayExpr
|> Array.toList
|> function
| Unroller exprs -> exprs

and (|Unroller|): Expression list -> Expression list =
function
| [] -> []
| expr :: Unroller rest ->
match expr with
| CalledExpression "toList" exprs -> exprs @ rest
| CalledExpression "delay" exprs -> exprs @ rest
| ArrowFunctionExpression([||],
BlockStatement [| ReturnStatement(ArrayExpression(UnrollerFromArray exprs, _),
_) |],
_,
_,
_) -> exprs @ rest
| ArrowFunctionExpression([||],
BlockStatement [| ReturnStatement(UnrollerFromSingleton exprs, _) |],
_,
_,
_) -> exprs @ rest
| CalledExpression "append" exprs -> exprs @ rest
| CalledExpression "singleton" exprs -> exprs @ rest
// Note: Should we guard this unwrapper by checking that all the elements in the array are JsxElements?
| ArrayExpression(UnrollerFromArray exprs, _) -> exprs @ rest
| ConditionalExpression(testExpr, UnrollerFromSingleton thenExprs, UnrollerFromSingleton elseExprs, loc) ->
ConditionalExpression(
testExpr,
SequenceExpression(thenExprs |> List.toArray, None),
SequenceExpression(elseExprs |> List.toArray, None),
loc
)
:: rest
| expr ->
// Tips 💡
// If a pattern is not optimized, you can put a debug point here to capture it
expr :: rest

let transformJsxEl (com: IBabelCompiler) ctx componentOrTag props =
match transformJsxProps com props with
| None -> Expression.nullLiteral ()
Expand All @@ -2047,6 +2152,12 @@ module Util =
// Because of call optimizations, it may happen a list has been transformed to an array in JS
| [ ArrayExpression(children, _) ] -> Array.toList children
| children -> children
// Optimize AST, removing F# CEs from the output (see documentation in the JSX module)
|> List.map (fun child ->
match child with
| Jsx.UnrollerFromSingleton expr -> expr
)
|> List.concat

let props =
props |> List.rev |> List.map (fun (k, v) -> k, transformAsExpr com ctx v)
Expand Down
2 changes: 1 addition & 1 deletion tests/Integration/Integration/CompilationTests.fs
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ let tests =

// Compile project
let exitCode =
Fable.Cli.Entry.main [| project; "--cwd"; "$\"{testCaseDir}\""; "-e"; ".js.actual" |]
Fable.Cli.Entry.main [| project; "--cwd"; "$\"{testCaseDir}\""; "-e"; ".jsx.actual" |]

Expect.equal exitCode 0 "Expected exit code to be 0"

Expand Down
151 changes: 151 additions & 0 deletions tests/Integration/Integration/data/jsxListOptimisation/Components.fs
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
module JsxListOptimisation.Components


open Fable.Core
open Fable.Core.JsInterop

(*

This file is used both in the Integration and React tests

- Integration is used to verify the syntax output
- React is used to verify that we emit valid JSX which generates the expected HTML

Also, when running React tests check if there are any warning generated by React like

Warning: Each child in a list should have a unique "key" prop.

which indicates a cases where we try to optimise the JSX output.

*)

// Create our own binding for React API, and make a minimal Feliz-like DSL

// Necessary for JSX compilation
let React: obj = importAll "react"

[<ImportMember("react")>]
[<AllowNullLiteral>]
type JSX_ReactElement = interface end

type JSX_IReactProperty = JSX.Prop

[<Erase>]
type prop =
static member inline children(children: JSX_ReactElement list) : JSX_IReactProperty = "children", children

static member inline text(value: int) : JSX_IReactProperty = "children", [ JSX.text !!value ]

static member inline key(value: int) : JSX_IReactProperty = "key", string value

static member inline className (cls : string) : JSX_IReactProperty = "className", cls

[<Erase>]
type Html =

static member inline fragment(children: JSX_ReactElement list) : JSX_ReactElement =
JSX.create "" [ "children" ==> children ] |> unbox

static member inline div(children: JSX_ReactElement list) : JSX_ReactElement =
JSX.create "div" [ "children" ==> children ] |> unbox

static member inline div(text: string) : JSX_ReactElement =
JSX.create "div" [ "children" ==> JSX.text text ] |> unbox

static member inline div(props: JSX_IReactProperty list) : JSX_ReactElement = JSX.create "div" props |> unbox

let divWithText =
Html.div "Test 1"

let divWithNestedList =
Html.div
[
yield! [
Html.div "Test 1"
Html.div "Test 2"
]
]

let divWithMultipleNestedList =
Html.div [
yield! [
Html.div "Test 1"
]
yield! [
Html.div "Test 2"
]
]

let divWithMixOfElementAndList =
Html.div [
Html.div "Test 1"
yield! [
Html.div "Test 2"
]
]

let multiLevelMixOfElementAndList =
Html.div [
Html.div "Test 1"
yield! [
Html.div "Test 2"
Html.div [
Html.div "Test 2.1"
yield! [
Html.div "Test 2.2"
]
]
]
]

// F# compiler can optimise condition sometimes
let divWithOptimisedTrueCondition =
Html.div [
if true then
Html.div "true"
else
Html.div "false"
]

// F# compiler can optimise condition sometimes
let divWithOptimisedFalseCondition =
Html.div [
if false then
Html.div "true"
else
Html.div "false"
]

// Prevent compiler optimisation
let divWithConditionalChildren a =
Html.div [
if a = "b" then
Html.div "true"
else
Html.div "false"
]

let divWithForLoop =
Html.div [
Html.div "Hello"
for i in 0..2 do
Html.div [
prop.key i
prop.text i
]
]

let divWithFragment =
Html.div [
Html.fragment [
Html.div "Test 1"
]
]

let divWithAttributes =
Html.div [
prop.className "header"
prop.children [
Html.div "Test 1"
]
]
Loading
Loading