From fe8781284196583d986052a27d2949672bc398eb Mon Sep 17 00:00:00 2001 From: Devin Alexander Torres Date: Sun, 12 Aug 2018 21:15:06 -0500 Subject: [PATCH] Several updates and improvements - Upgrade to Elixir 1.6+ - Format using mix format - Support multiple possible runtime executables - Support runtime command arguments - Add V8 runtime - Add Credo and Dialyxir static analysis - Relicense under CC0 1.0 Universal --- .credo.exs | 170 ++++++++++++++++++++++++++++++++ .formatter.exs | 5 + .gitignore | 23 ++++- .tool-versions | 2 + .travis.yml | 45 ++++----- LICENSE | 121 +++++++++++++++++++++++ README.md | 21 ++-- UNLICENSE | 24 ----- config/config.exs | 26 +++-- lib/execjs.ex | 97 ++++++++++-------- lib/execjs/escape.ex | 27 ++--- lib/execjs/runtime.ex | 32 ++++-- lib/execjs/runtimes.ex | 89 +++++++++++------ mix.exs | 54 +++++----- mix.lock | 10 +- priv/jsc_runner.js.eex | 6 +- priv/node_runner.js.eex | 6 +- priv/rhino_runner.js.eex | 6 +- priv/spidermonkey_runner.js.eex | 6 +- priv/v8_runner.js.eex | 19 ++++ test/execjs_test.exs | 13 +-- test/test_helper.exs | 2 +- 22 files changed, 597 insertions(+), 207 deletions(-) create mode 100644 .credo.exs create mode 100644 .formatter.exs create mode 100644 .tool-versions create mode 100644 LICENSE delete mode 100644 UNLICENSE create mode 100644 priv/v8_runner.js.eex diff --git a/.credo.exs b/.credo.exs new file mode 100644 index 0000000..b71ec10 --- /dev/null +++ b/.credo.exs @@ -0,0 +1,170 @@ +# This file contains the configuration for Credo and you are probably reading +# this after creating it with `mix credo.gen.config`. +# +# If you find anything wrong or unclear in this file, please report an +# issue on GitHub: https://github.com/rrrene/credo/issues +# +%{ + # + # You can have as many configs as you like in the `configs:` field. + configs: [ + %{ + # + # Run any exec using `mix credo -C `. If no exec name is given + # "default" is used. + # + name: "default", + # + # These are the files included in the analysis: + files: %{ + # + # You can give explicit globs or simply directories. + # In the latter case `**/*.{ex,exs}` will be used. + # + included: [ + "lib/", + "src/", + "test/", + "web/", + "apps/", + "bench/", + "{mix,.credo,.formatter}.exs" + ], + excluded: [~r"/_build/", ~r"/deps/", ~r"/node_modules/"] + }, + # + # If you create your own checks, you must specify the source files for + # them here, so they can be loaded by Credo before running the analysis. + # + requires: [], + # + # If you want to enforce a style guide and need a more traditional linting + # experience, you can change `strict` to `true` below: + # + strict: true, + # + # If you want to use uncolored output by default, you can change `color` + # to `false` below: + # + color: true, + # + # You can customize the parameters of any check by adding a second element + # to the tuple. + # + # To disable a check put `false` as second element: + # + # {Credo.Check.Design.DuplicatedCode, false} + # + checks: [ + # + ## Consistency Checks + # + {Credo.Check.Consistency.ExceptionNames}, + {Credo.Check.Consistency.LineEndings}, + {Credo.Check.Consistency.ParameterPatternMatching}, + {Credo.Check.Consistency.SpaceAroundOperators}, + {Credo.Check.Consistency.SpaceInParentheses}, + {Credo.Check.Consistency.TabsOrSpaces}, + + # + ## Design Checks + # + # You can customize the priority of any check + # Priority values are: `low, normal, high, higher` + # + {Credo.Check.Design.AliasUsage, priority: :low}, + # For some checks, you can also set other parameters + # + # If you don't want the `setup` and `test` macro calls in ExUnit tests + # or the `schema` macro in Ecto schemas to trigger DuplicatedCode, just + # set the `excluded_macros` parameter to `[:schema, :setup, :test]`. + # + {Credo.Check.Design.DuplicatedCode, excluded_macros: []}, + # You can also customize the exit_status of each check. + # If you don't want TODO comments to cause `mix credo` to fail, just + # set this value to 0 (zero). + # + {Credo.Check.Design.TagTODO, exit_status: 2}, + {Credo.Check.Design.TagFIXME}, + + # + ## Readability Checks + # + {Credo.Check.Readability.AliasOrder}, + {Credo.Check.Readability.FunctionNames}, + {Credo.Check.Readability.LargeNumbers}, + {Credo.Check.Readability.MaxLineLength, priority: :low, max_length: 80}, + {Credo.Check.Readability.ModuleAttributeNames}, + {Credo.Check.Readability.ModuleDoc}, + {Credo.Check.Readability.ModuleNames}, + {Credo.Check.Readability.ParenthesesOnZeroArityDefs}, + {Credo.Check.Readability.ParenthesesInCondition}, + {Credo.Check.Readability.PredicateFunctionNames}, + {Credo.Check.Readability.PreferImplicitTry}, + {Credo.Check.Readability.RedundantBlankLines}, + {Credo.Check.Readability.StringSigils}, + {Credo.Check.Readability.TrailingBlankLine}, + {Credo.Check.Readability.TrailingWhiteSpace}, + {Credo.Check.Readability.VariableNames}, + {Credo.Check.Readability.Semicolons}, + {Credo.Check.Readability.SpaceAfterCommas}, + + # + ## Refactoring Opportunities + # + {Credo.Check.Refactor.DoubleBooleanNegation}, + {Credo.Check.Refactor.CondStatements}, + {Credo.Check.Refactor.CyclomaticComplexity}, + {Credo.Check.Refactor.FunctionArity}, + {Credo.Check.Refactor.LongQuoteBlocks}, + {Credo.Check.Refactor.MapInto}, + {Credo.Check.Refactor.MatchInCondition}, + {Credo.Check.Refactor.NegatedConditionsInUnless}, + {Credo.Check.Refactor.NegatedConditionsWithElse}, + {Credo.Check.Refactor.Nesting}, + {Credo.Check.Refactor.PipeChainStart, + excluded_argument_types: [:atom, :binary, :fn, :keyword], + excluded_functions: []}, + {Credo.Check.Refactor.UnlessWithElse}, + + # + ## Warnings + # + {Credo.Check.Warning.BoolOperationOnSameValues}, + {Credo.Check.Warning.ExpensiveEmptyEnumCheck}, + {Credo.Check.Warning.IExPry}, + {Credo.Check.Warning.IoInspect}, + {Credo.Check.Warning.LazyLogging}, + {Credo.Check.Warning.OperationOnSameValues}, + {Credo.Check.Warning.OperationWithConstantResult}, + {Credo.Check.Warning.UnusedEnumOperation}, + {Credo.Check.Warning.UnusedFileOperation}, + {Credo.Check.Warning.UnusedKeywordOperation}, + {Credo.Check.Warning.UnusedListOperation}, + {Credo.Check.Warning.UnusedPathOperation}, + {Credo.Check.Warning.UnusedRegexOperation}, + {Credo.Check.Warning.UnusedStringOperation}, + {Credo.Check.Warning.UnusedTupleOperation}, + {Credo.Check.Warning.RaiseInsideRescue}, + + # + # Controversial and experimental checks (opt-in, just remove `, false`) + # + {Credo.Check.Refactor.ABCSize, false}, + {Credo.Check.Refactor.AppendSingleItem, false}, + {Credo.Check.Refactor.VariableRebinding, false}, + {Credo.Check.Warning.MapGetUnsafePass, false}, + {Credo.Check.Consistency.MultiAliasImportRequireUse, false}, + + # + # Deprecated checks (these will be deleted after a grace period) + # + {Credo.Check.Readability.Specs, false} + + # + # Custom checks can be created using `mix credo.gen.check`. + # + ] + } + ] +} diff --git a/.formatter.exs b/.formatter.exs new file mode 100644 index 0000000..66926da --- /dev/null +++ b/.formatter.exs @@ -0,0 +1,5 @@ +# Used by "mix format" +[ + inputs: ["{mix,.credo,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"], + line_length: 80 +] diff --git a/.gitignore b/.gitignore index 9607671..a1c9d2f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,23 @@ -/_build -/deps +# The directory Mix will write compiled artifacts to. +/_build/ + +# If you run "mix test --cover", coverage assets end up here. +/cover/ + +# The directory Mix downloads your dependencies sources to. +/deps/ + +# Where 3rd-party dependencies like ExDoc output generated docs. +/doc/ + +# Ignore .fetch files in case you like to edit your project deps locally. +/.fetch + +# If the VM crashes, it generates a dump, let's ignore it too. erl_crash.dump + +# Also ignore archive artifacts (built via "mix archive.build"). *.ez + +# Ignore package tarball (built via "mix hex.build"). +execjs-*.tar diff --git a/.tool-versions b/.tool-versions new file mode 100644 index 0000000..09fb6e4 --- /dev/null +++ b/.tool-versions @@ -0,0 +1,2 @@ +erlang 21.0.5 +elixir 1.7.2-otp-21 diff --git a/.travis.yml b/.travis.yml index d33a8b2..5c5bf20 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,33 +1,24 @@ +--- +sudo: false + language: elixir elixir: - - 1.4.0 - - 1.3.4 - - 1.3.3 - - 1.3.2 - - 1.3.1 - - 1.3.0 - - 1.2.6 - - 1.2.5 - - 1.2.4 - - 1.2.3 - - 1.2.2 - - 1.2.1 - - 1.2.0 - - 1.1.1 - - 1.1.0 - - 1.0.5 - - 1.0.4 + - 1.7.2 + - 1.6.6 otp_release: - - 19.2 - - 19.1 - - 19.0 - - 18.3 - - 18.2 - - 18.1 - - 18.0 -before_install: - - sudo apt-get update -qq - - sudo apt-get install -qq rhino + - 21.0 + - 20.3 + +addons: + apt: + packages: + - nodejs + - libmozjs-24-bin + - libjavascriptcoregtk-3.0-bin + - rhino + env: - EXECJS_RUNTIME=Node + - EXECJS_RUNTIME=SpiderMonkey + - EXECJS_RUNTIME=JavaScriptCore - EXECJS_RUNTIME=Rhino diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..0e259d4 --- /dev/null +++ b/LICENSE @@ -0,0 +1,121 @@ +Creative Commons Legal Code + +CC0 1.0 Universal + + CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE + LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN + ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS + INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES + REGARDING THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS + PROVIDED HEREUNDER, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM + THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED + HEREUNDER. + +Statement of Purpose + +The laws of most jurisdictions throughout the world automatically confer +exclusive Copyright and Related Rights (defined below) upon the creator +and subsequent owner(s) (each and all, an "owner") of an original work of +authorship and/or a database (each, a "Work"). + +Certain owners wish to permanently relinquish those rights to a Work for +the purpose of contributing to a commons of creative, cultural and +scientific works ("Commons") that the public can reliably and without fear +of later claims of infringement build upon, modify, incorporate in other +works, reuse and redistribute as freely as possible in any form whatsoever +and for any purposes, including without limitation commercial purposes. +These owners may contribute to the Commons to promote the ideal of a free +culture and the further production of creative, cultural and scientific +works, or to gain reputation or greater distribution for their Work in +part through the use and efforts of others. + +For these and/or other purposes and motivations, and without any +expectation of additional consideration or compensation, the person +associating CC0 with a Work (the "Affirmer"), to the extent that he or she +is an owner of Copyright and Related Rights in the Work, voluntarily +elects to apply CC0 to the Work and publicly distribute the Work under its +terms, with knowledge of his or her Copyright and Related Rights in the +Work and the meaning and intended legal effect of CC0 on those rights. + +1. Copyright and Related Rights. A Work made available under CC0 may be +protected by copyright and related or neighboring rights ("Copyright and +Related Rights"). Copyright and Related Rights include, but are not +limited to, the following: + + i. the right to reproduce, adapt, distribute, perform, display, + communicate, and translate a Work; + ii. moral rights retained by the original author(s) and/or performer(s); +iii. publicity and privacy rights pertaining to a person's image or + likeness depicted in a Work; + iv. rights protecting against unfair competition in regards to a Work, + subject to the limitations in paragraph 4(a), below; + v. rights protecting the extraction, dissemination, use and reuse of data + in a Work; + vi. database rights (such as those arising under Directive 96/9/EC of the + European Parliament and of the Council of 11 March 1996 on the legal + protection of databases, and under any national implementation + thereof, including any amended or successor version of such + directive); and +vii. other similar, equivalent or corresponding rights throughout the + world based on applicable law or treaty, and any national + implementations thereof. + +2. Waiver. To the greatest extent permitted by, but not in contravention +of, applicable law, Affirmer hereby overtly, fully, permanently, +irrevocably and unconditionally waives, abandons, and surrenders all of +Affirmer's Copyright and Related Rights and associated claims and causes +of action, whether now known or unknown (including existing as well as +future claims and causes of action), in the Work (i) in all territories +worldwide, (ii) for the maximum duration provided by applicable law or +treaty (including future time extensions), (iii) in any current or future +medium and for any number of copies, and (iv) for any purpose whatsoever, +including without limitation commercial, advertising or promotional +purposes (the "Waiver"). Affirmer makes the Waiver for the benefit of each +member of the public at large and to the detriment of Affirmer's heirs and +successors, fully intending that such Waiver shall not be subject to +revocation, rescission, cancellation, termination, or any other legal or +equitable action to disrupt the quiet enjoyment of the Work by the public +as contemplated by Affirmer's express Statement of Purpose. + +3. Public License Fallback. Should any part of the Waiver for any reason +be judged legally invalid or ineffective under applicable law, then the +Waiver shall be preserved to the maximum extent permitted taking into +account Affirmer's express Statement of Purpose. In addition, to the +extent the Waiver is so judged Affirmer hereby grants to each affected +person a royalty-free, non transferable, non sublicensable, non exclusive, +irrevocable and unconditional license to exercise Affirmer's Copyright and +Related Rights in the Work (i) in all territories worldwide, (ii) for the +maximum duration provided by applicable law or treaty (including future +time extensions), (iii) in any current or future medium and for any number +of copies, and (iv) for any purpose whatsoever, including without +limitation commercial, advertising or promotional purposes (the +"License"). The License shall be deemed effective as of the date CC0 was +applied by Affirmer to the Work. Should any part of the License for any +reason be judged legally invalid or ineffective under applicable law, such +partial invalidity or ineffectiveness shall not invalidate the remainder +of the License, and in such case Affirmer hereby affirms that he or she +will not (i) exercise any of his or her remaining Copyright and Related +Rights in the Work or (ii) assert any associated claims and causes of +action with respect to the Work, in either case contrary to Affirmer's +express Statement of Purpose. + +4. Limitations and Disclaimers. + + a. No trademark or patent rights held by Affirmer are waived, abandoned, + surrendered, licensed or otherwise affected by this document. + b. Affirmer offers the Work as-is and makes no representations or + warranties of any kind concerning the Work, express, implied, + statutory or otherwise, including without limitation warranties of + title, merchantability, fitness for a particular purpose, non + infringement, or the absence of latent or other defects, accuracy, or + the present or absence of errors, whether or not discoverable, all to + the greatest extent permissible under applicable law. + c. Affirmer disclaims responsibility for clearing rights of other persons + that may apply to the Work or any use thereof, including without + limitation any person's Copyright and Related Rights in the Work. + Further, Affirmer disclaims responsibility for obtaining any necessary + consents, permissions or other rights required for any use of the + Work. + d. Affirmer understands and acknowledges that Creative Commons is not a + party to this document and has no duty or obligation with respect to + this CC0 or use of the Work. diff --git a/README.md b/README.md index db7abcb..7d119f9 100644 --- a/README.md +++ b/README.md @@ -1,20 +1,21 @@ # Execjs -[![Build Status](https://api.travis-ci.org/devinus/execjs.svg?branch=master)](https://travis-ci.org/devinus/execjs) +[![Build Status](https://travis-ci.org/devinus/execjs.svg?branch=master)](https://travis-ci.org/devinus/execjs) +[![Hex.pm Version](https://img.shields.io/hexpm/v/execjs.svg?style=flat-square)](https://hex.pm/packages/execjs) +[![Hex.pm Download Total](https://img.shields.io/hexpm/dt/execjs.svg?style=flat-square)](https://hex.pm/packages/execjs) -[![Support via Gratipay](https://cdn.rawgit.com/gratipay/gratipay-badge/2.3.0/dist/gratipay.png)](https://gratipay.com/devinus/) - -`Execjs` allows you run JavaScript from Elixir. It can automatically pick the -best runtime available on the system. +`Execjs` allows you easily run JavaScript from Elixir. It can automatically +pick the best runtime available on the system. ## Runtimes `Execjs` supports the following runtimes: -- [Node.js](http://nodejs.org/) +- [Node.js](https://nodejs.org/en/) +- [V8](https://developers.google.com/v8/) - [SpiderMonkey](https://developer.mozilla.org/en-US/docs/Mozilla/Projects/SpiderMonkey) -- [JavaScriptCore](http://trac.webkit.org/wiki/JSC) -- [Rhino](https://developer.mozilla.org/en-US/docs/Rhino) +- [JavaScriptCore](https://trac.webkit.org/wiki/JSC) +- [Rhino](https://developer.mozilla.org/en-US/docs/Mozilla/Projects/Rhino) Use the application environment (application key: `:execjs`, key: `:runtime`) to set the runtime `Execjs` uses. Alternatively, the `EXECJS_RUNTIME` @@ -25,14 +26,14 @@ environment variable can also be used to set the runtime. ### `eval` ```iex -iex> Execjs.eval "'red yellow blue'.split(' ')" +iex> "'red yellow blue'.split(' ')" |> Execjs.eval ["red", "yellow", "blue"] ``` ### `compile`/`call` ```iex -iex> {source, 0} = System.cmd("curl", ["-sL", "--compressed", "https://rawgit.com/jashkenas/coffeescript/master/extras/coffee-script.js"]) +iex> {source, 0} = System.cmd("curl", ["-fsSL", "--compressed", "https://coffeescript.org/browser-compiler/coffeescript.js"]) iex> context = Execjs.compile(source) iex> Execjs.call(context, "CoffeeScript.compile", ["square = (x) -> x * x"]) "(function() {\n var square;\n\n square = function(x) {\n return x * x;\n };\n\n}).call(this);\n" diff --git a/UNLICENSE b/UNLICENSE deleted file mode 100644 index 68a49da..0000000 --- a/UNLICENSE +++ /dev/null @@ -1,24 +0,0 @@ -This is free and unencumbered software released into the public domain. - -Anyone is free to copy, modify, publish, use, compile, sell, or -distribute this software, either in source code form or as a compiled -binary, for any purpose, commercial or non-commercial, and by any -means. - -In jurisdictions that recognize copyright laws, the author or authors -of this software dedicate any and all copyright interest in the -software to the public domain. We make this dedication for the benefit -of the public at large and to the detriment of our heirs and -successors. We intend this dedication to be an overt act of -relinquishment in perpetuity of all present and future rights to this -software under copyright law. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. -IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR -OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, -ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR -OTHER DEALINGS IN THE SOFTWARE. - -For more information, please refer to diff --git a/config/config.exs b/config/config.exs index b477e92..00eac07 100644 --- a/config/config.exs +++ b/config/config.exs @@ -1,17 +1,25 @@ # This file is responsible for configuring your application -# and its dependencies. The Mix.Config module provides functions -# to aid in doing so. +# and its dependencies with the aid of the Mix.Config module. use Mix.Config -# Note this file is loaded before any dependency and is restricted +# This configuration is loaded before any dependency and is restricted # to this project. If another project depends on this project, this -# file won't be loaded nor affect the parent project. +# file won't be loaded nor affect the parent project. For this reason, +# if you want to provide default values for your application for +# 3rd-party users, it should be done in your "mix.exs" file. -# Sample configuration: +# You can configure your application as: +# +# config :foo, key: :value +# +# and access this configuration in your application as: +# +# Application.get_env(:foo, :key) +# +# You can also configure a 3rd-party app: +# +# config :logger, level: :info # -# config :my_dep, -# key: :value, -# limit: 42 # It is also possible to import configuration files, relative to this # directory. For example, you can emulate configuration per environment @@ -19,4 +27,4 @@ use Mix.Config # Configuration from the imported file will override the ones defined # here (which is why it is important to import them last). # -# import_config "#{Mix.env}.exs" +# import_config "#{Mix.env()}.exs" diff --git a/lib/execjs.ex b/lib/execjs.ex index 252f2d4..a4f2126 100644 --- a/lib/execjs.ex +++ b/lib/execjs.ex @@ -1,81 +1,96 @@ defmodule Execjs do - import Execjs.Escape, only: [escape: 1] + readme_path = [__DIR__, "..", "README.md"] |> Path.join() |> Path.expand() - defmodule Error, do: defexception [:message] - defmodule RuntimeError, do: defexception [:message] + @external_resource readme_path + @moduledoc readme_path |> File.read!() |> String.trim() - defmodule RuntimeUnavailable do - defexception message: "Could not find JavaScript runtime" - end + alias Execjs.Runtimes + + defmodule(ExecError, do: defexception([:message])) + defmodule(RuntimeError, do: defexception([:message, :stack])) + + @type context() :: (iodata -> iodata) - @spec eval(String.t) :: any + @spec eval(String.t()) :: Poison.Parser.t() | :undefined | no_return def eval(source) when is_binary(source) do - exec ~s[eval("#{escape(source)}")] + exec(~s[eval(#{Poison.encode!(source, escape: :javascript)})]) end - @spec compile(String.t) :: (String.t -> String.t) - def compile(source) when is_binary(source) do - { pre, post } = { "(function(){\n#{source};\n", ";\n})()" } - fn (source) -> - pre <> source <> post - end + @spec compile(iodata) :: context() + def compile(source) do + preamble = IO.iodata_to_binary(["(function(){\n", source, ";\n"]) + &IO.iodata_to_binary([preamble, &1, ";\n})()"]) end - @spec call((String.t -> String.t), String.t, list(any)) :: any - def call(context, identifier, args) when is_binary(identifier) and is_list(args) do - source = "return #{identifier}.apply(this, #{Poison.encode!(args, escape: :javascript)})" - exec context.(source) + @spec call(context(), String.t(), list(Poison.Encoder.t())) :: + Poison.Parser.t() | :undefined | no_return + def call(context, identifier, args \\ []) + when is_binary(identifier) and is_list(args) do + source = + "return #{identifier}.apply(this, #{ + Poison.encode!(args, escape: :javascript) + })" + + exec(context.(source)) end defp exec(source) do - runtime = Execjs.Runtimes.best_available + runtime = Runtimes.best_available() + command = runtime.command |> System.find_executable() program = runtime.template(source) - command = runtime.command |> System.find_executable tmpfile = compile_to_tempfile(program) try do - port = Port.open({ :spawn_executable, command }, - [:stream, :in, :binary, :eof, :hide, { :args, [tmpfile] }]) + port = + Port.open({:spawn_executable, command}, [ + :stream, + :in, + :binary, + :eof, + :hide, + {:args, Enum.concat(runtime.arguments, [tmpfile])}, + {:parallelism, true} + ]) extract_result(loop(port)) after - File.rm! tmpfile + File.rm!(tmpfile) end end - defp loop(port) do - loop(port, "") - end - - defp loop(port, acc) do + defp loop(port, acc \\ "") do receive do - { ^port, { :data, data } } -> + {^port, {:data, data}} -> loop(port, acc <> data) - { ^port, :eof } -> - send port, { self(), :close } - receive do: ({ ^port, :closed } -> :ok) + + {^port, :eof} -> + send(port, {self(), :close}) + receive do: ({^port, :closed} -> :ok) acc end end defp compile_to_tempfile(program) do - hash = :erlang.phash2(:crypto.strong_rand_bytes(8)) + hash = :erlang.phash2({System.get_pid(), System.monotonic_time()}) filename = "execjs-#{hash}.js" - path = Path.join(System.tmp_dir!, filename) - File.write! path, program + path = Path.join(System.tmp_dir!(), filename) + File.write!(path, program, ~w[binary exclusive raw sync]a) path end defp extract_result(output) do case Poison.decode!(output) do - [ "ok", value ] -> + ["ok", value] -> value - [ "ok" ] -> + + ["ok"] -> :undefined - [ "err", message ] -> - raise %RuntimeError{message: message} - [ "err" ] -> - raise %Error{} + + ["err", message, stack] -> + raise RuntimeError, message: message, stack: stack + + ["err"] -> + raise ExecError, message: "Unexpected error" end end end diff --git a/lib/execjs/escape.ex b/lib/execjs/escape.ex index 3e57061..39f3f54 100644 --- a/lib/execjs/escape.ex +++ b/lib/execjs/escape.ex @@ -1,27 +1,28 @@ defmodule Execjs.Escape do - @compile :native + @moduledoc "Exposes the `escape/1` function to escape JavaScript source." - def escape(""), do: "" + def escape(""), do: "" def escape(string), do: escape(string, "") escape_map = [ - { ?\\, "\\\\" }, - { ?", "\\\"" }, - { ?\n, "\\n" }, - { ?\r, "\\r" }, + {?\\, "\\\\"}, + {?", "\\\""}, + {?\n, "\\n"}, + {?\r, "\\r"}, + # http://bclary.com/2004/11/07/#a-7.3 - { "\u2028", "\\u2028" }, - { "\u2029", "\\u2029" } + {"\u2028", "\\u2028"}, + {"\u2029", "\\u2029"} ] - for { char, escaped } <- escape_map do - defp escape(<< unquote(char), rest :: binary >>, acc) do - escape(rest, << acc :: binary, unquote(escaped) >>) + for {char, escaped} <- escape_map do + defp escape(<>, acc) do + escape(rest, <>) end end - defp escape(<< char :: utf8, rest :: binary >>, acc) do - escape(rest, << acc :: binary, char :: utf8 >>) + defp escape(<>, acc) do + escape(rest, <>) end defp escape(<<>>, acc) do diff --git a/lib/execjs/runtime.ex b/lib/execjs/runtime.ex index e6874a4..93dfa01 100644 --- a/lib/execjs/runtime.ex +++ b/lib/execjs/runtime.ex @@ -1,24 +1,40 @@ defmodule Execjs.Runtime do - app = Mix.Project.config[:app] + @moduledoc "Defines the `defruntime/2` macro to define JavaScript runtimes." + + alias Mix.Project + + app = Project.config()[:app] def runner_path(runner) do Path.join([:code.priv_dir(unquote(app)), runner]) end - defmacro defruntime(name, options) do + defmacro defruntime(runtime, options) do + name = Macro.to_string(runtime) + quote do - defmodule unquote(name) do + defmodule unquote(runtime) do + @moduledoc "Runtime definition for #{unquote(name)}." + require EEx - def command, do: unquote(options[:command]) + alias Execjs.Runtime + + def executables, do: unquote(options[:executables]) + + def command, do: Enum.find(executables(), &System.find_executable(&1)) + + def arguments, do: unquote(options[:arguments] || []) + + def available?, do: not (command() == nil) - def available?, do: !!System.find_executable(command()) + @runner_path Runtime.runner_path(unquote(options[:runner])) + @external_resource @runner_path - runner_path = Execjs.Runtime.runner_path(unquote(options[:runner])) - EEx.function_from_file :def, :template, runner_path, [:source] + EEx.function_from_file(:def, :template, @runner_path, [:source]) end - @runtimes unquote(name) + @runtimes unquote(runtime) end end end diff --git a/lib/execjs/runtimes.ex b/lib/execjs/runtimes.ex index e62e1f4..c3a14e7 100644 --- a/lib/execjs/runtimes.ex +++ b/lib/execjs/runtimes.ex @@ -1,47 +1,80 @@ defmodule Execjs.Runtimes do + @moduledoc """ + Registers supported runtimes and exposes the `best_available/0` function to + find the most optimal runtime on the current system. + """ + import Execjs.Runtime - alias Execjs.RuntimeUnavailable + defmodule UnavailableError do + defexception message: "Could not find suitable JavaScript runtime" + end + + Module.register_attribute(__MODULE__, :runtimes, accumulate: true) - Module.register_attribute __MODULE__, :runtimes, accumulate: true + defruntime(Node, + runner: "node_runner.js.eex", + executables: ~w[node nodejs], + arguments: ["--no-deprecation"] + ) - defruntime Node, - command: "node", - runner: "node_runner.js.eex" + defruntime(V8, + runner: "v8_runner.js.eex", + executables: ~w[v8 d8], + arguments: [] + ) - defruntime SpiderMonkey, - command: "js", - runner: "spidermonkey_runner.js.eex" + defruntime(SpiderMonkey, + runner: "spidermonkey_runner.js.eex", + executables: ~w[js52 js24 js], + arguments: [] + ) - defruntime JavaScriptCore, - command: "/System/Library/Frameworks/JavaScriptCore.framework/Versions/A/Resources/jsc", - runner: "jsc_runner.js.eex" + defruntime(JavaScriptCore, + runner: "jsc_runner.js.eex", + executables: ~w[ + jsc + /System/Library/Frameworks/JavaScriptCore.framework/Versions/A/Resources/jsc + ], + arguments: ["-s"] + ) - defruntime Rhino, - command: "rhino", - runner: "rhino_runner.js.eex" + defruntime(Rhino, + runner: "rhino_runner.js.eex", + executables: ["rhino"], + arguments: ["-debug"] + ) def runtimes do unquote(Enum.reverse(@runtimes)) end def best_available do - case :application.get_env(:execjs, :runtime) do - { :ok, runtime } -> + case Application.get_env(:execjs, :runtime) do + nil -> + runtime = guess_runtime() + Application.put_env(:execjs, :runtime, runtime) runtime - :undefined -> - runtime = case System.get_env("EXECJS_RUNTIME") do - nil -> - Enum.find(runtimes(), &(&1.available?)) || raise RuntimeUnavailable - name -> - runtime = Module.concat(__MODULE__, name) - Code.ensure_loaded?(runtime) - && function_exported?(runtime, :available?, 0) - && runtime.available? - || raise RuntimeUnavailable - runtime + + runtime -> + runtime + end + end + + defp guess_runtime do + case System.get_env("EXECJS_RUNTIME") do + nil -> + Enum.find(runtimes(), & &1.available?) || raise UnavailableError + + name -> + runtime = Module.concat(__MODULE__, name) + + if not (Code.ensure_loaded?(runtime) && + function_exported?(runtime, :available?, 0) && + runtime.available?) do + raise UnavailableError end - :application.set_env(:execjs, :runtime, runtime) + runtime end end diff --git a/mix.exs b/mix.exs index d7dddff..715adb1 100644 --- a/mix.exs +++ b/mix.exs @@ -1,41 +1,45 @@ defmodule Execjs.Mixfile do use Mix.Project - @version String.strip(File.read!("VERSION")) + @version_path Path.join([__DIR__, "VERSION"]) + @external_resource @version_path + + @version @version_path |> File.read!() |> String.trim() def project do - [app: :execjs, - version: @version, - elixir: "~> 1.0", - description: "Run JavaScript code from Elixir", - deps: deps(), - package: package()] + [ + app: :execjs, + version: @version, + elixir: "~> 1.6", + start_permanent: Mix.env() == :prod, + description: "Run JavaScript code from Elixir", + deps: deps(), + package: package() + ] end - # Configuration for the OTP application - # - # Type `mix help compile.app` for more information + # Run "mix help compile.app" to learn about applications. def application do - [applications: []] + [ + extra_applications: [:logger] + ] end - # Dependencies can be hex.pm packages: - # - # {:mydep, "~> 0.3.0"} - # - # Or git/path repositories: - # - # {:mydep, git: "https://github.com/elixir-lang/mydep.git", tag: "0.1"} - # - # Type `mix help deps` for more examples and options + # Run "mix help deps" to learn about dependencies. defp deps do - [{:poison, "~> 3.1"}] + [ + {:poison, "~> 4.0"}, + {:ex_doc, "~> 0.19", only: :dev, runtime: false}, + {:dialyxir, "~> 0.5", only: :dev, runtime: false} + ] end defp package do - [files: ~w(lib priv mix.exs README.md UNLICENSE VERSION), - maintainers: ["Devin Torres"], - licenses: ["Unlicense"], - links: %{"GitHub" => "https://github.com/devinus/execjs"}] + [ + files: ~w(lib priv mix.exs README.md LICENSE VERSION), + maintainers: ["Devin Alexander Torres"], + licenses: ["CC0-1.0"], + links: %{"GitHub" => "https://github.com/devinus/execjs"} + ] end end diff --git a/mix.lock b/mix.lock index abc459d..7311bf2 100644 --- a/mix.lock +++ b/mix.lock @@ -1 +1,9 @@ -%{"poison": {:hex, :poison, "3.1.0", "d9eb636610e096f86f25d9a46f35a9facac35609a7591b3be3326e99a0484665", [:mix], []}} +%{ + "dialyxir": {:hex, :dialyxir, "0.5.1", "b331b091720fd93e878137add264bac4f644e1ddae07a70bf7062c7862c4b952", [:mix], [], "hexpm"}, + "earmark": {:hex, :earmark, "1.2.5", "4d21980d5d2862a2e13ec3c49ad9ad783ffc7ca5769cf6ff891a4553fbaae761", [:mix], [], "hexpm"}, + "ex_doc": {:hex, :ex_doc, "0.19.1", "519bb9c19526ca51d326c060cb1778d4a9056b190086a8c6c115828eaccea6cf", [:mix], [{:earmark, "~> 1.1", [hex: :earmark, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.7", [hex: :makeup_elixir, repo: "hexpm", optional: false]}], "hexpm"}, + "makeup": {:hex, :makeup, "0.5.1", "966c5c2296da272d42f1de178c1d135e432662eca795d6dc12e5e8787514edf7", [:mix], [{:nimble_parsec, "~> 0.2.2", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm"}, + "makeup_elixir": {:hex, :makeup_elixir, "0.8.0", "1204a2f5b4f181775a0e456154830524cf2207cf4f9112215c05e0b76e4eca8b", [:mix], [{:makeup, "~> 0.5.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 0.2.2", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm"}, + "nimble_parsec": {:hex, :nimble_parsec, "0.2.2", "d526b23bdceb04c7ad15b33c57c4526bf5f50aaa70c7c141b4b4624555c68259", [:mix], [], "hexpm"}, + "poison": {:hex, :poison, "4.0.1", "bcb755a16fac91cad79bfe9fc3585bb07b9331e50cfe3420a24bcc2d735709ae", [:mix], [], "hexpm"}, +} diff --git a/priv/jsc_runner.js.eex b/priv/jsc_runner.js.eex index 9bdbc4c..5ed75de 100644 --- a/priv/jsc_runner.js.eex +++ b/priv/jsc_runner.js.eex @@ -1,6 +1,6 @@ -(function(program, runner) { runner(program); })(function() { +(function(program, runner) { runner(program); })(function(debug,describe,describeArray,print,printErr,quit,gc,fullGC,edenGC,forceGCSlowPaths,gcHeapSize,addressOf,version,run,runString,load,loadString,readFile,read,checkSyntax,sleepSeconds,jscStack,readline,preciseTime,neverInlineFunction,noInline,noDFG,noFTL,noOSRExitFuzzing,numberOfDFGCompiles,jscOptions,optimizeNextInvocation,reoptimizationRetryCount,transferArrayBuffer,failNextNewCodeBlock,DFGTrue,OSRExit,isFinalTier,predictInt32,isInt32,fiatInt52,effectful42,makeMasquerader,hasCustomProperties,createGlobalObject,dumpTypesForAllVariables,drainMicrotasks,getRandomSeed,setRandomSeed,isRope,callerSourceOrigin,is32BitPlatform,loadModule,checkModuleSyntax,platformSupportsSamplingProfiler,generateHeapSnapshot,resetSuperSamplerState,ensureArrayStorage,startSamplingProfiler,samplingProfilerStackTraces,maxArguments,asyncTestStart,asyncTestPassed,WebAssemblyMemoryMode,$,$262,waitForReport,heapCapacity,flashHeapAccess,disableRichSourceInfo,mallocInALoop) { return <%= source %>; -}, function(program) { +}, function(program, undefined) { var result; try { result = program(); @@ -14,6 +14,6 @@ print('["err"]'); } } catch (err) { - print(JSON.stringify(['err', '' + err])); + print(JSON.stringify(['err', '' + err, err.stack])); } }); diff --git a/priv/node_runner.js.eex b/priv/node_runner.js.eex index 2b46b58..9d6680f 100644 --- a/priv/node_runner.js.eex +++ b/priv/node_runner.js.eex @@ -1,6 +1,6 @@ -(function(program, runner) { runner(program); })(function() { +(function(program, runner) { runner(program); })(function(console,DTRACE_NET_SERVER_CONNECTION,DTRACE_NET_STREAM_END,DTRACE_HTTP_SERVER_REQUEST,DTRACE_HTTP_SERVER_RESPONSE,DTRACE_HTTP_CLIENT_REQUEST,DTRACE_HTTP_CLIENT_RESPONSE,global,process,GLOBAL,root,Buffer,clearImmediate,clearInterval,clearTimeout,setImmediate,setInterval,setTimeout,module,require,assert,async_hooks,buffer,child_process,cluster,crypto,dgram,dns,domain,events,fs,http,https,net,os,path,perf_hooks,punycode,querystring,readline,repl,stream,string_decoder,tls,tty,url,util,v8,vm,zlib,http2) { return <%= source %>; -}, function(program) { +}, function(program, undefined) { var result; try { result = program(); @@ -14,6 +14,6 @@ process.stdout.write('["err"]'); } } catch (err) { - process.stdout.write(JSON.stringify(['err', '' + err])); + process.stdout.write(JSON.stringify(['err', '' + err, err.stack])); } }); diff --git a/priv/rhino_runner.js.eex b/priv/rhino_runner.js.eex index 603df28..fb709d8 100644 --- a/priv/rhino_runner.js.eex +++ b/priv/rhino_runner.js.eex @@ -1,6 +1,6 @@ -(function(program, runner) { runner(program); })(function() { +(function(program, runner) { runner(program); })(function(Packages,getClass,JavaAdapter,JavaImporter,java,javax,org,com,edu,net,global,defineClass,deserialize,doctest,gc,help,load,loadClass,print,quit,readline,readFile,readUrl,runCommand,seal,serialize,spawn,sync,toint32,version,write,Environment,environment,history,arguments,i,importClass,importPackage) { return <%= source %>; -}, function(program) { +}, function(program, undefined) { var result; try { result = program(); @@ -14,6 +14,6 @@ java.lang.System.out.println('["err"]'); } } catch (err) { - java.lang.System.out.println(JSON.stringify(['err', '' + err])); + java.lang.System.out.println(JSON.stringify(['err', '' + err, err.stack])); } }); diff --git a/priv/spidermonkey_runner.js.eex b/priv/spidermonkey_runner.js.eex index 9bdbc4c..0e0bda4 100644 --- a/priv/spidermonkey_runner.js.eex +++ b/priv/spidermonkey_runner.js.eex @@ -1,6 +1,6 @@ -(function(program, runner) { runner(program); })(function() { +(function(program, runner) { runner(program); })(function(PerfMeasurement,version,revertVersion,options,load,evaluate,run,readline,print,putstr,dateNow,help,quit,assertEq,assertJit,gc,gcparam,countHeap,makeFinalizeObserver,finalizeCount,setDebug,setDebuggerHandler,setThrowHook,trap,untrap,line2pc,pc2line,stackQuota,stringsAreUTF8,testUTF8,throwError,build,clear,intern,clone,getpda,getslx,toint32,evalcx,evalInFrame,shapeOf,resolver,sleep,scatter,snarf,read,compile,parse,timeout,elapsed,parent,wrap,serialize,deserialize,mjitstats,stringstats,newGlobal,it,custom,customRdOnly,environment,Worker) { return <%= source %>; -}, function(program) { +}, function(program, undefined) { var result; try { result = program(); @@ -14,6 +14,6 @@ print('["err"]'); } } catch (err) { - print(JSON.stringify(['err', '' + err])); + print(JSON.stringify(['err', '' + err, err.stack])); } }); diff --git a/priv/v8_runner.js.eex b/priv/v8_runner.js.eex new file mode 100644 index 0000000..125bab8 --- /dev/null +++ b/priv/v8_runner.js.eex @@ -0,0 +1,19 @@ +(function(program, runner) { runner(program); })(function(print,write,read,readbuffer,readline,load,quit,version,Realm,os) { + return <%= source %>; +}, function(program, undefined) { + var result; + try { + result = program(); + try { + if (result === undefined) { + print('["ok"]'); + } else { + print(JSON.stringify(['ok', result])); + } + } catch (err) { + print('["err"]'); + } + } catch (err) { + print(JSON.stringify(['err', '' + err, err.stack])); + } +}); diff --git a/test/execjs_test.exs b/test/execjs_test.exs index 75242ac..5a47dfd 100644 --- a/test/execjs_test.exs +++ b/test/execjs_test.exs @@ -1,5 +1,5 @@ defmodule ExecjsTest do - use ExUnit.Case + use ExUnit.Case, async: true import Execjs @@ -15,11 +15,12 @@ defmodule ExecjsTest do end test "call" do - context = compile ~S""" - function addOne(n) { - return n + 1; - } - """ + context = + compile(~S""" + function addOne(n) { + return n + 1; + } + """) assert call(context, "addOne", [3]) == 4 assert call(context, "addOne", [-3]) == -2 diff --git a/test/test_helper.exs b/test/test_helper.exs index 4b8b246..869559e 100644 --- a/test/test_helper.exs +++ b/test/test_helper.exs @@ -1 +1 @@ -ExUnit.start +ExUnit.start()