-
Notifications
You must be signed in to change notification settings - Fork 2.8k
/
phx.new.ex
399 lines (305 loc) · 11 KB
/
phx.new.ex
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
defmodule Mix.Tasks.Phx.New do
@moduledoc """
Creates a new Phoenix project.
It expects the path of the project as an argument.
$ mix phx.new PATH [--module MODULE] [--app APP]
A project at the given PATH will be created. The
application name and module name will be retrieved
from the path, unless `--module` or `--app` is given.
## Options
* `--umbrella` - generate an umbrella project,
with one application for your domain, and
a second application for the web interface.
* `--app` - the name of the OTP application
* `--module` - the name of the base module in
the generated skeleton
* `--database` - specify the database adapter for Ecto. One of:
* `postgres` - via https://github.com/elixir-ecto/postgrex
* `mysql` - via https://github.com/elixir-ecto/myxql
* `mssql` - via https://github.com/livehelpnow/tds
* `sqlite3` - via https://github.com/elixir-sqlite/ecto_sqlite3
Please check the driver docs for more information
and requirements. Defaults to "postgres".
* `--adapter` - specify the http adapter. One of:
* `cowboy` - via https://github.com/elixir-plug/plug_cowboy
* `bandit` - via https://github.com/mtrudel/bandit
Please check the adapter docs for more information
and requirements. Defaults to "bandit".
* `--no-assets` - equivalent to `--no-esbuild` and `--no-tailwind`
* `--no-dashboard` - do not include Phoenix.LiveDashboard
* `--no-ecto` - do not generate Ecto files
* `--no-esbuild` - do not include esbuild dependencies and assets.
We do not recommend setting this option, unless for API only
applications, as doing so requires you to manually add and
track JavaScript dependencies
* `--no-gettext` - do not generate gettext files
* `--no-html` - do not generate HTML views
* `--no-live` - comment out LiveView socket setup in your Endpoint
and assets/js/app.js. Automatically disabled if --no-html is given
* `--no-mailer` - do not generate Swoosh mailer files
* `--no-tailwind` - do not include tailwind dependencies and assets.
The generated markup will still include Tailwind CSS classes, those
are left-in as reference for the subsequent styling of your layout
and components
* `--binary-id` - use `binary_id` as primary key type in Ecto schemas
* `--verbose` - use verbose output
* `-v`, `--version` - prints the Phoenix installer version
When passing the `--no-ecto` flag, Phoenix generators such as
`phx.gen.html`, `phx.gen.json`, `phx.gen.live`, and `phx.gen.context`
may no longer work as expected as they generate context files that rely
on Ecto for the database access. In those cases, you can pass the
`--no-context` flag to generate most of the HTML and JSON files
but skip the context, allowing you to fill in the blanks as desired.
Similarly, if `--no-html` is given, the files generated by
`phx.gen.html` will no longer work, as important HTML components
will be missing.
## Installation
`mix phx.new` by default prompts you to fetch and install your
dependencies. You can enable this behaviour by passing the
`--install` flag or disable it with the `--no-install` flag.
## Examples
$ mix phx.new hello_world
Is equivalent to:
$ mix phx.new hello_world --module HelloWorld
Or without the HTML and JS bits (useful for APIs):
$ mix phx.new ~/Workspace/hello_world --no-html --no-assets
As an umbrella:
$ mix phx.new hello --umbrella
Would generate the following directory structure and modules:
```text
hello_umbrella/ Hello.Umbrella
apps/
hello/ Hello
hello_web/ HelloWeb
```
You can read more about umbrella projects using the
official [Elixir guide](https://hexdocs.pm/elixir/dependencies-and-umbrella-projects.html#umbrella-projects)
"""
use Mix.Task
alias Phx.New.{Generator, Project, Single, Umbrella, Web, Ecto}
@version Mix.Project.config()[:version]
@shortdoc "Creates a new Phoenix v#{@version} application"
@switches [
dev: :boolean,
assets: :boolean,
esbuild: :boolean,
tailwind: :boolean,
ecto: :boolean,
app: :string,
module: :string,
web_module: :string,
database: :string,
binary_id: :boolean,
html: :boolean,
gettext: :boolean,
umbrella: :boolean,
verbose: :boolean,
live: :boolean,
dashboard: :boolean,
install: :boolean,
prefix: :string,
mailer: :boolean,
adapter: :string
]
@impl true
def run([version]) when version in ~w(-v --version) do
Mix.shell().info("Phoenix installer v#{@version}")
end
def run(argv) do
elixir_version_check!()
case OptionParser.parse!(argv, strict: @switches) do
{_opts, []} ->
Mix.Tasks.Help.run(["phx.new"])
{opts, [base_path | _]} ->
if opts[:umbrella] do
generate(base_path, Umbrella, :project_path, opts)
else
generate(base_path, Single, :base_path, opts)
end
end
end
@doc false
def run(argv, generator, path) do
elixir_version_check!()
case OptionParser.parse!(argv, strict: @switches) do
{_opts, []} -> Mix.Tasks.Help.run(["phx.new"])
{opts, [base_path | _]} -> generate(base_path, generator, path, opts)
end
end
defp generate(base_path, generator, path, opts) do
base_path
|> Project.new(opts)
|> generator.prepare_project()
|> Generator.put_binding()
|> validate_project(path)
|> generator.generate()
|> prompt_to_install_deps(generator, path)
end
defp validate_project(%Project{opts: opts} = project, path) do
check_app_name!(project.app, !!opts[:app])
check_directory_existence!(Map.fetch!(project, path))
check_module_name_validity!(project.root_mod)
check_module_name_availability!(project.root_mod)
project
end
defp prompt_to_install_deps(%Project{} = project, generator, path_key) do
path = Map.fetch!(project, path_key)
install? =
Keyword.get_lazy(project.opts, :install, fn ->
Mix.shell().yes?("\nFetch and install dependencies?")
end)
cd_step = ["$ cd #{relative_app_path(path)}"]
maybe_cd(path, fn ->
mix_step = install_mix(project, install?)
if mix_step == [] do
builders = Keyword.fetch!(project.binding, :asset_builders)
if builders != [] do
Mix.shell().info([:green, "* running ", :reset, "mix assets.setup"])
# First compile only builders so we can install in parallel
# TODO: Once we require Erlang/OTP 28, castore and jason may no longer be required
cmd(project, "mix deps.compile castore jason #{Enum.join(builders, " ")}", log: false)
end
tasks =
Enum.map(builders, fn builder ->
cmd = "mix do loadpaths --no-compile + #{builder}.install"
Task.async(fn -> cmd(project, cmd, log: false, cd: project.web_path) end)
end)
if rebar_available?() do
cmd(project, "mix deps.compile")
end
Task.await_many(tasks, :infinity)
end
print_missing_steps(cd_step ++ mix_step)
if Project.ecto?(project) do
print_ecto_info(generator)
end
if path_key == :web_path do
Mix.shell().info("""
Your web app requires a PubSub server to be running.
The PubSub server is typically defined in a `mix phx.new.ecto` app.
If you don't plan to define an Ecto app, you must explicitly start
the PubSub in your supervision tree as:
{Phoenix.PubSub, name: #{inspect(project.app_mod)}.PubSub}
""")
end
print_mix_info(generator)
end)
end
defp maybe_cd(path, func), do: path && File.cd!(path, func)
defp install_mix(project, install?) do
if install? && hex_available?() do
cmd(project, "mix deps.get")
else
["$ mix deps.get"]
end
end
# TODO: Elixir v1.15 automatically installs Hex/Rebar if missing, so we can simplify this.
defp hex_available? do
Code.ensure_loaded?(Hex)
end
defp rebar_available? do
Mix.Rebar.rebar_cmd(:rebar3)
end
defp print_missing_steps(steps) do
Mix.shell().info("""
We are almost there! The following steps are missing:
#{Enum.join(steps, "\n ")}
""")
end
defp print_ecto_info(Web), do: :ok
defp print_ecto_info(_gen) do
Mix.shell().info("""
Then configure your database in config/dev.exs and run:
$ mix ecto.create
""")
end
defp print_mix_info(Ecto) do
Mix.shell().info("""
You can run your app inside IEx (Interactive Elixir) as:
$ iex -S mix
""")
end
defp print_mix_info(_gen) do
Mix.shell().info("""
Start your Phoenix app with:
$ mix phx.server
You can also run your app inside IEx (Interactive Elixir) as:
$ iex -S mix phx.server
""")
end
defp relative_app_path(path) do
case Path.relative_to_cwd(path) do
^path -> Path.basename(path)
rel -> rel
end
end
## Helpers
defp cmd(%Project{} = project, cmd, opts \\ []) do
{log?, opts} = Keyword.pop(opts, :log, true)
if log? do
Mix.shell().info([:green, "* running ", :reset, cmd])
end
case Mix.shell().cmd(cmd, opts ++ cmd_opts(project)) do
0 -> []
_ -> ["$ #{cmd}"]
end
end
defp cmd_opts(%Project{} = project) do
if Project.verbose?(project) do
[]
else
[quiet: true]
end
end
defp check_app_name!(name, from_app_flag) do
unless name =~ Regex.recompile!(~r/^[a-z][\w_]*$/) do
extra =
if !from_app_flag do
". The application name is inferred from the path, if you'd like to " <>
"explicitly name the application then use the `--app APP` option."
else
""
end
Mix.raise(
"Application name must start with a letter and have only lowercase " <>
"letters, numbers and underscore, got: #{inspect(name)}" <> extra
)
end
end
defp check_module_name_validity!(name) do
unless inspect(name) =~ Regex.recompile!(~r/^[A-Z]\w*(\.[A-Z]\w*)*$/) do
Mix.raise(
"Module name must be a valid Elixir alias (for example: Foo.Bar), got: #{inspect(name)}"
)
end
end
defp check_module_name_availability!(name) do
[name]
|> Module.concat()
|> Module.split()
|> Enum.reduce([], fn name, acc ->
mod = Module.concat([Elixir, name | acc])
if Code.ensure_loaded?(mod) do
Mix.raise("Module name #{inspect(mod)} is already taken, please choose another name")
else
[name | acc]
end
end)
end
defp check_directory_existence!(path) do
if File.dir?(path) and
not Mix.shell().yes?(
"The directory #{path} already exists. Are you sure you want to continue?"
) do
Mix.raise("Please select another directory for installation.")
end
end
defp elixir_version_check! do
unless Version.match?(System.version(), "~> 1.14") do
Mix.raise(
"Phoenix v#{@version} requires at least Elixir v1.14\n " <>
"You have #{System.version()}. Please update accordingly"
)
end
end
end