├── .formatter.exs ├── .github ├── dependabot.yml └── workflows │ └── ci.yml ├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── lib ├── mix │ └── tasks │ │ └── importmap.copy.ex ├── phoenix_importmap.ex └── phoenix_importmap │ ├── asset.ex │ ├── importmap.ex │ ├── util.ex │ └── watcher.ex ├── mix.exs ├── mix.lock └── test ├── fixtures └── js │ └── app.js ├── importmap_test.exs ├── phoenix_importmap_test.exs ├── test_helper.exs └── watcher_test.exs /.formatter.exs: -------------------------------------------------------------------------------- 1 | # Used by "mix format" 2 | [ 3 | inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"] 4 | ] 5 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: 'mix' 4 | directory: '/' 5 | schedule: 6 | interval: 'weekly' 7 | - package-ecosystem: 'github-actions' 8 | directory: '/' 9 | schedule: 10 | interval: 'weekly' 11 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | pull_request: 6 | branches: 7 | - main 8 | 9 | jobs: 10 | mix_test: 11 | name: mix test (OTP ${{matrix.otp}} | Elixir ${{matrix.elixir}}) 12 | 13 | strategy: 14 | matrix: 15 | include: 16 | - elixir: 1.16.3 17 | otp: 26.2 18 | 19 | - elixir: 1.17.2 20 | otp: 27.0 21 | 22 | - elixir: 1.18.1 23 | otp: 27.0 24 | 25 | runs-on: ubuntu-latest 26 | 27 | steps: 28 | - name: Install inotify-tools 29 | run: | 30 | sudo apt update 31 | sudo apt install -y inotify-tools 32 | - name: Checkout 33 | uses: actions/checkout@v4 34 | 35 | - name: Set up Elixir 36 | uses: erlef/setup-beam@v1 37 | with: 38 | elixir-version: ${{ matrix.elixir }} 39 | otp-version: ${{ matrix.otp }} 40 | 41 | - name: Restore deps and _build cache 42 | uses: actions/cache@v4 43 | with: 44 | path: | 45 | deps 46 | _build 47 | key: deps-${{ runner.os }}-${{ matrix.otp }}-${{ matrix.elixir }}-${{ hashFiles('**/mix.lock') }} 48 | restore-keys: | 49 | deps-${{ runner.os }}-${{ matrix.otp }}-${{ matrix.elixir }} 50 | 51 | - name: Install dependencies 52 | run: mix deps.get --only test 53 | 54 | - name: Remove compiled application files 55 | run: mix clean 56 | 57 | - name: Compile dependencies 58 | run: mix compile 59 | # if: ${{ !matrix.lint }} 60 | env: 61 | MIX_ENV: test 62 | 63 | - name: Compile & lint dependencies 64 | run: mix compile --warnings-as-errors 65 | # if: ${{ matrix.lint }} 66 | env: 67 | MIX_ENV: test 68 | 69 | - name: Check if formatted 70 | run: mix format --check-formatted 71 | # if: ${{ matrix.lint }} 72 | env: 73 | MIX_ENV: test 74 | 75 | - name: Run tests 76 | run: mix test 77 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # The directory Mix will write compiled artifacts to. 2 | /_build/ 3 | 4 | # If you run "mix test --cover", coverage assets end up here. 5 | /cover/ 6 | 7 | # The directory Mix downloads your dependencies sources to. 8 | /deps/ 9 | 10 | # Where third-party dependencies like ExDoc output generated docs. 11 | /doc/ 12 | 13 | # Ignore .fetch files in case you like to edit your project deps locally. 14 | /.fetch 15 | 16 | # If the VM crashes, it generates a dump, let's ignore it too. 17 | erl_crash.dump 18 | 19 | # Also ignore archive artifacts (built via "mix archive.build"). 20 | *.ez 21 | 22 | # Ignore package tarball (built via "mix hex.build"). 23 | phoenix_importmap-*.tar 24 | 25 | # Temporary files, for example, from tests. 26 | /tmp/ 27 | /priv/static/assets -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## 0.4.0 (2025-01-16) 4 | 5 | ### Breaking 6 | - Adds a struct for the `PhoenixImportmap.Importmap` module that implements the `Phoenix.HTML.Safe` protocol. This change is a breaking change if you currently use `raw` to 7 | interpolate the importmap into the template. 8 | 9 | ## 0.3.0 (2025-01-09) 10 | 11 | ### Breaking 12 | - Use a reference to consuming projects' Endpoint module to generate digested asset paths in production. 13 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2024 Giles Thompson 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Phoenix Importmap 2 | 3 | [![CI](https://github.com/gilest/phoenix_importmap/actions/workflows/ci.yml/badge.svg)](https://github.com/gilest/phoenix_importmap/actions/workflows/ci.yml) 4 | 5 | 6 | 7 | Use [ES/JS Modules](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Modules) with [importmap](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script/type/importmap) to efficiently serve JavaScript without transpiling or bundling. 8 | 9 | With this approach you'll ship many small JavaScript files instead of one big JavaScript file. 10 | 11 | Import maps are [supported natively](https://caniuse.com/?search=importmap) in all major, modern browsers. 12 | 13 | ## Installation 14 | 15 | The package can be installed by adding `phoenix_importmap` to your list of dependencies in mix.exs: 16 | 17 | ```elixir 18 | def deps do 19 | [ 20 | {:phoenix_importmap, "~> 0.4.0"} 21 | ] 22 | end 23 | ``` 24 | 25 | If you are using the esbuild package you may also remove it, along with its configuration. 26 | 27 | In `config/dev.exs` add the asset watcher to your `Endpoint` configuration: 28 | 29 | ```elixir 30 | watchers: [ 31 | assets: {PhoenixImportmap, :copy_and_watch, [~w(/assets)]}, 32 | ] 33 | ``` 34 | 35 | In `config/config.exs` add an importmap. The following is a good start for a conventional Phoenix app: 36 | 37 | ```elixir 38 | config :phoenix_importmap, :importmap, %{ 39 | app: "/assets/js/app.js", 40 | topbar: "/assets/vendor/topbar.js", 41 | phoenix_html: "/deps/phoenix_html/priv/static/phoenix_html.js", 42 | phoenix: "/deps/phoenix/priv/static/phoenix.mjs", 43 | phoenix_live_view: "/deps/phoenix_live_view/priv/static/phoenix_live_view.esm.js" 44 | } 45 | ``` 46 | 47 | If you are using topbar, replace the relative topbar import in `assets/app/app.js` with a module specifier. This asset will be resolved by our importmap: 48 | 49 | ```js 50 | import topbar from 'topbar'; 51 | ``` 52 | 53 | You'll also need to replace the contents of `assets/vendor/topbar.js` with a wrapped version that supports ESM, like this [from jsDelivr](https://cdn.jsdelivr.net/npm/topbar@2.0.0/topbar.js/+esm). 54 | 55 | In `lib//components/layouts/root.html.heex` replace the `app.js` ` 63 | 66 | ``` 67 | 68 | Finally, in `mix.exs` update your assets aliases to replace esbuild with this library: 69 | 70 | ``` 71 | "assets.setup": ["tailwind.install --if-missing"], 72 | "assets.build": ["tailwind default", "phoenix_importmap.copy"], 73 | "assets.deploy": ["tailwind default --minify", "phoenix_importmap.copy", "phx.digest"] 74 | ``` 75 | 76 | The [phoenix_importmap_example repository](https://github.com/gilest/phoenix_importmap_example) demonstrates configuring a newly-generated Phoenix app. 77 | 78 | ## Importmap configuration 79 | 80 | - `:importmap` - Map representing your assets. This is used to copy and watch files, and resolve public paths in `PhoenixImportmap.importmap()` 81 | 82 | ## Asset path configuration 83 | 84 | The defaults should work out of the box with a conventional Phoenix application. There are two global configuration options available. 85 | 86 | - `:copy_destination_path` - Where your mapped assets will be copied to. Defaults to `/priv/static/assets` which is the default path for to serve assets from. 87 | 88 | - `:public_asset_path_prefix` - The public path from which your assets are served. Defaults to `/priv/static` which is the default path for `Plug.Static` to serve `/` at. 89 | -------------------------------------------------------------------------------- /lib/mix/tasks/importmap.copy.ex: -------------------------------------------------------------------------------- 1 | defmodule Mix.Tasks.PhoenixImportmap.Copy do 2 | @moduledoc """ 3 | Copies mapped assets according to importmap configuration. 4 | """ 5 | 6 | @shortdoc "Copies mapped assets according to importmap configuration" 7 | 8 | use Mix.Task 9 | 10 | @impl true 11 | def run(_args) do 12 | PhoenixImportmap.copy() 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /lib/phoenix_importmap.ex: -------------------------------------------------------------------------------- 1 | defmodule PhoenixImportmap do 2 | @moduledoc "README.md" 3 | |> File.read!() 4 | |> String.split("") 5 | |> Enum.fetch!(1) 6 | 7 | alias PhoenixImportmap.Importmap 8 | 9 | @doc """ 10 | Returns a `t:PhoenixImportmap.Importmap` struct that implements the `Phoenix.HTML.Safe` protocol, allowing safe interpolation in your template. 11 | 12 | The resulting JSON-formatted importmap is based on your application configuration. 13 | 14 | Requires `YourAppWeb.Endpoint` to be passed in for path generation. 15 | """ 16 | def importmap(endpoint) do 17 | application_importmap() 18 | |> Importmap.prepare(endpoint) 19 | end 20 | 21 | @doc """ 22 | Copies mapped assets to `:copy_destination_path`, which defaults to `/priv/static/assets`. 23 | 24 | For use in `phoenix_importmap.copy` mix task. 25 | """ 26 | def copy() do 27 | application_importmap() 28 | |> Importmap.copy() 29 | end 30 | 31 | @doc """ 32 | Does an initial copy of assets, then starts a child process to watch for asset changes. 33 | 34 | For use with [Phoenix.Endpoint](https://hexdocs.pm/phoenix/Phoenix.Endpoint.html) watchers. 35 | """ 36 | def copy_and_watch(watch_dirs) do 37 | importmap = application_importmap() 38 | 39 | :ok = Importmap.copy(importmap) 40 | PhoenixImportmap.Watcher.start_link(%{importmap: importmap, watch_dirs: watch_dirs}) 41 | end 42 | 43 | defp application_importmap() do 44 | Application.fetch_env!(:phoenix_importmap, :importmap) 45 | end 46 | end 47 | -------------------------------------------------------------------------------- /lib/phoenix_importmap/asset.ex: -------------------------------------------------------------------------------- 1 | defmodule PhoenixImportmap.Asset do 2 | @moduledoc """ 3 | Internal functions for working with asset paths. 4 | """ 5 | 6 | alias PhoenixImportmap.Util 7 | 8 | @doc """ 9 | Determine the copy destination path for a given source path. 10 | """ 11 | def dest_path("//:" <> _), do: nil 12 | def dest_path("http://" <> _), do: nil 13 | def dest_path("https://" <> _), do: nil 14 | 15 | def dest_path(source_path) do 16 | copy_destination_path() <> "/" <> filename(source_path) 17 | end 18 | 19 | @doc """ 20 | Determine the public path for a given source path. This is what will appear 21 | in the output of `PhoenixImportmap.importmap()`. 22 | """ 23 | def public_path("//:" <> _ = source_path), do: source_path 24 | def public_path("http://" <> _ = source_path), do: source_path 25 | def public_path("https://" <> _ = source_path), do: source_path 26 | 27 | def public_path(source_path) do 28 | source_path 29 | |> dest_path() 30 | |> String.replace(public_path_prefix(), "") 31 | end 32 | 33 | @doc """ 34 | Copy an asset from its `source_path` to its `dest_path`. 35 | """ 36 | def maybe_copy(_source_path, nil), do: {:ok, 0} 37 | 38 | def maybe_copy(source_path, dest_path) do 39 | Util.full_path(source_path) 40 | |> File.copy!(Util.full_path(dest_path)) 41 | end 42 | 43 | defp filename(path) do 44 | String.split(path, "/") 45 | |> Enum.at(-1) 46 | end 47 | 48 | defp public_path_prefix() do 49 | Application.get_env(:phoenix_importmap, :public_asset_path_prefix, "/priv/static") 50 | end 51 | 52 | defp copy_destination_path() do 53 | Application.get_env(:phoenix_importmap, :copy_destination_path, "/priv/static/assets") 54 | end 55 | end 56 | -------------------------------------------------------------------------------- /lib/phoenix_importmap/importmap.ex: -------------------------------------------------------------------------------- 1 | defmodule PhoenixImportmap.Importmap do 2 | @moduledoc """ 3 | Provides functions for working with importmaps. 4 | """ 5 | 6 | alias PhoenixImportmap.Asset 7 | 8 | @derive Jason.Encoder 9 | defstruct [:imports] 10 | 11 | @type t() :: %__MODULE__{ 12 | imports: map() 13 | } 14 | 15 | @doc """ 16 | Copies importmap assets to `:copy_destination_path`. 17 | """ 18 | def copy(importmap = %{}) do 19 | importmap 20 | |> Map.values() 21 | |> Enum.map(fn source_path -> 22 | Asset.maybe_copy(source_path, Asset.dest_path(source_path)) 23 | end) 24 | 25 | :ok 26 | end 27 | 28 | @doc """ 29 | Filters an importmap based on a given asset path. 30 | 31 | Used to update only assets that have changed in file watching. 32 | """ 33 | def filter(importmap = %{}, asset_path) do 34 | importmap 35 | |> Enum.reduce(%{}, fn {specifier, path}, acc -> 36 | if asset_path == path, do: Map.put(acc, specifier, path), else: acc 37 | end) 38 | end 39 | 40 | @doc """ 41 | Encodes an importmap into JSON. 42 | """ 43 | def json(importmap = %{}) do 44 | importmap 45 | |> Jason.encode!() 46 | end 47 | 48 | @doc """ 49 | Maps local paths from the configured importmap to the location they are being served from. 50 | 51 | - Strips `:public_asset_path_prefix` from asset paths so they may be resolved 52 | by `Plug.Static`. 53 | - Uses `YourAppWeb.Endpoint.static_path/1` to determine whether to use digest URLs. 54 | """ 55 | def prepare(importmap = %{}, endpoint) do 56 | %__MODULE__{ 57 | imports: 58 | importmap 59 | |> Enum.reduce(%{}, fn {specifier, path}, acc -> 60 | Map.put( 61 | acc, 62 | specifier, 63 | Asset.public_path(path) |> endpoint.static_path() 64 | ) 65 | end) 66 | } 67 | end 68 | 69 | defimpl Phoenix.HTML.Safe do 70 | def to_iodata(importmap) do 71 | Jason.encode_to_iodata!(importmap) 72 | end 73 | end 74 | end 75 | -------------------------------------------------------------------------------- /lib/phoenix_importmap/util.ex: -------------------------------------------------------------------------------- 1 | defmodule PhoenixImportmap.Util do 2 | @moduledoc false 3 | 4 | @doc false 5 | def relative_path(path) do 6 | path 7 | |> String.replace(File.cwd!(), "") 8 | end 9 | 10 | @doc false 11 | def full_path(path) do 12 | File.cwd!() <> path 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /lib/phoenix_importmap/watcher.ex: -------------------------------------------------------------------------------- 1 | defmodule PhoenixImportmap.Watcher do 2 | @moduledoc """ 3 | A child-process which watches `watch_dirs` for changed files, and (if they are present in the project importmap) copies them to `:copy_destination_path`. 4 | 5 | Public entrypoint is `PhoenixImportmap.copy_and_watch/1`. 6 | """ 7 | 8 | use GenServer 9 | 10 | alias PhoenixImportmap.Util 11 | 12 | def start_link(%{importmap: importmap, watch_dirs: watch_dirs}) do 13 | GenServer.start_link(__MODULE__, %{importmap: importmap, watch_dirs: watch_dirs}) 14 | end 15 | 16 | def init(%{importmap: importmap, watch_dirs: watch_dirs}) do 17 | {:ok, _pid} = 18 | FileSystem.start_link( 19 | dirs: Enum.map(watch_dirs, &Util.full_path/1), 20 | name: :phoenix_importmap_file_monitor 21 | ) 22 | 23 | :ok = FileSystem.subscribe(:phoenix_importmap_file_monitor) 24 | {:ok, %{importmap: importmap}} 25 | end 26 | 27 | def handle_info( 28 | {:file_event, _pid, {changed_asset_path, _events}} = _event, 29 | %{importmap: importmap} = state 30 | ) do 31 | importmap 32 | |> PhoenixImportmap.Importmap.filter(Util.relative_path(changed_asset_path)) 33 | |> PhoenixImportmap.Importmap.copy() 34 | 35 | {:noreply, state} 36 | end 37 | 38 | def handle_info(_event, state) do 39 | {:noreply, state} 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /mix.exs: -------------------------------------------------------------------------------- 1 | defmodule PhoenixImportmap.MixProject do 2 | use Mix.Project 3 | 4 | def project do 5 | [ 6 | app: :phoenix_importmap, 7 | version: "0.4.0", 8 | description: description(), 9 | package: package(), 10 | elixir: "~> 1.16", 11 | start_permanent: Mix.env() == :prod, 12 | deps: deps(), 13 | 14 | # Docs 15 | name: "Phoenix Importmap", 16 | source_url: "https://github.com/gilest/phoenix_importmap", 17 | docs: [ 18 | main: "PhoenixImportmap" 19 | ] 20 | ] 21 | end 22 | 23 | # Run "mix help compile.app" to learn about applications. 24 | def application do 25 | [ 26 | extra_applications: [:logger] 27 | ] 28 | end 29 | 30 | # Run "mix help deps" to learn about dependencies. 31 | defp deps do 32 | [ 33 | {:file_system, "~> 1.0"}, 34 | {:jason, "~> 1.4.4"}, 35 | {:phoenix_html, "~> 4.1"}, 36 | {:ex_doc, ">= 0.0.0", only: :dev, runtime: false} 37 | ] 38 | end 39 | 40 | defp description() do 41 | "Use ESM with importmap to efficiently serve JavaScript without transpiling or bundling." 42 | end 43 | 44 | defp package() do 45 | [ 46 | licenses: ["MIT"], 47 | maintainers: ["Giles Thompson"], 48 | links: %{"GitHub" => "https://github.com/gilest/phoenix_importmap"} 49 | ] 50 | end 51 | end 52 | -------------------------------------------------------------------------------- /mix.lock: -------------------------------------------------------------------------------- 1 | %{ 2 | "earmark_parser": {:hex, :earmark_parser, "1.4.44", "f20830dd6b5c77afe2b063777ddbbff09f9759396500cdbe7523efd58d7a339c", [:mix], [], "hexpm", "4778ac752b4701a5599215f7030989c989ffdc4f6df457c5f36938cc2d2a2750"}, 3 | "ex_doc": {:hex, :ex_doc, "0.38.0", "0ab17291b71f9b2c479c0b92404107ac5005214872c3b43f845f6f644ba14f56", [:mix], [{:earmark_parser, "~> 1.4.44", [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", "dee6d6485ef501384fbfc7c90cb0fe621636078bebc0f7a1fd2ddcc20b185013"}, 4 | "file_system": {:hex, :file_system, "1.1.0", "08d232062284546c6c34426997dd7ef6ec9f8bbd090eb91780283c9016840e8f", [:mix], [], "hexpm", "bfcf81244f416871f2a2e15c1b515287faa5db9c6bcf290222206d120b3d43f6"}, 5 | "jason": {:hex, :jason, "1.4.4", "b9226785a9aa77b6857ca22832cffa5d5011a667207eb2a0ad56adb5db443b8a", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "c5eb0cab91f094599f94d55bc63409236a8ec69a21a67814529e8d5f6cc90b3b"}, 6 | "makeup": {:hex, :makeup, "1.2.1", "e90ac1c65589ef354378def3ba19d401e739ee7ee06fb47f94c687016e3713d1", [:mix], [{:nimble_parsec, "~> 1.4", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "d36484867b0bae0fea568d10131197a4c2e47056a6fbe84922bf6ba71c8d17ce"}, 7 | "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"}, 8 | "makeup_erlang": {:hex, :makeup_erlang, "1.0.2", "03e1804074b3aa64d5fad7aa64601ed0fb395337b982d9bcf04029d68d51b6a7", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "af33ff7ef368d5893e4a267933e7744e46ce3cf1f61e2dccf53a111ed3aa3727"}, 9 | "nimble_parsec": {:hex, :nimble_parsec, "1.4.2", "8efba0122db06df95bfaa78f791344a89352ba04baedd3849593bfce4d0dc1c6", [:mix], [], "hexpm", "4b21398942dda052b403bbe1da991ccd03a053668d147d53fb8c4e0efe09c973"}, 10 | "phoenix_html": {:hex, :phoenix_html, "4.2.1", "35279e2a39140068fc03f8874408d58eef734e488fc142153f055c5454fd1c08", [:mix], [], "hexpm", "cff108100ae2715dd959ae8f2a8cef8e20b593f8dfd031c9cba92702cf23e053"}, 11 | } 12 | -------------------------------------------------------------------------------- /test/fixtures/js/app.js: -------------------------------------------------------------------------------- 1 | // Fixture for testing purposes -------------------------------------------------------------------------------- /test/importmap_test.exs: -------------------------------------------------------------------------------- 1 | defmodule PhoenixImportmapImportmapTest do 2 | use ExUnit.Case, async: true 3 | 4 | alias PhoenixImportmap.Util 5 | 6 | @moduletag :tmp_dir 7 | 8 | @example_importmap %{ 9 | app: "/test/fixtures/js/app.js", 10 | remote: "https://cdn.es6/package.js" 11 | } 12 | 13 | setup %{tmp_dir: tmp_dir} do 14 | relative_tmp_dir = Util.relative_path(tmp_dir) 15 | 16 | Application.put_env( 17 | :phoenix_importmap, 18 | :copy_destination_path, 19 | relative_tmp_dir <> "/assets" 20 | ) 21 | 22 | Application.put_env(:phoenix_importmap, :public_asset_path_prefix, relative_tmp_dir) 23 | File.mkdir_p!(tmp_dir <> "/assets") 24 | end 25 | 26 | test "copy succeeds" do 27 | %{app: app_js_path} = @example_importmap 28 | app_js_full_path = Util.full_path(PhoenixImportmap.Asset.dest_path(app_js_path)) 29 | 30 | assert !File.exists?(app_js_full_path) 31 | assert PhoenixImportmap.Importmap.copy(@example_importmap) == :ok 32 | assert File.exists?(app_js_full_path) 33 | end 34 | 35 | test "copy fails on missing file" do 36 | assert_raise(File.CopyError, fn -> 37 | PhoenixImportmap.Importmap.copy(%{ 38 | app: "/test/fixtures/js/missing.js" 39 | }) 40 | end) 41 | end 42 | 43 | test "json" do 44 | assert PhoenixImportmap.Importmap.json(@example_importmap) == 45 | "{\"remote\":\"https://cdn.es6/package.js\",\"app\":\"/test/fixtures/js/app.js\"}" 46 | end 47 | 48 | test "filter" do 49 | assert PhoenixImportmap.Importmap.filter( 50 | %{app: "/test/fixtures/js/app.js", other: "/nonesense"}, 51 | "/test/fixtures/js/app.js" 52 | ) == %{app: "/test/fixtures/js/app.js"} 53 | end 54 | end 55 | -------------------------------------------------------------------------------- /test/phoenix_importmap_test.exs: -------------------------------------------------------------------------------- 1 | defmodule PhoenixImportmapTest do 2 | use ExUnit.Case, async: true 3 | 4 | alias PhoenixImportmap.Util 5 | 6 | doctest PhoenixImportmap 7 | 8 | @moduletag :tmp_dir 9 | 10 | defmodule MockEndpoint do 11 | def static_path(path), do: "#{path}?busted=t" 12 | end 13 | 14 | setup %{tmp_dir: tmp_dir} do 15 | relative_tmp_dir = Util.relative_path(tmp_dir) 16 | 17 | Application.put_env( 18 | :phoenix_importmap, 19 | :copy_destination_path, 20 | relative_tmp_dir <> "/assets" 21 | ) 22 | 23 | Application.put_env(:phoenix_importmap, :public_asset_path_prefix, relative_tmp_dir) 24 | 25 | Application.put_env(:phoenix_importmap, :importmap, %{ 26 | app: "/test/fixtures/js/app.js", 27 | remote: "https://cdn.es6/package.js" 28 | }) 29 | 30 | File.mkdir_p!(tmp_dir <> "/assets") 31 | end 32 | 33 | test "importmap" do 34 | html_escaped_string = 35 | PhoenixImportmap.importmap(MockEndpoint) 36 | |> Phoenix.HTML.html_escape() 37 | |> Phoenix.HTML.safe_to_string() 38 | 39 | assert html_escaped_string == 40 | "{\"imports\":{\"remote\":\"https://cdn.es6/package.js?busted=t\",\"app\":\"/assets/app.js?busted=t\"}}" 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start() 2 | -------------------------------------------------------------------------------- /test/watcher_test.exs: -------------------------------------------------------------------------------- 1 | defmodule WatcherTest do 2 | use ExUnit.Case, async: true 3 | 4 | alias PhoenixImportmap.Util 5 | 6 | @moduletag :tmp_dir 7 | 8 | @example_importmap %{ 9 | app: "/test/fixtures/js/app.js", 10 | remote: "https://cdn.es6/package.js" 11 | } 12 | 13 | setup %{tmp_dir: tmp_dir} do 14 | relative_tmp_dir = Util.relative_path(tmp_dir) 15 | 16 | Application.put_env( 17 | :phoenix_importmap, 18 | :copy_destination_path, 19 | relative_tmp_dir <> "/assets" 20 | ) 21 | 22 | Application.put_env(:phoenix_importmap, :public_asset_path_prefix, relative_tmp_dir) 23 | File.mkdir_p!(tmp_dir <> "/assets") 24 | 25 | {:ok, pid} = 26 | start_supervised( 27 | {PhoenixImportmap.Watcher, 28 | %{ 29 | importmap: @example_importmap, 30 | watch_dirs: ~w(/test/fixtures) 31 | }} 32 | ) 33 | 34 | %{pid: pid} 35 | end 36 | 37 | test "start supervised", %{pid: pid} do 38 | assert pid 39 | end 40 | end 41 | --------------------------------------------------------------------------------