Fable icon indicating copy to clipboard operation
Fable copied to clipboard

Collect desired exports in one file?

Open Freymaurer opened this issue 2 years ago • 7 comments

Description

We are developing a large library in F# that will also be used in JavaScript. However, our classes are now spread over several dozen files and we would like to give JS developers the ability to import all necessary classes from one file.

Is this possible? Is there already best practice for this?

Repro code

The library in question is this

Expected and actual results

Best case: Being able to import everything from the given npm package name, instead of single files:

image

Freymaurer avatar Sep 25 '23 11:09 Freymaurer

Hello @Freymaurer,

I believe what you are looking for is exportDefault:

Makes an expression the default export for the JS module. Used to interact with JS tools that require a default export. ATTENTION: This statement must appear on the root level of the file module.

If you have the following 3 files:

|- FileA.fs
|- FileB.fs
|- Index.fs

FileA.fs

module FileA

let add x y = x + y

FileA.fs

module FileB

let sub x y = x + y

Index.fs

module Index

open Fable.Core
open Fable.Core.JsInterop

exportDefault {|
    add = FileA.add
    sub = FileB.sub
|}

This will generates:

import { add } from "./FileA.fs.js";
import { sub } from "./FileB.fs.js";

export default {
    add: add,
    sub: sub,
};

Meaning that you are still able to import function individually from each files (I don't think this is possible to prevent that even in JavaScript). But more importantly, you can now import everything available from the index.js file via a single import statement.

MangelMaxime avatar Sep 25 '23 13:09 MangelMaxime

Yes something like this is what i am looking for. But sadly this does not work for types?

In the following case "ArcAssay", "ArcStudy" and "ArcInvestigation" are classes with constructor for which i can reference the main constructor (optional parameters do not work out of the box though).

But for a true and simple record type this is not usable.

image

Freymaurer avatar Sep 25 '23 14:09 Freymaurer

I would require something like this:

export { OntologyAnnotation } from "./ARCtrl/ISA/ISA/JsonTypes/OntologyAnnotation.js"
export { ArcAssay, ArcStudy, ArcInvestigation } from "./ARCtrl/ISA/ISA/ArcTypes/ArcTypes.js";

Freymaurer avatar Sep 25 '23 14:09 Freymaurer

Hum indeed, the problem is I don't think F# as a mechanism for re-exporting types from modules.

So we will need a custom Fable syntax / code to cover this kind of re-export.

I think right now, you are best to manually edit an index.js file for exposing the types your want or have a post-build script which walk through the generated code to generate it.

MangelMaxime avatar Sep 25 '23 15:09 MangelMaxime

I think right now, you are best to manually edit an index.js file for exposing the types your want or have a post-build script which walk through the generated code to generate it.

Yeah that is what i thought 😄

Freymaurer avatar Sep 25 '23 15:09 Freymaurer

So if anybody finds this, here is how i solved it:

module GenerateIndexJs =

    open System
    open System.IO
    open System.Text.RegularExpressions

    let private getAllJsFiles(path: string) = 
        let options = EnumerationOptions()
        options.RecurseSubdirectories <- true
        IO.Directory.EnumerateFiles(path,"*.js",options)

    let private pattern (className: string) = sprintf @"^export class (?<ClassName>%s)+[\s{].*({)?" className

    type private FileInformation = {
        FilePath : string
        Lines : string []
    } with
        static member create(filePath: string, lines: string []) = {
            FilePath = filePath
            Lines = lines
        }

    let private findClasses (rootPath: string) (cois: string []) (filePaths: seq<string> ) = 
        let files = [|
            for fp in filePaths do
                yield FileInformation.create(fp, System.IO.File.ReadAllLines (fp))
        |]
        let importStatements = ResizeArray()
        let findClass (className: string) = 
            /// maybe set this as default if you do not want any whitelist
            let classNameDefault = @"[a-zA-Z_0-9]"
            let regex = Regex(Regex.Escape(className) |> pattern)
            let mutable found = false
            let mutable result = None
            let enum = files.GetEnumerator()
            while not found && enum.MoveNext() do
                let fileInfo = enum.Current :?> FileInformation
                for line in fileInfo.Lines do
                    let m = regex.Match(line)
                    match m.Success with
                    | true -> 
                        found <- true
                        result <- Some <| (className, IO.Path.GetRelativePath(rootPath,fileInfo.FilePath))
                    | false ->
                        ()
            match result with
            | None ->
                failwithf "Unable to find %s" className
            | Some r ->
                importStatements.Add r
        for coi in cois do findClass coi
        importStatements
        |> Array.ofSeq

    open System.Text

    let private createImportStatements (info: (string*string) []) =
        let sb = StringBuilder()
        let importCollection = info |> Array.groupBy snd |> Array.map (fun (p,a) -> p, a |> Array.map fst )
        for filePath, imports in importCollection do
            let p = filePath.Replace("\\","/")
            sb.Append "export { " |> ignore
            sb.AppendJoin(", ", imports) |> ignore
            sb.Append " } from " |> ignore
            sb.Append (sprintf "\"./%s\"" p) |> ignore
            sb.Append ";" |> ignore
            sb.AppendLine() |> ignore
        sb.ToString()

    let private writeJsIndexfile (path: string) (fileName: string) (content: string) =
        let filePath = Path.Combine(path, fileName)
        File.WriteAllText(filePath, content)

    let generateIndexfile (rootPath: string, fileName: string, whiteList: string []) =
        getAllJsFiles(rootPath)
        |> findClasses rootPath whiteList
        |> createImportStatements
        |> writeJsIndexfile rootPath fileName

Creates something like this:

export { Comment$ } from "./ISA/ISA/JsonTypes/Comment.js";
export { Person } from "./ISA/ISA/JsonTypes/Person.js";
export { OntologyAnnotation } from "./ISA/ISA/JsonTypes/OntologyAnnotation.js";
export { IOType, CompositeHeader } from "./ISA/ISA/ArcTypes/CompositeHeader.js";
export { CompositeCell } from "./ISA/ISA/ArcTypes/CompositeCell.js";
export { CompositeColumn } from "./ISA/ISA/ArcTypes/CompositeColumn.js";
export { ArcTable } from "./ISA/ISA/ArcTypes/ArcTable.js";
export { ArcAssay, ArcStudy, ArcInvestigation } from "./ISA/ISA/ArcTypes/ArcTypes.js";
export { Template, Organisation } from "./Templates/Template.js";
export { JsWeb } from "./Templates/Template.Web.js";
export { ARC } from "./ARCtrl.js";

Freymaurer avatar Sep 25 '23 21:09 Freymaurer

@Freymaurer Thank you for sharing the script

MangelMaxime avatar Sep 26 '23 13:09 MangelMaxime