Skip to content

Commit 35a03c9

Browse files
authored
[JS/TS] Optimise JSX output in order to avoid F# list CEs to surface in it (#4095)
1 parent 5a59e3a commit 35a03c9

File tree

21 files changed

+657
-22
lines changed

21 files changed

+657
-22
lines changed

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -229,3 +229,6 @@ tests/**/*.actual
229229
# Rust
230230
target/
231231
Cargo.lock
232+
233+
# This file is copied as part of the Restore task
234+
tests/React/Components.Copied.fs

.vscode/settings.json

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,5 +32,9 @@
3232
"temp"
3333
],
3434
"python.testing.unittestEnabled": false,
35-
"python.testing.pytestEnabled": true
35+
"python.testing.pytestEnabled": true,
36+
"files.associations": {
37+
"*.jsx.actual": "javascript",
38+
"*.jsx.expected": "javascript"
39+
}
3640
}

src/Fable.Build/Test/JavaScript.fs

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,11 +21,23 @@ let private testReact (isWatch: bool) =
2121
if isWatch then
2222
Async.Parallel
2323
[
24-
Command.WatchFableAsync(CmdLine.appendRaw "--noCache", workingDirectory = workingDirectory)
24+
Command.WatchFableAsync(
25+
CmdLine.appendRaw "watch"
26+
>> CmdLine.appendRaw "--noCache"
27+
// There seems to be some strange console Log writting
28+
>> CmdLine.appendRaw "--verbose"
29+
>> CmdLine.appendRaw "--runWatch"
30+
>> CmdLine.appendRaw "npx jest",
31+
workingDirectory = workingDirectory
32+
)
2533
|> Async.AwaitTask
2634

27-
Command.RunAsync("npx", "jest --watch", workingDirectory = workingDirectory)
28-
|> Async.AwaitTask
35+
// Running both command in the same shell don't seems to be working as expected.
36+
37+
// For now, we expect the user to use `./build.sh test javascript --react-only --watch`
38+
// and `npx jest --watch` in a second terminal
39+
// Command.RunAsync("npx", "jest --watch", workingDirectory = workingDirectory)
40+
// |> Async.AwaitTask
2941
]
3042
|> Async.RunSynchronously
3143
|> ignore

