├── .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 | --------------------------------------------------------------------------------