diff --git a/config/config.exs b/config/config.exs index dd37b33..bd68881 100644 --- a/config/config.exs +++ b/config/config.exs @@ -1,7 +1,7 @@ import Config config :nodelix, - version: "3.2.7", + version: "20.10.0", another: [ args: ["--help"] ] diff --git a/lib/mix/tasks/nodelix.ex b/lib/mix/tasks/nodelix.ex index cf18af7..7dbc9df 100644 --- a/lib/mix/tasks/nodelix.ex +++ b/lib/mix/tasks/nodelix.ex @@ -2,21 +2,18 @@ defmodule Mix.Tasks.Nodelix do use Mix.Task @moduledoc """ - Invokes tailwind with the given args. + Invokes `node` with the given args. Usage: - $ mix nodelix TASK_OPTIONS PROFILE TAILWIND_ARGS + $ mix nodelix TASK_OPTIONS PROFILE NODE_ARGS Example: - $ mix nodelix default --config=tailwind.config.js \ - --input=css/app.css \ - --output=../priv/static/assets/app.css \ - --minify + $ mix nodelix default --tls-min-v1.3 - If tailwind is not installed, it is automatically downloaded. - Note the arguments given to this task will be appended + If Node.js is not installed, it is automatically downloaded. + The arguments given to this task will be appended to any configured arguments. ## Options @@ -24,13 +21,13 @@ defmodule Mix.Tasks.Nodelix do * `--runtime-config` - load the runtime configuration before executing command - Note flags to control this Mix task must be given before the + Flags to control this Mix task must be given before the profile: $ mix nodelix --runtime-config default """ - @shortdoc "Invokes tailwind with the profile and args" + @shortdoc "Invokes node with the profile and args" @compile {:no_warn_undefined, Mix} @impl true diff --git a/lib/mix/tasks/nodelix.install.ex b/lib/mix/tasks/nodelix.install.ex index 8c52f78..cd66ddf 100644 --- a/lib/mix/tasks/nodelix.install.ex +++ b/lib/mix/tasks/nodelix.install.ex @@ -1,18 +1,18 @@ defmodule Mix.Tasks.Nodelix.Install do use Mix.Task - alias Nodelix.NodeManager + alias Nodelix.NodeDownloader @moduledoc """ - Installs Tailwind executable and assets. + Installs Node.js. $ mix nodelix.install $ mix nodelix.install --if-missing - By default, it installs #{NodeManager.latest_version()} but you + By default, it installs #{NodeDownloader.latest_lts_version()} but you can configure it in your config files, such as: - config :nodelix, :version, "#{NodeManager.latest_version()}" + config :nodelix, :version, "#{NodeDownloader.latest_lts_version()}" ## Options @@ -21,30 +21,9 @@ defmodule Mix.Tasks.Nodelix.Install do * `--if-missing` - install only if the given version does not exist - - * `--no-assets` - does not install Tailwind assets - - ## Assets - - Whenever Tailwind is installed, a default tailwind configuration - will be placed in a new `assets/tailwind.config.js` file. See - the [tailwind documentation](https://tailwindcss.com/docs/configuration) - on configuration options. - - The default tailwind configuration includes Tailwind variants for Phoenix - LiveView specific lifecycle classes: - - * phx-no-feedback - applied when feedback should be hidden from the user - * phx-click-loading - applied when an event is sent to the server on click - while the client awaits the server response - * phx-submit-loading - applied when a form is submitted while the client awaits the server response - * phx-submit-loading - applied when a form input is changed while the client awaits the server response - - Therefore, you may apply a variant, such as `phx-click-loading:animate-pulse` - to customize tailwind classes when Phoenix LiveView classes are applied. """ - @shortdoc "Installs Tailwind executable and assets" + @shortdoc "Installs Node.js" @compile {:no_warn_undefined, Mix} @impl true @@ -54,7 +33,7 @@ defmodule Mix.Tasks.Nodelix.Install do {opts, base_url} = case OptionParser.parse_head!(args, strict: valid_options) do {opts, []} -> - {opts, NodeManager.default_base_url()} + {opts, NodeDownloader.default_base_url()} {opts, [base_url]} -> {opts, base_url} @@ -64,7 +43,7 @@ defmodule Mix.Tasks.Nodelix.Install do Invalid arguments to nodelix.install, expected one of: mix nodelix.install - mix nodelix.install 'https://github.com/tailwindlabs/tailwindcss/releases/download/v$version/tailwindcss-$target' + mix nodelix.install 'https://nodejs.org/dist/v$version/node-v$version-$target' mix nodelix.install --runtime-config mix nodelix.install --if-missing """) @@ -72,84 +51,21 @@ defmodule Mix.Tasks.Nodelix.Install do if opts[:runtime_config], do: Mix.Task.run("app.config") - if opts[:if_missing] && latest_version?() do + if opts[:if_missing] && latest_lts_version?() do :ok else - if Keyword.get(opts, :assets, true) do - File.mkdir_p!("assets/css") - tailwind_config_path = Path.expand("assets/tailwind.config.js") - - prepare_app_css() - prepare_app_js() - - unless File.exists?(tailwind_config_path) do - File.write!(tailwind_config_path, """ - // See the Tailwind configuration guide for advanced usage - // https://tailwindcss.com/docs/configuration - - let plugin = require('tailwindcss/plugin') - - module.exports = { - content: [ - './js/**/*.js', - '../lib/*_web.ex', - '../lib/*_web/**/*.*ex' - ], - theme: { - extend: {}, - }, - plugins: [ - require('@tailwindcss/forms'), - plugin(({addVariant}) => addVariant('phx-no-feedback', ['&.phx-no-feedback', '.phx-no-feedback &'])), - plugin(({addVariant}) => addVariant('phx-click-loading', ['&.phx-click-loading', '.phx-click-loading &'])), - plugin(({addVariant}) => addVariant('phx-submit-loading', ['&.phx-submit-loading', '.phx-submit-loading &'])), - plugin(({addVariant}) => addVariant('phx-change-loading', ['&.phx-change-loading', '.phx-change-loading &'])) - ] - } - """) - end - end - if function_exported?(Mix, :ensure_application!, 1) do Mix.ensure_application!(:inets) Mix.ensure_application!(:ssl) end Mix.Task.run("loadpaths") - NodeManager.install(base_url) + NodeDownloader.install(base_url) end end - defp latest_version?() do + defp latest_lts_version?() do version = Nodelix.configured_version() - match?({:ok, ^version}, NodeManager.bin_version()) - end - - defp prepare_app_css do - app_css = - case File.read("assets/css/app.css") do - {:ok, str} -> str - {:error, _} -> "" - end - - unless app_css =~ "tailwind" do - File.write!("assets/css/app.css", """ - @import "tailwindcss/base"; - @import "tailwindcss/components"; - @import "tailwindcss/utilities"; - - #{String.replace(app_css, ~s|@import "./phoenix.css";\n|, "")}\ - """) - end - end - - defp prepare_app_js do - case File.read("assets/js/app.js") do - {:ok, app_js} -> - File.write!("assets/js/app.js", String.replace(app_js, ~s|import "../css/app.css"\n|, "")) - - {:error, _} -> - :ok - end + match?({:ok, ^version}, NodeDownloader.bin_version()) end end diff --git a/lib/nodelix.ex b/lib/nodelix.ex index b152b62..6097ba7 100644 --- a/lib/nodelix.ex +++ b/lib/nodelix.ex @@ -2,88 +2,56 @@ defmodule Nodelix do use Application require Logger - alias Nodelix.NodeManager + alias Nodelix.NodeDownloader @moduledoc """ - Nodelix is an installer and runner for [tailwind](https://tailwindcss.com/). + Nodelix is an installer and runner for [Node.js](https://nodejs.org/). ## Profiles - You can define multiple tailwind profiles. By default, there is a + You can define multiple nodelix profiles. By default, there is a profile called `:default` which you can configure its args, current directory and environment: config :nodelix, - version: "#{NodeManager.latest_version()}", + version: "#{NodeDownloader.latest_lts_version()}", default: [ args: ~w( - --config=tailwind.config.js - --input=css/app.css - --output=../priv/static/assets/app.css + --version ), cd: Path.expand("../assets", __DIR__), ] ## Nodelix configuration - There are two global configurations for the tailwind application: + There are two global configurations for the nodelix application: - * `:version` - the expected tailwind version + * `:version` - the expected Node.js version * `:cacerts_path` - the directory to find certificates for https connections - * `:path` - the path to find the tailwind executable at. By - default, it is automatically downloaded and placed inside - the `_build` directory of your current app - - Overriding the `:path` is not recommended, as we will automatically - download and manage `tailwind` for you. But in case you can't download - it (for example, GitHub behind a proxy), you may want to - set the `:path` to a configurable system location. - - For instance, you can install `tailwind` globally with `npm`: - - $ npm install -g tailwindcss - - On Unix, the executable will be at: - - NPM_ROOT/tailwind/node_modules/tailwind-TARGET/bin/tailwind - - On Windows, it will be at: - - NPM_ROOT/tailwind/node_modules/tailwind-windows-(32|64)/tailwind.exe - - Where `NPM_ROOT` is the result of `npm root -g` and `TARGET` is your system - target architecture. - - Once you find the location of the executable, you can store it in a - `MIX_TAILWIND_PATH` environment variable, which you can then read in - your configuration file: - - config :nodelix, path: System.get_env("MIX_TAILWIND_PATH") - """ @doc false def start(_, _) do unless Application.get_env(:nodelix, :version) do Logger.warn(""" - tailwind version is not configured. Please set it in your config files: + Node.js version is not configured. Please set it in your config files: - config :nodelix, :version, "#{NodeManager.latest_version()}" + config :nodelix, :version, "#{NodeDownloader.latest_lts_version()}" """) end configured_version = configured_version() - case NodeManager.bin_version() do + case NodeDownloader.bin_version() do {:ok, ^configured_version} -> :ok {:ok, version} -> Logger.warn(""" - Outdated tailwind version. Expected #{configured_version}, got #{version}. \ + Outdated Node.js version. Expected #{configured_version}, got #{version}. \ Please run `mix nodelix.install` or update the version in your config files.\ """) @@ -95,10 +63,10 @@ defmodule Nodelix do end @doc """ - Returns the configured tailwind version. + Returns the configured Node.js version. """ def configured_version do - Application.get_env(:nodelix, :version, NodeManager.latest_version()) + Application.get_env(:nodelix, :version, NodeDownloader.latest_lts_version()) end @doc """ @@ -109,15 +77,13 @@ defmodule Nodelix do def config_for!(profile) when is_atom(profile) do Application.get_env(:nodelix, profile) || raise ArgumentError, """ - unknown tailwind profile. Make sure the profile is defined in your config/config.exs file, such as: + unknown nodelix profile. Make sure the profile is defined in your config/config.exs file, such as: config :nodelix, - version: "#{NodeManager.latest_version()}", + version: "#{NodeDownloader.latest_lts_version()}", #{profile}: [ args: ~w( - --config=tailwind.config.js - --input=css/app.css - --output=../priv/static/assets/app.css + --version ), cd: Path.expand("../assets", __DIR__) ] @@ -147,7 +113,7 @@ defmodule Nodelix do stderr_to_stdout: true ] - NodeManager.bin_path() + NodeDownloader.bin_path() |> System.cmd(args ++ extra_args, opts) |> elem(1) end @@ -157,13 +123,13 @@ defmodule Nodelix do end @doc """ - Installs, if not available, and then runs `tailwind`. + Installs, if not available, and then runs `node`. Returns the same as `run/2`. """ def install_and_run(profile, args) do - unless File.exists?(NodeManager.bin_path()) do - NodeManager.install() + unless File.exists?(NodeDownloader.bin_path()) do + NodeDownloader.install() end run(profile, args) diff --git a/lib/nodelix/node_downloader.ex b/lib/nodelix/node_downloader.ex index cb12a7a..72ec9f8 100644 --- a/lib/nodelix/node_downloader.ex +++ b/lib/nodelix/node_downloader.ex @@ -1,42 +1,34 @@ -defmodule Nodelix.NodeManager do - # https://github.com/tailwindlabs/tailwindcss/releases - @latest_version "3.2.7" +defmodule Nodelix.NodeDownloader do + # https://nodejs.org/en/about/previous-releases + @latest_lts_version "20.10.0" require Logger @moduledoc """ TODO - - fetch Node.js archive for a version and platform (https://nodejs.org/dist/v20.10.0/) - - fetch checksum file (https://nodejs.org/dist/v20.10.0/SHASUMS256.txt) - - fetch checksum file signature (https://nodejs.org/dist/v20.10.0/SHASUMS256.txt.sig) - - fetch Node.js signing keys list (https://raw.githubusercontent.com/nodejs/release-keys/main/keys.list) - - fetch keys (https://raw.githubusercontent.com/nodejs/release-keys/main/keys/4ED778F539E3634C779C87C6D7062848A1AB005C.asc) - - convert keys to PEM (https://stackoverflow.com/questions/10966256/erlang-importing-gpg-public-key) - - check signature of the checksum file with each key until there's a match - - match the hash for the archive filename - - check integrity of the downloaded archive - - return the archive + - [X] fetch Node.js archive for a version and platform (https://nodejs.org/dist/v20.10.0/) + - [ ] fetch checksum file (https://nodejs.org/dist/v20.10.0/SHASUMS256.txt) + - [ ] fetch checksum file signature (https://nodejs.org/dist/v20.10.0/SHASUMS256.txt.sig) + - [ ] fetch Node.js signing keys list (https://raw.githubusercontent.com/nodejs/release-keys/main/keys.list) + - [ ] fetch keys (https://raw.githubusercontent.com/nodejs/release-keys/main/keys/4ED778F539E3634C779C87C6D7062848A1AB005C.asc) + - [ ] convert keys to PEM (https://stackoverflow.com/questions/10966256/erlang-importing-gpg-public-key) + - [ ] check signature of the checksum file with each key until there's a match + - [ ] match the hash for the archive filename + - [ ] check integrity of the downloaded archive + - [ ] return the archive """ - @doc false - # Latest known version at the time of publishing. - def latest_version, do: @latest_version + @doc """ + Returns the latest known LTS version at the time of publishing. + """ + def latest_lts_version, do: @latest_lts_version @doc """ Returns the path to the executable. The executable may not be available if it was not yet installed. """ - def bin_path do - name = "tailwind-#{target()}" - - Application.get_env(:nodelix, :path) || - if Code.ensure_loaded?(Mix.Project) do - Path.join(Path.dirname(Mix.Project.build_path()), name) - else - Path.expand("_build/#{name}") - end - end + def bin_path, do: archive_path() @doc """ Returns the version of the tailwind executable. @@ -56,6 +48,21 @@ defmodule Nodelix.NodeManager do end end + @doc """ + Returns the path to the archive. + + The archive may not be available if it was not yet installed. + """ + def archive_path do + name = "nodejs-#{target()}" + + if Code.ensure_loaded?(Mix.Project) do + Path.join(Path.dirname(Mix.Project.build_path()), name) + else + Path.expand("_build/#{name}") + end + end + @doc """ The default URL to fetch the Node.js archive from. """ @@ -64,22 +71,21 @@ defmodule Nodelix.NodeManager do end @doc """ - Installs tailwind with `configured_version/0`. + Installs Node.js with `configured_version/0`. """ def install(base_url \\ default_base_url()) do url = get_url(base_url) - bin_path = bin_path() + archive_path = archive_path() binary = fetch_body!(url) - File.mkdir_p!(Path.dirname(bin_path)) + File.mkdir_p!(Path.dirname(archive_path)) # MacOS doesn't recompute code signing information if a binary # is overwritten with a new version, so we force creation of a new file - if File.exists?(bin_path) do - File.rm!(bin_path) + if File.exists?(archive_path) do + File.rm!(archive_path) end - File.write!(bin_path, binary, [:binary]) - File.chmod(bin_path, 0o755) + File.write!(archive_path, binary, [:binary]) end # Available targets: @@ -117,7 +123,7 @@ defmodule Nodelix.NodeManager do defp fetch_body!(url) do scheme = URI.parse(url).scheme url = String.to_charlist(url) - Logger.debug("Downloading tailwind from #{url}") + Logger.debug("Downloading Node.js from #{url}") {:ok, _} = Application.ensure_all_started(:inets) {:ok, _} = Application.ensure_all_started(:ssl) @@ -163,10 +169,7 @@ defmodule Nodelix.NodeManager do your certificates are set via the cacerts_path configuration 2. Manually download the executable from the URL above and - place it inside "_build/tailwind-#{target()}" - - 3. Install and use Tailwind from npmJS. See our module documentation - to learn more: https://hexdocs.pm/tailwind + place it inside "_build/node-#{target()}" """ end end diff --git a/test/nodelix_test.exs b/test/nodelix_test.exs index 2a39bab..46f5b66 100644 --- a/test/nodelix_test.exs +++ b/test/nodelix_test.exs @@ -1,14 +1,10 @@ defmodule NodelixTest do use ExUnit.Case, async: true - @version Nodelix.NodeManager.latest_version() + @version Nodelix.NodeDownloader.latest_lts_version() setup do Application.put_env(:nodelix, :version, @version) - File.mkdir_p!("assets/js") - File.mkdir_p!("assets/css") - File.rm("assets/tailwind.config.js") - File.rm("assets/css/app.css") :ok end @@ -24,74 +20,27 @@ defmodule NodelixTest do end) =~ @version end - test "run with pre-existing app.css" do - assert ExUnit.CaptureIO.capture_io(fn -> - assert Nodelix.run(:default, []) == 0 - end) =~ @version - end - test "updates on install" do - Application.put_env(:nodelix, :version, "3.0.3") + Application.put_env(:nodelix, :version, "20.9.0") Mix.Task.rerun("nodelix.install", ["--if-missing"]) assert ExUnit.CaptureIO.capture_io(fn -> assert Nodelix.run(:default, ["--help"]) == 0 - end) =~ "3.0.3" + end) =~ "20.9.0" Application.delete_env(:nodelix, :version) Mix.Task.rerun("nodelix.install", ["--if-missing"]) - assert File.exists?("assets/tailwind.config.js") - assert File.read!("assets/css/app.css") =~ "tailwind" assert ExUnit.CaptureIO.capture_io(fn -> assert Nodelix.run(:default, ["--help"]) == 0 end) =~ @version end - test "install on existing app.css and app.js" do - File.write!("assets/css/app.css", """ - @import "./phoenix.css"; - body { - } - """) - - File.write!("assets/js/app.js", """ - import "../css/app.css" - - let Hooks = {} - """) - - Mix.Task.rerun("nodelix.install") - - expected_css = - String.trim(""" - @import "tailwindcss/base"; - @import "tailwindcss/components"; - @import "tailwindcss/utilities"; - - body { - } - """) - - expected_js = - String.trim(""" - - let Hooks = {} - """) - - assert String.trim(File.read!("assets/css/app.css")) == expected_css - assert String.trim(File.read!("assets/js/app.js")) == expected_js - - Mix.Task.rerun("nodelix.install") - - assert String.trim(File.read!("assets/js/app.js")) == expected_js - end - test "installs with custom URL" do assert :ok = Mix.Task.rerun("nodelix.install", [ - "https://github.com/tailwindlabs/tailwindcss/releases/download/v$version/tailwindcss-$target" + "https://nodejs.org/dist/v$version/node-v$version-$target" ]) end end