src/Fable.Cli/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1313
* [Python] Fixed error when type contains multiple generic type parameters (#3986) (by @dbrattli)
1414
* [Python] Fixed import path handling for libraries (#4088) (by @dbrattli)
1515
* [Python] Reenable type aliasing for imports with name "*" (by @freymauer)
16+
* [JS/TS] Optimise JSX output in order to avoid F# list CEs to surface in it (by @MangelMaxime)
1617

1718
### Removed
1819

src/Fable.Compiler/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1515

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

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

src/Fable.Transforms/Fable2Babel.fs

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2034,6 +2034,111 @@ module Util =
20342034
None
20352035
)
20362036

2037+
module Jsx =
2038+
2039+
(***
2040+
2041+
For JSX, we want to rewrite the default output in order to remove all the code coming from list CEs
2042+
2043+
By default, this code
2044+
2045+
```fs
2046+
Html.div
2047+
[
2048+
yield! [
2049+
Html.div "Test 1"
2050+
Html.div "Test 2"
2051+
]
2052+
]
2053+
```
2054+
2055+
generates something like
2056+
2057+
```jsx
2058+
<div>
2059+
{toList(delay(() => [<div>
2060+
Test 1
2061+
</div>, <div>
2062+
Test 2
2063+
</div>]))}
2064+
</div>;
2065+
```
2066+
2067+
but thanks to the optimisation done below we get
2068+
2069+
```jsx
2070+
<div>
2071+
<div>
2072+
Test 1
2073+
</div>
2074+
<div>
2075+
Test 2
2076+
</div>
2077+
</div>
2078+
```
2079+
2080+
Initial implementation of this optimiser comes from https://github.com/shayanhabibi/Partas.Solid/blob/master/Partas.Solid.FablePlugin/Plugin.fs
2081+
2082+
***)
2083+
2084+
// Check if the provided expression is equal to the expected identiferText (as a string)
2085+
let rec (|IdentifierIs|_|) (identifierText: string) expression =
2086+
match expression with
2087+
| Expression.Identifier(Identifier(currentCallerText, _)) when identifierText = currentCallerText -> Some()
2088+
| _ -> None
2089+
2090+
// Make it easy to check if we are calling the expected function
2091+
and (|CalledExpression|_|) (callerText: string) value =
2092+
match value with
2093+
| CallExpression(IdentifierIs callerText, UnrollerFromArray exprs, _, _) -> Some exprs
2094+
| _ -> None
2095+
2096+
and (|UnrollerFromSingleton|) (expr: Expression) : Expression list =
2097+
[ expr ]
2098+
|> function
2099+
| Unroller exprs -> exprs
2100+
2101+
and (|UnrollerFromArray|) (arrayExpr: Expression array) : Expression list =
2102+
arrayExpr
2103+
|> Array.toList
2104+
|> function
2105+
| Unroller exprs -> exprs
2106+
2107+
and (|Unroller|): Expression list -> Expression list =
2108+
function
2109+
| [] -> []
2110+
| expr :: Unroller rest ->
2111+
match expr with
2112+
| CalledExpression "toList" exprs -> exprs @ rest
2113+
| CalledExpression "delay" exprs -> exprs @ rest
2114+
| ArrowFunctionExpression([||],
2115+
BlockStatement [| ReturnStatement(ArrayExpression(UnrollerFromArray exprs, _),
2116+
_) |],
2117+
_,
2118+
_,
2119+
_) -> exprs @ rest
2120+
| ArrowFunctionExpression([||],
2121+
BlockStatement [| ReturnStatement(UnrollerFromSingleton exprs, _) |],
2122+
_,
2123+
_,
2124+
_) -> exprs @ rest
2125+
| CalledExpression "append" exprs -> exprs @ rest
2126+
| CalledExpression "singleton" exprs -> exprs @ rest
2127+
// Note: Should we guard this unwrapper by checking that all the elements in the array are JsxElements?
2128+
| ArrayExpression(UnrollerFromArray exprs, _) -> exprs @ rest
2129+
| ConditionalExpression(testExpr, UnrollerFromSingleton thenExprs, UnrollerFromSingleton elseExprs, loc) ->
2130+
ConditionalExpression(
2131+
testExpr,
2132+
SequenceExpression(thenExprs |> List.toArray, None),
2133+
SequenceExpression(elseExprs |> List.toArray, None),
2134+
loc
2135+
)
2136+
:: rest
2137+
| expr ->
2138+
// Tips 💡
2139+
// If a pattern is not optimized, you can put a debug point here to capture it
2140+
expr :: rest
2141+
20372142
let transformJsxEl (com: IBabelCompiler) ctx componentOrTag props =
20382143
match transformJsxProps com props with
20392144
| None -> Expression.nullLiteral ()
@@ -2047,6 +2152,12 @@ module Util =
20472152
// Because of call optimizations, it may happen a list has been transformed to an array in JS
20482153
| [ ArrayExpression(children, _) ] -> Array.toList children
20492154
| children -> children
2155+
// Optimize AST, removing F# CEs from the output (see documentation in the JSX module)
2156+
|> List.map (fun child ->
2157+
match child with
2158+
| Jsx.UnrollerFromSingleton expr -> expr
2159+
)
2160+
|> List.concat
20502161

20512162
let props =
20522163
props |> List.rev |> List.map (fun (k, v) -> k, transformAsExpr com ctx v)

tests/Integration/Integration/CompilationTests.fs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ let tests =
2020

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

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

tests/Integration/Integration/data/eraseAttribute/ErasedProperty.js.expected renamed to tests/Integration/Integration/data/eraseAttribute/ErasedProperty.jsx.expected

File renamed without changes.

tests/Integration/Integration/data/eraseAttribute/ErasedTypeWithProperty.js.expected renamed to tests/Integration/Integration/data/eraseAttribute/ErasedTypeWithProperty.jsx.expected

File renamed without changes.

tests/Integration/Integration/data/eraseAttribute/Members.js.expected renamed to tests/Integration/Integration/data/eraseAttribute/Members.jsx.expected

File renamed without changes.

0 commit comments

Comments
 (0)