Skip to content
Open
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
1 change: 1 addition & 0 deletions Feliz.CompilerPlugins/Feliz.CompilerPlugins.fsproj
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
<PackageReleaseNotes>Remove redundant declaration of imported React component</PackageReleaseNotes>
</PropertyGroup>
<ItemGroup>
<Compile Include="StringSyntax.fs" />
<Compile Include="AstUtils.fs" />
<Compile Include="ReactComponent.fs" />
<Compile Include="Hook.fs" />
Expand Down
80 changes: 66 additions & 14 deletions Feliz.CompilerPlugins/ReactComponent.fs
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,14 @@
open Fable
open Fable.AST
open Fable.AST.Fable
open System.Diagnostics.CodeAnalysis

// Tell Fable to scan for plugins in this assembly
[<assembly:ScanForPlugins>]
do()

type MemoStrategy = EqualsShallow | EqualsButFunctions | EqualsCustom of string

module internal ReactComponentHelpers =
let (|ReactMemo|_|) = function
| Import({ Selector = "memo"; Path = "react" },_,_) as e -> Some e
Expand All @@ -24,7 +27,29 @@ module internal ReactComponentHelpers =
AstUtils.makeImport "default as React" "react"
yield! body
]


let [<Literal; StringSyntax("javascript")>] private ``JS<equalsButFunctions>`` = """function equalsButFunctions(x, y) {
if (x === y) {
return true;
}
else if ((typeof x === 'object' && !x[Symbol.iterator]) && !(y == null)) {
const keys = Object.keys(x);
const length = keys.length | 0;
let i = 0;
let result = true;
while ((i < length) && result) {
const key = keys[i];
i = ((i + 1) | 0);
const xValue = x[key];
result = ((typeof xValue === 'function') ? true : equals(xValue, y[key]));
}
return result;
}
else {
return equals(x, y);
}
}"""

let applyImportOrMemo import from memo (decl: MemberDecl) =
match import, from, memo with
| Some _, Some _, _ ->
Expand All @@ -33,13 +58,24 @@ module internal ReactComponentHelpers =
let tags = "remove-declaration" :: decl.Tags
{ decl with Body = AstUtils.emptyReactElement reactElType; Tags = tags }

| _, _, Some true ->
let memoFn = AstUtils.makeImport "memo" "react"
let body =
decl.Body
|> injectReactImport
|> fun body -> [Delegate(decl.Args, body, None, Tags.empty)]
|> AstUtils.makeCall memoFn
| _, _, Some memo ->
let memoFn = Sequential [
AstUtils.makeImport "default as React" "react"
match memo with
| EqualsShallow | EqualsCustom _ -> ()
| EqualsButFunctions -> AstUtils.makeImport "equals" "@fable-org/fable-library-js/Util.js"
AstUtils.makeImport "memo" "react"
]

let body = AstUtils.makeCall memoFn [
AstUtils.emitJs (sprintf "function %s(props) {{ return ($0)(props); }}" decl.Name) [
Delegate(decl.Args, decl.Body, Some decl.Name, Tags.empty)
]
match memo with
| EqualsShallow -> ()
| EqualsCustom js -> AstUtils.emitJs js []
| EqualsButFunctions -> AstUtils.emitJs ``JS<equalsButFunctions>`` []
]
// Change declaration kind from function to value
let info =
AstUtils.memberName decl.MemberRef
Expand All @@ -53,7 +89,7 @@ module internal ReactComponentHelpers =
open ReactComponentHelpers

