core icon indicating copy to clipboard operation
core copied to clipboard

Change WebSharper output to TypeScript

Open granicz opened this issue 8 years ago • 12 comments

So far, in all pre-4.0 and Zafir (4.0 alpha+beta+RC) releases, WebSharper generated JavaScript from F# (and with Zafir also C#) code. In addition to this, 3.x releases were capable of generating TypeScript declaration files (.d.ts) for WebSharper libraries and extensions, and also consume TypeScript declaration files to generate WebSharper extensions.

In order to strengthen the F#/C#-TypeScript-JavaScript interoperability and enable new innovative integrations (webpack, etc.), we would like to switch to generating TypeScript as a standard output format for WebSharper projects.

This ticket is a placeholder to document the main TODOs and the ways this change will affect the generated code and its use.

granicz avatar Sep 21 '17 21:09 granicz

Work plan:

  • [x] Add module import/export support to packager. Each project still will produce a single .ts and will act as a TS module.
  • [x] Fix up minimal syntax differences, for example optional arguments have to be marked on definition so that they could be omitted at call.
  • [ ] Fix Activator to look up types for RPC results from modules.
  • [x] Convert class definitions to use TS class, expose interfaces too. This needs some constructor call convention changes because TS only allows a single implementation for a class constructor.
  • [ ] Add type signatures for all members. This will require some redirection from WebSharper's JS/DOM classes to their standard TS type names, also custom logic for every type that WS translates specially (function interop types, erased unions).
  • [ ] Generate .d.ts definitions for WIG libraries.

Jand42 avatar Sep 22 '17 08:09 Jand42

Details on constructor tranlation: for this definition

type MyType(x: int) =
    new() = MyType(2)   

WebSharper currently emits this:

 MyType=M.MyType=Runtime.Class({},null,MyType);
 MyType.New=Runtime.Ctor(function()
 {
  MyType.New$1.call(this,2);
 },MyType);
 MyType.New$1=Runtime.Ctor(function(x)
 {
 },MyType);

So there is multiple constructors, class prototype is at MyType but instances are created with either new MyType.New() or new MyType.New$1(x). TS does not support constructor overloading like this, multiple constructors for a single class. the only thing it can do is a single constructor implementation, and multiple constructor signatures, all of which must be compatible with the implementation. So the implementation could take an additional string to disambiguate the overload. String literals can be used as types, so output would look something like this:

    constructor(i: '0', x: int);
    constructor(i: '1');
    constructor(i: string, x) {
        switch (i) {
            // ...
        }
    }

This gives nice code service support, and full safety when writing TS against WS output. Activator needs to be changed to use Object.create instead of calling the default object copier that current Runtime.Class creates.

Jand42 avatar Sep 22 '17 08:09 Jand42

On module generation by the packager: all common namespace path would be removed. For example WebSharper.UI.Next defines all its types/functions within that namespace. Currently output sets it all up at window.WebSharper.UI.Next. TS would create a module that never contains a single top level namespace as is recommended, but moved up. So consuming it would be

import * as UIN from "./WebSharper.UI.Next";

where UIN is a recommended short name, set by a new feature, using Name attribute on the assembly level.

Jand42 avatar Sep 22 '17 08:09 Jand42

Follow-ups: Use more types/syntax for cleaner output and better interoperability.

  • Use accessibility modifiers on class members and do not export everything, but based on accessibility in the source.
  • let instead of var, => instead of function for better scoping without current fixers for block and this scoping.
  • TS generators and async instead of custom state machine generation.
  • Use Promises for Tasks and F# asyncs (latter could have type CancellationToken => Promise<T>). inside the Promise constructor they would still use a round-robin scheduler as currently to avoid hangups.
  • Recognize functions that always throw an exception to mark return type as never.

Jand42 avatar Sep 22 '17 08:09 Jand42

Progress is on ts-output branch.

My original idea was that I can have mostly correct TS output by converting to export/import and using namespaces. But there is still a lot of red because classes are still defined with WS custom logic, so they are objects, and there is no way to merge objects with namespaces (having them use the same name). It is fine with TS classes and namespaces, so I'm moving on to do the writer for TS-style classes.

Jand42 avatar Sep 22 '17 21:09 Jand42

Current status:

  • Namespaces and classes are using TS constructs
  • Class constructors use translation as above for overloaded .NET constructors, needs fixes for calling another constructor of current type and copy constructors used by F# unions/records and RPC result activator.
  • Class fields and abstract method signatures are exposed.

Current goals:

  • Fix chained constructors
  • Generate constructors for F# unions/records and use Object.create inside Activator
  • Mark all optional parameters properly
  • Avoid emitting unreachable code

Jand42 avatar Sep 25 '17 13:09 Jand42

