diff --git a/.formatter.exs b/.formatter.exs new file mode 100644 index 0000000..d2cda26 --- /dev/null +++ b/.formatter.exs @@ -0,0 +1,4 @@ +# Used by "mix format" +[ + inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"] +] diff --git a/.gitignore b/.gitignore index c56604d..df858fd 100644 --- a/.gitignore +++ b/.gitignore @@ -27,3 +27,6 @@ igniter_js-*.tar # Also ignore archive artifacts (built via "mix archive.build"). *.ez + +# Developer environment files +.DS_Store diff --git a/.iex.exs b/.iex.exs new file mode 100644 index 0000000..128c00a --- /dev/null +++ b/.iex.exs @@ -0,0 +1 @@ +IEx.configure(auto_reload: true) diff --git a/lib/igniter_js.ex b/lib/igniter_js.ex new file mode 100644 index 0000000..5bdc9fd --- /dev/null +++ b/lib/igniter_js.ex @@ -0,0 +1,3 @@ +defmodule IgniterJS do + @moduledoc false +end diff --git a/lib/igniter_js/application.ex b/lib/igniter_js/application.ex new file mode 100644 index 0000000..d5b9af7 --- /dev/null +++ b/lib/igniter_js/application.ex @@ -0,0 +1,20 @@ +defmodule IgniterJS.Application do + # See https://hexdocs.pm/elixir/Application.html + # for more information on OTP Applications + @moduledoc false + + use Application + + @impl true + def start(_type, _args) do + children = [ + # Starts a worker by calling: IgniterJS.Worker.start_link(arg) + # {IgniterJS.Worker, arg} + ] + + # See https://hexdocs.pm/elixir/Supervisor.html + # for other strategies and supported options + opts = [strategy: :one_for_one, name: IgniterJS.Supervisor] + Supervisor.start_link(children, opts) + end +end diff --git a/lib/igniter_js/helpers.ex b/lib/igniter_js/helpers.ex new file mode 100644 index 0000000..511c86d --- /dev/null +++ b/lib/igniter_js/helpers.ex @@ -0,0 +1,72 @@ +defmodule IgniterJS.Helpers do + @moduledoc """ + A module that contains helper functions for IgniterJS. For example it helps to normalize the + output of the NIFs, read and validate the file, and call the NIF function with the + given file path or content. + """ + + @doc """ + Normalize the output of the NIFs. It is a macro and returns a tuple with the first + element as the output, the second element as the caller function name, and the + third element as the status. + + ```elixir + require IgniterJS.Helpers + normalize_output({:ok, :fun_atom, result}, __ENV__.function) + normalize_output({:error, :fun_atom, result}, __ENV__.function) + ``` + """ + defmacro normalize_output(output, caller_function) do + quote do + {elem(unquote(output), 0), elem(unquote(caller_function), 0), elem(unquote(output), 2)} + end + end + + @doc """ + Read and validate the file. It returns the file content if the file exists and the + extension is `.js` or `.ts`, otherwise, it returns an error tuple. + + ```elixir + read_and_validate_file("/path/to/file.js") + ``` + """ + def read_and_validate_file(file_path) do + with true <- File.exists?(file_path), + true <- Path.extname(file_path) in [".js", ".ts"], + {:ok, file_content} <- File.read(file_path) do + {:ok, file_content} + else + {:error, reason} -> {:error, reason} + _ -> {:error, "Invalid file path or format."} + end + end + + @doc """ + Call the NIF function with the given file path or content and return the result. + It helps to change the function name as atom based on its caller function. + + ```elixir + call_nif_fn("/path/to/file.js", __ENV__.function, fn content -> content end, :path) + call_nif_fn("file content", __ENV__.function, fn content -> content end) + call_nif_fn("file content", __ENV__.function, fn content -> content end, :content) + ``` + """ + def call_nif_fn(file_path, caller_function, processing_fn, type \\ :content) + + def call_nif_fn(file_content, caller_function, processing_fn, :content) do + processing_fn.(file_content) + |> normalize_output(caller_function) + end + + def call_nif_fn(file_path, caller_function, processing_fn, :path) do + case read_and_validate_file(file_path) do + {:ok, file_content} -> + processing_fn.(file_content) + |> normalize_output(caller_function) + + reason -> + Tuple.insert_at(reason, 1, :none) + |> normalize_output(caller_function) + end + end +end diff --git a/lib/igniter_js/native.ex b/lib/igniter_js/native.ex new file mode 100644 index 0000000..5aa860b --- /dev/null +++ b/lib/igniter_js/native.ex @@ -0,0 +1,19 @@ +defmodule IgniterJS.Native do + @moduledoc false + use Rustler, otp_app: :igniter_js, crate: "igniter_js" + + # When your NIF is loaded, it will override this function. + def is_module_imported_from_ast_nif(_file_content, _module_name), do: error() + + def insert_import_to_ast_nif(_file_content, _import_lines), do: error() + + def remove_import_from_ast_nif(_file_content, _modules), do: error() + + def find_live_socket_node_from_ast_nif(_file_content), do: error() + + def extend_hook_object_to_ast_nif(_file_content, _names), do: error() + + def remove_objects_of_hooks_from_ast_nif(_file_content, _object_names), do: error() + + defp error(), do: :erlang.nif_error(:nif_not_loaded) +end diff --git a/lib/igniter_js/parsers/javascript/parser.ex b/lib/igniter_js/parsers/javascript/parser.ex new file mode 100644 index 0000000..71b1268 --- /dev/null +++ b/lib/igniter_js/parsers/javascript/parser.ex @@ -0,0 +1,183 @@ +defmodule IgniterJS.Parsers.Javascript.Parser do + alias IgniterJS.Native + import IgniterJS.Helpers, only: [call_nif_fn: 4] + + @doc """ + Check if a module is imported in the given file or content or content and returns boolean. + + ```elixir + alias IgniterJS.Parsers.Javascript.Parser + Parser.module_imported?(js_content, "module") + Parser.module_imported?(js_content, "module", :content) + Parser.module_imported?("/path/to/file.js", "module", :path) + ``` + """ + def module_imported?(file_path_or_content, module, type \\ :content) do + elem(module_imported(file_path_or_content, module, type), 0) == :ok + end + + @doc """ + Check if a module is imported in the given file or content or contents and return tuple. + + ```elixir + alias IgniterJS.Parsers.Javascript.Parser + Parser.module_imported(js_content, "module") + Parser.module_imported(js_content, "module", :content) + Parser.module_imported("/path/to/file.js", "module", :path) + ``` + """ + def module_imported(file_path_or_content, module, type \\ :content) do + call_nif_fn( + file_path_or_content, + __ENV__.function, + fn file_content -> + Native.is_module_imported_from_ast_nif(file_content, module) + end, + type + ) + end + + @doc """ + Insert imports to the given file or content and returns tuple. + + ```elixir + alias IgniterJS.Parsers.Javascript.Parser + Parser.insert_imports(js_content, imports_lines) + Parser.insert_imports(js_content, imports_lines, :content) + Parser.insert_imports("/path/to/file.js", imports_lines, :path) + ``` + """ + def insert_imports(file_path_or_content, imports_lines, type \\ :content) do + call_nif_fn( + file_path_or_content, + __ENV__.function, + fn file_content -> + Native.insert_import_to_ast_nif(file_content, imports_lines) + end, + type + ) + end + + @doc """ + Remove imports from the given file or content. it accepts a single module or a list of modules. + It returns a tuple. + + ```elixir + alias IgniterJS.Parsers.Javascript.Parser + Parser.remove_imports(js_content, "SomeModule") + Parser.remove_imports(js_content, ["SomeModule", "AnotherModule"], :content) + Parser.remove_imports("/path/to/file.js", "SomeModule", :path) + ``` + """ + def remove_imports(file_path_or_content, module, type \\ :content) + + def remove_imports(file_path_or_content, module, type) when is_binary(module) do + remove_imports(file_path_or_content, [module], type) + end + + def remove_imports(file_path_or_content, modules, type) when is_list(modules) do + call_nif_fn( + file_path_or_content, + __ENV__.function, + fn file_content -> + Native.remove_import_from_ast_nif(file_content, modules) + end, + type + ) + end + + @doc """ + Check if a LiveSocket var exists in the given file or content and returns boolean. + + ```elixir + alias IgniterJS.Parsers.Javascript.Parser + Parser.exist_live_socket?(js_content) + Parser.exist_live_socket?(js_content, :content) + Parser.exist_live_socket?("/path/to/file.js", :path) + ``` + """ + def exist_live_socket?(file_path_or_content, type \\ :content) do + elem(exist_live_socket(file_path_or_content, type), 0) == :ok + end + + @doc """ + Check if a LiveSocket var exists in the given file or content and returns tuple. + + ```elixir + alias IgniterJS.Parsers.Javascript.Parser + Parser.exist_live_socket(js_content) + Parser.exist_live_socket(js_content, :content) + Parser.exist_live_socket("/path/to/file.js", :path) + ``` + """ + def exist_live_socket(file_path_or_content, type \\ :content) do + call_nif_fn( + file_path_or_content, + __ENV__.function, + fn file_content -> + Native.find_live_socket_node_from_ast_nif(file_content) + end, + type + ) + end + + @doc """ + Extend the hook object in the given file or content. It accepts a single object + or a list of objects. + It returns a tuple. + + ```elixir + alias IgniterJS.Parsers.Javascript.Parser + Parser.extend_hook_object(js_content, "SomeObject") + Parser.extend_hook_object(js_content, ["SomeObject", "AnotherObject"], :content) + Parser.extend_hook_object("/path/to/file.js", "SomeObject", :path) + ``` + """ + def extend_hook_object(file_path_or_content, object_name, type \\ :content) + + def extend_hook_object(file_path_or_content, object_name, type) when is_binary(object_name) do + extend_hook_object(file_path_or_content, [object_name], type) + end + + def extend_hook_object(file_path_or_content, objects_names, type) when is_list(objects_names) do + call_nif_fn( + file_path_or_content, + __ENV__.function, + fn file_content -> + Native.extend_hook_object_to_ast_nif(file_content, objects_names) + end, + type + ) + end + + @doc """ + Remove objects from the hooks in the given file or content. It accepts a single o + bject or a list of objects. + It returns a tuple. + + ```elixir + alias IgniterJS.Parsers.Javascript.Parser + Parser.remove_objects_from_hooks(js_content, "SomeObject") + Parser.remove_objects_from_hooks(js_content, ["SomeObject", "AnotherObject"], :content) + Parser.remove_objects_from_hooks("/path/to/file.js", "SomeObject", :path) + ``` + """ + def remove_objects_from_hooks(file_path_or_content, object_name, type \\ :content) + + def remove_objects_from_hooks(file_path_or_content, object_name, type) + when is_binary(object_name) do + remove_objects_from_hooks(file_path_or_content, [object_name], type) + end + + def remove_objects_from_hooks(file_path_or_content, objects_names, type) + when is_list(objects_names) do + call_nif_fn( + file_path_or_content, + __ENV__.function, + fn file_content -> + Native.remove_objects_of_hooks_from_ast_nif(file_content, objects_names) + end, + type + ) + end +end diff --git a/mix.exs b/mix.exs new file mode 100644 index 0000000..eeaee36 --- /dev/null +++ b/mix.exs @@ -0,0 +1,57 @@ +defmodule IgniterJS.MixProject do + use Mix.Project + @version "0.0.1" + @source_url "https://github.com/ash-project/igniter_js" + + def project do + [ + app: :igniter_js, + version: @version, + elixir: "~> 1.17", + name: "IgniterJS", + elixirc_paths: elixirc_paths(Mix.env()), + start_permanent: Mix.env() == :prod, + deps: deps(), + description: description(), + package: package(), + source_url: @source_url + ] + end + + defp description() do + "Codemods for JavaScript in Elixir, powered by a high-performance Rust parser integrated via NIFs" + end + + defp package() do + [ + files: ~w(lib .formatter.exs mix.exs LICENSE README*), + licenses: ["MIT"], + links: %{ + "GitHub" => @source_url, + "Discord" => "https://discord.gg/HTHRaaVPUc", + "Website" => "https://ash-hq.org", + "Forum" => "https://elixirforum.com/c/ash-framework-forum/", + "Changelog" => "#{@source_url}/blob/main/CHANGELOG.md" + } + ] + end + + defp elixirc_paths(:test), do: ["lib", "test/support"] + defp elixirc_paths(_), do: ["lib"] + + # Run "mix help compile.app" to learn about applications. + def application do + [ + extra_applications: [:logger], + mod: {IgniterJS.Application, []} + ] + end + + # Run "mix help deps" to learn about dependencies. + defp deps do + [ + {:rustler, "~> 0.35.1"}, + {:ex_doc, "~> 0.35.1", only: [:dev, :test], runtime: false} + ] + end +end diff --git a/mix.lock b/mix.lock new file mode 100644 index 0000000..8f87b48 --- /dev/null +++ b/mix.lock @@ -0,0 +1,19 @@ +%{ + "earmark_parser": {:hex, :earmark_parser, "1.4.42", "f23d856f41919f17cd06a493923a722d87a2d684f143a1e663c04a2b93100682", [:mix], [], "hexpm", "6915b6ca369b5f7346636a2f41c6a6d78b5af419d61a611079189233358b8b8b"}, + "ex_doc": {:hex, :ex_doc, "0.35.1", "de804c590d3df2d9d5b8aec77d758b00c814b356119b3d4455e4b8a8687aecaf", [:mix], [{:earmark_parser, "~> 1.4.39", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_c, ">= 0.1.0", [hex: :makeup_c, repo: "hexpm", optional: true]}, {:makeup_elixir, "~> 0.14 or ~> 1.0", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1 or ~> 1.0", [hex: :makeup_erlang, repo: "hexpm", optional: false]}, {:makeup_html, ">= 0.1.0", [hex: :makeup_html, repo: "hexpm", optional: true]}], "hexpm", "2121c6402c8d44b05622677b761371a759143b958c6c19f6558ff64d0aed40df"}, + "finch": {:hex, :finch, "0.19.0", "c644641491ea854fc5c1bbaef36bfc764e3f08e7185e1f084e35e0672241b76d", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.6.2 or ~> 1.7", [hex: :mint, repo: "hexpm", optional: false]}, {:nimble_options, "~> 0.4 or ~> 1.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:nimble_pool, "~> 1.1", [hex: :nimble_pool, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "fc5324ce209125d1e2fa0fcd2634601c52a787aff1cd33ee833664a5af4ea2b6"}, + "hpax": {:hex, :hpax, "1.0.2", "762df951b0c399ff67cc57c3995ec3cf46d696e41f0bba17da0518d94acd4aac", [:mix], [], "hexpm", "2f09b4c1074e0abd846747329eaa26d535be0eb3d189fa69d812bfb8bfefd32f"}, + "jason": {:hex, :jason, "1.4.4", "b9226785a9aa77b6857ca22832cffa5d5011a667207eb2a0ad56adb5db443b8a", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "c5eb0cab91f094599f94d55bc63409236a8ec69a21a67814529e8d5f6cc90b3b"}, + "makeup": {:hex, :makeup, "1.2.1", "e90ac1c65589ef354378def3ba19d401e739ee7ee06fb47f94c687016e3713d1", [:mix], [{:nimble_parsec, "~> 1.4", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "d36484867b0bae0fea568d10131197a4c2e47056a6fbe84922bf6ba71c8d17ce"}, + "makeup_elixir": {:hex, :makeup_elixir, "1.0.1", "e928a4f984e795e41e3abd27bfc09f51db16ab8ba1aebdba2b3a575437efafc2", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2.3 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "7284900d412a3e5cfd97fdaed4f5ed389b8f2b4cb49efc0eb3bd10e2febf9507"}, + "makeup_erlang": {:hex, :makeup_erlang, "1.0.1", "c7f58c120b2b5aa5fd80d540a89fdf866ed42f1f3994e4fe189abebeab610839", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "8a89a1eeccc2d798d6ea15496a6e4870b75e014d1af514b1b71fa33134f57814"}, + "mime": {:hex, :mime, "2.0.6", "8f18486773d9b15f95f4f4f1e39b710045fa1de891fada4516559967276e4dc2", [:mix], [], "hexpm", "c9945363a6b26d747389aac3643f8e0e09d30499a138ad64fe8fd1d13d9b153e"}, + "mint": {:hex, :mint, "1.6.2", "af6d97a4051eee4f05b5500671d47c3a67dac7386045d87a904126fd4bbcea2e", [:mix], [{:castore, "~> 0.1.0 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:hpax, "~> 0.1.1 or ~> 0.2.0 or ~> 1.0", [hex: :hpax, repo: "hexpm", optional: false]}], "hexpm", "5ee441dffc1892f1ae59127f74afe8fd82fda6587794278d924e4d90ea3d63f9"}, + "nimble_options": {:hex, :nimble_options, "1.1.1", "e3a492d54d85fc3fd7c5baf411d9d2852922f66e69476317787a7b2bb000a61b", [:mix], [], "hexpm", "821b2470ca9442c4b6984882fe9bb0389371b8ddec4d45a9504f00a66f650b44"}, + "nimble_parsec": {:hex, :nimble_parsec, "1.4.0", "51f9b613ea62cfa97b25ccc2c1b4216e81df970acd8e16e8d1bdc58fef21370d", [:mix], [], "hexpm", "9c565862810fb383e9838c1dd2d7d2c437b3d13b267414ba6af33e50d2d1cf28"}, + "nimble_pool": {:hex, :nimble_pool, "1.1.0", "bf9c29fbdcba3564a8b800d1eeb5a3c58f36e1e11d7b7fb2e084a643f645f06b", [:mix], [], "hexpm", "af2e4e6b34197db81f7aad230c1118eac993acc0dae6bc83bac0126d4ae0813a"}, + "req": {:hex, :req, "0.5.8", "50d8d65279d6e343a5e46980ac2a70e97136182950833a1968b371e753f6a662", [:mix], [{:brotli, "~> 0.3.1", [hex: :brotli, repo: "hexpm", optional: true]}, {:ezstd, "~> 1.0", [hex: :ezstd, repo: "hexpm", optional: true]}, {:finch, "~> 0.17", [hex: :finch, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mime, "~> 2.0.6 or ~> 2.1", [hex: :mime, repo: "hexpm", optional: false]}, {:nimble_csv, "~> 1.0", [hex: :nimble_csv, repo: "hexpm", optional: true]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "d7fc5898a566477e174f26887821a3c5082b243885520ee4b45555f5d53f40ef"}, + "rustler": {:hex, :rustler, "0.35.1", "ec81961ef9ee833d721dafb4449cab29b16b969a3063a842bb9e3ea912f6b938", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:req, "~> 0.5", [hex: :req, repo: "hexpm", optional: false]}, {:toml, "~> 0.6", [hex: :toml, repo: "hexpm", optional: false]}], "hexpm", "3713b2e70e68ec2bfa8291dfd9cb811fe64a770f254cd9c331f8b34fa7989115"}, + "telemetry": {:hex, :telemetry, "1.3.0", "fedebbae410d715cf8e7062c96a1ef32ec22e764197f70cda73d82778d61e7a2", [:rebar3], [], "hexpm", "7015fc8919dbe63764f4b4b87a95b7c0996bd539e0d499be6ec9d7f3875b79e6"}, + "toml": {:hex, :toml, "0.7.0", "fbcd773caa937d0c7a02c301a1feea25612720ac3fa1ccb8bfd9d30d822911de", [:mix], [], "hexpm", "0690246a2478c1defd100b0c9b89b4ea280a22be9a7b313a8a058a2408a2fa70"}, +} diff --git a/native/igniter_js/.cargo/config.toml b/native/igniter_js/.cargo/config.toml new file mode 100644 index 0000000..20f03f3 --- /dev/null +++ b/native/igniter_js/.cargo/config.toml @@ -0,0 +1,5 @@ +[target.'cfg(target_os = "macos")'] +rustflags = [ + "-C", "link-arg=-undefined", + "-C", "link-arg=dynamic_lookup", +] diff --git a/native/igniter_js/.gitignore b/native/igniter_js/.gitignore new file mode 100644 index 0000000..ea8c4bf --- /dev/null +++ b/native/igniter_js/.gitignore @@ -0,0 +1 @@ +/target diff --git a/native/igniter_js/Cargo.lock b/native/igniter_js/Cargo.lock new file mode 100644 index 0000000..0fad5f9 --- /dev/null +++ b/native/igniter_js/Cargo.lock @@ -0,0 +1,805 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "aho-corasick" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +dependencies = [ + "memchr", +] + +[[package]] +name = "allocator-api2" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" + +[[package]] +name = "assert-unchecked" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7330592adf847ee2e3513587b4db2db410a0d751378654e7e993d9adcbe5c795" + +[[package]] +name = "autocfg" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" + +[[package]] +name = "base64-simd" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "339abbe78e73178762e23bea9dfd08e697eb3f3301cd4be981c0f78ba5859195" +dependencies = [ + "outref", + "vsimd", +] + +[[package]] +name = "bitflags" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" + +[[package]] +name = "bumpalo" +version = "3.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" +dependencies = [ + "allocator-api2", +] + +[[package]] +name = "castaway" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0abae9be0aaf9ea96a3b1b8b1b55c602ca751eba1b1500220cea4ecbafe7c0d5" +dependencies = [ + "rustversion", +] + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "compact_str" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6050c3a16ddab2e412160b31f2c871015704239bca62f72f6e5f0be631d3f644" +dependencies = [ + "castaway", + "cfg-if", + "itoa", + "rustversion", + "ryu", + "static_assertions", +] + +[[package]] +name = "cow-utils" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "417bef24afe1460300965a25ff4a24b8b45ad011948302ec221e8a0a81eb2c79" + +[[package]] +name = "either" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" + +[[package]] +name = "equivalent" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" + +[[package]] +name = "fixedbitset" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80" + +[[package]] +name = "hashbrown" +version = "0.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "igniter_js" +version = "0.0.1" +dependencies = [ + "oxc", + "rustler", +] + +[[package]] +name = "indexmap" +version = "2.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62f822373a4fe84d4bb149bf54e584a7f4abec90e072ed49cda0edea5b95471f" +dependencies = [ + "equivalent", + "hashbrown", +] + +[[package]] +name = "inventory" +version = "0.3.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5d80fade88dd420ce0d9ab6f7c58ef2272dde38db874657950f827d4982c817" +dependencies = [ + "rustversion", +] + +[[package]] +name = "itertools" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d75a2a4b1b190afb6f5425f10f6a8f959d2ea0b9c2b1d79553551850539e4674" + +[[package]] +name = "memchr" +version = "2.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" + +[[package]] +name = "nonmax" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "610a5acd306ec67f907abe5567859a3c693fb9886eb1f012ab8f2a47bef3db51" + +[[package]] +name = "num-bigint" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" +dependencies = [ + "num-integer", + "num-traits", +] + +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "outref" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4030760ffd992bef45b0ae3f10ce1aba99e33464c90d14dd7c039884963ddc7a" + +[[package]] +name = "owo-colors" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb37767f6569cd834a413442455e0f066d0d522de8630436e2a1761d9726ba56" + +[[package]] +name = "oxc" +version = "0.42.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6bf8f5dfcc7c7b252121efd0c0009ccc8fe15c7c0ed78bbaa3da7c1b9f38283b" +dependencies = [ + "oxc_allocator", + "oxc_ast", + "oxc_codegen", + "oxc_diagnostics", + "oxc_parser", + "oxc_regular_expression", + "oxc_span", + "oxc_syntax", +] + +[[package]] +name = "oxc-miette" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e03e63fd113c068b82d07c9c614b0b146c08a3ac0a4dface3ea1d1a9d14d549e" +dependencies = [ + "cfg-if", + "owo-colors", + "oxc-miette-derive", + "textwrap", + "thiserror", + "unicode-width 0.2.0", +] + +[[package]] +name = "oxc-miette-derive" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e21f680e8c5f1900297d394627d495351b9e37761f7bbf90116bd5eeb6e80967" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "oxc_allocator" +version = "0.42.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38c19205a570ffe6b797638180713bfb7c87e5fb0e3764c82632b6bed63e10a9" +dependencies = [ + "allocator-api2", + "bumpalo", +] + +[[package]] +name = "oxc_ast" +version = "0.42.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f09e7b4d18228b53adff6bdb94ff287ffe557d00f401e06c7c5f66238682062" +dependencies = [ + "bitflags", + "cow-utils", + "num-bigint", + "num-traits", + "oxc_allocator", + "oxc_ast_macros", + "oxc_estree", + "oxc_regular_expression", + "oxc_span", + "oxc_syntax", +] + +[[package]] +name = "oxc_ast_macros" +version = "0.42.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82148b5e788cf9ea20492c7be3579a0372a156535c2ac0b8b0aa67651a2474da" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "oxc_cfg" +version = "0.42.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830d4a6e2eb93abc09bc639274a43ff93b8f652634e4e7ee4e943b08268f1600" +dependencies = [ + "bitflags", + "itertools", + "nonmax", + "oxc_index", + "oxc_syntax", + "petgraph", + "rustc-hash", +] + +[[package]] +name = "oxc_codegen" +version = "0.42.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64f2ace1d346a24b63e9bea7cbdd73d15ae8e28d5f6af0e8780dfb6b25bb92a3" +dependencies = [ + "assert-unchecked", + "bitflags", + "cow-utils", + "nonmax", + "oxc_allocator", + "oxc_ast", + "oxc_index", + "oxc_mangler", + "oxc_sourcemap", + "oxc_span", + "oxc_syntax", + "rustc-hash", +] + +[[package]] +name = "oxc_diagnostics" +version = "0.42.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d69607b06cc5d4e54eabe5f72e924dab326943d920b8f25870c324325305f984" +dependencies = [ + "oxc-miette", + "rustc-hash", +] + +[[package]] +name = "oxc_ecmascript" +version = "0.42.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fc78c3cad8742f9db34be10997306a67b719ef809cd151245db5d5493d36a31" +dependencies = [ + "num-bigint", + "num-traits", + "oxc_ast", + "oxc_span", + "oxc_syntax", +] + +[[package]] +name = "oxc_estree" +version = "0.42.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57dfaf4522aed562130ad29789149f8844008dce53f7a754031d631a3c7a9d5b" + +[[package]] +name = "oxc_index" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5eca5d9726cd0a6e433debe003b7bc88b2ecad0bb6109f0cef7c55e692139a34" + +[[package]] +name = "oxc_mangler" +version = "0.42.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20f074ae04248cbb64a8196b7638a0a7062354033d1546dae91267baf4cfb2c7" +dependencies = [ + "itertools", + "oxc_ast", + "oxc_index", + "oxc_semantic", + "oxc_span", +] + +[[package]] +name = "oxc_parser" +version = "0.42.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9ac7ddf81599e98604c51176934549e5bef3944dbd4138c78e2c57cc679300d" +dependencies = [ + "assert-unchecked", + "bitflags", + "cow-utils", + "memchr", + "num-bigint", + "num-traits", + "oxc_allocator", + "oxc_ast", + "oxc_diagnostics", + "oxc_ecmascript", + "oxc_regular_expression", + "oxc_span", + "oxc_syntax", + "rustc-hash", + "seq-macro", +] + +[[package]] +name = "oxc_regular_expression" +version = "0.42.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f5657b1e93c1d568852c07293833acfde0ca7adb298296229280f873e2fb82fe" +dependencies = [ + "oxc_allocator", + "oxc_ast_macros", + "oxc_diagnostics", + "oxc_estree", + "oxc_span", + "phf", + "rustc-hash", + "unicode-id-start", +] + +[[package]] +name = "oxc_semantic" +version = "0.42.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d1b9f2708051b8e6588c6f376e0e61b095263d5b98e94fa1c08bd97213d61ad" +dependencies = [ + "assert-unchecked", + "indexmap", + "itertools", + "oxc_allocator", + "oxc_ast", + "oxc_cfg", + "oxc_diagnostics", + "oxc_ecmascript", + "oxc_index", + "oxc_span", + "oxc_syntax", + "phf", + "rustc-hash", +] + +[[package]] +name = "oxc_sourcemap" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48557f779d04c8bfa8a930db5a3c35c0a86ff4e6bf1552ce446b9596a6e77c42" +dependencies = [ + "base64-simd", + "cfg-if", + "cow-utils", + "rustc-hash", + "serde", + "serde_json", +] + +[[package]] +name = "oxc_span" +version = "0.42.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91f30db7710245b69ea6ac977b4a846144369bc5621a8c8376faa6172dc87eaf" +dependencies = [ + "compact_str", + "oxc-miette", + "oxc_allocator", + "oxc_ast_macros", + "oxc_estree", +] + +[[package]] +name = "oxc_syntax" +version = "0.42.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4bfc39f2110677d0dc40452037eb4349820a6d265ca323a6dfb89595db2490e8" +dependencies = [ + "assert-unchecked", + "bitflags", + "nonmax", + "oxc_allocator", + "oxc_ast_macros", + "oxc_estree", + "oxc_index", + "oxc_span", + "phf", + "rustc-hash", + "ryu-js", + "unicode-id-start", +] + +[[package]] +name = "petgraph" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4c5cc86750666a3ed20bdaf5ca2a0344f9c67674cae0515bec2da16fbaa47db" +dependencies = [ + "fixedbitset", + "indexmap", +] + +[[package]] +name = "phf" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ade2d8b8f33c7333b51bcf0428d37e217e9f32192ae4772156f65063b8ce03dc" +dependencies = [ + "phf_macros", + "phf_shared", +] + +[[package]] +name = "phf_generator" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48e4cc64c2ad9ebe670cb8fd69dd50ae301650392e81c05f9bfcb2d5bdbc24b0" +dependencies = [ + "phf_shared", + "rand", +] + +[[package]] +name = "phf_macros" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3444646e286606587e49f3bcf1679b8cef1dc2c5ecc29ddacaffc305180d464b" +dependencies = [ + "phf_generator", + "phf_shared", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "phf_shared" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90fcb95eef784c2ac79119d1dd819e162b5da872ce6f3c3abe1e8ca1c082f72b" +dependencies = [ + "siphasher", +] + +[[package]] +name = "proc-macro2" +version = "1.0.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37d3544b3f2748c54e147655edb5025752e2303145b5aefb3c3ea2c78b973bb0" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" + +[[package]] +name = "regex" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" + +[[package]] +name = "rustc-hash" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7fb8039b3032c191086b10f11f319a6e99e1e82889c5cc6046f515c9db1d497" + +[[package]] +name = "rustler" +version = "0.34.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e94bdfa68c0388cbd725f1ca54e975956482c262599e5cced04a903eec918b7f" +dependencies = [ + "inventory", + "rustler_codegen", + "rustler_sys", +] + +[[package]] +name = "rustler_codegen" +version = "0.34.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "996dc019acb78b91b4e0c1bd6fa2cd509a835d309de762dc15213b97eac399da" +dependencies = [ + "heck", + "inventory", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "rustler_sys" +version = "2.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd0e2c955cfc86ea4680067e1d5e711427b43f7befcb6e23c7807cf3dd90e97" +dependencies = [ + "regex", + "unreachable", +] + +[[package]] +name = "rustversion" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e819f2bc632f285be6d7cd36e25940d45b2391dd6d9b939e79de557f7014248" + +[[package]] +name = "ryu" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" + +[[package]] +name = "ryu-js" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad97d4ce1560a5e27cec89519dc8300d1aa6035b099821261c651486a19e44d5" + +[[package]] +name = "seq-macro" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3f0bf26fd526d2a95683cd0f87bf103b8539e2ca1ef48ce002d67aad59aa0b4" + +[[package]] +name = "serde" +version = "1.0.216" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b9781016e935a97e8beecf0c933758c97a5520d32930e460142b4cd80c6338e" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.216" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46f859dbbf73865c6627ed570e78961cd3ac92407a2d117204c49232485da55e" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.133" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7fceb2473b9166b2294ef05efcb65a3db80803f0b03ef86a5fc88a2b85ee377" +dependencies = [ + "itoa", + "memchr", + "ryu", + "serde", +] + +[[package]] +name = "siphasher" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d" + +[[package]] +name = "smawk" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7c388c1b5e93756d0c740965c41e8822f866621d41acbdf6336a6a168f8840c" + +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + +[[package]] +name = "syn" +version = "2.0.90" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "919d3b74a5dd0ccd15aeb8f93e7006bd9e14c295087c9896a110f490752bcf31" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "textwrap" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23d434d3f8967a09480fb04132ebe0a3e088c173e6d0ee7897abbdf4eab0f8b9" +dependencies = [ + "smawk", + "unicode-linebreak", + "unicode-width 0.1.14", +] + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "unicode-id-start" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f322b60f6b9736017344fa0635d64be2f458fbc04eef65f6be22976dd1ffd5b" + +[[package]] +name = "unicode-ident" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adb9e6ca4f869e1180728b7950e35922a7fc6397f7b641499e8f3ef06e50dc83" + +[[package]] +name = "unicode-linebreak" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b09c83c3c29d37506a3e260c08c03743a6bb66a9cd432c6934ab501a190571f" + +[[package]] +name = "unicode-width" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" + +[[package]] +name = "unicode-width" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fc81956842c57dac11422a97c3b8195a1ff727f06e85c84ed2e8aa277c9a0fd" + +[[package]] +name = "unreachable" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "382810877fe448991dfc7f0dd6e3ae5d58088fd0ea5e35189655f84e6814fa56" +dependencies = [ + "void", +] + +[[package]] +name = "void" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a02e4885ed3bc0f2de90ea6dd45ebcbb66dacffe03547fadbb0eeae2770887d" + +[[package]] +name = "vsimd" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c3082ca00d5a5ef149bb8b555a72ae84c9c59f7250f013ac822ac2e49b19c64" diff --git a/native/igniter_js/Cargo.toml b/native/igniter_js/Cargo.toml new file mode 100644 index 0000000..eb11995 --- /dev/null +++ b/native/igniter_js/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "igniter_js" +version = "0.0.1" +authors = ["Shahryar Tavakkoli"] +edition = "2021" + +[lib] +name = "igniter_js" +path = "src/lib.rs" +crate-type = ["cdylib"] + +[dependencies] +oxc = { version = "0.42.0", features = ["codegen"] } +rustler = { version = "0.34.0" } diff --git a/native/igniter_js/README.md b/native/igniter_js/README.md new file mode 100644 index 0000000..f71ce87 --- /dev/null +++ b/native/igniter_js/README.md @@ -0,0 +1,20 @@ +# NIF for Elixir.IgniterJS.Native + +## To build the NIF module: + +- Your NIF will now build along with your project. + +## To load the NIF: + +```elixir +defmodule IgniterJS.Native do + use Rustler, otp_app: :igniter_js, crate: "igniter_js" + + # When your NIF is loaded, it will override this function. + def add(_a, _b), do: :erlang.nif_error(:nif_not_loaded) +end +``` + +## Examples + +[This](https://github.com/rusterlium/NifIo) is a complete example of a NIF written in Rust. diff --git a/native/igniter_js/src/atoms.rs b/native/igniter_js/src/atoms.rs new file mode 100644 index 0000000..003bc8d --- /dev/null +++ b/native/igniter_js/src/atoms.rs @@ -0,0 +1,17 @@ +rustler::atoms! { + // Success Atoms + ok, + + // Error Atoms + error, + + // Nif Functions Atoms + source_to_ast_nif, + is_module_imported_from_ast_nif, + insert_import_to_ast_nif, + remove_import_from_ast_nif, + find_live_socket_node_from_ast, + extend_hook_object_to_ast_nif, + remove_objects_of_hooks_from_ast_nif, + // Resource Atoms +} diff --git a/native/igniter_js/src/helpers.rs b/native/igniter_js/src/helpers.rs new file mode 100644 index 0000000..e4738ba --- /dev/null +++ b/native/igniter_js/src/helpers.rs @@ -0,0 +1,56 @@ +//! Helper functions for encoding and formatting responses. +//! +//! This module provides utility functions for encoding consistent responses +//! in Elixir NIFs using Rust. It leverages the Rustler library for seamless +//! integration with the Erlang VM. + +use rustler::{Encoder, Env, NifResult, Term}; + +/// Encodes a response into an Erlang term. +/// +/// This function takes a status, a source, and a message, and encodes them into +/// a tuple format that can be passed to the Erlang/Elixir runtime. +/// +/// # Arguments +/// +/// - `env`: The environment in which the term is created. This is required to interact +/// with the Erlang/Elixir runtime. +/// - `status`: An atom representing the status of the response (e.g., `:ok` or `:error`). +/// - `source`: An atom representing the source or context of the response, which can help +/// identify where the response originated. +/// - `message`: A generic type `T` representing the message or payload of the response. +/// This must implement the `Encoder` trait to allow encoding into an Erlang term. +/// +/// # Returns +/// +/// Returns a `NifResult` containing the encoded tuple `(status, source, message)` +/// as a `Term` that can be sent to the Erlang/Elixir runtime. +/// +/// # Example +/// +/// ```rust +/// use rustler::{Atom, Env, Encoder, NifResult}; +/// +/// fn example(env: Env) -> NifResult { +/// let status = Atom::from_str(env, "ok").unwrap(); +/// let source = Atom::from_str(env, "parser").unwrap(); +/// let message = "Parsing completed successfully"; +/// +/// encode_response(env, status, source, message) +/// } +/// ``` +/// +/// This function is useful for building consistent response formats +/// when integrating Rust code with Elixir applications. + +pub fn encode_response<'a, T>( + env: Env<'a>, + status: rustler::types::atom::Atom, + source: rustler::types::atom::Atom, + message: T, +) -> NifResult> +where + T: Encoder, +{ + Ok((status, source, message).encode(env)) +} diff --git a/native/igniter_js/src/lib.rs b/native/igniter_js/src/lib.rs new file mode 100644 index 0000000..173d6c1 --- /dev/null +++ b/native/igniter_js/src/lib.rs @@ -0,0 +1,7 @@ +pub mod atoms; +pub mod helpers; +pub mod parsers { + pub mod javascript; +} + +rustler::init!("Elixir.IgniterJS.Native"); diff --git a/native/igniter_js/src/parsers/javascript/ast.rs b/native/igniter_js/src/parsers/javascript/ast.rs new file mode 100644 index 0000000..0d3525d --- /dev/null +++ b/native/igniter_js/src/parsers/javascript/ast.rs @@ -0,0 +1,803 @@ +//! Utility functions for manipulating JavaScript Abstract Syntax Trees (ASTs). +//! +//! This module provides various tools for working with JavaScript ASTs, including: +//! - Parsing JavaScript code into an AST. +//! - Modifying AST nodes such as `hooks` objects or import declarations. +//! - Performing queries on the AST, such as checking for specific variable declarations. +//! +//! The module leverages a Rust-based parser and integrates seamlessly with Elixir through NIFs. + +// Based on: +// +// Tasks: +// https://github.com/ash-project/igniter/issues/106 +// https://github.com/mishka-group/mishka_chelekom/issues/181 +// +// My Questions: +// https://users.rust-lang.org/t/122689/ +// https://users.rust-lang.org/t/122507/ +// https://users.rust-lang.org/t/122153/ +// https://users.rust-lang.org/t/121861/ + +use oxc::{ + allocator::{Allocator, Box as OXCBox, Vec as OXCVec}, + ast::ast::{ + Argument, Expression, IdentifierName, IdentifierReference, NewExpression, ObjectExpression, + ObjectProperty, ObjectPropertyKind, Program, PropertyKey, PropertyKind, Statement, + VariableDeclarator, + }, + codegen::Codegen, + parser::{ParseOptions, Parser}, + span::{Atom, SourceType, Span}, +}; + +use std::cell::Cell; + +/// Parses JavaScript source code into an AST. +/// +/// Converts the provided JavaScript source code (`file_content`) into an +/// abstract syntax tree (AST) using the specified `allocator`. +/// +/// # Arguments +/// - `file_content`: The JavaScript source code as a string slice. +/// - `allocator`: A reference to the memory allocator used during parsing. +/// +/// # Returns +/// A `Result` containing the parsed `Program` on success or an error message on failure. +pub fn source_to_ast<'a>( + file_content: &'a str, + allocator: &'a Allocator, +) -> Result, String> { + let source_type = SourceType::default(); + let parser = Parser::new(allocator, file_content, source_type).with_options(ParseOptions { + parse_regular_expression: true, + ..ParseOptions::default() + }); + + let parse_result = parser.parse(); + Ok(parse_result.program) +} + +/// Checks if a specific module is imported in the JavaScript source code. +/// +/// This function parses the given JavaScript source code into an AST +/// and determines if the specified `module_name` is imported. +/// +/// # Arguments +/// - `file_content`: The JavaScript source code as a string slice. +/// - `module_name`: The name of the module to check for imports. +/// - `allocator`: A reference to the memory allocator used during parsing. +/// +/// # Returns +/// A `Result` containing `true` if the module is imported, `false` otherwise, +/// or an error message if parsing fails. +pub fn is_module_imported_from_ast<'a>( + file_content: &str, + module_name: &str, + allocator: &Allocator, +) -> Result { + let program = source_to_ast(file_content, allocator)?; + + for node in program.body { + if let Statement::ImportDeclaration(import_decl) = node { + if import_decl.source.value == module_name { + return Ok(true); + } + } + } + + Ok(false) +} + +/// Inserts new import statements into JavaScript source code. +/// +/// Parses the provided JavaScript source code into an AST, adds the specified +/// `import_lines` as import declarations, and ensures no duplicate imports are added. +/// Returns the updated JavaScript code as a string. +/// +/// # Arguments +/// - `file_content`: The JavaScript source code as a string slice. +/// - `import_lines`: The new import lines to be added, separated by newlines. +/// - `allocator`: A reference to the memory allocator used during parsing. +/// +/// # Returns +/// A `Result` containing the updated JavaScript code as a `String` on success, +/// or an error message if parsing or insertion fails. +/// +/// # Behavior +/// - Ensures duplicate imports are skipped. +/// - Inserts new import statements after existing ones or at the top if none exist. +pub fn insert_import_to_ast<'a>( + file_content: &str, + import_lines: &str, + allocator: &Allocator, +) -> Result { + let mut program = source_to_ast(file_content, allocator)?; + + for import_line in import_lines.lines() { + let import_source_type = SourceType::default(); + let parser = + Parser::new(allocator, import_line, import_source_type).with_options(ParseOptions { + parse_regular_expression: true, + ..ParseOptions::default() + }); + + let parsed_result = parser.parse(); + if let Some(errors) = parsed_result.errors.first() { + return Err(format!("Failed to parse import line: {:?}", errors)); + } + + let new_import = parsed_result + .program + .body + .into_iter() + .find(|node| matches!(node, Statement::ImportDeclaration(_))) + .ok_or_else(|| "No import declaration found in parsed import line".to_string())?; + + if program.body.iter().any(|node| { + matches!( + (node, &new_import), + ( + Statement::ImportDeclaration(existing_import), + Statement::ImportDeclaration(new_import_node) + ) if existing_import.source.value == new_import_node.source.value + ) + }) { + continue; // Skip duplicate imports + } + + let position = program + .body + .iter() + .rposition(|node| matches!(node, Statement::ImportDeclaration(_))) + .map(|index| index + 1) + .unwrap_or(0); + + program.body.insert(position, new_import); + } + + let codegen = Codegen::new(); + let generated_code = codegen.build(&program).code; + + Ok(generated_code) +} + +/// Removes specified import statements from JavaScript source code. +/// +/// Parses the given JavaScript source code into an AST, locates the specified +/// modules in the `modules` iterator, and removes their corresponding import +/// declarations. Returns the updated JavaScript code as a string. +/// +/// # Arguments +/// - `file_content`: The JavaScript source code as a string slice. +/// - `modules`: An iterable collection of module names (as strings) to be removed. +/// - `allocator`: A reference to the memory allocator used during parsing. +/// +/// # Returns +/// A `Result` containing the updated JavaScript code as a `String` on success, +/// or an error message if parsing fails. +/// +/// # Behavior +/// - Retains all other import statements and code structure. +/// - Removes only the specified modules from the import declarations. +pub fn remove_import_from_ast<'a>( + file_content: &str, + modules: impl IntoIterator>, + allocator: &'a Allocator, +) -> Result { + // Parse the source file into AST + let mut program = source_to_ast(file_content, allocator)?; + + // Find and remove the specified import declaration + let modules: Vec = modules + .into_iter() + .map(|n| n.as_ref().to_string()) + .collect(); + + program.body.retain(|node| { + if let Statement::ImportDeclaration(import_decl) = node { + let source_value = import_decl.source.value.to_string(); + !modules.contains(&source_value) + } else { + true + } + }); + + let codegen = Codegen::new(); + let generated_code = codegen.build(&program).code; + Ok(generated_code) +} + +/// Checks if a `liveSocket` variable is declared in the JavaScript AST. +/// +/// Scans the provided AST to determine if a `liveSocket` variable declaration exists. +/// This function searches through all variable declarations in the program's body. +/// +/// # Arguments +/// - `program`: A reference to the parsed JavaScript AST (`Program`) to search within. +/// +/// # Returns +/// - `Ok(true)`: If a `liveSocket` variable declaration is found. +/// - `Err(false)`: If no such variable declaration exists. +/// +/// # Behavior +/// - Iterates through all `VariableDeclaration` nodes in the AST to check +/// for a variable with the identifier `liveSocket`. +pub fn find_live_socket_node_from_ast<'a>(program: &'a Program<'a>) -> Result { + if program.body.iter().any(|node| { + if let Statement::VariableDeclaration(var_decl) = node { + var_decl.declarations.iter().any(|decl| { + decl.id + .get_identifier() + .map_or(false, |ident| ident == "liveSocket") + }) + } else { + false + } + }) { + Ok(true) + } else { + Err(false) + } +} + +// These are different ways +// names: impl IntoIterator + 'a + ?Sized)>, +// names: impl IntoIterator>, +// under program: let names2: Vec; +// let new_property = create_and_import_object_into_hook(name, allocator); +// obj_expr.properties.push(new_property); + +// use the outer one +// names2 = names.into_iter().map(|n| n.as_ref().to_string()).collect(); +// for name in &names2 { +// let new_property = create_and_import_object_into_hook(name, allocator); +// obj_expr.properties.push(new_property); +// } + +/// Extends the `hooks` object in the JavaScript AST by adding new properties. +/// +/// This function parses the given JavaScript source code, checks for the existence +/// of a `liveSocket` variable, and adds new properties to the `hooks` object. +/// If the `hooks` object or `liveSocket` variable is not found, it initializes +/// or returns an appropriate error. +/// +/// # Arguments +/// - `file_content`: The JavaScript source code as a string slice. +/// - `names`: An iterable collection of property names to be added to the `hooks` object. +/// - `allocator`: A reference to the memory allocator used during parsing. +/// +/// # Returns +/// A `Result` containing the updated JavaScript code as a `String` on success, +/// or an error message if parsing or manipulation fails. +/// +/// # Behavior +/// - Checks for the presence of `liveSocket` in the AST. +/// - Finds or initializes the `hooks` object in the AST. +/// - Adds new properties to the `hooks` object without duplicating existing ones. +/// +/// # Errors +/// - Returns an error if `liveSocket` is not found in the source code. +/// - Returns an error if the required `hooks` object properties cannot be located or created. +pub fn extend_hook_object_to_ast<'a>( + file_content: &str, + names: impl IntoIterator, + allocator: &Allocator, +) -> Result { + let mut program = source_to_ast(file_content, allocator)?; + + if find_live_socket_node_from_ast(&program).is_err() { + return Err("liveSocket not found.".to_string()); + } + + let maybe_properties = get_properties(&mut program.body); + if let Some(properties) = maybe_properties { + let hooks_property = match find_hooks_property(properties) { + Some(prop) => prop, + None => { + let new_hooks_property = create_init_hooks(allocator); + properties.push(new_hooks_property); + get_property_by_key(properties.last_mut().unwrap(), "hooks").unwrap() + } + }; + + if let Expression::ObjectExpression(obj_expr) = hooks_property { + for name in names { + let new_property = create_and_import_object_into_hook(name.as_ref(), allocator); + obj_expr.properties.push(new_property) + } + } + } else { + return Err("properties not found in the AST.".to_string()); + } + + let codegen = Codegen::new(); + let generated_code = codegen.build(&program).code; + Ok(generated_code) +} + +/// Removes specified objects from the `hooks` object in the JavaScript AST. +/// +/// This function parses the given JavaScript source code, checks for the presence of a +/// `liveSocket` variable, and removes specified properties from the `hooks` object. +/// If the `hooks` object or `liveSocket` variable is not found, an appropriate error is returned. +/// +/// # Arguments +/// - `file_content`: The JavaScript source code as a string slice. +/// - `object_names`: An iterable collection of object names (as strings) to be removed from the `hooks` object. +/// - `allocator`: A reference to the memory allocator used during parsing. +/// +/// # Returns +/// A `Result` containing the updated JavaScript code as a `String` on success, +/// or an error message if parsing or manipulation fails. +/// +/// # Behavior +/// - Ensures the `liveSocket` variable exists in the AST. +/// - Locates the `hooks` object or initializes it if absent. +/// - Removes specified properties from the `hooks` object while retaining all others. +/// +/// # Errors +/// - Returns an error if `liveSocket` is not found in the source code. +/// - Returns an error if the `hooks` object properties cannot be located in the AST. +pub fn remove_objects_of_hooks_from_ast( + file_content: &str, + object_names: impl IntoIterator>, + allocator: &Allocator, +) -> Result { + let mut program = source_to_ast(file_content, allocator)?; + + if find_live_socket_node_from_ast(&program).is_err() { + return Err("liveSocket not found.".to_string()); + } + + let maybe_properties = get_properties(&mut program.body); + if let Some(properties) = maybe_properties { + let hooks_property = match find_hooks_property(properties) { + Some(prop) => prop, + None => { + let new_hooks_property = create_init_hooks(allocator); + + properties.push(new_hooks_property); + get_property_by_key(properties.last_mut().unwrap(), "hooks").unwrap() + } + }; + + if let Expression::ObjectExpression(hooks_obj) = hooks_property { + let object_names: Vec = object_names + .into_iter() + .map(|n| n.as_ref().to_string()) + .collect(); + hooks_obj.properties.retain(|property| { + match property { + ObjectPropertyKind::ObjectProperty(prop) => { + !matches!(&prop.key, PropertyKey::StaticIdentifier(key) if object_names.iter().any(|name| name == key.name.as_str())) + } + _ => true, + } + }); + } + } else { + return Err("properties not found in the AST.".to_string()); + } + + let codegen = Codegen::new(); + let generated_code = codegen.build(&program).code; + Ok(generated_code) +} + +fn get_properties<'short, 'long>( + body: &'short mut OXCVec<'long, Statement<'long>>, +) -> Option<&'short mut OXCVec<'long, ObjectPropertyKind<'long>>> { + body.iter_mut().find_map(|node| match node { + Statement::VariableDeclaration(var_decl) => { + var_decl.declarations.iter_mut().find_map(|decl| { + let obj_expr = get_new_expression(decl)?; + obj_expr.arguments.iter_mut().find_map(|arg| { + let obj_expr_inner = get_object_expression(arg)?; + Some(&mut obj_expr_inner.properties) + }) + }) + } + _ => None, + }) +} + +fn find_hooks_property<'short, 'long>( + properties: &'short mut OXCVec<'long, ObjectPropertyKind<'long>>, +) -> Option<&'short mut Expression<'long>> { + properties + .iter_mut() + .find_map(|prop| get_property_by_key(prop, "hooks")) +} + +fn create_and_import_object_into_hook<'a>( + name: &'a str, + allocator: &Allocator, +) -> ObjectPropertyKind<'a> { + ObjectPropertyKind::ObjectProperty(OXCBox::new_in( + ObjectProperty { + span: Span::default(), + kind: PropertyKind::Init, + key: PropertyKey::StaticIdentifier(OXCBox::new_in( + IdentifierName { + span: Span::default(), + name: Atom::from(name), + }, + allocator, + )), + value: Expression::Identifier(OXCBox::new_in( + IdentifierReference { + span: Span::default(), + name: Atom::from(name), + reference_id: Cell::new(None), + }, + allocator, + )), + method: false, + shorthand: true, + computed: false, + }, + allocator, + )) +} + +fn create_init_hooks(allocator: &Allocator) -> ObjectPropertyKind { + ObjectPropertyKind::ObjectProperty(OXCBox::new_in( + ObjectProperty { + span: Span::default(), + kind: PropertyKind::Init, + key: PropertyKey::StaticIdentifier(OXCBox::new_in( + IdentifierName { + span: Span::default(), + name: Atom::from("hooks"), + }, + allocator, + )), + value: Expression::ObjectExpression(OXCBox::new_in( + ObjectExpression { + span: Span::default(), + properties: OXCVec::new_in(allocator), + trailing_comma: None, + }, + allocator, + )), + method: false, + shorthand: false, + computed: false, + }, + allocator, + )) +} + +fn get_new_expression<'short, 'long>( + decl: &'short mut VariableDeclarator<'long>, +) -> Option<&'short mut NewExpression<'long>> { + match decl.init.as_mut()? { + Expression::NewExpression(expr) => Some(expr), + _ => None, + } +} + +fn get_object_expression<'short, 'long>( + arg: &'short mut Argument<'long>, +) -> Option<&'short mut ObjectExpression<'long>> { + arg.as_expression_mut().and_then(|expr| match expr { + Expression::ObjectExpression(boxed_obj_expr) => Some(boxed_obj_expr.as_mut()), + _ => None, + }) +} + +fn get_property_by_key<'short, 'long>( + property: &'short mut ObjectPropertyKind<'long>, + key_name: &str, +) -> Option<&'short mut Expression<'long>> { + match property { + ObjectPropertyKind::ObjectProperty(prop) => match &prop.key { + PropertyKey::StaticIdentifier(key) if key.as_ref().name == key_name => { + Some(&mut prop.value) + } + _ => None, + }, + _ => None, + } +} + +#[cfg(test)] +mod tests { + use super::*; + // use std::path::Path; + + fn create_allocator<'a>() -> &'a Allocator { + let allocator = Box::new(Allocator::default()); + Box::leak(allocator) + } + + #[test] + fn test_parse_and_display_ast() { + let js_content = r#" + import { foo } from 'module-name'; + import bar from 'another-module'; + + console.log('Testing AST parsing'); + "#; + + // Test the AST parsing + let allocator = create_allocator(); + + match source_to_ast(js_content, allocator) { + Ok(ast) => { + println!("{:#?}", ast.body); + assert!(!ast.body.is_empty(), "AST body should not be empty"); + } + Err(e) => panic!("Error while parsing AST: {}", e), + } + } + + #[test] + fn test_is_module_imported_from_ast() { + // Write a test JavaScript file + let js_content = r#" + import { foo } from 'module-name'; + import bar from 'another-module'; + "#; + + // Test the function with a valid module + let allocator = create_allocator(); + match is_module_imported_from_ast(js_content, "module-name", allocator) { + Ok(true) => println!("Module 'module-name' is imported as expected."), + Ok(false) => panic!("Module 'module-name' should be imported but was not detected."), + Err(e) => panic!("Error while checking module: {}", e), + } + + // Test the function with another valid module + match is_module_imported_from_ast(js_content, "another-module", allocator) { + Ok(true) => println!("Module 'another-module' is imported as expected."), + Ok(false) => panic!("Module 'another-module' should be imported but was not detected."), + Err(e) => panic!("Error while checking module: {}", e), + } + + // Test the function with a non-existent module + match is_module_imported_from_ast(js_content, "non-existent-module", allocator) { + Ok(true) => panic!("Module 'non-existent-module' should not be imported."), + Ok(false) => println!("Module 'non-existent-module' is correctly not imported."), + Err(e) => panic!("Error while checking module: {}", e), + } + } + + #[test] + fn test_insert_duplicate_import() { + let js_content = r#" + import { foo } from "module-name"; + console.log("Duplicate import test"); + "#; + + let duplicate_import = r#"import { foo } from "module-name";"#; + let allocator = create_allocator(); + let result = insert_import_to_ast(js_content, duplicate_import, allocator); + + match result { + Ok(updated_content) => { + println!("Updated Content:\n{}", updated_content); + // Ensure the duplicate import is not added + let import_count = updated_content.matches(duplicate_import).count(); + assert_eq!( + import_count, 1, + "Duplicate import should not be added, but it was found multiple times" + ); + } + Err(e) => panic!("Unexpected error: {}", e), + } + } + + #[test] + fn test_insert_import_to_ast_with_existing_imports() { + let js_content = r#" + import bar from "another-module"; + console.log("Some imports here!"); + "#; + + let new_import = r#"import { foo } from "module-name";"#; + let allocator = create_allocator(); + let result = insert_import_to_ast(js_content, new_import, allocator); + + match result { + Ok(updated_content) => { + println!( + "Updated Content::test_insert_import_to_ast_with_existing_imports:::\n{}", + updated_content + ); + let lines: Vec<&str> = updated_content.lines().collect(); + let last_import_position = + lines.iter().rposition(|&line| line.starts_with("import")); + assert_eq!( + lines[last_import_position.unwrap() + 1], + "console.log(\"Some imports here!\");", + "New import should be added after the last import" + ); + } + Err(e) => panic!("Error while inserting import: {}", e), + } + } + + #[test] + fn test_insert_multiple_imports() { + let js_content = r#" + console.log("Starting with no imports!"); + "#; + + let imports = vec![ + r#"import { foo } from "module-one";"#, + r#"import bar from "module-two";"#, + r#"import * as namespace from "module-three";"#, + r#"import something, { foo as bar } from "module-four";"#, + ]; + + let allocator = create_allocator(); + for import in &imports { + let result = insert_import_to_ast(js_content, import, allocator); + match result { + Ok(updated_content) => { + println!( + "Updated Content::test_insert_multiple_imports::\n{}", + updated_content + ); + + assert!( + updated_content.contains(import), + "Import not added: {}", + import + ); + } + Err(e) => panic!("Error while inserting import '{}': {}", import, e), + } + } + } + + #[test] + fn test_insert_import_to_ast_with_alert_only() { + // Write a test JavaScript file with only an alert + let js_content = r#" + alert('Hello, world!'); + "#; + + // Insert a new import + let new_import = r#"import { foo } from "module-name";"#; + let allocator = create_allocator(); + let result = insert_import_to_ast(js_content, new_import, allocator); + + match result { + Ok(updated_content) => { + println!("Updated Content:\n{}", updated_content); + assert!(updated_content.contains(new_import), "New import not added"); + assert!( + updated_content.starts_with(new_import), + "New import should be at the top" + ); + } + Err(e) => panic!("Error while inserting import: {}", e), + } + } + + #[test] + fn test_remove_import_from_ast() { + // Write a test JavaScript file + let js_content = r#" + import { foo } from "module-name"; + import bar from "another-module"; + + console.log("Testing remove import"); + "#; + + // Test the function to remove an existing module + let allocator = create_allocator(); + let module_names = vec!["module-name"]; + match remove_import_from_ast(js_content, module_names, allocator) { + Ok(updated_code) => { + let expected_snippet = "module-name"; + + assert!( + !updated_code.contains(expected_snippet), + "The updated code is missing expected content: '{}'", + expected_snippet + ); + } + Err(e) => panic!("Error while removing import: {}", e), + } + } + + #[test] + fn test_find_live_socket_variable() { + // Set up a test JavaScript file + let js_content = r#" + const someVar = 42; + let liveSocket = new LiveSocket("/live", Socket, { + hooks: { ...Hooks, CopyMixInstallationHook }, + longPollFallbackMs: 2500, + params: { _csrf_token: csrfToken }, + }); + const anotherVar = "hello"; + "#; + + let allocator = create_allocator(); + let program = source_to_ast(js_content, allocator).expect("Failed to parse AST"); + + // Test the function + let result = find_live_socket_node_from_ast(&program); + println!("Result for test_find_live_socket_variable: {:?}", result); + + assert_eq!(result, Ok(true)); + } + + #[test] + fn test_find_live_socket_variable_not_found() { + // Set up a test JavaScript file + let js_content = r#" + const someVar = 42; + const anotherVar = "hello"; + "#; + + let allocator = create_allocator(); + let program = source_to_ast(js_content, allocator).expect("Failed to parse AST"); + + // Test the function + let result = find_live_socket_node_from_ast(&program); + println!( + "Result for test_find_live_socket_variable_not_found: {:?}", + result + ); + + assert_eq!(result, Err(false)); + } + + #[test] + fn test_new_extend_hook_object_to_ast() { + let js_content = r#" + let liveSocket = new LiveSocket("/live", Socket, { + hooks: { ...Hooks, CopyMixInstallationHook }, + longPollFallbackMs: 2500, + params: { _csrf_token: csrfToken }, + }); + "#; + + let allocator = create_allocator(); + let object_names = vec!["OXCTestHook", "MishkaHooks"]; + match extend_hook_object_to_ast(js_content, object_names, allocator) { + Ok(ast) => { + println!("Hook object extended successfully. ==> {}", ast); + } + Err(e) => { + eprintln!("Error: {}", e); + panic!("Failed to extend hook object."); + } + } + } + + #[test] + fn test_remove_an_object_from_ast() { + let js_content = r#" + let liveSocket = new LiveSocket("/live", Socket, { + hooks: { ...Hooks, CopyMixInstallationHook }, + longPollFallbackMs: 2500, + params: { _csrf_token: csrfToken }, + }); + "#; + + let allocator = create_allocator(); + let object_names = vec!["CopyMixInstallationHook"]; + + let expected_snippet = "hooks: { ...Hooks }"; + + match remove_objects_of_hooks_from_ast(js_content, object_names, &allocator) { + Ok(updated_code) => { + println!("Updated Code:\n{}", updated_code); + + assert!( + updated_code.contains(expected_snippet), + "The updated code is missing expected content: '{}'", + expected_snippet + ); + } + Err(e) => eprintln!("Error: {}", e), + } + } +} diff --git a/native/igniter_js/src/parsers/javascript/ast_ex.rs b/native/igniter_js/src/parsers/javascript/ast_ex.rs new file mode 100644 index 0000000..9f524c9 --- /dev/null +++ b/native/igniter_js/src/parsers/javascript/ast_ex.rs @@ -0,0 +1,138 @@ +use crate::atoms; +use crate::helpers::encode_response; +use crate::parsers::javascript::ast::*; +use oxc::allocator::Allocator; +use rustler::{Env, NifResult, Term}; + +// TODO: We are not going to use it for now, until the OXC supports Json ast +// #[rustler::nif] +// fn source_to_ast_nif(env: Env, file_content: String) -> NifResult { +// let allocator = Allocator::default(); // Create an OXC allocator +// match source_to_ast(&file_content, &allocator) { +// Ok(ast) => { +// // Attempt to serialize the AST into JSON +// let ast_json_result: Result = +// to_value(&ast).map_err(|e| format!("Serialization error: {}", e)); + +// let (status, message) = match ast_json_result { +// // If serialization is successful, modify the JSON object and return it +// Ok(mut ast_json) => { +// if let Value::Object(ref mut map) = ast_json { +// map.insert( +// "source_text".to_string(), +// Value::String(ast.source_text.to_string()), +// ); +// } + +// (atoms::ok(), ast_json.to_string()) +// } +// // If serialization fails, send the error message to Elixir +// Err(serialization_error) => (atoms::error(), serialization_error), +// }; + +// encode_response(env, status, atoms::source_to_ast_nif(), message) +// } +// // If the source_to_ast function fails, send the error message to Elixir +// Err(msg) => encode_response(env, atoms::error(), atoms::source_to_ast_nif(), msg), +// } +// } + +#[rustler::nif] +pub fn is_module_imported_from_ast_nif( + env: Env, + file_content: String, + module_name: String, +) -> NifResult { + let allocator = Allocator::default(); // Create an OXC allocator + let fn_atom = atoms::is_module_imported_from_ast_nif(); + let (status, result) = + match is_module_imported_from_ast(&file_content, &module_name, &allocator) { + Ok(true) => (atoms::ok(), true), + _ => (atoms::error(), false), + }; + + encode_response(env, status, fn_atom, result) +} + +#[rustler::nif] +pub fn insert_import_to_ast_nif( + env: Env, + file_content: String, + import_lines: String, +) -> NifResult { + let allocator = Allocator::default(); // Create an OXC allocator + let (status, result) = match insert_import_to_ast(&file_content, &import_lines, &allocator) { + Ok(updated_code) => (atoms::ok(), updated_code), + Err(error_msg) => (atoms::error(), error_msg), + }; + + encode_response(env, status, atoms::insert_import_to_ast_nif(), result) +} + +#[rustler::nif] +fn remove_import_from_ast_nif( + env: Env, + file_content: String, + modules: Vec, +) -> NifResult { + let allocator = Allocator::default(); // Create an OXC allocator + let names_iter = modules.iter().map(|s| s.as_str()); + let (status, result) = match remove_import_from_ast(&file_content, names_iter, &allocator) { + Ok(updated_code) => (atoms::ok(), updated_code), + Err(error_msg) => (atoms::error(), error_msg), + }; + + encode_response(env, status, atoms::remove_import_from_ast_nif(), result) +} + +#[rustler::nif] +pub fn find_live_socket_node_from_ast_nif(env: Env, file_content: String) -> NifResult { + let allocator = Allocator::default(); // Create an OXC allocator + let ast = source_to_ast(&file_content, &allocator); + let fn_atom = atoms::find_live_socket_node_from_ast(); + if ast.is_err() { + let msg = "Invalid JS file."; + return encode_response(env, atoms::error(), fn_atom, msg); + } + + let (status, result) = match find_live_socket_node_from_ast(&ast.unwrap()) { + Ok(true) => (atoms::ok(), true), + _ => (atoms::error(), false), + }; + + encode_response(env, status, fn_atom, result) +} + +#[rustler::nif] +pub fn extend_hook_object_to_ast_nif( + env: Env, + file_content: String, + names: Vec, +) -> NifResult { + let allocator = Allocator::default(); // Create an OXC allocator + let names_iter = names.iter().map(|s| s.as_str()); + let (status, result) = match extend_hook_object_to_ast(&file_content, names_iter, &allocator) { + Ok(updated_code) => (atoms::ok(), updated_code), + Err(error_msg) => (atoms::error(), error_msg), + }; + + encode_response(env, status, atoms::extend_hook_object_to_ast_nif(), result) +} + +#[rustler::nif] +fn remove_objects_of_hooks_from_ast_nif( + env: Env, + file_content: String, + object_names: Vec, +) -> NifResult { + let allocator = Allocator::default(); // Create an OXC allocator + let fn_atom = atoms::remove_objects_of_hooks_from_ast_nif(); + let names_iter = object_names.iter().map(|s| s.as_str()); + let (status, result) = + match remove_objects_of_hooks_from_ast(&file_content, names_iter, &allocator) { + Ok(updated_code) => (atoms::ok(), updated_code), + Err(error_msg) => (atoms::error(), error_msg), + }; + + encode_response(env, status, fn_atom, result) +} diff --git a/native/igniter_js/src/parsers/javascript/mod.rs b/native/igniter_js/src/parsers/javascript/mod.rs new file mode 100644 index 0000000..68aeb18 --- /dev/null +++ b/native/igniter_js/src/parsers/javascript/mod.rs @@ -0,0 +1,2 @@ +pub mod ast; +pub mod ast_ex; diff --git a/priv/native/libigniter_js.so b/priv/native/libigniter_js.so new file mode 100755 index 0000000..752e541 Binary files /dev/null and b/priv/native/libigniter_js.so differ diff --git a/test/assets/invalidAppWithRemovedImport.js b/test/assets/invalidAppWithRemovedImport.js new file mode 100644 index 0000000..371249c --- /dev/null +++ b/test/assets/invalidAppWithRemovedImport.js @@ -0,0 +1,3 @@ +import { foo } from "module-name"; +import bar from "another-module"; +let Hooks = {}; diff --git a/test/assets/invalidAppWithoutHooksKey.js b/test/assets/invalidAppWithoutHooksKey.js new file mode 100644 index 0000000..5d8d4e3 --- /dev/null +++ b/test/assets/invalidAppWithoutHooksKey.js @@ -0,0 +1,5 @@ +let Hooks = {}; +let liveSocket = new LiveSocket("/live", Socket, { + longPollFallbackMs: 2500, + params: { _csrf_token: csrfToken }, +}); diff --git a/test/assets/invalidAppWithoutLiveSockerObject.js b/test/assets/invalidAppWithoutLiveSockerObject.js new file mode 100644 index 0000000..792bb4e --- /dev/null +++ b/test/assets/invalidAppWithoutLiveSockerObject.js @@ -0,0 +1,2 @@ +let Hooks = {}; +let liveSocket = {}; diff --git a/test/assets/invalidAppWithoutLiveSocket.js b/test/assets/invalidAppWithoutLiveSocket.js new file mode 100644 index 0000000..1277149 --- /dev/null +++ b/test/assets/invalidAppWithoutLiveSocket.js @@ -0,0 +1 @@ +let Hooks = {}; diff --git a/test/assets/validApp.js b/test/assets/validApp.js new file mode 100644 index 0000000..dcdcc76 --- /dev/null +++ b/test/assets/validApp.js @@ -0,0 +1,35 @@ +import "phoenix_html"; +// Establish Phoenix Socket and LiveView configuration. +import { Socket } from "phoenix"; +import { LiveSocket } from "phoenix_live_view"; +// import { LiveSocket } from "../../../phoenix_live_view"; +import topbar from "../vendor/topbar"; +import { initDarkMode } from "../vendor/darkmode"; +import CopyMixInstallationHook from "../vendor/mixCopy"; + +let Hooks = {}; + +let csrfToken = document + .querySelector("meta[name='csrf-token']") + .getAttribute("content"); +let liveSocket = new LiveSocket("/live", Socket, { + hooks: { ...Hooks, CopyMixInstallationHook }, + longPollFallbackMs: 2500, + params: { _csrf_token: csrfToken }, +}); + +// Show progress bar on live navigation and form submits +topbar.config({ barColors: { 0: "#29d" }, shadowColor: "rgba(0, 0, 0, .3)" }); +window.addEventListener("phx:page-loading-start", (_info) => topbar.show(300)); +window.addEventListener("phx:page-loading-stop", (_info) => topbar.hide()); + +initDarkMode(); + +// connect if there are any LiveViews on the page +liveSocket.connect(); + +// expose liveSocket on window for web console debug logs and latency simulation: +// >> liveSocket.enableDebug() +// >> liveSocket.enableLatencySim(1000) // enabled for duration of browser session +// >> liveSocket.disableLatencySim() +window.liveSocket = liveSocket; diff --git a/test/assets/validAppWithSomeHooksObjects.js b/test/assets/validAppWithSomeHooksObjects.js new file mode 100644 index 0000000..7517221 --- /dev/null +++ b/test/assets/validAppWithSomeHooksObjects.js @@ -0,0 +1,5 @@ +let liveSocket = new LiveSocket("/live", Socket, { + hooks: { ...Hooks, CopyMixInstallationHook, OXCExampleObjectHook }, + longPollFallbackMs: 2500, + params: { _csrf_token: csrfToken }, +}); diff --git a/test/parsers/javascript/parser_test.exs b/test/parsers/javascript/parser_test.exs new file mode 100644 index 0000000..1a43b67 --- /dev/null +++ b/test/parsers/javascript/parser_test.exs @@ -0,0 +1,264 @@ +defmodule IgniterJSTest.Parsers.Javascript.ParserTest do + use ExUnit.Case + alias IgniterJS.Parsers.Javascript.Parser + + @valid_app_js "test/assets/validApp.js" + @invalid_app_without_live_socket "test/assets/invalidAppWithoutLiveSocket.js" + @invalid_app_with_removed_import "test/assets/invalidAppWithRemovedImport.js" + @invalid_app_without_live_socket_object "test/assets/invalidAppWithoutLiveSockerObject.js" + @invalid_app_without_hooks_key "test/assets/invalidAppWithoutHooksKey.js" + @valid_app_with_hooks_objects "test/assets/validAppWithSomeHooksObjects.js" + + test "User requested module imported? :: module_imported" do + {:ok, :module_imported, true} = + assert Parser.module_imported(@valid_app_js, "phoenix_live_view", :path) + + {:error, :module_imported, false} = + assert Parser.module_imported(@invalid_app_without_live_socket, "none_live_view", :path) + + assert Parser.module_imported?(@valid_app_js, "phoenix_live_view", :path) + + assert !Parser.module_imported?(@invalid_app_without_live_socket, "none_live_view", :path) + + {:ok, :module_imported, true} = + assert Parser.module_imported(File.read!(@valid_app_js), "phoenix_live_view") + + {:error, :module_imported, false} = + assert Parser.module_imported( + File.read!(@invalid_app_without_live_socket), + "none_live_view" + ) + + assert Parser.module_imported?(File.read!(@valid_app_js), "phoenix_live_view") + + assert !Parser.module_imported?( + File.read!(@invalid_app_without_live_socket), + "none_live_view" + ) + end + + test "Insert some js lines for import modules :: insert_imports" do + imports = """ + import { foo } from "module-name"; + import bar from "another-module"; + """ + + considerd_output = + "import { foo } from \"module-name\";\nimport bar from \"another-module\";\nlet Hooks = {};\n" + + {:ok, :insert_imports, js_output} = + assert Parser.insert_imports(@invalid_app_without_live_socket, imports, :path) + + ^js_output = assert considerd_output + + {:ok, :insert_imports, js_output} = + assert Parser.insert_imports(File.read!(@invalid_app_without_live_socket), imports) + + ^js_output = assert considerd_output + end + + test "Remove imported modules :: remove_imports" do + none_imported_module_output = + "import { foo } from \"module-name\";\nimport bar from \"another-module\";\nlet Hooks = {};\n" + + {:ok, :remove_imports, outptu} = + Parser.remove_imports(@invalid_app_with_removed_import, "phoenix_live_view", :path) + + ^none_imported_module_output = assert outptu + + remove_a_module_output = "import bar from \"another-module\";\nlet Hooks = {};\n" + + {:ok, :remove_imports, outptu} = + Parser.remove_imports(@invalid_app_with_removed_import, "module-name", :path) + + ^remove_a_module_output = assert outptu + + remove_two_modules_output = "let Hooks = {};\n" + + {:ok, :remove_imports, outptu} = + Parser.remove_imports( + @invalid_app_with_removed_import, + [ + "module-name", + "another-module" + ], + :path + ) + + ^remove_two_modules_output = assert outptu + + none_imported_module_output = + "import { foo } from \"module-name\";\nimport bar from \"another-module\";\nlet Hooks = {};\n" + + {:ok, :remove_imports, outptu} = + Parser.remove_imports(File.read!(@invalid_app_with_removed_import), "phoenix_live_view") + + ^none_imported_module_output = assert outptu + + remove_a_module_output = "import bar from \"another-module\";\nlet Hooks = {};\n" + + {:ok, :remove_imports, outptu} = + Parser.remove_imports(File.read!(@invalid_app_with_removed_import), "module-name") + + ^remove_a_module_output = assert outptu + + remove_two_modules_output = "let Hooks = {};\n" + + {:ok, :remove_imports, outptu} = + Parser.remove_imports( + File.read!(@invalid_app_with_removed_import), + ["module-name", "another-module"] + ) + + ^remove_two_modules_output = assert outptu + end + + test "LiveSocket var exist :: exist_live_socket" do + {:ok, :exist_live_socket, true} = + assert Parser.exist_live_socket(@valid_app_js, :path) + + {:error, :exist_live_socket, false} = + assert Parser.exist_live_socket(@invalid_app_without_live_socket, :path) + + assert Parser.exist_live_socket?(@valid_app_js, :path) + + assert !Parser.exist_live_socket?(@invalid_app_without_live_socket, :path) + + {:ok, :exist_live_socket, true} = + assert Parser.exist_live_socket(File.read!(@valid_app_js)) + + {:error, :exist_live_socket, false} = + assert Parser.exist_live_socket(File.read!(@invalid_app_without_live_socket)) + + assert Parser.exist_live_socket?(File.read!(@valid_app_js)) + + assert !Parser.exist_live_socket?(File.read!(@invalid_app_without_live_socket)) + end + + test "Extend hook objects :: extend_hook_object" do + {:error, :extend_hook_object, "liveSocket not found."} = + Parser.extend_hook_object(@invalid_app_without_live_socket, "something", :path) + + {:error, :extend_hook_object, "properties not found in the AST."} = + Parser.extend_hook_object(@invalid_app_without_live_socket_object, "something", :path) + + considerd_output = + "let Hooks = {};\nlet liveSocket = new LiveSocket(\"/live\", Socket, {\n\tlongPollFallbackMs: 2500,\n\tparams: { _csrf_token: csrfToken },\n\thooks: { something }\n});\n" + + {:ok, :extend_hook_object, output} = + assert Parser.extend_hook_object(@invalid_app_without_hooks_key, "something", :path) + + ^considerd_output = assert output + + {:ok, :extend_hook_object, output} = + assert Parser.extend_hook_object( + @invalid_app_without_hooks_key, + [ + "something", + "another" + ], + :path + ) + + considerd_output = + "let Hooks = {};\nlet liveSocket = new LiveSocket(\"/live\", Socket, {\n\tlongPollFallbackMs: 2500,\n\tparams: { _csrf_token: csrfToken },\n\thooks: {\n\t\tsomething,\n\t\tanother\n\t}\n});\n" + + ^considerd_output = assert output + + {:error, :extend_hook_object, "liveSocket not found."} = + Parser.extend_hook_object(File.read!(@invalid_app_without_live_socket), "something") + + {:error, :extend_hook_object, "properties not found in the AST."} = + Parser.extend_hook_object(File.read!(@invalid_app_without_live_socket_object), "something") + + considerd_output = + "let Hooks = {};\nlet liveSocket = new LiveSocket(\"/live\", Socket, {\n\tlongPollFallbackMs: 2500,\n\tparams: { _csrf_token: csrfToken },\n\thooks: { something }\n});\n" + + {:ok, :extend_hook_object, output} = + assert Parser.extend_hook_object(File.read!(@invalid_app_without_hooks_key), "something") + + ^considerd_output = assert output + + {:ok, :extend_hook_object, output} = + assert Parser.extend_hook_object( + File.read!(@invalid_app_without_hooks_key), + ["something", "another"] + ) + + considerd_output = + "let Hooks = {};\nlet liveSocket = new LiveSocket(\"/live\", Socket, {\n\tlongPollFallbackMs: 2500,\n\tparams: { _csrf_token: csrfToken },\n\thooks: {\n\t\tsomething,\n\t\tanother\n\t}\n});\n" + + ^considerd_output = assert output + end + + test "Remove objects of hooks key inside LiveSocket:: remove_objects_from_hooks" do + considerd_output = + "let liveSocket = new LiveSocket(\"/live\", Socket, {\n\thooks: {\n\t\t...Hooks,\n\t\tCopyMixInstallationHook,\n\t\tOXCExampleObjectHook\n\t},\n\tlongPollFallbackMs: 2500,\n\tparams: { _csrf_token: csrfToken }\n});\n" + + {:ok, :remove_objects_from_hooks, output} = + assert Parser.remove_objects_from_hooks( + @valid_app_with_hooks_objects, + ["something", "another"], + :path + ) + + ^considerd_output = assert output + + considerd_output = + "let liveSocket = new LiveSocket(\"/live\", Socket, {\n\thooks: {\n\t\t...Hooks,\n\t\tCopyMixInstallationHook\n\t},\n\tlongPollFallbackMs: 2500,\n\tparams: { _csrf_token: csrfToken }\n});\n" + + {:ok, :remove_objects_from_hooks, output} = + assert Parser.remove_objects_from_hooks( + @valid_app_with_hooks_objects, + "OXCExampleObjectHook", + :path + ) + + ^considerd_output = assert output + + considerd_output = + "let liveSocket = new LiveSocket(\"/live\", Socket, {\n\thooks: { ...Hooks },\n\tlongPollFallbackMs: 2500,\n\tparams: { _csrf_token: csrfToken }\n});\n" + + {:ok, :remove_objects_from_hooks, output} = + assert Parser.remove_objects_from_hooks( + @valid_app_with_hooks_objects, + ["OXCExampleObjectHook", "CopyMixInstallationHook"], + :path + ) + + ^considerd_output = assert output + + considerd_output = + "let liveSocket = new LiveSocket(\"/live\", Socket, {\n\thooks: {\n\t\t...Hooks,\n\t\tCopyMixInstallationHook,\n\t\tOXCExampleObjectHook\n\t},\n\tlongPollFallbackMs: 2500,\n\tparams: { _csrf_token: csrfToken }\n});\n" + + {:ok, :remove_objects_from_hooks, output} = + assert Parser.remove_objects_from_hooks( + File.read!(@valid_app_with_hooks_objects), + ["something", "another"] + ) + + ^considerd_output = assert output + + considerd_output = + "let liveSocket = new LiveSocket(\"/live\", Socket, {\n\thooks: {\n\t\t...Hooks,\n\t\tCopyMixInstallationHook\n\t},\n\tlongPollFallbackMs: 2500,\n\tparams: { _csrf_token: csrfToken }\n});\n" + + {:ok, :remove_objects_from_hooks, output} = + assert Parser.remove_objects_from_hooks( + File.read!(@valid_app_with_hooks_objects), + "OXCExampleObjectHook" + ) + + ^considerd_output = assert output + + considerd_output = + "let liveSocket = new LiveSocket(\"/live\", Socket, {\n\thooks: { ...Hooks },\n\tlongPollFallbackMs: 2500,\n\tparams: { _csrf_token: csrfToken }\n});\n" + + {:ok, :remove_objects_from_hooks, output} = + assert Parser.remove_objects_from_hooks( + File.read!(@valid_app_with_hooks_objects), + ["OXCExampleObjectHook", "CopyMixInstallationHook"] + ) + + ^considerd_output = assert output + end +end diff --git a/test/test_helper.exs b/test/test_helper.exs new file mode 100644 index 0000000..869559e --- /dev/null +++ b/test/test_helper.exs @@ -0,0 +1 @@ +ExUnit.start()