├── 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 | 
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 | 
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 | [](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 | [](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 |
--------------------------------------------------------------------------------