All projects compile, TS output needs fixes. Biggest one is that packager needs to use dependency graph to better explore all .js resources that has to be imported.

Syntax errors:

Operator '===' cannot be applied to types '3' and '"3"'. Operator '!=' cannot be applied to types '"foo"' and '"bar"'.

  • Literals are their own types, so operators are not allowed even between 2 strings if they are different. Solution is to optimize these away to true/false by evaluating the statically known expression.

Unused label.

  • Same as statements, unused stuff for some reason are not just a warning but error in TS, so it needs to be eliminated. Async state machine transformer shares basic logic with sequence generator, creates a label for top loop but does not need it (returns the task object instead).

Constructors for derived classes must contain a 'super' call. 'super' must be called before accessing 'this' in the constructor of a derived class.

  • Solvable by rearranging how extra stuff is written into constructor bodies

A namespace declaration cannot be located prior to a class or function with which it is merged.

  • Solvable by first emitting all class definitions (instance parts + constructor) then all statics (these can grouped by namespace so there is no more jumping between namespaces)

Property '_v' does not exist on type 'typeof WebSharper$Web$Tests_JsonEncoder'.

  • JSON macro also has to emit a field declaration where it is storing pre-calculated functions.

Jand42 avatar Sep 26 '17 09:09 Jand42

Down to 11 syntax errors on all output of WS solution. And it is only 4 different cases:

Cannot invoke an expression whose type lacks a call signature. Type 'void' has no compatible call signatures.

  • In some tests there is an inner lambda that deliberately fails (confirming that lambda is not called). But it is inside a curried lambda, and the inner one returns void by TS type inference, so applying it as a function then fails.

Expected 0 arguments, but got 1.

  • Only on calls to WebSharper.Testing.Random.Natural. It is a generic module value, should be called with just (), maybe FCS gets confused about passing generic type parameter.

Cannot invoke an expression whose type lacks a call signature. Type 'typeof String' has no compatible call signatures.

  • Because a namespace named String is in scope. When namepsace names would collide with standard library stuff, local name should be renamed then exported with an alias.

Cannot find name 'StubTest'.

  • Packager should recognize stub declarations, that while they are inside current assembly, they are not adding any code, so needs to be translated to just declares.

Jand42 avatar Sep 26 '17 22:09 Jand42

Current challenge is to have all tests run correctly, this still needs some smarter naming to create aliases for outside global objects when a namespace/class/var/function name would shadow it. For example this is currently failing:

module String =
    let myToChar (x: int) = char x

because it translates to

export namespace String {
  export function myToChar(x) {
    return String.fromCharCode(x);
  }
}

where the outside String object is shadowed by the namespace name. Aliases where needed can solve this, resolved in such a way that there are no naming collisions with any public fixed name.

Jand42 avatar Sep 29 '17 14:09 Jand42

TypeScript code generator will have 2 settings for using import/export or just references. The latter can be used so we have full compatibility with current Sitelets logic. So that would be the recommended way to do multi-page applications. For SPAs (including packaged applications, like Cordova apps) the option to write modules will be more useful as tools like webpack can be used to bundle the site/app.

Reference version would keep current namespace structure, but module output would support splitting a single project into multiple TS module outputs, and all flattened that contents are top-level exports.

Jand42 avatar Sep 29 '17 14:09 Jand42

TS output now runs without regression from master.

Next steps:

  • Alternate modes for packager to write .ts files using triple-slash references (good for Sitelets compatibility) and modules (good for bundles and exposing WebSharper-generated code for larger TS projects).
  • Write code files only on unpacking, using config. Do not overwrite output files when the .dll has an older timestamp than already existing .ts and config is the same. Doing https://github.com/intellifactory/websharper/issues/798 first can help, then there is also a timestamp on the .config file that unpacker can check.

Jand42 avatar Oct 03 '17 14:10 Jand42

Currently adding signatures. Challenges:

  • Interfaces has to be exposed.
  • Proper use of generics. In first implementation, translated type parameters will always be named T0, T1, ... and an extra rule in name resolver makes sure that no other identifiers take this form.
  • Base classes and implemented interfaces has to be stored in metadata with generics (currently only type definition is stored for base class and no list of implemented interfaces, only the implementations itself)
  • Type parameter constraints need to be translated, for example 'T when 'T :> System.IEquatable<'T> to <T0 extends WebSharper.IEquatable<T0>>
  • Internal casts has to be translated. For example while the As function was entirely erased in .js output, (having Inline "$x"), now it will need support for casting (Inline "$x as T0"). It would be better that while translating inlined casts, unnecessary ones (TS target type are the same or compatible) are removed.
  • Fix typing of static module values. Containing field should be untyped as is private and should be not exported, access functions should have return type annotations.

Jand42 avatar Oct 09 '17 07:10 Jand42