Avoiding compile dependencies in router via plugs

I’m fighting against a mess of compile dependencies.

One issue is that many things have a runtime dependency on the router (ok) and that the router has multiple compile-time dependencies on views and layouts (not ok).

I notice that changing the modules of these views to Module.concat solves the problem:

  # router.ex

  # introduces a compile-time dependency
  pipeline :example do
    plug(:put_layout, {MyApp.LayoutView, :example})
  end
  # no dependency
  pipeline :example do
    plug(:put_layout, {Module.concat(:MyApp, :LayoutView), :example})
  end
  1. Is this expected / am I missing something?

  2. Before I write a macro to do this, is there a better way to resolve this?

This kind of issue does not seem unique.

I was naively thinking that this pattern was pretty natural and would be handled properly, either by put_layout accepting a string for the module name, or by plug being a smart macro that could avoid the compile-time dependency altogether, or by pipelines being in a different files from routes for example.

1 Like

One issue is that many things have a runtime dependency on the router (ok) and that the router has multiple compile-time dependencies on views and layouts (not ok).

The combination of:

  1. Elixir v1.11 (separately tracks imports so these don’t become compile-time deps)
  2. plug_init_mode (What's new in Phoenix development - February 2018 - DockYard)
  3. explicit router helper aliases (ditto; also thanks to 1. should be less of an issue in general)

should hopefully have alleviated those issues. Have you already tried all three? If not, did it help at all?

plug_init_mode is only added to config/dev.exs by default which means when running tests we’d potentially have more recompilations than in dev, perhaps we should be adding it to test.exs too, not sure.

Sorry, forgot to mention, this is

  1. using shiny new Elixir 1.12.1 + OTP 24
  2. plug_init_mode is set to :runtime in dev mode (and I wish I knew if it could be in test mode).
  3. probably not using explicit router help aliases. We probably should do that to avoid some recompilation when we change routes, but my understanding though is that there is no fundamental reason for the router to have a compile-time dependency on these layouts, right?

That mix xref graph --source lib/app_web/router.ex --label compile has refs to lib/app_web/plugs/ is fine, but lib/app_web/views/ is not.

Actually, my followup question is: I understand this better, but can I avoid it too?

Very surprising to me, but moving a plug module from pipe_through and by using an intermediary pipeline removes the compile-time dependency?!

Again, is that expected, where is that documented, and can’t this be improved?

$ mix xref graph --source lib/bobby_web/router.ex --label compile  | wc -l
      113
$ git stash pop && git diff

lib/bobby_web/router.ex
──────────────────────────────────┐
429: defmodule BobbyWeb.Router do β”‚
β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

+ pipeline :active_policies do
+   plug Plugs.ActivePolicies
+ end

  scope "/", BobbyWeb do
-   pipe_through([:browser, Plugs.ActivePolicies])
+   pipe_through([:browser, :active_policies])

$ mix xref graph --source lib/bobby_web/router.ex --label compile  | wc -l
     86
1 Like

For layout files I usually create a separate plug instead of calling :put_layout in the router:

I’m surprised that an intermediary pipeline would change the compile-time dependencies.

Thanks. It’s cleaner than my β€œsolution”, but more verbose.