14 KiB
Project Configuration, Mix, etc
- Project
.gitignore
file - Mix Configuration (/rrix/lipu_kasi/src/branch/main/mix.exs)
- Elixir Formatter Configurations
- Runtime Configurations
- Error Views and other base views
In here I de-tangle the Phoenix and Mix infrastructure for §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 §Literate Programming §org-mode application, developed within my §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 §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.
# 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/
Files which are provided from tangles will need to be managed here:
/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
This file should be checked in when it's changed, along with mix.lock
Mix Configuration (/rrix/lipu_kasi/src/branch/main/mix.exs)
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 §noweb syntax to tangle the file from multiple source code blocks so that the documentation can be placed around each important segment.
defmodule LipuKasi.MixProject do
use Mix.Project
<<project-definition>>
<<application-definition>>
<<dependencies>>
<<mix-aliases>>
<<elixirc-paths>>
end
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 §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!
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
Configuration for the OTP application. Type mix help compile.app
for more information.
def application do
[
mod: {LipuKasi.Application, []},
extra_applications: [:logger, :runtime_tools]
]
end
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.
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
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
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
In :test
environment
defp elixirc_paths(:test), do: ["lib", "test/support"]
defp elixirc_paths(_), do: ["lib"]
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
[
import_deps: [:ecto, :phoenix],
inputs: ["*.{ex,exs}", "priv/*/seeds.exs", "{config,lib,test}/**/*.{ex,exs}"],
subdirectories: ["priv/*/migrations"]
]
This one is in priv/repo/migrations/
[
import_deps: [:ecto_sql],
inputs: ["*.exs"]
]
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:
use Mix.Config
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.
config :lipu_kasi,
ecto_repos: [LipuKasi.Repo]
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.
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]
Logger configuration is simple, I haven't changed this, though I probably would like to have JSON logs some day.
config :logger, :console,
format: "$time $metadata[$level] $message\n",
metadata: [:request_id]
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.
config :phoenix, :json_library, Jason
Lastly, config.exs loads environment-specific configuration.
import_config "#{Mix.env()}.exs"
Development Configuration
use Mix.Config
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 §Arcology project right now.
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
Disable the cache and enable debugging and hot code reloading, as well as setting up the webpack development server for assets.
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__)
]
]
Configure Phoenix's LiveReload functionality, which watches some file paths and will recompile and reload the browser page if any of the files change:
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)$"
]
]
In development we don't need timestamps or other metadata in the logs, and provide more stacktrace context when there is an error.
config :logger, :console, format: "[$level] $message\n"
config :phoenix, :stacktrace_depth, 20
"Initialize plugs at runtime for faster development compilation", sure!
config :phoenix, :plug_init_mode, :runtime
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.
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
Production Configuration
use Mix.Config
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.
config :lipu_kasi, LipuKasiWeb.Endpoint,
url: [host: "lipu.garden", port: 80],
cache_static_manifest: "priv/static/cache_manifest.json"
Don't print debug messages in production.
config :logger, level: :info
Keeping this secrets file in sync is an §open thread.
import_config "prod.secret.exs"
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.
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
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
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
IDK why I need to hold on to this one.
defmodule LipuKasiWeb.LayoutView do
use LipuKasiWeb, :view
end