Adding `--tailwind` flag to phx.new

orthogonal https://github.com/pragdave/mix_templates

3 Likes

As the person who literally implemented the Rails Application Templates in the incarnation still used today, they have many pitfalls that may not be obvious unless you maintain them and/or write anything mildly complex with them. For example:

  1. Brittle. Whenever you change the generated code, which is often common, there is a chance template code will break or it will generate the wrong application

  2. They don’t compose. As application templates get more and more complex, it is unlikely you can run multiple of them

  3. Untested. Back then I don’t remember seeing a single application generator which comes with a comprehensive test suite (which would help addressing previous points)

  4. Hard to maintain. It is really hard to maintain an application template as you extend it because they lack structure

But most importantly, I think application templates most often miss the point: what are the odds that you need to execute something exactly when the application is being generated? For example, take mix phx.gen.auth, it could be designed as an --auth flag to phx.new but it is just so much better as its own standalone project.

So what about Tailwind support on Phoenix if it is not part of Phoenix? How would someone approach that? Easy:

$ mix archive.install hex phx_new
$ mix archive.install hex phx_new_tailwind
$ mix phx.new foo
$ cd foo
$ mix phx.new.tailwind # change everything required for tailwind support

While this approach doesn’t necessarily address point 1, because this type of code will always be brittle, it pretty much does better in all other points mentioned above. The beauty of phx_new_tailwind is:

  1. Is its own project, with proper structure, and hopefully tests

  2. Has its own command, which means it has a proper place to document all of its options and behaviour (something you don’t get with application templates iirc)

I am sure this process is nowhere as fleshed out as Rails Application Templates, but I would rather use this approach as a starting point and improve it based on feedback than pick a solution that has many known pitfalls. In any case, it is 100% possible today.

This is not even specific to Phoenix… as the same can be achieved with Rails generators and it is pretty much the approach I preferred back then: devise and simple_form used rails devise:install and rails simple_form:install commands instead of templates. I remember maintaining some front-end based installers way back then too.

14 Likes

I’ve maintained one project for years. Funny enough it ended up being one of my most popular projects on GitHub. I think folks just like the idea of starting off with something instead of a blank slate. From a code organization POV I didn’t find it tough to maintain and it was pretty complex.

At the time I had integration tests too, basically running the CLI tool to generate the app and then run assertions on that generated app to make sure certain things existed. It also even ran the test suite of the generated app to make sure all of its tests passed (which I had a lot of).

Was it perfect? No. Is it the best code ever written? Hell no. I ended up making this thing shortly after learning Rails and building a few real world apps. I figured rolling up all of those patterns I learned into something someone else can use to bootstrap their app quicker with a bit of opinions felt useful.

Totally, but there’s at least 1 missing link.

Right now everyone needs to go off and do their own thing, creating their own custom generators with no unified DSL for manipulating files. That’s why you have all of that generic file and project manipulation code tucked away in the gen auth project. IMO it would be much more beneficial to have that stuff available in an official package (or built into mix, etc.) that anyone can use for their own generators.

Agreed. I ran into this problem with orats even while working on only 1 template and eventually switched away from application templates to just having a skeleton of a project available, and a thin CLI wrapper to customize names of things.

Although Rails Bytes seems to be quite popular nowadays where the community can find and contribute templates.

Maybe the missing links isn’t so much Rails application templates for Phoenix as a direct port, but more so a really slick set of functions to CRUD text in an Elixir / Phoenix project (the DSL) to begin with?

At the end of the day, generating the initial project becomes a trigger to execute the application template. Technically another trigger could do this, but it feels natural to happen during the generation of a new project.

I think running commands is also a key component besides the functions that give you a nice DSL to CRUD files because think back to your Devise gem. It had a generator that would insert files into your project.

A Rails application template could get triggered from creating a new project, add devise to the gemfile, install everything, run the devise generator and edit the files that generator created. This only becomes convenient and possible when you have access to both the DSL for CRUDing files and a means to run commands.

2 Likes

:+1:

Exactly. I would 100% prefer to focus on tackling this problem than providing templates as a whole. Otherwise I expect anyone using templates in anger to run into the same issues as you and eventually move away.

Mix does ship with a Mix.Generator module but its API today is tiny. We definitely can augment it but I also suspect a high-level API that knows about Phoenix specific constructs would be necessary.

1 Like

Wonderful! This phx_new_tailwind may need to pin itself against versions of Phoenix to ensure compatibility since Phoenix generates with foo (for example Webpack) by default, but I don’t see that as a huge problem.

