Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Best practise for native files in .fsproj #3657

Open
Freymaurer opened this issue Dec 13, 2023 · 13 comments
Open

Best practise for native files in .fsproj #3657

Freymaurer opened this issue Dec 13, 2023 · 13 comments

Comments

@Freymaurer
Copy link

Description

We have a fable-library for which we wrote some native js code. We included it as EmbeddedResource in .fsproj, so it works on the .NET side with nuget packaging. But we noticed, that Fable does not copy it to the output directory. Should we refactor it to rmv the native file and do everything with fable bindings or is there a best practise on how to copy it?

Repro code

Here is the relevant code from .fsproj:

...
<!--native js file-->
<EmbeddedResource Include="Validation\JsonValidation.js">
  <CopyToOutputDirectory>Always</CopyToOutputDirectory>
</EmbeddedResource>
<Compile Include="Validation/ValidationResult.fs" />
<!--This references native js file-->
<Compile Include="Validation/Fable.fs" />
<Compile Include="Validation/JsonSchemaValidation.fs" />
...

native js file is referenced via:

[<ImportAll("./JsonValidation.js")>]
let JsonValidation: IValidate = jsNative

Related information

  • Fable version: 4.8.1
  • Operating system
@MangelMaxime
Copy link
Member

Hello,

Fable is only using files that are included in the fable folders.

In general, in your fsproj you should have line like:

<ItemGroup>
    <Content Include="*.fsproj; *.fs; *.fsi" PackagePath="fable\" />
  </ItemGroup>

You need to change it to:

<ItemGroup>
    <Content Include="*.fsproj; *.fs; *.fsi; *.js" Exclude="**\*.fs.js" PackagePath="fable\" />
  </ItemGroup>

Depending on how you build test your project you could have some Fable generate files .fs.js near your .fs files so by security I added the rule Exclude="**\*.fs.js" to not include theses files as you want Fable to compiled the files.

@Freymaurer
Copy link
Author

Ahh that solution was so obvious! I am very sorry, could have thought about this myself.

@MangelMaxime
Copy link
Member

MangelMaxime commented Dec 13, 2023

No worries, this is not that much explains in the documentation either.

I will add a note for it

@Freymaurer
Copy link
Author

Ah i actually just noticed that this change only affects nuget packaging. Which was already doing fine with the embedded ressource. We currently publish an npm package from the result of dotnet fable path/to/project -o dist/js and running this will not copy the native js file.

@Freymaurer Freymaurer reopened this Dec 13, 2023
@MangelMaxime
Copy link
Member

In this case, it depends on how your code/package is structured.

For example, in Nacara I have a js folder at the root of my package which contains native JavaScript files which is consumed later by F# code.

And what I did is in the package.json, I told NPM to package the dist folder and js folder in the package:

{
    "name": "nacara-layout-standard",
    "version": "1.8.0",
    "description": "",
    "exports": {
        ".": "./dist/Export.js",
        "./*": "./*"
    },
    "type": "module",
    "files": [
        "dist/**/*.*",
        "scss",
        "scripts",
        "js"
    ]
}

Which results in the following package structure:

CleanShot 2023-12-13 at 12 23 43@2x

In theory Fable, imports are computed relatively to the destination of the file so you don't have anything special to do in that case.

If you don't have having several folders in your package, you can also put your native javascript files inside of dist and them use the macro ${outDir} in your import. In this case, Fable will not try adapt the import and just replace ${outDir} with . at compilation time.

[<Import("hello", "./util.js")>]
let hello : unit -> unit = nativeOnly

[<Import("hello", "${outDir}/util.js")>]
let hello2 : unit -> unit = nativeOnly

generates

import { hello } from "../src/util.js";
import { hello as hello_1 } from "./util.js";

See how the second import is not relative to the destination of the compile file.

@laurentpayot
Copy link

laurentpayot commented Dec 13, 2023

@Freymaurer in the context of a pure Elmish example, I had to add the following lines to copy native files to my output directory:

    <!-- TargetPath are relative to bin/Debug/net8.0/ -->
    <Content Include="src/index.html" CopyToOutputDirectory="PreserveNewest" TargetPath="../../../output/src/index.html" />
    <Content Include="src/index.js" CopyToOutputDirectory="PreserveNewest" TargetPath="../../../output/src/index.js" />

@MangelMaxime PackagePath="<whatever>" did not work when I tried with my example above. I’m new to MSBuild stuff and their documentation is quite confusing. Would you have an advice on how to copy native files in a Vite build of a pure Elmish app like my example?

@MangelMaxime
Copy link
Member

@laurentpayot PackagePath="<whatever>"

Is only used when package your package by running dotnet pack, it will create a fable folder in the NuGet pacakge.

When creating an application via Vite, you should not need to copy things with MSBuild. Vite should do it for you when you point it to the entry file or your project.

When creating a JavaScript application with Fable what is important to understand is this paragraph from the Fable doc:

CleanShot 2023-12-13 at 15 04 23@2x

In your case, you want to compile the file using Fable and setup Vite to use them as the entry point. Then when you build using Vite, vite will create the bundle and everything for you in its output folder.

Documentation on how to setup Fable and Vite are available here;

You can also have a look at this template which has Vite, Fable and React setup together. Using Elmish instead of pure React, should just be a matter of dependencies and touching the F# code. Not the project setup.

