From 1d3c0c6f557101260abf6a2eb65b6d149b25dddf Mon Sep 17 00:00:00 2001 From: Tom Taylor Date: Sat, 7 Dec 2024 11:07:57 +0000 Subject: [PATCH] Digest async imports in javascript assets --- lib/phoenix/digester.ex | 51 +++++++++++- .../digest/priv/static/async_import.js | 32 ++++++++ test/phoenix/digester_test.exs | 80 ++++++++++++++++++- 3 files changed, 160 insertions(+), 3 deletions(-) create mode 100644 test/fixtures/digest/priv/static/async_import.js diff --git a/lib/phoenix/digester.ex b/lib/phoenix/digester.ex index 69b1b4fa84..6b6ec89cba 100644 --- a/lib/phoenix/digester.ex +++ b/lib/phoenix/digester.ex @@ -256,14 +256,61 @@ defmodule Phoenix.Digester do end) end + defp digest_javascript_asset_references(file, latest) do + content = file.content + + content + |> digest_javascript_asset_source_map_references(file, latest) + |> digest_javascript_asset_await_import_references(file, latest) + |> digest_javascript_asset_promise_import_references(file, latest) + end + @javascript_source_map_regex ~r{(//#\s*sourceMappingURL=\s*)(\S+)} - defp digest_javascript_asset_references(file, latest) do - Regex.replace(@javascript_source_map_regex, file.content, fn _, source_map_text, url -> + defp digest_javascript_asset_source_map_references(content, file, latest) do + Regex.replace(@javascript_source_map_regex, content, fn _, source_map_text, url -> source_map_text <> digested_url(url, file, latest, false) end) end + @javascript_await_import_regex ~r/(await import)(\()([^\)]+)(\))/ + + defp digest_javascript_asset_await_import_references(content, file, latest) do + Regex.replace(@javascript_await_import_regex, content, fn str, prefix, open, url, close -> + case Regex.run(@quoted_text_regex, url) do + [_, quote_symbol, url] -> + prefix <> + open <> + quote_symbol <> digested_url(url, file, latest, false) <> quote_symbol <> close + + nil -> + str + end + end) + end + + @javascript_promise_import_regex ~r/(import)(\()([^\)]+)(\))(.then)/ + + defp digest_javascript_asset_promise_import_references(content, file, latest) do + Regex.replace(@javascript_promise_import_regex, content, fn str, + prefix, + open, + url, + close, + suffix -> + case Regex.run(@quoted_text_regex, url) do + [_, quote_symbol, url] -> + prefix <> + open <> + quote_symbol <> + digested_url(url, file, latest, false) <> quote_symbol <> close <> suffix + + nil -> + str + end + end) + end + @javascript_map_file_regex ~r{(['"]file['"]:['"])([^,"']+)(['"])} defp digest_javascript_map_asset_references(file, latest) do diff --git a/test/fixtures/digest/priv/static/async_import.js b/test/fixtures/digest/priv/static/async_import.js new file mode 100644 index 0000000000..bb0abb7762 --- /dev/null +++ b/test/fixtures/digest/priv/static/async_import.js @@ -0,0 +1,32 @@ +async function asyncImport() { + const app = await import("./app.js"); + console.log(app); +} + +async function defaultAsyncImport() { + const app = (await import("./app.js")).default; + console.log(app); +} + +function promiseImport() { + import("./app.js").then((app) => { + console.log(app); + }); +} + +async function noDigestImport() { + const thing = await import("https://example.com/thing.js"); + console.log(thing); +} + +async function notAPathAsyncImport() { + const notAsyncPath = {}; + await import(notAsyncPath); +} + +async function notAPathPromiseImport() { + const notPromisePath = {}; + import(notPromisePath).then(function (thing) { + console.log(thing); + }); +} diff --git a/test/phoenix/digester_test.exs b/test/phoenix/digester_test.exs index 708e264c21..95b1a4b17a 100644 --- a/test/phoenix/digester_test.exs +++ b/test/phoenix/digester_test.exs @@ -19,7 +19,8 @@ defmodule Phoenix.DigesterTest do describe "compile" do test "fails when the given paths are invalid" do - assert {:error, :invalid_path} = Phoenix.Digester.compile("nonexistent path", "/ ?? /path", true) + assert {:error, :invalid_path} = + Phoenix.Digester.compile("nonexistent path", "/ ?? /path", true) end test "digests and compress files" do @@ -326,6 +327,83 @@ defmodule Phoenix.DigesterTest do assert digested_js_map =~ ~r"#{digested_js_filename}" end + test "digests await import paths found within javascript source files" do + input_path = "test/fixtures/digest/priv/static/" + assert :ok = Phoenix.Digester.compile(input_path, @output_path, true) + + digested_js_filename = + assets_files(@output_path) + |> Enum.find(&(&1 =~ ~r"async_import-#{@hash_regex}.js")) + + digested_app_js_filename = + assets_files(@output_path) + |> Enum.find(&(&1 =~ ~r"app-#{@hash_regex}.js")) + + digested_js = Path.join(@output_path, digested_js_filename) |> File.read!() + assert String.contains?(digested_js, "await import(\"./#{digested_app_js_filename}\");") + + assert String.contains?( + digested_js, + "(await import(\"./#{digested_app_js_filename}\")).default;" + ) + end + + test "does not digest await import called not on a path" do + input_path = "test/fixtures/digest/priv/static/" + assert :ok = Phoenix.Digester.compile(input_path, @output_path, true) + + digested_js_filename = + assets_files(@output_path) + |> Enum.find(&(&1 =~ ~r"async_import-#{@hash_regex}.js")) + + digested_js = Path.join(@output_path, digested_js_filename) |> File.read!() + assert String.contains?(digested_js, "await import(notAsyncPath);") + end + + test "digests promise based import paths found within javascript source files" do + input_path = "test/fixtures/digest/priv/static/" + assert :ok = Phoenix.Digester.compile(input_path, @output_path, true) + + digested_js_filename = + assets_files(@output_path) + |> Enum.find(&(&1 =~ ~r"async_import-#{@hash_regex}.js")) + + digested_app_js_filename = + assets_files(@output_path) + |> Enum.find(&(&1 =~ ~r"app-#{@hash_regex}.js")) + + digested_js = Path.join(@output_path, digested_js_filename) |> File.read!() + + assert String.contains?( + digested_js, + "import(\"./#{digested_app_js_filename}\").then((app) => {" + ) + end + + test "does not digest promise based import called not on a path" do + input_path = "test/fixtures/digest/priv/static/" + assert :ok = Phoenix.Digester.compile(input_path, @output_path, true) + + digested_js_filename = + assets_files(@output_path) + |> Enum.find(&(&1 =~ ~r"async_import-#{@hash_regex}.js")) + + digested_js = Path.join(@output_path, digested_js_filename) |> File.read!() + assert String.contains?(digested_js, "import(notPromisePath).then(") + end + + test "does not digest import paths that don't exist" do + input_path = "test/fixtures/digest/priv/static/" + assert :ok = Phoenix.Digester.compile(input_path, @output_path, true) + + digested_js_filename = + assets_files(@output_path) + |> Enum.find(&(&1 =~ ~r"async_import-#{@hash_regex}.js")) + + digested_js = Path.join(@output_path, digested_js_filename) |> File.read!() + assert String.contains?(digested_js, "await import(\"https://example.com/thing.js\");") + end + test "does not digest assets within undigested files" do input_path = "test/fixtures/digest/priv/static/" assert :ok = Phoenix.Digester.compile(input_path, @output_path, true)