├── lib ├── nimble_template │ ├── addons │ │ ├── variants │ │ │ ├── mix │ │ │ │ └── .keep │ │ │ └── phoenix │ │ │ │ ├── live │ │ │ │ └── .keep │ │ │ │ ├── makefile.ex │ │ │ │ ├── web │ │ │ │ ├── node_package.ex │ │ │ │ ├── esbuild.ex │ │ │ │ ├── assets.ex │ │ │ │ ├── heex_formatter.ex │ │ │ │ ├── nimble_css.ex │ │ │ │ ├── core_js.ex │ │ │ │ ├── sobelow.ex │ │ │ │ ├── nimble_js.ex │ │ │ │ ├── style_lint.ex │ │ │ │ ├── prettier.ex │ │ │ │ ├── post_css.ex │ │ │ │ ├── es_lint.ex │ │ │ │ └── dart_sass.ex │ │ │ │ ├── seeds.ex │ │ │ │ ├── gettext.ex │ │ │ │ ├── api │ │ │ │ ├── json_api.ex │ │ │ │ ├── config.ex │ │ │ │ ├── error_view.ex │ │ │ │ ├── empty_body_params_plug.ex │ │ │ │ ├── fallback_controller.ex │ │ │ │ └── params_validation.ex │ │ │ │ ├── docker.ex │ │ │ │ ├── ex_vcr.ex │ │ │ │ ├── ecto_data_migration.ex │ │ │ │ ├── mix_release.ex │ │ │ │ └── ex_machina.ex │ │ ├── faker.ex │ │ ├── dialyxir.ex │ │ ├── iex.ex │ │ ├── git.ex │ │ ├── ex_unit.ex │ │ ├── test_interactive.ex │ │ ├── asdf_tool_version.ex │ │ ├── readme.ex │ │ ├── addon.ex │ │ ├── mimic.ex │ │ └── credo.ex │ ├── http_client │ │ └── http_adapter.ex │ ├── exception │ │ └── mock_hex_package_required.ex │ ├── hex │ │ ├── package_behaviour.ex │ │ ├── package.ex │ │ ├── hex_client.ex │ │ └── package_mock.ex │ ├── templates │ │ ├── variants │ │ │ ├── phoenix │ │ │ │ ├── live │ │ │ │ │ └── template.ex │ │ │ │ ├── api │ │ │ │ │ └── template.ex │ │ │ │ └── web │ │ │ │ │ └── template.ex │ │ │ └── mix │ │ │ │ └── template.ex │ │ └── template.ex │ └── helpers │ │ ├── addon.ex │ │ ├── dependency.ex │ │ ├── github.ex │ │ └── credo.ex └── mix │ └── tasks │ ├── nimble_template.bump_version.ex │ ├── nimble_template.upgrade_stack.ex │ └── nimble_template.gen.ex ├── test ├── nimble_template │ ├── addons │ │ ├── variants │ │ │ ├── mix │ │ │ │ └── .keep │ │ │ ├── phoenix │ │ │ │ ├── live │ │ │ │ │ └── .keep │ │ │ │ ├── web │ │ │ │ │ ├── node_package_test.exs │ │ │ │ │ ├── assets_test.exs │ │ │ │ │ ├── hex_formatter_test.exs │ │ │ │ │ ├── sobelow_test.exs │ │ │ │ │ ├── esbuild_test.exs │ │ │ │ │ ├── core_js_test.exs │ │ │ │ │ ├── nimble_css_test.exs │ │ │ │ │ ├── nimble_js_test.exs │ │ │ │ │ ├── prettier_test.exs │ │ │ │ │ └── style_lint_test.exs │ │ │ │ └── api │ │ │ │ │ ├── error_view_test.exs │ │ │ │ │ ├── json_api_test.exs │ │ │ │ │ ├── fallback_controller_test.exs │ │ │ │ │ ├── params_validation_test.exs │ │ │ │ │ ├── config_test.exs │ │ │ │ │ └── empty_body_plug_test.exs │ │ │ ├── makefile_test.exs │ │ │ ├── seeds_test.exs │ │ │ ├── gettext_test.exs │ │ │ ├── ex_vcr_test.exs │ │ │ ├── ecto_data_migration_test.exs │ │ │ ├── ex_machina_test.exs │ │ │ └── mix_release_test.exs │ │ ├── ex_unit_test.exs │ │ ├── faker_test.exs │ │ ├── dialyxir_test.exs │ │ ├── iex_test.exs │ │ ├── git_test.exs │ │ ├── mimic_test.exs │ │ ├── asdf_tool_version_test.exs │ │ └── test_interactive_test.exs │ ├── release_version_test.exs │ ├── hex │ │ └── package_mock_test.exs.exs │ └── helpers │ │ ├── dependency_test.exs │ │ ├── credo_test.exs │ │ ├── addon_test.exs │ │ └── generator_test.exs ├── test_helper.exs └── support │ ├── template_case.ex │ └── helpers │ └── file_helper.ex ├── priv └── templates │ └── nimble_template │ ├── assets │ ├── nimble_js │ │ ├── lib │ │ │ └── .keep │ │ ├── adapters │ │ │ └── .keep │ │ ├── config │ │ │ └── .keep │ │ ├── components │ │ │ └── .keep │ │ ├── screens │ │ │ └── index.js │ │ ├── initializers │ │ │ └── index.js │ │ └── helpers │ │ │ ├── screen.js │ │ │ └── event.js │ ├── nimble_css │ │ ├── _variables.scss │ │ ├── base │ │ │ └── _index.scss │ │ ├── screens │ │ │ └── _index.scss │ │ ├── vendor │ │ │ └── _index.scss │ │ ├── components │ │ │ └── _index.scss │ │ ├── layouts │ │ │ ├── _index.scss │ │ │ └── _default.scss │ │ ├── functions │ │ │ ├── _index.scss │ │ │ └── _sizing.scss │ │ ├── mixins │ │ │ ├── _index.scss │ │ │ └── _text-ellipsis.scss │ │ └── app.scss │ ├── postcss.config.js │ ├── package.json │ ├── .stylelintrc.json │ ├── .eslintrc.json │ └── bootstrap_css │ │ └── vendor │ │ └── _bootstrap.scss │ ├── priv │ └── repo │ │ ├── data_migrations │ │ └── .keep │ │ └── migrations │ │ └── add_oban_jobs_table.exs.eex │ ├── .github │ ├── wiki │ │ ├── Home.md │ │ ├── _Sidebar.md.mix │ │ ├── _Sidebar.md │ │ ├── Application-Status.md │ │ ├── Environment-Variables.md.eex │ │ ├── Icon-Sprite.md │ │ ├── Getting-Started.md.mix.eex │ │ └── Getting-Started.md.eex │ ├── ISSUE_TEMPLATE.md │ ├── workflows │ │ ├── publish_wiki.yml │ │ ├── deploy_heroku.yml │ │ └── README.md.eex │ ├── PULL_REQUEST_TEMPLATE │ │ └── RELEASE_TEMPLATE.md │ └── PULL_REQUEST_TEMPLATE.md │ ├── .prettierrc.yaml │ ├── Makefile │ ├── bin │ └── start.sh.eex │ ├── .prettierignore │ ├── .tool-versions.eex │ ├── lib │ ├── otp_app_web │ │ ├── helpers │ │ │ ├── router_helper.ex.eex │ │ │ └── icon_helper.ex.eex │ │ ├── params │ │ │ ├── params.ex.eex │ │ │ └── params_validator.ex.eex │ │ ├── plugs │ │ │ ├── check_empty_body_params_plug.ex.eex │ │ │ └── health_plug.ex.eex │ │ ├── controllers │ │ │ └── api │ │ │ │ └── fallback_controller.ex.eex │ │ └── views │ │ │ ├── api │ │ │ └── error_view.ex.eex │ │ │ └── error_helpers.ex.eex │ └── otp_app │ │ └── release_tasks.ex.eex │ ├── .dockerignore │ ├── .sobelow-conf │ ├── test │ ├── support │ │ ├── factory.ex.eex │ │ ├── feature_case.ex.eex │ │ └── view_case.ex.eex │ ├── features │ │ └── home_page │ │ │ └── view_home_page_test.exs.eex │ └── otp_app_web │ │ ├── helpers │ │ ├── router_helper_test.exs.eex │ │ └── icon_helper_test.exs.eex │ │ ├── requests │ │ └── _health │ │ │ ├── liveness_request_test.exs.eex │ │ │ └── readiness_request_test.exs.eex │ │ ├── views │ │ └── error_helpers_test.exs │ │ └── plugs │ │ ├── health_plug_test.exs.eex │ │ └── check_empty_body_params_plug_test.exs.eex │ ├── coveralls.json.mix.eex │ ├── docker-compose.dev.yml.eex │ ├── .iex.exs.eex │ ├── config │ └── runtime.exs.eex │ ├── docker-compose.yml.eex │ ├── coveralls.json.eex │ ├── README.md.mix.eex │ ├── Dockerfile.eex │ └── README.md.eex ├── .github ├── wiki │ ├── Home.md │ ├── _Footer.md │ ├── assets │ │ └── images │ │ │ ├── addon-structure.png │ │ │ ├── bump-version-workflow.png │ │ │ └── upgrade-stack-versions-workflow.png │ ├── _Sidebar.md │ ├── Upgrade-Stack.md │ ├── Release.md │ ├── Docker.md │ └── Generation.md ├── CODEOWNERS ├── ISSUE_TEMPLATE │ ├── feature_template.md │ └── bug_template.md ├── workflows │ ├── publish_wiki.yml │ ├── apply_live_variant.yml │ ├── apply_web_variant.yml │ ├── apply_mix_variant.yml │ ├── publish_to_hex_pm.yml │ ├── verify_release_version.yml │ ├── reusable_mix_project.yml │ ├── apply_api_variant.yml │ └── bump_version.yml └── PULL_REQUEST_TEMPLATE.md ├── .tool-versions ├── config ├── test.exs └── config.exs ├── .formatter.exs ├── .gitignore ├── LICENSE └── mix.exs /lib/nimble_template/addons/variants/mix/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/nimble_template/addons/variants/mix/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /lib/nimble_template/addons/variants/phoenix/live/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /priv/templates/nimble_template/assets/nimble_js/lib/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/nimble_template/addons/variants/phoenix/live/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /priv/templates/nimble_template/assets/nimble_js/adapters/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /priv/templates/nimble_template/assets/nimble_js/config/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /priv/templates/nimble_template/priv/repo/data_migrations/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /priv/templates/nimble_template/assets/nimble_css/_variables.scss: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /priv/templates/nimble_template/assets/nimble_css/base/_index.scss: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /priv/templates/nimble_template/assets/nimble_css/screens/_index.scss: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /priv/templates/nimble_template/assets/nimble_css/vendor/_index.scss: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /priv/templates/nimble_template/assets/nimble_js/components/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /priv/templates/nimble_template/assets/nimble_js/screens/index.js: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.github/wiki/Home.md: -------------------------------------------------------------------------------- 1 | Phoenix/Mix template for projects at Nimble. 2 | -------------------------------------------------------------------------------- /priv/templates/nimble_template/assets/nimble_css/components/_index.scss: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /priv/templates/nimble_template/assets/nimble_js/initializers/index.js: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.github/wiki/_Footer.md: -------------------------------------------------------------------------------- 1 | **Developed by [Nimble](https://nimblehq.co/)** 2 | -------------------------------------------------------------------------------- /.tool-versions: -------------------------------------------------------------------------------- 1 | erlang 25.2.3 2 | elixir 1.14.3-otp-25 3 | nodejs 18.14.2 4 | -------------------------------------------------------------------------------- /priv/templates/nimble_template/assets/nimble_css/layouts/_index.scss: -------------------------------------------------------------------------------- 1 | @import './default'; 2 | -------------------------------------------------------------------------------- /priv/templates/nimble_template/assets/nimble_css/functions/_index.scss: -------------------------------------------------------------------------------- 1 | @import './sizing'; 2 | -------------------------------------------------------------------------------- /priv/templates/nimble_template/assets/nimble_css/mixins/_index.scss: -------------------------------------------------------------------------------- 1 | @import './text-ellipsis'; 2 | -------------------------------------------------------------------------------- /priv/templates/nimble_template/.github/wiki/Home.md: -------------------------------------------------------------------------------- 1 | > *Insert information about your project here!* 2 | -------------------------------------------------------------------------------- /priv/templates/nimble_template/.prettierrc.yaml: -------------------------------------------------------------------------------- 1 | printWidth: 120 2 | trailingComma: "none" 3 | semi: false 4 | -------------------------------------------------------------------------------- /config/test.exs: -------------------------------------------------------------------------------- 1 | import Config 2 | 3 | config :nimble_template, hex_package_resource: NimbleTemplate.Hex.PackageMock 4 | -------------------------------------------------------------------------------- /priv/templates/nimble_template/assets/postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: [require("autoprefixer")], 3 | }; 4 | -------------------------------------------------------------------------------- /priv/templates/nimble_template/.github/wiki/_Sidebar.md.mix: -------------------------------------------------------------------------------- 1 | ## Table of Contents 2 | 3 | - [[Home]] 4 | - [[Getting Started]] 5 | -------------------------------------------------------------------------------- /priv/templates/nimble_template/Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: docker_setup 2 | 3 | docker_setup: 4 | docker-compose -f docker-compose.dev.yml up -d 5 | -------------------------------------------------------------------------------- /.github/wiki/assets/images/addon-structure.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nimblehq/elixir-templates/HEAD/.github/wiki/assets/images/addon-structure.png -------------------------------------------------------------------------------- /lib/nimble_template/http_client/http_adapter.ex: -------------------------------------------------------------------------------- 1 | defmodule NimbleTemplate.HttpClient.HttpAdapter do 2 | @moduledoc false 3 | 4 | use HTTPoison.Base 5 | end 6 | -------------------------------------------------------------------------------- /.github/wiki/assets/images/bump-version-workflow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nimblehq/elixir-templates/HEAD/.github/wiki/assets/images/bump-version-workflow.png -------------------------------------------------------------------------------- /lib/nimble_template/exception/mock_hex_package_required.ex: -------------------------------------------------------------------------------- 1 | defmodule NimbleTemplate.Exception.MockHexPackageRequiredException do 2 | defexception [:message] 3 | end 4 | -------------------------------------------------------------------------------- /priv/templates/nimble_template/bin/start.sh.eex: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | bin/<%= otp_app %> eval "<%= base_module %>.ReleaseTasks.migrate()" 4 | 5 | bin/<%= otp_app %> start 6 | -------------------------------------------------------------------------------- /priv/templates/nimble_template/.prettierignore: -------------------------------------------------------------------------------- 1 | deps/ 2 | _build/ 3 | .elixir_ls 4 | assets 5 | priv 6 | cover/ 7 | .github/ 8 | README.md 9 | .idea 10 | .vscode 11 | -------------------------------------------------------------------------------- /.formatter.exs: -------------------------------------------------------------------------------- 1 | [ 2 | line_length: 100, 3 | inputs: ["*.{ex,exs}", "priv/*/seeds.exs", "{config,lib,test}/**/*.{ex,exs}"], 4 | subdirectories: ["priv/*/migrations"] 5 | ] 6 | -------------------------------------------------------------------------------- /priv/templates/nimble_template/.tool-versions.eex: -------------------------------------------------------------------------------- 1 | # Configuration file for https://github.com/asdf-vm/asdf 2 | erlang <%= erlang_version %> 3 | elixir <%= elixir_asdf_version %> 4 | -------------------------------------------------------------------------------- /.github/wiki/assets/images/upgrade-stack-versions-workflow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nimblehq/elixir-templates/HEAD/.github/wiki/assets/images/upgrade-stack-versions-workflow.png -------------------------------------------------------------------------------- /priv/templates/nimble_template/assets/nimble_js/helpers/screen.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | export const isMobileScreenSize = () => { 4 | return window.matchMedia("(max-width: 767px)").matches; 5 | }; 6 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | # Team Members 2 | * @byhbt @andyduong1920 @bterone @hanam1ni @longnd @rosle @topnimble @Nihisil @nvminhtue @liamstevens111 3 | 4 | # Engineering Leads 5 | CODEOWNERS @nimblehq/engineering-leads 6 | -------------------------------------------------------------------------------- /priv/templates/nimble_template/.github/wiki/_Sidebar.md: -------------------------------------------------------------------------------- 1 | ## Table of Contents 2 | 3 | - [[Home]] 4 | - [[Getting Started]] 5 | 6 | ## Infrastructure 7 | 8 | - [[Application Status]] 9 | - [[Environment Variables]] 10 | -------------------------------------------------------------------------------- /priv/templates/nimble_template/lib/otp_app_web/helpers/router_helper.ex.eex: -------------------------------------------------------------------------------- 1 | defmodule <%= web_module %>.RouterHelper do 2 | def health_path, do: Application.get_env(:<%= otp_app %>, <%= web_module %>.Endpoint)[:health_path] 3 | end 4 | -------------------------------------------------------------------------------- /priv/templates/nimble_template/assets/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "repository": {}, 3 | "description": " ", 4 | "license": "MIT", 5 | "scripts": { 6 | }, 7 | "dependencies": { 8 | }, 9 | "devDependencies": { 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /priv/templates/nimble_template/assets/nimble_css/layouts/_default.scss: -------------------------------------------------------------------------------- 1 | .layout-default { 2 | > body { 3 | display: grid; 4 | grid-template-columns: auto; 5 | grid-template-rows: auto 1fr auto; 6 | min-height: 100vh; 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /priv/templates/nimble_template/.dockerignore: -------------------------------------------------------------------------------- 1 | _build 2 | .elixir_ls 3 | .github 4 | .gitignore 5 | .dockerignore 6 | assets/node_modules 7 | deps 8 | docker-compose.yml 9 | docker-compose.dev.yml 10 | dockerfile 11 | README.md 12 | cover 13 | **/tmp/ 14 | doc 15 | -------------------------------------------------------------------------------- /priv/templates/nimble_template/.sobelow-conf: -------------------------------------------------------------------------------- 1 | [ 2 | verbose: false, 3 | private: false, 4 | compact: true, 5 | skip: true, 6 | router: "", 7 | exit: "low", 8 | format: "txt", 9 | ignore: ["Config.HTTPS", "Config.CSP"], 10 | ignore_files: [] 11 | ] 12 | -------------------------------------------------------------------------------- /config/config.exs: -------------------------------------------------------------------------------- 1 | import Config 2 | 3 | config :phoenix, :json_library, Jason 4 | 5 | config :nimble_template, hex_package_resource: NimbleTemplate.Hex.Package 6 | 7 | if File.exists?("config/#{config_env()}.exs") do 8 | import_config "#{config_env()}.exs" 9 | end 10 | -------------------------------------------------------------------------------- /lib/nimble_template/hex/package_behaviour.ex: -------------------------------------------------------------------------------- 1 | defmodule NimbleTemplate.Hex.PackageBehaviour do 2 | alias NimbleTemplate.Exception.MockHexPackageRequiredException 3 | 4 | @callback get_latest_version(list(String.t())) :: String.t() | MockHexPackageRequiredException 5 | end 6 | -------------------------------------------------------------------------------- /priv/templates/nimble_template/test/support/factory.ex.eex: -------------------------------------------------------------------------------- 1 | defmodule <%= base_module %>.Factory do 2 | use ExMachina.Ecto, repo: <%= base_module %>.Repo 3 | 4 | # Define your factories in /test/factories and declare it here, 5 | # eg: `use <%= base_module %>.Accounts.UserFactory` 6 | end 7 | -------------------------------------------------------------------------------- /priv/templates/nimble_template/assets/nimble_css/app.scss: -------------------------------------------------------------------------------- 1 | @import './variables'; 2 | 3 | @import './functions'; 4 | 5 | @import './vendor'; 6 | 7 | @import './mixins'; 8 | 9 | @import './base'; 10 | 11 | @import './components'; 12 | 13 | @import './layouts'; 14 | 15 | @import './screens'; 16 | -------------------------------------------------------------------------------- /test/test_helper.exs: -------------------------------------------------------------------------------- 1 | Code.put_compiler_option(:warnings_as_errors, true) 2 | 3 | {:ok, _} = Application.ensure_all_started(:mimic) 4 | 5 | Mimic.copy(Calendar) 6 | Mimic.copy(NimbleTemplate.Hex.PackageMock) 7 | 8 | ExUnit.start(capture_log: true) 9 | ExUnit.configure(exclude: :release_version) 10 | -------------------------------------------------------------------------------- /priv/templates/nimble_template/.github/wiki/Application-Status.md: -------------------------------------------------------------------------------- 1 | 1. Liveness 2 | - URL: `/_health/liveness` 3 | - Response: 4 | - HTTP status: 200 5 | - Body: Alive 6 | 7 | 2. Readiness 8 | - URL: `/_health/readiness` 9 | - Response: 10 | - HTTP status: 200 11 | - Body: Ready 12 | -------------------------------------------------------------------------------- /.github/wiki/_Sidebar.md: -------------------------------------------------------------------------------- 1 | ## Table of Contents 2 | 3 | - [[Home]] 4 | - [[Generation]] 5 | - [[Contribution]] 6 | 7 | ## Architecture 8 | 9 | - [[Docker]] 10 | - [[Testing]] 11 | 12 | ## Release 13 | 14 | - [[Release]] 15 | 16 | ## Upgrade stack (Elixir, Erlang, Alpine, Node version) 17 | - [[Upgrade-Stack]] 18 | -------------------------------------------------------------------------------- /priv/templates/nimble_template/coveralls.json.mix.eex: -------------------------------------------------------------------------------- 1 | { 2 | "skip_files": [ 3 | "lib/<%= otp_app %>/application.ex", 4 | "test/support" 5 | ], 6 | "coverage_options": { 7 | "minimum_coverage": <%= minimum_coverage %>, 8 | "html_filter_full_covered": <%= html_filter_full_covered %>, 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /priv/templates/nimble_template/docker-compose.dev.yml.eex: -------------------------------------------------------------------------------- 1 | version: "3.8" 2 | 3 | services: 4 | db: 5 | image: postgres:14.2 6 | container_name: <%= otp_app %>_db 7 | environment: 8 | - POSTGRES_HOST_AUTH_METHOD=trust 9 | - POSTGRES_DB=<%= otp_app %>_dev 10 | ports: 11 | - "5432:5432" 12 | -------------------------------------------------------------------------------- /priv/templates/nimble_template/assets/nimble_js/helpers/event.js: -------------------------------------------------------------------------------- 1 | export const dispatchEventFromElement = ( 2 | element, 3 | eventName, 4 | eventDetail = {} 5 | ) => { 6 | const event = new CustomEvent(eventName, { 7 | detail: eventDetail, 8 | cancelable: true, 9 | }); 10 | 11 | element.dispatchEvent(event); 12 | }; 13 | -------------------------------------------------------------------------------- /lib/nimble_template/addons/variants/phoenix/makefile.ex: -------------------------------------------------------------------------------- 1 | defmodule NimbleTemplate.Addons.Phoenix.Makefile do 2 | @moduledoc false 3 | 4 | use NimbleTemplate.Addons.Addon 5 | 6 | @impl true 7 | def do_apply!(%Project{} = project, _opts) do 8 | Generator.copy_file!([{:text, "Makefile", "Makefile"}]) 9 | 10 | project 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_template.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: "Feature" 3 | about: "Open a feature issue to add new functionalities." 4 | title: "Add " 5 | labels: "type : feature" 6 | --- 7 | 8 | ## Why 9 | 10 | Describe the big picture of the feature and why it's needed. 11 | 12 | ## Who Benefits? 13 | 14 | Describe who will be the beneficiaries e.g. everyone, clients... -------------------------------------------------------------------------------- /priv/templates/nimble_template/.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ## Bug report 2 | 3 | Provide a brief summary of your issue **AND** if reporting a build issue include the version/build number. 4 | 5 | ## Feature request 6 | 7 | Provide a brief summary of the new feature required. 8 | 9 | **Please note by far the quickest way to get a new feature is to file a Pull Request.** 10 | -------------------------------------------------------------------------------- /lib/nimble_template/addons/variants/phoenix/web/node_package.ex: -------------------------------------------------------------------------------- 1 | defmodule NimbleTemplate.Addons.Phoenix.Web.NodePackage do 2 | @moduledoc false 3 | 4 | use NimbleTemplate.Addons.Addon 5 | 6 | @impl true 7 | def do_apply!(%Project{} = project, _opts) do 8 | Generator.copy_file!([{:text, "assets/package.json", "assets/package.json"}]) 9 | 10 | project 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /priv/templates/nimble_template/.iex.exs.eex: -------------------------------------------------------------------------------- 1 | # Load personal IEx config 2 | import_file_if_available("~/.iex.exs") 3 | <%= if web_project? do %> 4 | import_if_available(Ecto.Query) 5 | 6 | alias <%= base_module %>.Repo 7 | <% end %> 8 | IEx.configure( 9 | history_size: 200, 10 | inspect: [ 11 | pretty: true, 12 | limit: :infinity, 13 | charlists: :as_lists 14 | ] 15 | ) 16 | -------------------------------------------------------------------------------- /priv/templates/nimble_template/config/runtime.exs.eex: -------------------------------------------------------------------------------- 1 | # In this file, we load production configuration and secrets 2 | # from environment variables. You can also hardcode secrets, 3 | # although such is generally not recommended and you have to 4 | # remember to add this file to your .gitignore. 5 | import Config 6 | 7 | if config_env() == :prod do 8 | <%= prod_secret_content %> 9 | end 10 | -------------------------------------------------------------------------------- /lib/nimble_template/addons/faker.ex: -------------------------------------------------------------------------------- 1 | defmodule NimbleTemplate.Addons.Faker do 2 | @moduledoc false 3 | 4 | use NimbleTemplate.Addons.Addon 5 | 6 | @impl true 7 | def do_apply!(%Project{} = project, _opts) do 8 | Generator.inject_mix_dependency!( 9 | {:faker, latest_package_version(:faker), only: [:dev, :test], runtime: false} 10 | ) 11 | 12 | project 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /priv/templates/nimble_template/.github/wiki/Environment-Variables.md.eex: -------------------------------------------------------------------------------- 1 | ## List of Environment Variables 2 | 3 | ### Database 4 | 5 | - `DB_URL`: URL to database 6 | - `DB_POOL_SIZE`: Number of connections to the database 7 | 8 | ### Endpoint configuration 9 | 10 | - `HEALTH_PATH`: Health path (eg: "/_health") 11 | - `PHX_HOST`: The application endpoint. 12 | - `SECRET_KEY_BASE`: Secret key base 13 | -------------------------------------------------------------------------------- /lib/nimble_template/addons/dialyxir.ex: -------------------------------------------------------------------------------- 1 | defmodule NimbleTemplate.Addons.Dialyxir do 2 | @moduledoc false 3 | 4 | use NimbleTemplate.Addons.Addon 5 | 6 | @impl true 7 | def do_apply!(%Project{} = project, _opts) do 8 | Generator.inject_mix_dependency!( 9 | {:dialyxir, latest_package_version(:dialyxir), only: [:dev], runtime: false} 10 | ) 11 | 12 | project 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /lib/nimble_template/hex/package.ex: -------------------------------------------------------------------------------- 1 | defmodule NimbleTemplate.Hex.Package do 2 | @moduledoc false 3 | 4 | @behaviour NimbleTemplate.Hex.PackageBehaviour 5 | 6 | alias NimbleTemplate.Hex.HexClient 7 | 8 | def get_latest_version(package) do 9 | {:ok, package_info} = HexClient.get("packages/#{package}") 10 | 11 | package_info["releases"] |> List.first() |> Map.get("version") 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /priv/templates/nimble_template/test/features/home_page/view_home_page_test.exs.eex: -------------------------------------------------------------------------------- 1 | defmodule <%= web_module %>.HomePage.ViewHomePageTest do 2 | use <%= web_module %>.FeatureCase, async: false 3 | 4 | feature "view home page", %{session: session} do 5 | visit(session, Routes.page_path(<%= web_module %>.Endpoint, :index)) 6 | 7 | assert_has(session, Query.text("Welcome to Phoenix!")) 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /priv/templates/nimble_template/assets/nimble_css/functions/_sizing.scss: -------------------------------------------------------------------------------- 1 | @function rem($values, $base-font-size: 16px) { 2 | $list: (); 3 | 4 | @each $value in $values { 5 | @if ($value == 0 or $value == auto) { 6 | $list: append($list, $value); 7 | } @else { 8 | $rem-value: calc($value / $base-font-size) + rem; 9 | $list: append($list, $rem-value); 10 | } 11 | } 12 | 13 | @return $list; 14 | } 15 | -------------------------------------------------------------------------------- /priv/templates/nimble_template/test/otp_app_web/helpers/router_helper_test.exs.eex: -------------------------------------------------------------------------------- 1 | defmodule <%= web_module %>.RouterHelperTest do 2 | use <%= web_module %>.ConnCase, async: true 3 | 4 | alias <%= web_module %>.RouterHelper 5 | 6 | describe "health_path/0" do 7 | test "returns the `health_path` from the Application configuration" do 8 | assert RouterHelper.health_path() == "/_health" 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /priv/templates/nimble_template/docker-compose.yml.eex: -------------------------------------------------------------------------------- 1 | version: "3.8" 2 | 3 | services: 4 | build: 5 | build: 6 | context: . 7 | cache_from: 8 | - <%= otp_app %>-build 9 | target: build 10 | image: <%= otp_app %>-build 11 | 12 | app: 13 | build: 14 | context: . 15 | cache_from: 16 | - <%= otp_app %>-build 17 | - <%= otp_app %> 18 | target: app 19 | image: <%= otp_app %> 20 | -------------------------------------------------------------------------------- /lib/nimble_template/addons/iex.ex: -------------------------------------------------------------------------------- 1 | defmodule NimbleTemplate.Addons.Iex do 2 | @moduledoc false 3 | 4 | use NimbleTemplate.Addons.Addon 5 | 6 | @impl true 7 | def do_apply!(%Project{base_module: base_module, web_project?: web_project?} = project, _opts) do 8 | Generator.copy_file!([{:eex, ".iex.exs.eex", ".iex.exs"}], 9 | base_module: base_module, 10 | web_project?: web_project? 11 | ) 12 | 13 | project 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /test/nimble_template/release_version_test.exs: -------------------------------------------------------------------------------- 1 | defmodule NimbleTemplate.ReleaseVersionTest do 2 | use ExUnit.Case, async: true 3 | 4 | alias NimbleTemplate.Hex.Package 5 | 6 | @tag :release_version 7 | test "the new version is greater than the hex version" do 8 | new_version = Mix.Project.config()[:version] 9 | hex_version = Package.get_latest_version("nimble_template") 10 | 11 | assert new_version > hex_version 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_template.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: "Bug Report" 3 | about: "You found something that is not working. Report it so that it can be fixed. 👷‍" 4 | title: "Fix: " 5 | labels: "type : bug" 6 | --- 7 | 8 | ## Issue 9 | 10 | Describe the issue you are facing. Show us the implementation: screenshots, gif, etc. 11 | 12 | ## Expected 13 | 14 | Describe what should be the correct behaviour. 15 | 16 | ## Steps to reproduce 17 | 18 | 1. 19 | 2. 20 | 3. -------------------------------------------------------------------------------- /lib/nimble_template/templates/variants/phoenix/live/template.ex: -------------------------------------------------------------------------------- 1 | defmodule NimbleTemplate.Templates.Phoenix.Live.Template do 2 | @moduledoc false 3 | 4 | alias NimbleTemplate.Projects.Project 5 | alias NimbleTemplate.Templates.Phoenix.Web.Template, as: WebTemplate 6 | 7 | def apply!(%Project{} = project) do 8 | apply_web_addons(project) 9 | end 10 | 11 | defp apply_web_addons(project) do 12 | WebTemplate.apply!(project) 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /.github/workflows/publish_wiki.yml: -------------------------------------------------------------------------------- 1 | name: Publish Wiki 2 | 3 | on: 4 | push: 5 | branches: 6 | - develop 7 | paths: 8 | - .github/wiki/** 9 | 10 | jobs: 11 | publish: 12 | name: Publish wiki 13 | uses: nimblehq/github-actions-workflows/.github/workflows/publish_wiki.yml@0.1.0 14 | with: 15 | USER_NAME: github-wiki-action 16 | USER_EMAIL: dev@nimblehq.co 17 | secrets: 18 | USER_TOKEN: ${{ secrets.WIKI_ACTION_TOKEN }} 19 | -------------------------------------------------------------------------------- /priv/templates/nimble_template/priv/repo/migrations/add_oban_jobs_table.exs.eex: -------------------------------------------------------------------------------- 1 | defmodule <%= base_module %>.Repo.Migrations.AddObanJobsTable do 2 | use Ecto.Migration 3 | 4 | def up do 5 | Oban.Migrations.up() 6 | end 7 | 8 | # We specify `version: 1` in `down`, ensuring that we'll roll all the way back down if 9 | # necessary, regardless of which version we've migrated `up` to. 10 | def down do 11 | Oban.Migrations.down(version: 1) 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /priv/templates/nimble_template/test/otp_app_web/requests/_health/liveness_request_test.exs.eex: -------------------------------------------------------------------------------- 1 | defmodule <%= web_module %>.LivenessRequestTest do 2 | use <%= web_module %>.ConnCase, async: true 3 | 4 | test "returns 200", %{conn: conn} do 5 | conn = 6 | get( 7 | conn, 8 | "#{Application.get_env(:<%= otp_app %>, <%= web_module %>.Endpoint)[:health_path]}/liveness" 9 | ) 10 | 11 | assert response(conn, :ok) =~ "alive" 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /priv/templates/nimble_template/test/otp_app_web/requests/_health/readiness_request_test.exs.eex: -------------------------------------------------------------------------------- 1 | defmodule <%= web_module %>.ReadinessRequestTest do 2 | use <%= web_module %>.ConnCase, async: true 3 | 4 | test "returns 200", %{conn: conn} do 5 | conn = 6 | get( 7 | conn, 8 | "#{Application.get_env(:<%= otp_app %>, <%= web_module %>.Endpoint)[:health_path]}/readiness" 9 | ) 10 | 11 | assert response(conn, :ok) =~ "ready" 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /test/nimble_template/addons/variants/makefile_test.exs: -------------------------------------------------------------------------------- 1 | defmodule NimbleTemplate.Addons.Phoenix.MakefileTest do 2 | use NimbleTemplate.AddonCase, async: false 3 | 4 | describe "#apply!/2" do 5 | test "copies the Makefile", %{project: project, test_project_path: test_project_path} do 6 | in_test_project!(test_project_path, fn -> 7 | PhoenixAddons.Makefile.apply!(project) 8 | 9 | assert_file("Makefile") 10 | end) 11 | end 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /lib/nimble_template/addons/git.ex: -------------------------------------------------------------------------------- 1 | defmodule NimbleTemplate.Addons.Git do 2 | @moduledoc false 3 | 4 | use NimbleTemplate.Addons.Addon 5 | 6 | @impl true 7 | def do_apply!(%Project{} = project, _opts) do 8 | Generator.append_content!(".gitignore", """ 9 | # Mac OS 10 | .DS_Store 11 | 12 | # IDE 13 | .idea 14 | .vscode 15 | 16 | # Iex 17 | .iex.exs 18 | 19 | # Ignore ElixirLS files 20 | .elixir_ls 21 | """) 22 | 23 | project 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /priv/templates/nimble_template/.github/workflows/publish_wiki.yml: -------------------------------------------------------------------------------- 1 | name: Publish Wiki 2 | 3 | on: 4 | push: 5 | paths: 6 | - .github/wiki/** 7 | branches: 8 | - develop 9 | 10 | jobs: 11 | publish: 12 | name: Publish Wiki 13 | uses: nimblehq/github-actions-workflows/.github/workflows/publish_wiki.yml@0.1.0 14 | with: 15 | USER_NAME: github-wiki-workflow 16 | USER_EMAIL: ${{ secrets.GH_EMAIL }} 17 | secrets: 18 | USER_TOKEN: ${{ secrets.GH_TOKEN }} 19 | -------------------------------------------------------------------------------- /priv/templates/nimble_template/assets/.stylelintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "stylelint-config-sass-guidelines", 4 | "stylelint-config-property-sort-order-smacss" 5 | ], 6 | "ignoreFiles": [ 7 | "css/app.css", 8 | "css/phoenix.css" 9 | ], 10 | "rules": { 11 | "selector-class-pattern": null, 12 | "no-eol-whitespace": true, 13 | "max-nesting-depth": 3, 14 | "scss/at-extend-no-missing-placeholder": null, 15 | "order/properties-alphabetical-order": null 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /test/nimble_template/hex/package_mock_test.exs.exs: -------------------------------------------------------------------------------- 1 | defmodule NimbleTemplate.Hex.PackageMockTest do 2 | use ExUnit.Case, async: true 3 | 4 | alias NimbleTemplate.Exception.MockHexPackageRequiredException 5 | alias NimbleTemplate.Hex.PackageMock 6 | 7 | describe "get_latest_version/1" do 8 | test "raises a MockHexPackageRequiredException" do 9 | assert_raise MockHexPackageRequiredException, fn -> 10 | PackageMock.get_latest_version("credo") 11 | end 12 | end 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /.github/wiki/Upgrade-Stack.md: -------------------------------------------------------------------------------- 1 | 1. Visit the [Upgrade Stack Github Action Workflow](https://github.com/nimblehq/elixir-templates/actions/workflows/upgrade_stack.yml). 2 | 3 | 2. Choose `develop` branch. 4 | 5 | 3. Trigger the workflow with the new stack versions. That workflow will create a PR into the `develop` branch with the title `[Chore] Upgrade stack versions`. 6 | 7 | ![upgrade-stack-versions-workflow](assets/images/upgrade-stack-versions-workflow.png) 8 | 9 | 4. Merge the PR above into the `develop` branch. 10 | -------------------------------------------------------------------------------- /lib/nimble_template/hex/hex_client.ex: -------------------------------------------------------------------------------- 1 | defmodule NimbleTemplate.Hex.HexClient do 2 | @moduledoc false 3 | 4 | alias NimbleTemplate.HttpClient.HttpAdapter 5 | 6 | @base_url "https://hex.pm/api/" 7 | 8 | def get(path) do 9 | url = @base_url <> URI.encode(path) 10 | 11 | case HttpAdapter.get(url) do 12 | {:ok, %HTTPoison.Response{status_code: 200, body: body}} -> 13 | {:ok, Jason.decode!(body)} 14 | 15 | {:error, %HTTPoison.Error{reason: reason}} -> 16 | {:error, reason} 17 | end 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /lib/nimble_template/addons/ex_unit.ex: -------------------------------------------------------------------------------- 1 | defmodule NimbleTemplate.Addons.ExUnit do 2 | @moduledoc false 3 | 4 | use NimbleTemplate.Addons.Addon 5 | 6 | @impl true 7 | def do_apply!(%Project{} = project, _opts), do: edit_test_helper!(project) 8 | 9 | defp edit_test_helper!(%Project{} = project) do 10 | Generator.replace_content!( 11 | "test/test_helper.exs", 12 | """ 13 | ExUnit.start() 14 | """, 15 | """ 16 | ExUnit.start(capture_log: true) 17 | """ 18 | ) 19 | 20 | project 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /lib/nimble_template/addons/variants/phoenix/seeds.ex: -------------------------------------------------------------------------------- 1 | defmodule NimbleTemplate.Addons.Phoenix.Seeds do 2 | @moduledoc false 3 | 4 | use NimbleTemplate.Addons.Addon 5 | 6 | @impl true 7 | def do_apply!(%Project{} = project, _opts) do 8 | edit_seeds_file!(project) 9 | 10 | project 11 | end 12 | 13 | defp edit_seeds_file!(project) do 14 | Generator.append_content!("priv/repo/seeds.exs", """ 15 | if Mix.env() == :dev || System.get_env("ENABLE_DB_SEED") == "true" do 16 | end 17 | """) 18 | 19 | project 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /test/nimble_template/addons/variants/phoenix/web/node_package_test.exs: -------------------------------------------------------------------------------- 1 | defmodule NimbleTemplate.Addons.Phoenix.Web.NodePackageTest do 2 | use NimbleTemplate.AddonCase, async: false 3 | 4 | describe "#apply!/2" do 5 | test "copies the package.json into assets", %{ 6 | project: project, 7 | test_project_path: test_project_path 8 | } do 9 | in_test_project!(test_project_path, fn -> 10 | WebAddons.NodePackage.apply!(project) 11 | 12 | assert_file("assets/package.json") 13 | end) 14 | end 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /priv/templates/nimble_template/lib/otp_app_web/params/params.ex.eex: -------------------------------------------------------------------------------- 1 | defmodule <%= web_module %>.Params do 2 | @moduledoc """ 3 | Apply to the params module to define params schema with validation. 4 | """ 5 | 6 | @callback changeset(map(), map()) :: Ecto.Changeset.t() 7 | 8 | @optional_callbacks [changeset: 2] 9 | 10 | @type t :: module 11 | 12 | defmacro __using__(_) do 13 | quote do 14 | use Ecto.Schema 15 | 16 | import Ecto.Changeset 17 | 18 | @primary_key false 19 | 20 | @behaviour <%= web_module %>.Params 21 | end 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /lib/nimble_template/hex/package_mock.ex: -------------------------------------------------------------------------------- 1 | defmodule NimbleTemplate.Hex.PackageMock do 2 | @moduledoc false 3 | 4 | @behaviour NimbleTemplate.Hex.PackageBehaviour 5 | 6 | alias NimbleTemplate.Exception.MockHexPackageRequiredException 7 | 8 | # Prevent the call to get the latest version if the test forgets to mock the external library 9 | def get_latest_version(package) do 10 | raise(MockHexPackageRequiredException, 11 | message: 12 | "Requires to mock Hex package with `@describetag mock_latest_package_versions: [{#{package}, :version}]`" 13 | ) 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /test/nimble_template/addons/ex_unit_test.exs: -------------------------------------------------------------------------------- 1 | defmodule NimbleTemplate.Addons.ExUnitTest do 2 | use NimbleTemplate.AddonCase, async: false 3 | 4 | describe "#apply!/2" do 5 | test "sets `ExUnit.start(capture_log: true)` in `test/test_helper.exs`", 6 | %{project: project, test_project_path: test_project_path} do 7 | in_test_project!(test_project_path, fn -> 8 | Addons.ExUnit.apply!(project) 9 | 10 | assert_file("test/test_helper.exs", fn file -> 11 | assert file =~ "ExUnit.start(capture_log: true)" 12 | end) 13 | end) 14 | end 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /priv/templates/nimble_template/assets/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "node": true, 5 | "es2021": true 6 | }, 7 | "extends": [ 8 | "eslint:recommended", 9 | "prettier" 10 | ], 11 | "parserOptions": { 12 | "ecmaVersion": 13, 13 | "sourceType": "module" 14 | }, 15 | "plugins": [ 16 | "prettier" 17 | ], 18 | "rules": { 19 | "prettier/prettier": "error", 20 | "no-unused-vars": [ 21 | "error", { 22 | "argsIgnorePattern": "^_" 23 | } 24 | ] 25 | }, 26 | "ignorePatterns": [ 27 | "/vendor/topbar.js" 28 | ] 29 | } 30 | -------------------------------------------------------------------------------- /test/nimble_template/addons/variants/phoenix/web/assets_test.exs: -------------------------------------------------------------------------------- 1 | defmodule NimbleTemplate.Addons.Phoenix.Web.AssetsTest do 2 | use NimbleTemplate.AddonCase, async: false 3 | 4 | describe "#apply!/2" do 5 | test "enables gzip for static", %{ 6 | project: project, 7 | test_project_path: test_project_path 8 | } do 9 | in_test_project!(test_project_path, fn -> 10 | WebAddons.Assets.apply!(project) 11 | 12 | assert_file("lib/nimble_template_web/endpoint.ex", fn file -> 13 | assert file =~ "gzip: true," 14 | end) 15 | end) 16 | end 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /priv/templates/nimble_template/test/support/feature_case.ex.eex: -------------------------------------------------------------------------------- 1 | defmodule <%= web_module %>.FeatureCase do 2 | use ExUnit.CaseTemplate 3 | 4 | using do 5 | quote do 6 | <%= if with_ex_vcr? do %> 7 | use ExVCR.Mock, adapter: ExVCR.Adapter.Hackney 8 | <% end %> 9 | use Wallaby.Feature 10 | use Mimic 11 | 12 | import <%= base_module %>.Factory 13 | import <%= web_module %>.Gettext 14 | 15 | alias <%= base_module %>.Repo 16 | alias <%= web_module %>.Endpoint 17 | alias <%= web_module %>.Router.Helpers, as: Routes 18 | 19 | @moduletag :feature_test 20 | end 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | https://github.com/nimblehq/elixir-templates/issues/?? 2 | 3 | ## What happened 👀 4 | 5 | Provide a description of the **changes** this pull request brings to the codebase. Additionally, when the pull request is still being worked on, a checklist of the planned changes is welcome to track progress. 6 | 7 | ## Insight 📝 8 | 9 | Describe in detail why this solution is the most appropriate, which solution you tried but did not go with, and how to test the changes. References to relevant documentation are welcome as well. 10 | 11 | ## Proof Of Work 📹 12 | 13 | Show us the implementation: screenshots, GIFs, etc. 14 | -------------------------------------------------------------------------------- /lib/nimble_template/templates/variants/phoenix/api/template.ex: -------------------------------------------------------------------------------- 1 | defmodule NimbleTemplate.Templates.Phoenix.Api.Template do 2 | @moduledoc false 3 | 4 | alias NimbleTemplate.Addons.Phoenix.Api 5 | alias NimbleTemplate.Projects.Project 6 | 7 | def apply!(%Project{} = project) do 8 | apply_default_api_addons(project) 9 | end 10 | 11 | defp apply_default_api_addons(project) do 12 | project 13 | |> Api.Config.apply!() 14 | |> Api.EmptyBodyParamsPlug.apply!() 15 | |> Api.ParamsValidation.apply!() 16 | |> Api.ErrorView.apply!() 17 | |> Api.JsonApi.apply!() 18 | |> Api.FallbackController.apply!() 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /priv/templates/nimble_template/.github/PULL_REQUEST_TEMPLATE/RELEASE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | Link to the milestone on Github e.g. https://github.com/nimblehq/git-templates/milestone/41?closed=1 2 | or 3 | Link to the project management tool for the release 4 | 5 | ## Features 6 | 7 | Provide the ID and title of the issue in the section for each type (feature, chore and bug). The link is optional. 8 | 9 | - [ch1234] As a user, I can log in 10 | or 11 | - [[ch1234](https://github.com/nimblehq/git-templates/issues/1234)] As a user, I can log in 12 | 13 | ## Chores 14 | - Same structure as in ## Feature 15 | 16 | ## Bugs 17 | - Same structure as in ## Feature 18 | -------------------------------------------------------------------------------- /priv/templates/nimble_template/test/support/view_case.ex.eex: -------------------------------------------------------------------------------- 1 | defmodule <%= web_module %>.ViewCase do 2 | use ExUnit.CaseTemplate 3 | 4 | using do 5 | quote do 6 | import Mimic 7 | import Phoenix.View 8 | import <%= base_module %>.Factory 9 | import <%= web_module %>.ViewCase 10 | import Phoenix.ConnTest, only: [get: 2, bypass_through: 1] 11 | 12 | alias <%= web_module %>.Router.Helpers, as: Routes 13 | 14 | # The default endpoint for testing 15 | @endpoint <%= web_module %>.Endpoint 16 | end 17 | end 18 | 19 | setup do 20 | {:ok, conn: Phoenix.ConnTest.build_conn()} 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /priv/templates/nimble_template/.github/wiki/Icon-Sprite.md: -------------------------------------------------------------------------------- 1 | ## Generate/update the `icon-sprite.svg` file 2 | 3 | 1. Export SVG icon from Figma. 4 | 5 | 2. Add icon file to `priv/static/images/icons/` (without the `icon-` prefix) (e.g. `active.svg`). 6 | 7 | 3. Generate/Update the `icon-sprite.svg` by running this command: 8 | ```sh 9 | npm run svg-sprite.generate-icon --prefix assets 10 | ``` 11 | 12 | ## Using Icon Sprite in Template 13 | 14 | ```sh 15 | <%= icon_tag(@conn, "active", class: "something") %> 16 | 17 | # 18 | ``` 19 | -------------------------------------------------------------------------------- /priv/templates/nimble_template/lib/otp_app_web/plugs/check_empty_body_params_plug.ex.eex: -------------------------------------------------------------------------------- 1 | defmodule <%= web_module %>.CheckEmptyBodyParamsPlug do 2 | @behaviour Plug 3 | 4 | import Phoenix.Controller, only: [put_view: 2, render: 3] 5 | import Plug.Conn 6 | 7 | alias <%= web_module %>.Api.ErrorView 8 | 9 | def init(opts), do: opts 10 | 11 | def call(%{body_params: body_params, method: "POST"} = conn, _opts) 12 | when body_params == %{} do 13 | conn 14 | |> put_status(:bad_request) 15 | |> put_view(ErrorView) 16 | |> render("400.json", %{message: "Missing body params"}) 17 | |> halt() 18 | end 19 | 20 | def call(conn, _opts), do: conn 21 | end 22 | -------------------------------------------------------------------------------- /test/support/template_case.ex: -------------------------------------------------------------------------------- 1 | defmodule NimbleTemplate.TemplateCase do 2 | use ExUnit.CaseTemplate 3 | 4 | alias NimbleTemplate.Test.FileHelper 5 | 6 | using do 7 | quote do 8 | import NimbleTemplate.Test.FileHelper 9 | end 10 | end 11 | 12 | setup do 13 | test_project_path = FileHelper.parent_test_project_path() 14 | 15 | File.mkdir_p!(test_project_path) 16 | 17 | Mix.shell().cmd("cp ./* .tool-versions #{test_project_path} 2> /dev/null") 18 | Mix.shell().cmd("cp -r ./lib ./test #{test_project_path} 2> /dev/null") 19 | 20 | on_exit(fn -> 21 | File.rm_rf!(test_project_path) 22 | end) 23 | 24 | {:ok, test_project_path: test_project_path} 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /priv/templates/nimble_template/.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | Add the story URL here. Prefer the short link format, e.g. https://app.shortcut.com/acme/story/1234/ 2 | 3 | ## What happened 👀 4 | 5 | Provide a description of the **changes** this pull request brings to the codebase. Additionally, when the pull request is still being worked on, a checklist of the planned changes is welcome to track progress. 6 | 7 | ## Insight 📝 8 | 9 | Describe in detail why this solution is the most appropriate, which solution you tried but did not go with, and how to test the changes. References to relevant documentation are welcome as well. 10 | 11 | ## Proof Of Work 📹 12 | 13 | Show us the implementation: screenshots, GIFs, etc. 14 | -------------------------------------------------------------------------------- /lib/nimble_template/helpers/addon.ex: -------------------------------------------------------------------------------- 1 | defmodule NimbleTemplate.AddonHelper do 2 | alias NimbleTemplate.Projects.Project 3 | 4 | @spec install_addon_prompt(Project.t(), atom(), String.t() | nil) :: Project.t() 5 | def install_addon_prompt(project, addon, addon_label \\ nil) do 6 | if Mix.shell().yes?( 7 | "\nWould you like to add the #{build_addon_label(addon, addon_label)} addon?" 8 | ) do 9 | Project.prepend_optional_addon(project, addon) 10 | else 11 | project 12 | end 13 | end 14 | 15 | defp build_addon_label(addon, nil) do 16 | addon 17 | |> Module.split() 18 | |> List.last() 19 | end 20 | 21 | defp build_addon_label(_addon, addon_label), do: addon_label 22 | end 23 | -------------------------------------------------------------------------------- /lib/nimble_template/addons/variants/phoenix/web/esbuild.ex: -------------------------------------------------------------------------------- 1 | defmodule NimbleTemplate.Addons.Phoenix.Web.EsBuild do 2 | @moduledoc false 3 | 4 | use NimbleTemplate.Addons.Addon 5 | 6 | @impl true 7 | def do_apply!(%Project{} = project, _opts) do 8 | project 9 | |> edit_config!() 10 | |> edit_mix!() 11 | end 12 | 13 | defp edit_config!(%Project{} = project) do 14 | Generator.replace_content!("config/config.exs", "default: [", "app: [") 15 | 16 | Generator.replace_content!("config/dev.exs", "[:default", "[:app") 17 | 18 | project 19 | end 20 | 21 | defp edit_mix!(project) do 22 | Generator.replace_content!("mix.exs", "esbuild default", "esbuild app") 23 | 24 | project 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /priv/templates/nimble_template/coveralls.json.eex: -------------------------------------------------------------------------------- 1 | { 2 | "skip_files": [ 3 | "lib/mix", 4 | "lib/<%= otp_app %>/application.ex", 5 | "lib/<%= otp_app %>/release_tasks.ex", 6 | "lib/<%= otp_app %>_web.ex", 7 | "lib/<%= otp_app %>_mail.ex", 8 | "lib/<%= otp_app %>_web/endpoint.ex", 9 | "lib/<%= otp_app %>_web/telemetry.ex", 10 | "lib/<%= otp_app %>_web/channels/user_socket.ex", 11 | "lib/<%= otp_app %>_web/views/error_helpers.ex", 12 | "lib/<%= otp_app %>_web/controllers/api/fallback_controller.ex", 13 | "test/support" 14 | ], 15 | "coverage_options": { 16 | "minimum_coverage": <%= minimum_coverage %>, 17 | "html_filter_full_covered": <%= html_filter_full_covered %> 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /test/nimble_template/addons/variants/seeds_test.exs: -------------------------------------------------------------------------------- 1 | defmodule NimbleTemplate.Addons.Phoenix.SeedsTest do 2 | use NimbleTemplate.AddonCase, async: false 3 | 4 | describe "#apply!/2" do 5 | test "adds the condition into the seeds.exs file", %{ 6 | project: project, 7 | test_project_path: test_project_path 8 | } do 9 | in_test_project!(test_project_path, fn -> 10 | PhoenixAddons.Seeds.apply!(project) 11 | 12 | assert_file("priv/repo/seeds.exs", fn file -> 13 | assert file =~ """ 14 | if Mix.env() == :dev || System.get_env("ENABLE_DB_SEED") == "true" do 15 | end 16 | """ 17 | end) 18 | end) 19 | end 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /lib/nimble_template/addons/variants/phoenix/web/assets.ex: -------------------------------------------------------------------------------- 1 | defmodule NimbleTemplate.Addons.Phoenix.Web.Assets do 2 | @moduledoc false 3 | 4 | use NimbleTemplate.Addons.Addon 5 | 6 | @impl true 7 | def do_apply!(%Project{} = project, _opts) do 8 | edit_files!(project) 9 | end 10 | 11 | defp edit_files!(%Project{} = project) do 12 | enable_gzip_for_static_assets!(project) 13 | 14 | project 15 | end 16 | 17 | defp enable_gzip_for_static_assets!(%Project{web_path: web_path} = project) do 18 | Generator.replace_content!( 19 | "#{web_path}/endpoint.ex", 20 | """ 21 | gzip: false, 22 | """, 23 | """ 24 | gzip: true, 25 | """ 26 | ) 27 | 28 | project 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /priv/templates/nimble_template/lib/otp_app_web/helpers/icon_helper.ex.eex: -------------------------------------------------------------------------------- 1 | defmodule <%= web_module %>.IconHelper do 2 | @moduledoc """ 3 | Generate the SVG icon tag 4 | """ 5 | 6 | use Phoenix.HTML 7 | 8 | alias <%= web_module %>.Router.Helpers, as: Routes 9 | 10 | @svg_shape_id_prefix "icon-priv--static--images--icons--" 11 | 12 | def icon_tag(conn, icon_file_name, opts \\ []) do 13 | classes = "icon " <> Keyword.get(opts, :class, "") 14 | 15 | content_tag(:svg, class: classes) do 16 | tag(:use, 17 | "xlink:href": 18 | Routes.static_path( 19 | conn, 20 | "/images/icon-sprite.svg#" <> @svg_shape_id_prefix <> icon_file_name 21 | ) 22 | ) 23 | end 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /priv/templates/nimble_template/lib/otp_app_web/controllers/api/fallback_controller.ex.eex: -------------------------------------------------------------------------------- 1 | defmodule <%= web_module %>.Api.FallbackController do 2 | use Phoenix.Controller 3 | 4 | alias Ecto.Changeset 5 | alias <%= web_module %>.Api.ErrorView 6 | 7 | def call(conn, {:error, :invalid_params, %Changeset{valid?: false} = changeset}) do 8 | conn 9 | |> put_status(:unprocessable_entity) 10 | |> put_view(ErrorView) 11 | |> render("error.json", %{code: :validation_error, changeset: changeset}) 12 | end 13 | 14 | def call(conn, {:error, %Changeset{valid?: false} = changeset}) do 15 | conn 16 | |> put_status(:unprocessable_entity) 17 | |> put_view(ErrorView) 18 | |> render("error.json", %{code: :unprocessable_entity, changeset: changeset}) 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /test/nimble_template/addons/faker_test.exs: -------------------------------------------------------------------------------- 1 | defmodule NimbleTemplate.Addons.FakerTest do 2 | use NimbleTemplate.AddonCase, async: false 3 | 4 | describe "#apply!/2" do 5 | @describetag mock_latest_package_versions: [{:faker, "0.17.0"}] 6 | 7 | test "injects faker to mix dependency", %{ 8 | project: project, 9 | test_project_path: test_project_path 10 | } do 11 | in_test_project!(test_project_path, fn -> 12 | Addons.Faker.apply!(project) 13 | 14 | assert_file("mix.exs", fn file -> 15 | assert file =~ """ 16 | defp deps do 17 | [ 18 | {:faker, "~> 0.17.0", [only: [:dev, :test], runtime: false]}, 19 | """ 20 | end) 21 | end) 22 | end 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /test/nimble_template/addons/dialyxir_test.exs: -------------------------------------------------------------------------------- 1 | defmodule NimbleTemplate.Addons.DialyxirTest do 2 | use NimbleTemplate.AddonCase, async: false 3 | 4 | describe "#apply!/2" do 5 | @describetag mock_latest_package_versions: [{:dialyxir, "1.0"}] 6 | 7 | test "injects dialyxir to mix dependency", %{ 8 | project: project, 9 | test_project_path: test_project_path 10 | } do 11 | in_test_project!(test_project_path, fn -> 12 | Addons.Dialyxir.apply!(project) 13 | 14 | assert_file("mix.exs", fn file -> 15 | assert file =~ """ 16 | defp deps do 17 | [ 18 | {:dialyxir, "~> 1.0", [only: [:dev], runtime: false]}, 19 | """ 20 | end) 21 | end) 22 | end 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /lib/nimble_template/helpers/dependency.ex: -------------------------------------------------------------------------------- 1 | defmodule NimbleTemplate.DependencyHelper do 2 | alias NimbleTemplate.Generator 3 | 4 | def order_dependencies! do 5 | file_content = File.read!("mix.exs") 6 | 7 | dependencies = extract_dependencies(file_content) 8 | 9 | ordered_dependencies = 10 | dependencies 11 | |> String.split(",\n") 12 | |> Enum.sort() 13 | |> Enum.join(",\n") 14 | 15 | Generator.replace_content!("mix.exs", dependencies, ordered_dependencies) 16 | end 17 | 18 | defp extract_dependencies(contents) do 19 | [_, deps_with_file_footer] = 20 | String.split(contents, """ 21 | defp deps do 22 | [ 23 | """) 24 | 25 | [deps | _footer] = String.split(deps_with_file_footer, "\n ]\n end") 26 | 27 | deps 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /lib/nimble_template/addons/test_interactive.ex: -------------------------------------------------------------------------------- 1 | defmodule NimbleTemplate.Addons.TestInteractive do 2 | @moduledoc false 3 | 4 | use NimbleTemplate.Addons.Addon 5 | 6 | @impl true 7 | def do_apply!(%Project{} = project, _opts) do 8 | latest_version = latest_package_version(:mix_test_interactive) 9 | 10 | Generator.inject_mix_dependency!( 11 | {:mix_test_interactive, latest_version, only: :dev, runtime: false} 12 | ) 13 | 14 | add_dev_config!(project) 15 | end 16 | 17 | defp add_dev_config!(%Project{mix_project?: true} = project), do: project 18 | 19 | defp add_dev_config!(project) do 20 | Generator.append_content!( 21 | "config/dev.exs", 22 | """ 23 | config :mix_test_interactive, 24 | clear: true 25 | """ 26 | ) 27 | 28 | project 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /priv/templates/nimble_template/test/otp_app_web/views/error_helpers_test.exs: -------------------------------------------------------------------------------- 1 | defmodule <%= web_module %>.ErrorHelpersTest do 2 | use <%= web_module %>.ViewCase, async: true 3 | 4 | alias <%= web_module %>.ErrorHelpers 5 | 6 | describe "status_code_from_template/1" do 7 | test "given 404.json, returns :not_found error code" do 8 | assert ErrorHelpers.status_code_from_template("404.json") == :not_found 9 | end 10 | 11 | test "given 500.json, returns :internal_server_error error code" do 12 | assert ErrorHelpers.status_code_from_template("500.json") == :internal_server_error 13 | end 14 | 15 | test "given non-existing error number, returns :internal_server_error error code" do 16 | assert ErrorHelpers.status_code_from_template("99999.json") == :internal_server_error 17 | end 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /.github/workflows/apply_live_variant.yml: -------------------------------------------------------------------------------- 1 | name: Apply Live variant 2 | 3 | on: push 4 | 5 | jobs: 6 | standard_project: 7 | name: Test on a Standard Live project 8 | uses: ./.github/workflows/reusable_phoenix_project.yml 9 | with: 10 | new_project_options: "" 11 | variant: "live" 12 | 13 | long_custom_name_live_project: 14 | name: Test on a long custom name Live project 15 | uses: ./.github/workflows/reusable_phoenix_project.yml 16 | with: 17 | new_project_options: "--module=SampleCustomModule --app=sample_custom_app" 18 | variant: "live" 19 | 20 | short_custom_name_live_project: 21 | name: Test on a short custom name Live project 22 | uses: ./.github/workflows/reusable_phoenix_project.yml 23 | with: 24 | new_project_options: "--module=Z --app=z" 25 | variant: "live" 26 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # The directory Mix will write compiled artifacts to. 2 | /_build/ 3 | 4 | /.elixir_ls/ 5 | 6 | # If you run "mix test --cover", coverage assets end up here. 7 | /cover/ 8 | 9 | # The directory Mix downloads your dependencies sources to. 10 | /deps/ 11 | 12 | # Where third-party dependencies like ExDoc output generated docs. 13 | /doc/ 14 | 15 | # Ignore .fetch files in case you like to edit your project deps locally. 16 | /.fetch 17 | 18 | # If the VM crashes, it generates a dump, let's ignore it too. 19 | erl_crash.dump 20 | 21 | # Also ignore archive artifacts (built via "mix archive.build"). 22 | *.ez 23 | 24 | # Ignore package tarball (built via "mix hex.build"). 25 | nimble_template-*.tar 26 | 27 | # Ignore folder information and IDE-specific files 28 | .DS_Store 29 | .idea/* 30 | nimble_template.iml 31 | 32 | **/tmp 33 | -------------------------------------------------------------------------------- /priv/templates/nimble_template/assets/nimble_css/mixins/_text-ellipsis.scss: -------------------------------------------------------------------------------- 1 | $font-size-base: 1rem !default; 2 | $line-height-base: 1.5 !default; 3 | 4 | @mixin text-ellipsis( 5 | $lines: 2, 6 | $font-size: $font-size-base, 7 | $line-height: $line-height-base 8 | ) { 9 | /* stylelint-disable property-no-vendor-prefix */ 10 | /* stylelint-disable value-no-vendor-prefix */ 11 | -webkit-box-orient: vertical; 12 | display: -webkit-box; 13 | -webkit-line-clamp: $lines; 14 | max-height: calc(#{$line-height} * #{$font-size} * #{$lines}); 15 | overflow: hidden; 16 | word-break: break-word; 17 | /* stylelint-enable property-no-vendor-prefix */ 18 | /* stylelint-enable value-no-vendor-prefix */ 19 | 20 | @include media-breakpoint-up(md) { 21 | max-height: calc(#{$line-height} * #{$font-size} * #{$lines}); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /test/nimble_template/addons/variants/phoenix/api/error_view_test.exs: -------------------------------------------------------------------------------- 1 | defmodule NimbleTemplate.Addons.Phoenix.Api.ErrorViewTest do 2 | use NimbleTemplate.AddonCase, async: false 3 | 4 | describe "#apply!/2" do 5 | test "copies the error view files", %{ 6 | project: project, 7 | test_project_path: project_path 8 | } do 9 | in_test_project!(project_path, fn -> 10 | ApiAddons.ErrorView.apply!(project) 11 | 12 | assert_file("lib/nimble_template_web/views/error_helpers.ex") 13 | assert_file("lib/nimble_template_web/views/api/error_view.ex") 14 | assert_file("test/nimble_template_web/views/error_helpers_test.exs") 15 | assert_file("test/nimble_template_web/views/api/error_view_test.exs") 16 | assert_file("test/support/view_case.ex") 17 | end) 18 | end 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /lib/nimble_template/addons/variants/phoenix/web/heex_formatter.ex: -------------------------------------------------------------------------------- 1 | defmodule NimbleTemplate.Addons.Phoenix.Web.HeexFormatter do 2 | @moduledoc false 3 | 4 | use NimbleTemplate.Addons.Addon 5 | 6 | @impl true 7 | def do_apply!(%Project{} = project, _opts) do 8 | Generator.replace_content!(".formatter.exs", "\"*.{ex,exs}\"", "\"*.{heex,ex,exs}\"") 9 | 10 | Generator.replace_content!( 11 | ".formatter.exs", 12 | "\"{config,lib,test}/**/*.{ex,exs}\"", 13 | "\"{config,lib,test}/**/*.{heex,ex,exs}\"" 14 | ) 15 | 16 | Generator.replace_content!( 17 | ".formatter.exs", 18 | """ 19 | import_deps: [:ecto, :phoenix], 20 | """, 21 | """ 22 | import_deps: [:ecto, :phoenix], 23 | plugins: [Phoenix.LiveView.HTMLFormatter], 24 | """ 25 | ) 26 | 27 | project 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /.github/workflows/apply_web_variant.yml: -------------------------------------------------------------------------------- 1 | name: Apply Web variant 2 | 3 | on: push 4 | 5 | jobs: 6 | standard_project: 7 | name: Test on a Standard Web project 8 | uses: ./.github/workflows/reusable_phoenix_project.yml 9 | with: 10 | new_project_options: "--no-live" 11 | variant: "web" 12 | 13 | long_custom_name_web_project: 14 | name: Test on a long custom name Web project 15 | uses: ./.github/workflows/reusable_phoenix_project.yml 16 | with: 17 | new_project_options: "--no-live --module=SampleCustomModule --app=sample_custom_app" 18 | variant: "web" 19 | 20 | short_custom_name_web_project: 21 | name: Test on a short custom name Web project 22 | uses: ./.github/workflows/reusable_phoenix_project.yml 23 | with: 24 | new_project_options: "--no-live --module=Z --app=z" 25 | variant: "web" 26 | -------------------------------------------------------------------------------- /lib/nimble_template/addons/variants/phoenix/gettext.ex: -------------------------------------------------------------------------------- 1 | defmodule NimbleTemplate.Addons.Phoenix.Gettext do 2 | @moduledoc false 3 | 4 | use NimbleTemplate.Addons.Addon 5 | 6 | @impl true 7 | def do_apply!(%Project{} = project, _opts) do 8 | edit_mix!(project) 9 | end 10 | 11 | defp edit_mix!(%Project{} = project) do 12 | Generator.replace_content!( 13 | "mix.exs", 14 | """ 15 | codebase: [ 16 | """, 17 | """ 18 | codebase: [ 19 | "gettext.extract --check-up-to-date", 20 | """ 21 | ) 22 | 23 | Generator.replace_content!( 24 | "mix.exs", 25 | """ 26 | "codebase.fix": [ 27 | """, 28 | """ 29 | "codebase.fix": [ 30 | "gettext.extract --merge --no-fuzzy", 31 | """ 32 | ) 33 | 34 | project 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /test/nimble_template/addons/variants/phoenix/web/hex_formatter_test.exs: -------------------------------------------------------------------------------- 1 | defmodule NimbleTemplate.Addons.Phoenix.Web.HeexFormatterTest do 2 | use NimbleTemplate.AddonCase, async: false 3 | 4 | describe "#apply!/2" do 5 | test "adds the HTMLFormatter into the .formatter.exs", %{ 6 | project: project, 7 | test_project_path: test_project_path 8 | } do 9 | in_test_project!(test_project_path, fn -> 10 | WebAddons.HeexFormatter.apply!(project) 11 | 12 | assert_file(".formatter.exs", fn file -> 13 | assert file =~ """ 14 | plugins: [Phoenix.LiveView.HTMLFormatter], 15 | """ 16 | 17 | assert file =~ """ 18 | inputs: ["*.{heex,ex,exs}", "priv/*/seeds.exs", "{config,lib,test}/**/*.{heex,ex,exs}"], 19 | """ 20 | end) 21 | end) 22 | end 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /lib/nimble_template/addons/asdf_tool_version.ex: -------------------------------------------------------------------------------- 1 | defmodule NimbleTemplate.Addons.AsdfToolVersion do 2 | @moduledoc false 3 | 4 | use NimbleTemplate.Addons.Addon 5 | 6 | @impl true 7 | def do_apply!( 8 | %Project{ 9 | erlang_version: erlang_version, 10 | elixir_asdf_version: elixir_asdf_version, 11 | node_asdf_version: node_asdf_version, 12 | web_project?: web_project? 13 | } = project, 14 | _opts 15 | ) do 16 | Generator.copy_file!([{:eex, ".tool-versions.eex", ".tool-versions"}], 17 | erlang_version: erlang_version, 18 | elixir_asdf_version: elixir_asdf_version 19 | ) 20 | 21 | if web_project? do 22 | Generator.append_content!( 23 | ".tool-versions", 24 | """ 25 | nodejs #{node_asdf_version} 26 | """ 27 | ) 28 | end 29 | 30 | project 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /.github/wiki/Release.md: -------------------------------------------------------------------------------- 1 | 1. Set the `HEX_API_KEY` as a Github secret (skip this step if it has been done). 2 | 3 | 2. Visit the [Bump Version Github Action Workflow](https://github.com/nimblehq/elixir-templates/actions/workflows/bump_version.yml). 4 | 5 | 3. Choose `develop` branch. 6 | 7 | 4. Trigger the workflow with the next ``. That workflow will create a PR into the `develop` branch with the title `[Chore] Bump version to `. 8 | 9 | ![bump-version-workflow](assets/images/bump-version-workflow.png) 10 | 11 | 5. Merge the Bump version PR above into the `develop` branch. 12 | 13 | 6. Create the `release/` from the `develop` branch, pointing to the `main` branch. 14 | 15 | 7. Create a Release from the `main` branch. 16 | 17 | 8. Once the release is published, Github Action automatically publishes the template to [https://hex.pm/packages/nimble_template](https://hex.pm/packages/nimble_template). 18 | -------------------------------------------------------------------------------- /test/nimble_template/addons/variants/gettext_test.exs: -------------------------------------------------------------------------------- 1 | defmodule NimbleTemplate.Addons.Phoenix.GettextTest do 2 | use NimbleTemplate.AddonCase, async: false 3 | 4 | describe "#apply!/2" do 5 | @describetag required_addons: [:TestEnv] 6 | 7 | test "injects gettext.extract-and-merge command to mix aliases", %{ 8 | project: project, 9 | test_project_path: test_project_path 10 | } do 11 | in_test_project!(test_project_path, fn -> 12 | PhoenixAddons.Gettext.apply!(project) 13 | 14 | assert_file("mix.exs", fn file -> 15 | assert file =~ """ 16 | codebase: [ 17 | "gettext.extract --check-up-to-date", 18 | """ 19 | 20 | assert file =~ """ 21 | "codebase.fix": [ 22 | "gettext.extract --merge --no-fuzzy", 23 | """ 24 | end) 25 | end) 26 | end 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /.github/wiki/Docker.md: -------------------------------------------------------------------------------- 1 | Prefer to use the image from the Hexpm team instead of the Docker team, the reason below 2 | was mentioned on the [Elixir Forums](https://elixirforum.com/t/yet-another-elixir-and-erlang-docker-image/28740): 3 | 4 | - Image tags are not immutable. 5 | - Delay in the availability of new versions. 6 | - Cannot pick the combination of Elixir and Erlang versions you want. 7 | 8 | Refer: 9 | 10 | - [hexpm/elixir](https://hub.docker.com/r/hexpm/elixir) 11 | - [hexpm/erlang](https://hub.docker.com/r/hexpm/erlang) 12 | 13 | Example Dockerfile: 14 | 15 | ```dockerfile 16 | FROM hexpm/elixir:1.11.0-erlang-23.1.1-alpine-3.12.0 AS build 17 | 18 | .... 19 | FROM alpine:3.12.0 AS app 20 | ... 21 | 22 | COPY --from=build --chown=nobody:nobody /app/_build/prod/rel/app ./ 23 | ... 24 | ``` 25 | 26 | Based on the image tag, it's clear which versions we use in the Build Release step 27 | 28 | - Elixir: 1.11.0 29 | - Erlang: 23.1.1 30 | - Alpine: 3.12.0 31 | 32 | Easily we can choose the OS to run the app on, that is `alpine:3.12.0` 33 | -------------------------------------------------------------------------------- /lib/mix/tasks/nimble_template.bump_version.ex: -------------------------------------------------------------------------------- 1 | defmodule Mix.Tasks.NimbleTemplate.BumpVersion do 2 | @shortdoc "Bump the template into specific version." 3 | 4 | @moduledoc """ 5 | #{@shortdoc} 6 | 7 | - Hex package: https://hex.pm/packages/nimble_template 8 | - Github: https://github.com/nimblehq/elixir-templates 9 | 10 | # Usage 11 | 12 | - mix help nimble_template.bump_version # Print help 13 | - mix nimble_template.bump_version [new_version] # Bump the template version to the [new_version]. 14 | """ 15 | 16 | use Mix.Task 17 | 18 | alias NimbleTemplate.Version 19 | 20 | def run(args) do 21 | new_version = parse_opts(args) 22 | 23 | Version.bump!(new_version) 24 | end 25 | 26 | defp parse_opts(args) do 27 | case OptionParser.parse(args, strict: []) do 28 | {[], [new_version], []} -> 29 | new_version 30 | 31 | _other -> 32 | Mix.raise( 33 | "Invalid command. Check `mix help nimble_template.bump_version` for more information." 34 | ) 35 | end 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /lib/nimble_template/addons/variants/phoenix/web/nimble_css.ex: -------------------------------------------------------------------------------- 1 | defmodule NimbleTemplate.Addons.Phoenix.Web.NimbleCSS do 2 | @moduledoc false 3 | 4 | use NimbleTemplate.Addons.Addon 5 | 6 | @impl true 7 | def do_apply!(%Project{} = project, _opts) do 8 | project 9 | |> remove_default_phoenix_structure!() 10 | |> copy_nimble_structure!() 11 | |> edit_style_lint_rc!() 12 | end 13 | 14 | defp remove_default_phoenix_structure!(project) do 15 | File.rm_rf!("assets/css") 16 | 17 | project 18 | end 19 | 20 | defp copy_nimble_structure!(project) do 21 | Generator.copy_directory!("assets/nimble_css", "assets/css") 22 | 23 | project 24 | end 25 | 26 | defp edit_style_lint_rc!(project) do 27 | Generator.replace_content!( 28 | "assets/.stylelintrc.json", 29 | """ 30 | "ignoreFiles": [ 31 | "css/app.css", 32 | "css/phoenix.css" 33 | ], 34 | """, 35 | """ 36 | "ignoreFiles": [], 37 | """ 38 | ) 39 | 40 | project 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /priv/templates/nimble_template/test/otp_app_web/helpers/icon_helper_test.exs.eex: -------------------------------------------------------------------------------- 1 | defmodule <%= web_module %>.IconHelperTest do 2 | use <%= web_module %>.ConnCase, async: true 3 | 4 | import Phoenix.HTML, only: [safe_to_string: 1] 5 | 6 | alias <%= web_module %>.IconHelper 7 | 8 | describe "icon_tag/3" do 9 | test "renders a svg icon" do 10 | first_svg_icon = 11 | <%= web_module %>.Endpoint 12 | |> IconHelper.icon_tag("active", class: "customize-icon-class") 13 | |> safe_to_string() 14 | 15 | second_svg_icon = 16 | <%= web_module %>.Endpoint 17 | |> IconHelper.icon_tag("icon-lock") 18 | |> safe_to_string() 19 | 20 | assert first_svg_icon == 21 | "" 22 | 23 | assert second_svg_icon == 24 | "" 25 | end 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /priv/templates/nimble_template/lib/otp_app/release_tasks.ex.eex: -------------------------------------------------------------------------------- 1 | defmodule <%= base_module %>.ReleaseTasks do 2 | @app :<%= otp_app %> 3 | 4 | def migrate do 5 | load_app() 6 | 7 | for repo <- repos() do 8 | schema_migrations = Ecto.Migrator.migrations_path(repo, "migrations") 9 | data_migrations = Ecto.Migrator.migrations_path(repo, "data_migrations") 10 | 11 | {:ok, _, _} = 12 | Ecto.Migrator.with_repo( 13 | repo, 14 | &Ecto.Migrator.run(&1, [schema_migrations, data_migrations], :up, all: true) 15 | ) 16 | end 17 | end 18 | 19 | def rollback(repo, version) do 20 | load_app() 21 | {:ok, _, _} = Ecto.Migrator.with_repo(repo, &Ecto.Migrator.run(&1, :down, to: version)) 22 | end 23 | 24 | def create_db(repo) do 25 | load_app() 26 | 27 | repo.__adapter__.storage_up(repo.config) 28 | end 29 | 30 | defp repos do 31 | Application.fetch_env!(@app, :ecto_repos) 32 | end 33 | 34 | defp load_app do 35 | Application.ensure_all_started(:ssl) 36 | 37 | Application.load(@app) 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /lib/nimble_template/addons/variants/phoenix/web/core_js.ex: -------------------------------------------------------------------------------- 1 | defmodule NimbleTemplate.Addons.Phoenix.Web.CoreJS do 2 | @moduledoc false 3 | 4 | use NimbleTemplate.Addons.Addon 5 | 6 | @impl true 7 | def do_apply!(%Project{} = project, _opts) do 8 | edit_files!(project) 9 | end 10 | 11 | def edit_package_json!(%Project{} = project) do 12 | Generator.replace_content!( 13 | "assets/package.json", 14 | """ 15 | "dependencies": { 16 | """, 17 | """ 18 | "dependencies": { 19 | "core-js": "3.22.0" 20 | """ 21 | ) 22 | 23 | project 24 | end 25 | 26 | def edit_app_js!(project) do 27 | Generator.replace_content!( 28 | "assets/js/app.js", 29 | """ 30 | import "phoenix_html" 31 | """, 32 | """ 33 | // CoreJS 34 | import "core-js/stable" 35 | 36 | import "phoenix_html" 37 | """ 38 | ) 39 | 40 | project 41 | end 42 | 43 | defp edit_files!(%Project{} = project) do 44 | project 45 | |> edit_package_json!() 46 | |> edit_app_js!() 47 | end 48 | end 49 | -------------------------------------------------------------------------------- /priv/templates/nimble_template/.github/wiki/Getting-Started.md.mix.eex: -------------------------------------------------------------------------------- 1 | ### Erlang & Elixir 2 | 3 | - Erlang <%= erlang_version %> 4 | 5 | - Elixir <%= elixir_version %> 6 | 7 | - Recommended version manager. 8 | 9 | - [asdf](https://github.com/asdf-vm/asdf) 10 | - [asdf-erlang](https://github.com/asdf-vm/asdf-erlang) 11 | - [asdf-elixir](https://github.com/asdf-vm/asdf-elixir) 12 | 13 | ### Development 14 | 15 | - Install Elixir dependencies: 16 | 17 | ```sh 18 | mix deps.get 19 | ``` 20 | 21 | - Run all tests: 22 | 23 | ```sh 24 | mix test 25 | ``` 26 | 27 | - Run tests with `watch` option: 28 | 29 | ```sh 30 | mix test.interactive # Watch the whole test suite 31 | 32 | mix test.interactive test_file_path # Watch a specific test file 33 | 34 | mix test.interactive test_file_path:line_number # Watch a specific test line number 35 | ``` 36 | 37 | - Run all lint: 38 | 39 | ```sh 40 | mix codebase 41 | ``` 42 | 43 | - Fix all lint: 44 | 45 | ```sh 46 | mix codebase.fix 47 | ``` 48 | 49 | - Test coverage: 50 | 51 | ```sh 52 | mix coverage 53 | ``` 54 | -------------------------------------------------------------------------------- /priv/templates/nimble_template/.github/workflows/deploy_heroku.yml: -------------------------------------------------------------------------------- 1 | name: Deploy to Heroku 2 | 3 | on: 4 | workflow_run: 5 | workflows: 6 | - Test 7 | branches: 8 | - develop 9 | types: 10 | - completed 11 | workflow_dispatch: 12 | 13 | env: 14 | # To API Key is used by Heroku CLI to authenticate 15 | HEROKU_API_KEY: ${{ secrets.HEROKU_API_KEY }} 16 | HEROKU_APP_NAME: ${{ secrets.HEROKU_APP_NAME }} 17 | 18 | jobs: 19 | deploy: 20 | name: Deploy to Heroku 21 | runs-on: ubuntu-latest 22 | 23 | steps: 24 | - name: Cancel previous runs 25 | uses: styfle/cancel-workflow-action@0.9.1 26 | with: 27 | access_token: ${{ github.token }} 28 | 29 | - name: Checkout repository 30 | uses: actions/checkout@v3 31 | 32 | - name: Login to Heroku Container registry 33 | run: heroku container:login 34 | 35 | - name: Build and push 36 | run: heroku container:push -a ${{ env.HEROKU_APP_NAME }} web 37 | 38 | - name: Release 39 | run: heroku container:release -a ${{ env.HEROKU_APP_NAME }} web 40 | -------------------------------------------------------------------------------- /test/nimble_template/helpers/dependency_test.exs: -------------------------------------------------------------------------------- 1 | defmodule NimbleTemplate.DependencyTest do 2 | use NimbleTemplate.AddonCase, async: false 3 | 4 | alias NimbleTemplate.{Addons, DependencyHelper} 5 | alias NimbleTemplate.Addons.Phoenix, as: PhoenixAddons 6 | 7 | describe "order_dependencies!/0" do 8 | @describetag mock_latest_package_versions: [{:exvcr, "0.12.2"}, {:mimic, "1.3.1"}] 9 | 10 | test "orders dependencies in alphabetical order", %{ 11 | project: project, 12 | test_project_path: test_project_path 13 | } do 14 | in_test_project!(test_project_path, fn -> 15 | PhoenixAddons.ExVCR.apply!(project) 16 | Addons.Mimic.apply!(project) 17 | 18 | # Unordered mix dependencies 19 | assert_file("mix.exs", fn file -> 20 | assert file =~ ~r/(:mimic).*(:exvcr)/s 21 | end) 22 | 23 | DependencyHelper.order_dependencies!() 24 | 25 | # Ordered mix dependencies 26 | assert_file("mix.exs", fn file -> 27 | assert file =~ ~r/(:exvcr).*(:mimic)/s 28 | end) 29 | end) 30 | end 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014-2021 Nimble. 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /test/nimble_template/addons/variants/phoenix/api/json_api_test.exs: -------------------------------------------------------------------------------- 1 | defmodule NimbleTemplate.Addons.Phoenix.Api.JsonApiTest do 2 | use NimbleTemplate.AddonCase, async: false 3 | 4 | describe "#apply!/2" do 5 | @describetag mock_latest_package_versions: [{:jsonapi, "1.3.0"}] 6 | 7 | test "injects jsonapi to mix dependency", %{ 8 | project: project, 9 | test_project_path: test_project_path 10 | } do 11 | in_test_project!(test_project_path, fn -> 12 | ApiAddons.JsonApi.apply!(project) 13 | 14 | assert_file("mix.exs", fn file -> 15 | assert file =~ "{:jsonapi, \"~> 1.3.0\"}" 16 | end) 17 | end) 18 | end 19 | 20 | test "adds config for jsonapi in config/config.exs", %{ 21 | project: project, 22 | test_project_path: test_project_path 23 | } do 24 | in_test_project!(test_project_path, fn -> 25 | ApiAddons.JsonApi.apply!(project) 26 | 27 | assert_file("config/config.exs", fn file -> 28 | assert file =~ "config :jsonapi, remove_links: true" 29 | end) 30 | end) 31 | end 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /test/nimble_template/addons/variants/phoenix/api/fallback_controller_test.exs: -------------------------------------------------------------------------------- 1 | defmodule NimbleTemplate.Addons.Phoenix.Api.FallbackControllerTest do 2 | use NimbleTemplate.AddonCase, async: false 3 | 4 | describe "#apply!/2" do 5 | @describetag required_addons: [:"Phoenix.Api.ParamsValidation"] 6 | 7 | test "copies the FallbackController module", %{ 8 | project: project, 9 | test_project_path: project_path 10 | } do 11 | in_test_project!(project_path, fn -> 12 | ApiAddons.FallbackController.apply!(project) 13 | 14 | assert_file("lib/nimble_template_web/controllers/api/fallback_controller.ex") 15 | end) 16 | end 17 | 18 | test "adds FallbackController alias into the web entry_point", %{ 19 | project: project, 20 | test_project_path: project_path 21 | } do 22 | in_test_project!(project_path, fn -> 23 | ApiAddons.FallbackController.apply!(project) 24 | 25 | assert_file("lib/nimble_template_web.ex", fn file -> 26 | assert file =~ "action_fallback NimbleTemplateWeb.Api.FallbackController" 27 | end) 28 | end) 29 | end 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /lib/nimble_template/addons/readme.ex: -------------------------------------------------------------------------------- 1 | defmodule NimbleTemplate.Addons.Readme do 2 | @moduledoc false 3 | 4 | use NimbleTemplate.Addons.Addon 5 | 6 | @impl true 7 | def do_apply!(%Project{} = project, _opts) do 8 | project 9 | |> delete_files!() 10 | |> copy_files!() 11 | end 12 | 13 | def delete_files!(project) do 14 | File.rm!("README.md") 15 | 16 | project 17 | end 18 | 19 | defp copy_files!( 20 | %Project{ 21 | web_project?: web_project?, 22 | mix_project?: mix_project?, 23 | erlang_version: erlang_version, 24 | elixir_version: elixir_version, 25 | node_asdf_version: node_asdf_version 26 | } = project 27 | ) do 28 | template_file_path = 29 | if mix_project? do 30 | "README.md.mix.eex" 31 | else 32 | "README.md.eex" 33 | end 34 | 35 | Generator.copy_file!([{:eex, template_file_path, "README.md"}], 36 | erlang_version: erlang_version, 37 | elixir_version: elixir_version, 38 | node_asdf_version: node_asdf_version, 39 | web_project?: web_project? 40 | ) 41 | 42 | project 43 | end 44 | end 45 | -------------------------------------------------------------------------------- /lib/nimble_template/addons/variants/phoenix/api/json_api.ex: -------------------------------------------------------------------------------- 1 | defmodule NimbleTemplate.Addons.Phoenix.Api.JsonApi do 2 | @moduledoc false 3 | 4 | use NimbleTemplate.Addons.Addon 5 | 6 | @impl true 7 | def do_apply!(%Project{} = project, _opts) do 8 | edit_files!(project) 9 | end 10 | 11 | def inject_mix_dependency!(project) do 12 | Generator.inject_mix_dependency!({:jsonapi, latest_package_version(:jsonapi)}) 13 | 14 | project 15 | end 16 | 17 | def edit_config!(project) do 18 | Generator.replace_content!( 19 | "config/config.exs", 20 | """ 21 | # Import environment specific config. This must remain at the bottom 22 | # of this file so it overrides the configuration defined above. 23 | """, 24 | """ 25 | config :jsonapi, remove_links: true 26 | 27 | # Import environment specific config. This must remain at the bottom 28 | # of this file so it overrides the configuration defined above. 29 | """ 30 | ) 31 | 32 | project 33 | end 34 | 35 | defp edit_files!(%Project{} = project) do 36 | project 37 | |> inject_mix_dependency!() 38 | |> edit_config!() 39 | end 40 | end 41 | -------------------------------------------------------------------------------- /lib/nimble_template/addons/variants/phoenix/web/sobelow.ex: -------------------------------------------------------------------------------- 1 | defmodule NimbleTemplate.Addons.Phoenix.Web.Sobelow do 2 | @moduledoc false 3 | 4 | use NimbleTemplate.Addons.Addon 5 | 6 | @impl true 7 | def do_apply!(%Project{} = project, _opts) do 8 | project 9 | |> copy_files!() 10 | |> edit_files!() 11 | end 12 | 13 | defp copy_files!(%Project{} = project) do 14 | Generator.copy_file!([{:text, ".sobelow-conf", ".sobelow-conf"}]) 15 | 16 | project 17 | end 18 | 19 | defp edit_files!(%Project{} = project) do 20 | project 21 | |> inject_mix_dependency!() 22 | |> edit_mix!() 23 | end 24 | 25 | defp inject_mix_dependency!(%Project{} = project) do 26 | Generator.inject_mix_dependency!( 27 | {:sobelow, latest_package_version(:sobelow), only: [:dev, :test], runtime: false} 28 | ) 29 | 30 | project 31 | end 32 | 33 | defp edit_mix!(%Project{} = project) do 34 | Generator.replace_content!( 35 | "mix.exs", 36 | """ 37 | codebase: [ 38 | """, 39 | """ 40 | codebase: [ 41 | "sobelow --config", 42 | """ 43 | ) 44 | 45 | project 46 | end 47 | end 48 | -------------------------------------------------------------------------------- /test/nimble_template/addons/iex_test.exs: -------------------------------------------------------------------------------- 1 | defmodule NimbleTemplate.Addons.IexTest do 2 | use NimbleTemplate.AddonCase, async: false 3 | 4 | describe "#apply!/2" do 5 | test "add the .iex.exs file with the Repo alias", %{ 6 | project: project, 7 | test_project_path: test_project_path 8 | } do 9 | in_test_project!(test_project_path, fn -> 10 | Addons.Iex.apply!(project) 11 | 12 | assert_file(".iex.exs", fn file -> 13 | assert file =~ """ 14 | alias NimbleTemplate.Repo 15 | """ 16 | end) 17 | end) 18 | end 19 | end 20 | 21 | describe "#apply!/2 with mix_project" do 22 | @describetag mix_project?: true 23 | 24 | test "does not include the Repo alias in .iex.exs file", %{ 25 | project: project, 26 | test_project_path: test_project_path 27 | } do 28 | in_test_project!(test_project_path, fn -> 29 | Addons.Iex.apply!(project) 30 | 31 | assert_file(".iex.exs", fn file -> 32 | refute file =~ """ 33 | alias NimbleTemplate.Repo 34 | """ 35 | end) 36 | end) 37 | end 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /test/support/helpers/file_helper.ex: -------------------------------------------------------------------------------- 1 | defmodule NimbleTemplate.Test.FileHelper do 2 | import ExUnit.Assertions 3 | 4 | # ATTENTION: File.cd! doesn't support `async: true`, the test will fail randomly in async mode 5 | # https://elixirforum.com/t/randomly-getting-compilationerror-on-tests/17298/3 6 | def in_test_project!(test_project_path, function), do: File.cd!(test_project_path, function) 7 | 8 | def assert_file(path), 9 | do: assert(File.regular?(path), "Expected #{path} to exist, but does not") 10 | 11 | def assert_directory(path), 12 | do: assert(File.dir?(path), "Expected #{path} to exist, but does not") 13 | 14 | def assert_file(path, match) do 15 | assert_file(path) 16 | match.(File.read!(path)) 17 | end 18 | 19 | def refute_file(path), 20 | do: refute(File.regular?(path), "Expected #{path} does not exist, but it does") 21 | 22 | def parent_test_project_path do 23 | project_directory = 24 | 20 25 | |> :crypto.strong_rand_bytes() 26 | |> Base.url_encode64(padding: false) 27 | |> String.downcase() 28 | 29 | tmp_path = Path.expand("../../../tmp", __DIR__) 30 | 31 | Path.join(tmp_path, project_directory) 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /test/nimble_template/addons/variants/phoenix/api/params_validation_test.exs: -------------------------------------------------------------------------------- 1 | defmodule NimbleTemplate.Addons.Phoenix.Api.ParamsValidationTest do 2 | use NimbleTemplate.AddonCase, async: false 3 | 4 | describe "#apply!/2" do 5 | test "copies the params validation module and test files", %{ 6 | project: project, 7 | test_project_path: project_path 8 | } do 9 | in_test_project!(project_path, fn -> 10 | ApiAddons.ParamsValidation.apply!(project) 11 | 12 | assert_file("lib/nimble_template_web/params/params.ex") 13 | assert_file("lib/nimble_template_web/params/params_validator.ex") 14 | assert_file("test/nimble_template_web/params/params_validator_test.exs") 15 | end) 16 | end 17 | 18 | test "adds ParamsValidator alias into the web entry_point", %{ 19 | project: project, 20 | test_project_path: project_path 21 | } do 22 | in_test_project!(project_path, fn -> 23 | ApiAddons.ParamsValidation.apply!(project) 24 | 25 | assert_file("lib/nimble_template_web.ex", fn file -> 26 | assert file =~ "alias NimbleTemplateWeb.ParamsValidator" 27 | end) 28 | end) 29 | end 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /lib/nimble_template/addons/variants/phoenix/docker.ex: -------------------------------------------------------------------------------- 1 | defmodule NimbleTemplate.Addons.Phoenix.Docker do 2 | @moduledoc false 3 | 4 | use NimbleTemplate.Addons.Addon 5 | 6 | @impl true 7 | def do_apply!( 8 | %Project{ 9 | web_project?: web_project?, 10 | otp_app: otp_app, 11 | base_module: base_module, 12 | alpine_version: alpine_version, 13 | elixir_version: elixir_version, 14 | erlang_version: erlang_version 15 | } = project, 16 | _opts 17 | ) do 18 | Generator.copy_file!( 19 | [ 20 | {:eex, "docker-compose.dev.yml.eex", "docker-compose.dev.yml"}, 21 | {:eex, "docker-compose.yml.eex", "docker-compose.yml"}, 22 | {:eex, "Dockerfile.eex", "Dockerfile"}, 23 | {:eex, "bin/start.sh.eex", "bin/start.sh"}, 24 | {:text, ".dockerignore", ".dockerignore"} 25 | ], 26 | otp_app: otp_app, 27 | base_module: base_module, 28 | alpine_version: alpine_version, 29 | elixir_version: elixir_version, 30 | erlang_version: erlang_version, 31 | web_project?: web_project? 32 | ) 33 | 34 | Mix.shell().cmd("chmod +x bin/start.sh") 35 | 36 | project 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /priv/templates/nimble_template/lib/otp_app_web/plugs/health_plug.ex.eex: -------------------------------------------------------------------------------- 1 | defmodule <%= web_module %>.HealthPlug do 2 | @behaviour Plug 3 | 4 | import Plug.Conn 5 | 6 | alias Ecto.Adapters.SQL 7 | alias <%= base_module %>.Repo 8 | 9 | require Logger 10 | 11 | @liveness_path ["liveness"] 12 | @readiness_path ["readiness"] 13 | 14 | @impl true 15 | def init(opts), do: opts 16 | 17 | @impl true 18 | def call(%{path_info: @liveness_path} = conn, _opts) do 19 | conn 20 | |> resp(:ok, "alive") 21 | |> halt() 22 | end 23 | 24 | @impl true 25 | def call(%{path_info: @readiness_path} = conn, _opts) do 26 | case readiness_check() do 27 | :ok -> 28 | conn 29 | |> resp(:ok, "ready") 30 | |> halt() 31 | 32 | :error -> 33 | conn 34 | |> resp(:internal_server_error, "") 35 | |> halt() 36 | end 37 | end 38 | 39 | @impl true 40 | def call(conn, _opts) do 41 | conn 42 | |> resp(:not_found, "") 43 | |> halt() 44 | end 45 | 46 | defp readiness_check do 47 | case SQL.query(Repo, "SELECT 1") do 48 | {:ok, _result} -> 49 | :ok 50 | 51 | {:error, exception} -> 52 | Logger.error(inspect(exception)) 53 | 54 | :error 55 | end 56 | end 57 | end 58 | -------------------------------------------------------------------------------- /lib/nimble_template/helpers/github.ex: -------------------------------------------------------------------------------- 1 | defmodule NimbleTemplate.GithubHelper do 2 | def host_on_github?(), do: Mix.shell().yes?("\nWill you host this project on Github?") 3 | 4 | def generate_github_template?(), 5 | do: 6 | Mix.shell().yes?( 7 | "\nDo you want to generate the .github/ISSUE_TEMPLATE and .github/PULL_REQUEST_TEMPLATE?" 8 | ) 9 | 10 | def generate_github_action?(), 11 | do: Mix.shell().yes?("\nDo you want to generate the Github Action workflow?") 12 | 13 | def generate_github_wiki?(), 14 | do: 15 | Mix.shell().yes?( 16 | "\nDo you want to publish a Github Wiki for this project? You'd need to manually create the first Github Wiki Page and set the GH_TOKEN and GH_EMAIL secret for this to properly function." 17 | ) 18 | 19 | def generate_github_workflows_readme?(), 20 | do: Mix.shell().yes?("\nDo you want to generate the .github/workflows/README file?") 21 | 22 | def generate_github_action_test?(), 23 | do: Mix.shell().yes?("\nDo you want to generate the Github Action workflows: Test?") 24 | 25 | def generate_github_action_deploy_heroku?(), 26 | do: Mix.shell().yes?("\nDo you want to generate the Github Action workflows: Deploy to Heroku?") 27 | 28 | def has_github_wiki_directory?(), do: File.dir?(".github/wiki/") 29 | end 30 | -------------------------------------------------------------------------------- /test/nimble_template/addons/variants/phoenix/api/config_test.exs: -------------------------------------------------------------------------------- 1 | defmodule NimbleTemplate.Addons.Phoenix.Api.ConfigTest do 2 | use NimbleTemplate.AddonCase, async: false 3 | 4 | describe "#apply!/2" do 5 | test "removes cache_static_manifest setting in config/prod.exs", %{ 6 | project: project, 7 | test_project_path: test_project_path 8 | } do 9 | in_test_project!(test_project_path, fn -> 10 | ApiAddons.Config.apply!(project) 11 | 12 | assert_file("config/prod.exs", fn file -> 13 | refute file =~ "cache_static_manifest: \"priv/static/cache_manifest.json\"" 14 | 15 | refute file =~ """ 16 | # For production, don't forget to configure the url host 17 | # to something meaningful, Phoenix uses this information 18 | # when generating URLs. 19 | # 20 | # Note we also include the path to a cache manifest 21 | # containing the digested version of static files. This 22 | # manifest is generated by the `mix phx.digest` task, 23 | # which you should run after static files are built and 24 | # before starting your production server. 25 | """ 26 | end) 27 | end) 28 | end 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /priv/templates/nimble_template/lib/otp_app_web/params/params_validator.ex.eex: -------------------------------------------------------------------------------- 1 | defmodule <%= web_module %>.ParamsValidator do 2 | @moduledoc """ 3 | Helper module for validating given request params with params module 4 | """ 5 | 6 | alias Ecto.Changeset 7 | alias <%= web_module %>.Params 8 | 9 | @spec validate(map(), atom(), [{:for, Params.t()}]) :: 10 | {:ok, map()} | {:error, :invalid_params, Ecto.Changeset.t()} 11 | def validate(params, changeset_method \\ :changeset, for: params_module) do 12 | params_module 13 | |> Kernel.apply(changeset_method, [params]) 14 | |> handle_changeset() 15 | end 16 | 17 | defp handle_changeset(%Changeset{valid?: true} = changeset), 18 | do: {:ok, extract_changes(changeset)} 19 | 20 | defp handle_changeset(changeset), do: {:error, :invalid_params, put_action(changeset)} 21 | 22 | defp extract_changes(%Changeset{} = changeset) do 23 | Enum.reduce(changeset.changes, %{}, fn {key, value}, params -> 24 | Map.put(params, key, extract_changes(value)) 25 | end) 26 | end 27 | 28 | defp extract_changes([%Changeset{} | _] = changesets), 29 | do: Enum.map(changesets, &extract_changes/1) 30 | 31 | defp extract_changes(value), do: value 32 | 33 | defp put_action(%Changeset{} = changeset), do: Map.put(changeset, :action, :validate) 34 | end 35 | -------------------------------------------------------------------------------- /lib/nimble_template/addons/variants/phoenix/api/config.ex: -------------------------------------------------------------------------------- 1 | defmodule NimbleTemplate.Addons.Phoenix.Api.Config do 2 | @moduledoc false 3 | 4 | use NimbleTemplate.Addons.Addon 5 | 6 | @impl true 7 | def do_apply!(%Project{} = project, _opts) do 8 | edit_files!(project) 9 | end 10 | 11 | def edit_config_prod!(%Project{otp_app: otp_app, web_module: web_module} = project) do 12 | Generator.delete_content!( 13 | "config/prod.exs", 14 | """ 15 | # For production, don't forget to configure the url host 16 | # to something meaningful, Phoenix uses this information 17 | # when generating URLs. 18 | # 19 | # Note we also include the path to a cache manifest 20 | # containing the digested version of static files. This 21 | # manifest is generated by the `mix phx.digest` task, 22 | # which you should run after static files are built and 23 | # before starting your production server. 24 | """ 25 | ) 26 | 27 | Generator.delete_content!( 28 | "config/prod.exs", 29 | "config :#{otp_app}, #{web_module}.Endpoint," 30 | ) 31 | 32 | Generator.delete_content!( 33 | "config/prod.exs", 34 | "cache_static_manifest: \"priv/static/cache_manifest.json\"" 35 | ) 36 | 37 | project 38 | end 39 | 40 | defp edit_files!(%Project{} = project) do 41 | edit_config_prod!(project) 42 | 43 | project 44 | end 45 | end 46 | -------------------------------------------------------------------------------- /lib/nimble_template/addons/addon.ex: -------------------------------------------------------------------------------- 1 | defmodule NimbleTemplate.Addons.Addon do 2 | @moduledoc false 3 | 4 | alias __MODULE__ 5 | alias NimbleTemplate.Projects.Project 6 | 7 | @callback apply!(%Project{}, %{}) :: %Project{} 8 | @callback do_apply!(%Project{}, %{}) :: %Project{} 9 | 10 | defmacro __using__(opts) do 11 | quote location: :keep, bind_quoted: [opts: opts] do 12 | @behaviour Addon 13 | 14 | import NimbleTemplate.GithubHelper, only: [has_github_wiki_directory?: 0] 15 | 16 | alias NimbleTemplate.Generator 17 | alias NimbleTemplate.Hex.Package 18 | alias NimbleTemplate.Projects.Project 19 | alias NimbleTemplate.ProjectHelper 20 | 21 | def apply!(%Project{} = project, opts \\ %{}) when is_map(opts) do 22 | Generator.info_log("* applying ", inspect(__MODULE__)) 23 | 24 | do_apply!(project, opts) 25 | end 26 | 27 | def do_apply!(%Project{} = project, opts) when is_map(opts), do: project 28 | 29 | defp latest_package_version(package), 30 | do: "~> " <> hex_package_resource().get_latest_version(package) 31 | 32 | # TODO: `Application.get_env(:nimble_template, :hex_package_resource)` returns nil on runtime, temporary fix by fallback to `Package` 33 | defp hex_package_resource, 34 | do: Application.get_env(:nimble_template, :hex_package_resource, Package) 35 | 36 | defoverridable do_apply!: 2 37 | end 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /.github/workflows/apply_mix_variant.yml: -------------------------------------------------------------------------------- 1 | name: Apply Mix variant 2 | 3 | on: push 4 | 5 | jobs: 6 | standard_mix_project: 7 | name: Test on a Standard Mix project 8 | uses: ./.github/workflows/reusable_mix_project.yml 9 | with: 10 | new_project_options: "" 11 | 12 | long_custom_name_mix_project: 13 | name: Test on a long custom name Mix project 14 | uses: ./.github/workflows/reusable_mix_project.yml 15 | with: 16 | new_project_options: "--module=SampleCustomModule --app=sample_custom_app" 17 | 18 | short_custom_name_mix_project: 19 | name: Test on a short custom name Mix project 20 | uses: ./.github/workflows/reusable_mix_project.yml 21 | with: 22 | new_project_options: "--module=Z --app=z" 23 | 24 | standard_mix_supervision_project: 25 | name: Test on a Standard Supervision Mix project 26 | uses: ./.github/workflows/reusable_mix_project.yml 27 | with: 28 | new_project_options: "--sup" 29 | 30 | long_custom_name_mix_supervision_project: 31 | name: Test on a long custom name Supervision Mix project 32 | uses: ./.github/workflows/reusable_mix_project.yml 33 | with: 34 | new_project_options: "--sup --module=SampleCustomModule --app=sample_custom_app" 35 | 36 | short_custom_name_mix_supervision_project: 37 | name: Test on a short custom name Supervision Mix project 38 | uses: ./.github/workflows/reusable_mix_project.yml 39 | with: 40 | new_project_options: "--sup --module=Z --app=z" 41 | -------------------------------------------------------------------------------- /lib/nimble_template/addons/variants/phoenix/api/error_view.ex: -------------------------------------------------------------------------------- 1 | defmodule NimbleTemplate.Addons.Phoenix.Api.ErrorView do 2 | @moduledoc false 3 | 4 | use NimbleTemplate.Addons.Addon 5 | 6 | @impl true 7 | def do_apply!(%Project{} = project, _opts) do 8 | project 9 | |> delete_files!() 10 | |> copy_files!() 11 | end 12 | 13 | defp delete_files!(%Project{web_path: web_path} = project) do 14 | File.rm!("#{web_path}/views/error_helpers.ex") 15 | 16 | project 17 | end 18 | 19 | defp copy_files!( 20 | %Project{ 21 | base_module: base_module, 22 | web_module: web_module, 23 | web_path: web_path, 24 | web_test_path: web_test_path 25 | } = project 26 | ) do 27 | binding = [ 28 | web_module: web_module, 29 | base_module: base_module 30 | ] 31 | 32 | files = [ 33 | {:eex, "lib/otp_app_web/views/error_helpers.ex.eex", "#{web_path}/views/error_helpers.ex"}, 34 | {:eex, "lib/otp_app_web/views/api/error_view.ex.eex", "#{web_path}/views/api/error_view.ex"}, 35 | {:eex, "test/otp_app_web/views/error_helpers_test.exs", 36 | "#{web_test_path}/views/error_helpers_test.exs"}, 37 | {:eex, "test/otp_app_web/views/api/error_view_test.exs", 38 | "#{web_test_path}/views/api/error_view_test.exs"}, 39 | {:eex, "test/support/view_case.ex.eex", "test/support/view_case.ex"} 40 | ] 41 | 42 | Generator.copy_file!(files, binding) 43 | 44 | project 45 | end 46 | end 47 | -------------------------------------------------------------------------------- /lib/nimble_template/addons/mimic.ex: -------------------------------------------------------------------------------- 1 | defmodule NimbleTemplate.Addons.Mimic do 2 | @moduledoc false 3 | 4 | use NimbleTemplate.Addons.Addon 5 | 6 | @impl true 7 | def do_apply!(%Project{} = project, _opts) do 8 | edit_files!(project) 9 | end 10 | 11 | defp edit_files!(%Project{} = project) do 12 | project 13 | |> inject_mix_dependency!() 14 | |> edit_test_helper!() 15 | |> edit_case!() 16 | end 17 | 18 | defp inject_mix_dependency!(project) do 19 | Generator.inject_mix_dependency!({:mimic, latest_package_version(:mimic), only: :test}) 20 | 21 | project 22 | end 23 | 24 | defp edit_test_helper!(project) do 25 | Generator.replace_content!( 26 | "test/test_helper.exs", 27 | """ 28 | ExUnit.start() 29 | """, 30 | """ 31 | {:ok, _} = Application.ensure_all_started(:mimic) 32 | 33 | ExUnit.start() 34 | """ 35 | ) 36 | 37 | project 38 | end 39 | 40 | defp edit_case!(%Project{mix_project?: false} = project) do 41 | Generator.inject_content!( 42 | "test/support/conn_case.ex", 43 | """ 44 | quote do 45 | """, 46 | """ 47 | use Mimic 48 | 49 | """ 50 | ) 51 | 52 | Generator.inject_content!( 53 | "test/support/data_case.ex", 54 | """ 55 | quote do 56 | """, 57 | """ 58 | use Mimic 59 | 60 | """ 61 | ) 62 | 63 | project 64 | end 65 | 66 | defp edit_case!(project) do 67 | project 68 | end 69 | end 70 | -------------------------------------------------------------------------------- /lib/nimble_template/addons/variants/phoenix/api/empty_body_params_plug.ex: -------------------------------------------------------------------------------- 1 | defmodule NimbleTemplate.Addons.Phoenix.Api.EmptyBodyParamsPlug do 2 | @moduledoc false 3 | 4 | use NimbleTemplate.Addons.Addon 5 | 6 | @impl true 7 | def do_apply!(%Project{} = project, _opts) do 8 | project 9 | |> copy_files!() 10 | |> edit_files!() 11 | end 12 | 13 | defp copy_files!( 14 | %Project{ 15 | web_module: web_module, 16 | web_path: web_path, 17 | web_test_path: web_test_path 18 | } = project 19 | ) do 20 | binding = [ 21 | web_module: web_module 22 | ] 23 | 24 | files = [ 25 | {:eex, "lib/otp_app_web/plugs/check_empty_body_params_plug.ex.eex", 26 | "#{web_path}/plugs/check_empty_body_params_plug.ex"}, 27 | {:eex, "test/otp_app_web/plugs/check_empty_body_params_plug_test.exs.eex", 28 | "#{web_test_path}/plugs/check_empty_body_params_plug_test.exs"} 29 | ] 30 | 31 | Generator.copy_file!(files, binding) 32 | 33 | project 34 | end 35 | 36 | defp edit_files!(%Project{web_path: web_path, web_module: web_module} = project) do 37 | Generator.replace_content!( 38 | "#{web_path}/router.ex", 39 | """ 40 | pipeline :api do 41 | plug :accepts, ["json"] 42 | end 43 | """, 44 | """ 45 | pipeline :api do 46 | plug :accepts, ["json"] 47 | plug #{web_module}.CheckEmptyBodyParamsPlug 48 | end 49 | """ 50 | ) 51 | 52 | project 53 | end 54 | end 55 | -------------------------------------------------------------------------------- /lib/nimble_template/addons/variants/phoenix/web/nimble_js.ex: -------------------------------------------------------------------------------- 1 | defmodule NimbleTemplate.Addons.Phoenix.Web.NimbleJS do 2 | @moduledoc false 3 | 4 | use NimbleTemplate.Addons.Addon 5 | 6 | @impl true 7 | def do_apply!(%Project{} = project, _opts) do 8 | project 9 | |> copy_nimble_structure!() 10 | |> moving_js_vendor!() 11 | |> edit_app_js!() 12 | |> edit_es_lint_config!() 13 | end 14 | 15 | defp copy_nimble_structure!(project) do 16 | Generator.copy_directory!("assets/nimble_js", "assets/js") 17 | 18 | project 19 | end 20 | 21 | defp moving_js_vendor!(project) do 22 | Generator.rename_file!("assets/vendor", "assets/js/vendor") 23 | 24 | project 25 | end 26 | 27 | defp edit_app_js!(project) do 28 | Generator.append_content!( 29 | "assets/js/app.js", 30 | """ 31 | 32 | // Application 33 | import "./initializers/"; 34 | 35 | import "./screens/"; 36 | """ 37 | ) 38 | 39 | Generator.replace_content!("assets/js/app.js", "assets/vendor", "assets/js/vendor") 40 | 41 | Generator.replace_content!( 42 | "assets/js/app.js", 43 | "../vendor/some-package.js", 44 | "./vendor/some-package.js" 45 | ) 46 | 47 | Generator.replace_content!("assets/js/app.js", "../vendor/topbar", "./vendor/topbar") 48 | 49 | project 50 | end 51 | 52 | defp edit_es_lint_config!(project) do 53 | Generator.replace_content!( 54 | "assets/.eslintrc.json", 55 | "/vendor/topbar.js", 56 | "/js/vendor/topbar.js" 57 | ) 58 | 59 | project 60 | end 61 | end 62 | -------------------------------------------------------------------------------- /priv/templates/nimble_template/README.md.mix.eex: -------------------------------------------------------------------------------- 1 | [![Build Status](CI_BADGE_URL goes here)](REPO_URL goes here) 2 | 3 | ## Introduction 4 | 5 | > *App introduction goes here ...* 6 | 7 | ## Project Setup 8 | 9 | ### Erlang & Elixir 10 | 11 | - Erlang <%= erlang_version %> 12 | 13 | - Elixir <%= elixir_version %> 14 | 15 | - Recommended version manager. 16 | 17 | - [asdf](https://github.com/asdf-vm/asdf) 18 | - [asdf-erlang](https://github.com/asdf-vm/asdf-erlang) 19 | - [asdf-elixir](https://github.com/asdf-vm/asdf-elixir) 20 | 21 | ### Development 22 | 23 | - Install Elixir dependencies: 24 | 25 | ```sh 26 | mix deps.get 27 | ``` 28 | 29 | - Run all tests: 30 | 31 | ```sh 32 | mix test 33 | ``` 34 | 35 | - Run tests with `watch` option: 36 | 37 | ```sh 38 | mix test.interactive # Watch the whole test suite 39 | 40 | mix test.interactive test_file_path # Watch a specific test file 41 | 42 | mix test.interactive test_file_path:line_number # Watch a specific test line number 43 | ``` 44 | 45 | - Run all lint: 46 | 47 | ```sh 48 | mix codebase 49 | ``` 50 | 51 | - Fix all lint: 52 | 53 | ```sh 54 | mix codebase.fix 55 | ``` 56 | 57 | - Test coverage: 58 | 59 | ```sh 60 | mix coverage 61 | ``` 62 | 63 | ### CI/CD 64 | 65 | The project relies entirely on [Github Actions](https://github.com/features/actions) for CI/CD via multiple workflows located under the [`.github/workflows/`](.github/workflows) directory. 66 | Please check out the [`.github/workflows/README.md`](.github/workflows/README.md) file for further instructions. 67 | -------------------------------------------------------------------------------- /lib/mix/tasks/nimble_template.upgrade_stack.ex: -------------------------------------------------------------------------------- 1 | defmodule Mix.Tasks.NimbleTemplate.UpgradeStack do 2 | @shortdoc "Upgrade Elixir, Erlang, Alpine or Node version." 3 | 4 | @moduledoc """ 5 | #{@shortdoc} 6 | 7 | - Hex package: https://hex.pm/packages/nimble_template 8 | - Github: https://github.com/nimblehq/elixir-templates 9 | 10 | # Usage 11 | 12 | - mix help nimble_template.upgrade_stack # Print help 13 | - mix nimble_template.upgrade_stack elixir x.x.x # Upgrade Elixir to x.x.x 14 | - mix nimble_template.upgrade_stack erlang x.x.x # Upgrade Erlang to x.x.x 15 | - mix nimble_template.upgrade_stack alpine x.x.x # Upgrade Alpine to x.x.x 16 | - mix nimble_template.upgrade_stack node x.x.x # Upgrade Node to x.x.x 17 | - mix nimble_template.upgrade_stack elixir x.x.x erlang y.y.y alpine z.z.z node w.w.w # Upgrade Elixir, Erlang, Alpine and Node 18 | """ 19 | 20 | use Mix.Task 21 | 22 | alias NimbleTemplate.Version 23 | 24 | def run(args) do 25 | stack_versions = parse_opts(args) 26 | 27 | Version.upgrade_stack!(stack_versions) 28 | end 29 | 30 | defp parse_opts(args) do 31 | case OptionParser.parse(args, 32 | strict: [ 33 | elixir: :string, 34 | erlang: :string, 35 | alpine: :string, 36 | node: :string 37 | ] 38 | ) do 39 | {stack_versions, _args, _invalid} -> 40 | stack_versions 41 | 42 | _other -> 43 | Mix.raise( 44 | "Invalid command. Check `mix help nimble_template.upgrade_stack` for more information." 45 | ) 46 | end 47 | end 48 | end 49 | -------------------------------------------------------------------------------- /lib/nimble_template/addons/credo.ex: -------------------------------------------------------------------------------- 1 | defmodule NimbleTemplate.Addons.Credo do 2 | @moduledoc false 3 | 4 | use NimbleTemplate.Addons.Addon 5 | 6 | @impl true 7 | def do_apply!(%Project{} = project, _opts) do 8 | project 9 | |> copy_files!() 10 | |> edit_files!() 11 | end 12 | 13 | defp copy_files!(%Project{mix_project?: true} = project) do 14 | Generator.copy_file!([{:text, ".credo.mix.exs", ".credo.exs"}]) 15 | 16 | project 17 | end 18 | 19 | defp copy_files!( 20 | %Project{web_path: web_path, base_path: base_path, mix_project?: false} = project 21 | ) do 22 | binding = [ 23 | base_entry_path: "#{base_path}.ex", 24 | web_entry_path: "#{web_path}.ex" 25 | ] 26 | 27 | Generator.copy_file!([{:eex, ".credo.exs", ".credo.exs"}], binding) 28 | 29 | project 30 | end 31 | 32 | defp edit_files!(%Project{} = project) do 33 | project 34 | |> inject_mix_dependency!() 35 | |> edit_mix!() 36 | end 37 | 38 | defp inject_mix_dependency!(project) do 39 | Generator.inject_mix_dependency!([ 40 | {:credo, latest_package_version(:credo), only: [:dev, :test], runtime: false}, 41 | {:compass_credo_plugin, latest_package_version(:compass_credo_plugin), 42 | only: [:dev, :test], runtime: false} 43 | ]) 44 | 45 | project 46 | end 47 | 48 | defp edit_mix!(project) do 49 | Generator.replace_content!( 50 | "mix.exs", 51 | """ 52 | codebase: [ 53 | """, 54 | """ 55 | codebase: [ 56 | "credo --strict", 57 | """ 58 | ) 59 | 60 | project 61 | end 62 | end 63 | -------------------------------------------------------------------------------- /lib/nimble_template/templates/variants/phoenix/web/template.ex: -------------------------------------------------------------------------------- 1 | defmodule NimbleTemplate.Templates.Phoenix.Web.Template do 2 | @moduledoc false 3 | 4 | alias NimbleTemplate.Addons.Phoenix.Web 5 | alias NimbleTemplate.Projects.Project 6 | 7 | def apply!(%Project{} = project) do 8 | project 9 | |> apply_default_web_addons() 10 | |> apply_optional_web_addons() 11 | end 12 | 13 | defp apply_default_web_addons(project) do 14 | project 15 | |> Web.NodePackage.apply!() 16 | |> Web.Assets.apply!() 17 | |> Web.CoreJS.apply!() 18 | |> Web.Prettier.apply!() 19 | |> Web.Sobelow.apply!() 20 | |> Web.Wallaby.apply!() 21 | |> Web.EsLint.apply!() 22 | |> Web.StyleLint.apply!() 23 | |> Web.EsBuild.apply!() 24 | |> Web.PostCSS.apply!() 25 | |> Web.HeexFormatter.apply!() 26 | end 27 | 28 | defp apply_optional_web_addons(%Project{optional_addons: optional_addons} = project) do 29 | with_nimble_css_addon? = Web.NimbleCSS in optional_addons 30 | with_nimble_js_addon? = Web.NimbleJS in optional_addons 31 | 32 | if Web.SvgSprite in optional_addons, do: Web.SvgSprite.apply!(project) 33 | if Web.DartSass in optional_addons, do: Web.DartSass.apply!(project) 34 | if with_nimble_css_addon?, do: Web.NimbleCSS.apply!(project) 35 | if with_nimble_js_addon?, do: Web.NimbleJS.apply!(project) 36 | 37 | if Web.Bootstrap in optional_addons, 38 | do: 39 | Web.Bootstrap.apply!(project, %{ 40 | with_nimble_css_addon: with_nimble_css_addon?, 41 | with_nimble_js_addon: with_nimble_js_addon? 42 | }) 43 | 44 | project 45 | end 46 | end 47 | -------------------------------------------------------------------------------- /test/nimble_template/addons/variants/phoenix/api/empty_body_plug_test.exs: -------------------------------------------------------------------------------- 1 | defmodule NimbleTemplate.Addons.Phoenix.Api.EmptyBodyParamsPlugTest do 2 | use NimbleTemplate.AddonCase, async: false 3 | 4 | describe "#apply!/2" do 5 | test "copies the empty body plug file", %{ 6 | project: project, 7 | test_project_path: test_project_path 8 | } do 9 | in_test_project!(test_project_path, fn -> 10 | ApiAddons.EmptyBodyParamsPlug.apply!(project) 11 | 12 | assert_file("lib/nimble_template_web/plugs/check_empty_body_params_plug.ex") 13 | end) 14 | end 15 | 16 | test "copies the empty body plug test file", %{ 17 | project: project, 18 | test_project_path: test_project_path 19 | } do 20 | in_test_project!(test_project_path, fn -> 21 | ApiAddons.EmptyBodyParamsPlug.apply!(project) 22 | 23 | assert_file("test/nimble_template_web/plugs/check_empty_body_params_plug_test.exs") 24 | end) 25 | end 26 | 27 | test "adds CheckEmptyBodyParamsPlug into the api pipeline", %{ 28 | project: project, 29 | test_project_path: test_project_path 30 | } do 31 | in_test_project!(test_project_path, fn -> 32 | ApiAddons.EmptyBodyParamsPlug.apply!(project) 33 | 34 | assert_file("lib/nimble_template_web/router.ex", fn file -> 35 | assert file =~ """ 36 | pipeline :api do 37 | plug :accepts, ["json"] 38 | plug NimbleTemplateWeb.CheckEmptyBodyParamsPlug 39 | end 40 | """ 41 | end) 42 | end) 43 | end 44 | end 45 | end 46 | -------------------------------------------------------------------------------- /test/nimble_template/addons/git_test.exs: -------------------------------------------------------------------------------- 1 | defmodule NimbleTemplate.Addons.GitTest do 2 | use NimbleTemplate.AddonCase, async: false 3 | 4 | describe "#apply!/2" do 5 | test "adjusts the .gitignore file", %{ 6 | project: project, 7 | test_project_path: test_project_path 8 | } do 9 | in_test_project!(test_project_path, fn -> 10 | Addons.Git.apply!(project) 11 | 12 | assert_file(".gitignore", fn file -> 13 | assert file =~ """ 14 | # Mac OS 15 | .DS_Store 16 | 17 | # IDE 18 | .idea 19 | .vscode 20 | 21 | # Iex 22 | .iex.exs 23 | 24 | # Ignore ElixirLS files 25 | .elixir_ls 26 | """ 27 | end) 28 | end) 29 | end 30 | end 31 | 32 | describe "#apply!/2 with mix_project" do 33 | @describetag mix_project?: true 34 | 35 | test "adjusts the .gitignore file", %{ 36 | project: project, 37 | test_project_path: test_project_path 38 | } do 39 | in_test_project!(test_project_path, fn -> 40 | Addons.Git.apply!(project) 41 | 42 | assert_file(".gitignore", fn file -> 43 | assert file =~ """ 44 | # Mac OS 45 | .DS_Store 46 | 47 | # IDE 48 | .idea 49 | .vscode 50 | 51 | # Iex 52 | .iex.exs 53 | 54 | # Ignore ElixirLS files 55 | .elixir_ls 56 | """ 57 | end) 58 | end) 59 | end 60 | end 61 | end 62 | -------------------------------------------------------------------------------- /test/nimble_template/addons/variants/phoenix/web/sobelow_test.exs: -------------------------------------------------------------------------------- 1 | defmodule NimbleTemplate.Addons.Phoenix.Web.SobelowTest do 2 | use NimbleTemplate.AddonCase, async: false 3 | 4 | describe "#apply!/2" do 5 | @describetag mock_latest_package_versions: [{:sobelow, "0.8"}] 6 | @describetag required_addons: [:TestEnv] 7 | 8 | test "copies the .sobelow-conf", %{ 9 | project: project, 10 | test_project_path: test_project_path 11 | } do 12 | in_test_project!(test_project_path, fn -> 13 | WebAddons.Sobelow.apply!(project) 14 | 15 | assert_file(".sobelow-conf") 16 | end) 17 | end 18 | 19 | test "injects sobelow to mix dependency", %{ 20 | project: project, 21 | test_project_path: test_project_path 22 | } do 23 | in_test_project!(test_project_path, fn -> 24 | WebAddons.Sobelow.apply!(project) 25 | 26 | assert_file("mix.exs", fn file -> 27 | assert file =~ """ 28 | defp deps do 29 | [ 30 | {:sobelow, "~> 0.8", [only: [:dev, :test], runtime: false]}, 31 | """ 32 | end) 33 | end) 34 | end 35 | 36 | test "adds sobelow codebase alias", %{project: project, test_project_path: test_project_path} do 37 | in_test_project!(test_project_path, fn -> 38 | WebAddons.Sobelow.apply!(project) 39 | 40 | assert_file("mix.exs", fn file -> 41 | assert file =~ """ 42 | codebase: [ 43 | "sobelow --config", 44 | """ 45 | end) 46 | end) 47 | end 48 | end 49 | end 50 | -------------------------------------------------------------------------------- /mix.exs: -------------------------------------------------------------------------------- 1 | defmodule NimbleTemplate.MixProject do 2 | use Mix.Project 3 | 4 | def project do 5 | [ 6 | app: :nimble_template, 7 | version: "4.7.0", 8 | description: "Phoenix/Mix template for projects at [Nimble](https://nimblehq.co/).", 9 | elixir: "~> 1.14.0", 10 | elixirc_paths: elixirc_paths(Mix.env()), 11 | start_permanent: Mix.env() == :prod, 12 | deps: deps(), 13 | aliases: aliases(), 14 | package: package(), 15 | source_url: "https://github.com/nimblehq/elixir-templates" 16 | ] 17 | end 18 | 19 | # Run "mix help compile.app" to learn about applications. 20 | def application do 21 | [ 22 | extra_applications: [:logger] 23 | ] 24 | end 25 | 26 | # Specifies which paths to compile per environment. 27 | defp elixirc_paths(:test), do: ["lib", "test/support"] 28 | defp elixirc_paths(_), do: ["lib"] 29 | 30 | # Run "mix help deps" to learn about dependencies. 31 | defp deps do 32 | [ 33 | {:credo, "~> 1.4", only: [:dev, :test], runtime: false}, 34 | {:ex_doc, "~> 0.23", only: :dev, runtime: false}, 35 | {:httpoison, "~> 1.7"}, 36 | {:jason, "~> 1.2"}, 37 | {:mimic, "~> 1.3", only: :test}, 38 | {:phoenix, "~> 1.6.11"} 39 | ] 40 | end 41 | 42 | defp aliases do 43 | [ 44 | codebase: ["deps.unlock --check-unused", "format --check-formatted", "credo --strict"], 45 | "codebase.fix": ["deps.clean --unlock --unused", "format"] 46 | ] 47 | end 48 | 49 | defp package do 50 | [ 51 | licenses: ["MIT"], 52 | links: %{"GitHub" => "https://github.com/nimblehq/elixir-templates"} 53 | ] 54 | end 55 | end 56 | -------------------------------------------------------------------------------- /test/nimble_template/addons/variants/phoenix/web/esbuild_test.exs: -------------------------------------------------------------------------------- 1 | defmodule NimbleTemplate.Addons.Phoenix.Web.EsBuildTest do 2 | use NimbleTemplate.AddonCase, async: false 3 | 4 | describe "#apply!/2" do 5 | test "renames esbuild namespace to app in the mix assets.deploy alias", %{ 6 | project: project, 7 | test_project_path: test_project_path 8 | } do 9 | in_test_project!(test_project_path, fn -> 10 | WebAddons.EsBuild.apply!(project) 11 | 12 | assert_file("mix.exs", fn file -> 13 | assert file =~ "\"assets.deploy\": [\"esbuild app --minify\"," 14 | end) 15 | end) 16 | end 17 | 18 | test "renames esbuild namespace to app in the config.exs", %{ 19 | project: project, 20 | test_project_path: test_project_path 21 | } do 22 | in_test_project!(test_project_path, fn -> 23 | WebAddons.EsBuild.apply!(project) 24 | 25 | assert_file("config/config.exs", fn file -> 26 | assert file =~ """ 27 | config :esbuild, 28 | version: "0.14.29", 29 | app: [ 30 | args: 31 | """ 32 | end) 33 | end) 34 | end 35 | 36 | test "renames esbuild namespace to app in the development watcher", %{ 37 | project: project, 38 | test_project_path: test_project_path 39 | } do 40 | in_test_project!(test_project_path, fn -> 41 | WebAddons.EsBuild.apply!(project) 42 | 43 | assert_file("config/dev.exs", fn file -> 44 | assert file =~ "esbuild: {Esbuild, :install_and_run, [:app," 45 | end) 46 | end) 47 | end 48 | end 49 | end 50 | -------------------------------------------------------------------------------- /priv/templates/nimble_template/lib/otp_app_web/views/api/error_view.ex.eex: -------------------------------------------------------------------------------- 1 | defmodule <%= web_module %>.Api.ErrorView do 2 | use <%= web_module %>, :view 3 | 4 | alias Ecto.Changeset 5 | 6 | def template_not_found(template, assigns) do 7 | build_error_response( 8 | code: assigns[:code] || status_code_from_template(template), 9 | detail: %{}, 10 | message: assigns[:message] || Phoenix.Controller.status_message_from_template(template) 11 | ) 12 | end 13 | 14 | def render("error.json", %{code: code, changeset: %Changeset{} = changeset}) do 15 | build_error_response( 16 | code: code, 17 | detail: translate_errors(changeset), 18 | message: build_changeset_error_message(changeset) 19 | ) 20 | end 21 | 22 | defp build_changeset_error_message(%Changeset{} = changeset) do 23 | changeset 24 | |> translate_errors() 25 | |> Enum.flat_map(fn {key, messages} -> 26 | Enum.map(messages, &"#{Phoenix.Naming.humanize(key)} #{&1}") 27 | end) 28 | |> to_sentence() 29 | end 30 | 31 | defp to_sentence([message]), do: message 32 | 33 | defp to_sentence(messages) do 34 | sentence = 35 | messages 36 | |> Enum.slice(0..(length(messages) - 2)) 37 | |> Enum.join(", ") 38 | 39 | "#{sentence} and #{List.last(messages)}" 40 | end 41 | 42 | defp build_error_response(code: code, detail: detail, message: message) do 43 | %{ 44 | errors: [ 45 | %{ 46 | code: code, 47 | detail: detail, 48 | message: message 49 | } 50 | ] 51 | } 52 | end 53 | 54 | defp translate_errors(changeset), do: Changeset.traverse_errors(changeset, &translate_error/1) 55 | end 56 | -------------------------------------------------------------------------------- /priv/templates/nimble_template/Dockerfile.eex: -------------------------------------------------------------------------------- 1 | ARG ELIXIR_IMAGE_VERSION=<%= elixir_version %> 2 | ARG ERLANG_IMAGE_VERSION=<%= erlang_version %> 3 | ARG RELEASE_IMAGE_VERSION=<%= alpine_version %> 4 | 5 | FROM hexpm/elixir:${ELIXIR_IMAGE_VERSION}-erlang-${ERLANG_IMAGE_VERSION}-alpine-${RELEASE_IMAGE_VERSION} AS build 6 | 7 | RUN apk update && \ 8 | apk upgrade --no-cache && \ 9 | apk add --no-cache \ 10 | nodejs \ 11 | npm \ 12 | git \ 13 | build-base && \ 14 | mix local.rebar --force && \ 15 | mix local.hex --force 16 | <%= if web_project? do %> 17 | RUN wget -q -O /etc/apk/keys/sgerrand.rsa.pub https://alpine-pkgs.sgerrand.com/sgerrand.rsa.pub 18 | RUN wget https://github.com/sgerrand/alpine-pkg-glibc/releases/download/2.34-r0/glibc-2.34-r0.apk 19 | RUN apk add glibc-2.34-r0.apk 20 | <% end %> 21 | WORKDIR /app 22 | 23 | COPY . . 24 | 25 | ENV MIX_ENV=prod 26 | 27 | RUN mix do deps.get, deps.compile, compile 28 | <%= if web_project? do %> 29 | RUN cd assets && \ 30 | npm ci --progress=false --no-audit --loglevel=error 31 | 32 | ENV NODE_ENV=production 33 | 34 | RUN mix assets.deploy 35 | <% end %> 36 | RUN mix release 37 | 38 | # 39 | # Release 40 | # 41 | FROM alpine:${RELEASE_IMAGE_VERSION} AS app 42 | 43 | RUN apk update && \ 44 | apk add --no-cache \ 45 | libstdc++ \ 46 | libgcc \ 47 | bash \ 48 | openssl-dev 49 | 50 | WORKDIR /opt/app 51 | EXPOSE 4000 52 | 53 | RUN addgroup -g 1000 appuser && \ 54 | adduser -u 1000 -G appuser -g appuser -s /bin/sh -D appuser && \ 55 | chown 1000:1000 /opt/app 56 | 57 | COPY --from=build --chown=1000:1000 /app/_build/prod/rel/<%= otp_app %> ./ 58 | COPY bin/start.sh ./bin/start.sh 59 | 60 | USER appuser 61 | 62 | CMD bin/start.sh 63 | -------------------------------------------------------------------------------- /priv/templates/nimble_template/assets/bootstrap_css/vendor/_bootstrap.scss: -------------------------------------------------------------------------------- 1 | // Configuration 2 | @import 'bootstrap/scss/functions'; 3 | @import 'bootstrap/scss/variables'; 4 | @import 'bootstrap/scss/mixins'; 5 | @import 'bootstrap/scss/utilities'; 6 | 7 | // Re-enable these if needed. 8 | // Do not forget to check the order of import components before re-enable. 9 | 10 | // Layout & components 11 | @import 'bootstrap/scss/root'; 12 | @import 'bootstrap/scss/reboot'; 13 | @import 'bootstrap/scss/type'; 14 | // @import 'bootstrap/scss/images'; 15 | @import 'bootstrap/scss/containers'; 16 | @import 'bootstrap/scss/grid'; 17 | // @import 'bootstrap/scss/tables'; 18 | // @import 'bootstrap/scss/forms'; 19 | // @import 'bootstrap/scss/buttons'; 20 | // @import 'bootstrap/scss/transitions'; 21 | // @import 'bootstrap/scss/dropdown'; 22 | // @import 'bootstrap/scss/button-group'; 23 | // @import 'bootstrap/scss/nav'; 24 | // @import 'bootstrap/scss/navbar'; 25 | // @import 'bootstrap/scss/card'; 26 | // @import 'bootstrap/scss/accordion'; 27 | // @import 'bootstrap/scss/breadcrumb'; 28 | // @import 'bootstrap/scss/pagination'; 29 | // @import 'bootstrap/scss/badge'; 30 | // @import 'bootstrap/scss/alert'; 31 | // @import 'bootstrap/scss/progress'; 32 | // @import 'bootstrap/scss/list-group'; 33 | // @import 'bootstrap/scss/close'; 34 | // @import 'bootstrap/scss/toasts'; 35 | // @import 'bootstrap/scss/modal'; 36 | // @import 'bootstrap/scss/tooltip'; 37 | // @import 'bootstrap/scss/popover'; 38 | // @import 'bootstrap/scss/carousel'; 39 | // @import 'bootstrap/scss/spinners'; 40 | // @import 'bootstrap/scss/offcanvas'; 41 | 42 | // Helpers 43 | // @import 'bootstrap/scss/helpers'; 44 | 45 | // Utilities 46 | // @import 'bootstrap/scss/utilities/api'; 47 | -------------------------------------------------------------------------------- /lib/nimble_template/addons/variants/phoenix/api/fallback_controller.ex: -------------------------------------------------------------------------------- 1 | defmodule NimbleTemplate.Addons.Phoenix.Api.FallbackController do 2 | @moduledoc false 3 | 4 | use NimbleTemplate.Addons.Addon 5 | 6 | @impl true 7 | def do_apply!(%Project{} = project, _opts) do 8 | project 9 | |> edit_files!() 10 | |> copy_files!() 11 | end 12 | 13 | defp copy_files!(%Project{web_module: web_module, web_path: web_path} = project) do 14 | binding = [ 15 | web_module: web_module 16 | ] 17 | 18 | files = [ 19 | {:eex, "lib/otp_app_web/controllers/api/fallback_controller.ex.eex", 20 | "#{web_path}/controllers/api/fallback_controller.ex"} 21 | ] 22 | 23 | Generator.copy_file!(files, binding) 24 | 25 | project 26 | end 27 | 28 | defp edit_files!(%Project{web_module: web_module, web_path: web_entry_point} = project) do 29 | Generator.replace_content!( 30 | "#{web_entry_point}.ex", 31 | """ 32 | def controller do 33 | quote do 34 | use Phoenix.Controller, namespace: #{web_module} 35 | 36 | import Plug.Conn 37 | import #{web_module}.Gettext 38 | 39 | alias #{web_module}.ParamsValidator 40 | alias #{web_module}.Router.Helpers, as: Routes 41 | """, 42 | """ 43 | def controller do 44 | quote do 45 | use Phoenix.Controller, namespace: #{web_module} 46 | 47 | import Plug.Conn 48 | import #{web_module}.Gettext 49 | 50 | alias #{web_module}.ParamsValidator 51 | alias #{web_module}.Router.Helpers, as: Routes 52 | 53 | action_fallback #{web_module}.Api.FallbackController 54 | """ 55 | ) 56 | 57 | project 58 | end 59 | end 60 | -------------------------------------------------------------------------------- /test/nimble_template/addons/mimic_test.exs: -------------------------------------------------------------------------------- 1 | defmodule NimbleTemplate.Addons.MimicTest do 2 | use NimbleTemplate.AddonCase, async: false 3 | 4 | describe "#apply!/2" do 5 | @describetag mock_latest_package_versions: [{:mimic, "1.3.1"}] 6 | 7 | test "injects mimic to mix dependency", %{ 8 | project: project, 9 | test_project_path: test_project_path 10 | } do 11 | in_test_project!(test_project_path, fn -> 12 | Addons.Mimic.apply!(project) 13 | 14 | assert_file("mix.exs", fn file -> 15 | assert file =~ """ 16 | defp deps do 17 | [ 18 | {:mimic, "~> 1.3.1", [only: :test]}, 19 | """ 20 | end) 21 | end) 22 | end 23 | 24 | test "ensures mimic is started in test/test_helper.exs", %{ 25 | project: project, 26 | test_project_path: test_project_path 27 | } do 28 | in_test_project!(test_project_path, fn -> 29 | Addons.Mimic.apply!(project) 30 | 31 | assert_file("test/test_helper.exs", fn file -> 32 | assert file =~ """ 33 | {:ok, _} = Application.ensure_all_started(:mimic) 34 | 35 | ExUnit.start() 36 | """ 37 | end) 38 | end) 39 | end 40 | 41 | test "updates test cases", %{project: project, test_project_path: test_project_path} do 42 | in_test_project!(test_project_path, fn -> 43 | Addons.Mimic.apply!(project) 44 | 45 | assert_file("test/support/data_case.ex", fn file -> 46 | assert file =~ "use Mimic" 47 | end) 48 | 49 | assert_file("test/support/conn_case.ex", fn file -> 50 | assert file =~ "use Mimic" 51 | end) 52 | end) 53 | end 54 | end 55 | end 56 | -------------------------------------------------------------------------------- /.github/workflows/publish_to_hex_pm.yml: -------------------------------------------------------------------------------- 1 | name: Publish to Hex package manager 2 | 3 | on: 4 | release: 5 | types: [published] 6 | 7 | jobs: 8 | publish: 9 | name: Publish hex package 10 | 11 | runs-on: ubuntu-latest 12 | 13 | if: ${{ github.event_name != 'workflow_run' || github.event.workflow_run.conclusion == 'success' }} 14 | 15 | steps: 16 | - name: Cancel Previous Runs 17 | uses: styfle/cancel-workflow-action@0.9.1 18 | with: 19 | access_token: ${{ github.token }} 20 | 21 | - name: Checkout repository 22 | uses: actions/checkout@v3 23 | with: 24 | ref: ${{ github.event.workflow_run.head_branch || github.ref }} 25 | 26 | - name: Setup asdf 27 | uses: asdf-vm/actions/setup@v1 28 | 29 | - name: Cache asdf 30 | uses: actions/cache@v3 31 | with: 32 | path: /home/runner/.asdf 33 | key: ${{ runner.os }}-asdf-${{ hashFiles('**/.tool-versions') }} 34 | restore-keys: ${{ runner.os }}-asdf- 35 | 36 | - name: Install dependencies in .tool-versions 37 | uses: asdf-vm/actions/install@v1 38 | 39 | - name: Install rebar 40 | run: mix local.rebar --force 41 | 42 | - name: Install hex 43 | run: mix local.hex --force 44 | 45 | - name: Cache Elixir build 46 | uses: actions/cache@v3 47 | with: 48 | path: | 49 | _build 50 | deps 51 | key: ${{ runner.os }}-mix-${{ hashFiles('**/mix.lock') }} 52 | restore-keys: ${{ runner.os }}-mix- 53 | 54 | - name: Install Dependencies 55 | run: mix deps.get 56 | 57 | - name: Publish to Hex.pm 58 | run: mix hex.publish --yes 59 | env: 60 | HEX_API_KEY: ${{ secrets.HEX_API_KEY }} 61 | -------------------------------------------------------------------------------- /lib/nimble_template/addons/variants/phoenix/ex_vcr.ex: -------------------------------------------------------------------------------- 1 | defmodule NimbleTemplate.Addons.Phoenix.ExVCR do 2 | @moduledoc false 3 | 4 | use NimbleTemplate.Addons.Addon 5 | 6 | @cassette_directory "test/support/fixtures/vcr_cassettes" 7 | 8 | @impl true 9 | def do_apply!(%Project{} = project, _opts) do 10 | project 11 | |> edit_files!() 12 | |> create_cassette_directory!() 13 | end 14 | 15 | defp edit_files!(%Project{} = project) do 16 | project 17 | |> inject_mix_dependency!() 18 | |> edit_test_config!() 19 | |> edit_case!() 20 | end 21 | 22 | defp inject_mix_dependency!(%Project{} = project) do 23 | Generator.inject_mix_dependency!({:exvcr, latest_package_version(:exvcr), only: :test}) 24 | 25 | project 26 | end 27 | 28 | defp edit_test_config!(project) do 29 | Generator.append_content!( 30 | "config/test.exs", 31 | """ 32 | 33 | # Configurations for ExVCR 34 | config :exvcr, 35 | vcr_cassette_library_dir: "#{@cassette_directory}", 36 | ignore_localhost: true 37 | """ 38 | ) 39 | 40 | project 41 | end 42 | 43 | defp edit_case!(project) do 44 | Generator.inject_content!( 45 | "test/support/conn_case.ex", 46 | """ 47 | quote do 48 | """, 49 | """ 50 | use ExVCR.Mock, adapter: ExVCR.Adapter.Hackney 51 | 52 | """ 53 | ) 54 | 55 | Generator.inject_content!( 56 | "test/support/data_case.ex", 57 | """ 58 | quote do 59 | """, 60 | """ 61 | use ExVCR.Mock, adapter: ExVCR.Adapter.Hackney 62 | 63 | """ 64 | ) 65 | 66 | project 67 | end 68 | 69 | defp create_cassette_directory!(project) do 70 | Generator.make_directory!(@cassette_directory) 71 | 72 | project 73 | end 74 | end 75 | -------------------------------------------------------------------------------- /lib/nimble_template/addons/variants/phoenix/ecto_data_migration.ex: -------------------------------------------------------------------------------- 1 | defmodule NimbleTemplate.Addons.Phoenix.EctoDataMigration do 2 | @moduledoc false 3 | 4 | use NimbleTemplate.Addons.Addon 5 | 6 | @impl true 7 | def do_apply!(%Project{} = project, _opts) do 8 | project 9 | |> copy_files!() 10 | |> edit_files!() 11 | end 12 | 13 | defp copy_files!(%Project{} = project) do 14 | Generator.copy_file!([ 15 | {:text, "priv/repo/data_migrations/.keep", "priv/repo/data_migrations/.keep"} 16 | ]) 17 | 18 | project 19 | end 20 | 21 | defp edit_files!(%Project{} = project) do 22 | edit_mix!(project) 23 | 24 | project 25 | end 26 | 27 | defp edit_mix!(project) do 28 | Generator.inject_content!( 29 | "mix.exs", 30 | """ 31 | defp aliases do 32 | [ 33 | """, 34 | """ 35 | "ecto.migrate_all": [ 36 | "ecto.migrate --migrations-path=priv/repo/migrations --migrations-path=priv/repo/data_migrations" 37 | ], 38 | """ 39 | ) 40 | 41 | Generator.replace_content!( 42 | "mix.exs", 43 | """ 44 | end 45 | end 46 | """, 47 | """ 48 | end 49 | 50 | defp migrate(_) do 51 | if Mix.env() == :test do 52 | Mix.Task.run("ecto.migrate", ["--quiet"]) 53 | else 54 | Mix.Task.run("ecto.migrate_all", []) 55 | end 56 | end 57 | end 58 | """ 59 | ) 60 | 61 | Generator.replace_content!( 62 | "mix.exs", 63 | """ 64 | "ecto.setup": ["ecto.create", "ecto.migrate", "run priv/repo/seeds.exs"], 65 | """, 66 | """ 67 | "ecto.setup": ["ecto.create", &migrate/1, "run priv/repo/seeds.exs"], 68 | """ 69 | ) 70 | 71 | project 72 | end 73 | end 74 | -------------------------------------------------------------------------------- /test/nimble_template/addons/variants/phoenix/web/core_js_test.exs: -------------------------------------------------------------------------------- 1 | defmodule NimbleTemplate.Addons.Phoenix.Web.CoreJSTest do 2 | use NimbleTemplate.AddonCase, async: false 3 | 4 | describe "#apply!/2" do 5 | @describetag required_addons: [:"Phoenix.Web.NodePackage"] 6 | 7 | test "adds core-js into package.json", %{ 8 | project: project, 9 | test_project_path: test_project_path 10 | } do 11 | in_test_project!(test_project_path, fn -> 12 | WebAddons.CoreJS.apply!(project) 13 | 14 | assert_file("assets/package.json", fn file -> 15 | assert file =~ """ 16 | "dependencies": { 17 | "core-js": "3.22.0" 18 | """ 19 | end) 20 | end) 21 | end 22 | 23 | test "imports core-js into app.js", %{project: project, test_project_path: test_project_path} do 24 | in_test_project!(test_project_path, fn -> 25 | WebAddons.CoreJS.apply!(project) 26 | 27 | assert_file("assets/js/app.js", fn file -> 28 | assert file =~ """ 29 | // CoreJS 30 | import "core-js/stable" 31 | """ 32 | end) 33 | end) 34 | end 35 | end 36 | 37 | describe "#apply!/2 to a Live project" do 38 | @describetag live_project?: true 39 | @describetag required_addons: [:"Phoenix.Web.NodePackage"] 40 | 41 | test "adds core-js into package.json", %{ 42 | project: project, 43 | test_project_path: test_project_path 44 | } do 45 | in_test_project!(test_project_path, fn -> 46 | WebAddons.CoreJS.apply!(project) 47 | 48 | assert_file("assets/package.json", fn file -> 49 | assert file =~ """ 50 | "dependencies": { 51 | "core-js": "3.22.0" 52 | """ 53 | end) 54 | end) 55 | end 56 | end 57 | end 58 | -------------------------------------------------------------------------------- /test/nimble_template/addons/variants/phoenix/web/nimble_css_test.exs: -------------------------------------------------------------------------------- 1 | defmodule NimbleTemplate.Addons.Phoenix.Web.NimbleCSSTest do 2 | use NimbleTemplate.AddonCase, async: false 3 | 4 | describe "#apply!/2" do 5 | @describetag required_addons: [:TestEnv, :"Phoenix.Web.NodePackage", :"Phoenix.Web.StyleLint"] 6 | 7 | test "copies Nimble CSS structure", %{ 8 | project: project, 9 | test_project_path: test_project_path 10 | } do 11 | in_test_project!(test_project_path, fn -> 12 | WebAddons.NimbleCSS.apply!(project) 13 | 14 | assert_directory("assets/css/base") 15 | assert_directory("assets/css/components") 16 | assert_directory("assets/css/functions") 17 | assert_directory("assets/css/layouts") 18 | assert_directory("assets/css/mixins") 19 | assert_directory("assets/css/screens") 20 | assert_directory("assets/css/vendor") 21 | 22 | assert_file("assets/css/_variables.scss") 23 | assert_file("assets/css/app.scss") 24 | end) 25 | end 26 | 27 | test "removes the default Phoenix styles", %{ 28 | project: project, 29 | test_project_path: test_project_path 30 | } do 31 | in_test_project!(test_project_path, fn -> 32 | WebAddons.NimbleCSS.apply!(project) 33 | 34 | refute_file("assets/css/app.css") 35 | refute_file("assets/css/phoenix.css") 36 | end) 37 | end 38 | 39 | test "removes `css/app.css` and `css/phoenix.css` in assets/.stylelintrc.json", %{ 40 | project: project, 41 | test_project_path: test_project_path 42 | } do 43 | in_test_project!(test_project_path, fn -> 44 | WebAddons.NimbleCSS.apply!(project) 45 | 46 | assert_file("assets/.stylelintrc.json", fn file -> 47 | refute file =~ "css/app.css" 48 | refute file =~ "css/phoenix.css" 49 | end) 50 | end) 51 | end 52 | end 53 | end 54 | -------------------------------------------------------------------------------- /.github/workflows/verify_release_version.yml: -------------------------------------------------------------------------------- 1 | name: Verify Release version 2 | 3 | on: 4 | push: 5 | branches: 6 | - 'release/**' 7 | 8 | env: 9 | PHOENIX_VERSION: 1.6.11 10 | MIX_ENV: test 11 | 12 | jobs: 13 | release_version_test: 14 | name: Verify the Release version 15 | runs-on: ubuntu-latest 16 | 17 | steps: 18 | - name: Cancel Previous Runs 19 | uses: styfle/cancel-workflow-action@0.9.1 20 | with: 21 | access_token: ${{ github.token }} 22 | 23 | - name: Checkout repository 24 | uses: actions/checkout@v3 25 | with: 26 | ref: ${{ github.head_ref }} 27 | 28 | - name: Setup asdf 29 | uses: asdf-vm/actions/setup@v1 30 | 31 | - name: Cache asdf 32 | uses: actions/cache@v3 33 | with: 34 | path: /home/runner/.asdf 35 | key: ${{ runner.os }}-asdf-${{ hashFiles('**/.tool-versions') }} 36 | restore-keys: ${{ runner.os }}-asdf- 37 | 38 | - name: Install dependencies in .tool-versions 39 | uses: asdf-vm/actions/install@v1 40 | 41 | - name: Install rebar 42 | run: mix local.rebar --force 43 | 44 | - name: Install hex 45 | run: mix local.hex --force 46 | 47 | - name: Cache Elixir build 48 | uses: actions/cache@v3 49 | with: 50 | path: | 51 | _build 52 | deps 53 | key: ${{ runner.os }}-mix-${{ hashFiles('**/mix.lock') }} 54 | restore-keys: ${{ runner.os }}-mix- 55 | 56 | - name: Install Dependencies 57 | run: mix deps.get 58 | 59 | - name: Compile dependencies 60 | run: mix compile --warnings-as-errors --all-warnings 61 | 62 | - name: Install Phoenix ${{ env.PHOENIX_VERSION }} 63 | run: make install_phoenix PHOENIX_VERSION=${{ env.PHOENIX_VERSION }} 64 | 65 | - name: Run Tests 66 | run: mix test --only release_version 67 | -------------------------------------------------------------------------------- /priv/templates/nimble_template/test/otp_app_web/plugs/health_plug_test.exs.eex: -------------------------------------------------------------------------------- 1 | defmodule <%= web_module %>.HealthPlugTest do 2 | use <%= web_module %>.ConnCase, async: true 3 | 4 | import ExUnit.CaptureLog 5 | 6 | alias <%= web_module %>.HealthPlug 7 | 8 | describe "init/1" do 9 | test "returns given options" do 10 | assert HealthPlug.init([]) == [] 11 | end 12 | end 13 | 14 | describe "call/2" do 15 | test "given the liveness path, returns 200 status " do 16 | conn = 17 | :get 18 | |> build_conn("/liveness") 19 | |> HealthPlug.call([]) 20 | 21 | assert conn.halted == true 22 | assert response(conn, :ok) =~ "alive" 23 | end 24 | 25 | test "given the readiness path, returns 200 status " do 26 | conn = 27 | :get 28 | |> build_conn("/readiness") 29 | |> HealthPlug.call([]) 30 | 31 | assert conn.halted == true 32 | assert response(conn, :ok) =~ "ready" 33 | end 34 | 35 | test "given the database is not ready, returns 500 internal server error " do 36 | expect(Ecto.Adapters.SQL, :query, fn <%= base_module %>.Repo, "SELECT 1" -> 37 | {:error, DBConnection.ConnectionError.exception("Database connection error")} 38 | end) 39 | 40 | error_message = 41 | capture_log(fn -> 42 | conn = 43 | :get 44 | |> build_conn("/readiness") 45 | |> HealthPlug.call([]) 46 | 47 | assert conn.halted == true 48 | assert response(conn, :internal_server_error) =~ "" 49 | end) 50 | 51 | assert error_message =~ "Database connection error" 52 | end 53 | 54 | test "given a path does NOT exist, returns 404 status " do 55 | conn = 56 | :get 57 | |> build_conn("/something_else") 58 | |> HealthPlug.call([]) 59 | 60 | assert conn.halted == true 61 | assert response(conn, :not_found) =~ "" 62 | end 63 | end 64 | end 65 | -------------------------------------------------------------------------------- /lib/mix/tasks/nimble_template.gen.ex: -------------------------------------------------------------------------------- 1 | defmodule Mix.Tasks.NimbleTemplate.Gen do 2 | @shortdoc "Apply Nimble's Elixir/Phoenix template" 3 | 4 | @moduledoc """ 5 | #{@shortdoc} 6 | 7 | - Hex package: https://hex.pm/packages/nimble_template 8 | - Github: https://github.com/nimblehq/elixir-templates 9 | 10 | # Usage 11 | 12 | - mix help nimble_template.gen # Print help 13 | - mix nimble_template.gen -v # Print the version 14 | 15 | ### Phoenix application 16 | 17 | - mix nimble_template.gen --api # Apply the Phoenix API template 18 | - mix nimble_template.gen --live # Apply the Phoenix LiveView template 19 | - mix nimble_template.gen --web # Apply the Phoenix Web template 20 | 21 | ### Non-Phoenix application 22 | 23 | - mix nimble_template.gen --mix # Apply the Mix template 24 | """ 25 | 26 | use Mix.Task 27 | 28 | alias NimbleTemplate.Projects.Project 29 | alias NimbleTemplate.Templates.Template 30 | 31 | @version Mix.Project.config()[:version] 32 | @variants [api: :boolean, web: :boolean, live: :boolean, mix: :boolean] 33 | 34 | def run([args]) when args in ~w(-v --version), do: Mix.shell().info("NimbleTemplate v#{@version}") 35 | 36 | def run(args) do 37 | if Mix.Project.umbrella?() do 38 | Mix.raise("mix nimble_template.gen can only be run inside an application directory") 39 | end 40 | 41 | {opts, _params} = parse_opts(args) 42 | 43 | {:ok, _} = Application.ensure_all_started(:httpoison) 44 | 45 | opts 46 | |> Project.new() 47 | |> Template.apply!() 48 | end 49 | 50 | defp parse_opts(args) do 51 | case OptionParser.parse(args, strict: @variants) do 52 | {opts, args, []} -> 53 | {opts, args} 54 | 55 | {_opts, _args, [switch | _]} -> 56 | Mix.raise("Invalid option: " <> humanize_variant_option(switch)) 57 | end 58 | end 59 | 60 | defp humanize_variant_option({name, nil}), do: name 61 | defp humanize_variant_option({name, val}), do: name <> "=" <> val 62 | end 63 | -------------------------------------------------------------------------------- /.github/wiki/Generation.md: -------------------------------------------------------------------------------- 1 | ## Manually 2 | 3 | 1. Clone [Elixir templates](https://github.com/nimblehq/elixir-templates) project to your local directory. 4 | 5 | 2. Create a new project using `mix phx.new sample_project` or `mix new sample_project` command 6 | 7 | 3. Answer the prompt `Fetch and install dependencies? [Yn]` with `n` (not install) 8 | 9 | 4. Change directory into `sample_project` using `cd sample_project` command 10 | 11 | 5. Edit `mix.exs` file and add this line to the dependencies. 12 | 13 | ```elixir 14 | {:nimble_template, "~> 3.0", path: "~/elixir-templates", only: :dev, runtime: false} 15 | ``` 16 | 17 | Modify the path to locate the `elixir-templates` directory. For example `~/elixir-templates` if inside the home directory. 18 | 19 | 6. Then go back to the Terminal and run 20 | 21 | `mix do deps.get, deps.compile` 22 | 23 | 7. Choose which template you want to use inside your project 24 | 25 | - Web: `mix nimble_template.gen --web` 26 | - Live: `mix nimble_template.gen --live` 27 | - API: `mix nimble_template.gen --api` 28 | 29 | 8. Answer the interactive prompt that will be displayed during the setup 30 | 31 | ## Using the Makefile 32 | 33 | 1. Clone Elixir templates project to your local directory. 34 | 35 | 2. Generate a new Phoenix project 36 | 37 | - Web project: `make create_phoenix_project PROJECT_DIRECTORY=sample_project OPTIONS="--no-live"` 38 | - Live project: `make create_phoenix_project PROJECT_DIRECTORY=sample_project OPTIONS=""` 39 | - API project: `make create_phoenix_project PROJECT_DIRECTORY=sample_project OPTIONS="--no-html --no-assets --no-live"` 40 | 41 | 3. Answer the prompt `Fetch and install dependencies? [Yn]` with `n` (not install) 42 | 43 | 4. Apply the template 44 | 45 | - Web variant: `make apply_phoenix_template PROJECT_DIRECTORY=sample_project VARIANT=web` 46 | - Live variant: `make apply_phoenix_template PROJECT_DIRECTORY=sample_project VARIANT=live` 47 | - API variant: `make apply_phoenix_template PROJECT_DIRECTORY=sample_project VARIANT=api` 48 | -------------------------------------------------------------------------------- /lib/nimble_template/addons/variants/phoenix/web/style_lint.ex: -------------------------------------------------------------------------------- 1 | defmodule NimbleTemplate.Addons.Phoenix.Web.StyleLint do 2 | @moduledoc false 3 | 4 | use NimbleTemplate.Addons.Addon 5 | 6 | @impl true 7 | def do_apply!(%Project{} = project, _opts) do 8 | project 9 | |> edit_files!() 10 | |> copy_files!() 11 | end 12 | 13 | defp edit_files!(%Project{} = project) do 14 | project 15 | |> edit_assets_package!() 16 | |> edit_mix!() 17 | end 18 | 19 | defp copy_files!(%Project{} = project) do 20 | Generator.copy_file!([{:text, "assets/.stylelintrc.json", "assets/.stylelintrc.json"}]) 21 | 22 | project 23 | end 24 | 25 | defp edit_assets_package!(%Project{} = project) do 26 | Generator.replace_content!( 27 | "assets/package.json", 28 | """ 29 | "scripts": { 30 | """, 31 | """ 32 | "scripts": { 33 | "stylelint": "stylelint --color ./css", 34 | "stylelint.fix": "stylelint --color --fix ./css", 35 | """ 36 | ) 37 | 38 | Generator.replace_content!( 39 | "assets/package.json", 40 | """ 41 | "devDependencies": { 42 | """, 43 | """ 44 | "devDependencies": { 45 | "stylelint": "14.7.1", 46 | "stylelint-config-property-sort-order-smacss": "9.0.0", 47 | "stylelint-config-sass-guidelines": "9.0.1", 48 | """ 49 | ) 50 | 51 | project 52 | end 53 | 54 | defp edit_mix!(%Project{} = project) do 55 | Generator.replace_content!( 56 | "mix.exs", 57 | """ 58 | codebase: [ 59 | """, 60 | """ 61 | codebase: [ 62 | "cmd npm run stylelint --prefix assets", 63 | """ 64 | ) 65 | 66 | Generator.replace_content!( 67 | "mix.exs", 68 | """ 69 | "codebase.fix": [ 70 | """, 71 | """ 72 | "codebase.fix": [ 73 | "cmd npm run stylelint.fix --prefix assets", 74 | """ 75 | ) 76 | 77 | project 78 | end 79 | end 80 | -------------------------------------------------------------------------------- /lib/nimble_template/templates/variants/mix/template.ex: -------------------------------------------------------------------------------- 1 | defmodule NimbleTemplate.Templates.Mix.Template do 2 | @moduledoc false 3 | 4 | import NimbleTemplate.{AddonHelper, GithubHelper} 5 | 6 | alias NimbleTemplate.Addons 7 | alias NimbleTemplate.Projects.Project 8 | 9 | def pre_apply(%Project{} = project), do: install_addon_prompt(project, Addons.Mimic) 10 | 11 | def apply!(%Project{} = project) do 12 | project 13 | |> apply_default_mix_addons() 14 | |> apply_optional_mix_addons() 15 | end 16 | 17 | defp apply_default_mix_addons(project) do 18 | project 19 | |> Addons.AsdfToolVersion.apply!() 20 | |> Addons.Readme.apply!() 21 | |> Addons.TestEnv.apply!() 22 | |> Addons.Credo.apply!() 23 | |> Addons.Dialyxir.apply!() 24 | |> Addons.ExCoveralls.apply!() 25 | |> Addons.Faker.apply!() 26 | |> Addons.Git.apply!() 27 | |> Addons.TestInteractive.apply!() 28 | |> Addons.Iex.apply!() 29 | end 30 | 31 | defp apply_optional_mix_addons(%Project{optional_addons: optional_addons} = project) do 32 | if host_on_github?() do 33 | if generate_github_template?(), 34 | do: Addons.Github.apply!(project, %{github_template: true}) 35 | 36 | generate_github_action_test? = generate_github_action_test?() 37 | 38 | if generate_github_action_test?, 39 | do: Addons.Github.apply!(project, %{github_action_test: true}) 40 | 41 | generate_github_wiki? = generate_github_wiki?() 42 | 43 | if generate_github_wiki?, 44 | do: Addons.Github.apply!(project, %{github_wiki: true}) 45 | 46 | if generate_github_workflows_readme?(), 47 | do: 48 | Addons.Github.apply!(project, %{ 49 | github_workflows_readme: true, 50 | with_test_workflow?: generate_github_action_test?, 51 | with_github_wiki?: generate_github_wiki?, 52 | with_deploy_to_heroku_workflow?: false 53 | }) 54 | end 55 | 56 | if Addons.Mimic in optional_addons, do: Addons.Mimic.apply!(project) 57 | 58 | project 59 | end 60 | end 61 | -------------------------------------------------------------------------------- /lib/nimble_template/templates/template.ex: -------------------------------------------------------------------------------- 1 | defmodule NimbleTemplate.Templates.Template do 2 | @moduledoc false 3 | 4 | alias NimbleTemplate.Addons.ExUnit 5 | alias NimbleTemplate.{CredoHelper, DependencyHelper} 6 | alias NimbleTemplate.Projects.Project 7 | alias NimbleTemplate.Templates.Mix.Template, as: MixTemplate 8 | alias NimbleTemplate.Templates.Phoenix.Template, as: PhoenixTemplate 9 | 10 | def apply!(%Project{mix_project?: true} = project) do 11 | project 12 | |> MixTemplate.pre_apply() 13 | |> MixTemplate.apply!() 14 | 15 | ExUnit.apply!(project) 16 | 17 | post_apply!(project) 18 | end 19 | 20 | def apply!(%Project{mix_project?: false} = project) do 21 | project 22 | |> PhoenixTemplate.pre_apply() 23 | |> PhoenixTemplate.apply!() 24 | 25 | ExUnit.apply!(project) 26 | 27 | post_apply!(project) 28 | end 29 | 30 | defp post_apply!(%Project{mix_project?: true} = project) do 31 | DependencyHelper.order_dependencies!() 32 | install_elixir_dependencies() 33 | CredoHelper.suppress_credo_warnings_for_base_project(project) 34 | format_codebase() 35 | end 36 | 37 | defp post_apply!(%Project{api_project?: true} = project) do 38 | DependencyHelper.order_dependencies!() 39 | install_elixir_dependencies() 40 | CredoHelper.suppress_credo_warnings_for_phoenix_api_project(project) 41 | format_codebase() 42 | end 43 | 44 | defp post_apply!(%Project{web_project?: true} = project) do 45 | DependencyHelper.order_dependencies!() 46 | install_elixir_dependencies() 47 | install_node_dependencies() 48 | CredoHelper.suppress_credo_warnings_for_phoenix_project(project) 49 | format_codebase() 50 | end 51 | 52 | defp install_elixir_dependencies() do 53 | Mix.shell().cmd("MIX_ENV=dev mix do deps.get, deps.compile") 54 | Mix.shell().cmd("MIX_ENV=test mix do deps.get, deps.compile") 55 | end 56 | 57 | defp install_node_dependencies(), do: Mix.shell().cmd("npm install --prefix assets") 58 | 59 | defp format_codebase(), do: Mix.shell().cmd("mix codebase.fix") 60 | end 61 | -------------------------------------------------------------------------------- /test/nimble_template/addons/asdf_tool_version_test.exs: -------------------------------------------------------------------------------- 1 | defmodule NimbleTemplate.Addons.AsdfToolVersionTest do 2 | use NimbleTemplate.AddonCase, async: false 3 | 4 | alias NimbleTemplate.Addons.AsdfToolVersion 5 | 6 | describe "#apply!/2 with web_project" do 7 | test "copies the .tool-versions", %{ 8 | project: project, 9 | test_project_path: test_project_path 10 | } do 11 | in_test_project!(test_project_path, fn -> 12 | AsdfToolVersion.apply!(project) 13 | 14 | assert_file(".tool-versions", fn file -> 15 | assert file =~ """ 16 | erlang 25.2.3 17 | elixir 1.14.3-otp-25 18 | nodejs 18.14.2 19 | """ 20 | end) 21 | end) 22 | end 23 | end 24 | 25 | describe "#apply!/2 with api_project" do 26 | test "copies the .tool-versions", %{ 27 | project: project, 28 | test_project_path: test_project_path 29 | } do 30 | project = %{project | api_project?: true, web_project?: false} 31 | 32 | in_test_project!(test_project_path, fn -> 33 | AsdfToolVersion.apply!(project) 34 | 35 | assert_file(".tool-versions", fn file -> 36 | assert file =~ """ 37 | erlang 25.2.3 38 | elixir 1.14.3-otp-25 39 | """ 40 | 41 | refute file =~ "nodejs 18.14.2" 42 | end) 43 | end) 44 | end 45 | end 46 | 47 | describe "#apply!/2 with mix_project" do 48 | @describetag mix_project?: true 49 | 50 | test "copies the .tool-versions", %{ 51 | project: project, 52 | test_project_path: test_project_path 53 | } do 54 | in_test_project!(test_project_path, fn -> 55 | AsdfToolVersion.apply!(project) 56 | 57 | assert_file(".tool-versions", fn file -> 58 | assert file =~ """ 59 | erlang 25.2.3 60 | elixir 1.14.3-otp-25 61 | """ 62 | 63 | refute file =~ "nodejs 18.14.2" 64 | end) 65 | end) 66 | end 67 | end 68 | end 69 | -------------------------------------------------------------------------------- /priv/templates/nimble_template/.github/workflows/README.md.eex: -------------------------------------------------------------------------------- 1 | # Github Actions 2 | 3 | The following workflows are supported. 4 | 5 | <%= if with_test_workflow? do %>- [Test](#test)<% end %> 6 | <%= if with_github_wiki? do %>- [Publish Wiki](#publish-wiki)<% end %> 7 | <%= if with_deploy_to_heroku_workflow? do %>- [Deploy to Heroku](#deploy-to-heroku-workflow-usage-instruction)<% end %> 8 | 9 | <%= if with_test_workflow? do %>## Test 10 | 11 | Runs a workflow to test the codebase formatting/linting, database seeds successfully seeding, and unit tests.<% end %> 12 | 13 | <%= if with_github_wiki? do %>## Publish Wiki 14 | 15 | Runs a workflow to publish updates of the `.github/wiki/` folder to the Github Wiki.<% end %> 16 | 17 | <%= if with_deploy_to_heroku_workflow? do %>## Deploy to Heroku Workflow usage instruction 18 | 19 | ### Requirements 20 | 21 | - A pre-generated [Heroku App](https://devcenter.heroku.com/articles/creating-apps) 22 | - A Heroku API key. It can be generated under your [Account Settings](https://dashboard.heroku.com/account#api-key) 23 | - Three Heroku config vars: 24 | - **DATABASE_URL**: It will be created automatically when the [PostgreSQL add-on](https://elements.heroku.com/addons/heroku-postgresql) is added. 25 | - **PHX_HOST**: if your app name is `acme`, the value of this var is: `acme.herokuapp.com` 26 | - **HEALTH_PATH**: Health path (eg: "/_health") 27 | - **SECRET_KEY_BASE**: use the `mix phx.gen.secret` to get a new secret and set it as the value of this var. 28 | 29 | ### How to use 30 | 31 | - Defining two [Github secrets](https://docs.github.com/en/actions/reference/encrypted-secrets) to hold the value of Heroku API key and Heroku app name: 32 | - HEROKU_API_KEY 33 | - HEROKU_APP_NAME 34 | - If you plan on using WebSockets, the timeout for the WebSocket transport needs to be decreased in `lib/hello_web/endpoint.ex`. 35 | 36 | ```elixir 37 | socket "/socket", HelloWeb.UserSocket, 38 | websocket: [timeout: 45_000], 39 | longpoll: false 40 | ... 41 | ``` 42 | 43 | otherwise, leaving it set to `false` as default.<% end %> 44 | -------------------------------------------------------------------------------- /priv/templates/nimble_template/lib/otp_app_web/views/error_helpers.ex.eex: -------------------------------------------------------------------------------- 1 | defmodule <%= web_module %>.ErrorHelpers do 2 | @moduledoc """ 3 | Conveniences for translating and building error messages. 4 | """ 5 | 6 | use Phoenix.HTML 7 | 8 | alias Plug.Conn.Status 9 | 10 | @doc """ 11 | Generates tag for inlined form input errors. 12 | """ 13 | def error_tag(form, field) do 14 | Enum.map(Keyword.get_values(form.errors, field), fn error -> 15 | content_tag(:span, translate_error(error), 16 | class: "invalid-feedback", 17 | phx_feedback_for: input_id(form, field) 18 | ) 19 | end) 20 | end 21 | 22 | @doc """ 23 | Translates an error message using gettext. 24 | """ 25 | def translate_error({msg, opts}) do 26 | # When using gettext, we typically pass the strings we want 27 | # to translate as a static argument: 28 | # 29 | # # Translate "is invalid" in the "errors" domain 30 | # dgettext("errors", "is invalid") 31 | # 32 | # # Translate the number of files with plural rules 33 | # dngettext("errors", "1 file", "%{count} files", count) 34 | # 35 | # Because the error messages we show in our forms and APIs 36 | # are defined inside Ecto, we need to translate them dynamically. 37 | # This requires us to call the Gettext module passing our gettext 38 | # backend as first argument. 39 | # 40 | # Note we use the "errors" domain, which means translations 41 | # should be written to the errors.po file. The :count option is 42 | # set by Ecto and indicates we should also apply plural rules. 43 | if count = opts[:count] do 44 | Gettext.dngettext(<%= web_module %>.Gettext, "errors", msg, msg, count, opts) 45 | else 46 | Gettext.dgettext(<%= web_module %>.Gettext, "errors", msg, opts) 47 | end 48 | end 49 | 50 | def status_code_from_template(template) do 51 | template 52 | |> String.split(".") 53 | |> hd() 54 | |> String.to_integer() 55 | |> Status.reason_atom() 56 | rescue 57 | _ -> :internal_server_error 58 | end 59 | end 60 | -------------------------------------------------------------------------------- /test/nimble_template/helpers/credo_test.exs: -------------------------------------------------------------------------------- 1 | defmodule NimbleTemplate.CredoHelperTest do 2 | use NimbleTemplate.AddonCase, async: false 3 | 4 | alias NimbleTemplate.CredoHelper 5 | 6 | describe "suppress_credo_warnings_for_base_project/1" do 7 | test "prepends credo rule disabling in the given file", %{ 8 | test_project_path: test_project_path, 9 | project: project 10 | } do 11 | in_test_project!(test_project_path, fn -> 12 | CredoHelper.suppress_credo_warnings_for_base_project(project) 13 | 14 | assert_file("#{test_project_path}/lib/nimble_template.ex", fn file -> 15 | assert file =~ """ 16 | # credo:disable-for-this-file CompassCredoPlugin.Check.DoSingleExpression 17 | """ 18 | end) 19 | end) 20 | end 21 | end 22 | 23 | describe "suppress_credo_warnings_for_phoenix_project/1" do 24 | test "prepends credo rule disabling in the given file", %{ 25 | test_project_path: test_project_path, 26 | project: project 27 | } do 28 | in_test_project!(test_project_path, fn -> 29 | CredoHelper.suppress_credo_warnings_for_phoenix_project(project) 30 | 31 | assert_file( 32 | "#{test_project_path}/lib/nimble_template_web/controllers/page_controller.ex", 33 | fn file -> 34 | assert file =~ """ 35 | # credo:disable-for-this-file CompassCredoPlugin.Check.DoSingleExpression 36 | """ 37 | end 38 | ) 39 | 40 | assert_file("#{test_project_path}/lib/nimble_template_web/telemetry.ex", fn file -> 41 | assert file =~ """ 42 | # credo:disable-for-this-file CompassCredoPlugin.Check.DoSingleExpression 43 | """ 44 | end) 45 | 46 | assert_file("#{test_project_path}/lib/nimble_template_web/views/error_view.ex", fn file -> 47 | assert file =~ """ 48 | # credo:disable-for-this-file CompassCredoPlugin.Check.DoSingleExpression 49 | """ 50 | end) 51 | end) 52 | end 53 | end 54 | end 55 | -------------------------------------------------------------------------------- /lib/nimble_template/addons/variants/phoenix/api/params_validation.ex: -------------------------------------------------------------------------------- 1 | defmodule NimbleTemplate.Addons.Phoenix.Api.ParamsValidation do 2 | @moduledoc false 3 | 4 | use NimbleTemplate.Addons.Addon 5 | 6 | def do_apply!(%Project{} = project, _opts) do 7 | project 8 | |> copy_files!() 9 | |> edit_files!() 10 | end 11 | 12 | defp edit_files!(project) do 13 | edit_web_entry_point!(project) 14 | end 15 | 16 | defp edit_web_entry_point!(%Project{web_module: web_module, web_path: web_entry_point} = project) do 17 | Generator.replace_content!( 18 | "#{web_entry_point}.ex", 19 | """ 20 | def controller do 21 | quote do 22 | use Phoenix.Controller, namespace: #{web_module} 23 | 24 | import Plug.Conn 25 | import #{web_module}.Gettext 26 | alias #{web_module}.Router.Helpers, as: Routes 27 | """, 28 | """ 29 | def controller do 30 | quote do 31 | use Phoenix.Controller, namespace: #{web_module} 32 | 33 | import Plug.Conn 34 | import #{web_module}.Gettext 35 | 36 | alias #{web_module}.ParamsValidator 37 | alias #{web_module}.Router.Helpers, as: Routes 38 | """ 39 | ) 40 | 41 | project 42 | end 43 | 44 | defp copy_files!( 45 | %Project{ 46 | base_module: base_module, 47 | web_module: web_module, 48 | web_path: web_path, 49 | web_test_path: web_test_path 50 | } = project 51 | ) do 52 | binding = [ 53 | web_module: web_module, 54 | base_module: base_module 55 | ] 56 | 57 | files = [ 58 | {:eex, "lib/otp_app_web/params/params.ex.eex", "#{web_path}/params/params.ex"}, 59 | {:eex, "lib/otp_app_web/params/params_validator.ex.eex", 60 | "#{web_path}/params/params_validator.ex"}, 61 | {:eex, "test/otp_app_web/params/params_validator_test.exs.eex", 62 | "#{web_test_path}/params/params_validator_test.exs"} 63 | ] 64 | 65 | Generator.copy_file!(files, binding) 66 | 67 | project 68 | end 69 | end 70 | -------------------------------------------------------------------------------- /test/nimble_template/addons/test_interactive_test.exs: -------------------------------------------------------------------------------- 1 | defmodule NimbleTemplate.Addons.TestInteractiveTest do 2 | use NimbleTemplate.AddonCase, async: false 3 | 4 | describe "#apply!/2" do 5 | @describetag mock_latest_package_versions: [{:mix_test_interactive, "1.2"}] 6 | 7 | test "injects mix_test_interactive to mix dependencies list", %{ 8 | project: project, 9 | test_project_path: test_project_path 10 | } do 11 | in_test_project!(test_project_path, fn -> 12 | Addons.TestInteractive.apply!(project) 13 | 14 | assert_file("mix.exs", fn file -> 15 | assert file =~ """ 16 | defp deps do 17 | [ 18 | {:mix_test_interactive, "~> 1.2", [only: :dev, runtime: false]}, 19 | """ 20 | end) 21 | end) 22 | end 23 | 24 | test "injects mix_test_interactive config to the dev config", %{ 25 | project: project, 26 | test_project_path: test_project_path 27 | } do 28 | in_test_project!(test_project_path, fn -> 29 | Addons.TestInteractive.apply!(project) 30 | 31 | assert_file("config/dev.exs", fn file -> 32 | assert file =~ """ 33 | config :mix_test_interactive, 34 | clear: true 35 | """ 36 | end) 37 | end) 38 | end 39 | end 40 | 41 | describe "#apply!/2 with mix_project" do 42 | @describetag mix_project?: true 43 | @describetag mock_latest_package_versions: [{:mix_test_interactive, "1.2"}] 44 | 45 | test "injects mix_test_interactive to mix dependencies list", %{ 46 | project: project, 47 | test_project_path: test_project_path 48 | } do 49 | in_test_project!(test_project_path, fn -> 50 | Addons.TestInteractive.apply!(project) 51 | 52 | assert_file("mix.exs", fn file -> 53 | assert file =~ """ 54 | defp deps do 55 | [ 56 | {:mix_test_interactive, "~> 1.2", [only: :dev, runtime: false]}, 57 | """ 58 | end) 59 | end) 60 | end 61 | end 62 | end 63 | -------------------------------------------------------------------------------- /test/nimble_template/helpers/addon_test.exs: -------------------------------------------------------------------------------- 1 | defmodule NimbleTemplate.AddonTest do 2 | use NimbleTemplate.AddonCase, async: false 3 | 4 | import ExUnit.CaptureIO 5 | 6 | alias NimbleTemplate.AddonHelper 7 | alias NimbleTemplate.Projects.Project 8 | 9 | describe "install_addon_prompt/3" do 10 | test "when response prompt with 'Y', prepends the addon to the project", %{project: project} do 11 | display_prompt = 12 | capture_io([input: "Y\n"], fn -> 13 | project = 14 | project 15 | |> Project.prepend_optional_addon(NimbleTemplate.Addons.FirstAddon) 16 | |> AddonHelper.install_addon_prompt(NimbleTemplate.Addons.SecondAddon) 17 | 18 | assert project.optional_addons == [ 19 | NimbleTemplate.Addons.SecondAddon, 20 | NimbleTemplate.Addons.FirstAddon 21 | ] 22 | end) 23 | 24 | assert display_prompt =~ "Would you like to add the SecondAddon addon? [Yn]" 25 | end 26 | 27 | test "when response prompt with 'N', does not prepend the addon to the project", %{ 28 | project: project 29 | } do 30 | display_prompt = 31 | capture_io([input: "N\n"], fn -> 32 | project = 33 | project 34 | |> Project.prepend_optional_addon(NimbleTemplate.Addons.FirstAddon) 35 | |> AddonHelper.install_addon_prompt(NimbleTemplate.Addons.SecondAddon) 36 | 37 | assert project.optional_addons == [ 38 | NimbleTemplate.Addons.FirstAddon 39 | ] 40 | end) 41 | 42 | assert display_prompt =~ "Would you like to add the SecondAddon addon? [Yn]" 43 | end 44 | 45 | test "given addon label, shows prompts with the given label", %{ 46 | project: project 47 | } do 48 | display_prompt = 49 | capture_io(fn -> 50 | AddonHelper.install_addon_prompt( 51 | project, 52 | NimbleTemplate.Addons.FirstAddon, 53 | "Custom addon name" 54 | ) 55 | end) 56 | 57 | assert display_prompt =~ "Would you like to add the Custom addon name addon? [Yn]" 58 | end 59 | end 60 | end 61 | -------------------------------------------------------------------------------- /.github/workflows/reusable_mix_project.yml: -------------------------------------------------------------------------------- 1 | name: Reusable Mix project 2 | 3 | on: 4 | workflow_call: 5 | inputs: 6 | new_project_options: 7 | required: true 8 | type: string 9 | 10 | env: 11 | BASE_PROJECT_DIRECTORY: sample_project 12 | MIX_ENV: dev 13 | 14 | jobs: 15 | unit_test: 16 | name: Unit test 17 | runs-on: ubuntu-latest 18 | 19 | steps: 20 | - name: Cancel Previous Runs 21 | uses: styfle/cancel-workflow-action@0.9.1 22 | with: 23 | access_token: ${{ github.token }} 24 | 25 | - name: Checkout repository 26 | uses: actions/checkout@v3 27 | with: 28 | ref: ${{ github.head_ref }} 29 | 30 | - name: Setup asdf 31 | uses: asdf-vm/actions/setup@v1 32 | 33 | - name: Cache asdf 34 | uses: actions/cache@v3 35 | with: 36 | path: /home/runner/.asdf 37 | key: ${{ runner.os }}-asdf-${{ hashFiles('**/.tool-versions') }} 38 | restore-keys: ${{ runner.os }}-asdf- 39 | 40 | - name: Install dependencies in .tool-versions 41 | uses: asdf-vm/actions/install@v1 42 | 43 | - name: Install rebar 44 | run: mix local.rebar --force 45 | 46 | - name: Install hex 47 | run: mix local.hex --force 48 | 49 | - name: Create Mix project 50 | run: make create_mix_project PROJECT_DIRECTORY=$BASE_PROJECT_DIRECTORY OPTIONS="${{ inputs.new_project_options }}" 51 | 52 | - name: Apply Mix template 53 | run: make apply_mix_template PROJECT_DIRECTORY=$BASE_PROJECT_DIRECTORY 54 | 55 | # Verify the new project after applying the nimble_template 56 | 57 | - name: Install Elixir Dependencies 58 | run: cd $BASE_PROJECT_DIRECTORY && mix deps.get 59 | 60 | - name: Compile dependencies 61 | run: cd $BASE_PROJECT_DIRECTORY && mix compile --warnings-as-errors --all-warnings 62 | 63 | - name: Run mix codebase 64 | run: cd $BASE_PROJECT_DIRECTORY && mix codebase 65 | env: 66 | MIX_ENV: test 67 | 68 | - name: Run mix test 69 | run: cd $BASE_PROJECT_DIRECTORY && mix test 70 | env: 71 | MIX_ENV: test 72 | -------------------------------------------------------------------------------- /lib/nimble_template/addons/variants/phoenix/mix_release.ex: -------------------------------------------------------------------------------- 1 | defmodule NimbleTemplate.Addons.Phoenix.MixRelease do 2 | @moduledoc false 3 | 4 | use NimbleTemplate.Addons.Addon 5 | 6 | @impl true 7 | def do_apply!(%Project{} = project, _opts) do 8 | project 9 | |> copy_files!() 10 | |> edit_files!() 11 | end 12 | 13 | defp copy_files!( 14 | %Project{otp_app: otp_app, base_module: base_module, base_path: base_path} = project 15 | ) do 16 | Generator.copy_file!( 17 | [ 18 | {:eex, "lib/otp_app/release_tasks.ex.eex", base_path <> "/release_tasks.ex"} 19 | ], 20 | otp_app: otp_app, 21 | base_module: base_module 22 | ) 23 | 24 | project 25 | end 26 | 27 | defp edit_files!(%{otp_app: otp_app, web_module: web_module} = project) do 28 | Generator.delete_content!( 29 | "config/runtime.exs", 30 | """ 31 | 32 | # ## Using releases 33 | # 34 | # If you use `mix release`, you need to explicitly enable the server 35 | # by passing the PHX_SERVER=true when you start it: 36 | # 37 | # PHX_SERVER=true bin/#{otp_app} start 38 | # 39 | # Alternatively, you can use `mix phx.gen.release` to generate a `bin/server` 40 | # script that automatically sets the env var above. 41 | if System.get_env("PHX_SERVER") do 42 | config :#{otp_app}, #{web_module}.Endpoint, server: true 43 | end 44 | 45 | """ 46 | ) 47 | 48 | Generator.replace_content!( 49 | "config/runtime.exs", 50 | """ 51 | config :#{otp_app}, #{web_module}.Endpoint, 52 | """, 53 | """ 54 | config :#{otp_app}, #{web_module}.Endpoint, 55 | server: true, 56 | """ 57 | ) 58 | 59 | Generator.replace_content!( 60 | "config/runtime.exs", 61 | """ 62 | host = System.get_env("PHX_HOST") || "example.com" 63 | """, 64 | """ 65 | host = 66 | System.get_env("PHX_HOST") || 67 | raise \"\"\" 68 | Environment variable PHX_HOST is missing. 69 | Set the Heroku endpoint to this variable. 70 | \"\"\" 71 | """ 72 | ) 73 | 74 | project 75 | end 76 | end 77 | -------------------------------------------------------------------------------- /priv/templates/nimble_template/test/otp_app_web/plugs/check_empty_body_params_plug_test.exs.eex: -------------------------------------------------------------------------------- 1 | defmodule <%= web_module %>.CheckEmptyBodyParamsPlugTest do 2 | use <%= web_module %>.ConnCase, async: true 3 | 4 | alias <%= web_module %>.CheckEmptyBodyParamsPlug 5 | 6 | describe "init/1" do 7 | test "returns given options" do 8 | assert CheckEmptyBodyParamsPlug.init([]) == [] 9 | end 10 | end 11 | 12 | describe "call/2" do 13 | test "given body params are empty on POST request, halts the conn and returns 400 status", 14 | %{ 15 | conn: conn 16 | } do 17 | conn = 18 | conn 19 | |> Map.put(:body_params, %{}) 20 | |> Map.put(:method, "POST") 21 | |> CheckEmptyBodyParamsPlug.call([]) 22 | 23 | assert conn.halted == true 24 | 25 | assert json_response(conn, 400) == %{ 26 | "errors" => [ 27 | %{ 28 | "code" => "bad_request", 29 | "detail" => %{}, 30 | "message" => "Missing body params" 31 | } 32 | ] 33 | } 34 | end 35 | 36 | test "given body params are NOT empty on POST request, does NOT halt the conn", %{conn: conn} do 37 | conn = 38 | conn 39 | |> Map.put(:body_params, %{name: "Android Phone"}) 40 | |> Map.put(:method, "POST") 41 | |> CheckEmptyBodyParamsPlug.call([]) 42 | 43 | assert conn.halted == false 44 | end 45 | 46 | test "given body params are empty on GET request, does NOT halt the conn", %{ 47 | conn: conn 48 | } do 49 | conn = 50 | conn 51 | |> Map.put(:body_params, %{}) 52 | |> Map.put(:method, "GET") 53 | |> CheckEmptyBodyParamsPlug.call([]) 54 | 55 | assert conn.halted == false 56 | end 57 | 58 | test "given body params are empty on PATCH request, does NOT halt the conn", %{ 59 | conn: conn 60 | } do 61 | conn = 62 | conn 63 | |> Map.put(:body_params, %{}) 64 | |> Map.put(:method, "PATCH") 65 | |> CheckEmptyBodyParamsPlug.call([]) 66 | 67 | assert conn.halted == false 68 | end 69 | end 70 | end 71 | -------------------------------------------------------------------------------- /test/nimble_template/addons/variants/ex_vcr_test.exs: -------------------------------------------------------------------------------- 1 | defmodule NimbleTemplate.Addons.Phoenix.ExVCRTest do 2 | use NimbleTemplate.AddonCase 3 | 4 | describe "#apply!/2" do 5 | @describetag mock_latest_package_versions: [{:exvcr, "0.12.2"}] 6 | 7 | test "injects ExVCR to mix dependency", %{ 8 | project: project, 9 | test_project_path: test_project_path 10 | } do 11 | in_test_project!(test_project_path, fn -> 12 | PhoenixAddons.ExVCR.apply!(project) 13 | 14 | assert_file("mix.exs", fn file -> 15 | assert file =~ "{:exvcr, \"~> 0.12.2\", [only: :test]}" 16 | end) 17 | end) 18 | end 19 | 20 | test "updates configurations for test env", %{ 21 | project: project, 22 | test_project_path: test_project_path 23 | } do 24 | in_test_project!(test_project_path, fn -> 25 | PhoenixAddons.ExVCR.apply!(project) 26 | 27 | assert_file("config/test.exs", fn file -> 28 | assert file =~ """ 29 | # Configurations for ExVCR 30 | config :exvcr, 31 | vcr_cassette_library_dir: "test/support/fixtures/vcr_cassettes", 32 | ignore_localhost: true 33 | """ 34 | end) 35 | end) 36 | end 37 | 38 | test "updates test cases", %{project: project, test_project_path: test_project_path} do 39 | in_test_project!(test_project_path, fn -> 40 | PhoenixAddons.ExVCR.apply!(project) 41 | 42 | assert_file("test/support/data_case.ex", fn file -> 43 | assert file =~ "use ExVCR.Mock, adapter: ExVCR.Adapter.Hackney" 44 | end) 45 | 46 | assert_file("test/support/conn_case.ex", fn file -> 47 | assert file =~ "use ExVCR.Mock, adapter: ExVCR.Adapter.Hackney" 48 | end) 49 | end) 50 | end 51 | 52 | test "creates cassettes directory with .keep file", %{ 53 | project: project, 54 | test_project_path: test_project_path 55 | } do 56 | in_test_project!(test_project_path, fn -> 57 | PhoenixAddons.ExVCR.apply!(project) 58 | 59 | assert(File.exists?("test/support/fixtures/vcr_cassettes/.keep")) == true 60 | end) 61 | end 62 | end 63 | end 64 | -------------------------------------------------------------------------------- /priv/templates/nimble_template/.github/wiki/Getting-Started.md.eex: -------------------------------------------------------------------------------- 1 | ## Erlang & Elixir 2 | 3 | - Erlang <%= erlang_version %> 4 | 5 | - Elixir <%= elixir_version %> 6 | <%= if web_project? do %> 7 | ### Node 8 | 9 | - Node <%= node_asdf_version %> 10 | <% end %> 11 | - Recommended version manager. 12 | 13 | - [asdf](https://github.com/asdf-vm/asdf) 14 | - [asdf-erlang](https://github.com/asdf-vm/asdf-erlang) 15 | - [asdf-elixir](https://github.com/asdf-vm/asdf-elixir) 16 | <%= if web_project? do %> 17 | - [asdf-node](https://github.com/asdf-vm/asdf-node) 18 | <% end %> 19 | 20 | ## Development 21 | 22 | - Install [Docker for Mac](https://docs.docker.com/docker-for-mac/install/) 23 | 24 | - Setup and boot the Docker containers: 25 | 26 | ```sh 27 | make docker_setup 28 | ``` 29 | 30 | - Install Elixir dependencies: 31 | 32 | ```sh 33 | mix do deps.get, deps.compile 34 | ``` 35 | <%= if web_project? do %> 36 | - Install Node dependencies: 37 | 38 | ```sh 39 | npm install --prefix assets 40 | ``` 41 | 42 | - Install `chromedriver` for Wallaby browser tests 43 | 44 | Homebrew 45 | 46 | ```sh 47 | brew install --cask chromedriver 48 | ``` 49 | 50 | Debian/Ubuntu 51 | 52 | ```sh 53 | brew install --cask chromedriver 54 | ``` 55 | <% end %> 56 | - Setup the databases: 57 | 58 | ```sh 59 | mix ecto.setup 60 | ``` 61 | 62 | - Start the Phoenix app 63 | 64 | ```sh 65 | iex -S mix phx.server 66 | ``` 67 | 68 | - Run all tests: 69 | 70 | ```sh 71 | mix test 72 | ``` 73 | 74 | - Run tests with `watch` option: 75 | 76 | ```sh 77 | mix test.interactive # Watch the whole test suite 78 | 79 | mix test.interactive test_file_path # Watch a specific test file 80 | 81 | mix test.interactive test_file_path:line_number # Watch a specific test line number 82 | ``` 83 | 84 | - Run all lint: 85 | 86 | ```sh 87 | mix codebase 88 | ``` 89 | 90 | - Fix all lint: 91 | 92 | ```sh 93 | mix codebase.fix 94 | ``` 95 | 96 | - Test coverage: 97 | 98 | ```sh 99 | mix coverage 100 | ``` 101 | 102 | ## Production 103 | 104 | - Build Docker image 105 | 106 | ```sh 107 | docker-compose build 108 | ``` 109 | -------------------------------------------------------------------------------- /lib/nimble_template/addons/variants/phoenix/web/prettier.ex: -------------------------------------------------------------------------------- 1 | defmodule NimbleTemplate.Addons.Phoenix.Web.Prettier do 2 | @moduledoc false 3 | 4 | use NimbleTemplate.Addons.Addon 5 | 6 | @impl true 7 | def do_apply!(%Project{} = project, _opts) do 8 | project 9 | |> edit_files!() 10 | |> copy_files!() 11 | end 12 | 13 | defp edit_files!(%Project{} = project) do 14 | project 15 | |> edit_npm_dev_dependencies!() 16 | |> edit_mix!() 17 | end 18 | 19 | defp edit_npm_dev_dependencies!(%Project{} = project) do 20 | Generator.replace_content!( 21 | "assets/package.json", 22 | """ 23 | "devDependencies": { 24 | """, 25 | """ 26 | "devDependencies": { 27 | "prettier": "2.6.2", 28 | "prettier-plugin-eex": "0.5.0" 29 | """ 30 | ) 31 | 32 | project 33 | end 34 | 35 | defp edit_mix!(%Project{} = project) do 36 | project 37 | |> add_prettier_aliases!() 38 | |> add_prettier_into_codebase!() 39 | |> add_prettier_fix_into_codebase_fix!() 40 | end 41 | 42 | defp add_prettier_aliases!(%Project{} = project) do 43 | Generator.inject_content!( 44 | "mix.exs", 45 | """ 46 | defp aliases do 47 | [ 48 | """, 49 | """ 50 | prettier: "cmd ./assets/node_modules/.bin/prettier --check . --color", 51 | "prettier.fix": "cmd ./assets/node_modules/.bin/prettier --write . --color", 52 | """ 53 | ) 54 | 55 | project 56 | end 57 | 58 | defp add_prettier_into_codebase!(%Project{} = project) do 59 | Generator.replace_content!( 60 | "mix.exs", 61 | """ 62 | codebase: [ 63 | """, 64 | """ 65 | codebase: [ 66 | "prettier", 67 | """ 68 | ) 69 | 70 | project 71 | end 72 | 73 | defp add_prettier_fix_into_codebase_fix!(%Project{} = project) do 74 | Generator.replace_content!( 75 | "mix.exs", 76 | """ 77 | "codebase.fix": [ 78 | """, 79 | """ 80 | "codebase.fix": [ 81 | "prettier.fix", 82 | """ 83 | ) 84 | 85 | project 86 | end 87 | 88 | defp copy_files!(%Project{} = project) do 89 | Generator.copy_file!([{:text, ".prettierignore", ".prettierignore"}]) 90 | Generator.copy_file!([{:text, ".prettierrc.yaml", ".prettierrc.yaml"}]) 91 | 92 | project 93 | end 94 | end 95 | -------------------------------------------------------------------------------- /test/nimble_template/addons/variants/ecto_data_migration_test.exs: -------------------------------------------------------------------------------- 1 | defmodule NimbleTemplate.Addons.Phoenix.EctoDataMigrationTest do 2 | use NimbleTemplate.AddonCase, async: false 3 | 4 | describe "#apply!/2" do 5 | test "adds `ecto.migrate_all` into mix aliases", %{ 6 | project: project, 7 | test_project_path: test_project_path 8 | } do 9 | in_test_project!(test_project_path, fn -> 10 | PhoenixAddons.EctoDataMigration.apply!(project) 11 | 12 | assert_file("mix.exs", fn file -> 13 | assert file =~ """ 14 | "ecto.migrate_all": [ 15 | "ecto.migrate --migrations-path=priv/repo/migrations --migrations-path=priv/repo/data_migrations" 16 | ], 17 | """ 18 | end) 19 | end) 20 | end 21 | 22 | test "defines defp migrate", %{ 23 | project: project, 24 | test_project_path: test_project_path 25 | } do 26 | in_test_project!(test_project_path, fn -> 27 | PhoenixAddons.EctoDataMigration.apply!(project) 28 | 29 | assert_file("mix.exs", fn file -> 30 | assert file =~ """ 31 | defp migrate(_) do 32 | if Mix.env() == :test do 33 | Mix.Task.run("ecto.migrate", ["--quiet"]) 34 | else 35 | Mix.Task.run("ecto.migrate_all", []) 36 | end 37 | end 38 | """ 39 | end) 40 | end) 41 | end 42 | 43 | test "adjusts the `ecto.setup` alias by using `&migrate/1`", %{ 44 | project: project, 45 | test_project_path: test_project_path 46 | } do 47 | in_test_project!(test_project_path, fn -> 48 | PhoenixAddons.EctoDataMigration.apply!(project) 49 | 50 | assert_file("mix.exs", fn file -> 51 | assert file =~ """ 52 | "ecto.setup": ["ecto.create", &migrate/1, "run priv/repo/seeds.exs"], 53 | """ 54 | end) 55 | end) 56 | end 57 | 58 | test "creates the data_migrations directory", %{ 59 | project: project, 60 | test_project_path: test_project_path 61 | } do 62 | in_test_project!(test_project_path, fn -> 63 | PhoenixAddons.EctoDataMigration.apply!(project) 64 | 65 | assert_file("priv/repo/data_migrations/.keep") 66 | end) 67 | end 68 | end 69 | end 70 | -------------------------------------------------------------------------------- /.github/workflows/apply_api_variant.yml: -------------------------------------------------------------------------------- 1 | name: Apply API variant 2 | 3 | on: push 4 | 5 | jobs: 6 | standard_api_project: 7 | name: Test on a Standard API project 8 | uses: ./.github/workflows/reusable_phoenix_project.yml 9 | with: 10 | new_project_options: "--no-html --no-assets --no-live" 11 | variant: "api" 12 | 13 | long_custom_name_api_project: 14 | name: Test on a long custom name API project 15 | uses: ./.github/workflows/reusable_phoenix_project.yml 16 | with: 17 | new_project_options: "--no-html --no-assets --no-live --module=SampleCustomModule --app=sample_custom_app" 18 | variant: "api" 19 | 20 | short_custom_name_api_project: 21 | name: Test on a short custom name API project 22 | uses: ./.github/workflows/reusable_phoenix_project.yml 23 | with: 24 | new_project_options: "--no-html --no-assets --module=Z --app=z" 25 | variant: "api" 26 | 27 | standard_web_project: 28 | name: Test on a Standard Web project 29 | uses: ./.github/workflows/reusable_phoenix_project.yml 30 | with: 31 | new_project_options: "--no-live" 32 | variant: "api" 33 | 34 | long_custom_name_web_project: 35 | name: Test on a long custom name Web project 36 | uses: ./.github/workflows/reusable_phoenix_project.yml 37 | with: 38 | new_project_options: "--no-live --module=SampleCustomModule --app=sample_custom_app" 39 | variant: "api" 40 | 41 | short_custom_name_web_project: 42 | name: Test on a short custom name Web project 43 | uses: ./.github/workflows/reusable_phoenix_project.yml 44 | with: 45 | new_project_options: "--no-live --module=Z --app=z" 46 | variant: "api" 47 | 48 | 49 | standard_live_project: 50 | name: Test on a Standard Live project 51 | uses: ./.github/workflows/reusable_phoenix_project.yml 52 | with: 53 | new_project_options: "" 54 | variant: "api" 55 | 56 | long_custom_name_live_project: 57 | name: Test on a long custom name Live project 58 | uses: ./.github/workflows/reusable_phoenix_project.yml 59 | with: 60 | new_project_options: "--module=SampleCustomModule --app=sample_custom_app" 61 | variant: "api" 62 | 63 | short_custom_name_live_project: 64 | name: Test on a short custom name Live project 65 | uses: ./.github/workflows/reusable_phoenix_project.yml 66 | with: 67 | new_project_options: "--module=Z --app=z" 68 | variant: "api" 69 | -------------------------------------------------------------------------------- /lib/nimble_template/addons/variants/phoenix/web/post_css.ex: -------------------------------------------------------------------------------- 1 | defmodule NimbleTemplate.Addons.Phoenix.Web.PostCSS do 2 | @moduledoc false 3 | 4 | use NimbleTemplate.Addons.Addon 5 | 6 | @impl true 7 | def do_apply!(%Project{} = project, _opts) do 8 | project 9 | |> edit_files!() 10 | |> copy_files!() 11 | end 12 | 13 | defp edit_files!(%Project{} = project) do 14 | project 15 | |> edit_package!() 16 | |> edit_mix!() 17 | |> edit_phoenix_watcher!() 18 | end 19 | 20 | defp edit_package!(project) do 21 | Generator.replace_content!( 22 | "assets/package.json", 23 | """ 24 | "devDependencies": { 25 | """, 26 | """ 27 | "devDependencies": { 28 | "postcss": "8.4.19", 29 | "postcss-scss": "4.0.6", 30 | "postcss-cli": "9.1.0", 31 | "postcss-load-config": "3.1.4", 32 | "autoprefixer": "10.4.5", 33 | """ 34 | ) 35 | 36 | Generator.replace_content!( 37 | "assets/package.json", 38 | """ 39 | "scripts": { 40 | """, 41 | """ 42 | "scripts": { 43 | "postcss": "postcss ../priv/static/assets/*.css --dir ../priv/static/assets/ --config ./", 44 | "postcss.watch": "postcss ../priv/static/assets/*.css --dir ../priv/static/assets/ --config ./ --watch", 45 | """ 46 | ) 47 | 48 | project 49 | end 50 | 51 | defp edit_mix!(project) do 52 | Generator.replace_content!( 53 | "mix.exs", 54 | """ 55 | "assets.deploy": ["esbuild app --minify", "phx.digest"] 56 | """, 57 | """ 58 | "assets.deploy": [ 59 | "esbuild app --minify", 60 | "cmd npm run postcss --prefix assets", 61 | "phx.digest" 62 | ] 63 | """ 64 | ) 65 | 66 | project 67 | end 68 | 69 | defp edit_phoenix_watcher!(project) do 70 | Generator.replace_content!( 71 | "config/dev.exs", 72 | """ 73 | watchers: [ 74 | """, 75 | """ 76 | watchers: [ 77 | npm: [ 78 | "run", 79 | "postcss.watch", 80 | cd: Path.expand("../assets", __DIR__) 81 | ], 82 | """ 83 | ) 84 | 85 | project 86 | end 87 | 88 | defp copy_files!(%Project{} = project) do 89 | Generator.copy_file!([{:text, "assets/postcss.config.js", "assets/postcss.config.js"}]) 90 | 91 | project 92 | end 93 | end 94 | -------------------------------------------------------------------------------- /lib/nimble_template/addons/variants/phoenix/ex_machina.ex: -------------------------------------------------------------------------------- 1 | defmodule NimbleTemplate.Addons.Phoenix.ExMachina do 2 | @moduledoc false 3 | 4 | use NimbleTemplate.Addons.Addon 5 | 6 | @impl true 7 | def do_apply!(%Project{} = project, _opts) do 8 | project 9 | |> copy_files!() 10 | |> edit_files!() 11 | end 12 | 13 | def edit_mix_elixirc_paths!(%Project{} = project) do 14 | Generator.replace_content!( 15 | "mix.exs", 16 | """ 17 | defp elixirc_paths(:test), do: ["lib", "test/support"] 18 | """, 19 | """ 20 | defp elixirc_paths(:test), do: ["lib", "test/support", "test/factories"] 21 | """ 22 | ) 23 | 24 | project 25 | end 26 | 27 | defp copy_files!(%Project{base_module: base_module} = project) do 28 | Generator.copy_file!([{:eex, "test/support/factory.ex.eex", "test/support/factory.ex"}], 29 | base_module: base_module 30 | ) 31 | 32 | project 33 | end 34 | 35 | defp edit_files!(%Project{} = project) do 36 | project 37 | |> inject_mix_dependency!() 38 | |> edit_mix_elixirc_paths!() 39 | |> edit_test_helper!() 40 | |> import_factory!() 41 | end 42 | 43 | defp inject_mix_dependency!(%Project{} = project) do 44 | Generator.inject_mix_dependency!( 45 | {:ex_machina, latest_package_version(:ex_machina), only: :test} 46 | ) 47 | 48 | project 49 | end 50 | 51 | defp edit_test_helper!(%Project{} = project) do 52 | Generator.replace_content!( 53 | "test/test_helper.exs", 54 | """ 55 | ExUnit.start() 56 | """, 57 | """ 58 | {:ok, _} = Application.ensure_all_started(:ex_machina) 59 | 60 | ExUnit.start() 61 | """ 62 | ) 63 | 64 | project 65 | end 66 | 67 | defp import_factory!(%Project{base_module: base_module, web_module: web_module} = project) do 68 | Generator.replace_content!( 69 | "test/support/data_case.ex", 70 | """ 71 | import #{base_module}.DataCase 72 | """, 73 | """ 74 | import #{base_module}.DataCase 75 | import #{base_module}.Factory 76 | """ 77 | ) 78 | 79 | Generator.replace_content!( 80 | "test/support/conn_case.ex", 81 | """ 82 | import #{web_module}.ConnCase 83 | """, 84 | """ 85 | import #{web_module}.ConnCase 86 | import #{base_module}.Factory 87 | """ 88 | ) 89 | 90 | project 91 | end 92 | end 93 | -------------------------------------------------------------------------------- /lib/nimble_template/helpers/credo.ex: -------------------------------------------------------------------------------- 1 | defmodule NimbleTemplate.CredoHelper do 2 | alias NimbleTemplate.Generator 3 | alias NimbleTemplate.Projects.Project 4 | 5 | @do_single_expression_rule_name "CompassCredoPlugin.Check.DoSingleExpression" 6 | @single_module_file_rule_name "CompassCredoPlugin.Check.SingleModuleFile" 7 | 8 | @spec suppress_credo_warnings_for_base_project(Project.t()) :: :ok 9 | def suppress_credo_warnings_for_base_project(%Project{base_module: base_module}) do 10 | base_module_path = "lib/#{Macro.underscore(base_module)}.ex" 11 | 12 | disable_rule(base_module_path, @do_single_expression_rule_name) 13 | end 14 | 15 | @spec suppress_credo_warnings_for_phoenix_project(Project.t()) :: :ok 16 | def suppress_credo_warnings_for_phoenix_project(project) do 17 | suppress_credo_warnings_for_base_project(project) 18 | 19 | project 20 | |> get_files_containing_single_expression() 21 | |> disable_rules(@do_single_expression_rule_name) 22 | end 23 | 24 | @spec suppress_credo_warnings_for_phoenix_api_project(Project.t()) :: :ok 25 | def suppress_credo_warnings_for_phoenix_api_project(project) do 26 | suppress_credo_warnings_for_base_project(project) 27 | 28 | project 29 | |> get_files_containing_multiple_modules() 30 | |> disable_rules(@single_module_file_rule_name) 31 | 32 | project 33 | |> get_files_containing_single_expression() 34 | |> disable_rules(@do_single_expression_rule_name) 35 | end 36 | 37 | defp get_files_containing_single_expression(%Project{ 38 | base_path: base_path, 39 | web_path: web_path 40 | }) do 41 | [ 42 | "#{base_path}/release_tasks.ex", 43 | "#{web_path}/controllers/page_controller.ex", 44 | "#{web_path}/telemetry.ex", 45 | "#{web_path}/views/error_view.ex" 46 | ] 47 | end 48 | 49 | defp get_files_containing_multiple_modules(%Project{ 50 | web_test_path: web_test_path 51 | }) do 52 | [ 53 | "#{web_test_path}/views/api/error_view_test.exs", 54 | "#{web_test_path}/params/params_validator_test.exs" 55 | ] 56 | end 57 | 58 | defp disable_rules(file_paths, rule_name) do 59 | Enum.each(file_paths, fn file_path -> 60 | disable_rule(file_path, rule_name) 61 | end) 62 | end 63 | 64 | defp disable_rule(file_path, rule) do 65 | if File.exists?(file_path) do 66 | Generator.prepend_content(file_path, """ 67 | # credo:disable-for-this-file #{rule} 68 | """) 69 | end 70 | end 71 | end 72 | -------------------------------------------------------------------------------- /test/nimble_template/helpers/generator_test.exs: -------------------------------------------------------------------------------- 1 | defmodule NimbleTemplate.GeneratorTest do 2 | use NimbleTemplate.AddonCase, async: false 3 | 4 | alias NimbleTemplate.Generator 5 | 6 | describe "prepend_content/2" do 7 | test "prepends the given content in the given file", %{ 8 | test_project_path: test_project_path 9 | } do 10 | in_test_project!(test_project_path, fn -> 11 | File.write!("sample_module.exs", """ 12 | defmodule SampleModule do 13 | def foo, do: "bar" 14 | end 15 | """) 16 | 17 | Generator.prepend_content("sample_module.exs", "# This is sample module\n") 18 | 19 | assert_file("sample_module.exs", fn file -> 20 | assert file == """ 21 | # This is sample module 22 | defmodule SampleModule do 23 | def foo, do: "bar" 24 | end 25 | """ 26 | end) 27 | end) 28 | end 29 | 30 | test "when the given file does not exist, returns an error", %{ 31 | test_project_path: test_project_path 32 | } do 33 | in_test_project!(test_project_path, fn -> 34 | assert Generator.prepend_content("unknown_file.exs", "# This is sample module\n") == 35 | {:error, :failed_to_read_file} 36 | end) 37 | end 38 | end 39 | 40 | describe "prepend_content!/2" do 41 | test "prepends the given content in the given file", %{ 42 | test_project_path: test_project_path 43 | } do 44 | in_test_project!(test_project_path, fn -> 45 | File.write!("sample_module.exs", """ 46 | defmodule SampleModule do 47 | def foo, do: "bar" 48 | end 49 | """) 50 | 51 | Generator.prepend_content!("sample_module.exs", "# This is sample module\n") 52 | 53 | assert_file("sample_module.exs", fn file -> 54 | assert file == """ 55 | # This is sample module 56 | defmodule SampleModule do 57 | def foo, do: "bar" 58 | end 59 | """ 60 | end) 61 | end) 62 | end 63 | 64 | test "when the given file does not exist, raises an error", %{ 65 | test_project_path: test_project_path 66 | } do 67 | in_test_project!(test_project_path, fn -> 68 | assert_raise Mix.Error, "Can't read unknown_file.exs", fn -> 69 | Generator.prepend_content!("unknown_file.exs", "# This is sample module\n") 70 | end 71 | end) 72 | end 73 | end 74 | end 75 | -------------------------------------------------------------------------------- /test/nimble_template/addons/variants/ex_machina_test.exs: -------------------------------------------------------------------------------- 1 | defmodule NimbleTemplate.Addons.Phoenix.ExMachinaTest do 2 | use NimbleTemplate.AddonCase, async: false 3 | 4 | describe "#apply!/2" do 5 | @describetag mock_latest_package_versions: [{:ex_machina, "2.4"}] 6 | 7 | test "copies the test/support/factory.ex", %{ 8 | project: project, 9 | test_project_path: test_project_path 10 | } do 11 | in_test_project!(test_project_path, fn -> 12 | PhoenixAddons.ExMachina.apply!(project) 13 | 14 | assert_file("test/support/factory.ex") 15 | end) 16 | end 17 | 18 | test "injects ex_machina to mix dependency", %{ 19 | project: project, 20 | test_project_path: test_project_path 21 | } do 22 | in_test_project!(test_project_path, fn -> 23 | PhoenixAddons.ExMachina.apply!(project) 24 | 25 | assert_file("mix.exs", fn file -> 26 | assert file =~ """ 27 | defp deps do 28 | [ 29 | {:ex_machina, "~> 2.4", [only: :test]}, 30 | """ 31 | end) 32 | end) 33 | end 34 | 35 | test "adds test/factories into elixirc_paths", %{ 36 | project: project, 37 | test_project_path: test_project_path 38 | } do 39 | in_test_project!(test_project_path, fn -> 40 | PhoenixAddons.ExMachina.apply!(project) 41 | 42 | assert_file("mix.exs", fn file -> 43 | assert file =~ """ 44 | defp elixirc_paths(:test), do: ["lib", "test/support", "test/factories"] 45 | """ 46 | end) 47 | end) 48 | end 49 | 50 | test "updates test/test_helper.exs", %{project: project, test_project_path: test_project_path} do 51 | in_test_project!(test_project_path, fn -> 52 | PhoenixAddons.ExMachina.apply!(project) 53 | 54 | assert_file("test/test_helper.exs", fn file -> 55 | assert file =~ """ 56 | {:ok, _} = Application.ensure_all_started(:ex_machina) 57 | 58 | ExUnit.start() 59 | """ 60 | end) 61 | end) 62 | end 63 | 64 | test "adds Factory module", %{project: project, test_project_path: test_project_path} do 65 | in_test_project!(test_project_path, fn -> 66 | PhoenixAddons.ExMachina.apply!(project) 67 | 68 | assert_file("test/support/data_case.ex", fn file -> 69 | assert file =~ "import NimbleTemplate.Factory" 70 | end) 71 | 72 | assert_file("test/support/conn_case.ex", fn file -> 73 | assert file =~ "import NimbleTemplate.Factory" 74 | end) 75 | end) 76 | end 77 | end 78 | end 79 | -------------------------------------------------------------------------------- /priv/templates/nimble_template/README.md.eex: -------------------------------------------------------------------------------- 1 | [![Build Status](CI_BADGE_URL goes here)](REPO_URL goes here) 2 | 3 | ## Introduction 4 | 5 | > *App introduction goes here ...* 6 | 7 | ## Project Setup 8 | 9 | ### Erlang & Elixir 10 | 11 | - Erlang <%= erlang_version %> 12 | 13 | - Elixir <%= elixir_version %> 14 | <%= if web_project? do %> 15 | ### Node 16 | 17 | - Node <%= node_asdf_version %> 18 | <% end %> 19 | - Recommended version manager. 20 | 21 | - [asdf](https://github.com/asdf-vm/asdf) 22 | - [asdf-erlang](https://github.com/asdf-vm/asdf-erlang) 23 | - [asdf-elixir](https://github.com/asdf-vm/asdf-elixir) 24 | <%= if web_project? do %> 25 | - [asdf-node](https://github.com/asdf-vm/asdf-node) 26 | <% end %> 27 | 28 | ### Development 29 | 30 | - Install [Docker for Mac](https://docs.docker.com/docker-for-mac/install/) 31 | 32 | - Setup and boot the Docker containers: 33 | 34 | ```sh 35 | make docker_setup 36 | ``` 37 | 38 | - Install Elixir dependencies: 39 | 40 | ```sh 41 | mix deps.get 42 | ``` 43 | <%= if web_project? do %> 44 | - Install Node dependencies: 45 | 46 | ```sh 47 | npm install --prefix assets 48 | ``` 49 | 50 | - Install `chromedriver` for Wallaby browser tests 51 | 52 | Homebrew 53 | 54 | ```sh 55 | brew install --cask chromedriver 56 | ``` 57 | 58 | Debian/Ubuntu 59 | 60 | ```sh 61 | brew install --cask chromedriver 62 | ``` 63 | <% end %> 64 | - Setup the databases: 65 | 66 | ```sh 67 | mix ecto.setup 68 | ``` 69 | 70 | - Start the Phoenix app 71 | 72 | ```sh 73 | iex -S mix phx.server 74 | ``` 75 | 76 | - Run all tests: 77 | 78 | ```sh 79 | mix test 80 | ``` 81 | 82 | - Run tests with `watch` option: 83 | 84 | ```sh 85 | mix test.interactive # Watch the whole test suite 86 | 87 | mix test.interactive test_file_path # Watch a specific test file 88 | 89 | mix test.interactive test_file_path:line_number # Watch a specific test line number 90 | ``` 91 | 92 | - Run all lint: 93 | 94 | ```sh 95 | mix codebase 96 | ``` 97 | 98 | - Fix all lint: 99 | 100 | ```sh 101 | mix codebase.fix 102 | ``` 103 | 104 | - Test coverage: 105 | 106 | ```sh 107 | mix coverage 108 | ``` 109 | 110 | ### Production 111 | 112 | - Build Docker image 113 | 114 | ```sh 115 | docker-compose build 116 | ``` 117 | 118 | ### CI/CD 119 | 120 | The project relies entirely on [Github Actions](https://github.com/features/actions) for CI/CD via multiple workflows located under the [`.github/workflows/`](.github/workflows) directory. 121 | Please check out the [`.github/workflows/README.md`](.github/workflows/README.md) file for further instructions. 122 | -------------------------------------------------------------------------------- /test/nimble_template/addons/variants/phoenix/web/nimble_js_test.exs: -------------------------------------------------------------------------------- 1 | defmodule NimbleTemplate.Addons.Phoenix.Web.NimbleJSTest do 2 | use NimbleTemplate.AddonCase, async: false 3 | 4 | describe "#apply!/2" do 5 | @describetag required_addons: [:TestEnv, :"Phoenix.Web.NodePackage", :"Phoenix.Web.EsLint"] 6 | 7 | test "copies Nimble JS structure", %{ 8 | project: project, 9 | test_project_path: test_project_path 10 | } do 11 | in_test_project!(test_project_path, fn -> 12 | WebAddons.NimbleJS.apply!(project) 13 | 14 | assert_directory("assets/js/adapters") 15 | assert_directory("assets/js/components") 16 | assert_directory("assets/js/config") 17 | assert_directory("assets/js/helpers") 18 | assert_directory("assets/js/initializers") 19 | assert_directory("assets/js/lib") 20 | assert_directory("assets/js/screens") 21 | 22 | assert_file("assets/js/app.js") 23 | end) 24 | end 25 | 26 | test "updates assets/js/app.js", %{ 27 | project: project, 28 | test_project_path: test_project_path 29 | } do 30 | in_test_project!(test_project_path, fn -> 31 | WebAddons.NimbleJS.apply!(project) 32 | 33 | assert_file("assets/js/app.js", fn file -> 34 | assert file =~ """ 35 | 36 | // Application 37 | import "./initializers/"; 38 | 39 | import "./screens/"; 40 | """ 41 | 42 | assert file =~ "\"./vendor/topbar\"" 43 | assert file =~ "\"./vendor/some-package.js\"" 44 | assert file =~ "assets/js/vendor" 45 | 46 | refute file =~ "\"../vendor/topbar\"" 47 | refute file =~ "\"../vendor/some-package.js\"" 48 | refute file =~ "assets/vendor" 49 | end) 50 | end) 51 | end 52 | 53 | test "moves assets/vendor/topbar.js into assets/js/vendor/topbar.js", %{ 54 | project: project, 55 | test_project_path: test_project_path 56 | } do 57 | in_test_project!(test_project_path, fn -> 58 | WebAddons.NimbleJS.apply!(project) 59 | 60 | assert_file("assets/js/vendor/topbar.js") 61 | refute_file("assets/vendor/topbar.js") 62 | end) 63 | end 64 | 65 | test "updates .eslintrc.json config", %{ 66 | project: project, 67 | test_project_path: test_project_path 68 | } do 69 | in_test_project!(test_project_path, fn -> 70 | WebAddons.NimbleJS.apply!(project) 71 | 72 | assert_file("assets/.eslintrc.json", fn file -> 73 | assert file =~ """ 74 | "ignorePatterns": [ 75 | "/js/vendor/topbar.js" 76 | ] 77 | """ 78 | end) 79 | end) 80 | end 81 | end 82 | end 83 | -------------------------------------------------------------------------------- /test/nimble_template/addons/variants/phoenix/web/prettier_test.exs: -------------------------------------------------------------------------------- 1 | defmodule NimbleTemplate.Addons.Phoenix.Web.PrettierTest do 2 | use NimbleTemplate.AddonCase, async: false 3 | 4 | describe "#apply!/2" do 5 | @describetag required_addons: [:TestEnv, :"Phoenix.Web.NodePackage"] 6 | 7 | test "adds prettier and prettier-plugin-eex into package.json", %{ 8 | project: project, 9 | test_project_path: test_project_path 10 | } do 11 | in_test_project!(test_project_path, fn -> 12 | WebAddons.Prettier.apply!(project) 13 | 14 | assert_file("assets/package.json", fn file -> 15 | assert file =~ """ 16 | "devDependencies": { 17 | "prettier": "2.6.2", 18 | "prettier-plugin-eex": "0.5.0" 19 | """ 20 | end) 21 | end) 22 | end 23 | 24 | test "injects prettier to mix aliases", %{ 25 | project: project, 26 | test_project_path: test_project_path 27 | } do 28 | in_test_project!(test_project_path, fn -> 29 | WebAddons.Prettier.apply!(project) 30 | 31 | assert_file("mix.exs", fn file -> 32 | assert file =~ """ 33 | defp aliases do 34 | [ 35 | prettier: "cmd ./assets/node_modules/.bin/prettier --check . --color", 36 | "prettier.fix": "cmd ./assets/node_modules/.bin/prettier --write . --color", 37 | """ 38 | end) 39 | end) 40 | end 41 | 42 | test "adds prettier into the codebase alias", %{ 43 | project: project, 44 | test_project_path: test_project_path 45 | } do 46 | in_test_project!(test_project_path, fn -> 47 | WebAddons.Prettier.apply!(project) 48 | 49 | assert_file("mix.exs", fn file -> 50 | assert file =~ """ 51 | codebase: [ 52 | "prettier", 53 | """ 54 | end) 55 | end) 56 | end 57 | 58 | test "adds prettier.fix into the codebase.fix alias", %{ 59 | project: project, 60 | test_project_path: test_project_path 61 | } do 62 | in_test_project!(test_project_path, fn -> 63 | WebAddons.Prettier.apply!(project) 64 | 65 | assert_file("mix.exs", fn file -> 66 | assert file =~ """ 67 | "codebase.fix": [ 68 | "prettier.fix", 69 | """ 70 | end) 71 | end) 72 | end 73 | 74 | test "copies the .prettierignore and .prettierrc.yaml", %{ 75 | project: project, 76 | test_project_path: test_project_path 77 | } do 78 | in_test_project!(test_project_path, fn -> 79 | WebAddons.Prettier.apply!(project) 80 | 81 | assert_file(".prettierignore") 82 | assert_file(".prettierrc.yaml") 83 | end) 84 | end 85 | end 86 | end 87 | -------------------------------------------------------------------------------- /.github/workflows/bump_version.yml: -------------------------------------------------------------------------------- 1 | name: Bump Version 2 | 3 | on: 4 | workflow_dispatch: 5 | inputs: 6 | newVersion: 7 | description: "New version" 8 | required: true 9 | type: string 10 | 11 | env: 12 | PHOENIX_VERSION: 1.6.11 13 | MIX_ENV: test 14 | 15 | jobs: 16 | release_version_test: 17 | name: Bump version 18 | runs-on: ubuntu-latest 19 | 20 | steps: 21 | - name: Cancel Previous Runs 22 | uses: styfle/cancel-workflow-action@0.9.1 23 | with: 24 | access_token: ${{ github.token }} 25 | 26 | - name: Checkout repository 27 | uses: actions/checkout@v3 28 | with: 29 | ref: ${{ github.head_ref }} 30 | 31 | - name: Setup asdf 32 | uses: asdf-vm/actions/setup@v1 33 | 34 | - name: Cache asdf 35 | uses: actions/cache@v3 36 | with: 37 | path: /home/runner/.asdf 38 | key: ${{ runner.os }}-asdf-${{ hashFiles('**/.tool-versions') }} 39 | restore-keys: ${{ runner.os }}-asdf- 40 | 41 | - name: Install dependencies in .tool-versions 42 | uses: asdf-vm/actions/install@v1 43 | 44 | - name: Install rebar 45 | run: mix local.rebar --force 46 | 47 | - name: Install hex 48 | run: mix local.hex --force 49 | 50 | - name: Cache Elixir build 51 | uses: actions/cache@v3 52 | with: 53 | path: | 54 | _build 55 | deps 56 | key: ${{ runner.os }}-mix-${{ hashFiles('**/mix.lock') }} 57 | restore-keys: ${{ runner.os }}-mix- 58 | 59 | - name: Install Dependencies 60 | run: mix deps.get 61 | 62 | - name: Compile dependencies 63 | run: mix compile --warnings-as-errors --all-warnings 64 | 65 | - name: Install Phoenix ${{ env.PHOENIX_VERSION }} 66 | run: make install_phoenix PHOENIX_VERSION=${{ env.PHOENIX_VERSION }} 67 | 68 | - name: Bump version 69 | run: mix nimble_template.bump_version ${{ github.event.inputs.newVersion }} 70 | 71 | - name: Create Pull Request 72 | uses: peter-evans/create-pull-request@v4 73 | with: 74 | assignees: andyduong1920 75 | token: ${{ secrets.WIKI_ACTION_TOKEN }} 76 | commit-message: Bump version to ${{ github.event.inputs.newVersion }} 77 | committer: Nimble Bot 78 | branch: chore/bump-version-to-${{ github.event.inputs.newVersion }} 79 | delete-branch: true 80 | title: '[Chore] Bump version to ${{ github.event.inputs.newVersion }}' 81 | labels: | 82 | type : chore 83 | body: | 84 | ## What happened 85 | 86 | Bump version to ${{ github.event.inputs.newVersion }} 87 | 88 | ## Insight 89 | 90 | Automatically created by the Bump Version workflow. 91 | 92 | ## Proof Of Work 93 | 94 | On the Files changed tab 95 | -------------------------------------------------------------------------------- /lib/nimble_template/addons/variants/phoenix/web/es_lint.ex: -------------------------------------------------------------------------------- 1 | defmodule NimbleTemplate.Addons.Phoenix.Web.EsLint do 2 | @moduledoc false 3 | 4 | use NimbleTemplate.Addons.Addon 5 | 6 | @impl true 7 | def do_apply!(%Project{} = project, _opts) do 8 | project 9 | |> edit_files!() 10 | |> copy_files!() 11 | end 12 | 13 | def edit_app_js!(%Project{live_project?: true} = project) do 14 | update_topbar_js_variables!() 15 | 16 | project 17 | end 18 | 19 | def edit_app_js!(%Project{web_project?: true} = project) do 20 | update_topbar_js_variables!() 21 | 22 | project 23 | end 24 | 25 | def edit_app_js!(project), do: project 26 | 27 | defp edit_files!(%Project{} = project) do 28 | project 29 | |> edit_assets_package!() 30 | |> edit_mix!() 31 | |> edit_app_js!() 32 | end 33 | 34 | defp copy_files!(%Project{} = project) do 35 | Generator.copy_file!([{:text, "assets/.eslintrc.json", "assets/.eslintrc.json"}]) 36 | 37 | project 38 | end 39 | 40 | defp edit_assets_package!(%Project{} = project) do 41 | Generator.replace_content!( 42 | "assets/package.json", 43 | """ 44 | "scripts": { 45 | """, 46 | """ 47 | "scripts": { 48 | "eslint": "eslint --color ./", 49 | "eslint.fix": "eslint --color --fix ./" 50 | """ 51 | ) 52 | 53 | Generator.replace_content!( 54 | "assets/package.json", 55 | """ 56 | "devDependencies": { 57 | """, 58 | """ 59 | "devDependencies": { 60 | "eslint": "8.14.0", 61 | "eslint-config-prettier": "8.5.0", 62 | "eslint-plugin-prettier": "4.0.0", 63 | """ 64 | ) 65 | 66 | project 67 | end 68 | 69 | defp edit_mix!(%Project{} = project) do 70 | Generator.replace_content!( 71 | "mix.exs", 72 | """ 73 | codebase: [ 74 | """, 75 | """ 76 | codebase: [ 77 | "cmd npm run eslint --prefix assets", 78 | """ 79 | ) 80 | 81 | Generator.replace_content!( 82 | "mix.exs", 83 | """ 84 | "codebase.fix": [ 85 | """, 86 | """ 87 | "codebase.fix": [ 88 | "cmd npm run eslint.fix --prefix assets", 89 | """ 90 | ) 91 | 92 | project 93 | end 94 | 95 | defp update_topbar_js_variables! do 96 | Generator.replace_content!( 97 | "assets/js/app.js", 98 | "window.addEventListener(\"phx:page-loading-start\", info => topbar.show())", 99 | "window.addEventListener(\"phx:page-loading-start\", _info => topbar.show())" 100 | ) 101 | 102 | Generator.replace_content!( 103 | "assets/js/app.js", 104 | "window.addEventListener(\"phx:page-loading-stop\", info => topbar.hide())", 105 | "window.addEventListener(\"phx:page-loading-stop\", _info => topbar.hide())" 106 | ) 107 | end 108 | end 109 | -------------------------------------------------------------------------------- /test/nimble_template/addons/variants/mix_release_test.exs: -------------------------------------------------------------------------------- 1 | defmodule NimbleTemplate.Addons.Phoenix.MixReleaseTest do 2 | use NimbleTemplate.AddonCase, async: false 3 | 4 | describe "#apply!/2" do 5 | test "creates the lib/nimble_template/release_tasks.ex", %{ 6 | project: project, 7 | test_project_path: test_project_path 8 | } do 9 | in_test_project!(test_project_path, fn -> 10 | PhoenixAddons.MixRelease.apply!(project) 11 | 12 | assert_file("lib/nimble_template/release_tasks.ex", fn file -> 13 | assert file =~ """ 14 | defmodule NimbleTemplate.ReleaseTasks do 15 | @app :nimble_template 16 | 17 | def migrate do 18 | load_app() 19 | 20 | for repo <- repos() do 21 | schema_migrations = Ecto.Migrator.migrations_path(repo, "migrations") 22 | data_migrations = Ecto.Migrator.migrations_path(repo, "data_migrations") 23 | 24 | {:ok, _, _} = 25 | Ecto.Migrator.with_repo( 26 | repo, 27 | &Ecto.Migrator.run(&1, [schema_migrations, data_migrations], :up, all: true) 28 | ) 29 | end 30 | end 31 | """ 32 | end) 33 | end) 34 | end 35 | 36 | test "adjusts the config/runtime.exs", %{ 37 | project: project, 38 | test_project_path: test_project_path 39 | } do 40 | in_test_project!(test_project_path, fn -> 41 | PhoenixAddons.MixRelease.apply!(project) 42 | 43 | assert_file("config/runtime.exs", fn file -> 44 | assert file =~ """ 45 | config :nimble_template, NimbleTemplateWeb.Endpoint, 46 | server: true, 47 | """ 48 | 49 | assert file =~ """ 50 | host = 51 | System.get_env("PHX_HOST") || 52 | raise \"\"\" 53 | Environment variable PHX_HOST is missing. 54 | Set the Heroku endpoint to this variable. 55 | \"\"\" 56 | """ 57 | 58 | refute file =~ """ 59 | 60 | # ## Using releases 61 | # 62 | # If you use `mix release`, you need to explicitly enable the server 63 | # by passing the PHX_SERVER=true when you start it: 64 | # 65 | # PHX_SERVER=true bin/nimble_template start 66 | # 67 | # Alternatively, you can use `mix phx.gen.release` to generate a `bin/server` 68 | # script that automatically sets the env var above. 69 | if System.get_env("PHX_SERVER") do 70 | config :nimble_template, NimbleTemplateWeb.Endpoint, server: true 71 | end 72 | """ 73 | end) 74 | end) 75 | end 76 | end 77 | end 78 | -------------------------------------------------------------------------------- /test/nimble_template/addons/variants/phoenix/web/style_lint_test.exs: -------------------------------------------------------------------------------- 1 | defmodule NimbleTemplate.Addons.Phoenix.Web.StyleLintTest do 2 | use NimbleTemplate.AddonCase, async: false 3 | 4 | describe "#apply!/2" do 5 | @describetag required_addons: [:TestEnv, :"Phoenix.Web.NodePackage"] 6 | 7 | test "adds stylelint, 8 | stylelint-config-property-sort-order-smacss 9 | and stylelint-config-sass-guidelines into package.json", 10 | %{ 11 | project: project, 12 | test_project_path: test_project_path 13 | } do 14 | in_test_project!(test_project_path, fn -> 15 | WebAddons.StyleLint.apply!(project) 16 | 17 | assert_file("assets/package.json", fn file -> 18 | assert file =~ """ 19 | "devDependencies": { 20 | "stylelint": "14.7.1", 21 | "stylelint-config-property-sort-order-smacss": "9.0.0", 22 | "stylelint-config-sass-guidelines": "9.0.1", 23 | """ 24 | end) 25 | end) 26 | end 27 | 28 | test "adds stylelint and stylelint.fix into package.json", %{ 29 | project: project, 30 | test_project_path: test_project_path 31 | } do 32 | in_test_project!(test_project_path, fn -> 33 | WebAddons.StyleLint.apply!(project) 34 | 35 | assert_file("assets/package.json", fn file -> 36 | assert file =~ """ 37 | "scripts": { 38 | "stylelint": "stylelint --color ./css", 39 | "stylelint.fix": "stylelint --color --fix ./css", 40 | """ 41 | end) 42 | end) 43 | end 44 | 45 | test "adds stylelint into the codebase alias", %{ 46 | project: project, 47 | test_project_path: test_project_path 48 | } do 49 | in_test_project!(test_project_path, fn -> 50 | WebAddons.StyleLint.apply!(project) 51 | 52 | assert_file("mix.exs", fn file -> 53 | assert file =~ """ 54 | codebase: [ 55 | "cmd npm run stylelint --prefix assets", 56 | """ 57 | end) 58 | end) 59 | end 60 | 61 | test "adds stylelint.fix into the codebase.fix alias", %{ 62 | project: project, 63 | test_project_path: test_project_path 64 | } do 65 | in_test_project!(test_project_path, fn -> 66 | WebAddons.StyleLint.apply!(project) 67 | 68 | assert_file("mix.exs", fn file -> 69 | assert file =~ """ 70 | "codebase.fix": [ 71 | "cmd npm run stylelint.fix --prefix assets", 72 | """ 73 | end) 74 | end) 75 | end 76 | 77 | test "copies the .stylelintrc.json", %{ 78 | project: project, 79 | test_project_path: test_project_path 80 | } do 81 | in_test_project!(test_project_path, fn -> 82 | WebAddons.StyleLint.apply!(project) 83 | 84 | assert_file("assets/.stylelintrc.json") 85 | end) 86 | end 87 | end 88 | end 89 | -------------------------------------------------------------------------------- /lib/nimble_template/addons/variants/phoenix/web/dart_sass.ex: -------------------------------------------------------------------------------- 1 | defmodule NimbleTemplate.Addons.Phoenix.Web.DartSass do 2 | @moduledoc false 3 | 4 | use NimbleTemplate.Addons.Addon 5 | 6 | @dart_sass_version "1.49.11" 7 | 8 | @impl true 9 | def do_apply!(%Project{} = project, _opts) do 10 | project 11 | |> inject_mix_dependency!() 12 | |> edit_config!() 13 | |> edit_mix!() 14 | |> edit_app_js!() 15 | |> rename_app_css!() 16 | end 17 | 18 | defp inject_mix_dependency!(%Project{} = project) do 19 | Generator.inject_mix_dependency!( 20 | {:dart_sass, latest_package_version(:dart_sass), runtime: "Mix.env() == :dev"} 21 | ) 22 | 23 | Generator.replace_content!( 24 | "mix.exs", 25 | "runtime: \"Mix.env() == :dev\"", 26 | "runtime: Mix.env() == :dev" 27 | ) 28 | 29 | project 30 | end 31 | 32 | defp edit_config!(%Project{} = project) do 33 | Generator.replace_content!( 34 | "config/config.exs", 35 | """ 36 | # Configure esbuild (the version is required) 37 | """, 38 | """ 39 | # Configure dart_sass (the version is required) 40 | config :dart_sass, 41 | version: "#{@dart_sass_version}", 42 | app: [ 43 | args: ~w( 44 | --load-path=./node_modules 45 | css/app.scss 46 | ../priv/static/assets/app.css 47 | ), 48 | cd: Path.expand("../assets", __DIR__) 49 | ] 50 | 51 | # Configure esbuild (the version is required) 52 | """ 53 | ) 54 | 55 | Generator.replace_content!( 56 | "config/dev.exs", 57 | """ 58 | watchers: [ 59 | """, 60 | """ 61 | watchers: [ 62 | app_sass: { 63 | DartSass, 64 | :install_and_run, 65 | [:app, ~w(--embed-source-map --source-map-urls=absolute --watch)] 66 | }, 67 | """ 68 | ) 69 | 70 | project 71 | end 72 | 73 | defp edit_mix!(project) do 74 | Generator.replace_content!( 75 | "mix.exs", 76 | """ 77 | "assets.deploy": [ 78 | "esbuild app --minify", 79 | "cmd npm run postcss --prefix assets", 80 | "phx.digest" 81 | ] 82 | """, 83 | """ 84 | "assets.deploy": [ 85 | "esbuild app --minify", 86 | "sass app --no-source-map --style=compressed", 87 | "cmd npm run postcss --prefix assets", 88 | "phx.digest" 89 | ] 90 | """ 91 | ) 92 | 93 | project 94 | end 95 | 96 | defp edit_app_js!(project) do 97 | Generator.delete_content!( 98 | "assets/js/app.js", 99 | """ 100 | // We import the CSS which is extracted to its own file by esbuild. 101 | // Remove this line if you add a your own CSS build pipeline (e.g postcss). 102 | import "../css/app.css" 103 | 104 | """ 105 | ) 106 | 107 | project 108 | end 109 | 110 | defp rename_app_css!(project) do 111 | Generator.rename_file!( 112 | "assets/css/app.css", 113 | "assets/css/app.scss" 114 | ) 115 | 116 | project 117 | end 118 | end 119 | --------------------------------------------------------------------------------