IntelliFactory

Create JavaScript libraries from your C# or F# code

websharper
By András Jankó on Friday, June 15, 2018 — 12 comments

There might be many reasons why a .NET-to-JavaScript transpiler is useful, some of the most common use cases:

  • Reuse existing code within a web application
  • Use the same data models and part of the code for the server and client side of the same web project
  • Write complicated algorithms or business logic with strong typing and advanced features of C# and/or F#, which is easier to implement and test in .NET

WebSharper is such a transpiler, but also more, a set of tools for creating full websites or standalone single page web application. But sometimes something simpler and more to the point tool is needed, to just create a single self-contained .js file that is exposing the translated code well and can be used as a module in a website or JavaScript-based mobile application using any other technologies and frameworks. WebSharper 4.2.13 introduces a simple way to do just this that previously required some hacks.

This article is using C# terminology and syntax for attributes, but all the same features apply to F# as well.

Translating a library project

Suppose that you have a library project that contains functionality that you would like to access in JavaScript. First, add WebSharper.CSharp nuget package (or WebSharper.FSharp for an F# project). This also installs the WebSharper package, containing the core APIs and proxies (JavaScript implementations of .NET framework built-in classes).

Translate whole code

WebSharper does not auto-translate anything in a project, only marked code parts. This is so that mixed client/server projects can be written within a single project. But if you do like to translate it all, use

1
2
3
using WebSharper;

[assembly: JavaScript]

If the project only uses .NET standard library classes and methods that WebSharper knows a translation for, you get no errors, otherwise at this point the WebSharper code analyzer alerts you at the method and constructor calls that are not supported for client-side use (You have to build the project to see WebSharper-related warnings/errors for F#). To handle these errors, look below.

Even if you have no errors, when you build the project, no output is produced other than the normal .dll. This is because the default mode of WebSharper is to prepare libraries to be used in WebSharper-based sites. When you look inside the .dll with a tool like ILSpy, you can find resources added to the assembly, for example a WebSharper.js which contains the translated code for the current library (but not any dependency it might have). Extracting this is still not enough, as this code can rely on JavaScript files contained in references (like WebSharper core libraries itself).

To create a self-contained .js, we need to configure the WebSharper compiler a bit.

The wsconfig.json file

The easiest way for configuration is to add a file named wsconfig.json to your project root folder. Initialize it like this:

1
2
3
4
5
6
{
  "$schema": "https://websharper.com/wsconfig.schema.json",
  "project": "bundle",
  "outputDir": "Content",
  "javascriptExport": true
}

Now if you build your library, you will see a Content folder populated within your project folder. Our main interest is in the MyLibrary.js file, which contains the full JavaScript output (there is also a minified version of it). The "bundle" project type uses dead code elimination to produce this, recursively only including those functions that are called from the starting code points. These starting points are:

  • You can have a static argumentless method marked with the [SPAEntryPoint] attribute. This will be included in bundle output (along with all code dependencies), and will run on script load.
  • Any other code marked directly or indirectly with [JavaScriptExport] (which is a stronger attribute than [JavaScript], marking it for translation but also inclusion in bundle output) and their code dependencies.

The "javascriptExport": true setting in wsconfig.json is actually equivalent to having [assembly: JavaScriptExport], so the previously added assembly-level attribute can be removed with no change.

Note, that you also have a MyLibrary.head.html and MyLibrary.css files. The former contains links generated to all external scripts that the bundle output .js file needs. The .css contains all style resources embedded in WebSharper-enabled libraries that are required by the bundled code. (This should be empty if you only use WebSharper core libraries.)

The MyLibrary.head.js is just a variant of the head.html to use in active development only, it uses document.write to add the required scripts. This is a deprecated feature in modern browsers, but allows linking this script and not worry about if required scripts are changing during development. It is highly recommended to take a look at head.html and making sure those dependencies are included in your html file when building for deployment.

Output .js file only

If you use the "project": "bundleOnly" project type, the output .dll will not get the WebSharper-specific resources added, so it will not be able to be used as a reference to another WebSharper-enabled project. (In the case of F#, even emitting a .ddl is skipped.) The only goal of WebSharper compiler here becomes to produce the bundle output files.

If you do not set "outputDir" but set one or both of "jsOutput" and "minJsOutput" (value must be a relative or absolute path to the output file), only the .js and/or .min.js files will be produced.

Translate select methods and types

If you only want to mark specific methods and/or types to be included in .js output (serving as starting points for dead code elimination), you can apply the JavaScriptExport attribute and/or wsconfig.json setting in various ways:

  • Add a [JavaScriptExport] attribute to the type or member itself or on a whole assembly. The latter is equivalent to having "javascriptExport": true.
  • Add a [JavaScriptExport(typeof(MyType))] attribute on either assembly-level or on a class that contains the targeted type as a nested class. This marks the targeted type, same as if it would have a [JavaScriptExport] directly. This can also be used to target types in another project. Note that the other project need to be WebSharper-enabled too, and have that type in marked with [JavaScript] (using one of the same ways as for JavaScriptExport). So when the first assembly is compiled, WebSharper can prepare the translation of that type, and when targeting it with JavaScriptExport, that can be added to the bundle output readily.
  • Add a [JavaScriptExport("MyNameSpace.MyType")] attribute, which works the same as using typeof. It also has a configuration equivalent in "javascriptExport": [ "MyNameSpace.MyType" ]. You can add multiple types to this array.
  • Add a [JavaScriptExport("AssemblyName")] attribute or equivalently use "javascriptExport": [ "AssemblyName" ]. This marks the full WebSharper-translated part of targeted assembly to be included in bundle output. So that library must have the WebSharper libraries and tools referenced and have the parts intended for client-side use marked for translation with JavaScript/JavaScriptExport.

Resolve not found in JavaScript translation compilation errors

There are limitations to WebSharper's ability to translate method and constructor calls, only those that have proxies in the core libraries will be able to be used in JavaScript-annotated code. WebSharper uses type-erasure for the output JavaScript code, using dynamic code generation based on type information at compile time. This means that using System.Type and reflection are not supported. So sometimes you have to modify your code to run in a suitable way in the browser while also keeping .NET functionality intact.

Branching via IsClient

The easiest solution is to introducing some branching around code parts that cannot be translated directly to JavaScript. Use the static property WebSharper.Pervasives.IsClient in a branching (if or ? :) to have the false branch not considered for translation, while it will be run in .NET proper. (You can write only IsClient in F# if you have open WebSharper)

1
2
3
4
5
    if (WebSharper.Pervasives.IsClient) {
        // this will be translated and run in the browser
    } else {
        // this will only run in .NET
    }

Adding proxies

WebSharper core libraries itself are defining types that are internal but marked with the Proxy attribute to denote that they are implementing another class for client-side use. You can do the same to add support for translating types that are found in non-WebSharper enabled assemblies (either framework assemblies or third-party libraries).

For a minimal implementation of StringBuilder, you can write:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
[Proxy(typeof(StringBuilder))]
internal class StringBuilderProxy
{
    private List<string> b = new List<string>();

    public StringBuilderProxy Append(string s)
    {
        b.Add(s);
        return this;
    }

    public override string ToString()
    {
        var s = String.Concat(b);
        b.Clear();
        b.Add(s);
        return s;
    }
}

Or the same in F#:

1
2
3
4
5
6
7
8
9
10
11
12
13
[<Proxy(typeof<System.Text.StringBuilder>)>]
type internal StringBuilderProxy () =
    let b = ResizeArray()

    member this.Append(s: string) =
        b.Add(s)
        this

    override this.ToString() =
        let s = String.concat "" b 
        b.Clear()
        b.Add(s)
        s

If the type signature of a method matches the a method on the targeted class (taking the proxy type itself to be equivalent to the target), then it will serve as the JavaScript implementation.

If you feel that some type or method should have a standard proxy, feel free to ask about it on the WebSharper forums or the submit a pull request to the open-source repository on GitHub.

Have a good time running .NET code in your websites!

  • user3359

    I have been trying this feature. There are a couple of issues:

    1.- Could we add command line options for jsOutput and minJsOutput so that wsconfig.json is not needed? 2.- when project is of type bundleonly wsfcs.exe ignores all errors and generates partial JavaScript code:

    1
    2
    3
    4
    5
    6
    
            module Hello
                open WebSharper
                [< assembly: JavaScriptExport >]
                do ()
                
                let run() = xxprintfn "Hello"

    compiles as

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    
    ...
    ...
    };
    (function()
    {
     "use strict";
     var Global,Hello;
     Global=self;
     Hello=Global.Hello=Global.Hello||{};
     Hello.run=function()
     {
      throw 1;
     };
    }());
    
    ...

    When there are more errors it simply compiles as void 0; plus the rest of the supporting code.

    When project type is not bundleany the compiler shows the correct error messages.

    • user3359

      Also bundle throws this error:

      1
      2
      3
      4
      5
      
      WebSharper translation: 00:00:00.5186946
      WebSharper error FS9001: Global error 'An index satisfying the predicate was not found in the collection.' at    at Microsoft.FSharp.Collections.ListModule.Find[T](FSharpFunc`2 predicate, FSharpList`1 list)
         at WebSharper.Compiler.Bundling.AddExtraBundles(WsConfig config, FSharpList`1 refMetas, Info currentMeta, FSharpList`1 refAssemblies, Compilation comp, FSharpChoice`2 assem)
         at WebSharper.FSharp.Program.Compile$cont@218-3(WsConfig config, Loader loader, FSharpList`1 refs, Task`1 wsRefsMeta, Compilation comp, Info currentMeta, Unit unitVar)
       ...

      Which goes away when either netstandard.dll or mscorlib.dll is included as a reference. Maybe the message should say so.

  • user3359

    Feature request: With the option bundleonly can we also get a list of dependencies? All the .js and .css files required by the jsoutput file. That way we can compile F# code and add it to a web page that is already running websharper by adding only the missing dependencies.

    I was thinking of doing it myself but I would need some guidance.

  • user3359

    Hi! The option bundle produces the .js files and the .head.html files with the external dependencies, but It does not produce a list of WebSharper .js dependencies. How can we know which WebSharper files to include?

    • JankoA

      Hi!

      You should need no more script links. The bundle build always produces a single .js that might have outside dependencies but have every WebSharper-related code self-contained. By default, dead code elimination is on, so it has only those functions that can be discovered from the function marked SPAEntryPoint.

      • user3359

        Hi Janko, thanks for your reply.

        That is true, I was thinking of the jsOutput file which has only the translated code excluding the dependencies. My use case is that I want to compile code to be added to an already running WebSharper site. For that reason it should not declare or initialize again code in dependencies that have already been loaded and at the same time it needs to be able to load any dependencies not yet loaded.

        • JankoA

          If it's not a static site, then WebSharper's server-side Sitelet runtime is doing the exploration of needed dependencies on every page request for the site. This uses metadata encoded in dlls to know about the call graph, so the recommended way is to have your library as a dependency and rebuild your site with that.

          But if for some reason, you really want to hot-swap and not recompile the site itself, then an extra link to a bundle output should not cause much problems just make sure you load the bundle dll first as it may have some incomplete class definitions that the site's own scripts will override with non DCE-d versions. There is not much way to eliminate some duplicate code loading here because then the build of your bundle project and site are independent.

          • user3359

            Hot swapping is exactly what I want to do. The issue I have is how to know the WebSharper dependencies of the new code? I know the external dependencies because they are written in the file .head.html but other dependencies like WebSharper.Collections.js are not included.

            • JankoA

              Sorry, I have realized that in bundle mode, there is still a standard style .js created too so the library can be used as a WebSharper dependency too. If you want only the bundled output, use bundleonly. Then the .js file will have everything included.

              Or alternatively, if you have your library referenced correctly, used from within C#/F# and not via some JS code or inlines and does not have breaking public API changes, you can still dump the new dll in the bin and js in Scripts/WebSharper folders without rebuilding. The dll is needed for the runtime graph update if you have new dependencies introduced with your changes.