Zaid-Ajaj / Feliz

A fresh retake of the React API in Fable and a collection of high-quality components to build React applications in F#, optimized for happiness
https://zaid-ajaj.github.io/Feliz/
MIT License
548 stars 83 forks source link

Path imported by `[<ReactComponent(import="...", from="...")>] should not be relative to the caller side #624

Open MangelMaxime opened 2 months ago

MangelMaxime commented 2 months ago

I have a Code.tsx file with a Components.fs file where I am binding the component for usage in F#:

[<ReactComponent(import="default", from="./${outDir}/Code.tsx")>]
let Code (code: string) (language : string) = React.imported()

Then in another file from another directory, I am calling the F# Code function to use the component. The generated code is:

import Code from "./Code.tsx";

The issue is that ./Code.tsx is invalid because the caller file is not inside of the same directory as Components.fs:

.
├── Components
│   ├── Code.tsx
│   ├── Components.fs
│   └── Components.fs.js
├── Utils
│   ├── Html.fs

The import path should be relative to where the API is defined.

IHMO this should be the default behaviour otherwise, it means that if you need to use a JSX component then you need to re-declare it several times 😅

A workaround, is to create a standard Feliz binding using Interop.reactApi.createElement to we can use Fable special macro${outDir}:

type Components =

    static member Code (code: string) (language: string) =
        Interop.reactApi.createElement (
            import "default" "${outDir}/Code.tsx",
            {|
                code = code
                language = language
            |}
        )

It is important to not mark this member as inline otherwise it will not work because the call site is not stable.

Zaid-Ajaj commented 1 month ago

Hi there @MangelMaxime so the problem here is that we need access to the value of outDir from the compiler. Here is where use it:

let makeImport (selector: string) (path: string) =
    Fable.Import({ Selector = selector.Trim()
                   Path = path.Trim()
                   Kind = Fable.UserImport(false) }, Fable.Any, None)

Maybe Fable compiler should automatically replace ${outDir} with its value when encountering Fable.Import AST node? 🤔

OR Fable could expose the outDir as an environment variable such that we get access to it in compile-time:

let path = 
  match env "FABLE_OUT_DIR" with
  | Some outDir -> path.Replace("${outDir}", outDir).Trim()
  | None -> path.Trim()
MangelMaxime commented 1 month ago

Replacing ${outDir} automatically was my idea but I need to check when it happens in Fable.

One reason to do it this way, is like that plugins don't know to worry about path and we don't have to duplicate the logic once per plugin.

Zaid-Ajaj commented 1 month ago

One reason to do it this way, is like that plugins don't know to worry about path and we don't have to duplicate the logic once per plugin.

Yeah it makes more sense to do that in the compiler ✅ you probably need to traverse the resulting AST from compiler plugins, check for Fable.Import nodes and update their Path if it contains the${outDir} macro