I’m following the Pragmatic Studio Full-Stack Phoenix course and so far everything’s been going smoothly, but I just ran into a weird bug that I’m trying to figure out by myself and I can’t imagine where to begin.
Some liveview routes that were working before stopped working, and even when I try to undo the last changes I did and stop and restart the server, they’re still not working, and I can’t figure out what’s wrong from the error message because it looks so strange to me.
I have this route, for an instance: live "/estimator", EstimatorLive
Corresponding to the live view I have at lib/raffley_web/live/estimator_live.ex. But now, when I run the server and try to browse to http://localhost:4000/estimator, this is the error I get:
UndefinedFunctionError at GET /estimator
function RaffleyWeb.RaffleyWeb.EstimatorLive.__live__/0 is undefined (module RaffleyWeb.RaffleyWeb.EstimatorLive is not available)
For the life of me I can’t understand why it’s looking for RaffleyWeb.RaffleyWeb.EstimatorLive.__live__/0 rather than RaffleyWeb.EstimatorLive.__live__/0. Can anyone shed some light on what I might’ve screwed up to get this issue…?
This usually happens when you are using an LSP that automatically adds aliases for you.
The way module names are “stitched” together in the router is done with a bit of magic that isn’t really done anywhere else. Since you are a beginner I won’t dive into it, but I’m assuming there is a line near the top of your router that looks like this:
On my phone, but your problem is probably from a scope “route”, MyAppWeb in your router. Perhaps someone else can provide a more comprehensive example of that issue as we’ve all done it before
defmodule RaffleyWeb.Router do
alias RaffleyWeb.EstimatorLive
alias RaffleyWeb.RaffleLive
HOW DID THEY GET THERE… I never would’ve found them since I wasn’t editing that part of the file at all… I know what to watch out for now Thank you so much!
It’s your LSP trying to be helpful. Normally it is helpful just not in the router.
It’s a bit hard to explain but when you have:
scope “route”, MyAppWeb do
live "/", EstimatorLive
end
it may look like a language feature to break up a module name like that, but it’s actually a Phoenix macro (scope) that combines them into MyAppWeb.EstimatorLive at compile time (EstimatorLive on its own is not a valid module in this context). But before that happens, aliases are expanded which is why you end up with MyAppWeb.MyAppWeb.EstimatorLive. I believe this is more about avoiding compile-time dependencies than it is convenience but ya, I won’t dive into that if you are just starting out
I’m honestly not a big fan of the approach of defining partial modules from day one, the fact that it doesn’t play well with the language server is even more annoying. I think if I knew only elixir today, I would have also took a long time to understand wtf is going on, the default generated route when creating a new phoenix project makes it easier to understand, but still.
For some reason @sasajuric implemented a similar concept in boundary, maybe he can enlighten us more on this decision?
I remember Chris talking about it having to do with compile time dependencies, and I’d believe it as I have done some similar tricks in different parts of the router to fool it into thinking an atom wasn’t referring to a module (this was on an existing project that had a crap ton of compile time deps). I tried to do a little demo to prove it but I can’t make it work soooooo you’d have to wait on other responses. I’m guessing Boundary is more for convenience (as I also couldn’t make it give me compile time deps but again I didn’t try too hard) but ya, we just added it to a new project last week and my coworker totally got tripped up by it I actually do appreciate the convenience aspect in Boundary, though. Especially if you are dealing with overly stringent Credo rules that force you to do this:
alias __MODULE__.Foo
alias __MODULE__.Bar
alias __MODULE__.Baz
use Boundary, export: [Foo, Bar, Baz]
These issues with compile time dependencies could be gone by now. There have been lot of improvments to that in elixir over time. But it was indeed a real concern in earlier times.
Ya, I wondering if this was the case too and so the syntax is just a relic. It was not too long ago that just naming an atom that referred to a module—ie, without calling a function from it—in a module body/macro would create a dependency but that is no longer the case.
I had never looked closely at the signature for scope and I see that scope("/", Module) is a convenience for scope("/", alias: Module). The latter is a little clearer. At this point status quo doesn’t really bother me at all, but it does cause confusion for beginners.