Currently, I'm thinking about using the following feature:
program.fs
module MyModule
let myExport() = ()
fsimport.ts
declare module "*.fs" {
val _ : any;
default export _;
}
index.ts
import * as cRaw from "./myModule.fs"
let c : any = cRaw
// call F# function
c.myExport()
Which seems to work fine in fable 2 with webpack.
My questions are:
There seems to be a lack of documentation when trying to export F# functionality back into the JS-ecosystem.
This will not work in all the case because of mangled names.
If you want to export an API, it's better to expose an Interface so the names are not mangled.
type IExports =
abstract RenderBlocks : unit -> React.ReactElement
let api =
{ new IExports with
member this.RenderBlocks () : React.ReactElement =
str "demo"
}
You can use any name instead of api
I just took, it from one of my application.
So question remains is this a feature and we should document it?
Sure this is something we should document. We are planning to redo the Fable website so perhaps we should include it in the rework.
Or temporary we can include it in the "Interop" part ?
Or temporary we can include it in the "Interop" part ?
This is what I mean, I just wanted to clarify what is just hacking and what is officially supported going forward :)
Functions in the root module of a file are more or less guaranteed not to be mangled (unless you use a JS keyword or a global API), but if they belong to a nested module or a type they'll be mangled to avoid name conflicts.
As @MangelMaxime says, it's recommended to use an interface to avoid mangling and also because you can expose that interface to other F# projects for lazy loading, etc. You can use the exportDefault function to export that interface as the default member (example).
Good to learn that the attribute [<ExportDefault>] is now a function :)
I was wondering where did it go. This will simplify some of my export ❤️
Sadly I'm not exactly an expert in the JS ecosystem. Can you show or link an example on how you would use exportDefault from the other side (JS or TS)? Maybe its all clear for you but there are apparently so many different ways to export stuff in JS I haven't figured it out completely :(
The difference between a member or a default import in JS/TS is whether you use brackets or not.
import foo from "./util" // Imports the default member, note you can use any name you want
import { foo } from "./util" // Imports a member named "foo" from "./util"
You can find more info in the MDN documentation: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/import
So let's say you have an F# file that looks like this:
module Util
// This is public so Fable automatically exports this
// Because it's a root module function, name won't be mangled
let printfFoo() = "foo"
type IExports =
abstract PrintBar: unit -> string
exportDefault
{ new IExports with
member this.PrintBar() = "bar" }
You can import them as:
import * as Util from "./Util.fs" // import all members
import { printFoo } from "./Util.fs" // import member named printFoo
import somethingSomething from "./Util.fs" // import default member
Util.printFoo() // foo
printFoo() // foo
somethingSomething.PrintBar() // bar
@matthid
You can use it like that:
type IExports =
abstract RenderBlocks : unit -> React.ReactElement
Fable.Core.JsInterop.exportDefault
{ new IExports with
member this.RenderBlocks () : React.ReactElement =
div [ ]
[ Switch.switch [ Switch.Id "switch-1" ]
[ str "Toggle setting n°1" ]
Switch.switch [ Switch.Id "switch-2" ]
[ str "Toggle setting n°2" ] ]
}
You should be able to import it like that:
import myApi from "./path/to/the/file"
// or
import default as myApi from "./path/to/the/file"
myApi.RenderBlocks() // returns a React elements
If you want to import only the RenderBlocks function you should be able to do something like:
import { RenderBlocks } from "./path/to/the/file"
RenderBlocks() // returns a React elements
Edit: Ok @alfonsogarciacaro was quicker that me 🤷♂️
Thanks! Exactly what I was looking for. Ironically your answers seem to differ:
@MangelMaxime you say import { RenderBlocks } from "./path/to/the/file" imports from the object of the exportDefault call.
@alfonsogarciacaro you say import { printFoo } from "./Util.fs" imports from the module (not from the exportDefault call)
Seems this stuff is not too obvious or I'm missing something :)
I am not sure of my example I didn't test it 😅
But I think both are valid in JavaScript, that's the problem with import/export in JavaScript there is not one way to do things.
If you want to export an API, it's better to expose an
Interfaceso the names are not mangled.
Today I noticed that this is not actually enough. You need to use { new Interface with ... } syntax, the following apparently doesn't work:
type IExports =
abstract RenderBlocks : unit -> React.ReactElement
type MyApi() =
interface IExports with
member this.RenderBlocks () : React.ReactElement =
str "demo"
let api = MyApi() :> IExports
We use the following (ugly) workaround:
type IExports =
abstract RenderBlocks : unit -> React.ReactElement
type MyApi() =
member x.Api =
{ new IExports with
member this.RenderBlocks () : React.ReactElement =
str "demo" }
let api = MyApi().Api
@matthid have you tried anonymous records:
// Code.fs
module Code
open Fable.Core.JsInterop
exportDefault {| addFive = fun x -> x + 5 |}
from Javascript
// util.js
import Code from './Code.fs'
console.log(Code.addFive(10)) // 15
Edited: tested the code above and it works. Destructuring however doesn't work so import { addFive } from './Code.fs will not work
@Zaid-Ajaj Nope, I just wanted to clarify if this is expected and if yes then I think fable should at least warn on the line :> IExports
Closing for now, please feel free to reopen if there are further questions.