/// <summary>Transforms a function into a React function component. Make sure the function is defined at the module level</summary>
type ReactComponentAttribute(?exportDefault: bool, ?import: string, ?from:string, ?memo: bool) =
type ReactComponentAttribute(?exportDefault: bool, ?import: string, ?from:string, ?memo: MemoStrategy) =
inherit MemberDeclarationPluginAttribute()
override _.FableMinimumVersion = "4.0"
new() = ReactComponentAttribute(exportDefault=false)
Expand Down Expand Up @@ -111,9 +147,9 @@ type ReactComponentAttribute(?exportDefault: bool, ?import: string, ?from:string
| None -> reactEl
| Some(ident, value) -> Let(ident, value, reactEl)

match [|memo, callee|] with
match [| memo, callee |] with
// If the call is memo and the function has an identifier, we can set the displayName
| [|(Some true), (IdentExpr i)|] ->
| [| Some _, IdentExpr i |] ->
Sequential [
(AstUtils.makeSet (IdentExpr(i)) "displayName" (AstUtils.makeStrConst i.Name))
expr
Expand Down Expand Up @@ -216,7 +252,23 @@ type ReactComponentAttribute(?exportDefault: bool, ?import: string, ?from:string
{ decl with Args = [propsArg]; Body = body }
|> applyImportOrMemo import from memo

type ReactMemoComponentAttribute(?exportDefault: bool) =
inherit ReactComponentAttribute(?exportDefault=exportDefault, ?import=None, ?from=None, memo=true)
new() = ReactMemoComponentAttribute(exportDefault=false)
type ReactMemoComponentAttribute private(memo: MemoStrategy, ?exportDefault: bool) =
inherit ReactComponentAttribute(memo=memo, ?exportDefault=exportDefault, ?import=None, ?from=None)
new() = ReactMemoComponentAttribute(memo=MemoStrategy.EqualsShallow, exportDefault=false)
new(exportDefault: bool) = ReactMemoComponentAttribute(memo=MemoStrategy.EqualsShallow, exportDefault=exportDefault)
new([<StringSyntax("javascript")>]equalsJs:string, exportDefault: bool) = ReactMemoComponentAttribute(memo=MemoStrategy.EqualsCustom equalsJs, exportDefault=exportDefault)
new([<StringSyntax("javascript")>]equalsJs:string) = ReactMemoComponentAttribute(memo=MemoStrategy.EqualsCustom equalsJs)


type ReactMemoEqualsButFunctionsComponentAttribute private(memo: MemoStrategy, ?exportDefault: bool) =
inherit ReactComponentAttribute(memo=memo, ?exportDefault=exportDefault, ?import=None, ?from=None)
new() = ReactMemoEqualsButFunctionsComponentAttribute(memo=MemoStrategy.EqualsButFunctions, exportDefault=false)
new(exportDefault: bool) = ReactMemoEqualsButFunctionsComponentAttribute(memo=MemoStrategy.EqualsButFunctions, exportDefault=exportDefault)

[<RequireQualifiedAccess>]
module ReactComponent =
type Memo = ReactMemoComponentAttribute
[<RequireQualifiedAccess>]
module Memo =
type EqualsButFunctions = ReactMemoEqualsButFunctionsComponentAttribute

32 changes: 32 additions & 0 deletions Feliz.CompilerPlugins/StringSyntax.fs
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
#if !NET7_0_OR_GREATER

namespace System.Diagnostics.CodeAnalysis

open System

/// <summary>Specifies the syntax used in a string.</summary>
[<AttributeUsage(AttributeTargets.Parameter ||| AttributeTargets.Field ||| AttributeTargets.Property, AllowMultiple = false, Inherited = false)>]
type internal StringSyntaxAttribute

/// <summary>
/// Initializes a new instance of the <see cref="StringSyntaxAttribute"/> class with the identifier of the syntax used.
/// </summary>
/// <param name="syntax">The syntax identifier.</param>
/// <param name="arguments">Optional arguments associated with the specific syntax employed.</param>
(syntax: string, [<ParamArray>] arguments: obj[]) =

inherit Attribute()

/// <summary>
/// Initializes a new instance of the <see cref="StringSyntaxAttribute"/> class with the identifier of the syntax used.
/// </summary>
/// <param name="syntax">The syntax identifier.</param>
new (syntax: string) = StringSyntaxAttribute(syntax, [||])

/// <summary>Gets the identifier of the syntax used.</summary>
member val public Syntax = syntax with get

/// <summary>Gets optional arguments associated with the specific syntax employed.</summary>
member val public Arguments = arguments with get

#endif