Using the Compiler API
Using the WebSharper compiler programmatically involves 3 main steps:
- Gather the WebSharper metadata, which is an object of type
WebSharper.Core.Metadata.Info. If you want to use WebSharper's .NET andFSharp.Coreproxies, or have other WebSharper projects/bindings you rely on, you must load the embedded metadata from your referenced assemblies, then merge them to a single metadata object for optimized dictionary lookups and ensuring no conflicts exist. - Create a
WebSharper.Compiler.Compilationinstance with this metadata, and populate it with types/members to translate. This can be done with different helpers depending on you are working from a checked F#, or C# project, or ReflectedDefinitions in an already compiled F#dll. Then callWebSharper.Compiler.Translator.DotNetToJavaScript.CompileFullon theCompilationto run the full translation. The resulting errors/warnings can be checked and handled. - Depending on the output required, code can be modularized (WebSharper calls creating a standalone module bundling, and creating modules with interdependencies packaging) and written to JavaScript/TypeScript/TypeScript declaration file(s). To create a
dllthat is usable by other WebSharper projects, the resulting current metadata can be serialized and embedded into thedll.
The packages needed are WebSharper.Compiler.Common for core functionality, WebSharper.Compiler.FSharp for processing F# code/projects, and WebSharper.Compiler.CSharp for processing C# code/projects.
Handling metadata and code dependency graphs
WebSharper-compiled dlls may have two kinds of metadata embedded in them: the metadata needed for compilation using that dll as a reference, and for web projects only, a runtime metadata that is optimized for the needs of the WebSharper sitelet and remoting middleware. For compilation purposes we need the non-runtime metadata.
Three options to load metadata from an assembly, all of these return an option value, None if no WebSharper metadata is present:
- Use
WebSharper.Core.Metadata.IO.LoadMetadatato deserialize metadata from aSystem.Reflection.Assembly. This assembly must not be loaded in reflection-only mode (e.g.Assembly.ReflectionOnlyLoad), so it requires loading the assembly to the defaultAssemblyLoadContextor an isolated one. - The
WebSharper.Compiler.FrontEnd.ReadFullFromFileusesMono.Cecilin the back to safely load metadata from adllfile path. - If also want caching and looking up
dlls by file names only in pre-determined locations or directories, you can create aWebSharper.Compiler.AssemblyResolverinstance withAssemblyResolver.Create(). Then you can adddllfile paths with theSearchPathsmethod, or directory paths to enumerate and add alldlls found with theSearchDirectoriesmethod. Once theAssemblyResolveris set up, create aWebSharper.Compiler.LoaderwithLoader.Create. It takes theAssemblyResolverand a logger function. Now you can use itsLoadFilemethod to get aWebSharper.Compiler.Assemblyencapsulation of the assembly, and it is also cached. Now theWebSharper.FrontEnd.TryReadFromAssemblyorReadFromAssemblygets the metadata from this assembly (the difference is thatTryReadFromAssemblyreturns metadata load failures as a value, not an exception). These also take aWebSharper.Core.Metadata.MetadataOptionsvalue, see the next section.
Filtered metadata
The WebSharper.Core.Metadata.Info type has three methods for discarding expressions inside for optimizing memory size.
DiscardExpressionsremoves all expressions, this is the default when creating runtime metadata, however for compilation this is undesirable.DiscardInlineExpressionscan be used when the metadata will be used only to create new bundles, but not generating new code that could possibly use WebSharper's[<Inline>]annotated type members.DiscardNotInlineExpressionscan be used when the metadata will be used only to compile and write new code, that will not include any new code from references, but using JS imports for them.
The WebSharper.Core.Metadata.MetadataOptions type can specify a filter on which of the above to apply or none, FullMetadata applies none, while the other three values are matching the name of the corresponding Discard... method they use.
Merging metadata
When metadata are loaded from all references of a project, they must be merged into a single WebSharper.Core.Metadata.Info object for optimized dictionary lookups. This can be done with the Metadata.Info.UnionWithoutDependencies static method which takes a collection of metadata objects. The method name clarifies that the merged metadata will not contain a merge of the code dependency graph. This is because the dependency graph is only required for DCE (dead code elimination), not for any other compilation steps, so it should be done separately when neeeded, see later.
The UnionWithoutDependencies method can raise exceptions when conflicting members are found (for example the same member is proxied in two different references). This is a fatal error because it cannot be determined which client-side representation to use for a consistently working translation.
Creating a compilation
For translating F# or C# source, the next step is to create a WebSharper.Compiler.Compilation object with the metadata passed in to the constructor. It has another optional constructor argument hasGraph, which is true by default, you can set it to false if you don't want to do any dead code elimination later.
Then you can populate it with WebSharper.Compiler.FSharp.ProjectReader.TransformAssembly or WebSharper.Compiler.CSharp.ProjectReader.TransformAssembly that takes an FSharp.Compiler.Service or Microsoft.CodeAnalysis.CSharp (Roslyn) representation of a checked project and extracts all code marked for WebSharper.
The third option is to use WebSharper.Compiler.QuotationCompiler to transform a runtime assembly's ReflectedDefinition expressions with CompileReflectedDefinitions. This class creates its own Compilation object automatically and exposes it on the Compilation property. It can also be used to transform further F# quotations on the fly with CompileExpression.
Run the transformers
Call the WebSharper.Compiler.Translator.DotNetToJavaScript.CompileFull method on a Compilation to process all of its untranslated types and members. Then you can inspect the Errors, and Warnings properties for any compilation diagnostics. These lists can also contain source positions, and their message can be read using a .ToString().
Also the WebSharper metadata output becomes available on .ToCurrentMetadata().
Packaging and writing single files
Three steps remain to get final JavaScipt/TypeScript output. These are:
- Creating a file package (a
Statement[]representing file contents) with some funtions from theWebSharper.Compiler.JavaScriptPackagermodule. - Using
WebSharper.Compiler.JavaScriptWriter.transformProgramto convert theWebSharper.CoreAST Statements intoWebSharper.Core.JavaScriptparse tree. - Writing string with
WebSharper.Compiler.JavaScriptPackager.programToString.
The first step has multiple options corresponding to different WebSharper output modes:
bundleAssemblyallows creating dead code eliminated bundles (SPA mode).packageEntryPointcreates separate dead code eliminated page bundles (sitelets prod mode).packageEntryPointReexportcreates one-file-per-class plus an additionalroot.jsthat re-exports everything needed for sitelet page initializations (sitelets debug mode).packageLibraryBundlecreates dead code eliminated library output (npm library output mode).
Dependency graph and DCE
The modes using dead code elimination also expect a dependency graph. Construct it from your project reference metadata and current compilation like this:
The bundleAssembly function does not do the dead code elimination itself, but you can do it by trimming the metadata you pass to it:
Here, EntryPointNode will find the member annotated with the [<EntryPoint>] attribute. You can create your own list of top level entry point nodes too with other cases like MethodNode, ConstructorNode, and more.
Generating all WebSharper resources
To mimic the WebSharper compiler's behavior the closest, you can call WebSharper.Compiler.FrontEnd.CreateResources. This takes a record with various settings mapping to WebSharper settings.