diff --git a/CHANGELOG.md b/CHANGELOG.md index b484b9e3..e3f5598c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/JavascriptInterop.md b/JavascriptInterop.md index db0c0e45..de8bf745 100644 --- a/JavascriptInterop.md +++ b/JavascriptInterop.md @@ -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 diff --git a/lib/elixir_script/beam.ex b/lib/elixir_script/beam.ex index d8cb7213..e850d837 100644 --- a/lib/elixir_script/beam.ex +++ b/lib/elixir_script/beam.ex @@ -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 diff --git a/lib/elixir_script/cli.ex b/lib/elixir_script/cli.ex index fd27b355..79ffcd80 100644 --- a/lib/elixir_script/cli.ex +++ b/lib/elixir_script/cli.ex @@ -8,8 +8,7 @@ defmodule ElixirScript.CLI do help: :boolean, version: :boolean, watch: :boolean, - format: :string, - js_module: [:string, :keep] + format: :string ] @aliases [ @@ -43,8 +42,6 @@ defmodule ElixirScript.CLI do the entry module of your application options: - --js-module [:] 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. @@ -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) @@ -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 diff --git a/lib/elixir_script/compiler.ex b/lib/elixir_script/compiler.ex index 79fe7238..1c2d9c85 100644 --- a/lib/elixir_script/compiler.ex +++ b/lib/elixir_script/compiler.ex @@ -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 diff --git a/lib/elixir_script/ffi.ex b/lib/elixir_script/ffi.ex new file mode 100644 index 00000000..abffc29b --- /dev/null +++ b/lib/elixir_script/ffi.ex @@ -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 \ No newline at end of file diff --git a/lib/elixir_script/lib/agent.ex b/lib/elixir_script/lib/agent.ex index 8ae7b1eb..cb444941 100644 --- a/lib/elixir_script/lib/agent.ex +++ b/lib/elixir_script/lib/agent.ex @@ -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 diff --git a/lib/elixir_script/lib/store.ex b/lib/elixir_script/lib/store.ex index ad5b777c..14447d9d 100644 --- a/lib/elixir_script/lib/store.ex +++ b/lib/elixir_script/lib/store.ex @@ -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 \ No newline at end of file diff --git a/lib/elixir_script/lib/string.ex b/lib/elixir_script/lib/string.ex index 1ad3d9bf..d692f815 100644 --- a/lib/elixir_script/lib/string.ex +++ b/lib/elixir_script/lib/string.ex @@ -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 diff --git a/lib/elixir_script/module_systems/common.ex b/lib/elixir_script/module_systems/common.ex index 3236f92b..2f4da76b 100644 --- a/lib/elixir_script/module_systems/common.ex +++ b/lib/elixir_script/module_systems/common.ex @@ -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) @@ -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 diff --git a/lib/elixir_script/module_systems/es.ex b/lib/elixir_script/module_systems/es.ex index 3e92049c..72cd042a 100644 --- a/lib/elixir_script/module_systems/es.ex +++ b/lib/elixir_script/module_systems/es.ex @@ -7,8 +7,7 @@ defmodule ElixirScript.ModuleSystems.ES do imports = js_imports |> Enum.map(fn - {module, path} -> import_module(module, path) - {module, path, default: false} -> import_namespace_module(module, path) + {_module, name, path} -> import_module(name, path) end) imports = Enum.uniq(imports ++ module_imports) @@ -17,19 +16,8 @@ defmodule ElixirScript.ModuleSystems.ES do imports ++ body ++ export end - defp import_namespace_module(module_name, from) do - js_module_name = ElixirScript.Translate.Identifier.make_namespace_members(module_name) - - import_specifier = JS.import_namespace_specifier( - js_module_name, - js_module_name - ) - - do_import_module([import_specifier], from) - end - defp import_module(import_name, from) do - js_module_name = ElixirScript.Translate.Identifier.make_namespace_members(import_name) + js_module_name = JS.identifier(import_name) import_specifier = JS.import_default_specifier( js_module_name diff --git a/lib/elixir_script/module_systems/umd.ex b/lib/elixir_script/module_systems/umd.ex index e9894c24..fcd30795 100644 --- a/lib/elixir_script/module_systems/umd.ex +++ b/lib/elixir_script/module_systems/umd.ex @@ -9,7 +9,7 @@ defmodule ElixirScript.ModuleSystems.UMD 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) @@ -19,7 +19,7 @@ defmodule ElixirScript.ModuleSystems.UMD do end defp import_module(module_name, from) do - js_module_name = ElixirScript.Translate.Identifier.make_namespace_members(module_name) + js_module_name = JS.identifier(module_name) {js_module_name, JS.literal(from)} end diff --git a/lib/elixir_script/passes/find_used_modules.ex b/lib/elixir_script/passes/find_used_modules.ex index ccd94136..e352a935 100644 --- a/lib/elixir_script/passes/find_used_modules.ex +++ b/lib/elixir_script/passes/find_used_modules.ex @@ -25,6 +25,16 @@ defmodule ElixirScript.FindUsedModules do end end + defp walk_module(module, %{attributes: [__foreign_info__: %{path: path, name: name, global: global}]} = info, pid) do + path = if global, do: nil, else: path + name = if global, do: module, else: name + + ModuleState.put_javascript_module(pid, module, name, path) + ModuleState.put_module(pid, module, info) + + nil + end + defp walk_module(module, info, pid) do %{ attributes: _attrs, @@ -232,7 +242,7 @@ defmodule ElixirScript.FindUsedModules do walk({function, [], params}, state) end - defp walk({{:., _, [module, function]} = ast, _, params}, state) do + defp walk({{:., _, [_module, _function]} = ast, _, params}, state) do walk(ast, state) walk(params, state) end @@ -243,8 +253,6 @@ defmodule ElixirScript.FindUsedModules do defp walk({:., _, [module, function]}, state) do cond do - ElixirScript.Translate.Module.is_js_module(module, state) -> - nil ElixirScript.Translate.Module.is_elixir_module(module) -> if ModuleState.get_module(state.pid, module) == nil do execute(module, state.pid) diff --git a/lib/elixir_script/passes/output.ex b/lib/elixir_script/passes/output.ex index 14872ba0..7b6575b1 100644 --- a/lib/elixir_script/passes/output.ex +++ b/lib/elixir_script/passes/output.ex @@ -19,13 +19,23 @@ defmodule ElixirScript.Output do opts = ModuleState.get_compiler_opts(pid) - bundle(modules, opts) - |> output(Map.get(opts, :output)) + js_modules = ModuleState.js_modules(pid) + |> Enum.filter(fn + {_module, _name, nil} -> false + _ -> true + end) + |> Enum.map(fn + {module, name, path} -> + {module, name, Path.join(".", path)} + end) + + bundle(modules, opts, js_modules) + |> output(Map.get(opts, :output), js_modules) end - defp bundle(modules, opts) do + defp bundle(modules, opts, js_modules) do modules - |> ElixirScript.Output.JSModule.compile(opts) + |> ElixirScript.Output.JSModule.compile(opts, js_modules) |> List.wrap |> Builder.program |> prepare_js_ast @@ -55,21 +65,27 @@ defmodule ElixirScript.Output do end end - defp output(code, nil) do + defp output(code, nil, _) do code end - defp output(code, :stdout) do + defp output(code, :stdout, _) do IO.puts(code) end - defp output(code, path) do + defp output(code, path, js_modules) do file_name = get_output_file_name(path) if !File.exists?(Path.dirname(file_name)) do File.mkdir_p!(Path.dirname(file_name)) end + apps = get_app_names() + output_dir = Path.dirname(file_name) + Enum.each(js_modules, fn({_, _, path}) -> + copy_javascript_module(apps, output_dir, path) + end) + File.write!(file_name, code) end @@ -81,4 +97,27 @@ defmodule ElixirScript.Output do Path.join([path, @generated_name]) end end + + defp get_app_names() do + Mix.Project.config()[:app] + deps = Mix.Project.deps_paths() + |> Map.keys + + [Mix.Project.config()[:app]] ++ deps + end + + defp copy_javascript_module(apps, output_dir, js_module_path) do + Enum.each(apps, fn(app) -> + full_path = Path.join([:code.priv_dir(app), "elixir_script", js_module_path]) <> ".js" + + if File.exists?(full_path) do + js_output_path = Path.join(output_dir, js_module_path) <> ".js" + if !File.exists?(Path.dirname(js_output_path)) do + File.mkdir_p!(Path.dirname(js_output_path)) + end + + File.cp(full_path, js_output_path) + end + end) + end end \ No newline at end of file diff --git a/lib/elixir_script/passes/output/js_module.ex b/lib/elixir_script/passes/output/js_module.ex index 75eb960f..a5cea45f 100644 --- a/lib/elixir_script/passes/output/js_module.ex +++ b/lib/elixir_script/passes/output/js_module.ex @@ -3,7 +3,7 @@ defmodule ElixirScript.Output.JSModule do alias ESTree.Tools.Builder, as: J - def compile(body, opts) do + def compile(body, opts, js_modules) do declarator = J.variable_declarator( J.identifier("Elixir"), J.object_expression([]) @@ -11,15 +11,10 @@ defmodule ElixirScript.Output.JSModule do elixir = J.variable_declaration([declarator], :const) - table_additions = Enum.map(opts.js_modules, fn - {module, _} -> add_import_to_table(module) - {module, _, _} -> add_import_to_table(module) - end) - ast = opts.module_formatter.build( [], - opts.js_modules, - [elixir, create_atom_table(), start(), load()] ++ table_additions ++ body, + js_modules, + [elixir, create_atom_table(), start(), load()] ++ body, J.identifier("Elixir") ) @@ -99,26 +94,4 @@ defmodule ElixirScript.Output.JSModule do ) end - defp add_import_to_table(module_name) do - ref = ElixirScript.Translate.Identifier.make_namespace_members(module_name) - J.assignment_expression( - :=, - J.member_expression( - J.member_expression( - J.identifier("Elixir"), - J.identifier("__table__") - ), - J.call_expression( - J.member_expression( - J.identifier("Symbol"), - J.identifier("for") - ), - [J.literal(ref.name)] - ), - true - ), - ref - ) - end - end \ No newline at end of file diff --git a/lib/elixir_script/passes/translate/forms/remote.ex b/lib/elixir_script/passes/translate/forms/remote.ex index 66740877..c09a065d 100644 --- a/lib/elixir_script/passes/translate/forms/remote.ex +++ b/lib/elixir_script/passes/translate/forms/remote.ex @@ -3,6 +3,7 @@ defmodule ElixirScript.Translate.Forms.Remote do alias ESTree.Tools.Builder, as: J alias ElixirScript.Translate.{Form, Identifier} + alias ElixirScript.State, as: ModuleState @erlang_modules [ :erlang, @@ -125,11 +126,7 @@ defmodule ElixirScript.Translate.Forms.Remote do end def compile({:., _, [module, function]}, state) do - function_name = if ElixirScript.Translate.Module.is_js_module(module, state) do - ElixirScript.Translate.Identifier.make_extern_function_name(function) - else - ElixirScript.Translate.Identifier.make_function_name(function) - end + function_name = ElixirScript.Translate.Identifier.make_function_name(function) ast = J.member_expression( process_module_name(module, state), @@ -166,20 +163,13 @@ defmodule ElixirScript.Translate.Forms.Remote do Form.compile!(module, state) end - defp process_js_module_name(module, _) do - case Module.split(module) do - ["JS"] -> - J.member_expression( - J.member_expression( - J.identifier("Bootstrap"), - J.identifier("Core") - ), - J.identifier("global") - ) - ["JS" | rest] -> - Identifier.make_namespace_members(rest) - x -> - Identifier.make_namespace_members(x) + defp process_js_module_name(module, state) do + case ModuleState.get_js_module_name(state.pid, module) do + name when is_atom(name) -> + members = Module.split(module) + Identifier.make_namespace_members(members) + name -> + J.identifier(name) end end diff --git a/lib/elixir_script/passes/translate/function.ex b/lib/elixir_script/passes/translate/function.ex index 148fe2ad..ecd353cf 100644 --- a/lib/elixir_script/passes/translate/function.ex +++ b/lib/elixir_script/passes/translate/function.ex @@ -5,8 +5,7 @@ defmodule ElixirScript.Translate.Function do @moduledoc """ Translates the given Elixir function AST into the - equivalent JavaScript AST. Function names are - + equivalent JavaScript AST. """ def patterns_ast() do diff --git a/lib/elixir_script/passes/translate/identifier.ex b/lib/elixir_script/passes/translate/identifier.ex index e91c02d2..a619f1fa 100644 --- a/lib/elixir_script/passes/translate/identifier.ex +++ b/lib/elixir_script/passes/translate/identifier.ex @@ -75,8 +75,4 @@ defmodule ElixirScript.Translate.Identifier do J.identifier(name) end - def make_extern_function_name(name) do - J.identifier("#{name}") - end - end diff --git a/lib/elixir_script/passes/translate/module.ex b/lib/elixir_script/passes/translate/module.ex index 54ca1c7e..7dde8c3e 100644 --- a/lib/elixir_script/passes/translate/module.ex +++ b/lib/elixir_script/passes/translate/module.ex @@ -12,6 +12,15 @@ defmodule ElixirScript.Translate.Module do ElixirScript.Translate.Protocol.compile(module, info, pid) end + def compile(module, %{attributes: [__foreign_info__: %{path: path, name: name, global: global}]}, pid) do + path = if global, do: nil, else: path + name = if global, do: module, else: name + + ModuleState.put_javascript_module(pid, module, name, path) + + nil + end + def compile(module, info, pid) do %{ attributes: attrs, @@ -133,12 +142,10 @@ defmodule ElixirScript.Translate.Module do """ def is_js_module(module, state) do cond do - module in ModuleState.get_javascript_modules(state.pid) -> + module in ModuleState.list_javascript_modules(state.pid) -> true module === Elixir -> false - is_elixir_module(module) and hd(Module.split(module)) == "JS" -> - true true -> false end diff --git a/lib/elixir_script/state.ex b/lib/elixir_script/state.ex index f7e82449..50ff4cb7 100644 --- a/lib/elixir_script/state.ex +++ b/lib/elixir_script/state.ex @@ -8,7 +8,8 @@ defmodule ElixirScript.State do %{ compiler_opts: compiler_opts, modules: Keyword.new, - refs: [] + refs: [], + js_modules: [] } end) end @@ -57,14 +58,44 @@ defmodule ElixirScript.State do end) end - def get_javascript_modules(pid) do + def put_javascript_module(pid, module, name, path) do + Agent.update(pid, fn(state) -> + js_modules = Map.get(state, :js_modules, []) + js_modules = js_modules ++ [{module, name, path}] + %{ state | js_modules: js_modules } + end) + end + + def list_javascript_modules(pid) do Agent.get(pid, fn(state) -> - Map.get(state.compiler_opts, :js_modules, []) + state.js_modules |> Enum.map(fn - {module_name, _path} -> - module_name - {module_name, _path, _opts} -> - module_name + {module, _name, _path} -> + module + end) + end) + end + + def js_modules(pid) do + Agent.get(pid, fn(state) -> + state.js_modules + end) + end + + def get_js_module_name(pid, module) do + Agent.get(pid, fn(state) -> + {_, name, _} = state.js_modules + |> Enum.find(fn {m, _, _} -> module == m end) + name + end) + end + + def list_foreign_modules(pid) do + Agent.get(pid, fn(state) -> + state.modules + |> Enum.filter(fn + (%{attributes: [__foreign_info__: _]}) -> true + (_) -> false end) end) end diff --git a/lib/mix/tasks/compile.elixir_script.ex b/lib/mix/tasks/compile.elixir_script.ex index b01abbcb..1e6067e2 100644 --- a/lib/mix/tasks/compile.elixir_script.ex +++ b/lib/mix/tasks/compile.elixir_script.ex @@ -65,8 +65,7 @@ defmodule Mix.Tasks.Compile.ElixirScript do input = Keyword.fetch!(elixirscript_config, :input) opts = [ output: Keyword.get(elixirscript_config, :output), - format: Keyword.get(elixirscript_config, :format), - js_modules: Keyword.get(elixirscript_config, :js_modules, []) + format: Keyword.get(elixirscript_config, :format) ] {input, opts} diff --git a/mix.lock b/mix.lock index 9de5d64f..992c5f2f 100644 --- a/mix.lock +++ b/mix.lock @@ -1,10 +1,10 @@ %{"bunt": {:hex, :bunt, "0.2.0", "951c6e801e8b1d2cbe58ebbd3e616a869061ddadcc4863d0a2182541acae9a38", [:mix], [], "hexpm"}, "certifi": {:hex, :certifi, "1.2.1", "c3904f192bd5284e5b13f20db3ceac9626e14eeacfbb492e19583cf0e37b22be", [:rebar3], [], "hexpm"}, - "credo": {:hex, :credo, "0.8.1", "137efcc99b4bc507c958ba9b5dff70149e971250813cbe7d4537ec7e36997402", [:mix], [{:bunt, "~> 0.2.0", [hex: :bunt, repo: "hexpm", optional: false]}], "hexpm"}, + "credo": {:hex, :credo, "0.8.3", "efe6e9078de64cefdd25d8df7a97292e29e63f42a8988990340eaf1f40d93224", [:mix], [{:bunt, "~> 0.2.0", [hex: :bunt, repo: "hexpm", optional: false]}], "hexpm"}, "earmark": {:hex, :earmark, "1.2.2", "f718159d6b65068e8daeef709ccddae5f7fdc770707d82e7d126f584cd925b74", [:mix], [], "hexpm"}, "estree": {:hex, :estree, "2.6.0", "86a301b0c355fa55c19e7ef9dceb1b1e983c6df526a2b7846818a38c258fc3fb", [:mix], [], "hexpm"}, - "ex_doc": {:hex, :ex_doc, "0.16.1", "b4b8a23602b4ce0e9a5a960a81260d1f7b29635b9652c67e95b0c2f7ccee5e81", [:mix], [{:earmark, "~> 1.1", [hex: :earmark, repo: "hexpm", optional: false]}], "hexpm"}, - "excoveralls": {:hex, :excoveralls, "0.7.0", "05cb3332c2b0f799df3ab90eb7df1ae5a147c86776e91792848a12b7ed87242f", [:mix], [{:exjsx, ">= 3.0.0", [hex: :exjsx, repo: "hexpm", optional: false]}, {:hackney, ">= 0.12.0", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm"}, + "ex_doc": {:hex, :ex_doc, "0.16.2", "3b3e210ebcd85a7c76b4e73f85c5640c011d2a0b2f06dcdf5acdb2ae904e5084", [:mix], [{:earmark, "~> 1.1", [hex: :earmark, repo: "hexpm", optional: false]}], "hexpm"}, + "excoveralls": {:hex, :excoveralls, "0.7.1", "3dd659db19c290692b5e2c4a2365ae6d4488091a1ba58f62dcbdaa0c03da5491", [:mix], [{:exjsx, ">= 3.0.0", [hex: :exjsx, repo: "hexpm", optional: false]}, {:hackney, ">= 0.12.0", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm"}, "exjsx": {:hex, :exjsx, "4.0.0", "60548841e0212df401e38e63c0078ec57b33e7ea49b032c796ccad8cde794b5c", [:mix], [{:jsx, "~> 2.8.0", [hex: :jsx, repo: "hexpm", optional: false]}], "hexpm"}, "fs": {:hex, :fs, "3.4.0", "6d18575c250b415b3cad559e6f97a4c822516c7bc2c10bfbb2493a8f230f5132", [:rebar3], [], "hexpm"}, "hackney": {:hex, :hackney, "1.8.6", "21a725db3569b3fb11a6af17d5c5f654052ce9624219f1317e8639183de4a423", [:rebar3], [{:certifi, "1.2.1", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "5.0.2", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "1.0.1", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "1.0.2", [hex: :mimerl, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "1.1.1", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}], "hexpm"}, diff --git a/src/javascript/lib/core.js b/src/javascript/lib/core.js index bacfe6fe..b1c85c9e 100644 --- a/src/javascript/lib/core.js +++ b/src/javascript/lib/core.js @@ -5,6 +5,7 @@ import SpecialForms from './core/special_forms'; import erlang from './core/erlang_compat/erlang'; import maps from './core/erlang_compat/maps'; import lists from './core/erlang_compat/lists'; +import Store from './core/store'; class Integer {} class Float {} @@ -36,8 +37,9 @@ export default { Float, Functions, SpecialForms, + Store, global: globalState, erlang, maps, - lists, + lists }; diff --git a/src/javascript/lib/core/erlang_compat/erlang.js b/src/javascript/lib/core/erlang_compat/erlang.js index aefbf7b7..5a0bc2d4 100644 --- a/src/javascript/lib/core/erlang_compat/erlang.js +++ b/src/javascript/lib/core/erlang_compat/erlang.js @@ -309,6 +309,14 @@ function tuple_size(tuple) { return tuple.length; } +function binary_to_float(str) { + return parseFloat(str); +} + +function binary_to_integer(str, base = 10) { + return parseInt(str, base); +} + export default { atom_to_binary, binary_to_atom, @@ -372,5 +380,7 @@ export default { round, tl, trunc, - tuple_size + tuple_size, + binary_to_float, + binary_to_integer }; diff --git a/src/javascript/lib/core/store.js b/src/javascript/lib/core/store.js new file mode 100644 index 00000000..f0ebc0ec --- /dev/null +++ b/src/javascript/lib/core/store.js @@ -0,0 +1,47 @@ +import Core from '../core'; + +function get_key(key) { + let real_key = key; + + if (__elixirscript_names__.has(key)) { + real_key = __elixirscript_names__.get(key); + } + + if (__elixirscript_store__.has(real_key)) { + return real_key; + } + + throw new Error(`Key ${real_key} not found`); +} + +function create(value, name = null) { + const key = new Core.PID(); + + if (name !== null) { + __elixirscript_names__.set(name, key); + } + + __elixirscript_store__.set(key, value); +} + +function update(key, value) { + const real_key = get_key(key); + __elixirscript_store__.set(real_key, value); +} + +function read(key) { + const real_key = get_key(key); + __elixirscript_store__.get(real_key); +} + +function remove(key) { + const real_key = get_key(key); + __elixirscript_store__.delete(real_key); +} + +export default { + create, + update, + read, + remove +}; diff --git a/test/cli_test.exs b/test/cli_test.exs index 53589cc9..b08c1d93 100644 --- a/test/cli_test.exs +++ b/test/cli_test.exs @@ -30,10 +30,4 @@ defmodule ElixirScript.CLI.Test do ElixirScript.CLI.process({["Atom"], []}) end) =~ "export default Elixir" end - - test "process js modules" do - assert capture_io(fn -> - ElixirScript.CLI.process({["Atom"], [js_module: "React:react"]}) - end) =~ "import React from 'react'" - end end \ No newline at end of file diff --git a/test/compiler_test.exs b/test/compiler_test.exs index c154e67c..f41aa7bf 100644 --- a/test/compiler_test.exs +++ b/test/compiler_test.exs @@ -7,7 +7,7 @@ defmodule ElixirScript.Compiler.Test do end test "Can compile multiple entry modules" do - result = ElixirScript.Compiler.compile([Atom, String]) + result = ElixirScript.Compiler.compile([Atom, String, Agent]) assert is_binary(result) end diff --git a/test/ffi_test.exs b/test/ffi_test.exs new file mode 100644 index 00000000..dbbaa77d --- /dev/null +++ b/test/ffi_test.exs @@ -0,0 +1,17 @@ +defmodule ElixirScript.FFI.Test do + use ExUnit.Case + + defmodule MyTestModule do + use ElixirScript.FFI + + foreign my_test_function(arg1, arg2) + end + + test "FFI module has __foreign_info__ attribute" do + assert Keyword.has_key?(MyTestModule.__info__(:attributes), :__foreign_info__) + end + + test "FFI module makes foreign function" do + assert Keyword.has_key?(MyTestModule.__info__(:functions), :my_test_function) + end +end \ No newline at end of file diff --git a/test/passes/translate/forms/js_test.exs b/test/passes/translate/forms/js_test.exs index c6d7c121..31bf2af3 100644 --- a/test/passes/translate/forms/js_test.exs +++ b/test/passes/translate/forms/js_test.exs @@ -64,42 +64,4 @@ defmodule ElixirScript.Translate.Forms.JS.Test do [J.literal("react")] ) end - - test "global function or property" do - ast = {{:., [], [JS, :self]}, [], []} - state = %{function: {:each, nil}, module: Enum, vars: %{:_ => 0, "entry" => 0, "enumerable" => 0, "fun" => 0}} - - {js_ast, _} = Form.compile(ast, state) - assert js_ast == J.call_expression( - ElixirScript.Translate.Forms.JS.call_property(), - [ - ElixirScript.Translate.Forms.JS.global(), - J.literal("self") - ] - ) - end - - test "global function with params" do - ast = {{:., [], [JS, :self]}, [], ["something"]} - state = %{function: {:each, nil}, module: Enum, vars: %{:_ => 0, "entry" => 0, "enumerable" => 0, "fun" => 0}} - - {js_ast, _} = Form.compile(ast, state) - assert js_ast == J.call_expression( - J.identifier(:self), - [J.literal("something")] - ) - end - - test "JavaScript module call", %{state: state} do - ast = {{:., [], [JS.Object, :keys]}, [], [{:obj, [], nil}]} - - {js_ast, _} = Form.compile(ast, state) - assert js_ast == J.call_expression( - J.member_expression( - J.identifier("Object"), - J.identifier("keys") - ), - [J.identifier("obj")] - ) - end end \ No newline at end of file diff --git a/test/support/main.ex b/test/support/main.ex index 64562538..a18d3d90 100644 --- a/test/support/main.ex +++ b/test/support/main.ex @@ -1,7 +1,5 @@ defmodule Main do def start(:normal, [callback]) do callback.("started") - - Enum.each(1..3, fn x -> JS.console.log(x) end) end end