The unfortunate part about separate projects like this is that it requires more instruction to the user and has less visibility.

  • re: visibility: If there is a family of generators like this, it would be helpful to have a section on the Phoenix readme/guides that points to these generators since they’re potentially important to the user when starting a project, so they don’t run mix phx.new before they realize these other starters are available.

  • re: user instruction: if the user is expected to run mix phx.new first and then mix phx.new.tailwind, then that’s adding more friction to these generators that flags like --live don’t have. I’m not suggesting those flags also be separated; just saying that experience is nicer to new users. An alternative is to have the user only run mix phx.new.tailwind, which passes original flags onto mix phx.new and then proceeds to add its own templates and adjustments.

If you have implementation suggestions, I’d love them. Like I said in the original post, I’d love to work on this feature. Thanks for your thoughts!


separately, I found a need to see the diffs between generated Phoenix projects. Anyone know of a tool that shows these? Something like http://railsdiff.org. I’d like to add support for these in https://diff.hex.pm somehow.

2 Likes

Whether we use tailwind itself has other considerations for the team which we are still exploring, so hang tight for now! :slight_smile:

From a maintenance perspective alone on our side, I encourage folks to go the separate project route if they want to ship specialized generators. I don’t foresee us going an extensible template route in any near term future.

3 Likes

I think that would go a long ways and yes, having knowledge about Phoenix would be very helpful. Although I’m not sure how that would pan out in the end for both end users and generator authors.

For example earlier you mentioned this workflow:

$ mix archive.install hex phx_new
$ mix archive.install hex phx_new_tailwind
$ mix phx.new foo
$ cd foo
$ mix phx.new.tailwind # change everything required for tailwind support

As an end user that’s a few extra steps to add Tailwind to a project. Realistically it’ll probably be more steps because I would guess that most folks would install phx_new_tailwind as a dependency of their existing project, generate the files and then remove the dependency since all it did was produce files in a specific location.

I mostly agree with you that despite Bytepack existing, realistically I can’t see anyone cherry picking 15 tiny isolated templates and having them all play nice together with little to no user intervention. There’s just too many conflicts about where things may or may not exist in a file once you have 10 other things writing to that file that you have no control over.

And that leads full circle back to having pre-created applications that have whatever opinions you want baked in, and you take it or leave it with no generators. This works and it’s what I do but it also has its own set of problems like wanting to customize the names of things, so you end up with some crazy shell script to do find / replaces in a bunch of files and directories.

I’ll admit it’s not an easy problem to solve, and tailwind support is just an introduction to the idea of wanting to quickly add custom optional features to an existing base application.

1 Like

My 2 cents on that is:

Most users do not start projects which needs so many customisations from day one (and if they do, then I would say, they do something wrong). In a lot of cases people will do mix phx.new once, and then they will go with the project, because let’s be honest, it is not that often that you start new projects from the ground up. For sure it is less often than adding features to existing projects.

For example, I am using completely different approach for naming my controllers and stuff (instead of MyAppWeb.FooController I use MyAppWeb.Controllers.Foo to follow naming convention used almost all other Elixir projects) which requires me to do “few” manual steps. I do it by the hand each time, and TBH I never had a problem with that, because in last year I needed to do it twice. Oh, and by the way, I am not using Webpack at all, so I need to change it to Parcel as well, so this is quite some time for preparing that. Having such generator maybe would be useful, but I do all of that so rarely, that https://xkcd.com/1319/ and https://xkcd.com/1205/ describes why it is not worth my time.

2 Likes

I love Tailwind, and use it a lot, but I’m not sure that this proposal is a good idea. I realise that there are lots of other folks who would prefer to use Bulma, or Bootstrap or [insert CSS framework here].

I’ve also set up Tailwind with Phoenix, and it really isn’t that difficult if you know your way around Webpack. In fact all you need is the default how to on the Tailwind page.

Regarding generators, there’s perhaps one more piece of context worth adding and a distinction to be made. We have:

  1. application generators like mix phx.new or the potential new mix phx.new.tailwind
  2. phx.gen.* generators, like mix phx.gen.html, mix phx.gen.live, etc.

The latter happen to be configurable to some extent, by default they read templates from Phoenix’s priv/templates directory, but if your app has it’s own priv/templates, that’d take priority. Thus, you can customise the template files like this:

$ cp -R _build/dev/lib/phoenix/priv/templates priv/
$ open priv/templates/phx.gen.live/index.html.leex
$ mix phx.gen.live Blog Post posts title:string # uses your local overrides

I couldn’t find this behaviour documented anywhere and thus there’s probably no explicit contract between mix phx.gen.* and the respective templates, so there’s no compatibility guarantee going forward. However, if there’s a mix phx.gen.tailwind task, that is pinned to a specific Phoenix version, it could generate priv/templates that are augmented with tailwind related changes if that’d be useful.

5 Likes

