Skip to content

Commit

Permalink
Fix fable-compiler#859: Add dynamic import and import for side effects
Browse files Browse the repository at this point in the history
  • Loading branch information
alfonsogarciacaro committed May 6, 2017
1 parent 290a7b7 commit 7217e20
Show file tree
Hide file tree
Showing 6 changed files with 200 additions and 171 deletions.
47 changes: 30 additions & 17 deletions src/dotnet/Fable.Compiler/Fable2Babel.fs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ type ReturnStrategy =
type Import = {
path: string
selector: string
localIdent: string
localIdent: string option
internalFile: string option
}

Expand Down Expand Up @@ -1106,7 +1106,6 @@ module Util =
declarePlugins
member bcom.GetRootModule(file) =
state.GetRootModule(file)
// TODO: Create a cache to optimize imports
member bcom.GetImportExpr ctx selector path kind =
let sanitizeSelector selector =
if selector = "*"
Expand All @@ -1118,15 +1117,19 @@ module Util =
else Naming.replaceIdentForbiddenChars selector
let getLocalIdent (ctx: Context) (selector: string) =
match selector with
| "*" | "default" | "" ->
| "" -> None
| "*" | "default" ->
let x = path.TrimEnd('/')
x.Substring(x.LastIndexOf '/' + 1)
| _ -> selector
|> Naming.sanitizeIdent (fun s ->
x.Substring(x.LastIndexOf '/' + 1) |> Some
| selector -> Some selector
|> Option.map (Naming.sanitizeIdent (fun s ->
ctx.file.UsedVarNames.Contains s
|| (imports.Values |> Seq.exists (fun i -> i.localIdent = s)))
|| (imports.Values |> Seq.exists (fun i -> i.localIdent = Some s))))
match imports.TryGetValue(path + "::" + selector) with
| true, i -> upcast Identifier(i.localIdent)
| true, i ->
match i.localIdent with
| Some localIdent -> upcast Identifier(localIdent)
| None -> upcast NullLiteral ()
| false, _ ->
let localId = getLocalIdent ctx selector
let i = {
Expand All @@ -1147,7 +1150,9 @@ module Util =
|> fun path -> path.TrimEnd('/')
}
imports.Add(path + "::" + selector, i)
upcast Identifier (localId)
match localId with
| Some localId -> upcast Identifier(localId)
| None -> upcast NullLiteral ()
member bcom.GetAllImports () = upcast imports.Values
member bcom.TransformExpr ctx e = transformExpr bcom ctx e
member bcom.TransformStatement ctx e = transformStatement bcom ctx e
Expand Down Expand Up @@ -1206,24 +1211,32 @@ module Compiler =
// Add imports
com.GetAllImports()
|> Seq.mapi (fun ident import ->
let localId = Identifier(import.localIdent)
let specifier =
match import.selector with
| "*" -> ImportNamespaceSpecifier(localId) |> U3.Case3
| "default" | "" -> ImportDefaultSpecifier(localId) |> U3.Case2
| memb -> ImportSpecifier(localId, Identifier memb) |> U3.Case1
import.localIdent
|> Option.map (fun localId ->
let localId = Identifier(localId)
match import.selector with
| "*" -> ImportNamespaceSpecifier(localId) |> U3.Case3
| "default" | "" -> ImportDefaultSpecifier(localId) |> U3.Case2
| memb -> ImportSpecifier(localId, Identifier memb) |> U3.Case1)
import.path, specifier)
|> Seq.groupBy (fun (path, _) -> path)
|> Seq.collect (fun (path, specifiers) ->
let mems, defs, alls =
(([], [], []), Seq.map snd specifiers)
(([], [], []), Seq.choose snd specifiers)
||> Seq.fold (fun (mems, defs, alls) x ->
match x.``type`` with
| "ImportNamespaceSpecifier" -> mems, defs, x::alls
| "ImportDefaultSpecifier" -> mems, x::defs, alls
| _ -> x::mems, defs, alls)
[mems; defs; alls]
|> Seq.choose (function
// There seem to be errors if we mix member, default and namespace imports
// so we must issue an import statement for each kind
match [mems; defs; alls] with
| [[];[];[]] ->
// No specifiers, so this is just a import for side effects
[ImportDeclaration([], StringLiteral path) :> ModuleDeclaration |> U2.Case2]
| specifiers ->
specifiers |> List.choose (function
| [] -> None
| specifiers ->
ImportDeclaration(specifiers, StringLiteral path)
Expand Down
6 changes: 5 additions & 1 deletion src/dotnet/Fable.Compiler/Replacements.fs
Original file line number Diff line number Diff line change
Expand Up @@ -577,6 +577,9 @@ module AstPass =
| expr when expr.Type = Fable.Unit -> []
| expr -> [expr]
match i.methodName with
| "importDynamic" ->
GlobalCall ("import", None, false, i.args)
|> makeCall i.range i.returnType |> Some
| Naming.StartsWith "import" _ ->
let fail() =
sprintf "%s.%s only accepts literal strings" i.ownerFullName i.methodName
Expand All @@ -589,7 +592,8 @@ module AstPass =
| _ -> fail(); "*", [makeStrConst "unknown"]
| "importMember" -> Naming.placeholder, i.args
| "importDefault" -> "default", i.args
| _ -> "*", i.args // importAllFrom
| "importSideEffects" -> "", i.args
| _ -> "*", i.args // importAll
let path =
match args with
| [Fable.Value(Fable.StringConst path)] -> path
Expand Down
163 changes: 163 additions & 0 deletions src/dotnet/Fable.Core/Fable.Core.JsInterop.fs
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
module Fable.Core.JsInterop

open System
open Fable.Core
open Fable.Import

/// Has same effect as `unbox` (dynamic casting erased in compiled JS code).
/// The casted type can be defined on the call site: `!!myObj?bar(5): float`
let (!!) x: 'T = jsNative

// Implicit cast for erased types
let inline (!^) (x:^t1) : ^t2 = ((^t1 or ^t2) : (static member op_ErasedCast : ^t1 -> ^t2) x)

/// Dynamically access a property of an arbitrary object.
/// `myObj?propA` in JS becomes `myObj.propA`
/// `myObj?(propA)` in JS becomes `myObj[propA]`
let (?) (o: obj) (prop: obj): Applicable = jsNative

/// Dynamically assign a value to a property of an arbitrary object.
/// `myObj?propA <- 5` in JS becomes `myObj.propA = 5`
/// `myObj?(propA) <- 5` in JS becomes `myObj[propA] = 5`
let (?<-) (o: obj) (prop: obj) (v: obj): unit = jsNative

/// Destructure and apply a tuple to an arbitrary value.
/// E.g. `myFn $ (arg1, arg2)` in JS becomes `myFn(arg1, arg2)`
let ($) (callee: obj) (args: obj): obj = jsNative

/// Upcast the right operand to obj and create a key-value tuple.
/// Mostly convenient when used with `createObj`.
/// E.g. `createObj [ "a" ==> 5 ]` in JS becomes `{ a: 5 }`
let (==>) (key: string) (v: obj): string*obj = jsNative

/// Destructure and apply a tuple to an arbitrary value with `new` keyword.
/// E.g. `createNew myCons (arg1, arg2)` in JS becomes `new myCons(arg1, arg2)`
let createNew (o: obj) (args: obj): obj = jsNative

/// Create a literal JS object from a collection of key-value tuples.
/// E.g. `createObj [ "a" ==> 5 ]` in JS becomes `{ a: 5 }`
let createObj (fields: #seq<string*obj>): obj = jsNative

/// Create a literal JS object from a collection of union constructors.
/// E.g. `keyValueList [ MyUnion 4 ]` in JS becomes `{ myUnion: 4 }`
let keyValueList (caseRule: CaseRules) (li: 'T list): obj = jsNative

/// Create an empty JS object: {}
let createEmpty<'T> : 'T = jsNative

/// Internally used by Fable, not intended for general use
let applySpread (callee: obj) (args: obj) : 'T = jsNative

/// Works like `ImportAttribute` (same semantics as ES6 imports).
/// You can use "*" or "default" selectors.
let import<'T> (selector: string) (path: string):'T = jsNative

/// F#: let myMember = importMember<string> "myModule"
/// JS: import { myMember } from "myModule"
/// Note the import must be immediately assigned to a value in a let binding
let importMember<'T> (path: string):'T = jsNative

/// F#: let defaultMember = importDefault<unit->obj> "myModule"
/// JS: import defaultMember from "myModule"
let importDefault<'T> (path: string):'T = jsNative

/// F#: let myLib = importAll<obj> "myLib"
/// JS: import * as myLib from "myLib"
let importAll<'T> (path: string):'T = jsNative

/// Imports a file only for its side effects
let importSideEffects (path: string): unit = jsNative

/// Imports a file dynamically at runtime
let importDynamic<'T> (path: string): JS.Promise<'T> = jsNative

/// Convert F# unions, records and classes into plain JS objects
/// When designing APIs, consider also using a Pojo record or union
let toPlainJsObj (o: 'T): obj = jsNative

/// Converts an F# object into a plain JS object (POJO)
/// This is only intended if you're using a custom serialization method
/// and will produce the same object structure that `toJson` encodes
/// NOTE: `deflate` is currently NOT recursive
let deflate(o: 'T): obj = jsNative

/// Serialize F# objects to JSON
let toJson(o: 'T): string = jsNative

/// Instantiate F# objects from JSON
let [<PassGenerics>] ofJson<'T>(json: string): 'T = jsNative

/// Serialize F# objects to JSON adding $type info
let toJsonWithTypeInfo(o: 'T): string = jsNative

/// Instantiate F# objects from JSON containing $type info
let [<PassGenerics>] ofJsonWithTypeInfo<'T>(json: string): 'T = jsNative

/// Converts a plain JS object (POJO) to an instance of the specified type.
/// This is only intended if you're using a custom serialization method
/// (that must produce same objects as `toJson`) instead of `ofJson`.
/// NOTE: `inflate` is currently NOT recursive
let [<PassGenerics>] inflate<'T>(pojo: obj): 'T = jsNative

/// Reads the name of an identifier, a property or a type
let nameof(expr: obj): string = jsNative

/// Reads the name of a property or a type from the lambda body
let nameofLambda(f: 'a->'b): string = jsNative

/// Compiles to JS `this` keyword.
///
/// ## Sample
/// let fn = JsFunc2(fun x y -> jsThis?add(x, y))
let [<Emit("this")>] jsThis<'T> : 'T = jsNative

/// Use it when importing a constructor from a JS library.
type [<AllowNullLiteral>] JsConstructor =
[<Emit("new $0($1...)")>]
abstract Create: [<ParamArray>]args: obj[] -> obj

/// Use it when importing a constructor from a JS library.
type [<AllowNullLiteral>] JsConstructor<'Out> =
[<Emit("new $0()")>]
abstract Create: unit->'Out

/// Use it when importing a constructor from a JS library.
type [<AllowNullLiteral>] JsConstructor<'Arg1,'Out> =
[<Emit("new $0($1...)")>]
abstract Create: 'Arg1->'Out

/// Use it when importing a constructor from a JS library.
type [<AllowNullLiteral>] JsConstructor<'Arg1,'Arg2,'Out> =
[<Emit("new $0($1...)")>]
abstract Create: 'Arg1*'Arg2->'Out

/// Use it when importing a constructor from a JS library.
type [<AllowNullLiteral>] JsConstructor<'Arg1,'Arg2,'Arg3,'Out> =
[<Emit("new $0($1...)")>]
abstract Create: 'Arg1*'Arg2*'Arg3->'Out

/// Use it when importing a constructor from a JS library.
type [<AllowNullLiteral>] JsConstructor<'Arg1,'Arg2,'Arg3,'Arg4,'Out> =
[<Emit("new $0($1...)")>]
abstract Create: 'Arg1*'Arg2*'Arg3*'Arg4->'Out

/// Use it when importing a constructor from a JS library.
type [<AllowNullLiteral>] JsConstructor<'Arg1,'Arg2,'Arg3,'Arg4,'Arg5,'Out> =
[<Emit("new $0($1...)")>]
abstract Create: 'Arg1*'Arg2*'Arg3*'Arg4*'Arg5->'Out

/// Use it when importing a constructor from a JS library.
type [<AllowNullLiteral>] JsConstructor<'Arg1,'Arg2,'Arg3,'Arg4,'Arg5,'Arg6,'Out> =
[<Emit("new $0($1...)")>]
abstract Create: 'Arg1*'Arg2*'Arg3*'Arg4*'Arg5*'Arg6->'Out

/// Use it to cast dynamic functions coming from JS. If you know the argument
/// and return types, use `System.Func<>` instead. If you need a constructor
/// (must be applied with `new` keyword), use `JsConstructor`.
///
/// ## Sample
/// let f: JsFunc = import "myFunction" "./myLib"
/// f.Invoke(5, "bar")
type [<AllowNullLiteral>] JsFunc =
[<Emit("$0($1...)")>]
abstract Invoke: [<ParamArray>]args:obj[]->obj
Loading

0 comments on commit 7217e20

Please sign in to comment.