├── .formatter.exs
├── .github
├── ISSUE_TEMPLATE
│ ├── bug_report.md
│ └── config.yml
├── dependabot.yml
└── workflows
│ ├── assets.yml
│ └── ci.yml
├── .gitignore
├── CHANGELOG.md
├── CODE_OF_CONDUCT.md
├── CONTRIBUTING.md
├── LICENSE.md
├── README.md
├── RELEASE.md
├── SECURITY.md
├── assets
├── js
│ └── phoenix
│ │ ├── ajax.js
│ │ ├── channel.js
│ │ ├── constants.js
│ │ ├── index.js
│ │ ├── longpoll.js
│ │ ├── presence.js
│ │ ├── push.js
│ │ ├── serializer.js
│ │ ├── socket.js
│ │ ├── timer.js
│ │ └── utils.js
└── test
│ ├── channel_test.js
│ ├── longpoll_test.js
│ ├── presence_test.js
│ ├── serializer.js
│ ├── serializer_test.js
│ └── socket_test.js
├── babel.config.json
├── config
└── config.exs
├── eslint.config.mjs
├── guides
├── asset_management.md
├── assets
│ └── images
│ │ ├── hello-from-phoenix.png
│ │ ├── hello-world-from-frank.png
│ │ └── welcome-to-phoenix.png
├── authn_authz
│ ├── api_authentication.md
│ ├── authn_authz.md
│ ├── mix_phx_gen_auth.md
│ └── scopes.md
├── cheatsheets
│ └── router.cheatmd
├── components.md
├── controllers.md
├── data_modelling
│ ├── contexts.md
│ ├── cross_context_boundaries.md
│ ├── faq.md
│ ├── in_context_relationships.md
│ ├── more_examples.md
│ └── your_first_context.md
├── deployment
│ ├── deployment.md
│ ├── fly.md
│ ├── gigalixir.md
│ ├── heroku.md
│ └── releases.md
├── directory_structure.md
├── ecto.md
├── howto
│ ├── custom_error_pages.md
│ ├── file_uploads.md
│ ├── swapping_databases.md
│ ├── using_ssl.md
│ └── writing_a_channels_client.md
├── introduction
│ ├── community.md
│ ├── installation.md
│ ├── overview.md
│ ├── packages_glossary.md
│ └── up_and_running.md
├── json_and_apis.md
├── live_view.md
├── plug.md
├── real_time
│ ├── channels.md
│ └── presence.md
├── request_lifecycle.md
├── routing.md
├── telemetry.md
└── testing
│ ├── testing.md
│ ├── testing_channels.md
│ ├── testing_contexts.md
│ └── testing_controllers.md
├── installer
├── README.md
├── lib
│ ├── mix
│ │ └── tasks
│ │ │ ├── local.phx.ex
│ │ │ ├── phx.new.ecto.ex
│ │ │ ├── phx.new.ex
│ │ │ └── phx.new.web.ex
│ └── phx_new
│ │ ├── ecto.ex
│ │ ├── generator.ex
│ │ ├── mailer.ex
│ │ ├── project.ex
│ │ ├── single.ex
│ │ ├── umbrella.ex
│ │ └── web.ex
├── mix.exs
├── mix.lock
├── recreate_default_css.exs
├── templates
│ ├── phx_assets
│ │ ├── app.css
│ │ ├── app.js
│ │ ├── daisyui-theme.js
│ │ ├── daisyui.js
│ │ ├── heroicons.js
│ │ ├── logo.svg
│ │ └── topbar.js
│ ├── phx_ecto
│ │ ├── data_case.ex
│ │ ├── formatter.exs
│ │ ├── repo.ex
│ │ └── seeds.exs
│ ├── phx_gettext
│ │ ├── en
│ │ │ └── LC_MESSAGES
│ │ │ │ └── errors.po
│ │ ├── errors.pot
│ │ └── gettext.ex
│ ├── phx_mailer
│ │ └── lib
│ │ │ └── app_name
│ │ │ └── mailer.ex
│ ├── phx_single
│ │ ├── README.md
│ │ ├── config
│ │ │ ├── config.exs
│ │ │ ├── dev.exs
│ │ │ ├── prod.exs
│ │ │ ├── runtime.exs
│ │ │ └── test.exs
│ │ ├── formatter.exs
│ │ ├── gitignore
│ │ ├── lib
│ │ │ ├── app_name.ex
│ │ │ ├── app_name
│ │ │ │ └── application.ex
│ │ │ └── app_name_web.ex
│ │ ├── mix.exs
│ │ └── test
│ │ │ └── test_helper.exs
│ ├── phx_static
│ │ ├── app.css
│ │ ├── app.js
│ │ ├── default.css
│ │ ├── favicon.ico
│ │ ├── phoenix.png
│ │ └── robots.txt
│ ├── phx_test
│ │ ├── controllers
│ │ │ ├── error_html_test.exs
│ │ │ ├── error_json_test.exs
│ │ │ └── page_controller_test.exs
│ │ └── support
│ │ │ └── conn_case.ex
│ ├── phx_umbrella
│ │ ├── README.md
│ │ ├── apps
│ │ │ ├── app_name
│ │ │ │ ├── README.md
│ │ │ │ ├── config
│ │ │ │ │ └── config.exs
│ │ │ │ ├── formatter.exs
│ │ │ │ ├── gitignore
│ │ │ │ ├── lib
│ │ │ │ │ ├── app_name.ex
│ │ │ │ │ └── app_name
│ │ │ │ │ │ └── application.ex
│ │ │ │ ├── mix.exs
│ │ │ │ └── test
│ │ │ │ │ └── test_helper.exs
│ │ │ └── app_name_web
│ │ │ │ ├── README.md
│ │ │ │ ├── config
│ │ │ │ ├── config.exs
│ │ │ │ ├── dev.exs
│ │ │ │ ├── prod.exs
│ │ │ │ ├── runtime.exs
│ │ │ │ └── test.exs
│ │ │ │ ├── formatter.exs
│ │ │ │ ├── gitignore
│ │ │ │ ├── lib
│ │ │ │ ├── app_name.ex
│ │ │ │ └── app_name
│ │ │ │ │ └── application.ex
│ │ │ │ ├── mix.exs
│ │ │ │ └── test
│ │ │ │ └── test_helper.exs
│ │ ├── config
│ │ │ ├── config.exs
│ │ │ ├── dev.exs
│ │ │ ├── extra_config.exs
│ │ │ ├── prod.exs
│ │ │ ├── runtime.exs
│ │ │ └── test.exs
│ │ ├── formatter.exs
│ │ ├── gitignore
│ │ └── mix.exs
│ └── phx_web
│ │ ├── components
│ │ ├── core_components.ex
│ │ ├── layouts.ex
│ │ └── layouts
│ │ │ └── root.html.heex
│ │ ├── controllers
│ │ ├── error_html.ex
│ │ ├── error_json.ex
│ │ ├── page_controller.ex
│ │ ├── page_html.ex
│ │ └── page_html
│ │ │ └── home.html.heex
│ │ ├── endpoint.ex
│ │ ├── router.ex
│ │ └── telemetry.ex
└── test
│ ├── mix_helper.exs
│ ├── phx_new_ecto_test.exs
│ ├── phx_new_test.exs
│ ├── phx_new_umbrella_test.exs
│ ├── phx_new_web_test.exs
│ └── test_helper.exs
├── integration_test
├── README.md
├── config
│ └── config.exs
├── docker-compose.yml
├── docker.sh
├── mix.exs
├── mix.lock
├── test.sh
└── test
│ ├── code_generation
│ ├── app_with_defaults_test.exs
│ ├── app_with_mssql_adapter_test.exs
│ ├── app_with_mysql_adapter_test.exs
│ ├── app_with_no_options_test.exs
│ ├── app_with_scopes_test.exs
│ ├── app_with_sqlite3_adapter_test.exs
│ └── umbrella_app_with_defaults_test.exs
│ ├── support
│ └── code_generator_case.ex
│ └── test_helper.exs
├── jest.config.js
├── lib
├── mix
│ ├── phoenix.ex
│ ├── phoenix
│ │ ├── context.ex
│ │ ├── schema.ex
│ │ └── scope.ex
│ └── tasks
│ │ ├── compile.phoenix.ex
│ │ ├── phx.digest.clean.ex
│ │ ├── phx.digest.ex
│ │ ├── phx.ex
│ │ ├── phx.gen.auth.ex
│ │ ├── phx.gen.auth
│ │ ├── hashing_library.ex
│ │ ├── injector.ex
│ │ └── migration.ex
│ │ ├── phx.gen.cert.ex
│ │ ├── phx.gen.channel.ex
│ │ ├── phx.gen.context.ex
│ │ ├── phx.gen.embedded.ex
│ │ ├── phx.gen.ex
│ │ ├── phx.gen.html.ex
│ │ ├── phx.gen.json.ex
│ │ ├── phx.gen.live.ex
│ │ ├── phx.gen.notifier.ex
│ │ ├── phx.gen.presence.ex
│ │ ├── phx.gen.release.ex
│ │ ├── phx.gen.schema.ex
│ │ ├── phx.gen.secret.ex
│ │ ├── phx.gen.socket.ex
│ │ ├── phx.routes.ex
│ │ └── phx.server.ex
├── phoenix.ex
└── phoenix
│ ├── channel.ex
│ ├── channel
│ └── server.ex
│ ├── code_reloader.ex
│ ├── code_reloader
│ ├── mix_listener.ex
│ ├── proxy.ex
│ └── server.ex
│ ├── config.ex
│ ├── controller.ex
│ ├── controller
│ └── pipeline.ex
│ ├── debug.ex
│ ├── digester.ex
│ ├── digester
│ ├── compressor.ex
│ └── gzip.ex
│ ├── endpoint.ex
│ ├── endpoint
│ ├── cowboy2_adapter.ex
│ ├── render_errors.ex
│ ├── supervisor.ex
│ ├── sync_code_reload_plug.ex
│ └── watcher.ex
│ ├── exceptions.ex
│ ├── flash.ex
│ ├── logger.ex
│ ├── naming.ex
│ ├── param.ex
│ ├── presence.ex
│ ├── router.ex
│ ├── router
│ ├── console_formatter.ex
│ ├── helpers.ex
│ ├── resource.ex
│ ├── route.ex
│ └── scope.ex
│ ├── socket.ex
│ ├── socket
│ ├── message.ex
│ ├── pool_supervisor.ex
│ ├── serializer.ex
│ ├── serializers
│ │ ├── v1_json_serializer.ex
│ │ └── v2_json_serializer.ex
│ └── transport.ex
│ ├── test
│ ├── channel_test.ex
│ └── conn_test.ex
│ ├── token.ex
│ ├── transports
│ ├── long_poll.ex
│ ├── long_poll_server.ex
│ └── websocket.ex
│ └── verified_routes.ex
├── logo.png
├── mix.exs
├── mix.lock
├── package-lock.json
├── package.json
├── priv
├── static
│ ├── favicon.ico
│ ├── phoenix-orange.png
│ ├── phoenix.cjs.js
│ ├── phoenix.cjs.js.map
│ ├── phoenix.js
│ ├── phoenix.min.js
│ ├── phoenix.mjs
│ ├── phoenix.mjs.map
│ └── phoenix.png
└── templates
│ ├── phx.gen.auth
│ ├── auth.ex
│ ├── auth_test.exs
│ ├── confirmation_live.ex
│ ├── confirmation_live_test.exs
│ ├── conn_case.exs
│ ├── context_fixtures_functions.ex
│ ├── context_functions.ex
│ ├── login_live.ex
│ ├── login_live_test.exs
│ ├── migration.ex
│ ├── notifier.ex
│ ├── registration_controller.ex
│ ├── registration_controller_test.exs
│ ├── registration_html.ex
│ ├── registration_live.ex
│ ├── registration_live_test.exs
│ ├── registration_new.html.heex
│ ├── routes.ex
│ ├── schema.ex
│ ├── schema_token.ex
│ ├── scope.ex
│ ├── session_confirm.html.heex
│ ├── session_controller.ex
│ ├── session_controller_test.exs
│ ├── session_html.ex
│ ├── session_new.html.heex
│ ├── settings_controller.ex
│ ├── settings_controller_test.exs
│ ├── settings_edit.html.heex
│ ├── settings_html.ex
│ ├── settings_live.ex
│ ├── settings_live_test.exs
│ └── test_cases.exs
│ ├── phx.gen.channel
│ ├── channel.ex
│ ├── channel_case.ex
│ └── channel_test.exs
│ ├── phx.gen.context
│ ├── access_no_schema.ex
│ ├── access_no_schema_scope.ex
│ ├── context.ex
│ ├── context_test.exs
│ ├── fixtures.ex
│ ├── fixtures_module.ex
│ ├── schema_access.ex
│ ├── schema_access_scope.ex
│ ├── test_cases.exs
│ └── test_cases_scope.exs
│ ├── phx.gen.embedded
│ └── embedded_schema.ex
│ ├── phx.gen.html
│ ├── controller.ex
│ ├── controller_test.exs
│ ├── edit.html.heex
│ ├── html.ex
│ ├── index.html.heex
│ ├── new.html.heex
│ ├── resource_form.html.heex
│ └── show.html.heex
│ ├── phx.gen.json
│ ├── changeset_json.ex
│ ├── controller.ex
│ ├── controller_test.exs
│ ├── fallback_controller.ex
│ └── json.ex
│ ├── phx.gen.live
│ ├── form.ex
│ ├── index.ex
│ ├── live_test.exs
│ └── show.ex
│ ├── phx.gen.notifier
│ ├── notifier.ex
│ └── notifier_test.exs
│ ├── phx.gen.presence
│ └── presence.ex
│ ├── phx.gen.release
│ ├── Dockerfile.eex
│ ├── dockerignore.eex
│ ├── rel
│ │ ├── migrate.bat.eex
│ │ ├── migrate.sh.eex
│ │ ├── server.bat.eex
│ │ └── server.sh.eex
│ └── release.ex
│ ├── phx.gen.schema
│ ├── migration.exs
│ └── schema.ex
│ └── phx.gen.socket
│ ├── socket.ex
│ └── socket.js
└── test
├── fixtures
├── digest
│ ├── cleaner
│ │ ├── cache_manifest.json
│ │ └── latest_not_most_recent_cache_manifest.json
│ ├── compile
│ │ ├── cache_manifest.json
│ │ └── cache_manifest_upgrade.json
│ └── priv
│ │ ├── output
│ │ ├── foo-288ea8c7954498e65663c817382eeac4.css
│ │ └── foo-d978852bea6530fcd197b5445ed008fd.css
│ │ └── static
│ │ ├── app.js
│ │ ├── app.js.map
│ │ ├── css
│ │ └── app.css
│ │ ├── foo.css
│ │ ├── images
│ │ └── relative.png
│ │ ├── manifest.json
│ │ ├── phoenix.png
│ │ ├── precompressed.js.br
│ │ └── precompressed.js.gz
├── hello.txt
├── ssl
│ ├── cert.pem
│ └── key.pem
├── templates
│ ├── custom.foo
│ ├── layout
│ │ ├── app.html.eex
│ │ └── root.html.eex
│ ├── no_trim.text.eex
│ ├── path.html.eex
│ ├── safe.html.eex
│ ├── show.html.eex
│ ├── trim.html.eex
│ └── user
│ │ ├── index.html.eex
│ │ ├── profiles
│ │ └── admin.html.eex
│ │ ├── render_template.html.eex
│ │ └── show.json.exs
└── views.exs
├── mix
├── phoenix_test.exs
└── tasks
│ ├── phx.digest.clean_test.exs
│ ├── phx.digest_test.exs
│ ├── phx.gen.auth
│ └── injector_test.exs
│ ├── phx.gen.auth_test.exs
│ ├── phx.gen.cert_test.exs
│ ├── phx.gen.channel_test.exs
│ ├── phx.gen.context_test.exs
│ ├── phx.gen.embedded_test.exs
│ ├── phx.gen.html_test.exs
│ ├── phx.gen.json_test.exs
│ ├── phx.gen.live_test.exs
│ ├── phx.gen.notifier_test.exs
│ ├── phx.gen.presence_test.exs
│ ├── phx.gen.release_test.exs
│ ├── phx.gen.schema_test.exs
│ ├── phx.gen.secret_test.exs
│ ├── phx.gen.socket_test.exs
│ ├── phx.routes_test.exs
│ └── phx_test.exs
├── phoenix
├── channel_test.exs
├── code_reloader_test.exs
├── config_test.exs
├── controller
│ ├── controller_test.exs
│ ├── flash_test.exs
│ ├── pipeline_test.exs
│ └── render_test.exs
├── debug_test.exs
├── digester
│ └── gzip_test.exs
├── digester_test.exs
├── endpoint
│ ├── endpoint_test.exs
│ ├── render_errors_test.exs
│ ├── supervisor_test.exs
│ └── watcher_test.exs
├── integration
│ ├── endpoint_test.exs
│ ├── long_poll_channels_test.exs
│ ├── long_poll_socket_test.exs
│ ├── websocket_channels_test.exs
│ └── websocket_socket_test.exs
├── logger_test.exs
├── naming_test.exs
├── param_test.exs
├── presence_test.exs
├── router
│ ├── console_formatter_test.exs
│ ├── forward_test.exs
│ ├── helpers_test.exs
│ ├── pipeline_test.exs
│ ├── resource_test.exs
│ ├── resources_test.exs
│ ├── route_test.exs
│ ├── routing_test.exs
│ └── scope_test.exs
├── socket
│ ├── message_test.exs
│ ├── socket_test.exs
│ ├── transport_test.exs
│ ├── v1_json_serializer_test.exs
│ └── v2_json_serializer_test.exs
├── test
│ ├── channel_test.exs
│ └── conn_test.exs
├── token_test.exs
└── verified_routes_test.exs
├── support
├── endpoint_helper.exs
├── http_client.exs
├── router_helper.exs
└── websocket_client.exs
└── test_helper.exs
/.formatter.exs:
--------------------------------------------------------------------------------
1 | locals_without_parens = [
2 | # Phoenix.Channel
3 | intercept: 1,
4 |
5 | # Phoenix.Router
6 | connect: 3,
7 | connect: 4,
8 | delete: 3,
9 | delete: 4,
10 | forward: 2,
11 | forward: 3,
12 | forward: 4,
13 | get: 3,
14 | get: 4,
15 | head: 3,
16 | head: 4,
17 | match: 4,
18 | match: 5,
19 | options: 3,
20 | options: 4,
21 | patch: 3,
22 | patch: 4,
23 | pipeline: 2,
24 | pipe_through: 1,
25 | post: 3,
26 | post: 4,
27 | put: 3,
28 | put: 4,
29 | resources: 2,
30 | resources: 3,
31 | resources: 4,
32 | trace: 4,
33 |
34 | # Phoenix.Controller
35 | action_fallback: 1,
36 |
37 | # Phoenix.Endpoint
38 | plug: 1,
39 | plug: 2,
40 | socket: 2,
41 | socket: 3,
42 |
43 | # Phoenix.Socket
44 | channel: 2,
45 | channel: 3,
46 |
47 | # Phoenix.ChannelTest
48 | assert_broadcast: 2,
49 | assert_broadcast: 3,
50 | assert_push: 2,
51 | assert_push: 3,
52 | assert_reply: 2,
53 | assert_reply: 3,
54 | assert_reply: 4,
55 | refute_broadcast: 2,
56 | refute_broadcast: 3,
57 | refute_push: 2,
58 | refute_push: 3,
59 | refute_reply: 2,
60 | refute_reply: 3,
61 | refute_reply: 4,
62 |
63 | # Phoenix.ConnTest
64 | assert_error_sent: 2,
65 |
66 | # Phoenix.Live{Dashboard,View}
67 | attr: 2,
68 | attr: 3,
69 | embed_templates: 1,
70 | embed_templates: 2,
71 | live: 2,
72 | live: 3,
73 | live: 4,
74 | live_dashboard: 1,
75 | live_dashboard: 2,
76 | on_mount: 1,
77 | slot: 1,
78 | slot: 2,
79 | slot: 3,
80 |
81 | # Phoenix.LiveViewTest
82 | assert_patch: 1,
83 | assert_patch: 2,
84 | assert_patch: 3,
85 | assert_patched: 2,
86 | assert_push_event: 3,
87 | assert_push_event: 4,
88 | assert_redirect: 1,
89 | assert_redirect: 2,
90 | assert_redirect: 3,
91 | assert_redirected: 2,
92 | assert_reply: 2,
93 | assert_reply: 3,
94 | refute_redirected: 2,
95 | refute_push_event: 3,
96 | refute_push_event: 4
97 | ]
98 |
99 | [
100 | locals_without_parens: locals_without_parens,
101 | export: [locals_without_parens: locals_without_parens]
102 | ]
103 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/bug_report.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Bug report
3 | about: Create a report to help us improve
4 | title: ''
5 | labels: ''
6 | assignees: ''
7 |
8 | ---
9 |
10 | ### Environment
11 |
12 | * Elixir version (elixir -v):
13 | * Phoenix version (mix deps):
14 | * Operating system:
15 |
16 | ### Actual behavior
17 |
18 |
24 |
25 | ### Expected behavior
26 |
27 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/config.yml:
--------------------------------------------------------------------------------
1 | ---
2 | blank_issues_enabled: true
3 |
4 | contact_links:
5 | - name: Ask questions, support, and general discussions
6 | url: https://elixirforum.com/c/phoenix-forum
7 | about: Ask questions, provide support, and more on Elixir Forum
8 |
9 | - name: Propose new features
10 | url: https://elixirforum.com/c/phoenix-forum
11 | about: Propose new features on Elixir Forum
12 |
--------------------------------------------------------------------------------
/.github/dependabot.yml:
--------------------------------------------------------------------------------
1 | version: 2
2 | updates:
3 | # Maintain dependencies for GitHub Actions
4 | - package-ecosystem: "github-actions"
5 | directory: "/"
6 | schedule:
7 | interval: "weekly"
8 |
9 |
--------------------------------------------------------------------------------
/.github/workflows/assets.yml:
--------------------------------------------------------------------------------
1 | name: Assets
2 |
3 | on:
4 | push:
5 | branches:
6 | - main
7 | - "v*.*"
8 |
9 | permissions:
10 | contents: read
11 |
12 | jobs:
13 | build:
14 | runs-on: ubuntu-24.04
15 | env:
16 | elixir: 1.18.3
17 | otp: 27.2
18 | permissions:
19 | contents: write # for stefanzweifel/git-auto-commit-action to push code in repo
20 | steps:
21 | - name: Checkout
22 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
23 |
24 | - name: Set up Elixir
25 | uses: erlef/setup-beam@8aa8a857c6be0daae6e97272bb299d5b942675a4 # v1.19.0
26 | with:
27 | elixir-version: ${{ env.elixir }}
28 | otp-version: ${{ env.otp }}
29 |
30 | - name: Restore deps and _build cache
31 | uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3
32 | with:
33 | path: |
34 | deps
35 | _build
36 | key: ${{ runner.os }}-mix-${{ env.elixir }}-${{ env.otp }}-${{ hashFiles('**/mix.lock') }}-dev
37 | restore-keys: |
38 | ${{ runner.os }}-mix-${{ env.elixir }}-${{ env.otp }}-
39 | - name: Install Dependencies
40 | run: mix deps.get --only dev
41 |
42 | - name: Set up Node.js 20.x
43 | uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
44 | with:
45 | node-version: 20.x
46 |
47 | - name: Restore npm cache
48 | uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3
49 | with:
50 | path: ~/.npm
51 | key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
52 | restore-keys: |
53 | ${{ runner.os }}-node-
54 |
55 | - name: Install npm dependencies
56 | run: npm ci
57 |
58 | - name: Build assets
59 | run: mix assets.build
60 |
61 | - name: Push updated assets
62 | id: push_assets
63 | uses: stefanzweifel/git-auto-commit-action@b863ae1933cb653a53c021fe36dbb774e1fb9403 # v5.2.0
64 | with:
65 | commit_message: Update assets
66 | file_pattern: priv/static
67 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | /_build/
2 | /deps/
3 | /doc/
4 | /node_modules/
5 | /tmp/
6 | /cover/
7 |
8 | /assets/node_modules/
9 |
10 | /installer/_build/
11 | /installer/assets/
12 | /installer/deps/
13 | /installer/doc/
14 | /installer/phx_new-*.ez
15 | /installer/tmp/
16 |
17 | /integration_test/_build/
18 | /integration_test/deps/
19 |
20 | erl_crash.dump
21 | phoenix-*.ez
22 |
23 | .DS_Store
24 |
25 | /priv/templates/phx.gen.live/core_components.ex
26 |
--------------------------------------------------------------------------------
/CODE_OF_CONDUCT.md:
--------------------------------------------------------------------------------
1 | # Contributor Code of Conduct
2 |
3 | As contributors and maintainers of this project, and in the interest of fostering an open and welcoming community, we pledge to respect all people who contribute through reporting issues, posting feature requests, updating documentation, submitting pull requests or patches, and other activities.
4 |
5 | We are committed to making participation in this project a harassment-free experience for everyone, regardless of level of experience, gender, gender identity and expression, sexual orientation, disability, personal appearance, body size, race, ethnicity, age, religion, or nationality.
6 |
7 | Examples of unacceptable behavior by participants include:
8 |
9 | * The use of sexualized language or imagery
10 | * Personal attacks
11 | * Trolling or insulting/derogatory comments
12 | * Public or private harassment
13 | * Publishing other's private information, such as physical or electronic addresses, without explicit permission
14 | * Other unethical or unprofessional conduct.
15 |
16 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct. By adopting this Code of Conduct, project maintainers commit themselves to fairly and consistently applying these principles to every aspect of managing this project. Project maintainers who do not follow or enforce the Code of Conduct may be permanently removed from the project team.
17 |
18 | This code of conduct applies both within project spaces and in public spaces when an individual is representing the project or its community.
19 |
20 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by opening an issue or contacting one or more of the project maintainers.
21 |
22 | This Code of Conduct is adapted from the [Contributor Covenant](https://www.contributor-covenant.org), version 1.2.0, available at [https://www.contributor-covenant.org/version/1/2/0/code-of-conduct/](https://www.contributor-covenant.org/version/1/2/0/code-of-conduct/)
23 |
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | # MIT License
2 |
3 | Copyright (c) 2014 Chris McCord
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining
6 | a copy of this software and associated documentation files (the
7 | "Software"), to deal in the Software without restriction, including
8 | without limitation the rights to use, copy, modify, merge, publish,
9 | distribute, sublicense, and/or sell copies of the Software, and to
10 | permit persons to whom the Software is furnished to do so, subject to
11 | the following conditions:
12 |
13 | The above copyright notice and this permission notice shall be
14 | included in all copies or substantial portions of the Software.
15 |
16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
23 |
--------------------------------------------------------------------------------
/RELEASE.md:
--------------------------------------------------------------------------------
1 | # Release Instructions
2 |
3 | 1. Check related deps for required version bumps and compatibility (`phoenix_ecto`, `phoenix_html`)
4 | 2. Bump version in related files below
5 | 3. Bump external dependency version in related external files below
6 | 4. Run tests:
7 | - `mix test` in the root folder
8 | - `mix test` in the `installer/` folder
9 | 5. Commit, push code
10 | 6. Publish `phx_new` and `phoenix` packages and docs after pruning any extraneous uncommitted files
11 | 7. Test installer by generating a new app, running `mix deps.get`, and compiling
12 | 8. Publish to `npm` with `npm publish`
13 | 9. Update Elixir and Erlang/OTP versions on new.phoenixframework.org
14 | 10. Start -dev version in related files below
15 |
16 | ## Files with version
17 |
18 | * `CHANGELOG`
19 | * `mix.exs`
20 | * `installer/mix.exs`
21 | * `package.json`
22 | * `assets/package.json`
23 |
24 | ## Files with external dependency versions
25 |
26 | * `priv/templates/phx.gen.release/Docker.eex` (debian)
27 | * `priv/templates/phx.gen.release/Docker.eex` (esbuild)
28 |
--------------------------------------------------------------------------------
/SECURITY.md:
--------------------------------------------------------------------------------
1 | # Security Policy
2 |
3 | ## Supported versions
4 |
5 | Phoenix applies bug fixes only to the latest minor branch. Security patches are
6 | available for the last 4 minor branches:
7 |
8 | Phoenix version | Support
9 | :-------------- | :-----------------------------
10 | 1.7 | Bug fixes and security patches
11 | 1.6 | Security patches only
12 | 1.5 | Security patches only
13 | 1.4 | Security patches only
14 |
15 | ## Announcements
16 |
17 | [Security advisories will be published on GitHub](https://github.com/phoenixframework/phoenix/security).
18 |
19 | ## Reporting a vulnerability
20 |
21 | [Please disclose security vulnerabilities privately via GitHub](https://github.com/phoenixframework/phoenix/security).
22 |
--------------------------------------------------------------------------------
/assets/js/phoenix/constants.js:
--------------------------------------------------------------------------------
1 | export const globalSelf = typeof self !== "undefined" ? self : null
2 | export const phxWindow = typeof window !== "undefined" ? window : null
3 | export const global = globalSelf || phxWindow || globalThis
4 | export const DEFAULT_VSN = "2.0.0"
5 | export const SOCKET_STATES = {connecting: 0, open: 1, closing: 2, closed: 3}
6 | export const DEFAULT_TIMEOUT = 10000
7 | export const WS_CLOSE_NORMAL = 1000
8 | export const CHANNEL_STATES = {
9 | closed: "closed",
10 | errored: "errored",
11 | joined: "joined",
12 | joining: "joining",
13 | leaving: "leaving",
14 | }
15 | export const CHANNEL_EVENTS = {
16 | close: "phx_close",
17 | error: "phx_error",
18 | join: "phx_join",
19 | reply: "phx_reply",
20 | leave: "phx_leave"
21 | }
22 |
23 | export const TRANSPORTS = {
24 | longpoll: "longpoll",
25 | websocket: "websocket"
26 | }
27 | export const XHR_STATES = {
28 | complete: 4
29 | }
30 | export const AUTH_TOKEN_PREFIX = "base64url.bearer.phx."
31 |
--------------------------------------------------------------------------------
/assets/js/phoenix/timer.js:
--------------------------------------------------------------------------------
1 | /**
2 | *
3 | * Creates a timer that accepts a `timerCalc` function to perform
4 | * calculated timeout retries, such as exponential backoff.
5 | *
6 | * @example
7 | * let reconnectTimer = new Timer(() => this.connect(), function(tries){
8 | * return [1000, 5000, 10000][tries - 1] || 10000
9 | * })
10 | * reconnectTimer.scheduleTimeout() // fires after 1000
11 | * reconnectTimer.scheduleTimeout() // fires after 5000
12 | * reconnectTimer.reset()
13 | * reconnectTimer.scheduleTimeout() // fires after 1000
14 | *
15 | * @param {Function} callback
16 | * @param {Function} timerCalc
17 | */
18 | export default class Timer {
19 | constructor(callback, timerCalc){
20 | this.callback = callback
21 | this.timerCalc = timerCalc
22 | this.timer = null
23 | this.tries = 0
24 | }
25 |
26 | reset(){
27 | this.tries = 0
28 | clearTimeout(this.timer)
29 | }
30 |
31 | /**
32 | * Cancels any previous scheduleTimeout and schedules callback
33 | */
34 | scheduleTimeout(){
35 | clearTimeout(this.timer)
36 |
37 | this.timer = setTimeout(() => {
38 | this.tries = this.tries + 1
39 | this.callback()
40 | }, this.timerCalc(this.tries + 1))
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/assets/js/phoenix/utils.js:
--------------------------------------------------------------------------------
1 | // wraps value in closure or returns closure
2 | export let closure = (value) => {
3 | if(typeof value === "function"){
4 | return value
5 | } else {
6 | let closure = function (){ return value }
7 | return closure
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/assets/test/serializer.js:
--------------------------------------------------------------------------------
1 |
2 | export const encode = (msg) => {
3 | let payload = [
4 | msg.join_ref, msg.ref, msg.topic, msg.event, msg.payload
5 | ]
6 | return JSON.stringify(payload)
7 | }
8 |
9 | export const decode = (rawPayload) => {
10 | let [join_ref, ref, topic, event, payload] = JSON.parse(rawPayload)
11 |
12 | return {join_ref, ref, topic, event, payload}
13 | }
14 |
--------------------------------------------------------------------------------
/babel.config.json:
--------------------------------------------------------------------------------
1 | {
2 | "presets": [
3 | "@babel/preset-env"
4 | ]
5 | }
6 |
--------------------------------------------------------------------------------
/config/config.exs:
--------------------------------------------------------------------------------
1 | import Config
2 |
3 | config :logger, :console,
4 | colors: [enabled: false],
5 | format: "\n$time $metadata[$level] $message\n"
6 |
7 | config :phoenix,
8 | json_library: Jason,
9 | stacktrace_depth: 20,
10 | trim_on_html_eex_engine: false
11 |
12 | if Mix.env() == :dev do
13 | esbuild = fn args ->
14 | [
15 | args: ~w(./js/phoenix --bundle) ++ args,
16 | cd: Path.expand("../assets", __DIR__),
17 | env: %{"NODE_PATH" => Path.expand("../deps", __DIR__)}
18 | ]
19 | end
20 |
21 | config :esbuild,
22 | version: "0.25.4",
23 | module: esbuild.(~w(--format=esm --sourcemap --outfile=../priv/static/phoenix.mjs)),
24 | main: esbuild.(~w(--format=cjs --sourcemap --outfile=../priv/static/phoenix.cjs.js)),
25 | cdn:
26 | esbuild.(
27 | ~w(--target=es2016 --format=iife --global-name=Phoenix --outfile=../priv/static/phoenix.js)
28 | ),
29 | cdn_min:
30 | esbuild.(
31 | ~w(--target=es2016 --format=iife --global-name=Phoenix --minify --outfile=../priv/static/phoenix.min.js)
32 | )
33 | end
34 |
--------------------------------------------------------------------------------
/guides/assets/images/hello-from-phoenix.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/phoenixframework/phoenix/45e29e47de26d644725a4c0a1c0a46cc46f777e1/guides/assets/images/hello-from-phoenix.png
--------------------------------------------------------------------------------
/guides/assets/images/hello-world-from-frank.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/phoenixframework/phoenix/45e29e47de26d644725a4c0a1c0a46cc46f777e1/guides/assets/images/hello-world-from-frank.png
--------------------------------------------------------------------------------
/guides/assets/images/welcome-to-phoenix.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/phoenixframework/phoenix/45e29e47de26d644725a4c0a1c0a46cc46f777e1/guides/assets/images/welcome-to-phoenix.png
--------------------------------------------------------------------------------
/guides/authn_authz/authn_authz.md:
--------------------------------------------------------------------------------
1 | # Introduction to Auth
2 |
3 | Authentication (authn) and authorization (authz) are two important concepts in security. Authentication is the process of verifying the identity of a user or system, while authorization is the process of granting or denying access to resources based on the user's identity and permissions.
4 |
5 | Phoenix comes with built-in support for both. Generally speaking, developers use the `mix phx.gen.auth` generator to scaffold their authn and authz. Third-party libraries such as [Ueberauth](https://github.com/ueberauth/ueberauth) can be used either as complementary systems or by itself.
6 |
7 | Overall we have the following guides:
8 |
9 | * [mix phx.gen.auth](mix_phx_gen_auth.md) - An introduction to the `mix phx.gen.auth` generator and its security considerations.
10 |
11 | * [Scopes](scopes.md) - Scopes are the mechanism Phoenix v1.8 introduced to manage access to resources based on the user's identity and permissions.
12 |
13 | * [API Authentication](api_authentication.md) - An additional guide that shows how to expand `mix phx.gen.auth` code to support token-based API authentication.
14 |
--------------------------------------------------------------------------------
/guides/cheatsheets/router.cheatmd:
--------------------------------------------------------------------------------
1 | # Routing cheatsheet
2 |
3 | > Those need to be declared in the correct router module and scope.
4 |
5 | A quick reference to the common routing features' syntax. For an exhaustive overview, refer to the [routing guides](routing.md).
6 |
7 | ## Routing declaration
8 | {: .col-2}
9 |
10 | ### Single route
11 |
12 | ```elixir
13 | get "/users", UserController, :index
14 | patch "/users/:id", UserController, :update
15 | ```
16 | ```elixir
17 | # generated routes
18 | ~p"/users"
19 | ~p"/users/9" # user_id is 9
20 | ```
21 | Also accepts `put`, `patch`, `options`, `delete` and `head`.
22 |
23 | ### Resources
24 |
25 | #### Simple
26 |
27 | ```elixir
28 | resources "/users", UserController
29 | ```
30 | Generates `:index`, `:edit`, `:new`, `:show`, `:create`, `:update` and `:delete`.
31 |
32 | #### Options
33 |
34 | ```elixir
35 | resources "/users", UserController, only: [:show]
36 | resources "/users", UserController, except: [:create, :delete]
37 | resources "/users", UserController, as: :person # ~p"/person"
38 | ```
39 |
40 | #### Nested
41 |
42 | ```elixir
43 | resources "/users", UserController do
44 | resources "/posts", PostController
45 | end
46 | ```
47 | ```elixir
48 | # generated routes
49 | ~p"/users/3/posts" # user_id is 3
50 | ~p"/users/3/posts/17" # user_id is 3 and post_id = 17
51 | ```
52 | For more info check the [resources docs.](routing.html#resources)
53 |
54 | ### Scopes
55 |
56 | #### Simple
57 | ```elixir
58 | scope "/admin", HelloWeb.Admin do
59 | pipe_through :browser
60 |
61 | resources "/users", UserController
62 | end
63 | ```
64 | ```elixir
65 | # generated path helpers
66 | ~p"/admin/users"
67 | ```
68 |
69 | #### Nested
70 | ```elixir
71 | scope "/api", HelloWeb.Api, as: :api do
72 | pipe_through :api
73 |
74 | scope "/v1", V1, as: :v1 do
75 | resources "/users", UserController
76 | end
77 | end
78 | ```
79 | ```elixir
80 | # generated path helpers
81 | ~p"/api/v1/users"
82 | ```
83 | For more info check the [scoped routes](routing.md#scoped-routes) docs.
84 |
--------------------------------------------------------------------------------
/guides/introduction/overview.md:
--------------------------------------------------------------------------------
1 | # Overview
2 |
3 | Phoenix is a web development framework written in Elixir which implements the server-side Model View Controller (MVC) pattern. Many of its components and concepts will seem familiar to those of us with experience in other web frameworks like Ruby on Rails or Python's Django.
4 |
5 | Phoenix provides the best of both worlds - high developer productivity _and_ high application performance. It also has some interesting new twists like channels for implementing realtime features and pre-compiled templates for blazing speed.
6 |
7 | If you are already familiar with Elixir, great! If not, there are a number of places to learn. The [Elixir guides](https://hexdocs.pm/elixir/introduction.html) and the [Elixir learning resources page](https://elixir-lang.org/learning.html) are two great places to start.
8 |
9 | The guides that you are currently looking at provide an overview of all parts that make Phoenix. Here is a rundown of what they provide:
10 |
11 | * Introduction - the guides you are currently reading. They will cover how to get your first application up and running
12 |
13 | * Guides - in-depth guides covering the main components in Phoenix and Phoenix applications
14 |
15 | * Data modelling - building the initial features of an e-commerce application to learn about more data modelling with Phoenix
16 |
17 | * Authn and Authz - learn how to use the tools Phoenix provides for authentication and authorization
18 |
19 | * Real-time components - in-depth guides covering Phoenix's built-in real-time components
20 |
21 | * Testing - in-depth guides about testing
22 |
23 | * Deployment - in-depth guides about deployment
24 |
25 | * How-to's - a collection of articles on how to achieve certain things with Phoenix
26 |
27 | If you would prefer to read these guides as an EPUB, [click here!](Phoenix.epub)
28 |
29 | Note, these guides are not a step-by-step introduction to Phoenix. If you want a more structured approach to learning the framework, we have a large community and many books, courses, and screencasts available. See [our community page](community.html) for a complete list.
30 |
31 | [Let's get Phoenix installed](installation.html).
32 |
--------------------------------------------------------------------------------
/guides/introduction/packages_glossary.md:
--------------------------------------------------------------------------------
1 | # Packages Glossary
2 |
3 | By default, Phoenix applications depend on several packages with different purposes.
4 | This page is a quick reference of the different packages you may work with as a Phoenix
5 | developer.
6 |
7 | The main packages are:
8 |
9 | * [Ecto](https://hexdocs.pm/ecto) - a language integrated query and
10 | database wrapper
11 |
12 | * [Phoenix](https://hexdocs.pm/phoenix) - the Phoenix web framework
13 | (these docs)
14 |
15 | * [Phoenix LiveView](https://hexdocs.pm/phoenix_live_view) - build rich,
16 | real-time user experiences with server-rendered HTML. The LiveView
17 | project also defines [`Phoenix.Component`](https://hexdocs.pm/phoenix_live_view/Phoenix.Component.html) and
18 | [the HEEx template engine](https://hexdocs.pm/phoenix_live_view/Phoenix.Component.html#sigil_H/2),
19 | used for rendering HTML content in both regular and real-time applications
20 |
21 | * [Plug](https://hexdocs.pm/plug) - specification and conveniences for
22 | building composable modules web applications. This is the package
23 | responsible for the connection abstraction and the regular request-
24 | response life-cycle
25 |
26 | You will also work with the following:
27 |
28 | * [ExUnit](https://hexdocs.pm/ex_unit) - Elixir's built-in test framework
29 |
30 | * [Gettext](https://hexdocs.pm/gettext) - internationalization and
31 | localization through [`gettext`](https://www.gnu.org/software/gettext/)
32 |
33 | * [Swoosh](https://hexdocs.pm/swoosh) - a library for composing,
34 | delivering and testing emails, also used by `mix phx.gen.auth`
35 |
36 | When peeking under the covers, you will find these libraries play
37 | an important role in Phoenix applications:
38 |
39 | * [Phoenix HTML](https://hexdocs.pm/phoenix_html) - building blocks
40 | for working with HTML and forms safely
41 |
42 | * [Phoenix Ecto](https://hex.pm/packages/phoenix_ecto) - plugs and
43 | protocol implementations for using phoenix with ecto
44 |
45 | * [Phoenix PubSub](https://hexdocs.pm/phoenix_pubsub) - a distributed
46 | pub/sub system with presence support
47 |
48 | When it comes to instrumentation and monitoring, check out:
49 |
50 | * [Phoenix LiveDashboard](https://hexdocs.pm/phoenix_live_dashboard) -
51 | real-time performance monitoring and debugging tools for Phoenix
52 | developers
53 |
54 | * [Telemetry Metrics](https://hexdocs.pm/telemetry_metrics) - common
55 | interface for defining metrics based on Telemetry events
56 |
--------------------------------------------------------------------------------
/installer/README.md:
--------------------------------------------------------------------------------
1 | ## mix phx.new
2 |
3 | Provides `phx.new` installer as an archive.
4 |
5 | To install from Hex, run:
6 |
7 | $ mix archive.install hex phx_new
8 |
9 | To build and install it locally,
10 | ensure any previous archive versions are removed:
11 |
12 | $ mix archive.uninstall phx_new
13 |
14 | Then run:
15 |
16 | $ cd installer
17 | $ MIX_ENV=prod mix do archive.build, archive.install
18 |
--------------------------------------------------------------------------------
/installer/lib/mix/tasks/local.phx.ex:
--------------------------------------------------------------------------------
1 | defmodule Mix.Tasks.Local.Phx do
2 | use Mix.Task
3 |
4 | @shortdoc "Updates the Phoenix project generator locally"
5 |
6 | @moduledoc """
7 | Updates the Phoenix project generator locally.
8 |
9 | $ mix local.phx
10 |
11 | Accepts the same command line options as `archive.install hex phx_new`.
12 | """
13 |
14 | @impl true
15 | def run(args) do
16 | Mix.Task.run("archive.install", ["hex", "phx_new" | args])
17 | end
18 | end
19 |
--------------------------------------------------------------------------------
/installer/lib/mix/tasks/phx.new.ecto.ex:
--------------------------------------------------------------------------------
1 | defmodule Mix.Tasks.Phx.New.Ecto do
2 | @moduledoc """
3 | Creates a new Ecto project within an umbrella project.
4 |
5 | This task is intended to create a bare Ecto project without
6 | web integration, which serves as a core application of your
7 | domain for web applications and your greater umbrella
8 | platform to integrate with.
9 |
10 | It expects the name of the project as an argument.
11 |
12 | $ cd my_umbrella/apps
13 | $ mix phx.new.ecto APP [--module MODULE] [--app APP]
14 |
15 | A project at the given APP directory will be created. The
16 | application name and module name will be retrieved
17 | from the application name, unless `--module` or `--app` is given.
18 |
19 | ## Options
20 |
21 | * `--app` - the name of the OTP application
22 |
23 | * `--module` - the name of the base module in
24 | the generated skeleton
25 |
26 | * `--database` - specify the database adapter for Ecto. One of:
27 |
28 | * `postgres` - via https://github.com/elixir-ecto/postgrex
29 | * `mysql` - via https://github.com/elixir-ecto/myxql
30 | * `mssql` - via https://github.com/livehelpnow/tds
31 | * `sqlite3` - via https://github.com/elixir-sqlite/ecto_sqlite3
32 |
33 | Please check the driver docs for more information
34 | and requirements. Defaults to "postgres".
35 |
36 | * `--binary-id` - use `binary_id` as primary key type
37 | in Ecto schemas
38 |
39 | ## Examples
40 |
41 | $ mix phx.new.ecto hello_ecto
42 |
43 | Is equivalent to:
44 |
45 | $ mix phx.new.ecto hello_ecto --module HelloEcto
46 | """
47 |
48 | @shortdoc "Creates a new Ecto project within an umbrella project"
49 |
50 | use Mix.Task
51 |
52 | @impl true
53 | def run([]) do
54 | Mix.Tasks.Help.run(["phx.new.ecto"])
55 | end
56 |
57 | def run([path | _] = args) do
58 | unless Phx.New.Generator.in_umbrella?(path) do
59 | Mix.raise("The ecto task can only be run within an umbrella's apps directory")
60 | end
61 |
62 | Mix.Tasks.Phx.New.run(args ++ ["--no-assets", "--ecto"], Phx.New.Ecto, :app_path)
63 | end
64 | end
65 |
--------------------------------------------------------------------------------
/installer/lib/mix/tasks/phx.new.web.ex:
--------------------------------------------------------------------------------
1 | defmodule Mix.Tasks.Phx.New.Web do
2 | @moduledoc """
3 | Creates a new Phoenix web project within an umbrella project.
4 |
5 | It expects the name of the OTP app as the first argument and
6 | for the command to be run inside your umbrella application's
7 | apps directory:
8 |
9 | $ cd my_umbrella/apps
10 | $ mix phx.new.web APP [--module MODULE] [--app APP]
11 |
12 | This task is intended to create a bare Phoenix project without
13 | database integration, which interfaces with your greater
14 | umbrella application(s).
15 |
16 | ## Examples
17 |
18 | $ mix phx.new.web hello_web
19 |
20 | Is equivalent to:
21 |
22 | $ mix phx.new.web hello_web --module HelloWeb
23 |
24 | Supports the same options as the `phx.new` task.
25 | See `Mix.Tasks.Phx.New` for details.
26 | """
27 |
28 | @shortdoc "Creates a new Phoenix web project within an umbrella project"
29 |
30 | use Mix.Task
31 |
32 | @impl true
33 | def run([]) do
34 | Mix.Tasks.Help.run(["phx.new.web"])
35 | end
36 |
37 | def run([path | _] = args) do
38 | unless Phx.New.Generator.in_umbrella?(path) do
39 | Mix.raise "The web task can only be run within an umbrella's apps directory"
40 | end
41 |
42 | Mix.Tasks.Phx.New.run(args, Phx.New.Web, :web_path)
43 | end
44 | end
45 |
--------------------------------------------------------------------------------
/installer/lib/phx_new/ecto.ex:
--------------------------------------------------------------------------------
1 | defmodule Phx.New.Ecto do
2 | @moduledoc false
3 | use Phx.New.Generator
4 | alias Phx.New.{Project}
5 |
6 | @pre "phx_umbrella/apps/app_name"
7 |
8 | template(:new, [
9 | {:config, :project, "#{@pre}/config/config.exs": "config/config.exs"},
10 | {:eex, :app,
11 | "#{@pre}/lib/app_name/application.ex": "lib/:app/application.ex",
12 | "#{@pre}/lib/app_name.ex": "lib/:app.ex",
13 | "#{@pre}/test/test_helper.exs": "test/test_helper.exs",
14 | "#{@pre}/README.md": "README.md",
15 | "#{@pre}/mix.exs": "mix.exs",
16 | "#{@pre}/gitignore": ".gitignore",
17 | "#{@pre}/formatter.exs": ".formatter.exs"}
18 | ])
19 |
20 | def prepare_project(%Project{} = project) do
21 | app_path = Path.expand(project.base_path)
22 | project_path = Path.dirname(Path.dirname(app_path))
23 |
24 | %{project | in_umbrella?: true, app_path: app_path, project_path: project_path}
25 | end
26 |
27 | def generate(%Project{} = project) do
28 | inject_umbrella_config_defaults(project)
29 | copy_from(project, __MODULE__, :new)
30 | if Project.ecto?(project), do: Phx.New.Single.gen_ecto(project)
31 | project
32 | end
33 | end
34 |
--------------------------------------------------------------------------------
/installer/lib/phx_new/mailer.ex:
--------------------------------------------------------------------------------
1 | defmodule Phx.New.Mailer do
2 | @moduledoc false
3 | use Phx.New.Generator
4 | alias Phx.New.{Project}
5 |
6 | template(:new, [
7 | {:eex, :app, "phx_mailer/lib/app_name/mailer.ex": "lib/:app/mailer.ex"}
8 | ])
9 |
10 | def prepare_project(%Project{} = project) do
11 | app_path = Path.expand(project.base_path)
12 | project_path = Path.dirname(Path.dirname(app_path))
13 |
14 | %{project | in_umbrella?: true, app_path: app_path, project_path: project_path}
15 | end
16 |
17 | def generate(%Project{} = project) do
18 | inject_umbrella_config_defaults(project)
19 | copy_from(project, __MODULE__, :new)
20 | project
21 | end
22 | end
23 |
--------------------------------------------------------------------------------
/installer/lib/phx_new/project.ex:
--------------------------------------------------------------------------------
1 | defmodule Phx.New.Project do
2 | @moduledoc false
3 | alias Phx.New.Project
4 |
5 | defstruct base_path: nil,
6 | app: nil,
7 | app_mod: nil,
8 | app_path: nil,
9 | lib_web_name: nil,
10 | root_app: nil,
11 | root_mod: nil,
12 | project_path: nil,
13 | web_app: nil,
14 | web_namespace: nil,
15 | web_path: nil,
16 | opts: :unset,
17 | in_umbrella?: false,
18 | binding: [],
19 | cached_build_path: nil,
20 | generators: [timestamp_type: :utc_datetime]
21 |
22 | def new(project_path, opts) do
23 | project_path = Path.expand(project_path)
24 | app = opts[:app] || Path.basename(project_path)
25 | app_mod = Module.concat([opts[:module] || Macro.camelize(app)])
26 |
27 | %Project{
28 | base_path: project_path,
29 | app: app,
30 | app_mod: app_mod,
31 | root_app: app,
32 | root_mod: app_mod,
33 | opts: opts
34 | }
35 | end
36 |
37 | def ecto?(%Project{binding: binding}) do
38 | Keyword.fetch!(binding, :ecto)
39 | end
40 |
41 | def html?(%Project{binding: binding}) do
42 | Keyword.fetch!(binding, :html)
43 | end
44 |
45 | def gettext?(%Project{binding: binding}) do
46 | Keyword.fetch!(binding, :gettext)
47 | end
48 |
49 | def live?(%Project{binding: binding}) do
50 | Keyword.fetch!(binding, :live)
51 | end
52 |
53 | def dashboard?(%Project{binding: binding}) do
54 | Keyword.fetch!(binding, :dashboard)
55 | end
56 |
57 | def javascript?(%Project{binding: binding}) do
58 | Keyword.fetch!(binding, :javascript)
59 | end
60 |
61 | def css?(%Project{binding: binding}) do
62 | Keyword.fetch!(binding, :css)
63 | end
64 |
65 | def mailer?(%Project{binding: binding}) do
66 | Keyword.fetch!(binding, :mailer)
67 | end
68 |
69 | def verbose?(%Project{opts: opts}) do
70 | Keyword.get(opts, :verbose, false)
71 | end
72 |
73 | def join_path(%Project{} = project, location, path)
74 | when location in [:project, :app, :web] do
75 | project
76 | |> Map.fetch!(:"#{location}_path")
77 | |> Path.join(path)
78 | |> expand_path_with_bindings(project)
79 | end
80 |
81 | defp expand_path_with_bindings(path, %Project{} = project) do
82 | Regex.replace(Regex.recompile!(~r/:[a-zA-Z0-9_]+/), path, fn ":" <> key, _ ->
83 | project |> Map.fetch!(:"#{key}") |> to_string()
84 | end)
85 | end
86 | end
87 |
--------------------------------------------------------------------------------
/installer/lib/phx_new/umbrella.ex:
--------------------------------------------------------------------------------
1 | defmodule Phx.New.Umbrella do
2 | @moduledoc false
3 | use Phx.New.Generator
4 | alias Phx.New.{Ecto, Web, Project, Mailer}
5 |
6 | template(:new, [
7 | {:eex, :project,
8 | "phx_umbrella/gitignore": ".gitignore",
9 | "phx_umbrella/config/config.exs": "config/config.exs",
10 | "phx_umbrella/config/dev.exs": "config/dev.exs",
11 | "phx_umbrella/config/test.exs": "config/test.exs",
12 | "phx_umbrella/config/prod.exs": "config/prod.exs",
13 | "phx_umbrella/config/runtime.exs": "config/runtime.exs",
14 | "phx_umbrella/mix.exs": "mix.exs",
15 | "phx_umbrella/README.md": "README.md",
16 | "phx_umbrella/formatter.exs": ".formatter.exs"},
17 | {:config, :project, "phx_umbrella/config/extra_config.exs": "config/config.exs"}
18 | ])
19 |
20 | def prepare_project(%Project{app: app} = project) when not is_nil(app) do
21 | project
22 | |> put_app()
23 | |> put_web()
24 | |> put_root_app()
25 | end
26 |
27 | defp put_app(project) do
28 | project_path = Path.expand(project.base_path <> "_umbrella")
29 | app_path = Path.join(project_path, "apps/#{project.app}")
30 |
31 | %{project | in_umbrella?: true, app_path: app_path, project_path: project_path}
32 | end
33 |
34 | def put_web(%Project{app: app, opts: opts} = project) do
35 | web_app = :"#{app}_web"
36 | web_namespace = Module.concat([opts[:web_module] || "#{project.app_mod}Web"])
37 |
38 | %{
39 | project
40 | | web_app: web_app,
41 | lib_web_name: web_app,
42 | web_namespace: web_namespace,
43 | generators: [context_app: :"#{app}"],
44 | web_path: Path.join(project.project_path, "apps/#{web_app}/")
45 | }
46 | end
47 |
48 | defp put_root_app(%Project{app: app} = project) do
49 | %{
50 | project
51 | | root_app: :"#{app}_umbrella",
52 | root_mod: Module.concat(project.app_mod, "Umbrella")
53 | }
54 | end
55 |
56 | def generate(%Project{} = project) do
57 | if in_umbrella?(project.project_path) do
58 | Mix.raise("Unable to nest umbrella project within apps")
59 | end
60 |
61 | copy_from(project, __MODULE__, :new)
62 |
63 | project
64 | |> Web.generate()
65 | |> Ecto.generate()
66 | |> maybe_generate_mailer()
67 | end
68 |
69 | defp maybe_generate_mailer(project) do
70 | if Project.mailer?(project) do
71 | Mailer.generate(project)
72 | else
73 | project
74 | end
75 | end
76 | end
77 |
--------------------------------------------------------------------------------
/installer/mix.exs:
--------------------------------------------------------------------------------
1 | for path <- :code.get_path(),
2 | Regex.match?(~r/phx_new-[\w\.\-]+\/ebin$/, List.to_string(path)) do
3 | Code.delete_path(path)
4 | end
5 |
6 | defmodule Phx.New.MixProject do
7 | use Mix.Project
8 |
9 | @version "1.8.0-rc.3"
10 | @scm_url "https://github.com/phoenixframework/phoenix"
11 |
12 | # If the elixir requirement is updated, we need to update:
13 | #
14 | # 1. all mix.exs generated by the installer
15 | # 2. guides/introduction/installation.md
16 | # 3. guides/deployment/releases.md
17 | # 4. test/test_helper.exs at the root
18 | # 5. installer/lib/mix/tasks/phx.new.ex
19 | #
20 | @elixir_requirement "~> 1.15"
21 |
22 | def project do
23 | [
24 | app: :phx_new,
25 | start_permanent: Mix.env() == :prod,
26 | version: @version,
27 | elixir: @elixir_requirement,
28 | deps: deps(),
29 | package: [
30 | maintainers: [
31 | "Chris McCord",
32 | "José Valim",
33 | "Gary Rennie",
34 | "Jason Stiebs"
35 | ],
36 | licenses: ["MIT"],
37 | links: %{"GitHub" => @scm_url},
38 | files: ~w(lib templates mix.exs README.md)
39 | ],
40 | source_url: @scm_url,
41 | docs: docs(),
42 | homepage_url: "https://www.phoenixframework.org",
43 | description: """
44 | Phoenix framework project generator.
45 |
46 | Provides a `mix phx.new` task to bootstrap a new Elixir application
47 | with Phoenix dependencies.
48 | """
49 | ]
50 | end
51 |
52 | def cli do
53 | [preferred_envs: [docs: :docs]]
54 | end
55 |
56 | def application do
57 | [
58 | extra_applications: [:eex, :crypto, :public_key]
59 | ]
60 | end
61 |
62 | def deps do
63 | [
64 | {:ex_doc, "~> 0.24", only: :docs}
65 | ]
66 | end
67 |
68 | defp docs do
69 | [
70 | source_url_pattern: "#{@scm_url}/blob/v#{@version}/installer/%{path}#L%{line}"
71 | ]
72 | end
73 | end
74 |
--------------------------------------------------------------------------------
/installer/mix.lock:
--------------------------------------------------------------------------------
1 | %{
2 | "earmark_parser": {:hex, :earmark_parser, "1.4.44", "f20830dd6b5c77afe2b063777ddbbff09f9759396500cdbe7523efd58d7a339c", [:mix], [], "hexpm", "4778ac752b4701a5599215f7030989c989ffdc4f6df457c5f36938cc2d2a2750"},
3 | "ex_doc": {:hex, :ex_doc, "0.37.3", "f7816881a443cd77872b7d6118e8a55f547f49903aef8747dbcb345a75b462f9", [:mix], [{:earmark_parser, "~> 1.4.42", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_c, ">= 0.1.0", [hex: :makeup_c, repo: "hexpm", optional: true]}, {:makeup_elixir, "~> 0.14 or ~> 1.0", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1 or ~> 1.0", [hex: :makeup_erlang, repo: "hexpm", optional: false]}, {:makeup_html, ">= 0.1.0", [hex: :makeup_html, repo: "hexpm", optional: true]}], "hexpm", "e6aebca7156e7c29b5da4daa17f6361205b2ae5f26e5c7d8ca0d3f7e18972233"},
4 | "makeup": {:hex, :makeup, "1.2.1", "e90ac1c65589ef354378def3ba19d401e739ee7ee06fb47f94c687016e3713d1", [:mix], [{:nimble_parsec, "~> 1.4", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "d36484867b0bae0fea568d10131197a4c2e47056a6fbe84922bf6ba71c8d17ce"},
5 | "makeup_elixir": {:hex, :makeup_elixir, "1.0.1", "e928a4f984e795e41e3abd27bfc09f51db16ab8ba1aebdba2b3a575437efafc2", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2.3 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "7284900d412a3e5cfd97fdaed4f5ed389b8f2b4cb49efc0eb3bd10e2febf9507"},
6 | "makeup_erlang": {:hex, :makeup_erlang, "1.0.2", "03e1804074b3aa64d5fad7aa64601ed0fb395337b982d9bcf04029d68d51b6a7", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "af33ff7ef368d5893e4a267933e7744e46ce3cf1f61e2dccf53a111ed3aa3727"},
7 | "nimble_parsec": {:hex, :nimble_parsec, "1.4.2", "8efba0122db06df95bfaa78f791344a89352ba04baedd3849593bfce4d0dc1c6", [:mix], [], "hexpm", "4b21398942dda052b403bbe1da991ccd03a053668d147d53fb8c4e0efe09c973"},
8 | }
9 |
--------------------------------------------------------------------------------
/installer/recreate_default_css.exs:
--------------------------------------------------------------------------------
1 | File.rm_rf!("installer/dayzee")
2 |
3 | shell! = fn command, opts ->
4 | {_, 0} =
5 | System.shell(
6 | command,
7 | Keyword.merge(
8 | [
9 | into: IO.binstream(:stdio, :line),
10 | stderr_to_stdout: true
11 | ],
12 | opts
13 | )
14 | )
15 | end
16 |
17 | shell_in_daisy! = fn command -> shell!.(command, cd: Path.expand("dayzee")) end
18 |
19 | File.cd!("installer", fn ->
20 | shell!.("mix phx.new dayzee --dev --database sqlite3 --install", [])
21 |
22 | shell_in_daisy!.("mix phx.gen.auth Accounts User users --live")
23 | shell_in_daisy!.("mix deps.get")
24 | shell_in_daisy!.("mix phx.gen.live Blog Post posts title:string body:text")
25 | shell_in_daisy!.("mix tailwind dayzee")
26 |
27 | content = File.read!("dayzee/priv/static/assets/css/app.css")
28 |
29 | File.write!("templates/phx_static/default.css", """
30 | /* These are daisyUI styles for styling the default CoreComponents and generator files
31 | * included to prevent shipping a completely unstyled page, even as you selected --no-tailwind.
32 | * You can safely remove the whole file and all references to "default.css".
33 | */
34 | #{content}
35 | """)
36 | end)
37 |
38 | File.rm_rf!("installer/dayzee")
39 |
--------------------------------------------------------------------------------
/installer/templates/phx_assets/heroicons.js:
--------------------------------------------------------------------------------
1 | const plugin = require("tailwindcss/plugin")
2 | const fs = require("fs")
3 | const path = require("path")
4 |
5 | module.exports = plugin(function({matchComponents, theme}) {
6 | let iconsDir = path.join(__dirname, "..<%= if @in_umbrella, do: "/../.." %>/../deps/heroicons/optimized")
7 | let values = {}
8 | let icons = [
9 | ["", "/24/outline"],
10 | ["-solid", "/24/solid"],
11 | ["-mini", "/20/solid"],
12 | ["-micro", "/16/solid"]
13 | ]
14 | icons.forEach(([suffix, dir]) => {
15 | fs.readdirSync(path.join(iconsDir, dir)).forEach(file => {
16 | let name = path.basename(file, ".svg") + suffix
17 | values[name] = {name, fullPath: path.join(iconsDir, dir, file)}
18 | })
19 | })
20 | matchComponents({
21 | "hero": ({name, fullPath}) => {
22 | let content = fs.readFileSync(fullPath).toString().replace(/\r?\n|\r/g, "")
23 | content = encodeURIComponent(content)
24 | let size = theme("spacing.6")
25 | if (name.endsWith("-mini")) {
26 | size = theme("spacing.5")
27 | } else if (name.endsWith("-micro")) {
28 | size = theme("spacing.4")
29 | }
30 | return {
31 | [`--hero-${name}`]: `url('data:image/svg+xml;utf8,${content}')`,
32 | "-webkit-mask": `var(--hero-${name})`,
33 | "mask": `var(--hero-${name})`,
34 | "mask-repeat": "no-repeat",
35 | "background-color": "currentColor",
36 | "vertical-align": "middle",
37 | "display": "inline-block",
38 | "width": size,
39 | "height": size
40 | }
41 | }
42 | }, {values})
43 | })
44 |
--------------------------------------------------------------------------------
/installer/templates/phx_ecto/data_case.ex:
--------------------------------------------------------------------------------
1 | defmodule <%= @app_module %>.DataCase do
2 | @moduledoc """
3 | This module defines the setup for tests requiring
4 | access to the application's data layer.
5 |
6 | You may define functions here to be used as helpers in
7 | your tests.
8 |
9 | Finally, if the test case interacts with the database,
10 | we enable the SQL sandbox, so changes done to the database
11 | are reverted at the end of every test. If you are using
12 | PostgreSQL, you can even run database tests asynchronously
13 | by setting `use <%= @app_module %>.DataCase, async: true`, although
14 | this option is not recommended for other databases.
15 | """
16 |
17 | use ExUnit.CaseTemplate
18 |
19 | using do
20 | quote do
21 | alias <%= @app_module %>.Repo
22 |
23 | import Ecto
24 | import Ecto.Changeset
25 | import Ecto.Query
26 | import <%= @app_module %>.DataCase
27 | end
28 | end
29 |
30 | setup tags do
31 | <%= @app_module %>.DataCase.setup_sandbox(tags)
32 | :ok
33 | end
34 |
35 | @doc """
36 | Sets up the sandbox based on the test tags.
37 | """
38 | def setup_sandbox(tags) do
39 | <%= @adapter_config[:test_setup] %>
40 | end
41 |
42 | @doc """
43 | A helper that transforms changeset errors into a map of messages.
44 |
45 | assert {:error, changeset} = Accounts.create_user(%{password: "short"})
46 | assert "password is too short" in errors_on(changeset).password
47 | assert %{password: ["password is too short"]} = errors_on(changeset)
48 |
49 | """
50 | def errors_on(changeset) do
51 | Ecto.Changeset.traverse_errors(changeset, fn {message, opts} ->
52 | Regex.replace(~r"%{(\w+)}", message, fn _, key ->
53 | opts |> Keyword.get(String.to_existing_atom(key), key) |> to_string()
54 | end)
55 | end)
56 | end
57 | end
58 |
--------------------------------------------------------------------------------
/installer/templates/phx_ecto/formatter.exs:
--------------------------------------------------------------------------------
1 | [
2 | import_deps: [:ecto_sql],
3 | inputs: ["*.exs"]
4 | ]
5 |
--------------------------------------------------------------------------------
/installer/templates/phx_ecto/repo.ex:
--------------------------------------------------------------------------------
1 | defmodule <%= @app_module %>.Repo do
2 | use Ecto.Repo,
3 | otp_app: :<%= @app_name %>,
4 | adapter: <%= inspect @adapter_module %>
5 | end
6 |
--------------------------------------------------------------------------------
/installer/templates/phx_ecto/seeds.exs:
--------------------------------------------------------------------------------
1 | # Script for populating the database. You can run it as:
2 | #
3 | # mix run priv/repo/seeds.exs
4 | #
5 | # Inside the script, you can read and write to any of your
6 | # repositories directly:
7 | #
8 | # <%= @app_module %>.Repo.insert!(%<%= @app_module %>.SomeSchema{})
9 | #
10 | # We recommend using the bang functions (`insert!`, `update!`
11 | # and so on) as they will fail if something goes wrong.
12 |
--------------------------------------------------------------------------------
/installer/templates/phx_gettext/gettext.ex:
--------------------------------------------------------------------------------
1 | defmodule <%= @web_namespace %>.Gettext do
2 | @moduledoc """
3 | A module providing Internationalization with a gettext-based API.
4 |
5 | By using [Gettext](https://hexdocs.pm/gettext), your module compiles translations
6 | that you can use in your application. To use this Gettext backend module,
7 | call `use Gettext` and pass it as an option:
8 |
9 | use Gettext, backend: <%= @web_namespace %>.Gettext
10 |
11 | # Simple translation
12 | gettext("Here is the string to translate")
13 |
14 | # Plural translation
15 | ngettext("Here is the string to translate",
16 | "Here are the strings to translate",
17 | 3)
18 |
19 | # Domain-based translation
20 | dgettext("errors", "Here is the error message to translate")
21 |
22 | See the [Gettext Docs](https://hexdocs.pm/gettext) for detailed usage.
23 | """
24 | use Gettext.Backend, otp_app: :<%= @web_app_name %>
25 | end
26 |
--------------------------------------------------------------------------------
/installer/templates/phx_mailer/lib/app_name/mailer.ex:
--------------------------------------------------------------------------------
1 | defmodule <%= @app_module %>.Mailer do
2 | use Swoosh.Mailer, otp_app: :<%= @app_name %>
3 | end
4 |
--------------------------------------------------------------------------------
/installer/templates/phx_single/README.md:
--------------------------------------------------------------------------------
1 | # <%= @app_module %>
2 |
3 | To start your Phoenix server:
4 |
5 | * Run `mix setup` to install and setup dependencies
6 | * Start Phoenix endpoint with `mix phx.server` or inside IEx with `iex -S mix phx.server`
7 |
8 | Now you can visit [`localhost:4000`](http://localhost:4000) from your browser.
9 |
10 | Ready to run in production? Please [check our deployment guides](https://hexdocs.pm/phoenix/deployment.html).
11 |
12 | ## Learn more
13 |
14 | * Official website: https://www.phoenixframework.org/
15 | * Guides: https://hexdocs.pm/phoenix/overview.html
16 | * Docs: https://hexdocs.pm/phoenix
17 | * Forum: https://elixirforum.com/c/phoenix-forum
18 | * Source: https://github.com/phoenixframework/phoenix
19 |
--------------------------------------------------------------------------------
/installer/templates/phx_single/config/prod.exs:
--------------------------------------------------------------------------------
1 | import Config
2 |
3 | <%= if @javascript or @css do %>
4 | # Note we also include the path to a cache manifest
5 | # containing the digested version of static files. This
6 | # manifest is generated by the `mix assets.deploy` task,
7 | # which you should run after static files are built and
8 | # before starting your production server.
9 | config :<%= @web_app_name %>, <%= @endpoint_module %>, cache_static_manifest: "priv/static/cache_manifest.json"<% end %><%= if @mailer do %>
10 |
11 | # Configures Swoosh API Client
12 | config :swoosh, api_client: Swoosh.ApiClient.Req
13 |
14 | # Disable Swoosh Local Memory Storage
15 | config :swoosh, local: false<% end %>
16 |
17 | # Do not print debug messages in production
18 | config :logger, level: :info
19 |
20 | # Runtime production configuration, including reading
21 | # of environment variables, is done on config/runtime.exs.
22 |
--------------------------------------------------------------------------------
/installer/templates/phx_single/config/test.exs:
--------------------------------------------------------------------------------
1 | import Config
2 |
3 | # We don't run a server during test. If one is required,
4 | # you can enable the server option below.
5 | config :<%= @app_name %>, <%= @endpoint_module %>,
6 | http: [ip: {127, 0, 0, 1}, port: 4002],
7 | secret_key_base: "<%= @secret_key_base_test %>",
8 | server: false<%= if @mailer do %>
9 |
10 | # In test we don't send emails
11 | config :<%= @app_name %>, <%= @app_module %>.Mailer,
12 | adapter: Swoosh.Adapters.Test
13 |
14 | # Disable swoosh api client as it is only required for production adapters
15 | config :swoosh, :api_client, false<% end %>
16 |
17 | # Print only warnings and errors during test
18 | config :logger, level: :warning
19 |
20 | # Initialize plugs at runtime for faster test compilation
21 | config :phoenix, :plug_init_mode, :runtime<%= if @html do %>
22 |
23 | # Enable helpful, but potentially expensive runtime checks
24 | config :phoenix_live_view,
25 | enable_expensive_runtime_checks: true<% end %>
26 |
--------------------------------------------------------------------------------
/installer/templates/phx_single/formatter.exs:
--------------------------------------------------------------------------------
1 | [
2 | import_deps: [<%= if @ecto do %>:ecto, :ecto_sql, <% end %>:phoenix],<%= if @ecto do %>
3 | subdirectories: ["priv/*/migrations"],<% end %><%= if @html do %>
4 | plugins: [Phoenix.LiveView.HTMLFormatter],<% end %>
5 | inputs: [<%= if @html do %>"*.{heex,ex,exs}", "{config,lib,test}/**/*.{heex,ex,exs}"<% else %>"*.{ex,exs}", "{config,lib,test}/**/*.{ex,exs}"<% end %><%= if @ecto do %>, "priv/*/seeds.exs"<% end %>]
6 | ]
7 |
--------------------------------------------------------------------------------
/installer/templates/phx_single/gitignore:
--------------------------------------------------------------------------------
1 | # The directory Mix will write compiled artifacts to.
2 | /_build/
3 |
4 | # If you run "mix test --cover", coverage assets end up here.
5 | /cover/
6 |
7 | # The directory Mix downloads your dependencies sources to.
8 | /deps/
9 |
10 | # Where 3rd-party dependencies like ExDoc output generated docs.
11 | /doc/
12 |
13 | # Ignore .fetch files in case you like to edit your project deps locally.
14 | /.fetch
15 |
16 | # If the VM crashes, it generates a dump, let's ignore it too.
17 | erl_crash.dump
18 |
19 | # Also ignore archive artifacts (built via "mix archive.build").
20 | *.ez
21 |
22 | # Temporary files, for example, from tests.
23 | /tmp/
24 |
25 | # Ignore package tarball (built via "mix hex.build").
26 | <%= @app_name %>-*.tar
27 | <%= if @javascript or @css do %>
28 | # Ignore assets that are produced by build tools.
29 | /priv/static/assets/
30 |
31 | # Ignore digested assets cache.
32 | /priv/static/cache_manifest.json
33 |
34 | # In case you use Node.js/npm, you want to ignore these.
35 | npm-debug.log
36 | /assets/node_modules/
37 | <% end %><%= if @adapter_app == :ecto_sqlite3 do %>
38 | # Database files
39 | *.db
40 | *.db-*
41 | <% end %>
42 |
--------------------------------------------------------------------------------
/installer/templates/phx_single/lib/app_name.ex:
--------------------------------------------------------------------------------
1 | defmodule <%= @app_module %> do
2 | @moduledoc """
3 | <%= @app_module %> keeps the contexts that define your domain
4 | and business logic.
5 |
6 | Contexts are also responsible for managing your data, regardless
7 | if it comes from the database, an external API or others.
8 | """
9 | end
10 |
--------------------------------------------------------------------------------
/installer/templates/phx_single/lib/app_name/application.ex:
--------------------------------------------------------------------------------
1 | defmodule <%= @app_module %>.Application do
2 | # See https://hexdocs.pm/elixir/Application.html
3 | # for more information on OTP Applications
4 | @moduledoc false
5 |
6 | use Application
7 |
8 | @impl true
9 | def start(_type, _args) do
10 | children = [
11 | <%= @web_namespace %>.Telemetry,<%= if @ecto do %>
12 | <%= @app_module %>.Repo,<% end %><%= if @adapter_app == :ecto_sqlite3 do %>
13 | {Ecto.Migrator,
14 | repos: Application.fetch_env!(<%= inspect(String.to_atom(@app_name)) %>, :ecto_repos), skip: skip_migrations?()},<% end %>
15 | {DNSCluster, query: Application.get_env(<%= inspect(String.to_atom(@app_name)) %>, :dns_cluster_query) || :ignore},
16 | {Phoenix.PubSub, name: <%= @app_module %>.PubSub},
17 | # Start a worker by calling: <%= @app_module %>.Worker.start_link(arg)
18 | # {<%= @app_module %>.Worker, arg},
19 | # Start to serve requests, typically the last entry
20 | <%= @endpoint_module %>
21 | ]
22 |
23 | # See https://hexdocs.pm/elixir/Supervisor.html
24 | # for other strategies and supported options
25 | opts = [strategy: :one_for_one, name: <%= @app_module %>.Supervisor]
26 | Supervisor.start_link(children, opts)
27 | end
28 |
29 | # Tell Phoenix to update the endpoint configuration
30 | # whenever the application is updated.
31 | @impl true
32 | def config_change(changed, _new, removed) do
33 | <%= @endpoint_module %>.config_change(changed, removed)
34 | :ok
35 | end<%= if @adapter_app == :ecto_sqlite3 do %>
36 |
37 | defp skip_migrations?() do
38 | # By default, sqlite migrations are run when using a release
39 | System.get_env("RELEASE_NAME") == nil
40 | end<% end %>
41 | end
42 |
--------------------------------------------------------------------------------
/installer/templates/phx_single/test/test_helper.exs:
--------------------------------------------------------------------------------
1 | ExUnit.start()<%= if @ecto do %>
2 | <%= @adapter_config[:test_setup_all] %><% end %>
3 |
--------------------------------------------------------------------------------
/installer/templates/phx_static/app.css:
--------------------------------------------------------------------------------
1 | /* This file is for your main application CSS */
2 |
--------------------------------------------------------------------------------
/installer/templates/phx_static/app.js:
--------------------------------------------------------------------------------
1 | // For Phoenix.HTML support, including form and button helpers
2 | // copy the following scripts into your javascript bundle:
3 | // * deps/phoenix_html/priv/static/phoenix_html.js
4 |
5 | // For Phoenix.Channels support, copy the following scripts
6 | // into your javascript bundle:
7 | // * deps/phoenix/priv/static/phoenix.js
8 |
9 | // For Phoenix.LiveView support, copy the following scripts
10 | // into your javascript bundle:
11 | // * deps/phoenix_live_view/priv/static/phoenix_live_view.js
12 |
--------------------------------------------------------------------------------
/installer/templates/phx_static/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/phoenixframework/phoenix/45e29e47de26d644725a4c0a1c0a46cc46f777e1/installer/templates/phx_static/favicon.ico
--------------------------------------------------------------------------------
/installer/templates/phx_static/phoenix.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/phoenixframework/phoenix/45e29e47de26d644725a4c0a1c0a46cc46f777e1/installer/templates/phx_static/phoenix.png
--------------------------------------------------------------------------------
/installer/templates/phx_static/robots.txt:
--------------------------------------------------------------------------------
1 | # See https://www.robotstxt.org/robotstxt.html for documentation on how to use the robots.txt file
2 | #
3 | # To ban all spiders from the entire site uncomment the next two lines:
4 | # User-agent: *
5 | # Disallow: /
6 |
--------------------------------------------------------------------------------
/installer/templates/phx_test/controllers/error_html_test.exs:
--------------------------------------------------------------------------------
1 | defmodule <%= @web_namespace %>.ErrorHTMLTest do
2 | use <%= @web_namespace %>.ConnCase, async: true
3 |
4 | # Bring render_to_string/4 for testing custom views
5 | import Phoenix.Template, only: [render_to_string: 4]
6 |
7 | test "renders 404.html" do
8 | assert render_to_string(<%= @web_namespace %>.ErrorHTML, "404", "html", []) == "Not Found"
9 | end
10 |
11 | test "renders 500.html" do
12 | assert render_to_string(<%= @web_namespace %>.ErrorHTML, "500", "html", []) == "Internal Server Error"
13 | end
14 | end
15 |
--------------------------------------------------------------------------------
/installer/templates/phx_test/controllers/error_json_test.exs:
--------------------------------------------------------------------------------
1 | defmodule <%= @web_namespace %>.ErrorJSONTest do
2 | use <%= @web_namespace %>.ConnCase, async: true
3 |
4 | test "renders 404" do
5 | assert <%= @web_namespace %>.ErrorJSON.render("404.json", %{}) == %{errors: %{detail: "Not Found"}}
6 | end
7 |
8 | test "renders 500" do
9 | assert <%= @web_namespace %>.ErrorJSON.render("500.json", %{}) ==
10 | %{errors: %{detail: "Internal Server Error"}}
11 | end
12 | end
13 |
--------------------------------------------------------------------------------
/installer/templates/phx_test/controllers/page_controller_test.exs:
--------------------------------------------------------------------------------
1 | defmodule <%= @web_namespace %>.PageControllerTest do
2 | use <%= @web_namespace %>.ConnCase
3 |
4 | test "GET /", %{conn: conn} do
5 | conn = get(conn, ~p"/")
6 | assert html_response(conn, 200) =~ "Peace of mind from prototype to production"
7 | end
8 | end
9 |
--------------------------------------------------------------------------------
/installer/templates/phx_test/support/conn_case.ex:
--------------------------------------------------------------------------------
1 | defmodule <%= @web_namespace %>.ConnCase do
2 | @moduledoc """
3 | This module defines the test case to be used by
4 | tests that require setting up a connection.
5 |
6 | Such tests rely on `Phoenix.ConnTest` and also
7 | import other functionality to make it easier
8 | to build common data structures and query the data layer.
9 |
10 | Finally, if the test case interacts with the database,
11 | we enable the SQL sandbox, so changes done to the database
12 | are reverted at the end of every test. If you are using
13 | PostgreSQL, you can even run database tests asynchronously
14 | by setting `use <%= @web_namespace %>.ConnCase, async: true`, although
15 | this option is not recommended for other databases.
16 | """
17 |
18 | use ExUnit.CaseTemplate
19 |
20 | using do
21 | quote do
22 | # The default endpoint for testing
23 | @endpoint <%= @endpoint_module %>
24 |
25 | use <%= @web_namespace %>, :verified_routes
26 |
27 | # Import conveniences for testing with connections
28 | import Plug.Conn
29 | import Phoenix.ConnTest
30 | import <%= @web_namespace %>.ConnCase
31 | end
32 | end<%= if @ecto do %>
33 |
34 | setup tags do
35 | <%= @app_module %>.DataCase.setup_sandbox(tags)
36 | {:ok, conn: Phoenix.ConnTest.build_conn()}
37 | end<% else %>
38 |
39 | setup _tags do
40 | {:ok, conn: Phoenix.ConnTest.build_conn()}
41 | end<% end %>
42 | end
43 |
--------------------------------------------------------------------------------
/installer/templates/phx_umbrella/README.md:
--------------------------------------------------------------------------------
1 | # <%= @root_app_module %>
2 |
--------------------------------------------------------------------------------
/installer/templates/phx_umbrella/apps/app_name/README.md:
--------------------------------------------------------------------------------
1 | # <%= @app_module %>
2 |
3 | **TODO: Add description**
4 |
--------------------------------------------------------------------------------
/installer/templates/phx_umbrella/apps/app_name/config/config.exs:
--------------------------------------------------------------------------------
1 | import Config
2 |
3 | <%= if @namespaced? || @ecto do %>
4 | # Configure Mix tasks and generators
5 | config :<%= @app_name %><%= if @namespaced? do %>,
6 | namespace: <%= @app_module %><% end %><%= if @ecto do %>,
7 | ecto_repos: [<%= @app_module %>.Repo]<% end %><% end %><%= if @mailer do %>
8 |
9 | # Configures the mailer
10 | #
11 | # By default it uses the "Local" adapter which stores the emails
12 | # locally. You can see the emails in your browser, at "/dev/mailbox".
13 | #
14 | # For production it's recommended to configure a different adapter
15 | # at the `config/runtime.exs`.
16 | config :<%= @app_name %>, <%= @app_module %>.Mailer, adapter: Swoosh.Adapters.Local<% end %>
17 |
--------------------------------------------------------------------------------
/installer/templates/phx_umbrella/apps/app_name/formatter.exs:
--------------------------------------------------------------------------------
1 | [<%= if @ecto do %>
2 | import_deps: [:ecto, :ecto_sql],
3 | subdirectories: ["priv/*/migrations"],<% end %><%= if @html do %>
4 | plugins: [Phoenix.LiveView.HTMLFormatter],<% end %>
5 | inputs: [<%= if @html do %>"*.{heex,ex,exs}", "{config,lib,test}/**/*.{heex,ex,exs}"<% else %>"*.{ex,exs}", "{config,lib,test}/**/*.{ex,exs}"<% end %><%= if @ecto do %>, "priv/*/seeds.exs"<% end %>]
6 | ]
7 |
--------------------------------------------------------------------------------
/installer/templates/phx_umbrella/apps/app_name/gitignore:
--------------------------------------------------------------------------------
1 | # The directory Mix will write compiled artifacts to.
2 | /_build/
3 |
4 | # If you run "mix test --cover", coverage assets end up here.
5 | /cover/
6 |
7 | # The directory Mix downloads your dependencies sources to.
8 | /deps/
9 |
10 | # Where 3rd-party dependencies like ExDoc output generated docs.
11 | /doc/
12 |
13 | # Ignore .fetch files in case you like to edit your project deps locally.
14 | /.fetch
15 |
16 | # If the VM crashes, it generates a dump, let's ignore it too.
17 | erl_crash.dump
18 |
19 | # Also ignore archive artifacts (built via "mix archive.build").
20 | *.ez
21 |
22 | # Temporary files, for example, from tests.
23 | /tmp/
24 |
25 | # Ignore package tarball (built via "mix hex.build").
26 | <%= @app_name %>-*.tar
27 |
--------------------------------------------------------------------------------
/installer/templates/phx_umbrella/apps/app_name/lib/app_name.ex:
--------------------------------------------------------------------------------
1 | defmodule <%= @app_module %> do
2 | @moduledoc """
3 | <%= @app_module %> keeps the contexts that define your domain
4 | and business logic.
5 |
6 | Contexts are also responsible for managing your data, regardless
7 | if it comes from the database, an external API or others.
8 | """
9 | end
10 |
--------------------------------------------------------------------------------
/installer/templates/phx_umbrella/apps/app_name/lib/app_name/application.ex:
--------------------------------------------------------------------------------
1 | defmodule <%= @app_module %>.Application do
2 | # See https://hexdocs.pm/elixir/Application.html
3 | # for more information on OTP Applications
4 | @moduledoc false
5 |
6 | use Application
7 |
8 | @impl true
9 | def start(_type, _args) do
10 | children = [<%= if @ecto do %>
11 | <%= @app_module %>.Repo,<% end %><%= if @adapter_app == :ecto_sqlite3 do %>
12 | {Ecto.Migrator,
13 | repos: Application.fetch_env!(<%= inspect(String.to_atom(@app_name)) %>, :ecto_repos), skip: skip_migrations?()},<% end %>
14 | {DNSCluster, query: Application.get_env(<%= inspect(String.to_atom(@app_name)) %>, :dns_cluster_query) || :ignore},
15 | {Phoenix.PubSub, name: <%= @app_module %>.PubSub}
16 | # Start a worker by calling: <%= @app_module %>.Worker.start_link(arg)
17 | # {<%= @app_module %>.Worker, arg}
18 | ]
19 |
20 | Supervisor.start_link(children, strategy: :one_for_one, name: <%= @app_module %>.Supervisor)
21 | end<%= if @adapter_app == :ecto_sqlite3 do %>
22 |
23 | defp skip_migrations?() do
24 | # By default, sqlite migrations are run when using a release
25 | System.get_env("RELEASE_NAME") == nil
26 | end<% end %>
27 | end
28 |
--------------------------------------------------------------------------------
/installer/templates/phx_umbrella/apps/app_name/mix.exs:
--------------------------------------------------------------------------------
1 | defmodule <%= @app_module %>.MixProject do
2 | use Mix.Project
3 |
4 | def project do
5 | [
6 | app: :<%= @app_name %>,
7 | version: "0.1.0",
8 | build_path: "../../_build",
9 | config_path: "../../config/config.exs",
10 | deps_path: "../../deps",
11 | lockfile: "../../mix.lock",
12 | elixir: "~> 1.15",
13 | elixirc_paths: elixirc_paths(Mix.env()),
14 | start_permanent: Mix.env() == :prod,
15 | aliases: aliases(),
16 | deps: deps()
17 | ]
18 | end
19 |
20 | # Configuration for the OTP application.
21 | #
22 | # Type `mix help compile.app` for more information.
23 | def application do
24 | [
25 | mod: {<%= @app_module %>.Application, []},
26 | extra_applications: [:logger, :runtime_tools]
27 | ]
28 | end
29 |
30 | # Specifies which paths to compile per environment.
31 | defp elixirc_paths(:test), do: ["lib", "test/support"]
32 | defp elixirc_paths(_), do: ["lib"]
33 |
34 | # Specifies your project dependencies.
35 | #
36 | # Type `mix help deps` for examples and options.
37 | defp deps do
38 | [
39 | {:dns_cluster, "~> 0.2.0"},
40 | {:phoenix_pubsub, "~> 2.1"}<%= if @ecto do %>,
41 | {:ecto_sql, "~> 3.10"},
42 | {:<%= @adapter_app %>, ">= 0.0.0"},
43 | {:jason, "~> 1.2"}<% end %><%= if @mailer do %>,
44 | {:swoosh, "~> 1.16"},
45 | {:req, "~> 0.5"}<% end %>
46 | ]
47 | end
48 |
49 | # Aliases are shortcuts or tasks specific to the current project.
50 | #
51 | # See the documentation for `Mix` for more info on aliases.
52 | defp aliases do
53 | [
54 | setup: ["deps.get"<%= if @ecto do %>, "ecto.setup"<% end %>]<%= if @ecto do %>,
55 | "ecto.setup": ["ecto.create", "ecto.migrate", "run #{__DIR__}/priv/repo/seeds.exs"],
56 | "ecto.reset": ["ecto.drop", "ecto.setup"],
57 | test: ["ecto.create --quiet", "ecto.migrate --quiet", "test"]<% end %>
58 | ]
59 | end
60 | end
61 |
--------------------------------------------------------------------------------
/installer/templates/phx_umbrella/apps/app_name/test/test_helper.exs:
--------------------------------------------------------------------------------
1 | ExUnit.start()<%= if @ecto do %>
2 | <%= @adapter_config[:test_setup_all] %><% end %>
3 |
--------------------------------------------------------------------------------
/installer/templates/phx_umbrella/apps/app_name_web/README.md:
--------------------------------------------------------------------------------
1 | # <%= @web_namespace %>
2 |
3 | To start your Phoenix server:
4 |
5 | * Run `mix setup` to install and setup dependencies
6 | * Start Phoenix endpoint with `mix phx.server`
7 |
8 | Now you can visit [`localhost:4000`](http://localhost:4000) from your browser.
9 |
10 | Ready to run in production? Please [check our deployment guides](https://hexdocs.pm/phoenix/deployment.html).
11 |
12 | ## Learn more
13 |
14 | * Official website: https://www.phoenixframework.org/
15 | * Guides: https://hexdocs.pm/phoenix/overview.html
16 | * Docs: https://hexdocs.pm/phoenix
17 | * Forum: https://elixirforum.com/c/phoenix-forum
18 | * Source: https://github.com/phoenixframework/phoenix
19 |
--------------------------------------------------------------------------------
/installer/templates/phx_umbrella/apps/app_name_web/config/config.exs:
--------------------------------------------------------------------------------
1 | import Config
2 |
3 | <%= if @namespaced? || @ecto || @generators do %>
4 | config :<%= @web_app_name %><%= if @namespaced? do %>,
5 | namespace: <%= @web_namespace %><% end %><%= if @ecto do %>,
6 | ecto_repos: [<%= @app_module %>.Repo]<% end %><%= if @generators do %>,
7 | generators: <%= inspect @generators %><% end %>
8 |
9 | <% end %># Configures the endpoint
10 | config :<%= @web_app_name %>, <%= @endpoint_module %>,
11 | url: [host: "localhost"],
12 | adapter: <%= inspect @web_adapter_module %>,
13 | render_errors: [
14 | formats: [<%= if @html do%>html: <%= @web_namespace %>.ErrorHTML, <% end %>json: <%= @web_namespace %>.ErrorJSON],
15 | layout: false
16 | ],
17 | pubsub_server: <%= @app_module %>.PubSub,
18 | live_view: [signing_salt: "<%= @lv_signing_salt %>"]<%= if @javascript do %>
19 |
20 | # Configure esbuild (the version is required)
21 | config :esbuild,
22 | version: "0.25.4",
23 | <%= @web_app_name %>: [
24 | args:
25 | ~w(js/app.js --bundle --target=es2022 --outdir=../priv/static/assets/js --external:/fonts/* --external:/images/*),
26 | cd: Path.expand("../apps/<%= @web_app_name %>/assets", __DIR__),
27 | env: %{"NODE_PATH" => Path.expand("../deps", __DIR__)}
28 | ]<% end %><%= if @css do %>
29 |
30 | # Configure tailwind (the version is required)
31 | config :tailwind,
32 | version: "4.1.7",
33 | <%= @web_app_name %>: [
34 | args: ~w(
35 | --input=assets/css/app.css
36 | --output=priv/static/assets/css/app.css
37 | ),
38 | cd: Path.expand("../apps/<%= @web_app_name %>", __DIR__)
39 | ]<% end %>
40 |
--------------------------------------------------------------------------------
/installer/templates/phx_umbrella/apps/app_name_web/config/prod.exs:
--------------------------------------------------------------------------------
1 | import Config
2 |
3 | # Note we also include the path to a cache manifest
4 | # containing the digested version of static files. This
5 | # manifest is generated by the `mix phx.digest` task,
6 | # which you should run after static files are built and
7 | # before starting your production server.
8 | config :<%= @web_app_name %>, <%= @endpoint_module %>,
9 | url: [host: "example.com", port: 80],
10 | cache_static_manifest: "priv/static/cache_manifest.json"
11 |
--------------------------------------------------------------------------------
/installer/templates/phx_umbrella/apps/app_name_web/config/test.exs:
--------------------------------------------------------------------------------
1 | import Config
2 |
3 | # We don't run a server during test. If one is required,
4 | # you can enable the server option below.
5 | config :<%= @web_app_name %>, <%= @endpoint_module %>,
6 | http: [ip: {127, 0, 0, 1}, port: 4002],
7 | secret_key_base: "<%= @secret_key_base_test %>",
8 | server: false
9 |
--------------------------------------------------------------------------------
/installer/templates/phx_umbrella/apps/app_name_web/formatter.exs:
--------------------------------------------------------------------------------
1 | [
2 | import_deps: [:phoenix],<%= if @html do %>
3 | plugins: [Phoenix.LiveView.HTMLFormatter],<% end %>
4 | inputs: [<%= if @html do %>"*.{heex,ex,exs}", "{config,lib,test}/**/*.{heex,ex,exs}"<% else %>"*.{ex,exs}", "{config,lib,test}/**/*.{ex,exs}"<% end %>]
5 | ]
6 |
--------------------------------------------------------------------------------
/installer/templates/phx_umbrella/apps/app_name_web/gitignore:
--------------------------------------------------------------------------------
1 | # The directory Mix will write compiled artifacts to.
2 | /_build/
3 |
4 | # If you run "mix test --cover", coverage assets end up here.
5 | /cover/
6 |
7 | # The directory Mix downloads your dependencies sources to.
8 | /deps/
9 |
10 | # Where 3rd-party dependencies like ExDoc output generated docs.
11 | /doc/
12 |
13 | # Ignore .fetch files in case you like to edit your project deps locally.
14 | /.fetch
15 |
16 | # If the VM crashes, it generates a dump, let's ignore it too.
17 | erl_crash.dump
18 |
19 | # Also ignore archive artifacts (built via "mix archive.build").
20 | *.ez
21 |
22 | # Temporary files, for example, from tests.
23 | /tmp/
24 |
25 | # Ignore package tarball (built via "mix hex.build").
26 | <%= @web_app_name %>-*.tar
27 | <%= if @javascript or @css do %>
28 | # Ignore assets that are produced by build tools.
29 | /priv/static/assets/
30 |
31 | # Ignore digested assets cache.
32 | /priv/static/cache_manifest.json
33 |
34 | # In case you use Node.js/npm, you want to ignore these.
35 | npm-debug.log
36 | /assets/node_modules/
37 | <% end %><%= if @adapter_app == :ecto_sqlite3 do %>
38 | # Database files
39 | *.db
40 | *.db-*
41 | <% end %>
42 |
--------------------------------------------------------------------------------
/installer/templates/phx_umbrella/apps/app_name_web/lib/app_name/application.ex:
--------------------------------------------------------------------------------
1 | defmodule <%= @web_namespace %>.Application do
2 | # See https://hexdocs.pm/elixir/Application.html
3 | # for more information on OTP Applications
4 | @moduledoc false
5 |
6 | use Application
7 |
8 | @impl true
9 | def start(_type, _args) do
10 | children = [
11 | <%= @web_namespace %>.Telemetry,
12 | # Start a worker by calling: <%= @web_namespace %>.Worker.start_link(arg)
13 | # {<%= @web_namespace %>.Worker, arg},
14 | # Start to serve requests, typically the last entry
15 | <%= @endpoint_module %>
16 | ]
17 |
18 | # See https://hexdocs.pm/elixir/Supervisor.html
19 | # for other strategies and supported options
20 | opts = [strategy: :one_for_one, name: <%= @web_namespace %>.Supervisor]
21 | Supervisor.start_link(children, opts)
22 | end
23 |
24 | # Tell Phoenix to update the endpoint configuration
25 | # whenever the application is updated.
26 | @impl true
27 | def config_change(changed, _new, removed) do
28 | <%= @endpoint_module %>.config_change(changed, removed)
29 | :ok
30 | end
31 | end
32 |
--------------------------------------------------------------------------------
/installer/templates/phx_umbrella/apps/app_name_web/test/test_helper.exs:
--------------------------------------------------------------------------------
1 | ExUnit.start()<%= if @ecto do %>
2 | <%= @adapter_config[:test_setup_all] %><% end %>
3 |
--------------------------------------------------------------------------------
/installer/templates/phx_umbrella/config/config.exs:
--------------------------------------------------------------------------------
1 | # This file is responsible for configuring your umbrella
2 | # and **all applications** and their dependencies with the
3 | # help of the Config module.
4 | #
5 | # Note that all applications in your umbrella share the
6 | # same configuration and dependencies, which is why they
7 | # all use the same configuration file. If you want different
8 | # configurations or dependencies per app, it is best to
9 | # move said applications out of the umbrella.
10 | import Config
11 |
--------------------------------------------------------------------------------
/installer/templates/phx_umbrella/config/dev.exs:
--------------------------------------------------------------------------------
1 | import Config
2 |
3 | # Do not include metadata nor timestamps in development logs
4 | config :logger, :default_formatter, format: "[$level] $message\n"
5 |
6 | # Initialize plugs at runtime for faster development compilation
7 | config :phoenix, :plug_init_mode, :runtime<%= if @html do %>
8 |
9 | config :phoenix_live_view,
10 | # Include debug annotations and locations in rendered markup.
11 | # Changing this configuration will require mix clean and a full recompile.
12 | debug_heex_annotations: true,
13 | debug_tags_location: true,
14 | # Enable helpful, but potentially expensive runtime checks
15 | enable_expensive_runtime_checks: true<% end %><%= if @mailer do %>
16 |
17 | # Disable swoosh api client as it is only required for production adapters.
18 | config :swoosh, :api_client, false<% end %>
19 |
20 | # Set a higher stacktrace during development. Avoid configuring such
21 | # in production as building large stacktraces may be expensive.
22 | config :phoenix, :stacktrace_depth, 20
23 |
--------------------------------------------------------------------------------
/installer/templates/phx_umbrella/config/extra_config.exs:
--------------------------------------------------------------------------------
1 | import Config
2 |
3 | # Configures Elixir's Logger
4 | config :logger, :default_formatter,
5 | format: "$time $metadata[$level] $message\n",
6 | metadata: [:request_id]
7 |
8 | # Use Jason for JSON parsing in Phoenix
9 | config :phoenix, :json_library, Jason
10 |
11 | # Import environment specific config. This must remain at the bottom
12 | # of this file so it overrides the configuration defined above.
13 | import_config "#{config_env()}.exs"
14 |
--------------------------------------------------------------------------------
/installer/templates/phx_umbrella/config/prod.exs:
--------------------------------------------------------------------------------
1 | import Config
2 |
3 | <%= if @mailer do %>
4 | # Configures Swoosh API Client
5 | config :swoosh, :api_client, Swoosh.ApiClient.Req
6 |
7 | # Disable Swoosh Local Memory Storage
8 | config :swoosh, local: false<% end %>
9 |
10 | # Do not print debug messages in production
11 | config :logger, level: :info
12 |
13 | # Runtime production configuration, including reading
14 | # of environment variables, is done on config/runtime.exs.
15 |
--------------------------------------------------------------------------------
/installer/templates/phx_umbrella/config/runtime.exs:
--------------------------------------------------------------------------------
1 | import Config
2 |
3 | # config/runtime.exs is executed for all environments, including
4 | # during releases. It is executed after compilation and before the
5 | # system starts, so it is typically used to load production configuration
6 | # and secrets from environment variables or elsewhere. Do not define
7 | # any compile-time configuration in here, as it won't be applied.
8 | # The block below contains prod specific runtime configuration.
9 | if config_env() == :prod do
10 | config :<%= @app_name %>, :dns_cluster_query, System.get_env("DNS_CLUSTER_QUERY")
11 | end
12 |
--------------------------------------------------------------------------------
/installer/templates/phx_umbrella/config/test.exs:
--------------------------------------------------------------------------------
1 | import Config
2 |
3 | # Print only warnings and errors during test
4 | config :logger, level: :warning<%= if @mailer do %>
5 |
6 | # In test we don't send emails
7 | config :<%= @app_name %>, <%= @app_module %>.Mailer,
8 | adapter: Swoosh.Adapters.Test
9 |
10 | # Disable swoosh api client as it is only required for production adapters
11 | config :swoosh, :api_client, false<% end %>
12 |
13 | # Initialize plugs at runtime for faster test compilation
14 | config :phoenix, :plug_init_mode, :runtime<%= if @html do %>
15 |
16 | # Enable helpful, but potentially expensive runtime checks
17 | config :phoenix_live_view,
18 | enable_expensive_runtime_checks: true<% end %>
19 |
--------------------------------------------------------------------------------
/installer/templates/phx_umbrella/formatter.exs:
--------------------------------------------------------------------------------
1 | [<%= if @html do %>
2 | plugins: [Phoenix.LiveView.HTMLFormatter],<% end %>
3 | inputs: ["mix.exs", "config/*.exs"],
4 | subdirectories: ["apps/*"]
5 | ]
6 |
--------------------------------------------------------------------------------
/installer/templates/phx_umbrella/gitignore:
--------------------------------------------------------------------------------
1 | # The directory Mix will write compiled artifacts to.
2 | /_build/
3 |
4 | # If you run "mix test --cover", coverage assets end up here.
5 | /cover/
6 |
7 | # The directory Mix downloads your dependencies sources to.
8 | /deps/
9 |
10 | # Where 3rd-party dependencies like ExDoc output generated docs.
11 | /doc/
12 |
13 | # Ignore .fetch files in case you like to edit your project deps locally.
14 | /.fetch
15 |
16 | # If the VM crashes, it generates a dump, let's ignore it too.
17 | erl_crash.dump
18 |
19 | # Also ignore archive artifacts (built via "mix archive.build").
20 | *.ez
21 |
22 | # Temporary files, for example, from tests.
23 | /tmp/
24 |
25 | <%= if @adapter_app == :ecto_sqlite3 do %>
26 | # Database files
27 | *.db
28 | *.db-*
29 | <% end %>
30 |
--------------------------------------------------------------------------------
/installer/templates/phx_umbrella/mix.exs:
--------------------------------------------------------------------------------
1 | defmodule <%= @root_app_module %>.MixProject do
2 | use Mix.Project
3 |
4 | def project do
5 | [
6 | apps_path: "apps",
7 | version: "0.1.0",
8 | start_permanent: Mix.env() == :prod,
9 | deps: deps(),
10 | aliases: aliases(),
11 | listeners: [Phoenix.CodeReloader]
12 | ]
13 | end
14 |
15 | # Dependencies can be Hex packages:
16 | #
17 | # {:mydep, "~> 0.3.0"}
18 | #
19 | # Or git/path repositories:
20 | #
21 | # {:mydep, git: "https://github.com/elixir-lang/mydep.git", tag: "0.1.0"}
22 | #
23 | # Type "mix help deps" for more examples and options.
24 | #
25 | # Dependencies listed here are available only for this project
26 | # and cannot be accessed from applications inside the apps/ folder.
27 | defp deps do<%= if @html do %>
28 | [
29 | <%= if @dev or @phoenix_version.pre != [] do %><%= @phoenix_dep_umbrella_root %>,
30 | <% end %># Required to run "mix format" on ~H/.heex files from the umbrella root
31 | {:phoenix_live_view, ">= 0.0.0"}
32 | ]<% else %>
33 | []<% end %>
34 | end
35 |
36 | # Aliases are shortcuts or tasks specific to the current project.
37 | # For example, to install project dependencies and perform other setup tasks, run:
38 | #
39 | # $ mix setup
40 | #
41 | # See the documentation for `Mix` for more info on aliases.
42 | #
43 | # Aliases listed here are available only for this project
44 | # and cannot be accessed from applications inside the apps/ folder.
45 | defp aliases do
46 | [
47 | # run `mix setup` in all child apps
48 | setup: ["cmd mix setup"]
49 | ]
50 | end
51 | end
52 |
--------------------------------------------------------------------------------
/installer/templates/phx_web/components/layouts/root.html.heex:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | <.live_title default="<%= @app_module %>" suffix=" · Phoenix Framework">
8 | {assigns[:page_title]}
9 |
10 | <%= if not @css do %>
11 | <% end %>
12 | <%= if @css do %>
14 | <% end %>
32 |
33 |
34 | {@inner_content}
35 |
36 |
37 |
--------------------------------------------------------------------------------
/installer/templates/phx_web/controllers/error_html.ex:
--------------------------------------------------------------------------------
1 | defmodule <%= @web_namespace %>.ErrorHTML do
2 | @moduledoc """
3 | This module is invoked by your endpoint in case of errors on HTML requests.
4 |
5 | See config/config.exs.
6 | """
7 | use <%= @web_namespace %>, :html
8 |
9 | # If you want to customize your error pages,
10 | # uncomment the embed_templates/1 call below
11 | # and add pages to the error directory:
12 | #
13 | # * lib/<%= @lib_web_name %>/controllers/error_html/404.html.heex
14 | # * lib/<%= @lib_web_name %>/controllers/error_html/500.html.heex
15 | #
16 | # embed_templates "error_html/*"
17 |
18 | # The default is to render a plain text page based on
19 | # the template name. For example, "404.html" becomes
20 | # "Not Found".
21 | def render(template, _assigns) do
22 | Phoenix.Controller.status_message_from_template(template)
23 | end
24 | end
25 |
--------------------------------------------------------------------------------
/installer/templates/phx_web/controllers/error_json.ex:
--------------------------------------------------------------------------------
1 | defmodule <%= @web_namespace %>.ErrorJSON do
2 | @moduledoc """
3 | This module is invoked by your endpoint in case of errors on JSON requests.
4 |
5 | See config/config.exs.
6 | """
7 |
8 | # If you want to customize a particular status code,
9 | # you may add your own clauses, such as:
10 | #
11 | # def render("500.json", _assigns) do
12 | # %{errors: %{detail: "Internal Server Error"}}
13 | # end
14 |
15 | # By default, Phoenix returns the status message from
16 | # the template name. For example, "404.json" becomes
17 | # "Not Found".
18 | def render(template, _assigns) do
19 | %{errors: %{detail: Phoenix.Controller.status_message_from_template(template)}}
20 | end
21 | end
22 |
--------------------------------------------------------------------------------
/installer/templates/phx_web/controllers/page_controller.ex:
--------------------------------------------------------------------------------
1 | defmodule <%= @web_namespace %>.PageController do
2 | use <%= @web_namespace %>, :controller
3 |
4 | def home(conn, _params) do
5 | render(conn, :home)
6 | end
7 | end
8 |
--------------------------------------------------------------------------------
/installer/templates/phx_web/controllers/page_html.ex:
--------------------------------------------------------------------------------
1 | defmodule <%= @web_namespace %>.PageHTML do
2 | @moduledoc """
3 | This module contains pages rendered by PageController.
4 |
5 | See the `page_html` directory for all templates available.
6 | """
7 | use <%= @web_namespace %>, :html
8 |
9 | embed_templates "page_html/*"
10 | end
11 |
--------------------------------------------------------------------------------
/installer/templates/phx_web/endpoint.ex:
--------------------------------------------------------------------------------
1 | defmodule <%= @endpoint_module %> do
2 | use Phoenix.Endpoint, otp_app: :<%= @web_app_name %>
3 |
4 | # The session will be stored in the cookie and signed,
5 | # this means its contents can be read but not tampered with.
6 | # Set :encryption_salt if you would also like to encrypt it.
7 | @session_options [
8 | store: :cookie,
9 | key: "_<%= @web_app_name %>_key",
10 | signing_salt: "<%= @signing_salt %>",
11 | same_site: "Lax"
12 | ]
13 |
14 | <%= if !(@dashboard || @live) do %><%= "# " %><% end %>socket "/live", Phoenix.LiveView.Socket,
15 | <%= if !(@dashboard || @live) do %><%= "# " %><% end %> websocket: [connect_info: [session: @session_options]],
16 | <%= if !(@dashboard || @live) do %><%= "# " %><% end %> longpoll: [connect_info: [session: @session_options]]
17 |
18 | # Serve at "/" the static files from "priv/static" directory.
19 | #
20 | # When code reloading is disabled (e.g., in production),
21 | # the `gzip` option is enabled to serve compressed
22 | # static files generated by running `phx.digest`.
23 | plug Plug.Static,
24 | at: "/",
25 | from: :<%= @web_app_name %>,
26 | gzip: not code_reloading?,
27 | only: <%= @web_namespace %>.static_paths()
28 |
29 | # Code reloading can be explicitly enabled under the
30 | # :code_reloader configuration of your endpoint.
31 | if code_reloading? do<%= if @html do %>
32 | socket "/phoenix/live_reload/socket", Phoenix.LiveReloader.Socket
33 | plug Phoenix.LiveReloader<% end %>
34 | plug Phoenix.CodeReloader<%= if @ecto do %>
35 | plug Phoenix.Ecto.CheckRepoStatus, otp_app: :<%= @web_app_name %><% end %>
36 | end<%= if @dashboard do %>
37 |
38 | plug Phoenix.LiveDashboard.RequestLogger,
39 | param_key: "request_logger",
40 | cookie_key: "request_logger"<% end %>
41 |
42 | plug Plug.RequestId
43 | plug Plug.Telemetry, event_prefix: [:phoenix, :endpoint]
44 |
45 | plug Plug.Parsers,
46 | parsers: [:urlencoded, :multipart, :json],
47 | pass: ["*/*"],
48 | json_decoder: Phoenix.json_library()
49 |
50 | plug Plug.MethodOverride
51 | plug Plug.Head
52 | plug Plug.Session, @session_options
53 | plug <%= @web_namespace %>.Router
54 | end
55 |
--------------------------------------------------------------------------------
/installer/templates/phx_web/router.ex:
--------------------------------------------------------------------------------
1 | defmodule <%= @web_namespace %>.Router do
2 | use <%= @web_namespace %>, :router<%= if @html do %>
3 |
4 | pipeline :browser do
5 | plug :accepts, ["html"]
6 | plug :fetch_session
7 | plug :fetch_live_flash
8 | plug :put_root_layout, html: {<%= @web_namespace %>.Layouts, :root}
9 | plug :protect_from_forgery
10 | plug :put_secure_browser_headers
11 | end<% end %>
12 |
13 | pipeline :api do
14 | plug :accepts, ["json"]
15 | end<%= if @html do %>
16 |
17 | scope "/", <%= @web_namespace %> do
18 | pipe_through :browser
19 |
20 | get "/", PageController, :home
21 | end
22 |
23 | # Other scopes may use custom stacks.
24 | # scope "/api", <%= @web_namespace %> do
25 | # pipe_through :api
26 | # end<% else %>
27 |
28 | scope "/api", <%= @web_namespace %> do
29 | pipe_through :api
30 | end<% end %><%= if @dashboard || @mailer do %>
31 |
32 | # Enable <%= [@dashboard && "LiveDashboard", @mailer && "Swoosh mailbox preview"] |> Enum.filter(&(&1)) |> Enum.join(" and ") %> in development
33 | if Application.compile_env(:<%= @web_app_name %>, :dev_routes) do<%= if @dashboard do %>
34 | # If you want to use the LiveDashboard in production, you should put
35 | # it behind authentication and allow only admins to access it.
36 | # If your application does not have an admins-only section yet,
37 | # you can use Plug.BasicAuth to set up some basic authentication
38 | # as long as you are also using SSL (which you should anyway).
39 | import Phoenix.LiveDashboard.Router<% end %>
40 |
41 | scope "/dev" do<%= if @html do %>
42 | pipe_through :browser<% else %>
43 | pipe_through [:fetch_session, :protect_from_forgery]<% end %>
44 | <%= if @dashboard do %>
45 | live_dashboard "/dashboard", metrics: <%= @web_namespace %>.Telemetry<% end %><%= if @mailer do %>
46 | forward "/mailbox", Plug.Swoosh.MailboxPreview<% end %>
47 | end
48 | end<% end %>
49 | end
50 |
--------------------------------------------------------------------------------
/installer/test/phx_new_ecto_test.exs:
--------------------------------------------------------------------------------
1 | Code.require_file "mix_helper.exs", __DIR__
2 |
3 | defmodule Mix.Tasks.Phx.New.EctoTest do
4 | use ExUnit.Case
5 | import MixHelper
6 | import ExUnit.CaptureIO
7 |
8 | setup do
9 | # The shell asks to install deps.
10 | # We will politely say not.
11 | send self(), {:mix_shell_input, :yes?, false}
12 | :ok
13 | end
14 |
15 | @app_name "phx_ecto"
16 |
17 | test "new without args" do
18 | assert capture_io(fn -> Mix.Tasks.Phx.New.Ecto.run([]) end) =~
19 | "Creates a new Ecto project within an umbrella project."
20 | end
21 |
22 | test "new with barebones umbrella" do
23 | in_tmp_umbrella_project "new with barebones umbrella", fn ->
24 | files = ~w[../config/dev.exs ../config/test.exs ../config/prod.exs ../config/runtime.exs]
25 | Enum.each(files, &File.rm/1)
26 |
27 | assert_file "../config/config.exs", &refute(&1 =~ ~S[import_config "#{config_env()}.exs"])
28 | Mix.Tasks.Phx.New.Ecto.run([@app_name])
29 | assert_file "../config/config.exs", &assert(&1 =~ ~S[import_config "#{config_env()}.exs"])
30 | end
31 | end
32 |
33 | test "new outside umbrella", config do
34 | in_tmp config.test, fn ->
35 | assert_raise Mix.Error, ~r"The ecto task can only be run within an umbrella's apps directory", fn ->
36 | Mix.Tasks.Phx.New.Ecto.run ["007invalid"]
37 | end
38 | end
39 | end
40 |
41 | test "new with defaults", config do
42 | in_tmp_umbrella_project config.test, fn ->
43 | Mix.Tasks.Phx.New.Ecto.run([@app_name])
44 |
45 | # Install dependencies?
46 | assert_received {:mix_shell, :yes?, ["\nFetch and install dependencies?"]}
47 |
48 | # Instructions
49 | assert_received {:mix_shell, :info, ["\nWe are almost there" <> _ = msg]}
50 | assert msg =~ "$ cd phx_ecto"
51 | assert msg =~ "$ mix deps.get"
52 |
53 | assert_received {:mix_shell, :info, ["Then configure your database in config/dev.exs" <> _]}
54 | end
55 | end
56 | end
57 |
--------------------------------------------------------------------------------
/installer/test/test_helper.exs:
--------------------------------------------------------------------------------
1 | ExUnit.start()
2 |
--------------------------------------------------------------------------------
/integration_test/README.md:
--------------------------------------------------------------------------------
1 | ## Phoenix Integration Tests
2 |
3 | This project contains integration tests for phoenix's generated projects.
4 |
5 | ## Running tests
6 |
7 | To install dependencies, run:
8 |
9 | $ mix deps.get
10 |
11 | Then run the basic test suite (no dependencies on the database) with:
12 |
13 | $ mix test
14 |
15 | To run the test suite with tests that test a specific database, run:
16 |
17 | $ mix test --include database:postgresql
18 | $ mix test --include database:mysql
19 | $ mix test --include database:mssql
20 | $ mix test --include database:sqlite3
21 |
22 | For convenience, there is also a `docker-compose.yml` file that allows for starting up all of the supported databases locally.
23 |
24 | $ docker-compose up
25 |
26 | This allows all tests to be run with the following command
27 |
28 | $ mix test --include database
29 |
30 | Or alternatively, with docker and docker compose installed, you can just run `./docker.sh`.
31 |
32 | ## How tests are written
33 |
34 | In order to have consistent, repeatable builds, all dependencies for all phoenix
35 | project variations are listed in `mix.exs` and locked via `mix.lock`. If a
36 | dependency version needs to be updated, it can be updated with `mix.exs` or
37 | using `mix deps.update `.
38 |
39 | It is also important to note that dependencies are initially compiled with
40 | `MIX_ENV=test` and then copied to `_build/dev_` to improve test speed.
41 | Therefore, dependencies should not be listed in `mix.exs` with an `only: `
42 | option.
43 |
--------------------------------------------------------------------------------
/integration_test/config/config.exs:
--------------------------------------------------------------------------------
1 | import Config
2 |
3 | config :phoenix, :json_library, Jason
4 |
5 | config :swoosh, api_client: false
6 |
7 | config :tailwind, :version, "4.1.7"
8 |
9 | config :phoenix_live_view, enable_expensive_runtime_checks: true
10 |
--------------------------------------------------------------------------------
/integration_test/docker-compose.yml:
--------------------------------------------------------------------------------
1 | version: '3'
2 | services:
3 | postgres:
4 | image: postgres
5 | ports:
6 | - "5432:5432"
7 | environment:
8 | POSTGRES_PASSWORD: postgres
9 | mysql:
10 | image: mysql
11 | ports:
12 | - "3306:3306"
13 | environment:
14 | MYSQL_ALLOW_EMPTY_PASSWORD: "yes"
15 | mssql:
16 | image: mcr.microsoft.com/mssql/server:2019-latest
17 | environment:
18 | ACCEPT_EULA: Y
19 | SA_PASSWORD: some!Password
20 | ports:
21 | - "1433:1433"
22 |
--------------------------------------------------------------------------------
/integration_test/docker.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env sh -e
2 |
3 | # adapt with versions from .github/versions/ci.yml if necessary;
4 | # you can also override these with environment variables
5 | ELIXIR="${ELIXIR:-1.17.3}"
6 | ERLANG="${ERLANG:-27.1.2}"
7 | SUFFIX="${SUFFIX:-alpine-3.20.3}"
8 |
9 | # Get the directory of the script
10 | SCRIPT_DIR=$(dirname "$(readlink -f "$0")")
11 |
12 | # Get the parent directory
13 | PARENT_DIR=$(dirname "$SCRIPT_DIR")
14 |
15 | # Check if docker-compose is available
16 | if command -v docker-compose &> /dev/null
17 | then
18 | COMPOSE_CMD="docker-compose"
19 | elif docker compose version &> /dev/null
20 | then
21 | COMPOSE_CMD="docker compose"
22 | else
23 | echo "Error: Neither docker-compose nor the docker compose plugin is available."
24 | exit 1
25 | fi
26 |
27 | # Start databases
28 | $COMPOSE_CMD up -d
29 |
30 | # Run test script in container
31 | docker run --rm --network=integration_test_default \
32 | -w $PARENT_DIR -v $PARENT_DIR:$PARENT_DIR \
33 | -it hexpm/elixir:$ELIXIR-erlang-$ERLANG-$SUFFIX sh integration_test/test.sh
34 |
35 | $COMPOSE_CMD down
36 |
--------------------------------------------------------------------------------
/integration_test/mix.exs:
--------------------------------------------------------------------------------
1 | for path <- :code.get_path(),
2 | Regex.match?(~r/phx_new-[\w\.\-]+\/ebin$/, List.to_string(path)) do
3 | Code.delete_path(path)
4 | end
5 |
6 | defmodule Phoenix.Integration.MixProject do
7 | use Mix.Project
8 |
9 | def project do
10 | [
11 | app: :phoenix_integration,
12 | version: "0.1.0",
13 | elixir: "~> 1.15",
14 | elixirc_paths: elixirc_paths(Mix.env()),
15 | start_permanent: Mix.env() == :prod,
16 | deps: deps()
17 | ]
18 | end
19 |
20 | defp elixirc_paths(:test), do: ["lib", "test/support"]
21 | defp elixirc_paths(_), do: ["lib"]
22 |
23 | def application do
24 | [
25 | extra_applications: [:logger, :inets]
26 | ]
27 | end
28 |
29 | # IMPORTANT: Dependencies are initially compiled with `MIX_ENV=test` and then
30 | # copied to `_build/dev` to save time. Any dependencies with `only: :dev` set
31 | # will not be copied.
32 | defp deps do
33 | [
34 | {:phx_new, path: "../installer"},
35 | {:phoenix, path: "..", override: true},
36 | {:phoenix_ecto, "~> 4.5"},
37 | {:esbuild, "~> 0.9", runtime: false},
38 | {:ecto_sql, "~> 3.10"},
39 | {:postgrex, ">= 0.0.0"},
40 | {:myxql, ">= 0.0.0"},
41 | {:tds, ">= 0.0.0"},
42 | {:ecto_sqlite3, ">= 0.0.0"},
43 | {:phoenix_html, "~> 4.1"},
44 | {:phoenix_live_view, "~> 1.0"},
45 | {:dns_cluster, "~> 0.2.0"},
46 | {:floki, ">= 0.30.0"},
47 | {:phoenix_live_reload, "~> 1.2"},
48 | {:phoenix_live_dashboard, "~> 0.8.3"},
49 | {:telemetry_metrics, "~> 1.0"},
50 | {:telemetry_poller, "~> 1.0"},
51 | {:gettext, "~> 0.26"},
52 | {:jason, "~> 1.2"},
53 | {:swoosh, "~> 1.16"},
54 | {:bandit, "~> 1.0"},
55 | {:bcrypt_elixir, "~> 3.0"},
56 | {:argon2_elixir, "~> 4.0"},
57 | {:pbkdf2_elixir, "~> 2.0"},
58 | {:tailwind, "~> 0.3"},
59 | {:heroicons,
60 | github: "tailwindlabs/heroicons",
61 | tag: "v2.2.0",
62 | sparse: "optimized",
63 | app: false,
64 | compile: false,
65 | depth: 1},
66 | {:req, "~> 0.5"}
67 | ]
68 | end
69 | end
70 |
--------------------------------------------------------------------------------
/integration_test/test.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh -e
2 |
3 | mix local.rebar --force
4 | mix local.hex --force
5 |
6 | # Install Dependencies
7 | apk add --no-progress --update git socat make gcc libc-dev
8 |
9 | # Set up local proxies
10 | socat TCP-LISTEN:5432,fork TCP-CONNECT:postgres:5432&
11 | socat TCP-LISTEN:3306,fork TCP-CONNECT:mysql:3306&
12 | socat TCP-LISTEN:1433,fork TCP-CONNECT:mssql:1433&
13 |
14 | # Run installer tests
15 | echo "Running installer tests"
16 | cd installer
17 | mix deps.get
18 | mix test
19 |
20 | echo "Running integration tests"
21 | cd ../integration_test
22 | mix deps.get
23 | mix test --include database
24 |
--------------------------------------------------------------------------------
/integration_test/test/test_helper.exs:
--------------------------------------------------------------------------------
1 | # Copy _build/test to _build/dev so it only has to be compiled once
2 | File.rm_rf!(Path.expand("../_build/dev", __DIR__))
3 |
4 | File.cp_r!(
5 | Path.expand("../_build/test", __DIR__),
6 | Path.expand("../_build/dev", __DIR__)
7 | )
8 |
9 | ExUnit.configure(timeout: 180_000, exclude: [:database])
10 | ExUnit.start()
11 |
--------------------------------------------------------------------------------
/jest.config.js:
--------------------------------------------------------------------------------
1 | /*
2 | * For a detailed explanation regarding each configuration property, visit:
3 | * https://jestjs.io/docs/configuration
4 | */
5 |
6 | module.exports = {
7 | // Automatically clear mock calls and instances between every test
8 | clearMocks: true,
9 |
10 | // Indicates which provider should be used to instrument code for coverage
11 | coverageProvider: "v8",
12 |
13 | // The paths to modules that run some code to configure or set up the testing environment before each test
14 | setupFiles: [
15 | // "/setupTests.js"
16 | ],
17 |
18 | // The test environment that will be used for testing
19 | testEnvironment: "jest-environment-jsdom-global",
20 |
21 | testEnvironmentOptions: {
22 | url: "https://example.com"
23 | },
24 |
25 | // The regexp pattern or array of patterns that Jest uses to detect test files
26 | testRegex: "/assets/test/.*_test\\.js$",
27 | }
28 |
--------------------------------------------------------------------------------
/lib/mix/tasks/compile.phoenix.ex:
--------------------------------------------------------------------------------
1 | defmodule Mix.Tasks.Compile.Phoenix do
2 | use Mix.Task
3 | @recursive true
4 | @moduledoc false
5 |
6 | @doc false
7 | def run(_args) do
8 | IO.warn("""
9 | the :phoenix compiler is no longer required in your mix.exs.
10 |
11 | Please find the following line in your mix.exs and remove the :phoenix entry:
12 |
13 | compilers: [..., :phoenix, ...] ++ Mix.compilers(),
14 | """)
15 |
16 | {:ok, _} = Application.ensure_all_started(:phoenix)
17 |
18 | case touch() do
19 | [] -> {:noop, []}
20 | _ -> {:ok, []}
21 | end
22 | end
23 |
24 | @doc false
25 | def touch do
26 | Mix.Phoenix.modules()
27 | |> modules_for_recompilation
28 | |> modules_to_file_paths
29 | |> Stream.map(&touch_if_exists(&1))
30 | |> Stream.filter(&(&1 == :ok))
31 | |> Enum.to_list()
32 | end
33 |
34 | defp touch_if_exists(path) do
35 | :file.change_time(path, :calendar.local_time())
36 | end
37 |
38 | defp modules_for_recompilation(modules) do
39 | Stream.filter(modules, fn mod ->
40 | Code.ensure_loaded?(mod) and (phoenix_recompile?(mod) or mix_recompile?(mod))
41 | end)
42 | end
43 |
44 | defp phoenix_recompile?(mod) do
45 | function_exported?(mod, :__phoenix_recompile__?, 0) and mod.__phoenix_recompile__?()
46 | end
47 |
48 | if Version.match?(System.version(), ">= 1.11.0") do
49 | # Recompile is provided by Mix, we don't need to do anything
50 | defp mix_recompile?(_mod), do: false
51 | else
52 | defp mix_recompile?(mod) do
53 | function_exported?(mod, :__mix_recompile__?, 0) and mod.__mix_recompile__?()
54 | end
55 | end
56 |
57 | defp modules_to_file_paths(modules) do
58 | Stream.map(modules, fn mod -> mod.__info__(:compile)[:source] end)
59 | end
60 | end
61 |
--------------------------------------------------------------------------------
/lib/mix/tasks/phx.ex:
--------------------------------------------------------------------------------
1 | defmodule Mix.Tasks.Phx do
2 | use Mix.Task
3 |
4 | @shortdoc "Prints Phoenix help information"
5 |
6 | @moduledoc """
7 | Prints Phoenix tasks and their information.
8 |
9 | $ mix phx
10 |
11 | To print the Phoenix version, pass `-v` or `--version`, for example:
12 |
13 | $ mix phx --version
14 |
15 | """
16 |
17 | @version Mix.Project.config()[:version]
18 |
19 | @impl true
20 | @doc false
21 | def run([version]) when version in ~w(-v --version) do
22 | Mix.shell().info("Phoenix v#{@version}")
23 | end
24 |
25 | def run(args) do
26 | case args do
27 | [] -> general()
28 | _ -> Mix.raise "Invalid arguments, expected: mix phx"
29 | end
30 | end
31 |
32 | defp general() do
33 | Application.ensure_all_started(:phoenix)
34 | Mix.shell().info "Phoenix v#{Application.spec(:phoenix, :vsn)}"
35 | Mix.shell().info "Peace of mind from prototype to production"
36 | Mix.shell().info "\n## Options\n"
37 | Mix.shell().info "-v, --version # Prints Phoenix version\n"
38 | Mix.Tasks.Help.run(["--search", "phx."])
39 | end
40 | end
41 |
--------------------------------------------------------------------------------
/lib/mix/tasks/phx.gen.auth/hashing_library.ex:
--------------------------------------------------------------------------------
1 | defmodule Mix.Tasks.Phx.Gen.Auth.HashingLibrary do
2 | @moduledoc false
3 |
4 | defstruct [:name, :module, :mix_dependency, :test_config]
5 |
6 | @type t :: %__MODULE__{
7 | name: atom(),
8 | module: module(),
9 | mix_dependency: binary(),
10 | test_config: binary()
11 | }
12 |
13 | def build("bcrypt") do
14 | lib = %__MODULE__{
15 | name: :bcrypt,
16 | module: Bcrypt,
17 | mix_dependency: ~s|{:bcrypt_elixir, "~> 3.0"}|,
18 | test_config: """
19 | config :bcrypt_elixir, :log_rounds, 1
20 | """
21 | }
22 |
23 | {:ok, lib}
24 | end
25 |
26 | def build("pbkdf2") do
27 | lib = %__MODULE__{
28 | name: :pbkdf2,
29 | module: Pbkdf2,
30 | mix_dependency: ~s|{:pbkdf2_elixir, "~> 2.0"}|,
31 | test_config: """
32 | config :pbkdf2_elixir, :rounds, 1
33 | """
34 | }
35 |
36 | {:ok, lib}
37 | end
38 |
39 | def build("argon2") do
40 | lib = %__MODULE__{
41 | name: :argon2,
42 | module: Argon2,
43 | mix_dependency: ~s|{:argon2_elixir, "~> 4.0"}|,
44 | test_config: """
45 | config :argon2_elixir, t_cost: 1, m_cost: 8
46 | """
47 | }
48 |
49 | {:ok, lib}
50 | end
51 |
52 | def build(other) do
53 | {:error, {:unknown_library, other}}
54 | end
55 | end
56 |
--------------------------------------------------------------------------------
/lib/mix/tasks/phx.gen.auth/migration.ex:
--------------------------------------------------------------------------------
1 | defmodule Mix.Tasks.Phx.Gen.Auth.Migration do
2 | @moduledoc false
3 |
4 | defstruct [:ecto_adapter, :extensions, :column_definitions]
5 |
6 | def build(ecto_adapter) when is_atom(ecto_adapter) do
7 | %__MODULE__{
8 | ecto_adapter: ecto_adapter,
9 | extensions: extensions(ecto_adapter),
10 | column_definitions: column_definitions(ecto_adapter)
11 | }
12 | end
13 |
14 | defp extensions(Ecto.Adapters.Postgres) do
15 | ["execute \"CREATE EXTENSION IF NOT EXISTS citext\", \"\""]
16 | end
17 |
18 | defp extensions(_), do: []
19 |
20 | defp column_definitions(ecto_adapter) do
21 | for field <- ~w(email token)a,
22 | into: %{},
23 | do: {field, column_definition(field, ecto_adapter)}
24 | end
25 |
26 | defp column_definition(:email, Ecto.Adapters.Postgres), do: "add :email, :citext, null: false"
27 | defp column_definition(:email, Ecto.Adapters.SQLite3), do: "add :email, :string, null: false, collate: :nocase"
28 | defp column_definition(:email, _), do: "add :email, :string, null: false, size: 160"
29 |
30 | defp column_definition(:token, Ecto.Adapters.Postgres), do: "add :token, :binary, null: false"
31 |
32 | defp column_definition(:token, _), do: "add :token, :binary, null: false, size: 32"
33 | end
34 |
--------------------------------------------------------------------------------
/lib/mix/tasks/phx.gen.ex:
--------------------------------------------------------------------------------
1 | defmodule Mix.Tasks.Phx.Gen do
2 | use Mix.Task
3 |
4 | @shortdoc "Lists all available Phoenix generators"
5 |
6 | @moduledoc """
7 | Lists all available Phoenix generators.
8 |
9 | ## CRUD related generators
10 |
11 | The table below shows a summary of the contents created by the CRUD generators:
12 |
13 | | Task | Schema | Migration | Context | Controller | View | LiveView |
14 | |:------------------ |:-:|:-:|:-:|:-:|:-:|:-:|
15 | | `phx.gen.embedded` | x | | | | | |
16 | | `phx.gen.schema` | x | x | | | | |
17 | | `phx.gen.context` | x | x | x | | | |
18 | | `phx.gen.live` | x | x | x | | | x |
19 | | `phx.gen.json` | x | x | x | x | x | |
20 | | `phx.gen.html` | x | x | x | x | x | |
21 | """
22 |
23 | def run(_args) do
24 | Mix.Task.run("help", ["--search", "phx.gen."])
25 | end
26 | end
27 |
--------------------------------------------------------------------------------
/lib/mix/tasks/phx.gen.presence.ex:
--------------------------------------------------------------------------------
1 | defmodule Mix.Tasks.Phx.Gen.Presence do
2 | @shortdoc "Generates a Presence tracker"
3 |
4 | @moduledoc """
5 | Generates a Presence tracker.
6 |
7 | $ mix phx.gen.presence
8 | $ mix phx.gen.presence MyPresence
9 |
10 | The argument, which defaults to `Presence`, defines the module name of the
11 | Presence tracker.
12 |
13 | Generates a new file, `lib/my_app_web/channels/my_presence.ex`, where
14 | `my_presence` is the snake-cased version of the provided module name.
15 | """
16 | use Mix.Task
17 |
18 | @doc false
19 | def run([]) do
20 | run(["Presence"])
21 | end
22 | def run([alias_name]) do
23 | if Mix.Project.umbrella?() do
24 | Mix.raise "mix phx.gen.presence must be invoked from within your *_web application's root directory"
25 | end
26 | context_app = Mix.Phoenix.context_app()
27 | otp_app = Mix.Phoenix.otp_app()
28 | web_prefix = Mix.Phoenix.web_path(context_app)
29 | inflections = Mix.Phoenix.inflect(alias_name)
30 | inflections = Keyword.put(inflections, :module, "#{inflections[:web_module]}.#{inflections[:scoped]}")
31 |
32 | binding = inflections ++ [
33 | otp_app: otp_app,
34 | pubsub_server: Module.concat(inflections[:base], "PubSub")
35 | ]
36 |
37 | files = [
38 | {:eex, "presence.ex", Path.join(web_prefix, "channels/#{binding[:path]}.ex")},
39 | ]
40 |
41 | Mix.Phoenix.copy_from paths(), "priv/templates/phx.gen.presence", binding, files
42 |
43 | Mix.shell().info """
44 |
45 | Add your new module to your supervision tree,
46 | in lib/#{otp_app}/application.ex:
47 |
48 | children = [
49 | ...
50 | #{binding[:module]}
51 | ]
52 |
53 | You're all set! See the Phoenix.Presence docs for more details:
54 | https://hexdocs.pm/phoenix/Phoenix.Presence.html
55 | """
56 | end
57 |
58 | defp paths do
59 | [".", :phoenix]
60 | end
61 | end
62 |
--------------------------------------------------------------------------------
/lib/mix/tasks/phx.gen.secret.ex:
--------------------------------------------------------------------------------
1 | defmodule Mix.Tasks.Phx.Gen.Secret do
2 | @shortdoc "Generates a secret"
3 |
4 | @moduledoc """
5 | Generates a secret and prints it to the terminal.
6 |
7 | $ mix phx.gen.secret [length]
8 |
9 | By default, mix phx.gen.secret generates a key 64 characters long.
10 |
11 | The minimum value for `length` is 32.
12 | """
13 | use Mix.Task
14 |
15 | @doc false
16 | def run([]), do: run(["64"])
17 | def run([int]), do: int |> parse!() |> random_string() |> Mix.shell().info()
18 | def run([_|_]), do: invalid_args!()
19 |
20 | defp parse!(int) do
21 | case Integer.parse(int) do
22 | {int, ""} -> int
23 | _ -> invalid_args!()
24 | end
25 | end
26 |
27 | defp random_string(length) when length > 31 do
28 | :crypto.strong_rand_bytes(length) |> Base.encode64(padding: false) |> binary_part(0, length)
29 | end
30 | defp random_string(_), do: Mix.raise "The secret should be at least 32 characters long"
31 |
32 | @spec invalid_args!() :: no_return()
33 | defp invalid_args! do
34 | Mix.raise "mix phx.gen.secret expects a length as integer or no argument at all"
35 | end
36 | end
37 |
--------------------------------------------------------------------------------
/lib/mix/tasks/phx.server.ex:
--------------------------------------------------------------------------------
1 | defmodule Mix.Tasks.Phx.Server do
2 | use Mix.Task
3 |
4 | @shortdoc "Starts applications and their servers"
5 |
6 | @moduledoc """
7 | Starts the application by configuring all endpoints servers to run.
8 |
9 | Note: to start the endpoint without using this mix task you must set
10 | `server: true` in your `Phoenix.Endpoint` configuration.
11 |
12 | ## Command line options
13 |
14 | * `--open` - open browser window for each started endpoint
15 |
16 | Furthermore, this task accepts the same command-line options as
17 | `mix run`.
18 |
19 | For example, to run `phx.server` without recompiling:
20 |
21 | $ mix phx.server --no-compile
22 |
23 | The `--no-halt` flag is automatically added.
24 |
25 | Note that the `--no-deps-check` flag cannot be used this way,
26 | because Mix needs to check dependencies to find `phx.server`.
27 |
28 | To run `phx.server` without checking dependencies, you can run:
29 |
30 | $ mix do deps.loadpaths --no-deps-check, phx.server
31 | """
32 |
33 | @impl true
34 | def run(args) do
35 | Application.put_env(:phoenix, :serve_endpoints, true, persistent: true)
36 | Mix.Tasks.Run.run(run_args() ++ open_args(args))
37 | end
38 |
39 | defp iex_running? do
40 | Code.ensure_loaded?(IEx) and IEx.started?()
41 | end
42 |
43 | defp open_args(args) do
44 | if "--open" in args do
45 | Application.put_env(:phoenix, :browser_open, true)
46 | args -- ["--open"]
47 | else
48 | args
49 | end
50 | end
51 |
52 | defp run_args do
53 | if iex_running?(), do: [], else: ["--no-halt"]
54 | end
55 | end
56 |
--------------------------------------------------------------------------------
/lib/phoenix/code_reloader/mix_listener.ex:
--------------------------------------------------------------------------------
1 | defmodule Phoenix.CodeReloader.MixListener do
2 | @moduledoc false
3 |
4 | use GenServer
5 |
6 | @name __MODULE__
7 |
8 | @spec start_link(keyword) :: GenServer.on_start()
9 | def start_link(_opts) do
10 | GenServer.start_link(__MODULE__, {}, name: @name)
11 | end
12 |
13 | @spec started? :: boolean()
14 | def started? do
15 | Process.whereis(Phoenix.CodeReloader.MixListener) != nil
16 | end
17 |
18 | @doc """
19 | Unloads all modules invalidated by external compilations.
20 |
21 | Only reloads modules from the given apps.
22 | """
23 | @spec purge([atom()]) :: :ok
24 | def purge(apps) do
25 | GenServer.call(@name, {:purge, apps}, :infinity)
26 | end
27 |
28 | @impl true
29 | def init({}) do
30 | {:ok, %{to_purge: %{}}}
31 | end
32 |
33 | @impl true
34 | def handle_call({:purge, apps}, _from, state) do
35 | for app <- apps, modules = state.to_purge[app] do
36 | purge_modules(modules)
37 | end
38 |
39 | {:reply, :ok, %{state | to_purge: %{}}}
40 | end
41 |
42 | @impl true
43 | def handle_info({:modules_compiled, info}, state) do
44 | if info.os_pid == System.pid() do
45 | # Ignore compilations from ourselves, because the modules are
46 | # already updated in memory
47 | {:noreply, state}
48 | else
49 | %{changed: changed, removed: removed} = info.modules_diff
50 |
51 | state =
52 | update_in(state.to_purge[info.app], fn to_purge ->
53 | to_purge = to_purge || MapSet.new()
54 | to_purge = Enum.into(changed, to_purge)
55 | Enum.into(removed, to_purge)
56 | end)
57 |
58 | {:noreply, state}
59 | end
60 | end
61 |
62 | def handle_info(_message, state) do
63 | {:noreply, state}
64 | end
65 |
66 | defp purge_modules(modules) do
67 | for module <- modules do
68 | :code.purge(module)
69 | :code.delete(module)
70 | end
71 | end
72 | end
73 |
--------------------------------------------------------------------------------
/lib/phoenix/code_reloader/proxy.ex:
--------------------------------------------------------------------------------
1 | # A tiny proxy that stores all output sent to the group leader
2 | # while forwarding all requests to it.
3 | defmodule Phoenix.CodeReloader.Proxy do
4 | @moduledoc false
5 | use GenServer
6 |
7 | def start() do
8 | GenServer.start(__MODULE__, :ok)
9 | end
10 |
11 | def diagnostics(proxy, diagnostics) do
12 | GenServer.cast(proxy, {:diagnostics, diagnostics})
13 | end
14 |
15 | def stop(proxy) do
16 | GenServer.call(proxy, :stop, :infinity)
17 | end
18 |
19 | ## Callbacks
20 |
21 | def init(:ok) do
22 | {:ok, []}
23 | end
24 |
25 | def handle_cast({:diagnostics, diagnostics}, output) do
26 | {:noreply, diagnostics |> Enum.map(&diagnostic_to_chars/1) |> Enum.reverse(output)}
27 | end
28 |
29 | def handle_call(:stop, _from, output) do
30 | {:stop, :normal, Enum.reverse(output), output}
31 | end
32 |
33 | def handle_info(msg, output) do
34 | case msg do
35 | {:io_request, from, reply, {:put_chars, chars}} ->
36 | put_chars(from, reply, chars, output)
37 |
38 | {:io_request, from, reply, {:put_chars, m, f, as}} ->
39 | put_chars(from, reply, apply(m, f, as), output)
40 |
41 | {:io_request, from, reply, {:put_chars, _encoding, chars}} ->
42 | put_chars(from, reply, chars, output)
43 |
44 | {:io_request, from, reply, {:put_chars, _encoding, m, f, as}} ->
45 | put_chars(from, reply, apply(m, f, as), output)
46 |
47 | {:io_request, _from, _reply, _request} = msg ->
48 | send(Process.group_leader(), msg)
49 | {:noreply, output}
50 |
51 | _ ->
52 | {:noreply, output}
53 | end
54 | end
55 |
56 | defp put_chars(from, reply, chars, output) do
57 | send(Process.group_leader(), {:io_request, from, reply, {:put_chars, chars}})
58 | {:noreply, [chars | output]}
59 | end
60 |
61 | defp diagnostic_to_chars(%{severity: :error, message: "**" <> _ = message}) do
62 | "\n#{message}\n"
63 | end
64 |
65 | defp diagnostic_to_chars(%{severity: severity, message: message, file: file, position: position}) when is_binary(file) do
66 | "\n#{severity}: #{message}\n #{Path.relative_to_cwd(file)}#{position(position)}\n"
67 | end
68 |
69 | defp diagnostic_to_chars(%{severity: severity, message: message}) do
70 | "\n#{severity}: #{message}\n"
71 | end
72 |
73 | defp position({line, col}), do: ":#{line}:#{col}"
74 | defp position(line) when is_integer(line) and line > 0, do: ":#{line}"
75 | defp position(_), do: ""
76 | end
77 |
--------------------------------------------------------------------------------
/lib/phoenix/digester/compressor.ex:
--------------------------------------------------------------------------------
1 | defmodule Phoenix.Digester.Compressor do
2 | @moduledoc ~S"""
3 | Defines the `Phoenix.Digester.Compressor` behaviour for
4 | implementing static file compressors.
5 |
6 | A custom compressor expects 2 functions to be implemented.
7 |
8 | By default, Phoenix uses only `Phoenix.Digester.Gzip` to compress
9 | static files, but additional compressors can be defined and added
10 | to the digest process.
11 |
12 | ## Example
13 |
14 | If you wanted to compress files using an external brotli compression
15 | library, you could define a new module implementing the behaviour and add the
16 | module to the list of configured Phoenix static compressors.
17 |
18 | defmodule MyApp.BrotliCompressor do
19 | @behaviour Phoenix.Digester.Compressor
20 |
21 | def compress_file(file_path, content) do
22 | valid_extension = Path.extname(file_path) in Application.fetch_env!(:phoenix, :gzippable_exts)
23 | {:ok, compressed_content} = :brotli.encode(content)
24 |
25 | if valid_extension && byte_size(compressed_content) < byte_size(content) do
26 | {:ok, compressed_content}
27 | else
28 | :error
29 | end
30 | end
31 |
32 | def file_extensions do
33 | [".br"]
34 | end
35 | end
36 |
37 | # config/config.exs
38 | config :phoenix,
39 | static_compressors: [Phoenix.Digester.Gzip, MyApp.BrotliCompressor],
40 | # ...
41 | """
42 | @callback compress_file(Path.t(), binary()) :: {:ok, binary()} | :error
43 | @callback file_extensions() :: nonempty_list(String.t())
44 | end
45 |
--------------------------------------------------------------------------------
/lib/phoenix/digester/gzip.ex:
--------------------------------------------------------------------------------
1 | defmodule Phoenix.Digester.Gzip do
2 | @moduledoc ~S"""
3 | Gzip compressor for Phoenix.Digester
4 | """
5 | @behaviour Phoenix.Digester.Compressor
6 |
7 | def compress_file(file_path, content) do
8 | if Path.extname(file_path) in Application.fetch_env!(:phoenix, :gzippable_exts) do
9 | {:ok, :zlib.gzip(content)}
10 | else
11 | :error
12 | end
13 | end
14 |
15 | def file_extensions do
16 | [".gz"]
17 | end
18 | end
19 |
--------------------------------------------------------------------------------
/lib/phoenix/endpoint/sync_code_reload_plug.ex:
--------------------------------------------------------------------------------
1 | defmodule Phoenix.Endpoint.SyncCodeReloadPlug do
2 | @moduledoc ~S"""
3 | Wraps an Endpoint, attempting to sync with Phoenix's code reloader if
4 | an exception is raised which indicates that we may be in the middle of a reload.
5 |
6 | We detect this by looking at the raised exception and seeing if it indicates
7 | that the endpoint is not defined. This indicates that the code reloader may be
8 | midway through a compile, and that we should attempt to retry the request
9 | after the compile has completed. This is also why this must be implemented in
10 | a separate module (one that is not recompiled in a typical code reload cycle),
11 | since otherwise it may be the case that the endpoint itself is not defined.
12 | """
13 |
14 | @behaviour Plug
15 |
16 | def init({endpoint, opts}), do: {endpoint, endpoint.init(opts)}
17 |
18 | def call(conn, {endpoint, opts}), do: do_call(conn, endpoint, opts, true)
19 |
20 | defp do_call(conn, endpoint, opts, retry?) do
21 | try do
22 | endpoint.call(conn, opts)
23 | rescue
24 | exception in [UndefinedFunctionError] ->
25 | case exception do
26 | %UndefinedFunctionError{module: ^endpoint} when retry? ->
27 | # Sync with the code reloader and retry once
28 | Phoenix.CodeReloader.sync()
29 | do_call(conn, endpoint, opts, false)
30 |
31 | exception ->
32 | reraise(exception, __STACKTRACE__)
33 | end
34 | end
35 | end
36 | end
37 |
--------------------------------------------------------------------------------
/lib/phoenix/endpoint/watcher.ex:
--------------------------------------------------------------------------------
1 | defmodule Phoenix.Endpoint.Watcher do
2 | @moduledoc false
3 | require Logger
4 |
5 | def child_spec(args) do
6 | %{
7 | id: make_ref(),
8 | start: {__MODULE__, :start_link, [args]},
9 | restart: :transient
10 | }
11 | end
12 |
13 | def start_link({cmd, args}) do
14 | Task.start_link(__MODULE__, :watch, [to_string(cmd), args])
15 | end
16 |
17 | def watch(_cmd, {mod, fun, args}) do
18 | try do
19 | apply(mod, fun, args)
20 | catch
21 | kind, reason ->
22 | # The function returned a non-zero exit code.
23 | # Sleep for a couple seconds before exiting to
24 | # ensure this doesn't hit the supervisor's
25 | # max_restarts/max_seconds limit.
26 | Process.sleep(2000)
27 | :erlang.raise(kind, reason, __STACKTRACE__)
28 | end
29 | end
30 |
31 | def watch(cmd, args) when is_list(args) do
32 | {args, opts} = Enum.split_while(args, &is_binary(&1))
33 | opts = Keyword.merge([into: IO.stream(:stdio, :line), stderr_to_stdout: true], opts)
34 |
35 | try do
36 | System.cmd(cmd, args, opts)
37 | catch
38 | :error, :enoent ->
39 | relative = Path.relative_to_cwd(cmd)
40 |
41 | Logger.error(
42 | "Could not start watcher #{inspect(relative)} from #{inspect(cd(opts))}, executable does not exist"
43 | )
44 |
45 | exit(:shutdown)
46 | else
47 | {_, 0} ->
48 | :ok
49 |
50 | {_, _} ->
51 | # System.cmd returned a non-zero exit code
52 | # sleep for a couple seconds before exiting to ensure this doesn't
53 | # hit the supervisor's max_restarts / max_seconds limit
54 | Process.sleep(2000)
55 | exit(:watcher_command_error)
56 | end
57 | end
58 |
59 | defp cd(opts), do: opts[:cd] || File.cwd!()
60 | end
61 |
--------------------------------------------------------------------------------
/lib/phoenix/exceptions.ex:
--------------------------------------------------------------------------------
1 | defmodule Phoenix.NotAcceptableError do
2 | @moduledoc """
3 | Raised when one of the `accept*` headers is not accepted by the server.
4 |
5 | This exception is commonly raised by `Phoenix.Controller.accepts/2`
6 | which negotiates the media types the server is able to serve with
7 | the contents the client is able to render.
8 |
9 | If you are seeing this error, you should check if you are listing
10 | the desired formats in your `:accepts` plug or if you are setting
11 | the proper accept header in the client. The exception contains the
12 | acceptable mime types in the `accepts` field.
13 | """
14 |
15 | defexception message: nil, accepts: [], plug_status: 406
16 | end
17 |
18 | defmodule Phoenix.MissingParamError do
19 | @moduledoc """
20 | Raised when a key is expected to be present in the request parameters,
21 | but is not.
22 |
23 | This exception is raised by `Phoenix.Controller.scrub_params/2` which:
24 |
25 | * Checks to see if the required_key is present (can be empty)
26 | * Changes all empty parameters to nils ("" -> nil)
27 |
28 | If you are seeing this error, you should handle the error and surface it
29 | to the end user. It means that there is a parameter missing from the request.
30 | """
31 |
32 | defexception [:message, plug_status: 400]
33 |
34 | def exception([key: value]) do
35 | msg = "expected key #{inspect value} to be present in params, " <>
36 | "please send the expected key or adapt your scrub_params/2 call"
37 | %Phoenix.MissingParamError{message: msg}
38 | end
39 | end
40 |
41 | defmodule Phoenix.ActionClauseError do
42 | exception_keys =
43 | FunctionClauseError.__struct__()
44 | |> Map.keys()
45 | |> Kernel.--([:__exception__, :__struct__])
46 |
47 | defexception exception_keys
48 |
49 | @impl true
50 | def message(exception) do
51 | exception
52 | |> Map.put(:__struct__, FunctionClauseError)
53 | |> FunctionClauseError.message()
54 | end
55 |
56 | @impl true
57 | def blame(exception, stacktrace) do
58 | {exception, stacktrace} =
59 | exception
60 | |> Map.put(:__struct__, FunctionClauseError)
61 | |> FunctionClauseError.blame(stacktrace)
62 |
63 | exception = Map.put(exception, :__struct__, __MODULE__)
64 |
65 | {exception, stacktrace}
66 | end
67 | end
68 |
69 | defimpl Plug.Exception, for: Phoenix.ActionClauseError do
70 | def status(_), do: 400
71 | def actions(_), do: []
72 | end
73 |
--------------------------------------------------------------------------------
/lib/phoenix/flash.ex:
--------------------------------------------------------------------------------
1 | defmodule Phoenix.Flash do
2 | @moduledoc """
3 | Provides shared flash access.
4 | """
5 |
6 | @doc """
7 | Gets the key from the map of flash data.
8 |
9 | ## Examples
10 |
11 | ```heex
12 | <%= Phoenix.Flash.get(@flash, :info) %>
13 | <%= Phoenix.Flash.get(@flash, :error) %>
14 | ```
15 | """
16 | def get(%mod{}, key) when is_atom(key) or is_binary(key) do
17 | raise ArgumentError, """
18 | expected a map of flash data, but got a %#{inspect(mod)}{}
19 |
20 | Use the @flash assign set by the :fetch_flash plug instead:
21 |
22 | <%= Phoenix.Flash.get(@flash, :#{key}) %>
23 | """
24 | end
25 |
26 | def get(%{} = flash, key) when is_atom(key) or is_binary(key) do
27 | Map.get(flash, to_string(key))
28 | end
29 | end
30 |
--------------------------------------------------------------------------------
/lib/phoenix/socket/serializer.ex:
--------------------------------------------------------------------------------
1 | defmodule Phoenix.Socket.Serializer do
2 | @moduledoc """
3 | A behaviour that serializes incoming and outgoing socket messages.
4 |
5 | By default Phoenix provides a serializer that encodes to JSON and
6 | decodes JSON messages.
7 |
8 | Custom serializers may be configured in the socket.
9 | """
10 |
11 | @doc """
12 | Encodes a `Phoenix.Socket.Broadcast` struct to fastlane format.
13 | """
14 | @callback fastlane!(Phoenix.Socket.Broadcast.t()) ::
15 | {:socket_push, :text, iodata()}
16 | | {:socket_push, :binary, iodata()}
17 |
18 | @doc """
19 | Encodes `Phoenix.Socket.Message` and `Phoenix.Socket.Reply` structs to push format.
20 | """
21 | @callback encode!(Phoenix.Socket.Message.t() | Phoenix.Socket.Reply.t()) ::
22 | {:socket_push, :text, iodata()}
23 | | {:socket_push, :binary, iodata()}
24 |
25 | @doc """
26 | Decodes iodata into `Phoenix.Socket.Message` struct.
27 | """
28 | @callback decode!(iodata, options :: Keyword.t()) :: Phoenix.Socket.Message.t()
29 | end
30 |
--------------------------------------------------------------------------------
/lib/phoenix/socket/serializers/v1_json_serializer.ex:
--------------------------------------------------------------------------------
1 | defmodule Phoenix.Socket.V1.JSONSerializer do
2 | @moduledoc false
3 | @behaviour Phoenix.Socket.Serializer
4 |
5 | alias Phoenix.Socket.{Broadcast, Message, Reply}
6 |
7 | @impl true
8 | def fastlane!(%Broadcast{} = msg) do
9 | map = %Message{topic: msg.topic, event: msg.event, payload: msg.payload}
10 | {:socket_push, :text, encode_v1_fields_only(map)}
11 | end
12 |
13 | @impl true
14 | def encode!(%Reply{} = reply) do
15 | map = %Message{
16 | topic: reply.topic,
17 | event: "phx_reply",
18 | ref: reply.ref,
19 | payload: %{status: reply.status, response: reply.payload}
20 | }
21 |
22 | {:socket_push, :text, encode_v1_fields_only(map)}
23 | end
24 |
25 | def encode!(%Message{} = map) do
26 | {:socket_push, :text, encode_v1_fields_only(map)}
27 | end
28 |
29 | @impl true
30 | def decode!(message, _opts) do
31 | payload = Phoenix.json_library().decode!(message)
32 |
33 | case payload do
34 | %{} ->
35 | Phoenix.Socket.Message.from_map!(payload)
36 |
37 | other ->
38 | raise "V1 JSON Serializer expected a map, got #{inspect(other)}"
39 | end
40 | end
41 |
42 | defp encode_v1_fields_only(%Message{} = msg) do
43 | msg
44 | |> Map.take([:topic, :event, :payload, :ref])
45 | |> Phoenix.json_library().encode_to_iodata!()
46 | end
47 | end
48 |
--------------------------------------------------------------------------------
/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/phoenixframework/phoenix/45e29e47de26d644725a4c0a1c0a46cc46f777e1/logo.png
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "phoenix",
3 | "version": "1.8.0-rc.3",
4 | "description": "The official JavaScript client for the Phoenix web framework.",
5 | "license": "MIT",
6 | "module": "./priv/static/phoenix.mjs",
7 | "main": "./priv/static/phoenix.cjs.js",
8 | "unpkg": "./priv/static/phoenix.min.js",
9 | "jsdelivr": "./priv/static/phoenix.min.js",
10 | "exports": {
11 | "import": "./priv/static/phoenix.mjs",
12 | "require": "./priv/static/phoenix.cjs.js"
13 | },
14 | "repository": {
15 | "type": "git",
16 | "url": "git://github.com/phoenixframework/phoenix.git"
17 | },
18 | "author": "Chris McCord (https://www.phoenixframework.org)",
19 | "files": [
20 | "README.md",
21 | "LICENSE.md",
22 | "package.json",
23 | "priv/static/*",
24 | "assets/js/phoenix/*"
25 | ],
26 | "devDependencies": {
27 | "@babel/cli": "7.27.2",
28 | "@babel/core": "7.27.4",
29 | "@babel/preset-env": "7.27.2",
30 | "@eslint/js": "^9.28.0",
31 | "@stylistic/eslint-plugin": "^4.4.0",
32 | "documentation": "^14.0.3",
33 | "eslint": "9.28.0",
34 | "eslint-plugin-jest": "28.12.0",
35 | "jest": "^29.7.0",
36 | "jest-environment-jsdom": "^29.7.0",
37 | "jest-environment-jsdom-global": "^4.0.0",
38 | "jsdom": "^26.1.0",
39 | "mock-socket": "^9.3.1"
40 | },
41 | "scripts": {
42 | "test": "jest",
43 | "test.coverage": "jest --coverage",
44 | "test.watch": "jest --watch",
45 | "docs": "documentation build assets/js/phoenix/index.js -f html -o doc/js"
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/priv/static/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/phoenixframework/phoenix/45e29e47de26d644725a4c0a1c0a46cc46f777e1/priv/static/favicon.ico
--------------------------------------------------------------------------------
/priv/static/phoenix-orange.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/phoenixframework/phoenix/45e29e47de26d644725a4c0a1c0a46cc46f777e1/priv/static/phoenix-orange.png
--------------------------------------------------------------------------------
/priv/static/phoenix.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/phoenixframework/phoenix/45e29e47de26d644725a4c0a1c0a46cc46f777e1/priv/static/phoenix.png
--------------------------------------------------------------------------------
/priv/templates/phx.gen.auth/conn_case.exs:
--------------------------------------------------------------------------------
1 |
2 | @doc """
3 | Setup helper that registers and logs in <%= schema.plural %>.
4 |
5 | setup :register_and_log_in_<%= schema.singular %>
6 |
7 | It stores an updated connection and a registered <%= schema.singular %> in the
8 | test context.
9 | """
10 | def register_and_log_in_<%= schema.singular %>(%{conn: conn} = context) do
11 | <%= schema.singular %> = <%= inspect context.module %>Fixtures.<%= schema.singular %>_fixture()
12 | scope = <%= inspect scope_config.scope.module %>.for_<%= schema.singular %>(<%= schema.singular %>)
13 |
14 | opts =
15 | context
16 | |> Map.take([:token_authenticated_at])
17 | |> Enum.into([])
18 |
19 | %{conn: log_in_<%= schema.singular %>(conn, <%= schema.singular %>, opts), <%= schema.singular %>: <%= schema.singular %>, scope: scope}
20 | end
21 |
22 | @doc """
23 | Logs the given `<%= schema.singular %>` into the `conn`.
24 |
25 | It returns an updated `conn`.
26 | """
27 | def log_in_<%= schema.singular %>(conn, <%= schema.singular %>, opts \\ []) do
28 | token = <%= inspect context.module %>.generate_<%= schema.singular %>_session_token(<%= schema.singular %>)
29 |
30 | maybe_set_token_authenticated_at(token, opts[:token_authenticated_at])
31 |
32 | conn
33 | |> Phoenix.ConnTest.init_test_session(%{})
34 | |> Plug.Conn.put_session(:<%= schema.singular %>_token, token)
35 | end
36 |
37 | defp maybe_set_token_authenticated_at(_token, nil), do: nil
38 |
39 | defp maybe_set_token_authenticated_at(token, authenticated_at) do
40 | <%= inspect context.module %>Fixtures.override_token_authenticated_at(token, authenticated_at)
41 | end
42 |
--------------------------------------------------------------------------------
/priv/templates/phx.gen.auth/migration.ex:
--------------------------------------------------------------------------------
1 | defmodule <%= inspect schema.repo %>.Migrations.Create<%= Macro.camelize(schema.table) %>AuthTables do
2 | use Ecto.Migration
3 |
4 | def change do<%= if Enum.any?(migration.extensions) do %><%= for extension <- migration.extensions do %>
5 | <%= extension %><% end %>
6 | <% end %>
7 | create table(:<%= schema.table %><%= if schema.binary_id do %>, primary_key: false<% end %>) do
8 | <%= if schema.binary_id do %> add :id, :binary_id, primary_key: true
9 | <% end %> <%= migration.column_definitions[:email] %>
10 | add :hashed_password, :string
11 | add :confirmed_at, <%= inspect schema.timestamp_type %>
12 |
13 | timestamps(<%= if schema.timestamp_type != :naive_datetime, do: "type: #{inspect schema.timestamp_type}" %>)
14 | end
15 |
16 | create unique_index(:<%= schema.table %>, [:email])
17 |
18 | create table(:<%= schema.table %>_tokens<%= if schema.binary_id do %>, primary_key: false<% end %>) do
19 | <%= if schema.binary_id do %> add :id, :binary_id, primary_key: true
20 | <% end %> add :<%= schema.singular %>_id, references(:<%= schema.table %>, <%= if schema.binary_id do %>type: :binary_id, <% end %>on_delete: :delete_all), null: false
21 | <%= migration.column_definitions[:token] %>
22 | add :context, :string, null: false
23 | add :sent_to, :string
24 | add :authenticated_at, <%= inspect schema.timestamp_type %>
25 |
26 | timestamps(<%= if schema.timestamp_type != :naive_datetime, do: "type: #{inspect schema.timestamp_type}, " %>updated_at: false)
27 | end
28 |
29 | create index(:<%= schema.table %>_tokens, [:<%= schema.singular %>_id])
30 | create unique_index(:<%= schema.table %>_tokens, [:context, :token])
31 | end
32 | end
33 |
--------------------------------------------------------------------------------
/priv/templates/phx.gen.auth/registration_controller.ex:
--------------------------------------------------------------------------------
1 | defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web_namespace, schema.alias) %>RegistrationController do
2 | use <%= inspect context.web_module %>, :controller
3 |
4 | alias <%= inspect context.module %>
5 | alias <%= inspect schema.module %>
6 |
7 | def new(conn, _params) do
8 | changeset = <%= inspect context.alias %>.change_<%= schema.singular %>_email(%<%= inspect schema.alias %>{})
9 | render(conn, :new, changeset: changeset)
10 | end
11 |
12 | def create(conn, %{"<%= schema.singular %>" => <%= schema.singular %>_params}) do
13 | case <%= inspect context.alias %>.register_<%= schema.singular %>(<%= schema.singular %>_params) do
14 | {:ok, <%= schema.singular %>} ->
15 | {:ok, _} =
16 | <%= inspect context.alias %>.deliver_login_instructions(
17 | <%= schema.singular %>,
18 | &url(~p"<%= schema.route_prefix %>/log-in/#{&1}")
19 | )
20 |
21 | conn
22 | |> put_flash(
23 | :info,
24 | "An email was sent to #{<%= schema.singular %>.email}, please access it to confirm your account."
25 | )
26 | |> redirect(to: ~p"<%= schema.route_prefix %>/log-in")
27 |
28 | {:error, %Ecto.Changeset{} = changeset} ->
29 | render(conn, :new, changeset: changeset)
30 | end
31 | end
32 | end
33 |
--------------------------------------------------------------------------------
/priv/templates/phx.gen.auth/registration_controller_test.exs:
--------------------------------------------------------------------------------
1 | defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web_namespace, schema.alias) %>RegistrationControllerTest do
2 | use <%= inspect context.web_module %>.ConnCase<%= test_case_options %>
3 |
4 | import <%= inspect context.module %>Fixtures
5 |
6 | describe "GET <%= schema.route_prefix %>/register" do
7 | test "renders registration page", %{conn: conn} do
8 | conn = get(conn, ~p"<%= schema.route_prefix %>/register")
9 | response = html_response(conn, 200)
10 | assert response =~ "Register"
11 | assert response =~ ~p"<%= schema.route_prefix %>/log-in"
12 | assert response =~ ~p"<%= schema.route_prefix %>/register"
13 | end
14 |
15 | test "redirects if already logged in", %{conn: conn} do
16 | conn = conn |> log_in_<%= schema.singular %>(<%= schema.singular %>_fixture()) |> get(~p"<%= schema.route_prefix %>/register")
17 |
18 | assert redirected_to(conn) == ~p"/"
19 | end
20 | end
21 |
22 | describe "POST <%= schema.route_prefix %>/register" do
23 | @tag :capture_log
24 | test "creates account but does not log in", %{conn: conn} do
25 | email = unique_<%= schema.singular %>_email()
26 |
27 | conn =
28 | post(conn, ~p"<%= schema.route_prefix %>/register", %{
29 | "<%= schema.singular %>" => valid_<%= schema.singular %>_attributes(email: email)
30 | })
31 |
32 | refute get_session(conn, :<%= schema.singular %>_token)
33 | assert redirected_to(conn) == ~p"<%= schema.route_prefix %>/log-in"
34 |
35 | assert conn.assigns.flash["info"] =~
36 | ~r/An email was sent to .*, please access it to confirm your account/
37 | end
38 |
39 | test "render errors for invalid data", %{conn: conn} do
40 | conn =
41 | post(conn, ~p"<%= schema.route_prefix %>/register", %{
42 | "<%= schema.singular %>" => %{"email" => "with spaces"}
43 | })
44 |
45 | response = html_response(conn, 200)
46 | assert response =~ "Register"
47 | assert response =~ "must have the @ sign and no spaces"
48 | end
49 | end
50 | end
51 |
--------------------------------------------------------------------------------
/priv/templates/phx.gen.auth/registration_html.ex:
--------------------------------------------------------------------------------
1 | defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web_namespace, schema.alias) %>RegistrationHTML do
2 | use <%= inspect context.web_module %>, :html
3 |
4 | embed_templates "<%= schema.singular %>_registration_html/*"
5 | end
6 |
--------------------------------------------------------------------------------
/priv/templates/phx.gen.auth/registration_new.html.heex:
--------------------------------------------------------------------------------
1 | ={@<%= scope_config.scope.assign_key %>}>
2 |
3 | <.header class="text-center">
4 | Register for an account
5 | <:subtitle>
6 | Already registered?
7 | <.link navigate={~p"<%= schema.route_prefix %>/log-in"} class="font-semibold text-brand hover:underline">
8 | Log in
9 |
10 | to your account now.
11 |
12 |
13 |
14 | <.form :let={f} for={@changeset} action={~p"<%= schema.route_prefix %>/register"}>
15 | <.input
16 | field={f[:email]}
17 | type="email"
18 | label="Email"
19 | autocomplete="username"
20 | required
21 | phx-mounted={JS.focus()}
22 | />
23 |
24 | <.button variant="primary" phx-disable-with="Creating account..." class="w-full">
25 | Create an account
26 |
27 |
28 |
29 |
30 |
--------------------------------------------------------------------------------
/priv/templates/phx.gen.auth/scope.ex:
--------------------------------------------------------------------------------
1 | defmodule <%= inspect scope_config.scope.module %> do
2 | @moduledoc """
3 | Defines the scope of the caller to be used throughout the app.
4 |
5 | The `<%= inspect scope_config.scope.module %>` allows public interfaces to receive
6 | information about the caller, such as if the call is initiated from an
7 | end-user, and if so, which user. Additionally, such a scope can carry fields
8 | such as "super user" or other privileges for use as authorization, or to
9 | ensure specific code paths can only be access for a given scope.
10 |
11 | It is useful for logging as well as for scoping pubsub subscriptions and
12 | broadcasts when a caller subscribes to an interface or performs a particular
13 | action.
14 |
15 | Feel free to extend the fields on this struct to fit the needs of
16 | growing application requirements.
17 | """
18 |
19 | alias <%= inspect schema.module %>
20 |
21 | defstruct <%= schema.singular %>: nil
22 |
23 | @doc """
24 | Creates a scope for the given <%= schema.singular %>.
25 |
26 | Returns nil if no <%= schema.singular %> is given.
27 | """
28 | def for_<%= schema.singular %>(%<%= inspect schema.alias %>{} = <%= schema.singular %>) do
29 | %__MODULE__{<%= schema.singular %>: <%= schema.singular %>}
30 | end
31 |
32 | def for_<%= schema.singular %>(nil), do: nil
33 | end
34 |
--------------------------------------------------------------------------------
/priv/templates/phx.gen.auth/session_confirm.html.heex:
--------------------------------------------------------------------------------
1 | ={@<%= scope_config.scope.assign_key %>}>
2 |
3 | <.header class="text-center">Welcome {@<%= schema.singular %>.email}
4 |
5 | <.form
6 | :if={!@<%= schema.singular %>.confirmed_at}
7 | for={@form}
8 | id="confirmation_form"
9 | action={~p"<%= schema.route_prefix %>/log-in?_action=confirmed"}
10 | >
11 |
12 | <.input
13 | :if={!@<%= scope_config.scope.assign_key %>}
14 | field={@form[:remember_me]}
15 | type="checkbox"
16 | label="Keep me logged in"
17 | />
18 |
19 | <.button variant="primary" phx-disable-with="Confirming..." class="w-full">
20 | Confirm my account
21 |
22 |
23 |
24 | <.form :if={@<%= schema.singular %>.confirmed_at} for={@form} id="login_form" action={~p"<%= schema.route_prefix %>/log-in"}>
25 |
26 | <.input
27 | :if={!@<%= scope_config.scope.assign_key %>}
28 | field={@form[:remember_me]}
29 | type="checkbox"
30 | label="Keep me logged in"
31 | />
32 | <.button variant="primary" phx-disable-with="Logging in..." class="w-full">Log in
33 |
34 |
35 |
.confirmed_at} class="alert alert-outline mt-8">
36 | Tip: If you prefer passwords, you can enable them in the <%= schema.singular %> settings.
37 |
38 |
39 |
40 |
--------------------------------------------------------------------------------
/priv/templates/phx.gen.auth/session_html.ex:
--------------------------------------------------------------------------------
1 | defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web_namespace, schema.alias) %>SessionHTML do
2 | use <%= inspect context.web_module %>, :html
3 |
4 | embed_templates "<%= schema.singular %>_session_html/*"
5 |
6 | defp local_mail_adapter? do
7 | Application.get_env(:<%= Mix.Phoenix.otp_app() %>, <%= inspect context.base_module %>.Mailer)[:adapter] == Swoosh.Adapters.Local
8 | end
9 | end
10 |
--------------------------------------------------------------------------------
/priv/templates/phx.gen.auth/settings_edit.html.heex:
--------------------------------------------------------------------------------
1 | ={@<%= scope_config.scope.assign_key %>}>
2 | <.header class="text-center">
3 | Account Settings
4 | <:subtitle>Manage your account email address and password settings
5 |
6 |
7 | <.form :let={f} for={@email_changeset} action={~p"<%= schema.route_prefix %>/settings"} id="update_email">
8 |
9 |
10 | <.input field={f[:email]} type="email" label="Email" autocomplete="username" required />
11 |
12 | <.button phx-disable-with="Changing...">Change Email
13 |
14 |
15 |
16 |
17 | <.form :let={f} for={@password_changeset} action={~p"<%= schema.route_prefix %>/settings"} id="update_password">
18 |
19 |
20 | <.input
21 | field={f[:password]}
22 | type="password"
23 | label="New password"
24 | autocomplete="new-password"
25 | required
26 | />
27 | <.input
28 | field={f[:password_confirmation]}
29 | type="password"
30 | label="Confirm new password"
31 | autocomplete="new-password"
32 | required
33 | />
34 | <.button phx-disable-with="Changing...">
35 | Save Password
36 |
37 |
38 |
39 |
--------------------------------------------------------------------------------
/priv/templates/phx.gen.auth/settings_html.ex:
--------------------------------------------------------------------------------
1 | defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web_namespace, schema.alias) %>SettingsHTML do
2 | use <%= inspect context.web_module %>, :html
3 |
4 | embed_templates "<%= schema.singular %>_settings_html/*"
5 | end
6 |
--------------------------------------------------------------------------------
/priv/templates/phx.gen.channel/channel.ex:
--------------------------------------------------------------------------------
1 | defmodule <%= module %>Channel do
2 | use <%= web_module %>, :channel
3 |
4 | @impl true
5 | def join("<%= singular %>:lobby", payload, socket) do
6 | if authorized?(payload) do
7 | {:ok, socket}
8 | else
9 | {:error, %{reason: "unauthorized"}}
10 | end
11 | end
12 |
13 | # Channels can be used in a request/response fashion
14 | # by sending replies to requests from the client
15 | @impl true
16 | def handle_in("ping", payload, socket) do
17 | {:reply, {:ok, payload}, socket}
18 | end
19 |
20 | # It is also common to receive messages from the client and
21 | # broadcast to everyone in the current topic (<%= singular %>:lobby).
22 | @impl true
23 | def handle_in("shout", payload, socket) do
24 | broadcast(socket, "shout", payload)
25 | {:noreply, socket}
26 | end
27 |
28 | # Add authorization logic here as required.
29 | defp authorized?(_payload) do
30 | true
31 | end
32 | end
33 |
--------------------------------------------------------------------------------
/priv/templates/phx.gen.channel/channel_case.ex:
--------------------------------------------------------------------------------
1 | defmodule <%= web_module %>.ChannelCase do
2 | @moduledoc """
3 | This module defines the test case to be used by
4 | channel tests.
5 |
6 | Such tests rely on `Phoenix.ChannelTest` and also
7 | import other functionality to make it easier
8 | to build common data structures and query the data layer.
9 |
10 | Finally, if the test case interacts with the database,
11 | we enable the SQL sandbox, so changes done to the database
12 | are reverted at the end of every test. If you are using
13 | PostgreSQL, you can even run database tests asynchronously
14 | by setting `use <%= web_module %>.ChannelCase, async: true`, although
15 | this option is not recommended for other databases.
16 | """
17 |
18 | use ExUnit.CaseTemplate
19 |
20 | using do
21 | quote do
22 | # Import conveniences for testing with channels
23 | import Phoenix.ChannelTest
24 | import <%= web_module %>.ChannelCase
25 |
26 | # The default endpoint for testing
27 | @endpoint <%= web_module %>.Endpoint
28 | end
29 | end<%= if Code.ensure_loaded?(Ecto.Adapters.SQL) do %>
30 |
31 | setup tags do
32 | <%= base %>.DataCase.setup_sandbox(tags)
33 | :ok
34 | end<% else %>
35 |
36 | setup _tags do
37 | :ok
38 | end<% end %>
39 | end
40 |
--------------------------------------------------------------------------------
/priv/templates/phx.gen.channel/channel_test.exs:
--------------------------------------------------------------------------------
1 | defmodule <%= module %>ChannelTest do
2 | use <%= web_module %>.ChannelCase
3 |
4 | setup do
5 | {:ok, _, socket} =
6 | <%= web_module %>.UserSocket
7 | |> socket("user_id", %{some: :assign})
8 | |> subscribe_and_join(<%= module %>Channel, "<%= singular %>:lobby")
9 |
10 | %{socket: socket}
11 | end
12 |
13 | test "ping replies with status ok", %{socket: socket} do
14 | ref = push(socket, "ping", %{"hello" => "there"})
15 | assert_reply ref, :ok, %{"hello" => "there"}
16 | end
17 |
18 | test "shout broadcasts to <%= singular %>:lobby", %{socket: socket} do
19 | push(socket, "shout", %{"hello" => "all"})
20 | assert_broadcast "shout", %{"hello" => "all"}
21 | end
22 |
23 | test "broadcasts are pushed to the client", %{socket: socket} do
24 | broadcast_from!(socket, "broadcast", %{"some" => "data"})
25 | assert_push "broadcast", %{"some" => "data"}
26 | end
27 | end
28 |
--------------------------------------------------------------------------------
/priv/templates/phx.gen.context/access_no_schema.ex:
--------------------------------------------------------------------------------
1 |
2 | alias <%= inspect schema.module %>
3 |
4 | @doc """
5 | Returns the list of <%= schema.plural %>.
6 |
7 | ## Examples
8 |
9 | iex> list_<%= schema.plural %>()
10 | [%<%= inspect schema.alias %>{}, ...]
11 |
12 | """
13 | def list_<%= schema.plural %> do
14 | raise "TODO"
15 | end
16 |
17 | @doc """
18 | Gets a single <%= schema.singular %>.
19 |
20 | Raises if the <%= schema.human_singular %> does not exist.
21 |
22 | ## Examples
23 |
24 | iex> get_<%= schema.singular %>!(123)
25 | %<%= inspect schema.alias %>{}
26 |
27 | """
28 | def get_<%= schema.singular %>!(<%= primary_key %>), do: raise "TODO"
29 |
30 | @doc """
31 | Creates a <%= schema.singular %>.
32 |
33 | ## Examples
34 |
35 | iex> create_<%= schema.singular %>(%{field: value})
36 | {:ok, %<%= inspect schema.alias %>{}}
37 |
38 | iex> create_<%= schema.singular %>(%{field: bad_value})
39 | {:error, ...}
40 |
41 | """
42 | def create_<%= schema.singular %>(attrs) do
43 | raise "TODO"
44 | end
45 |
46 | @doc """
47 | Updates a <%= schema.singular %>.
48 |
49 | ## Examples
50 |
51 | iex> update_<%= schema.singular %>(<%= schema.singular %>, %{field: new_value})
52 | {:ok, %<%= inspect schema.alias %>{}}
53 |
54 | iex> update_<%= schema.singular %>(<%= schema.singular %>, %{field: bad_value})
55 | {:error, ...}
56 |
57 | """
58 | def update_<%= schema.singular %>(%<%= inspect schema.alias %>{} = <%= schema.singular %>, attrs) do
59 | raise "TODO"
60 | end
61 |
62 | @doc """
63 | Deletes a <%= inspect schema.alias %>.
64 |
65 | ## Examples
66 |
67 | iex> delete_<%= schema.singular %>(<%= schema.singular %>)
68 | {:ok, %<%= inspect schema.alias %>{}}
69 |
70 | iex> delete_<%= schema.singular %>(<%= schema.singular %>)
71 | {:error, ...}
72 |
73 | """
74 | def delete_<%= schema.singular %>(%<%= inspect schema.alias %>{} = <%= schema.singular %>) do
75 | raise "TODO"
76 | end
77 |
78 | @doc """
79 | Returns a data structure for tracking <%= schema.singular %> changes.
80 |
81 | ## Examples
82 |
83 | iex> change_<%= schema.singular %>(<%= schema.singular %>)
84 | %Todo{...}
85 |
86 | """
87 | def change_<%= schema.singular %>(%<%= inspect schema.alias %>{} = <%= schema.singular %>, _attrs \\ %{}) do
88 | raise "TODO"
89 | end
90 |
--------------------------------------------------------------------------------
/priv/templates/phx.gen.context/context.ex:
--------------------------------------------------------------------------------
1 | defmodule <%= inspect context.module %> do
2 | @moduledoc """
3 | The <%= context.name %> context.
4 | """
5 |
6 | import Ecto.Query, warn: false
7 | alias <%= inspect schema.repo %><%= schema.repo_alias %>
8 | end
9 |
--------------------------------------------------------------------------------
/priv/templates/phx.gen.context/context_test.exs:
--------------------------------------------------------------------------------
1 | defmodule <%= inspect context.module %>Test do
2 | use <%= inspect context.base_module %>.DataCase
3 |
4 | alias <%= inspect context.module %>
5 | end
6 |
--------------------------------------------------------------------------------
/priv/templates/phx.gen.context/fixtures.ex:
--------------------------------------------------------------------------------
1 | <%= for {attr, {_function_name, function_def, _needs_impl?}} <- schema.fixture_unique_functions do %> @doc """
2 | Generate a unique <%= schema.singular %> <%= attr %>.
3 | """
4 | <%= function_def %>
5 | <% end %> @doc """
6 | Generate a <%= schema.singular %>.
7 | """
8 | def <%= schema.singular %>_fixture(<%= if scope do %>scope, <% end %>attrs \\ %{}) do<%= if scope do %>
9 | attrs =
10 | Enum.into(attrs, %{
11 | <%= schema.fixture_params |> Enum.map(fn {key, code} -> " #{key}: #{code}" end) |> Enum.join(",\n") %>
12 | })
13 |
14 | {:ok, <%= schema.singular %>} = <%= inspect context.module %>.create_<%= schema.singular %>(scope, attrs)<% else %>
15 | {:ok, <%= schema.singular %>} =
16 | attrs
17 | |> Enum.into(%{
18 | <%= schema.fixture_params |> Enum.map(fn {key, code} -> " #{key}: #{code}" end) |> Enum.join(",\n") %>
19 | })
20 | |> <%= inspect context.module %>.create_<%= schema.singular %>()
21 | <% end %>
22 | <%= schema.singular %>
23 | end
24 |
--------------------------------------------------------------------------------
/priv/templates/phx.gen.context/fixtures_module.ex:
--------------------------------------------------------------------------------
1 | defmodule <%= inspect context.module %>Fixtures do
2 | @moduledoc """
3 | This module defines test helpers for creating
4 | entities via the `<%= inspect context.module %>` context.
5 | """
6 | end
7 |
--------------------------------------------------------------------------------
/priv/templates/phx.gen.embedded/embedded_schema.ex:
--------------------------------------------------------------------------------
1 | defmodule <%= inspect schema.module %> do
2 | use Ecto.Schema
3 | import Ecto.Changeset
4 | alias <%= inspect schema.module %>
5 |
6 | embedded_schema do <%= if !Enum.empty?(schema.types) do %>
7 | <%= Mix.Phoenix.Schema.format_fields_for_schema(schema) %><% end %>
8 | <%= for {_, k, _, _} <- schema.assocs do %> field <%= inspect k %>, <%= if schema.binary_id do %>:binary_id<% else %>:id<% end %>
9 | <% end %> end
10 |
11 | @doc false
12 | def changeset(%<%= inspect schema.alias %>{} = <%= schema.singular %>, attrs) do
13 | <%= schema.singular %>
14 | |> cast(attrs, [<%= Enum.map_join(schema.attrs, ", ", &inspect(elem(&1, 0))) %>])
15 | |> validate_required([<%= Enum.map_join(schema.attrs, ", ", &inspect(elem(&1, 0))) %>])
16 | end
17 | end
18 |
--------------------------------------------------------------------------------
/priv/templates/phx.gen.html/edit.html.heex:
--------------------------------------------------------------------------------
1 | <%= scope.assign_key %>={@<%= scope.assign_key %>}<% end %>>
2 | <.header>
3 | Edit <%= schema.human_singular %> {@<%= schema.singular %>.<%= primary_key %>}
4 | <:subtitle>Use this form to manage <%= schema.singular %> records in your database.
5 |
6 |
7 | <.<%= schema.singular %>_form changeset={@changeset} action={~p"<%= scope_assign_route_prefix %><%= schema.route_prefix %>/#{@<%= schema.singular %>}"} return_to={~p"<%= scope_assign_route_prefix %><%= schema.route_prefix %>"} />
8 |
9 |
--------------------------------------------------------------------------------
/priv/templates/phx.gen.html/html.ex:
--------------------------------------------------------------------------------
1 | defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web_namespace, schema.alias) %>HTML do
2 | use <%= inspect context.web_module %>, :html
3 |
4 | embed_templates "<%= schema.singular %>_html/*"
5 |
6 | @doc """
7 | Renders a <%= schema.singular %> form.
8 |
9 | The form is defined in the template at
10 | <%= schema.singular %>_html/<%= schema.singular %>_form.html.heex
11 | """
12 | attr :changeset, Ecto.Changeset, required: true
13 | attr :action, :string, required: true
14 | attr :return_to, :string, default: nil
15 |
16 | def <%= schema.singular %>_form(assigns)
17 | end
18 |
--------------------------------------------------------------------------------
/priv/templates/phx.gen.html/index.html.heex:
--------------------------------------------------------------------------------
1 | <%= scope.assign_key %>={@<%= scope.assign_key %>}<% end %>>
2 | <.header>
3 | Listing <%= schema.human_plural %>
4 | <:actions>
5 | <.button href={~p"<%= scope_assign_route_prefix %><%= schema.route_prefix %>/new"}>
6 | <.icon name="hero-plus" /> New <%= schema.human_singular %>
7 |
8 |
9 |
10 |
11 | <.table id="<%= schema.plural %>" rows={@<%= schema.collection %>} row_click={&JS.navigate(~p"<%= scope_assign_route_prefix %><%= schema.route_prefix %>/#{&1}")}><%= for {k, _} <- schema.attrs do %>
12 | <:col :let={<%= schema.singular %>} label="<%= Phoenix.Naming.humanize(Atom.to_string(k)) %>">{<%= schema.singular %>.<%= k %>}<% end %>
13 | <:action :let={<%= schema.singular %>}>
14 |
15 | <.link navigate={~p"<%= scope_assign_route_prefix %><%= schema.route_prefix %>/#{<%= schema.singular %>}"}>Show
16 |
17 | <.link navigate={~p"<%= scope_assign_route_prefix %><%= schema.route_prefix %>/#{<%= schema.singular %>}/edit"}>Edit
18 |
19 | <:action :let={<%= schema.singular %>}>
20 | <.link href={~p"<%= scope_assign_route_prefix %><%= schema.route_prefix %>/#{<%= schema.singular %>}"} method="delete" data-confirm="Are you sure?">
21 | Delete
22 |
23 |
24 |
25 |
26 |
--------------------------------------------------------------------------------
/priv/templates/phx.gen.html/new.html.heex:
--------------------------------------------------------------------------------
1 | <%= scope.assign_key %>={@<%= scope.assign_key %>}<% end %>>
2 | <.header>
3 | New <%= schema.human_singular %>
4 | <:subtitle>Use this form to manage <%= schema.singular %> records in your database.
5 |
6 |
7 | <.<%= schema.singular %>_form changeset={@changeset} action={~p"<%= scope_assign_route_prefix %><%= schema.route_prefix %>"} return_to={~p"<%= scope_assign_route_prefix %><%= schema.route_prefix %>"} />
8 |
9 |
--------------------------------------------------------------------------------
/priv/templates/phx.gen.html/resource_form.html.heex:
--------------------------------------------------------------------------------
1 | <.form :let={f} for={@changeset} action={@action}>
2 | <%= Mix.Tasks.Phx.Gen.Html.indent_inputs(inputs, 2) %>
3 |
4 | <.button variant="primary">Save <%= schema.human_singular %>
5 | <.button :if={@return_to} href={@return_to}>Cancel
6 |
7 |
8 |
--------------------------------------------------------------------------------
/priv/templates/phx.gen.html/show.html.heex:
--------------------------------------------------------------------------------
1 | <%= scope.assign_key %>={@<%= scope.assign_key %>}<% end %>>
2 | <.header>
3 | <%= schema.human_singular %> {@<%= schema.singular %>.<%= primary_key %>}
4 | <:subtitle>This is a <%= schema.singular %> record from your database.
5 | <:actions>
6 | <.button navigate={~p"<%= scope_assign_route_prefix %><%= schema.route_prefix %>"}>
7 | <.icon name="hero-arrow-left" />
8 |
9 | <.button variant="primary" navigate={~p"<%= scope_assign_route_prefix %><%= schema.route_prefix %>/#{@<%= schema.singular %>}/edit?return_to=show"}>
10 | <.icon name="hero-pencil-square" /> Edit <%= schema.singular %>
11 |
12 |
13 |
14 |
15 | <.list><%= for {k, _} <- schema.attrs do %>
16 | <:item title="<%= Phoenix.Naming.humanize(Atom.to_string(k)) %>">{@<%= schema.singular %>.<%= k %>}<% end %>
17 |
18 |
19 |
--------------------------------------------------------------------------------
/priv/templates/phx.gen.json/changeset_json.ex:
--------------------------------------------------------------------------------
1 | defmodule <%= inspect context.web_module %>.ChangesetJSON do
2 | @doc """
3 | Renders changeset errors.
4 | """<%= if core_components? do %>
5 | def error(%{changeset: changeset}) do
6 | # When encoded, the changeset returns its errors
7 | # as a JSON object. So we just pass it forward.
8 | %{errors: Ecto.Changeset.traverse_errors(changeset, &<%= inspect context.web_module %>.CoreComponents.translate_error/1)}
9 | end<% else %>
10 | def error(%{changeset: changeset}) do
11 | # When encoded, the changeset returns its errors
12 | # as a JSON object. So we just pass it forward.
13 | %{errors: Ecto.Changeset.traverse_errors(changeset, &translate_error/1)}
14 | end
15 | <%= if gettext? do %>
16 | defp translate_error({msg, opts}) do
17 | # set by Ecto and indicates we should also apply plural rules.
18 | if count = opts[:count] do
19 | Gettext.dngettext(<%= inspect context.web_module %>.Gettext, "errors", msg, msg, count, opts)
20 | else
21 | Gettext.dgettext(<%= inspect context.web_module %>.Gettext, "errors", msg, opts)
22 | end
23 | end
24 | <% else %>
25 | defp translate_error({msg, opts}) do
26 | # You can make use of gettext to translate error messages by
27 | # uncommenting and adjusting the following code:
28 |
29 | # if count = opts[:count] do
30 | # Gettext.dngettext(<%= inspect context.web_module %>.Gettext, "errors", msg, msg, count, opts)
31 | # else
32 | # Gettext.dgettext(<%= inspect context.web_module %>.Gettext, "errors", msg, opts)
33 | # end
34 |
35 | Enum.reduce(opts, msg, fn {key, value}, acc ->
36 | String.replace(acc, "%{#{key}}", fn _ -> to_string(value) end)
37 | end)
38 | end<% end %><% end %>
39 | end
40 |
--------------------------------------------------------------------------------
/priv/templates/phx.gen.json/controller.ex:
--------------------------------------------------------------------------------
1 | defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web_namespace, schema.alias) %>Controller do
2 | use <%= inspect context.web_module %>, :controller
3 |
4 | alias <%= inspect context.module %>
5 | alias <%= inspect schema.module %>
6 |
7 | action_fallback <%= inspect context.web_module %>.FallbackController
8 |
9 | def index(conn, _params) do
10 | <%= schema.plural %> = <%= inspect context.alias %>.list_<%= schema.plural %>(<%= conn_scope %>)
11 | render(conn, :index, <%= schema.plural %>: <%= schema.plural %>)
12 | end
13 |
14 | def create(conn, %{<%= inspect schema.singular %> => <%= schema.singular %>_params}) do
15 | with {:ok, %<%= inspect schema.alias %>{} = <%= schema.singular %>} <- <%= inspect context.alias %>.create_<%= schema.singular %>(<%= context_scope_prefix %><%= schema.singular %>_params) do
16 | conn
17 | |> put_status(:created)
18 | |> put_resp_header("location", ~p"<%= schema.api_route_prefix %><%= scope_conn_route_prefix %><%= schema.route_prefix %>/#{<%= schema.singular %>}")
19 | |> render(:show, <%= schema.singular %>: <%= schema.singular %>)
20 | end
21 | end
22 |
23 | def show(conn, %{"<%= primary_key %>" => <%= primary_key %>}) do
24 | <%= schema.singular %> = <%= inspect context.alias %>.get_<%= schema.singular %>!(<%= context_scope_prefix %><%= primary_key %>)
25 | render(conn, :show, <%= schema.singular %>: <%= schema.singular %>)
26 | end
27 |
28 | def update(conn, %{"<%= primary_key %>" => <%= primary_key %>, <%= inspect schema.singular %> => <%= schema.singular %>_params}) do
29 | <%= schema.singular %> = <%= inspect context.alias %>.get_<%= schema.singular %>!(<%= context_scope_prefix %><%= primary_key %>)
30 |
31 | with {:ok, %<%= inspect schema.alias %>{} = <%= schema.singular %>} <- <%= inspect context.alias %>.update_<%= schema.singular %>(<%= context_scope_prefix %><%= schema.singular %>, <%= schema.singular %>_params) do
32 | render(conn, :show, <%= schema.singular %>: <%= schema.singular %>)
33 | end
34 | end
35 |
36 | def delete(conn, %{"<%= primary_key %>" => <%= primary_key %>}) do
37 | <%= schema.singular %> = <%= inspect context.alias %>.get_<%= schema.singular %>!(<%= context_scope_prefix %><%= primary_key %>)
38 |
39 | with {:ok, %<%= inspect schema.alias %>{}} <- <%= inspect context.alias %>.delete_<%= schema.singular %>(<%= context_scope_prefix %><%= schema.singular %>) do
40 | send_resp(conn, :no_content, "")
41 | end
42 | end
43 | end
44 |
--------------------------------------------------------------------------------
/priv/templates/phx.gen.json/fallback_controller.ex:
--------------------------------------------------------------------------------
1 | defmodule <%= inspect context.web_module %>.FallbackController do
2 | @moduledoc """
3 | Translates controller action results into valid `Plug.Conn` responses.
4 |
5 | See `Phoenix.Controller.action_fallback/1` for more details.
6 | """
7 | use <%= inspect context.web_module %>, :controller
8 |
9 | <%= if schema.generate? do %># This clause handles errors returned by Ecto's insert/update/delete.
10 | def call(conn, {:error, %Ecto.Changeset{} = changeset}) do
11 | conn
12 | |> put_status(:unprocessable_entity)
13 | |> put_view(json: <%= inspect context.web_module %>.ChangesetJSON)
14 | |> render(:error, changeset: changeset)
15 | end
16 |
17 | <% end %># This clause is an example of how to handle resources that cannot be found.
18 | def call(conn, {:error, :not_found}) do
19 | conn
20 | |> put_status(:not_found)
21 | |> put_view(html: <%= inspect context.web_module %>.ErrorHTML, json: <%= inspect context.web_module %>.ErrorJSON)
22 | |> render(:"404")
23 | end
24 | end
25 |
--------------------------------------------------------------------------------
/priv/templates/phx.gen.json/json.ex:
--------------------------------------------------------------------------------
1 | defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web_namespace, schema.alias) %>JSON do
2 | alias <%= inspect schema.module %>
3 |
4 | @doc """
5 | Renders a list of <%= schema.plural %>.
6 | """
7 | def index(%{<%= schema.plural %>: <%= schema.plural %>}) do
8 | %{data: for(<%= schema.singular %> <- <%= schema.plural %>, do: data(<%= schema.singular %>))}
9 | end
10 |
11 | @doc """
12 | Renders a single <%= schema.singular %>.
13 | """
14 | def show(%{<%= schema.singular %>: <%= schema.singular %>}) do
15 | %{data: data(<%= schema.singular %>)}
16 | end
17 |
18 | defp data(%<%= inspect schema.alias %>{} = <%= schema.singular %>) do
19 | %{
20 | <%= [{primary_key, :id} | schema.attrs] |> Enum.map(fn {k, _} -> " #{k}: #{schema.singular}.#{k}" end) |> Enum.join(",\n") %>
21 | }
22 | end
23 | end
24 |
--------------------------------------------------------------------------------
/priv/templates/phx.gen.notifier/notifier.ex:
--------------------------------------------------------------------------------
1 | defmodule <%= inspect context.module %> do
2 | import Swoosh.Email
3 | alias <%= inspect context.base_module %>.Mailer<%= for message <- notifier_messages do %>
4 |
5 | def deliver_<%= message %>(%{name: name, email: email}) do
6 | new()
7 | |> to({name, email})
8 | |> from({"Phoenix Team", "team@example.com"})
9 | |> subject("Welcome to Phoenix, #{name}!")
10 | |> html_body("Hello, #{name} ")
11 | |> text_body("Hello, #{name}\n")
12 | |> Mailer.deliver()
13 | end<% end %>
14 | end
15 |
--------------------------------------------------------------------------------
/priv/templates/phx.gen.notifier/notifier_test.exs:
--------------------------------------------------------------------------------
1 | defmodule <%= inspect context.module %>Test do
2 | use ExUnit.Case, async: true
3 | import Swoosh.TestAssertions
4 |
5 | alias <%= inspect context.module %><%= for message <- notifier_messages do %>
6 |
7 | test "deliver_<%= message %>/1" do
8 | user = %{name: "Alice", email: "alice@example.com"}
9 |
10 | <%= inflections[:alias] %>.deliver_<%= message %>(user)
11 |
12 | assert_email_sent(
13 | subject: "Welcome to Phoenix, Alice!",
14 | to: {"Alice", "alice@example.com"},
15 | text_body: ~r/Hello, Alice/
16 | )
17 | end<% end %>
18 | end
19 |
--------------------------------------------------------------------------------
/priv/templates/phx.gen.presence/presence.ex:
--------------------------------------------------------------------------------
1 | defmodule <%= module %> do
2 | @moduledoc """
3 | Provides presence tracking to channels and processes.
4 |
5 | See the [`Phoenix.Presence`](https://hexdocs.pm/phoenix/Phoenix.Presence.html)
6 | docs for more details.
7 | """
8 | use Phoenix.Presence,
9 | otp_app: <%= inspect otp_app %>,
10 | pubsub_server: <%= inspect pubsub_server %>
11 | end
12 |
--------------------------------------------------------------------------------
/priv/templates/phx.gen.release/dockerignore.eex:
--------------------------------------------------------------------------------
1 | # This file excludes paths from the Docker build context.
2 | #
3 | # By default, Docker's build context includes all files (and folders) in the
4 | # current directory. Even if a file isn't copied into the container it is still sent to
5 | # the Docker daemon.
6 | #
7 | # There are multiple reasons to exclude files from the build context:
8 | #
9 | # 1. Prevent nested folders from being copied into the container (ex: exclude
10 | # /assets/node_modules when copying /assets)
11 | # 2. Reduce the size of the build context and improve build time (ex. /build, /deps, /doc)
12 | # 3. Avoid sending files containing sensitive information
13 | #
14 | # More information on using .dockerignore is available here:
15 | # https://docs.docker.com/engine/reference/builder/#dockerignore-file
16 |
17 | .dockerignore
18 |
19 | # Ignore git, but keep git HEAD and refs to access current commit hash if needed:
20 | #
21 | # $ cat .git/HEAD | awk '{print ".git/"$2}' | xargs cat
22 | # d0b8727759e1e0e7aa3d41707d12376e373d5ecc
23 | .git
24 | !.git/HEAD
25 | !.git/refs
26 |
27 | # Common development/test artifacts
28 | /cover/
29 | /doc/
30 | /test/
31 | /tmp/
32 | .elixir_ls
33 |
34 | # Mix artifacts
35 | /_build/
36 | /deps/
37 | *.ez
38 |
39 | # Generated on crash by the VM
40 | erl_crash.dump
41 |
42 | # Static artifacts - These should be fetched and built inside the Docker image
43 | /assets/node_modules/
44 | /priv/static/assets/
45 | /priv/static/cache_manifest.json
46 |
--------------------------------------------------------------------------------
/priv/templates/phx.gen.release/rel/migrate.bat.eex:
--------------------------------------------------------------------------------
1 | call "%~dp0\<%= otp_app %>" eval <%= app_namespace %>.Release.migrate
2 |
--------------------------------------------------------------------------------
/priv/templates/phx.gen.release/rel/migrate.sh.eex:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | set -eu
3 |
4 | cd -P -- "$(dirname -- "$0")"
5 | exec ./<%= otp_app %> eval <%= app_namespace %>.Release.migrate
6 |
--------------------------------------------------------------------------------
/priv/templates/phx.gen.release/rel/server.bat.eex:
--------------------------------------------------------------------------------
1 | set PHX_SERVER=true
2 | call "%~dp0\<%= otp_app %>" start
3 |
--------------------------------------------------------------------------------
/priv/templates/phx.gen.release/rel/server.sh.eex:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | set -eu
3 |
4 | cd -P -- "$(dirname -- "$0")"
5 | PHX_SERVER=true exec ./<%= otp_app %> start
6 |
--------------------------------------------------------------------------------
/priv/templates/phx.gen.release/release.ex:
--------------------------------------------------------------------------------
1 | defmodule <%= app_namespace %>.Release do
2 | @moduledoc """
3 | Used for executing DB release tasks when run in production without Mix
4 | installed.
5 | """
6 | @app :<%= otp_app %>
7 |
8 | def migrate do
9 | load_app()
10 |
11 | for repo <- repos() do
12 | {:ok, _, _} = Ecto.Migrator.with_repo(repo, &Ecto.Migrator.run(&1, :up, all: true))
13 | end
14 | end
15 |
16 | def rollback(repo, version) do
17 | load_app()
18 | {:ok, _, _} = Ecto.Migrator.with_repo(repo, &Ecto.Migrator.run(&1, :down, to: version))
19 | end
20 |
21 | defp repos do
22 | Application.fetch_env!(@app, :ecto_repos)
23 | end
24 |
25 | defp load_app do
26 | # Many platforms require SSL when connecting to the database
27 | Application.ensure_all_started(:ssl)
28 | Application.ensure_loaded(@app)
29 | end
30 | end
31 |
--------------------------------------------------------------------------------
/priv/templates/phx.gen.schema/migration.exs:
--------------------------------------------------------------------------------
1 | defmodule <%= inspect schema.repo %>.Migrations.Create<%= Macro.camelize(schema.table) %> do
2 | use <%= inspect schema.migration_module %>
3 |
4 | def change do
5 | create table(:<%= schema.table %><%= if schema.binary_id || schema.opts[:primary_key] do %>, primary_key: false<% end %><%= if schema.prefix do %>, prefix: :<%= schema.prefix %><% end %>) do
6 | <%= if schema.binary_id do %> add :<%= primary_key %>, :binary_id, primary_key: true
7 | <% else %><%= if schema.opts[:primary_key] do %> add :<%= schema.opts[:primary_key] %>, :id, primary_key: true
8 | <% end %><% end %><%= for {k, v} <- schema.attrs do %> add <%= inspect k %>, <%= inspect Mix.Phoenix.Schema.type_for_migration(v) %><%= schema.migration_defaults[k] %>
9 | <% end %><%= for {_, i, _, s} <- schema.assocs do %> add <%= inspect(i) %>, references(<%= inspect(s) %>, on_delete: :nothing<%= if schema.binary_id do %>, type: :binary_id<% end %>)
10 | <% end %><%= if scope do %> add :<%= scope.schema_key %>, <%= if scope.schema_table do %>references(:<%= scope.schema_table %>, type: <%= inspect scope.schema_migration_type %>, on_delete: :delete_all)<% else %><%= inspect scope.schema_migration_type %><% end %>
11 | <% end %>
12 | timestamps(<%= if schema.timestamp_type != :naive_datetime, do: "type: #{inspect schema.timestamp_type}" %>)
13 | end<%= if scope do %>
14 |
15 | create index(:<%= schema.table %>, [:<%= scope.schema_key %>])<% end %>
16 | <%= if Enum.any?(schema.indexes) do %><%= for index <- schema.indexes do %>
17 | <%= index %><% end %>
18 | <% end %> end
19 | end
20 |
--------------------------------------------------------------------------------
/priv/templates/phx.gen.schema/schema.ex:
--------------------------------------------------------------------------------
1 | defmodule <%= inspect schema.module %> do
2 | use Ecto.Schema
3 | import Ecto.Changeset
4 | <%= if schema.prefix do %>
5 | @schema_prefix :<%= schema.prefix %><% end %><%= if schema.opts[:primary_key] do %>
6 | @derive {Phoenix.Param, key: :<%= schema.opts[:primary_key] %>}<% end %><%= if schema.binary_id do %>
7 | @primary_key {:<%= primary_key %>, :binary_id, autogenerate: true}
8 | @foreign_key_type :binary_id<% else %><%= if schema.opts[:primary_key] do %>
9 | @primary_key {:<%= schema.opts[:primary_key] %>, :id, autogenerate: true}<% end %><% end %>
10 | schema <%= inspect schema.table %> do
11 | <%= Mix.Phoenix.Schema.format_fields_for_schema(schema) %>
12 | <%= for {_, k, _, _} <- schema.assocs do %> field <%= inspect k %>, <%= if schema.binary_id do %>:binary_id<% else %>:id<% end %>
13 | <% end %><%= if scope do %> field :<%= scope.schema_key %>, <%= inspect scope.schema_type %>
14 | <% end %>
15 | timestamps(<%= if schema.timestamp_type != :naive_datetime, do: "type: #{inspect schema.timestamp_type}" %>)
16 | end
17 |
18 | @doc false
19 | def changeset(<%= schema.singular %>, attrs<%= if scope do %>, <%= scope.name %>_scope<% end %>) do
20 | <%= schema.singular %>
21 | |> cast(attrs, [<%= Enum.map_join(schema.attrs, ", ", &inspect(elem(&1, 0))) %>])
22 | |> validate_required([<%= Enum.map_join(Mix.Phoenix.Schema.required_fields(schema), ", ", &inspect(elem(&1, 0))) %>])
23 | <%= for k <- schema.uniques do %> |> unique_constraint(<%= inspect k %>)
24 | <% end %><%= if scope do %> |> put_change(:<%= scope.schema_key %>, <%= scope.name %>_scope.<%= Enum.join(scope.access_path, ".") %>)
25 | <% end %> end
26 | end
27 |
--------------------------------------------------------------------------------
/priv/templates/phx.gen.socket/socket.ex:
--------------------------------------------------------------------------------
1 | defmodule <%= module %>Socket do
2 | use Phoenix.Socket
3 |
4 | # A Socket handler
5 | #
6 | # It's possible to control the websocket connection and
7 | # assign values that can be accessed by your channel topics.
8 |
9 | ## Channels<%= if existing_channel do %>
10 |
11 | channel "<%= existing_channel[:singular] %>:*", <%= existing_channel[:module] %>Channel
12 | <% else %>
13 | # Uncomment the following line to define a "room:*" topic
14 | # pointing to the `<%= web_module %>.RoomChannel`:
15 | #
16 | # channel "room:*", <%= web_module %>.RoomChannel
17 | #
18 | # To create a channel file, use the mix task:
19 | #
20 | # mix phx.gen.channel Room
21 | #
22 | # See the [`Channels guide`](https://hexdocs.pm/phoenix/channels.html)
23 | # for further details.
24 |
25 | <% end %>
26 | # Socket params are passed from the client and can
27 | # be used to verify and authenticate a user. After
28 | # verification, you can put default assigns into
29 | # the socket that will be set for all channels, ie
30 | #
31 | # {:ok, assign(socket, :user_id, verified_user_id)}
32 | #
33 | # To deny connection, return `:error` or `{:error, term}`. To control the
34 | # response the client receives in that case, [define an error handler in the
35 | # websocket
36 | # configuration](https://hexdocs.pm/phoenix/Phoenix.Endpoint.html#socket/3-websocket-configuration).
37 | #
38 | # See `Phoenix.Token` documentation for examples in
39 | # performing token verification on connect.
40 | @impl true
41 | def connect(_params, socket, _connect_info) do
42 | {:ok, socket}
43 | end
44 |
45 | # Socket IDs are topics that allow you to identify all sockets for a given user:
46 | #
47 | # def id(socket), do: "user_socket:#{socket.assigns.user_id}"
48 | #
49 | # Would allow you to broadcast a "disconnect" event and terminate
50 | # all active sockets and channels for a given user:
51 | #
52 | # <%= endpoint_module %>.broadcast("user_socket:#{user.id}", "disconnect", %{})
53 | #
54 | # Returning `nil` makes this socket anonymous.
55 | @impl true
56 | def id(_socket), do: nil
57 | end
58 |
--------------------------------------------------------------------------------
/priv/templates/phx.gen.socket/socket.js:
--------------------------------------------------------------------------------
1 | // NOTE: The contents of this file will only be executed if
2 | // you uncomment its entry in "assets/js/app.js".
3 |
4 | // Bring in Phoenix channels client library:
5 | import {Socket} from "phoenix"
6 |
7 | // And connect to the path in "<%= web_prefix %>/endpoint.ex". We pass the
8 | // token for authentication.
9 | //
10 | // Read the [`Using Token Authentication`](https://hexdocs.pm/phoenix/channels.html#using-token-authentication)
11 | // section to see how the token should be used.
12 | let socket = new Socket("/socket", {authToken: window.userToken})
13 | socket.connect()
14 |
15 | // Now that you are connected, you can join channels with a topic.
16 | // Let's assume you have a channel with a topic named `room` and the
17 | // subtopic is its id - in this case 42:
18 | let channel = socket.channel("room:42", {})
19 | channel.join()
20 | .receive("ok", resp => { console.log("Joined successfully", resp) })
21 | .receive("error", resp => { console.log("Unable to join", resp) })
22 |
23 | export default socket
24 |
--------------------------------------------------------------------------------
/test/fixtures/digest/cleaner/cache_manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "digests": {
3 | "app-1.css": {
4 | "logical_path": "app.css",
5 | "mtime": 32132171,
6 | "size": 369053,
7 | "digest": "1"
8 | },
9 | "app-2.css": {
10 | "logical_path": "app.css",
11 | "mtime": 32132172,
12 | "size": 369053,
13 | "digest": "2"
14 | },
15 | "app-3.css": {
16 | "logical_path": "app.css",
17 | "mtime": 32132173,
18 | "size": 369053,
19 | "digest": "3"
20 | }
21 | },
22 | "latest": {
23 | "app.css": "app-3.css"
24 | },
25 | "version": 1
26 | }
27 |
--------------------------------------------------------------------------------
/test/fixtures/digest/cleaner/latest_not_most_recent_cache_manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "digests": {
3 | "app-1.css": {
4 | "logical_path": "app.css",
5 | "mtime": 32132171,
6 | "size": 369053,
7 | "digest": "1"
8 | },
9 | "app-2.css": {
10 | "logical_path": "app.css",
11 | "mtime": 32132172,
12 | "size": 369053,
13 | "digest": "2"
14 | },
15 | "app-3.css": {
16 | "logical_path": "app.css",
17 | "mtime": 32132170,
18 | "size": 369053,
19 | "digest": "3"
20 | }
21 | },
22 | "latest": {
23 | "app.css": "app-3.css"
24 | },
25 | "version": 1
26 | }
27 |
--------------------------------------------------------------------------------
/test/fixtures/digest/compile/cache_manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "digests": {
3 | "foo-d978852bea6530fcd197b5445ed008fd.css": {
4 | "logical_path": "foo.css",
5 | "mtime": 32132171,
6 | "size": 369053,
7 | "digest": "d978852bea6530fcd197b5445ed008fd"
8 | }
9 | },
10 | "latest": {
11 | "foo.css": "foo-d978852bea6530fcd197b5445ed008fd.css"
12 | },
13 | "version": 1
14 | }
15 |
--------------------------------------------------------------------------------
/test/fixtures/digest/compile/cache_manifest_upgrade.json:
--------------------------------------------------------------------------------
1 | {
2 | "digests": {
3 | "foo-abcdef.css": {
4 | "logical_path": "foo.css",
5 | "mtime": 32132171,
6 | "size": 369053,
7 | "digest": "abcdev"
8 | },
9 | "foo-ghijkl.css": {
10 | "logical_path": "foo.css",
11 | "mtime": 32193492,
12 | "size": 372059,
13 | "digest": "abcdev"
14 | }
15 | },
16 | "latest": {
17 | "foo.css": "foo-ghijkl.css"
18 | },
19 | "version": 1
20 | }
21 |
--------------------------------------------------------------------------------
/test/fixtures/digest/priv/output/foo-288ea8c7954498e65663c817382eeac4.css:
--------------------------------------------------------------------------------
1 | .foo { background-color: blue }
2 |
--------------------------------------------------------------------------------
/test/fixtures/digest/priv/output/foo-d978852bea6530fcd197b5445ed008fd.css:
--------------------------------------------------------------------------------
1 | .foo { background-color: red }
2 |
--------------------------------------------------------------------------------
/test/fixtures/digest/priv/static/app.js:
--------------------------------------------------------------------------------
1 | (function() {
2 | console.log('Hello World!');
3 | })();
4 | //# sourceMappingURL=app.js.map
5 |
--------------------------------------------------------------------------------
/test/fixtures/digest/priv/static/app.js.map:
--------------------------------------------------------------------------------
1 | {"version":3,"file":"app.js","sources":["app.js"],"names":["console","log"],"mappings":"CAAA,WACEA,QAAQC,IAAI"}
2 |
--------------------------------------------------------------------------------
/test/fixtures/digest/priv/static/css/app.css:
--------------------------------------------------------------------------------
1 | .absolute_path_logo {
2 | background-image: url("/phoenix.png");
3 | }
4 |
5 | .relative_path_logo {
6 | background-image: url('../images/relative.png');
7 | }
8 |
9 | .absolute_url_logo {
10 | background-image: url(http://www.phoenixframework.org/absolute.png);
11 | }
12 |
13 | .absolute_path_logo{background-image:url(/phoenix.png)}
14 | .relative_path_logo{background-image:url(../images/relative.png)}
15 | .absolute_url_logo{background-image:url(http://www.phoenixframework.org/absolute.png)}
16 |
--------------------------------------------------------------------------------
/test/fixtures/digest/priv/static/foo.css:
--------------------------------------------------------------------------------
1 | .foo { background-color: red }
2 |
--------------------------------------------------------------------------------
/test/fixtures/digest/priv/static/images/relative.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/phoenixframework/phoenix/45e29e47de26d644725a4c0a1c0a46cc46f777e1/test/fixtures/digest/priv/static/images/relative.png
--------------------------------------------------------------------------------
/test/fixtures/digest/priv/static/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "MyPhoenixApp",
3 | "short_name": "MyPhoenixApp",
4 | "start_url": ".",
5 | "display": "standalone",
6 | "background_color": "#F67938",
7 | "description": "A simple Phoenix app.",
8 | "icons": [{
9 | "src": "images/touch/homescreen48.png",
10 | "sizes": "48x48",
11 | "type": "image/png"
12 | }, {
13 | "src": "images/touch/homescreen72.png",
14 | "sizes": "72x72",
15 | "type": "image/png"
16 | }, {
17 | "src": "images/touch/homescreen96.png",
18 | "sizes": "96x96",
19 | "type": "image/png"
20 | }, {
21 | "src": "images/touch/homescreen144.png",
22 | "sizes": "144x144",
23 | "type": "image/png"
24 | }, {
25 | "src": "images/touch/homescreen168.png",
26 | "sizes": "168x168",
27 | "type": "image/png"
28 | }, {
29 | "src": "images/touch/homescreen192.png",
30 | "sizes": "192x192",
31 | "type": "image/png"
32 | }]
33 | }
34 |
--------------------------------------------------------------------------------
/test/fixtures/digest/priv/static/phoenix.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/phoenixframework/phoenix/45e29e47de26d644725a4c0a1c0a46cc46f777e1/test/fixtures/digest/priv/static/phoenix.png
--------------------------------------------------------------------------------
/test/fixtures/digest/priv/static/precompressed.js.br:
--------------------------------------------------------------------------------
1 | Brotli
2 |
--------------------------------------------------------------------------------
/test/fixtures/digest/priv/static/precompressed.js.gz:
--------------------------------------------------------------------------------
1 | gzip
2 |
--------------------------------------------------------------------------------
/test/fixtures/hello.txt:
--------------------------------------------------------------------------------
1 | world
--------------------------------------------------------------------------------
/test/fixtures/ssl/cert.pem:
--------------------------------------------------------------------------------
1 | -----BEGIN CERTIFICATE-----
2 | MIIEwTCCA6mgAwIBAgIJAJmNQmFFDZgVMA0GCSqGSIb3DQEBBQUAMIGbMQswCQYD
3 | VQQGEwJVUzENMAsGA1UECBMET2hpbzEPMA0GA1UEBxMGRGF5dG9uMRowGAYDVQQK
4 | ExFQaG9lbml4IEZyYW1ld29yazEaMBgGA1UECxMRU3lzdGVtIEFyY2hpdGVjdHMx
5 | EjAQBgNVBAMTCWxvY2FsaG9zdDEgMB4GCSqGSIb3DQEJARYRcGhvZW5peEBsb2Nh
6 | bGhvc3QwHhcNMTQwNTI1MDAxNTM5WhcNMTUwNTI1MDAxNTM5WjCBmzELMAkGA1UE
7 | BhMCVVMxDTALBgNVBAgTBE9oaW8xDzANBgNVBAcTBkRheXRvbjEaMBgGA1UEChMR
8 | UGhvZW5peCBGcmFtZXdvcmsxGjAYBgNVBAsTEVN5c3RlbSBBcmNoaXRlY3RzMRIw
9 | EAYDVQQDEwlsb2NhbGhvc3QxIDAeBgkqhkiG9w0BCQEWEXBob2VuaXhAbG9jYWxo
10 | b3N0MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA0g3Mlh7ZRYiE+btN
11 | bePpRGzdj4NIyleZj5LXxRe8xZs/WHb4j1+UfUxRrAO141UYcE1ZXkpg8x64uUiR
12 | m281JPvHGe8CqZlckZQGVF77Bv8oH6gGTxyrI7Gdni2CJgMqR9A4Kwdlczk7d+fV
13 | vQCl8B5izEmCTXWu8RG/QU1BHQLygT/DSHgeGjzQaaE0OoD/Hxa/zkVPEugjNtxF
14 | YVWGQ0gzqsTPFz1CkIp1+CJHzm/fhFtFaS0aDeFyMW1BRC8CTYXyYdcM9cSn/ahh
15 | gmKhYKY3EbRaiE4dfH2rqzvixc+OOEQVpMPDCVDG0ms/FhCrLzVBoY4/WiZVv6aq
16 | 2jznmwIDAQABo4IBBDCCAQAwHQYDVR0OBBYEFPQA6slwgMEFuEbYd+lG4WICZb1u
17 | MIHQBgNVHSMEgcgwgcWAFPQA6slwgMEFuEbYd+lG4WICZb1uoYGhpIGeMIGbMQsw
18 | CQYDVQQGEwJVUzENMAsGA1UECBMET2hpbzEPMA0GA1UEBxMGRGF5dG9uMRowGAYD
19 | VQQKExFQaG9lbml4IEZyYW1ld29yazEaMBgGA1UECxMRU3lzdGVtIEFyY2hpdGVj
20 | dHMxEjAQBgNVBAMTCWxvY2FsaG9zdDEgMB4GCSqGSIb3DQEJARYRcGhvZW5peEBs
21 | b2NhbGhvc3SCCQCZjUJhRQ2YFTAMBgNVHRMEBTADAQH/MA0GCSqGSIb3DQEBBQUA
22 | A4IBAQAZkmKd/KPN3FXsgYU+2iNKHAdzByEugGbTSdba5WBAwwJCumhGUy8CFXY6
23 | MhfrQjj7mkkU4UFHkHXZCWbucfTGxWFaizkkaStsY+5K6sKVFQZw8kc5llDD4dyG
24 | JNGX99VYtVvJG/GS/rXiOTHJvR4WYbgJ5DPYI+PVWBVIp5wG9T2G8TinTnguFGC6
25 | /EFmYf06XGUAKWsMfzNm8Dm7fkQ94W27FVtJ9RvdeObi2aJt37bE9DVqUkjZ1qtz
26 | fz3uJ5UyoNFn5tCBOdEooivPlvl4wqSTppIpflMNlE82KhSRnESnZF9JQUGW3Lnd
27 | 2H3UOAYVUDhGVP/J9ZJLTcr8O8zA
28 | -----END CERTIFICATE-----
29 |
--------------------------------------------------------------------------------
/test/fixtures/ssl/key.pem:
--------------------------------------------------------------------------------
1 | -----BEGIN RSA PRIVATE KEY-----
2 | MIIEowIBAAKCAQEA0g3Mlh7ZRYiE+btNbePpRGzdj4NIyleZj5LXxRe8xZs/WHb4
3 | j1+UfUxRrAO141UYcE1ZXkpg8x64uUiRm281JPvHGe8CqZlckZQGVF77Bv8oH6gG
4 | TxyrI7Gdni2CJgMqR9A4Kwdlczk7d+fVvQCl8B5izEmCTXWu8RG/QU1BHQLygT/D
5 | SHgeGjzQaaE0OoD/Hxa/zkVPEugjNtxFYVWGQ0gzqsTPFz1CkIp1+CJHzm/fhFtF
6 | aS0aDeFyMW1BRC8CTYXyYdcM9cSn/ahhgmKhYKY3EbRaiE4dfH2rqzvixc+OOEQV
7 | pMPDCVDG0ms/FhCrLzVBoY4/WiZVv6aq2jznmwIDAQABAoIBAFhR/QfSCME32dG3
8 | c6MVBWwD6lUBeoW5t5OqxpbUmEbuNABaZcDDC4hzopOVK9FeYlw16bG/zGvtKvad
9 | ELwuUkYup1S8Ln5pQYbkmpS3Kw2SE6jb2WtCPqNPd1qe/+5Dvm9bmYJeJcYA9oRA
10 | Mpq5vwvretcywVsYdGpgb+5hMVOkunw25I4eNeqZHX1ZKmcgq4I/UcZzoF2qPUC6
11 | jtU4reZn8yTgki0YRJVKvw2QLKTegVbK0JHhHDQBxHyfaoj+gd1abZHHMcG0k82k
12 | Hr86xOJYVGcmZeKk9P/VIPCASgfAbIR23lFkyTxH+GGcrUQvn/kvruKvpQqXgDRo
13 | pz+0IUECgYEA79u2t6SNjZf9z01ZZdWvI0q4q5/v/uuIx05NCbpugJh9XVCoJ88T
14 | jLf7Ul8EKFj9i4CG9HlrtFkIwgKoJGiiIOdUD0vNwSgFEwht8KnbxHASsqo8UU1x
15 | bjgKBPKz3c2hJu4iXQikjHD+wAl2m7X+K0Z62gcY2IKdFI67RpsxwWMCgYEA4DCd
16 | cIWtznK40PboyeUIYtgrNKSaS1kbQOb2nj3HhJ3X3cbFvk5Yl28ac4cjmidXvN7Q
17 | 3bthZFI0ItagLs1raMbiX4kKo2ISeS2jkJZK2PQFWh7Brc4Q+RktTIRFoHyP35uO
18 | dvHJH+rEz0oURbjqSlDbB/eFysiOK8mRUTd38mkCgYARWu5/nzJ22laNF2WujqWb
19 | gh6WnH37DgPZl/rPB2RTfbUkeV+RcdRSTEWtEh705GuEGoqpSdfXNtIBZ7vO1ptU
20 | kihs6uk6XrDvTZ7W2RODxTA1KUgwAdCBTyC6du040VYlwPlPjf6KAusL7iNc5PA9
21 | JV5iRD0x/VFsWV+HnlcdTQKBgQDPn3ZPPR4n8csDi4cvYzMPB4+L410Zpt48jyma
22 | hzB9uwit1WZQxpH5POXMVD0+iG0S92+Lyft6Qz8RfJ9AePGeSYJgY7Q8d5kQLJos
23 | T2Pl5KgIPC+2XP8PEqgHEwDAjltYBOI9edKAApZeOwbnQ0eHp7YRfMSldnNkTfqM
24 | ssgc8QKBgDflG6bfSrjOjgyppsII4tXKdpnRa/UwN98D3/ZU5VJA8jR8yzHuMico
25 | bpeooh03uH8xLalXlXNRAamSv03KprvXRXeA+GeAeoseB1qHJ7Z2pkkqiHFG2XxY
26 | u0jAsKamaT6YjcLqloxOdPSyDoXon6pKxvSsiUg1l9ZASexuTiZV
27 | -----END RSA PRIVATE KEY-----
28 |
--------------------------------------------------------------------------------
/test/fixtures/templates/custom.foo:
--------------------------------------------------------------------------------
1 | from foo
--------------------------------------------------------------------------------
/test/fixtures/templates/layout/app.html.eex:
--------------------------------------------------------------------------------
1 |
2 | <%= @title || default_title() %>
3 | <%= @inner_content %>
4 |
5 |
--------------------------------------------------------------------------------
/test/fixtures/templates/layout/root.html.eex:
--------------------------------------------------------------------------------
1 | ROOTSTART[<%= @title %>]<%= @inner_content %>ROOTEND
2 |
--------------------------------------------------------------------------------
/test/fixtures/templates/no_trim.text.eex:
--------------------------------------------------------------------------------
1 | <%= 123 %>
2 | <%= 456 %>
3 | <%= 789 %>
4 |
--------------------------------------------------------------------------------
/test/fixtures/templates/path.html.eex:
--------------------------------------------------------------------------------
1 | path
2 |
--------------------------------------------------------------------------------
/test/fixtures/templates/safe.html.eex:
--------------------------------------------------------------------------------
1 | Raw <%= {:safe, @message} %>
2 |
--------------------------------------------------------------------------------
/test/fixtures/templates/show.html.eex:
--------------------------------------------------------------------------------
1 | Show! <%= @message %>
2 | <% :ok # Template is still valid %>
3 |
--------------------------------------------------------------------------------
/test/fixtures/templates/trim.html.eex:
--------------------------------------------------------------------------------
1 | <%= 123 %>
2 | <%= 456 %>
3 | <%= 789 %>
4 |
--------------------------------------------------------------------------------
/test/fixtures/templates/user/index.html.eex:
--------------------------------------------------------------------------------
1 | <%= escaped_title @title %>
2 |
--------------------------------------------------------------------------------
/test/fixtures/templates/user/profiles/admin.html.eex:
--------------------------------------------------------------------------------
1 | admin profile
2 |
--------------------------------------------------------------------------------
/test/fixtures/templates/user/render_template.html.eex:
--------------------------------------------------------------------------------
1 | rendered template for <%= @name %>
2 |
--------------------------------------------------------------------------------
/test/fixtures/templates/user/show.json.exs:
--------------------------------------------------------------------------------
1 | %{foo: "bar"}
2 |
--------------------------------------------------------------------------------
/test/fixtures/views.exs:
--------------------------------------------------------------------------------
1 | defmodule MyApp.View do
2 | use Phoenix.View, root: "test/fixtures/templates"
3 |
4 | def escaped_title(title) do
5 | {:safe, Plug.HTML.html_escape(title)}
6 | end
7 | end
8 |
9 | defmodule MyApp.LayoutView do
10 | use Phoenix.View, root: "test/fixtures/templates"
11 |
12 | def default_title do
13 | "MyApp"
14 | end
15 | end
16 |
17 | defmodule MyApp.User do
18 | defstruct name: "name"
19 | end
20 |
21 | defmodule MyApp.PathView do
22 | use Phoenix.View, root: "test/fixtures/templates", path: ""
23 | end
24 |
25 | defmodule MyApp.UserView do
26 | use Phoenix.View, root: "test/fixtures/templates", pattern: "**/*"
27 |
28 | import Phoenix.Controller, only: [view_module: 1, view_template: 1]
29 |
30 | def escaped_title(title) do
31 | {:safe, Plug.HTML.html_escape(title)}
32 | end
33 |
34 | def render("message.html", _assigns) do
35 | send(self(), :message_sent)
36 | "message sent"
37 | end
38 |
39 | def render("show.text", %{user: user, prefix: prefix}) do
40 | "show user: " <> prefix <> user.name
41 | end
42 |
43 | def render("show.text", %{user: user}) do
44 | "show user: " <> user.name
45 | end
46 |
47 | def render("data.text", %{data: data}) do
48 | "show data: " <> data.name
49 | end
50 |
51 | def render("edit.html", %{} = assigns) do
52 | "EDIT#{assigns[:layout]} - #{assigns[:title]}"
53 | end
54 |
55 | def render("existing.html", _), do: "rendered existing"
56 |
57 | def render("inner.html", assigns) do
58 | """
59 | View module is #{view_module(assigns.conn)} and view template is #{view_template(assigns.conn)}
60 | """
61 | end
62 |
63 | def render("render_template.html" = tpl, %{name: name}) do
64 | render_template(tpl, %{name: String.upcase(name)})
65 | end
66 |
67 | def render("to_iodata.html", %{to_iodata: to_iodata}) do
68 | to_iodata
69 | end
70 | end
71 |
72 | defmodule MyApp.Templates.UserView do
73 | use Phoenix.View, root: "test/fixtures"
74 |
75 | def escaped_title(title) do
76 | {:safe, Plug.HTML.html_escape(title)}
77 | end
78 | end
79 |
80 | defmodule MyApp.Nested.User do
81 | defstruct name: "nested name"
82 | end
83 |
84 | defmodule MyApp.Nested.UserView do
85 | use Phoenix.View, root: "test/fixtures/templates", namespace: MyApp.Nested
86 |
87 | def render("show.text", %{user: user}) do
88 | "show nested user: " <> user.name
89 | end
90 |
91 | def escaped_title(title) do
92 | {:safe, Plug.HTML.html_escape(title)}
93 | end
94 | end
95 |
--------------------------------------------------------------------------------
/test/mix/tasks/phx.digest.clean_test.exs:
--------------------------------------------------------------------------------
1 | defmodule Mix.Tasks.Phx.Digest.CleanTest do
2 | use ExUnit.Case
3 |
4 | test "fails when the given paths are invalid" do
5 | Mix.Tasks.Phx.Digest.Clean.run(["--output", "invalid_path", "--no-compile"])
6 |
7 | assert_received {:mix_shell, :error, ["The output path \"invalid_path\" does not exist"]}
8 | end
9 |
10 | test "removes old versions", config do
11 | output_path = Path.join("tmp", to_string(config.test))
12 | input_path = "priv/static"
13 |
14 | try do
15 | :ok = File.mkdir_p!(output_path)
16 |
17 | Mix.Tasks.Phx.Digest.Clean.run([input_path, "-o", output_path, "--no-compile"])
18 |
19 | msg = "Clean complete for \"#{output_path}\""
20 | assert_received {:mix_shell, :info, [^msg]}
21 | after
22 | File.rm_rf!(output_path)
23 | end
24 | end
25 | end
26 |
--------------------------------------------------------------------------------
/test/mix/tasks/phx.digest_test.exs:
--------------------------------------------------------------------------------
1 | Code.require_file "../../../installer/test/mix_helper.exs", __DIR__
2 |
3 | defmodule Mix.Tasks.Phx.DigestTest do
4 | use ExUnit.Case
5 | import MixHelper
6 |
7 | test "logs when the path is invalid" do
8 | Mix.Tasks.Phx.Digest.run(["invalid_path", "--no-compile"])
9 | assert_received {:mix_shell, :error, ["The input path \"invalid_path\" does not exist"]}
10 | end
11 |
12 | @output_path "mix_phoenix_digest"
13 | test "digests and compress files" do
14 | in_tmp @output_path, fn ->
15 | File.mkdir_p!("priv/static")
16 | Mix.Tasks.Phx.Digest.run(["priv/static", "-o", @output_path, "--no-compile"])
17 | assert_received {:mix_shell, :info, ["Check your digested files at \"mix_phoenix_digest\""]}
18 | end
19 | end
20 |
21 | @output_path "mix_phoenix_digest_no_input"
22 | test "digests and compress files without the input path" do
23 | in_tmp @output_path, fn ->
24 | File.mkdir_p!("priv/static")
25 | Mix.Tasks.Phx.Digest.run(["-o", @output_path, "--no-compile"])
26 | assert_received {:mix_shell, :info, ["Check your digested files at \"mix_phoenix_digest_no_input\""]}
27 | end
28 | end
29 |
30 | @input_path "input_path"
31 | test "uses the input path as output path when no output path is given" do
32 | in_tmp @input_path, fn ->
33 | File.mkdir_p!(@input_path)
34 | Mix.Tasks.Phx.Digest.run([@input_path, "--no-compile"])
35 | assert_received {:mix_shell, :info, ["Check your digested files at \"input_path\""]}
36 | end
37 | end
38 | end
39 |
--------------------------------------------------------------------------------
/test/mix/tasks/phx.gen.cert_test.exs:
--------------------------------------------------------------------------------
1 | Code.require_file("../../../installer/test/mix_helper.exs", __DIR__)
2 |
3 | defmodule Mix.Tasks.Phx.CertTest do
4 | use ExUnit.Case
5 |
6 | import MixHelper
7 | alias Mix.Tasks.Phx.Gen
8 |
9 | @timeout 5_000
10 |
11 | test "write certificate and key files" do
12 | in_tmp("mix_phx_gen_cert", fn ->
13 | Gen.Cert.run([])
14 |
15 | assert_received {:mix_shell, :info, ["* creating priv/cert/selfsigned_key.pem"]}
16 | assert_received {:mix_shell, :info, ["* creating priv/cert/selfsigned.pem"]}
17 |
18 | assert_file("priv/cert/selfsigned_key.pem", "-----BEGIN RSA PRIVATE KEY-----")
19 | assert_file("priv/cert/selfsigned.pem", "-----BEGIN CERTIFICATE-----")
20 | end)
21 | end
22 |
23 | test "write certificate and key with custom filename" do
24 | in_tmp("mix_phx_gen_cert", fn ->
25 | Gen.Cert.run(["-o", "priv/cert/localhost"])
26 |
27 | assert_received {:mix_shell, :info, ["* creating priv/cert/localhost_key.pem"]}
28 | assert_received {:mix_shell, :info, ["* creating priv/cert/localhost.pem"]}
29 |
30 | assert_file("priv/cert/localhost_key.pem", "-----BEGIN RSA PRIVATE KEY-----")
31 | assert_file("priv/cert/localhost.pem", "-----BEGIN CERTIFICATE-----")
32 | end)
33 | end
34 |
35 | test "TLS connection with generated certificate and key" do
36 | Application.ensure_all_started(:ssl)
37 |
38 | in_tmp("mix_phx_gen_cert", fn ->
39 | Gen.Cert.run([])
40 |
41 | assert {:ok, server} =
42 | :ssl.listen(
43 | 0,
44 | certfile: "priv/cert/selfsigned.pem",
45 | keyfile: "priv/cert/selfsigned_key.pem"
46 | )
47 |
48 | {:ok, {_, port}} = :ssl.sockname(server)
49 |
50 | spawn_link(fn ->
51 | with {:ok, conn} <- :ssl.transport_accept(server, @timeout),
52 | :ok <- :ssl.handshake(conn, @timeout) do
53 | :ssl.close(conn)
54 | end
55 | end)
56 |
57 | # We don't actually verify the server cert contents, we just check that
58 | # the client and server are able to complete the TLS handshake
59 | assert {:ok, client} = :ssl.connect(~c"localhost", port, [verify: :verify_none], @timeout)
60 | :ssl.close(client)
61 | :ssl.close(server)
62 | end)
63 | end
64 | end
65 |
--------------------------------------------------------------------------------
/test/mix/tasks/phx.gen.presence_test.exs:
--------------------------------------------------------------------------------
1 | Code.require_file "../../../installer/test/mix_helper.exs", __DIR__
2 |
3 | defmodule Mix.Tasks.Phx.Gen.PresenceTest do
4 | use ExUnit.Case
5 | import MixHelper
6 |
7 | setup do
8 | Mix.Task.clear()
9 | :ok
10 | end
11 |
12 | test "generates presence" do
13 | in_tmp_project "generates presence", fn ->
14 | Mix.Tasks.Phx.Gen.Presence.run(["MyPresence"])
15 |
16 | assert_file "lib/phoenix_web/channels/my_presence.ex", fn file ->
17 | assert file =~ ~S|defmodule PhoenixWeb.MyPresence do|
18 | assert file =~ ~S|use Phoenix.Presence|
19 | assert file =~ ~S|otp_app: :phoenix|
20 | assert file =~ ~S|pubsub_server: Phoenix.PubSub|
21 | end
22 | end
23 | end
24 |
25 | test "passing no args defaults to Presence" do
26 | in_tmp_project "generates presence", fn ->
27 | Mix.Tasks.Phx.Gen.Presence.run([])
28 |
29 | assert_file "lib/phoenix_web/channels/presence.ex", fn file ->
30 | assert file =~ ~S|defmodule PhoenixWeb.Presence do|
31 | assert file =~ ~S|use Phoenix.Presence|
32 | assert file =~ ~S|otp_app: :phoenix|
33 | assert file =~ ~S|pubsub_server: Phoenix.PubSub|
34 | end
35 | end
36 | end
37 |
38 | test "in an umbrella with a context_app, the file goes in lib/app/channels" do
39 | in_tmp_umbrella_project "generates presences", fn ->
40 | Application.put_env(:phoenix, :generators, context_app: {:another_app, "another_app"})
41 | Mix.Tasks.Phx.Gen.Presence.run([])
42 | assert_file "lib/phoenix/channels/presence.ex"
43 | end
44 | end
45 | end
46 |
--------------------------------------------------------------------------------
/test/mix/tasks/phx.gen.secret_test.exs:
--------------------------------------------------------------------------------
1 | Code.require_file "../../../installer/test/mix_helper.exs", __DIR__
2 |
3 | defmodule Mix.Tasks.Phx.Gen.SecretTest do
4 | use ExUnit.Case
5 | import Mix.Tasks.Phx.Gen.Secret
6 |
7 | test "generates a secret" do
8 | run []
9 | assert_receive {:mix_shell, :info, [secret]} when byte_size(secret) == 64
10 | assert String.printable?(secret)
11 | end
12 |
13 | test "generates a secret with custom length" do
14 | run ["32"]
15 | assert_receive {:mix_shell, :info, [secret]} when byte_size(secret) == 32
16 | assert String.printable?(secret)
17 | end
18 |
19 | test "raises on invalid args" do
20 | message = "mix phx.gen.secret expects a length as integer or no argument at all"
21 | assert_raise Mix.Error, message, fn -> run ["bad"] end
22 | assert_raise Mix.Error, message, fn -> run ["32bad"] end
23 | assert_raise Mix.Error, message, fn -> run ["32", "bad"] end
24 | end
25 |
26 | test "raises when length is too short" do
27 | message = "The secret should be at least 32 characters long"
28 | assert_raise Mix.Error, message, fn -> run ["0"] end
29 | assert_raise Mix.Error, message, fn -> run ["31"] end
30 | end
31 | end
32 |
--------------------------------------------------------------------------------
/test/mix/tasks/phx_test.exs:
--------------------------------------------------------------------------------
1 | defmodule Mix.Tasks.Phx.Test do
2 | use ExUnit.Case
3 |
4 | test "provide a list of available phx mix tasks" do
5 | Mix.Tasks.Phx.run []
6 | assert_received {:mix_shell, :info, ["mix phx.digest" <> _]}
7 | assert_received {:mix_shell, :info, ["mix phx.digest.clean" <> _]}
8 | assert_received {:mix_shell, :info, ["mix phx.gen.channel" <> _]}
9 | assert_received {:mix_shell, :info, ["mix phx.gen.cert" <> _]}
10 | assert_received {:mix_shell, :info, ["mix phx.gen.context" <> _]}
11 | assert_received {:mix_shell, :info, ["mix phx.gen.embedded" <> _]}
12 | assert_received {:mix_shell, :info, ["mix phx.gen.html" <> _]}
13 | assert_received {:mix_shell, :info, ["mix phx.gen.json" <> _]}
14 | assert_received {:mix_shell, :info, ["mix phx.gen.live" <> _]}
15 | end
16 |
17 | test "expects no arguments" do
18 | assert_raise Mix.Error, fn ->
19 | Mix.Tasks.Phx.run ["invalid"]
20 | end
21 | end
22 | end
23 |
--------------------------------------------------------------------------------
/test/phoenix/code_reloader_test.exs:
--------------------------------------------------------------------------------
1 | defmodule Phoenix.CodeReloaderTest do
2 | use ExUnit.Case, async: true
3 | use RouterHelper
4 |
5 | defmodule Endpoint do
6 | def config(:reloadable_compilers), do: [:unknown_compiler, :elixir]
7 | def config(:reloadable_apps), do: nil
8 | end
9 |
10 | def reload(_, _) do
11 | {:error, "oops \e[31merror"}
12 | end
13 |
14 | @tag :capture_log
15 | test "syncs with code server" do
16 | assert Phoenix.CodeReloader.sync() == :ok
17 |
18 | # Suspend so we can monitor the process until we get a reply.
19 | # There is an inherent race condition here in that the process
20 | # may die before we request but the code should work in both
21 | # cases, so we are fine.
22 | :sys.suspend(Phoenix.CodeReloader.Server)
23 | ref = Process.monitor(Phoenix.CodeReloader.Server)
24 |
25 | Task.start_link(fn ->
26 | Phoenix.CodeReloader.Server
27 | |> Process.whereis()
28 | |> Process.exit(:kill)
29 | end)
30 |
31 | assert Phoenix.CodeReloader.sync() == :ok
32 | assert_receive {:DOWN, ^ref, _, _, _}
33 | wait_until_is_up(Phoenix.CodeReloader.Server)
34 | end
35 |
36 | test "reloads on every request" do
37 | pid = Process.whereis(Phoenix.CodeReloader.Server)
38 | :erlang.trace(pid, true, [:receive])
39 |
40 | opts = Phoenix.CodeReloader.init([])
41 |
42 | conn =
43 | conn(:get, "/")
44 | |> Plug.Conn.put_private(:phoenix_endpoint, Endpoint)
45 | |> Phoenix.CodeReloader.call(opts)
46 |
47 | assert conn.state == :unset
48 |
49 | assert_receive {:trace, ^pid, :receive, {_, _, {:reload!, Endpoint, _}}}
50 | end
51 |
52 | test "renders compilation error on failure" do
53 | opts = Phoenix.CodeReloader.init(reloader: &__MODULE__.reload/2)
54 |
55 | conn =
56 | conn(:get, "/")
57 | |> Plug.Conn.put_private(:phoenix_endpoint, Endpoint)
58 | |> Phoenix.CodeReloader.call(opts)
59 |
60 | assert conn.state == :sent
61 | assert conn.status == 500
62 | assert conn.resp_body =~ "oops error"
63 | assert conn.resp_body =~ "CompileError"
64 | assert conn.resp_body =~ "Compilation error"
65 | end
66 |
67 | defp wait_until_is_up(process) do
68 | if Process.whereis(process) do
69 | :ok
70 | else
71 | Process.sleep(10)
72 | wait_until_is_up(process)
73 | end
74 | end
75 | end
76 |
--------------------------------------------------------------------------------
/test/phoenix/digester/gzip_test.exs:
--------------------------------------------------------------------------------
1 | defmodule Phoenix.Digester.GzipTest do
2 | use ExUnit.Case, async: true
3 | alias Phoenix.Digester.Gzip
4 |
5 | test "compress_file/2 compresses file" do
6 | file_path = "test/fixtures/digest/priv/static/css/app.css"
7 | content = File.read!(file_path)
8 |
9 | {:ok, compressed} = Gzip.compress_file(file_path, content)
10 |
11 | assert is_binary(compressed)
12 | end
13 | end
14 |
--------------------------------------------------------------------------------
/test/phoenix/endpoint/watcher_test.exs:
--------------------------------------------------------------------------------
1 | defmodule Phoenix.Endpoint.WatcherTest do
2 | use ExUnit.Case, async: true
3 |
4 | alias Phoenix.Endpoint.Watcher
5 | import ExUnit.CaptureIO
6 |
7 | test "starts watching and writes to stdio with args" do
8 | assert capture_io(fn ->
9 | {:ok, pid} = Watcher.start_link({"echo", ["hello", cd: File.cwd!()]})
10 | ref = Process.monitor(pid)
11 | assert_receive {:DOWN, ^ref, :process, ^pid, :normal}, 1000
12 | end) == "hello\n"
13 | end
14 |
15 | test "starts watching and writes to stdio with fun" do
16 | assert capture_io(fn ->
17 | {:ok, pid} = Watcher.start_link({"echo", {IO, :puts, ["hello"]}})
18 | ref = Process.monitor(pid)
19 | assert_receive {:DOWN, ^ref, :process, ^pid, :normal}, 1000
20 | end) == "hello\n"
21 | end
22 | end
23 |
--------------------------------------------------------------------------------
/test/phoenix/naming_test.exs:
--------------------------------------------------------------------------------
1 | defmodule Phoenix.NamingTest do
2 | use ExUnit.Case, async: true
3 | alias Phoenix.Naming
4 |
5 | doctest Naming
6 |
7 | test "underscore/1 converts Strings to underscore" do
8 | assert Naming.underscore("FooBar") == "foo_bar"
9 | assert Naming.underscore("Foobar") == "foobar"
10 | assert Naming.underscore("APIWorld") == "api_world"
11 | assert Naming.underscore("ErlangVM") == "erlang_vm"
12 | assert Naming.underscore("API.V1.User") == "api/v1/user"
13 | assert Naming.underscore("") == ""
14 | assert Naming.underscore("FooBar1") == "foo_bar1"
15 | assert Naming.underscore("fooBar1") == "foo_bar1"
16 | end
17 |
18 | test "camelize/1 converts Strings to camel case" do
19 | assert Naming.camelize("foo_bar") == "FooBar"
20 | assert Naming.camelize("foo__bar") == "FooBar"
21 | assert Naming.camelize("foobar") == "Foobar"
22 | assert Naming.camelize("_foobar") == "Foobar"
23 | assert Naming.camelize("__foobar") == "Foobar"
24 | assert Naming.camelize("_FooBar") == "FooBar"
25 | assert Naming.camelize("foobar_") == "Foobar"
26 | assert Naming.camelize("foobar_1") == "Foobar1"
27 | assert Naming.camelize("") == ""
28 | assert Naming.camelize("_foo_bar") == "FooBar"
29 | assert Naming.camelize("foo_bar_1") == "FooBar1"
30 | end
31 |
32 | test "camelize/2 converts Strings to lower camel case" do
33 | assert Naming.camelize("foo_bar", :lower) == "fooBar"
34 | assert Naming.camelize("foo__bar", :lower) == "fooBar"
35 | assert Naming.camelize("foobar", :lower) == "foobar"
36 | assert Naming.camelize("_foobar", :lower) == "foobar"
37 | assert Naming.camelize("__foobar", :lower) == "foobar"
38 | assert Naming.camelize("_FooBar", :lower) == "fooBar"
39 | assert Naming.camelize("foobar_", :lower) == "foobar"
40 | assert Naming.camelize("foobar_1", :lower) == "foobar1"
41 | assert Naming.camelize("", :lower) == ""
42 | assert Naming.camelize("_foo_bar", :lower) == "fooBar"
43 | assert Naming.camelize("foo_bar_1", :lower) == "fooBar1"
44 | end
45 | end
46 |
--------------------------------------------------------------------------------
/test/phoenix/param_test.exs:
--------------------------------------------------------------------------------
1 | defmodule Phoenix.ParamTest do
2 | use ExUnit.Case, async: true
3 |
4 | import Phoenix.Param
5 |
6 | test "to_param for integers" do
7 | assert to_param(1) == "1"
8 | end
9 |
10 | test "to_param for floats" do
11 | assert to_param(3.14) == "3.14"
12 | end
13 |
14 | test "to_param for binaries" do
15 | assert to_param("foo") == "foo"
16 | end
17 |
18 | test "to_param for atoms" do
19 | assert to_param(:foo) == "foo"
20 | assert to_param(true) == "true"
21 | assert to_param(false) == "false"
22 | assert_raise ArgumentError, fn -> to_param(nil) end
23 | end
24 |
25 | test "to_param for maps" do
26 | assert_raise ArgumentError, fn -> to_param(%{id: 1}) end
27 | end
28 |
29 | test "to_param for structs" do
30 | defmodule Foo do
31 | defstruct [:id]
32 | end
33 | assert to_param(struct(Foo, id: 1)) == "1"
34 | assert to_param(struct(Foo, id: "foo")) == "foo"
35 | after
36 | :code.purge(__MODULE__.Foo)
37 | :code.delete(__MODULE__.Foo)
38 | end
39 |
40 | test "to_param for derivable structs without id" do
41 | msg = ~r"cannot derive Phoenix.Param for struct Phoenix.ParamTest.Bar"
42 | assert_raise ArgumentError, msg, fn ->
43 | defmodule Bar do
44 | @derive Phoenix.Param
45 | defstruct [:uuid]
46 | end
47 | end
48 |
49 | defmodule Bar do
50 | @derive {Phoenix.Param, key: :uuid}
51 | defstruct [:uuid]
52 | end
53 |
54 | assert to_param(struct(Bar, uuid: 1)) == "1"
55 | assert to_param(struct(Bar, uuid: "foo")) == "foo"
56 |
57 | msg = ~r"cannot convert Phoenix.ParamTest.Bar to param, key :uuid contains a nil value"
58 | assert_raise ArgumentError, msg, fn ->
59 | to_param(struct(Bar, uuid: nil))
60 | end
61 | after
62 | :code.purge(Module.concat(Phoenix.Param, __MODULE__.Bar))
63 | :code.delete(Module.concat(Phoenix.Param, __MODULE__.Bar))
64 | :code.purge(__MODULE__.Bar)
65 | :code.delete(__MODULE__.Bar)
66 | end
67 | end
68 |
--------------------------------------------------------------------------------
/test/phoenix/socket/message_test.exs:
--------------------------------------------------------------------------------
1 | defmodule Phoenix.Socket.MessageTest do
2 | use ExUnit.Case, async: true
3 | doctest Phoenix.Socket.Message
4 |
5 | alias Phoenix.Socket.Message
6 |
7 | describe "inspect/2 custom implementation" do
8 | test "filters sensitive values in form submit events" do
9 | message = %Message{
10 | topic: "lv:1",
11 | event: "event",
12 | payload: %{
13 | "event" => "submit",
14 | "type" => "form",
15 | "value" => "username=john&password=secret123&email=john@example.com"
16 | },
17 | ref: "1",
18 | join_ref: "1"
19 | }
20 |
21 | assert inspect(message) =~ "\"value\" => \"[FILTERED]\""
22 | end
23 |
24 | test "filters sensitive values at the end of form submit events" do
25 | message = %Message{
26 | topic: "lv:1",
27 | event: "event",
28 | payload: %{
29 | "event" => "submit",
30 | "type" => "form",
31 | "value" => "username=john&password=secret123"
32 | },
33 | ref: "1",
34 | join_ref: "1"
35 | }
36 |
37 | assert inspect(message) =~ "\"value\" => \"[FILTERED]\""
38 | end
39 |
40 | test "handles malformed query strings gracefully" do
41 | message = %Message{
42 | topic: "lv:1",
43 | event: "event",
44 | payload: %{
45 | "event" => "submit",
46 | "type" => "form",
47 | "value" => "invalid=query=string&password=secret"
48 | },
49 | ref: "1",
50 | join_ref: "1"
51 | }
52 |
53 | assert inspect(message) =~ "\"value\" => \"[FILTERED]\""
54 | end
55 | end
56 | end
57 |
--------------------------------------------------------------------------------
/test/phoenix/socket/v1_json_serializer_test.exs:
--------------------------------------------------------------------------------
1 | defmodule Phoenix.Socket.V1.JSONSerializerTest do
2 | use ExUnit.Case, async: true
3 |
4 | alias Phoenix.Socket.{Broadcast, Message, Reply, V1}
5 |
6 | # v1 responses must not contain join_ref
7 | @serializer V1.JSONSerializer
8 | @v1_msg_json "{\"event\":\"e\",\"payload\":\"m\",\"ref\":null,\"topic\":\"t\"}"
9 | @v1_bad_json "[null,null,\"t\",\"e\",{\"m\":1}]"
10 |
11 | def encode!(serializer, msg) do
12 | {:socket_push, :text, encoded} = serializer.encode!(msg)
13 | IO.iodata_to_binary(encoded)
14 | end
15 |
16 | def decode!(serializer, msg, opts), do: serializer.decode!(msg, opts)
17 |
18 | def fastlane!(serializer, msg) do
19 | {:socket_push, :text, encoded} = serializer.fastlane!(msg)
20 | IO.iodata_to_binary(encoded)
21 | end
22 |
23 | test "encode!/1 encodes `Phoenix.Socket.Message` as JSON" do
24 | msg = %Message{topic: "t", event: "e", payload: "m"}
25 | encoded = encode!(@serializer, msg)
26 |
27 | assert Jason.decode!(encoded) == %{
28 | "event" => "e",
29 | "payload" => "m",
30 | "ref" => nil,
31 | "topic" => "t"
32 | }
33 | end
34 |
35 | test "encode!/1 encodes `Phoenix.Socket.Reply` as JSON" do
36 | msg = %Reply{topic: "t", ref: "null"}
37 | encoded = encode!(@serializer, msg)
38 |
39 | assert Jason.decode!(encoded) == %{
40 | "event" => "phx_reply",
41 | "payload" => %{"response" => nil, "status" => nil},
42 | "ref" => "null",
43 | "topic" => "t"
44 | }
45 | end
46 |
47 | test "decode!/2 decodes `Phoenix.Socket.Message` from JSON" do
48 | assert %Message{topic: "t", event: "e", payload: "m"} ==
49 | decode!(@serializer, @v1_msg_json, opcode: :text)
50 | end
51 |
52 | test "decode!/2 raise a PayloadFormatException if the JSON doesn't contain a map" do
53 | assert_raise(
54 | RuntimeError,
55 | "V1 JSON Serializer expected a map, got [nil, nil, \"t\", \"e\", %{\"m\" => 1}]",
56 | fn -> decode!(@serializer, @v1_bad_json, opcode: :text) end
57 | )
58 | end
59 |
60 | test "fastlane!/1 encodes a broadcast into a message as JSON" do
61 | msg = %Broadcast{topic: "t", event: "e", payload: "m"}
62 | encoded = fastlane!(@serializer, msg)
63 |
64 | assert Jason.decode!(encoded) == %{
65 | "event" => "e",
66 | "payload" => "m",
67 | "ref" => nil,
68 | "topic" => "t"
69 | }
70 | end
71 | end
72 |
--------------------------------------------------------------------------------
/test/support/endpoint_helper.exs:
--------------------------------------------------------------------------------
1 | defmodule Phoenix.Integration.EndpointHelper do
2 | @moduledoc """
3 | Utility functions for integration testing endpoints.
4 | """
5 |
6 | @doc """
7 | Finds `n` unused network port numbers.
8 | """
9 | def get_unused_port_numbers(n) when is_integer(n) and n > 1 do
10 | (1..n)
11 | # Open up `n` sockets at the same time, so we don't get
12 | # duplicate port numbers
13 | |> Enum.map(&listen_on_os_assigned_port/1)
14 | |> Enum.map(&get_port_number_and_close/1)
15 | end
16 |
17 | defp listen_on_os_assigned_port(_) do
18 | {:ok, socket} = :gen_tcp.listen(0, [])
19 | socket
20 | end
21 |
22 | defp get_port_number_and_close(socket) do
23 | {:ok, port_number} = :inet.port(socket)
24 | :gen_tcp.close(socket)
25 | port_number
26 | end
27 | end
28 |
--------------------------------------------------------------------------------
/test/support/http_client.exs:
--------------------------------------------------------------------------------
1 | defmodule Phoenix.Integration.HTTPClient do
2 | @doc """
3 | Performs HTTP Request and returns Response
4 |
5 | * method - The http method, for example :get, :post, :put, etc
6 | * url - The string url, for example "http://example.com"
7 | * headers - The map of headers
8 | * body - The optional string body. If the body is a map, it is converted
9 | to a URI encoded string of parameters
10 |
11 | ## Examples
12 |
13 | iex> HTTPClient.request(:get, "http://127.0.0.1", %{})
14 | {:ok, %Response{..})
15 |
16 | iex> HTTPClient.request(:post, "http://127.0.0.1", %{}, param1: "val1")
17 | {:ok, %Response{..})
18 |
19 | iex> HTTPClient.request(:get, "http://unknownhost", %{}, param1: "val1")
20 | {:error, ...}
21 |
22 | """
23 | def request(method, url, headers, body \\ "")
24 | def request(method, url, headers, body) when is_map body do
25 | request(method, url, headers, URI.encode_query(body))
26 | end
27 | def request(method, url, headers, body) do
28 | url = String.to_charlist(url)
29 | headers = headers |> Map.put_new("content-type", "text/html")
30 | ct_type = headers["content-type"] |> String.to_charlist
31 |
32 | header = Enum.map headers, fn {k, v} ->
33 | {String.to_charlist(k), String.to_charlist(v)}
34 | end
35 |
36 | # Generate a random profile per request to avoid reuse
37 | profile = :crypto.strong_rand_bytes(4) |> Base.encode16 |> String.to_atom
38 | {:ok, pid} = :inets.start(:httpc, profile: profile)
39 |
40 | resp =
41 | case method do
42 | :get -> :httpc.request(:get, {url, header}, [], [body_format: :binary], pid)
43 | _ -> :httpc.request(method, {url, header, ct_type, body}, [], [body_format: :binary], pid)
44 | end
45 |
46 | :inets.stop(:httpc, pid)
47 | format_resp(resp)
48 | end
49 |
50 | defp format_resp({:ok, {{_http, status, _status_phrase}, headers, body}}) do
51 | {:ok, %{status: status, headers: headers, body: body}}
52 | end
53 | defp format_resp({:error, reason}), do: {:error, reason}
54 | end
55 |
--------------------------------------------------------------------------------
/test/support/router_helper.exs:
--------------------------------------------------------------------------------
1 | defmodule RouterHelper do
2 | @moduledoc """
3 | Conveniences for testing routers and controllers.
4 |
5 | Must not be used to test endpoints as it does some
6 | pre-processing (like fetching params) which could
7 | skew endpoint tests.
8 | """
9 |
10 | import Plug.Test
11 |
12 | defmacro __using__(_) do
13 | quote do
14 | import Plug.Test
15 | import Plug.Conn
16 | import RouterHelper
17 | end
18 | end
19 |
20 | def call(router, verb, path, params \\ nil, script_name \\ []) do
21 | verb
22 | |> conn(path, params)
23 | |> Plug.Conn.fetch_query_params()
24 | |> Map.put(:script_name, script_name)
25 | |> router.call(router.init([]))
26 | end
27 |
28 | def action(controller, verb, action, params \\ nil) do
29 | conn = conn(verb, "/", params) |> Plug.Conn.fetch_query_params
30 | controller.call(conn, controller.init(action))
31 | end
32 | end
33 |
--------------------------------------------------------------------------------
/test/test_helper.exs:
--------------------------------------------------------------------------------
1 | Code.require_file("support/router_helper.exs", __DIR__)
2 |
3 | # Starts web server applications
4 | Application.ensure_all_started(:plug_cowboy)
5 |
6 | # Used whenever a router fails. We default to simply
7 | # rendering a short string.
8 | defmodule Phoenix.ErrorView do
9 | def render("404.json", %{kind: kind, reason: _reason, stack: _stack, conn: conn}) do
10 | %{error: "Got 404 from #{kind} with #{conn.method}"}
11 | end
12 |
13 | def render(template, %{conn: conn}) do
14 | unless conn.private.phoenix_endpoint do
15 | raise "no endpoint in error view"
16 | end
17 | "#{template} from Phoenix.ErrorView"
18 | end
19 | end
20 |
21 | # For mix tests
22 | Mix.shell(Mix.Shell.Process)
23 |
24 | assert_timeout = String.to_integer(
25 | System.get_env("ELIXIR_ASSERT_TIMEOUT") || "200"
26 | )
27 |
28 | excludes =
29 | if Version.match?(System.version(), "~> 1.15") do
30 | []
31 | else
32 | [:mix_phx_new]
33 | end
34 |
35 | ExUnit.start(assert_receive_timeout: assert_timeout, exclude: excludes)
36 |
--------------------------------------------------------------------------------