Hmm, I thought mix phx.new.tailwind would use the phx.new task under the hood but I misunderstood the intention. If it would work that way, though, fwiw we’d have one less step: (not that I personally think having multiple steps is a big deal)

$ mix archive.install hex phx_new
$ mix archive.install hex phx_new_tailwind
$ mix phx.new.tailwind foo
$ cd foo

Archives can’t have deps so we need to explicitly first install phx_new. Again, since we can’t have deps we can’t pin to a specific phoenix version either, however I suppose phx_new_tailwind could vendor a specific phx_new version for maximum compatibility in which case we’d have:

$ mix archive.install hex phx_new_tailwind
$ mix phx.new.tailwind foo
$ cd foo

If the idea for the tailwind generator is to be used on an existing app, I’d consider calling it mix phx.gen.tailwind. A very subtle difference but perhaps worth making.

6 Likes

although I’m a fan of including tailwind, I also acknowledge the criticism… and really like the “gen” idea…

to further complicate the “gen” idea - I feel like it would be also great to have gen “heroku”/“buildpacks” (buildpacks used by various paas platforms,dokku etc), and a gen “docker” (and possibility for others, even react/vue etc down the line)

or the very least include those future requirements, in however the gen “tailwind” solution is crafted…

This is what I imagined too, sort of. I imagined that someone would run phx.new and then run a series of tasks that had been installed locally, e.g.:

mix phx.new my_app --live
cd my_app
mix phx.add.tailwind
mix phx.add.alpine

I wrote up something last night just to see how this might work. I put in on GitHub just as a talking point (not as an example of what I would recommend or a good implementation underneath).

Note I specifically called it “add” and not “gen” only to distinguish it from generators that are part of Phoenix proper or otherwise “official.”

Thinking about this further, I might instead want something like:

mix phx.custom my_app  --live --tailwind --no-ecto --alpine

Where this task parses the options, runs phx.new with the applicable options, and then runs locally archived tasks similar to the first example for the other options. Of course phx.new could do the same thing at some point.

That preserves access to the options that were specified when the stock app was created, which can be helpful. For example, there are some changes that need to be made to app.js for Alpine to work in Live View apps, that aren’t necessary otherwise. It would also give all of the custom generators access what other generators were included and in what order. Theoretically then, if you knew that a generator conflicted with another one, you (or the generator author) could handle that. Which is not to say it isn’t still all brittle.

One problem with this is for generators that have their own arguments (like phx.gen.auth). That could be messy. I suppose you could use prompts as one solution, or you could allow a file to be specified with a list of custom generators and installation switches. I don’t know that I love either of those ideas.

3 Likes

I used to think that too until I started consulting an agency, and what I saw made me change my minds. My clients start new projects fairly frequently. They want to keep the different projects technically as similar as possible to each other (language, frameworks, libraries, CI/CD). When I started working with them they have already built their own custom generator which expanded on phx.new generating some custom stuff (like e.g. deploy pipeline).

One of my main tasks has been introducing common Elixir style & practices to their projects, with the purpose of assisting with project switches. A company-wide custom generator has been indispensable in making this happen, so I’ve spent significant amount of time expanding it. Examples of simpler things we’re doing include generating our own custom credo configuration, or setting up default CI checks (formatter, credo, dialyzer, migrations reversibility, OTP release).

In addition, the generator also performs some more complicated changes on top of phx.new, such as moving db/endpoint configuration to init callbacks, renaming some files, most notably everything under the web folder, together with renaming corresponding modules. Changing some configurations in config scripts, etc. Such changes are made using a hacky combination of regex search & replaces, file operations (e.g. rename), or in some cases by completely overwriting the generated files.

It all feels fragile, and I sometimes wonder if we should completely part ways with phx.new and generate everything ourselves. However, the main challenge is that it’s unclear what exactly should be done to add different layers of Phoenix to the existing non-Phoenix project. Last time I was doing that, I invoked mix phx.new my_existing_project in /tmp, then copied that over the existing project, and carefully analyzed git differences, which was far from perfect. Moreover, once such generator is built, I fear that upgrading it to the next Phoenix is going to be much harder.

A comprehensive step-by-step guide explaining how to add Phoenix to existing project to existing project might be of great help here. I’d expect such guide to cover various scenarios, starting with the basic API, and then expanding with HTML, LiveView, and webpack, and of course including recipes for std configuration (e.g. dev-only live reload, debug errors, etc.). Understanding the changes between two Phoenix version could then amount to diffing the guides, which should be easier to comprehend than diff of the generator project.

Other than that, I’m not really sure what kind of support could be offered by the core generators to simplify the kind of changes we’re doing.

11 Likes

feels more and more like a phx generator contrib pattern requirement

2 Likes