Skip to content
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,14 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](http://keepachangelog.com/)
and this project adheres to [Semantic Versioning](http://semver.org/).

## [Unreleased]

### Added
- ElixirScript now has an FFI layer for interoperability with JavaScript. For more details, see documentation at `ElixirScript.FFI`

### Changed
- Compiler has been completely rewritten. ElixirScript now requires Erlang 20+ and Elixir 1.5+

## [0.28.0] - 2017-06-11

### Added
Expand Down
58 changes: 19 additions & 39 deletions JavascriptInterop.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,55 +13,35 @@ JS.debugger()

# Getting the type of a value
JS.typeof(my_value)

# Creating a new JavaScript Map
map = JS.new(JS.Map, [])
```

### Accessing Global Objects, Functions, and Properties

In order to interact with JavaScript things in the global scope, append "JS" to them. The global scope corresponds to whatever the global object is in the JavaScript environment you are in. For example, in a browser this would be `window` or `self`:
### Foreign Function Interface

```elixir
# Calling alert
JS.alert("hello")
ElixirScript calls JavaScript modules through a Foreign Function Interface (FFI). A foreign module is defined by creating a new module and adding `use ElixirScript.FFI` to it.

# Calling a method on Object
JS.Object.keys(my_object)
Here is an example of a foreign module for a JSON module

# Creating a new JavaScript Date
JS.new(JS.Date, [])
```elixir
defmodule MyApp.JSON do
use ElixirScript.FFI

# Getting the outer width
JS.outerWidth
foreign stringify(map)
foreign parse(string)
end
```

### JavaScript modules

ElixirScript can use JavaScript modules from the supported modules systems.
In order to do so, you must tell ElixirScript about them upfront.
Foreign modules map to JavaScript files that export functions defined with the `foreign` macro.
ElixirScript expects JavaScript modules to be in the `priv/elixir_script` directory.
These modules are copied to the output directory upon compilation.

If using ElixirScript in a mix project, you can do so inside of the ElixirScript configuration keyword list
For our example, a JavaScript file must be placed at `priv/elixir_script/my_app/json.js`.

```elixir
def project do
[
app: :my_project,
elixir_script: [
format: :es,
js_modules: [
{React, "react"},
{ReactDOM, "react-dom"}
]
]
]
end
```

Interacting with these modules works the same as interacting with an ElixirScript module

```elixir
React.createElement(...)
It looks like this
```javascript
export default {
stringify: JSON.stringify,
parse: JSON.parse
}
```

## JavaScript Calling ElixirScript
Expand Down
18 changes: 4 additions & 14 deletions lib/elixir_script/beam.ex
Original file line number Diff line number Diff line change
Expand Up @@ -10,21 +10,11 @@ defmodule ElixirScript.Beam do
@spec debug_info(atom) :: {:ok | :error, map | binary}
def debug_info(module)

#Replacing String module with our ElixirScript's version
def debug_info(String) do
case debug_info(ElixirScript.String) do
#Replace some modules with ElixirScript versions
def debug_info(module) when module in [String, Agent] do
case debug_info(Module.concat(ElixirScript, module)) do
{:ok, info} ->
{:ok, Map.put(info, :module, String)}
e ->
e
end
end

#Replacing Agent module with our ElixirScript's version
def debug_info(Agent) do
case debug_info(ElixirScript.Agent) do
{:ok, info} ->
{:ok, Map.put(info, :module, Agent)}
{:ok, Map.put(info, :module, module)}
e ->
e
end
Expand Down
33 changes: 2 additions & 31 deletions lib/elixir_script/cli.ex
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,7 @@ defmodule ElixirScript.CLI do
help: :boolean,
version: :boolean,
watch: :boolean,
format: :string,
js_module: [:string, :keep]
format: :string
]

@aliases [
Expand Down Expand Up @@ -43,8 +42,6 @@ defmodule ElixirScript.CLI do
<module> the entry module of your application

options:
--js-module [<identifer>:<path>] A js module used in your code. ex: React:react
Multiple can be defined
-f --format [format] module format of output. options: es (default), common, umd
-o --output [path] places output at the given path.
Can be a directory or filename.
Expand Down Expand Up @@ -72,13 +69,9 @@ defmodule ElixirScript.CLI do
def do_process(input, options) do
{watch, options} = Keyword.pop(options, :watch, false)

js_modules = Keyword.get_values(options, :js_module)
|> build_js_modules

compile_opts = [
output: Keyword.get(options, :output, :stdout),
format: String.to_atom(Keyword.get(options, :format, "es")),
js_modules: js_modules,
format: String.to_atom(Keyword.get(options, :format, "es"))
]

input = handle_input(input)
Expand Down Expand Up @@ -106,26 +99,4 @@ defmodule ElixirScript.CLI do
|> List.flatten
|> Enum.map(fn(x) -> Module.concat([x]) end)
end

defp build_js_modules(values) do
values
|> Enum.map(fn x ->
[identifier, path] = String.split(x, ":", trim: true)
{ format_identifier(identifier), format_path(path) }
end)
end

defp format_identifier(id) do
id
|> String.split(".")
|> Module.concat
end


defp format_path(path) do
path
|> String.replace("\"", "")
|> String.replace("`", "")
|> String.replace("'", "")
end
end
1 change: 0 additions & 1 deletion lib/elixir_script/compiler.ex
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,6 @@ defmodule ElixirScript.Compiler do
default_options = Map.new
|> Map.put(:output, Keyword.get(opts, :output))
|> Map.put(:format, Keyword.get(opts, :format, :es))
|> Map.put(:js_modules, Keyword.get(opts, :js_modules, []))
|> Map.put(:entry_modules, entry_modules)

options = default_options
Expand Down
60 changes: 60 additions & 0 deletions lib/elixir_script/ffi.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
defmodule ElixirScript.FFI do
@moduledoc """
The foreign function interface for interacting with JavaScript

To define a foreign module, make a new module and add `use ElixirScript.FFI`. to it
To define foreign functions, use the `foreign` macro.

Here is an example of a foreign module for a JSON module

```elixir
defmodule MyApp.JSON do
use ElixirScript.FFI

foreign stringify(map)
foreign parse(string)
end
```

Foreign modules map to JavaScript files that export functions defined with the `foreign` macro.
ElixirScript expects JavaScript modules to be in the `priv/elixir_script` directory.
These modules are copied to the output directory upon compilation.

For our example, a JavaScript file must be placed at `priv/elixir_script/my_app/json.js`.

It looks like this
```javascript
export default {
stringify: JSON.stringify,
parse: JSON.parse
}
```
"""

defmacro __using__(opts) do
quote do
import ElixirScript.FFI
Module.register_attribute __MODULE__, :__foreign_info__, persist: true
@__foreign_info__ %{
path: Macro.underscore(__MODULE__),
name: Enum.join(Module.split(__MODULE__), "_"),
global: unquote(Keyword.get(opts, :global, false))
}
end
end

@doc """
Defines a JavaScript function to be called from Elixir modules

To define a foreign function, pass the name and arguments to `foreign`

```elixir
foreign my_js_function(arg1, arg2, arg3)
```
"""
defmacro foreign({name, _, args}) do
quote do
def unquote(name)(unquote_splicing(args)), do: nil
end
end
end
20 changes: 8 additions & 12 deletions lib/elixir_script/lib/agent.ex
Original file line number Diff line number Diff line change
Expand Up @@ -2,51 +2,47 @@ defmodule ElixirScript.Agent do
@moduledoc false

def start(fun, options \\ []) do
pid = JS.new JS.Bootstrap.Core.PID, []

name = if Keyword.has_key?(options, :name) do
Keyword.get(options, :name)
else
nil
end

ElixirScript.Store.create(pid, fun.(), name)
pid = Bootstrap.Core.Store.create(fun.(), name)
{ :ok, pid }
end

def start_link(fun, options \\ []) do
pid = JS.new JS.Bootstrap.Core.PID, []

name = if Keyword.has_key?(options, :name) do
Keyword.get(options, :name)
else
nil
end

ElixirScript.Store.create(pid, fun.(), name)
pid = Bootstrap.Core.Store.create(fun.(), name)
{ :ok, pid }
end

def stop(agent) do
ElixirScript.Store.remove(agent)
Bootstrap.Core.Store.remove(agent)
:ok
end

def update(agent, fun) do
current_state = ElixirScript.Store.read(agent)
ElixirScript.Store.update(agent, fun.(current_state))
current_state = Bootstrap.Core.Store.read(agent)
Bootstrap.Core.Store.update(agent, fun.(current_state))
:ok
end

def get(agent, fun) do
current_state = ElixirScript.Store.read(agent)
current_state = Bootstrap.Core.Store.read(agent)
fun.(current_state)
end

def get_and_update(agent, fun) do
current_state = ElixirScript.Store.read(agent)
current_state = Bootstrap.Core.Store.read(agent)
{val, new_state} = fun.(current_state)
ElixirScript.Store.update(agent, new_state)
Bootstrap.Core.Store.update(agent, new_state)
val
end

Expand Down
45 changes: 7 additions & 38 deletions lib/elixir_script/lib/store.ex
Original file line number Diff line number Diff line change
@@ -1,43 +1,12 @@
defmodule ElixirScript.Store do
defmodule Bootstrap.Core.Store do
@moduledoc false
use ElixirScript.FFI, global: true

defp get_key(key) do
real_key = case JS.__elixirscript_names__.has(key) do
true ->
JS.__elixirscript_names__.get(key)
false ->
key
end
foreign create(value, name \\ nil)

case JS.__elixirscript_store__.has(real_key) do
true ->
real_key
false ->
JS.throw JS.new(JS.Error, ["Key Not Found"])
end
end

def create(key, value, name \\ nil) do
if name != nil do
JS.__elixirscript_names__.set(name, key)
end

JS.__elixirscript_store__.set(key, value)
end

def update(key, value) do
real_key = get_key(key)
JS.__elixirscript_store__.set(real_key, value)
end

def read(key) do
real_key = get_key(key)
JS.__elixirscript_store__.get(real_key)
end

def remove(key) do
real_key = get_key(key)
JS.__elixirscript_store__.delete(real_key)
end
foreign update(key, value)

foreign read(key)

foreign remove(key)
end
6 changes: 3 additions & 3 deletions lib/elixir_script/lib/string.ex
Original file line number Diff line number Diff line change
Expand Up @@ -15,15 +15,15 @@ defmodule ElixirScript.String do
end

def to_float(str) do
JS.parseFloat(str)
:erlang.binary_to_float(str)
end

def to_integer(str) do
JS.parseInt(str, 10)
:erlang.binary_to_integer(str)
end

def to_integer(str, base) do
JS.parseInt(str, base)
:erlang.binary_to_integer(str, base)
end

def upcase(str) do
Expand Down
6 changes: 3 additions & 3 deletions lib/elixir_script/module_systems/common.ex
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ defmodule ElixirScript.ModuleSystems.Common do

imports = js_imports
|> Enum.map(fn
{module, path} -> import_module(module, path)
{_module, name, path} -> import_module(name, path)
end)

imports = Enum.uniq(imports ++ module_imports)
Expand All @@ -16,8 +16,8 @@ defmodule ElixirScript.ModuleSystems.Common do
imports ++ body ++ export
end

defp import_module(module_name, from) do
js_module_name = ElixirScript.Translate.Identifier.make_namespace_members(module_name)
defp import_module(name, from) do
js_module_name = JS.identifier(name)
do_import_module(js_module_name, from)
end

Expand Down
Loading