Expanding __DIR__ inside a macro

When defining a macro inside a module, how do you correctly expand __DIR__ such that it’s relative to the module that is including the macro, and not the module the macro is in?

defmodule Demo.EmbeddingJS do 
  defmacro __using__(opts \\ []) do 
    paths = resolve_paths()

    quote do 
      for path <- unquote(paths) do 
        # And this doesn't really seem to do anything, though
        # perhaps we're just not understanding macros properly.
        @external_resource path
      end
  
      # ... snip ...
    end 
  end

  defp resolve_paths do
    # This expands to "this" Module's directory, when we really
    # want it to expand to the directory of the Module using it.
    Path.expand(__DIR__)
      |> Path.join("templates_html/**/*.js")
      |> Path.wildcard()
  end
end

You need to put everything in resolve_paths inside the quote block. As you have it now, you are unquoteing the result of running resolve_paths in the context of where it’s at at compile time. From your last post, you actually want all the Path.expand(__DIR__) stuff to be written to the useing module, so you need to include it in the returned AST.

defmodule Demo.EmbeddingJS do 
  defmacro __using__(_) do 
    quote do
      paths =
        Path.expand(__DIR__)
        |> Path.join("templates_html/**/*.js")
        |> Path.wildcard()

      for path <- paths do 
        @external_resource path
      end
    end
  end
end
1 Like

Ah, ok. Thank you.

Is there a way to “break-up” the __using__ method such that we can still properly resolve things like __DIR__ and Application.compile_env!(...)?

For compile_env, we’re defining different arguments to pass to an external command based on dev/prod. (ie: We’re not minifying JS in dev, only prod.)

We tried to take inspiration from this article where they use helper functions to “gather up” all the macro dependencies but, as noted above, ran into issues.

Yes, but I never do it so I never feel confident to say it :sweat_smile: But the answer is in your MyAppWeb! Basically you define regular functions that return quoted expressions then return those from __using__:

defmodule Foo do
  def set_mod do
    quote do
      @mod __MODULE__
    end

  defmacro __using__(_) do
    set_mod()
  end
end

defmodule Bar do
  use Foo

  def get_mod, do: @mod
end
iex> Bar.get_mod()
Bar

The thing to keep in mind is that macros are just regular functions that run at compile time. quote on the other hand is a little more magical, but macro bodies just run in whatever context they are called in at compile time only. Only what is returned from the quoted expression ends up the resulting module.

Take a look at your MyAppWeb module for inspiration on how to pass it args!

Will do. We also picked up the book Metadata Programming Elixir from Pragmatic and will go through it.

We’re a bit more used to macros and preprocessors in C, so the differences are tripping us up. Appreciate the quick responses.

No prob, happy to share more though just waiting on a friend to get ready so we can hang out!

That book will definitely help. I’m somewhat familiar with C macros but not totally but from what I understand it’s more like inserting some code verbatim into another context? Elixir macros are quite close to that expect they have the hygiene and all the other options that come with them. It helped me to think of quote as a literal string " as that’s basically what it is.

In your original example, if we pretend that Elixir has an eval function instead of use, you were effectively doing this:

defmodule Demo.EmbeddingJS do 
  defmacro __using__(opts \\ []) do 
    paths = resolve_paths()

    code = """
    for path <- #{paths()} do 
      @external_resource path
    end
    """
  end

  defp resolve_paths do
    Path.expand(__DIR__)
    |> Path.join("templates_html/**/*.js")
    |> Path.wildcard()
  end

then:

defmodule Foo do
  eval Demo.EmbeddingJS.__using__()
end

The other thing I only just noticed is that your # ... snip ... is actually inside the defmacro! This means that you may not be returning the quoted expression! And if you are returning another quoted expression, the first one isn’t getting included.

EDIT: Had to fix my example a little as I realized part of it didn’t make much sense.

OK, I think we’re really close. For the most part what we have below is working. Is there a better way to write this or clean it up a bit? The embedded function component declaration is kind of messy…

Bonus round: The @external_resource declarations appear to be working however compilation does not trigger a reload in the browser, the way it would when you edit a heex template. (ie: If we edit the Javascript file, we have to reload the web browser to see changes.)

defmodule Demo.EmbedJS do
  # List of arguments for Terser. 
  # Does this have to be a Module attribute?
  # ex: ["terser", "--compress", "--mangle"]
  @terser_args Application.compile_env!(:listloupe, :terser_args)

  defmacro __using__(_opts) do
    quote do

      source_dir = Path.join(File.cwd!(), "assets")

      paths =
        Path.expand(__DIR__)
        |> Path.join("templates_html/**/*.js")
        |> Path.wildcard()

      file_map =
        paths
        |> Enum.map(fn p -> {Path.basename(p), unquote(@terser_args) ++ [p]} end)
        |> Enum.into(%{}, fn {file, args} -> {file, System.cmd("npx", args, cd: source_dir) |> elem(0)} end)

      @paths paths
      @file_map file_map

      for path <- paths do
        @external_resource path
      end

      # This is pretty ugly, is there anyway to clean it up?
      def script(var!(assigns)) do
        filename = var!(assigns).filename
        source = Map.fetch!(@file_map, filename)
        var!(assigns) = assign(var!(assigns), source: source)

        ~H"""
        <script>
          (function() {
            <%= raw(@source) %>
          })();
        </script>
        """
      end
    end
  end
end

From .heex template:

<div>My Div</div>
<.script filename="helpers.js" />

Solved by simply adding the js extension to the live_reloading: setting in dev.exs:

config :listloupe, ListloupeWeb.Endpoint,
  live_reload: [
    patterns: [
      ~r"priv/static/.*(js|css|png|jpeg|jpg|gif|svg)$",
      ~r"priv/gettext/.*(po)$",
      ~r"lib/listloupe_web/(controllers|live|components)/.*(ex|heex|js)$"
    ]
  ]
1 Like

Hey, sorry I’m trying to stay off Elixir Forum today as I have a lot to do :slight_smile:

Glad you figured out the live reloading!

I haven’t read your linked article in detail but this line sticks out to me—which is a bit embarassing as I copy-pasta’d it twice in my answers, but I was focused on other aspect :upside_down_face::

From what I can see in the docs, @external_resource does not accumulate, so the above code is just going to set it to whatever the last value in paths is. If this is what you want, I would do it another way.

I’m just in code review mode here but not sure if you meant to leave this here.

Honestly for the small amount of code you have in this module I think it’s all fine. It’s easy to read and digest and I personally wouldn’t see any real win in extracting anything. Of course what I’m getting at is that I don’t have any ideas of how to clean it up off the top of my head as I wouldn’t personally change it. Maybe someone else can offer some advice! It does get tricky as soon as you start using var!.

You’re right. What is the correct syntax to output multiple @external_resource lines inside a quote?

Sorry nm, what you have is fine. I wasn’t actually aware of @external_resource before this thread and didn’t look at the context in the docs. It is indeed an accumulating module attr.