431 lines
14 KiB
Org Mode
431 lines
14 KiB
Org Mode
#+TITLE: Project Configuration, Mix, etc
|
|
|
|
In here I de-tangle the Phoenix and Mix infrastructure for [[file:../lipu_kasi.org][§Lipu Kasi]]. In general, when I work in a code module it should get moved in to its own file, but I don't know how to address that for these "infrastructure" elements. For now they'll live here. I also need to design some build systems and build scripts, and set up my =.gitignore=
|
|
|
|
* Project =.gitignore= file
|
|
|
|
This project is intended to be developed as a [[file:../literate_programming.org][§Literate Programming]] [[file:../org-mode.org][§org-mode]] application, developed within my [[file:../org-roam.org][§org-roam]] wiki. The project is its own subdirectory under my =org-roam-directory= and it can be on yours as well, or provided standalone.
|
|
|
|
The repo will contain docs and assets and a build script that will call up an [[file:../Emacs.org][§Emacs]] that will "tangle" the files out and build the application. It will *not* contain the compiled code checked in, the docs must be considered the source of truth. We'll have facilities to "de-tangle" the source code eventually to aide contribution from folks who aren't running Emacs. For now, though, I'm gonna be a bastard 😄
|
|
|
|
This starts with a .gitignore, =phx.new= provides one that I will expand. When updating this, the order of operation is to delete the file from the index, update this, tangle it, and then commit. it's a bit of a mess, maybe detangling is better.
|
|
|
|
#+begin_src conf :tangle .gitignore
|
|
# elixir and mix outputs
|
|
/_build/
|
|
/cover/
|
|
/deps/
|
|
/doc/
|
|
/.fetch
|
|
|
|
# debug and dumps
|
|
erl_crash.dump
|
|
npm-debug.log
|
|
|
|
# Archives
|
|
,*.ez
|
|
lipu_kasi-*.tar
|
|
|
|
|
|
# The directory NPM downloads your dependencies sources to.
|
|
/assets/node_modules/
|
|
/priv/static/
|
|
#+end_src
|
|
|
|
Files which are provided from tangles will need to be managed here:
|
|
|
|
#+begin_src conf :tangle .gitignore
|
|
/mix.exs
|
|
# /mix.lock is checked in.
|
|
.formatter.exs
|
|
/config/*.exs
|
|
!/config/prod.secret.exs
|
|
|
|
# LipuKasi module
|
|
/lib/lipu_kasi.ex
|
|
/lib/lipu_kasi/*.ex
|
|
|
|
# LipuKasiWeb module
|
|
/lib/lipu_kasi_web.ex
|
|
/lib/lipu_kasi_web/*.ex
|
|
|
|
# Channels, Controllers, Views, Templates
|
|
/lib/lipu_kasi_web/channels/
|
|
/lib/lipu_kasi_web/controllers/
|
|
/lib/lipu_kasi_web/views/
|
|
/lib/lipu_kasi_web/templates/
|
|
/test/lipu_kasi_web/controllers/
|
|
/test/lipu_kasi_web/views/
|
|
|
|
/test/test_helper.exs
|
|
/test/support/
|
|
|
|
# Assets
|
|
/assets/js/app.js
|
|
/assets/js/foundation.js
|
|
/assets/js/sw.js
|
|
#+end_src
|
|
|
|
This file should be checked in when it's changed, along with =mix.lock=
|
|
|
|
* Mix Configuration ([[./mix.exs]])
|
|
:PROPERTIES:
|
|
:ID: 11e43150-7556-45a9-978e-05e942d40a68
|
|
:END:
|
|
|
|
Mix is the main tool interface for Elixir, an Elixir app almost always has a mix.exs in its root directory, and this is ours: it uses [[file:../noweb_syntax.org][§noweb syntax]] to tangle the file from multiple source code blocks so that the documentation can be placed around each important segment.
|
|
|
|
#+begin_src elixir :tangle mix.exs :noweb tangle
|
|
defmodule LipuKasi.MixProject do
|
|
use Mix.Project
|
|
|
|
<<project-definition>>
|
|
<<application-definition>>
|
|
<<dependencies>>
|
|
<<mix-aliases>>
|
|
<<elixirc-paths>>
|
|
end
|
|
#+end_src
|
|
|
|
Basic Mix project configuration, appliation version, required elixir version, similar things are returned by =project/0=, this calls in to some other functions in the module. My [[file:../fedora_linux.org][§Fedora Linux]] host has Elixir 1.5 installed on it now, I assume I'm going to have to update this sooner or later, though!
|
|
|
|
#+begin_src elixir :noweb-ref project-definition
|
|
def project do
|
|
[
|
|
app: :lipu_kasi,
|
|
version: "0.1.0",
|
|
elixir: "~> 1.5",
|
|
elixirc_paths: elixirc_paths(Mix.env()),
|
|
compilers: [:phoenix, :gettext] ++ Mix.compilers(),
|
|
start_permanent: Mix.env() == :prod,
|
|
aliases: aliases(),
|
|
deps: deps()
|
|
]
|
|
end
|
|
#+end_src
|
|
|
|
Configuration for the OTP application. Type =mix help compile.app= for more information.
|
|
|
|
#+begin_src elixir :noweb-ref application-definition
|
|
def application do
|
|
[
|
|
mod: {LipuKasi.Application, []},
|
|
extra_applications: [:logger, :runtime_tools]
|
|
]
|
|
end
|
|
#+end_src
|
|
|
|
The project dependencies are specified here, this is a simple Phoenix web app for now, we'll probably only need a few other things to ship this.
|
|
|
|
#+begin_src elixir :noweb-ref dependencies
|
|
defp deps do
|
|
[
|
|
{:phoenix, "~> 1.4.10"},
|
|
{:phoenix_pubsub, "~> 1.1"},
|
|
{:phoenix_ecto, "~> 4.0"},
|
|
{:ecto_sql, "~> 3.1"},
|
|
{:postgrex, ">= 0.0.0"},
|
|
{:phoenix_html, "~> 2.11"},
|
|
{:phoenix_live_reload, "~> 1.2", only: :dev},
|
|
{:gettext, "~> 0.11"},
|
|
{:jason, "~> 1.0"},
|
|
{:plug_cowboy, "~> 2.0"}
|
|
]
|
|
end
|
|
#+end_src
|
|
|
|
|
|
Aliases are shortcuts or tasks specific to the current project. For example, to create, migrate and run the seeds file at once: =mix ecto.setup=
|
|
|
|
#+begin_src elixir :noweb-ref mix-aliases
|
|
defp aliases do
|
|
[
|
|
"ecto.setup": ["ecto.create", "ecto.migrate", "run priv/repo/seeds.exs"],
|
|
"ecto.reset": ["ecto.drop", "ecto.setup"],
|
|
test: ["ecto.create --quiet", "ecto.migrate", "test"]
|
|
]
|
|
end
|
|
#+end_src
|
|
|
|
In =:test= environment
|
|
|
|
#+begin_src elixir :noweb-ref elixirc-paths
|
|
defp elixirc_paths(:test), do: ["lib", "test/support"]
|
|
defp elixirc_paths(_), do: ["lib"]
|
|
#+end_src
|
|
|
|
* Elixir Formatter Configurations
|
|
|
|
There are two =.formatter.exs= files, one in the project root, and one stored with the migrations. I'm not sure if this will be used by the org-babel programming, but I'll provide them for now so that I can =git clean=
|
|
|
|
#+begin_src elixir :tangle .formatter.exs
|
|
[
|
|
import_deps: [:ecto, :phoenix],
|
|
inputs: ["*.{ex,exs}", "priv/*/seeds.exs", "{config,lib,test}/**/*.{ex,exs}"],
|
|
subdirectories: ["priv/*/migrations"]
|
|
]
|
|
#+end_src
|
|
|
|
This one is in =priv/repo/migrations/=
|
|
|
|
#+begin_src elixir :tangle priv/repo/migrations/.formatter.exs
|
|
[
|
|
import_deps: [:ecto_sql],
|
|
inputs: ["*.exs"]
|
|
]
|
|
#+end_src
|
|
|
|
* Runtime Configurations
|
|
|
|
This is *not* noweb-tangled since it's not wrapped in a =defmodule=, just tangled line-by-line and concatenated.
|
|
|
|
This file is responsible for configuring your application and its dependencies with the aid of the Mix.Config module:
|
|
|
|
#+begin_src elixir :tangle config/config.exs
|
|
use Mix.Config
|
|
#+end_src
|
|
|
|
We've just got the one Ecto repo, it might be all we need. I don't expect this application to have a lot of weight to it.
|
|
|
|
#+begin_src elixir :tangle config/config.exs
|
|
config :lipu_kasi,
|
|
ecto_repos: [LipuKasi.Repo]
|
|
#+end_src
|
|
|
|
The Phoenix endpoint configuration is pretty straightforward, we use Phoenix's PostgreSQL PubSub adapter these days to have a durable message passing channel between frontend and backend. And this damned secret key will have to be provided for production builds somehow, that'll be fun.
|
|
|
|
#+begin_src elixir :tangle config/config.exs
|
|
config :lipu_kasi, LipuKasiWeb.Endpoint,
|
|
url: [host: "localhost"],
|
|
secret_key_base: "qjGyFdguDBSm8O7R4TI5M084mtgZ2vPE5p7a/2kTMuDCaPRUz9JiFdLK1RNUBggy",
|
|
render_errors: [view: LipuKasiWeb.ErrorView, accepts: ~w(html json)],
|
|
pubsub: [name: LipuKasi.PubSub, adapter: Phoenix.PubSub.PG2]
|
|
#+end_src
|
|
|
|
Logger configuration is simple, I haven't changed this, though I probably would like to have JSON logs some day.
|
|
|
|
#+begin_src elixir :tangle config/config.exs
|
|
config :logger, :console,
|
|
format: "$time $metadata[$level] $message\n",
|
|
metadata: [:request_id]
|
|
#+end_src
|
|
|
|
Use Jason for the Phoenix JSON library, this is also a default. I've used Poison in the past for personal projects, but I don't have anything resembling strong feelings.
|
|
|
|
#+begin_src elixir :tangle config/config.exs
|
|
config :phoenix, :json_library, Jason
|
|
#+end_src
|
|
|
|
Lastly, config.exs loads environment-specific configuration.
|
|
|
|
#+begin_src elixir :tangle config/config.exs
|
|
import_config "#{Mix.env()}.exs"
|
|
#+end_src
|
|
|
|
** Development Configuration
|
|
|
|
#+begin_src elixir :tangle config/dev.exs
|
|
use Mix.Config
|
|
#+end_src
|
|
|
|
The database configuration for local development is pretty simple, a local postgres instance is required, which makes it a bit heavy to do on older hardware, but this is an Elixir decision not mine. There is a sqlite for ecto2, but I only use that in the [[file:../arcology.org][§Arcology]] project right now.
|
|
|
|
#+begin_src elixir :tangle config/dev.exs
|
|
config :lipu_kasi, LipuKasi.Repo,
|
|
username: "postgres",
|
|
password: "postgres",
|
|
database: "lipu_kasi_dev",
|
|
hostname: "localhost",
|
|
show_sensitive_data_on_connection_error: true,
|
|
pool_size: 10
|
|
#+end_src
|
|
|
|
Disable the cache and enable debugging and hot code reloading, as well as setting up the webpack development server for assets.
|
|
|
|
#+begin_src elixir :tangle config/dev.exs
|
|
config :lipu_kasi, LipuKasiWeb.Endpoint,
|
|
http: [port: 4000],
|
|
debug_errors: true,
|
|
code_reloader: true,
|
|
check_origin: false,
|
|
watchers: [
|
|
node: [
|
|
"node_modules/webpack/bin/webpack.js",
|
|
"--mode",
|
|
"development",
|
|
"--watch-stdin",
|
|
cd: Path.expand("../assets", __DIR__)
|
|
]
|
|
]
|
|
#+end_src
|
|
|
|
Configure Phoenix's LiveReload functionality, which watches some file paths and will recompile and reload the browser page if any of the files change:
|
|
|
|
#+begin_src elixir :tangle config/dev.exs
|
|
config :lipu_kasi, LipuKasiWeb.Endpoint,
|
|
live_reload: [
|
|
patterns: [
|
|
~r"priv/static/.*(js|css|png|jpeg|jpg|gif|svg)$",
|
|
~r"priv/gettext/.*(po)$",
|
|
~r"lib/lipu_kasi_web/{live,views}/.*(ex)$",
|
|
~r"lib/lipu_kasi_web/templates/.*(eex)$"
|
|
]
|
|
]
|
|
#+end_src
|
|
|
|
In development we don't need timestamps or other metadata in the logs, and provide more stacktrace context when there is an error.
|
|
|
|
#+begin_src elixir :tangle config/dev.exs
|
|
config :logger, :console, format: "[$level] $message\n"
|
|
config :phoenix, :stacktrace_depth, 20
|
|
#+end_src
|
|
|
|
"Initialize plugs at runtime for faster development compilation", sure!
|
|
|
|
#+begin_src elixir :tangle config/dev.exs
|
|
config :phoenix, :plug_init_mode, :runtime
|
|
#+end_src
|
|
|
|
** Testing Configuration
|
|
|
|
Testing is pretty simple and similar to development, just less stuff going on. The server doesn't run, the logger is less loud, warnings and errors only.
|
|
|
|
#+begin_src elixir :tangle config/test.exs
|
|
use Mix.Config
|
|
|
|
config :lipu_kasi, LipuKasi.Repo,
|
|
username: "postgres",
|
|
password: "postgres",
|
|
database: "lipu_kasi_test",
|
|
hostname: "localhost",
|
|
pool: Ecto.Adapters.SQL.Sandbox
|
|
|
|
config :lipu_kasi, LipuKasiWeb.Endpoint,
|
|
http: [port: 4002],
|
|
server: false
|
|
|
|
config :logger, level: :warn
|
|
#+end_src
|
|
|
|
** Production Configuration
|
|
|
|
#+begin_src elixir :tangle config/prod.exs
|
|
use Mix.Config
|
|
#+end_src
|
|
|
|
Note we also include the path to a cache manifest containing the digested version of static files. This manifest is generated by the `mix phx.digest` task, which you should run after static files are built and before starting your production server.
|
|
|
|
#+begin_src elixir :tangle config/prod.exs
|
|
config :lipu_kasi, LipuKasiWeb.Endpoint,
|
|
url: [host: "lipu.garden", port: 80],
|
|
cache_static_manifest: "priv/static/cache_manifest.json"
|
|
#+end_src
|
|
|
|
Don't print debug messages in production.
|
|
|
|
#+begin_src elixir :tangle config/prod.exs
|
|
config :logger, level: :info
|
|
#+end_src
|
|
|
|
Keeping this secrets file in sync is an [[file:../open_threads.org][§open thread]].
|
|
|
|
#+begin_src elixir :tangle config/prod.exs
|
|
import_config "prod.secret.exs"
|
|
#+end_src
|
|
|
|
When deployed, we'll probably do SSL termination in =nginx=, it's not so hard.
|
|
* Error Views and other base views
|
|
|
|
These are default, I'll change them some day.
|
|
|
|
#+begin_src elixir :tangle lib/lipu_kasi_web/views/error_view.ex
|
|
defmodule LipuKasiWeb.ErrorView do
|
|
use LipuKasiWeb, :view
|
|
|
|
# If you want to customize a particular status code
|
|
# for a certain format, you may uncomment below.
|
|
# def render("500.html", _assigns) do
|
|
# "Internal Server Error"
|
|
# end
|
|
|
|
# By default, Phoenix returns the status message from
|
|
# the template name. For example, "404.html" becomes
|
|
# "Not Found".
|
|
def template_not_found(template, _assigns) do
|
|
Phoenix.Controller.status_message_from_template(template)
|
|
end
|
|
end
|
|
#+end_src
|
|
|
|
#+begin_src elixir :tangle lib/lipu_kasi_web/views/error_helpers.ex
|
|
defmodule LipuKasiWeb.ErrorHelpers do
|
|
@moduledoc """
|
|
Conveniences for translating and building error messages.
|
|
"""
|
|
|
|
use Phoenix.HTML
|
|
|
|
@doc """
|
|
Generates tag for inlined form input errors.
|
|
"""
|
|
def error_tag(form, field) do
|
|
Enum.map(Keyword.get_values(form.errors, field), fn error ->
|
|
content_tag(:span, translate_error(error), class: "help-block")
|
|
end)
|
|
end
|
|
|
|
@doc """
|
|
Translates an error message using gettext.
|
|
"""
|
|
def translate_error({msg, opts}) do
|
|
# When using gettext, we typically pass the strings we want
|
|
# to translate as a static argument:
|
|
#
|
|
# # Translate "is invalid" in the "errors" domain
|
|
# dgettext("errors", "is invalid")
|
|
#
|
|
# # Translate the number of files with plural rules
|
|
# dngettext("errors", "1 file", "%{count} files", count)
|
|
#
|
|
# Because the error messages we show in our forms and APIs
|
|
# are defined inside Ecto, we need to translate them dynamically.
|
|
# This requires us to call the Gettext module passing our gettext
|
|
# backend as first argument.
|
|
#
|
|
# Note we use the "errors" domain, which means translations
|
|
# should be written to the errors.po file. The :count option is
|
|
# set by Ecto and indicates we should also apply plural rules.
|
|
if count = opts[:count] do
|
|
Gettext.dngettext(LipuKasiWeb.Gettext, "errors", msg, msg, count, opts)
|
|
else
|
|
Gettext.dgettext(LipuKasiWeb.Gettext, "errors", msg, opts)
|
|
end
|
|
end
|
|
end
|
|
#+end_src
|
|
|
|
#+begin_src elixir :tangle test/lipu_kasi_web/views/error_view_test.exs :mkdirp yes
|
|
defmodule LipuKasiWeb.ErrorViewTest do
|
|
use LipuKasiWeb.ConnCase, async: true
|
|
|
|
# Bring render/3 and render_to_string/3 for testing custom views
|
|
import Phoenix.View
|
|
|
|
test "renders 404.html" do
|
|
assert render_to_string(LipuKasiWeb.ErrorView, "404.html", []) == "Not Found"
|
|
end
|
|
|
|
test "renders 500.html" do
|
|
assert render_to_string(LipuKasiWeb.ErrorView, "500.html", []) == "Internal Server Error"
|
|
end
|
|
end
|
|
#+end_src
|
|
|
|
IDK why I need to hold on to this one.
|
|
|
|
#+begin_src elixir :tangle lib/lipu_kasi_web/views/layout_view.ex :mkdirp yes
|
|
defmodule LipuKasiWeb.LayoutView do
|
|
use LipuKasiWeb, :view
|
|
end
|
|
#+end_src
|