Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adding initial files with limited functionality support for OXC in Elixir #1

Merged
merged 10 commits into from
Dec 24, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .formatter.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# Used by "mix format"
[
inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"]
]
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -27,3 +27,6 @@ igniter_js-*.tar

# Also ignore archive artifacts (built via "mix archive.build").
*.ez

# Developer environment files
.DS_Store
1 change: 1 addition & 0 deletions .iex.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
IEx.configure(auto_reload: true)
3 changes: 3 additions & 0 deletions lib/igniter_js.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
defmodule IgniterJS do
@moduledoc false
end
20 changes: 20 additions & 0 deletions lib/igniter_js/application.ex
Original file line number Diff line number Diff line change
@@ -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
72 changes: 72 additions & 0 deletions lib/igniter_js/helpers.ex
Original file line number Diff line number Diff line change
@@ -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
19 changes: 19 additions & 0 deletions lib/igniter_js/native.ex
Original file line number Diff line number Diff line change
@@ -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
183 changes: 183 additions & 0 deletions lib/igniter_js/parsers/javascript/parser.ex
Original file line number Diff line number Diff line change
@@ -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
57 changes: 57 additions & 0 deletions mix.exs
Original file line number Diff line number Diff line change
@@ -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
19 changes: 19 additions & 0 deletions mix.lock
Original file line number Diff line number Diff line change
@@ -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"},
}
5 changes: 5 additions & 0 deletions native/igniter_js/.cargo/config.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
[target.'cfg(target_os = "macos")']
rustflags = [
"-C", "link-arg=-undefined",
"-C", "link-arg=dynamic_lookup",
]
Loading