This was a project I created a hacky proof-of-concept for over a year ago
that I've picked back up, incidentally not inspired by the existing hex
package <https://github.com/gausby/scaffold>.
I'd intended to make it available as an archive, but as I started looking
at the implementation of Mix.Tasks.Archive.Install I realized integrating
with Mix.Local.Installer would greatly simplify it and enhance mix itself.
I figured I should pass this around before I get too deep into my fork of
mix, which is already halfway feature complete (not including rewriting mix
new to use scaffolding).
Mix.Tasks.Scaffold
*Goal:*
- Improve mix's ability to generate file systems in file generation tasks
like mix new, mix phoenix.gen.x, and others.
- Standardize file system generation procedures to allow portability of
such code.
- Finally, transparently allow mix users override project-provided file
system templates with their own.
A scaffold can be any file, folder, or tarball intended to act as as a
template file or file system of template files.
There are three clients of this code. To hoist some already-overloaded
terminology:
- producers: projects that want to generate files, but opt-in allow users
complete control of what gets generated
- consumers: developers using the producer project as a dependency that
might want to customize the files it generates
- providers: developers that want to distribute packaged alternative
scaffolds to consumers
Producers
Instead of using Mix.Generator by hand to generate filesystems, producers
can specify a :scaffold setting in their project config.
The scaffold configuration is either a path to a template
directory/file/tarball, or a list of two-tuples of scaffold name and path
to corresponding template directory/file/tarball.
Example:
def project do
[app: :phoenix,
scaffold: [
{"config", "priv/scaffolds/config.exs"},
{"new", "priv/scaffolds/new"},
{"ecto", "priv/scaffolds/ecto.tar.gz"},
]
]
end
When they want to generate files, for example by unpacking the ecto
scaffold into the foo/bar directory, they can use:
Mix.Project.config()
|> Keyword.fetch(:phoenix)
|> Mix.Local.fetch_scaffold("ecto")
|> Mix.Generator.create_files("foo/bar", force: true, eex: [assigns: [foo:
bar]])
If the project wants to defer to user-defined scaffolds, it can replace
fetch_scaffold with get_scaffold:
Mix.Project.config()
|> Keyword.fetch(:phoenix)
|> Mix.Local.get_scaffold("ecto")
|> Mix.Generator.create_files("foo/bar", force: true, eex: [assigns: [foo:
bar]])
*Functions:*
- Mix.Local.fetch_scaffold(:otp_app, "namespace" \\ nil)
Looks for scaffolds for the :otp_app base on its project config, completely
ignoring local user scaffold installs. Uses namespace for apps with
multiple scaffolds.
Returns path to a file, directory, or tarball so is suitable for
consumption by Mix.Generator.create_files.
- Mix.Local.get_scaffold(:otp_app, "namespace" \\ nil)
Looks for scaffolds for :otp_app first in local user installs, and falls
back to default defined in :otp_app project config. Uses namespace for apps
with multiple scaffolds.
Returns path to a file, directory, or tarball so is suitable for
consumption by Mix.Generator.create_files.
Namespaces are allowed to be relative file paths for namespace heirarchies.
- Mix.Generator.create_files("path/to/thing", "target_dir" \\ nil, opts \\
[])
Constructs a file, directory, or tarball into target_dir, using nice
Generator.create_file logging and overwrite prompts.
Opts include :force to not prompt on overwrites, and :eex to enable EEx
interpolation.
- Mix.Generator.create_file("path/to/file", "source", opts \\ [])
Same as current implementation, but now checks for a :eex opt during
processing.
If :eex is truthy, it will pass both the filepath and the source
through EEx before writing to disk.
If :eex is a list, it will use that as a binding during interpolation.
Consumers
People using producer projects can continue on just as before. However, if
they want to provide their own scaffolding, they simply put it in
~/.mix/scaffolds/:otp_app/namespace.
The Mix.Local.get_scaffold call in the producer project will find their
local version and honor it.
Scaffolds can be files, folders, or tarballs. This supports several
usecases:
- Keeping a personal scaffold for mix new
- Sharing git-distributed phoenix scaffold preferences inside an org
- Installing tarball scaffolds from remote sources
*Tasks:*
- mix scaffold.install
Installs file, folder, or tarball scaffolds from local path or remote uri
into ~/.mix/scaffolds/*
Remote uris must point to a single file or tarball; alternatively they can
be git uris and will be installed as a folder.
Tarballs are left alone during install: they're only extracted during the
actual file generation step.
- mix scaffold
Lists installed scaffolds
- mix scaffold.uninstall
Removes installed scaffold.
Providers
Creating a scaffold for distribution is as easy as hosting a file, tarball,
or git repository.
However, special support for projects that want to offer scaffold for other
otp_apps is available through mix scaffold.build.
*Tasks:*
- mix scaffold.build
Providers that want to distribute scaffolds as tarballs can use this task.
This mechanism should be preferred because it generates tarballs with the
expected paths for consumption by Mix.Local.get_scaffold.
It builds the tarball from the exact same :scaffold project configuration
as producers.
The target :otp_app is specified via :scaffold_app. If not provided, it
just uses the project :app name, making all producers able to run this
command on their default templates out of the box.
Alternatively, multiple otp_apps can be targeted by providing a Keyword.t
scaffold configuration of otp apps and scaffold configurations.
Enhancements
The phoenix generators currently rely on 4 different manually specified
<https://github.com/phoenixframework/phoenix/blob/90f8b45163a545b264787b73388cb2356d775c3a/installer/lib/phoenix_new.ex#L11-L76>
template generation modes
<https://github.com/phoenixframework/phoenix/blob/90f8b45163a545b264787b73388cb2356d775c3a/installer/lib/phoenix_new.ex#L536-L546>
:
- :eex interpolate file
This is the default behaviour of a scaffold with eex enabled, as phoenix
likely will always do. As proposed there is no way to disable this per file.
- :text raw file
This is the default behaviour of a scaffold with eex disabled. In order to
emulate old phoenix behaviour, these should be rewritten with eex inside of
them using eex quotations: <%% "<%= raw eex%>" %>
- :keep empty folder
This behaviour is totally unsupported by scaffolds, which operates entirely
on mkdirp!ing a filepath Dir.name before writing to it. This is probably
fine: scaffolds should include a .gitkeep in their stead.
- :append to existing file
This is the only behaviour totally unsupported with no workaround, used
exactly once in phoenix for css.
*Prompt for append:*
This lacking could be remedied by augmenting Mix.Utils.can_write? to offer
a third option: append instead of just y/n overwrite. Alternatively
Mix.Utils.should_append? could be added, putting the user through two
prompts for append behaviour.
This implies that passing force: true would eliminate any opportunity to
append.
Maybe we could support additional force strategies: force: :caution for
never overwriting, force: :append for pure addition, and even force:
:conflict to generate git merge conflict syntax around the entire file
instead.
*Sprock it:*
Another approach would be to have mix scaffold.build and
Mix.Generator.create_files deal in middleware that parses certain
intent-revealing file extensions a la Rails Sprockets, breaking portability
and inviting doom to this RFC.
Summary
Everything proposed is 100% backwards compatible and opt-in; mix will
behave as it used to in all existing calls to Mix.Generate and Mix.Local
functions. Ie. no tests need to be modified in my proof of concept; only
added. Projects that choose to use scaffolds will need to depend on at
least the version of elixir that first shipped mix with scaffold support,
so this should be incorporated into an elixir minor bump.
Everything should just work when presented with symlinks. Enhancements to
Mix.Local.Installer, like ability to read SFTP or other uri protocols,
propagate to scaffolds.
Experimenting with local scaffold handling kind of gears me up for
returning to my previous, more ambitious mix proposal
<https://groups.google.com/forum/#!searchin/elixir-lang-core/local$20dependency%7Csort:relevance/elixir-lang-core/yGPZ3rM-ETc/2mmG8Wl0AAAJ>
.
What do you think about this? Sufficiently useful to bring in to core?
Opinions on rewriting mix new and phoenix to use it? Any other projects
that might benefit from this sort of scaffolding?
--
You received this message because you are subscribed to the Google Groups
"elixir-lang-core" group.
To unsubscribe from this group and stop receiving emails from it, send an email
to [email protected].
To view this discussion on the web visit
https://groups.google.com/d/msgid/elixir-lang-core/4606a92d-3512-497a-a73d-6756fc222672%40googlegroups.com.
For more options, visit https://groups.google.com/d/optout.