If you still have problems, please open an issue so it can be discussed separately.

@laurentpayot
Copy link

Thanks for your answer @MangelMaxime.

I should have mentioned that I use Fable with the outDir option to put transpilated files where they are meant to be, i. e. outside the source directory (the mere existence of fable clean means the default usage of the source directory as an output directory is kinda dirty).
Things get complicated in Vite when JS files are not in the same directory as index.html because relative links to JS files do not work. The most simple simple option is to copy index.html and index.js to the output directory and setup Vite to work with that JS output directory instead of the F# source directory. By index.js I mean the Vite JS entry file used to, between other things, call the Fable-generated and exported startApp() function to start the UI (with non hard-coded startup flags as a parameter).

I think opening a new issue would be redundant as I am literally looking for "Best practice for native files in a .fsproj", but simply when the output directory is not the same as the source directory (my example repo is just some preliminary research before a potential non-trivial Elm PWA rewrite to F#).

@MangelMaxime
Copy link
Member

I should have mentioned that I use Fable with the outDir option to put transpilated files where they are meant to be, i. e. outside the source directory (the mere existence of fable clean means the default usage of the source directory as an output directory is kinda dirty).

There is not silver solution to the best practise unfortunately. Its depends on what people values the most.

For some people having, generated files near the source is good because they can just configure their IDE to hide them or nest them inside of their parent file like VSCode allows you to do.

For others, they prefer to have the output separated from the sources files and in this case they could follow .NET philosophy by using a sub-folder inside of the source directory. I often use the fableBuild folder myself.

Another possibility, could be to move the index.html outside of the src folder (this is/was common on some JavaScript project with TypeScript appearance). Like that it can consume the generated files who are placed outside of the source directory.

To show one of the many possible structures:

CleanShot 2023-12-13 at 18 42 32@2x

  • dist is the output of Vite
  • fableBuild is the output of Fable
  • src contains both the Fable and JavaScript sources files (kind of expect to the entry point managed by Fable, makes import cleaner as you don't have your src folder being aware of outside files)
  • public is a folder contains static assets like images, libraries (better to use NPM for that now days), polyfils, etc.

@Freymaurer
Copy link
Author

@Freymaurer in the context of a pure Elmish example, I had to add the following lines to copy native files to my output directory:

    <!-- TargetPath are relative to bin/Debug/net8.0/ -->
    <Content Include="src/index.html" CopyToOutputDirectory="PreserveNewest" TargetPath="../../../output/src/index.html" />
    <Content Include="src/index.js" CopyToOutputDirectory="PreserveNewest" TargetPath="../../../output/src/index.js" />

@MangelMaxime PackagePath="<whatever>" did not work when I tried with my example above. I’m new to MSBuild stuff and their documentation is quite confusing. Would you have an advice on how to copy native files in a Vite build of a pure Elmish app like my example?

Sadly this is not a solution for us, as i assume this will on-build copy the Include=""-file to TargetPath="" and we have a rather big repository with multiple projects and would require this file to be copied to multiple locations.

So currently for us it seems to be best to dismiss the js file and add some bindings, or use emit to call the js code in pure f# files.

@MangelMaxime maybe it would be possible to have a option in fable to copy files on transpile?

@laurentpayot
Copy link

Thanks again @MangelMaxime for your detailed answer. I didn’t know you could do this in VSCode.

@laurentpayot
Copy link

@Freymaurer I’m not 100% sure of what you mean but did you try the Copy Task? It even has a UseSymbolicLinksIfPossible option.

@MangelMaxime
Copy link
Member

@Freymaurer Can't you do like Nacara and have a js folder containing your native JavaScript files in your NPM packages?

@MangelMaxime maybe it would be possible to have a option in fable to copy files on transpile?

The difficulty I see with adding copy files support to Fable is that there are a lot of ways that could be done:

  1. Fable could detect import to local JavaScript files and decide to move them in the output and adapt the import
  2. Supports MSBuild features but this means that Fable needs to understand MSBuild syntax which is really complex and one of the most complex piece of software. We could support a subset of it, but people would probably always ask for more 😅

Also, when you write [<Import("hello", "./util.js")>] it is important to not that Fable adapt the import part relatively based on the destination of the so it can generates import { hello } from "../src/util.js"; for example.

So if you move the native JavaScript file out, you need to know about the less known ${outDir} macro which tells Fable that the import path is relative to the output folder.

[<Import("hello", "${outDir}/util.js")>] will always generates import { hello } from "./util.js";

@laurentpayot Fable only use MSBuild to restore the project and get the dependencies list. So in theory, MSBuild tasks are not going to be executed unless you invoke both dotnet fable and dotnet build for example.

This is reason why the following does nothing in Fable.

<EmbeddedResource Include="Validation\JsonValidation.js">
  <CopyToOutputDirectory>Always</CopyToOutputDirectory>
</EmbeddedResource>

I believe their are several solutions that can already be used today to work around this "limitation".

  1. You can have a watcher / post process task which copies the native files to their destination. This is for example, something we do when building Fable.

  2. You can write the native files directly in the destination folder and use gitignore to keep track of theses files while ignoring the generated ones

  3. You can have a dist and js folders side by side to avoid mixing generated files and native files.

  4. etc.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants