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

Digest dynamic/async imports in javascript assets #6003

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
Draft
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
51 changes: 49 additions & 2 deletions lib/phoenix/digester.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
32 changes: 32 additions & 0 deletions test/fixtures/digest/priv/static/async_import.js
Original file line number Diff line number Diff line change
@@ -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);
});
}
80 changes: 79 additions & 1 deletion test/phoenix/digester_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down