Use totally new build system written in Elixir
This allows for fewer npm dependencies and fewer packages to handle (some of the currently installed babel packages are for possible future use). Hopefully the new build style is more explicit even though it is more verbose. Both building in dev and prod mode and watching is supported.
This commit is contained in:
parent
b80cb5433f
commit
c733b37389
24 changed files with 727 additions and 105 deletions
7
.babelrc
Normal file
7
.babelrc
Normal file
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"presets": [
|
||||
["es2015", {"modules": false}],
|
||||
"es2016",
|
||||
"es2017"
|
||||
]
|
||||
}
|
2
.gitignore
vendored
2
.gitignore
vendored
|
@ -20,7 +20,7 @@ config/*.exs
|
|||
# we ignore priv/static/{css,js}. You may want to
|
||||
# comment this depending on your deployment strategy.
|
||||
/priv/static/
|
||||
/.tmp/
|
||||
|
||||
# Custom templates should be ignored
|
||||
/web/templates/custom/
|
||||
|
||||
|
|
|
@ -13,7 +13,7 @@ The engine consists of two parts:
|
|||
|
||||
* `git clone`
|
||||
* Copy `config/*.exs.dist`, removing the `.dist` ending and go through the configs.
|
||||
* `npm install && gulp` to build the frontend.
|
||||
* `npm install && mix frontend.build` to build the frontend.
|
||||
* Put some content into the data path you specified, at least a `menu` file.
|
||||
* `mix phoenix.server` to run the development server.
|
||||
|
||||
|
|
76
gulpfile.js
76
gulpfile.js
|
@ -1,76 +0,0 @@
|
|||
// Node modules
|
||||
var fs = require('fs'), vm = require('vm'), chalk = require('chalk');
|
||||
|
||||
// Gulp and plugins
|
||||
var gulp = require('gulp'), concat = require('gulp-concat'),
|
||||
replace = require('gulp-replace'), uglify = require('gulp-uglify');
|
||||
|
||||
// Gulp minify for smallinizing our CSS
|
||||
var minify = require('gulp-minify-css');
|
||||
|
||||
// Gulp filesize for printing sizes before and after minification
|
||||
var size = require('gulp-size');
|
||||
|
||||
// SASS compiler
|
||||
var sass = require('gulp-sass');
|
||||
|
||||
// Deleting files
|
||||
var vinylPaths = require('vinyl-paths');
|
||||
var del = require('del');
|
||||
|
||||
// Source maps
|
||||
var sourcemaps = require('gulp-sourcemaps');
|
||||
|
||||
var node_path = 'node_modules/';
|
||||
|
||||
var dest_path = 'priv/static/';
|
||||
|
||||
// Minifies all JS files and copies to target path. Files are not concatenated
|
||||
gulp.task('js', function() {
|
||||
return gulp.src([
|
||||
// Uncomment these two lines if you need bootstrap or jQuery
|
||||
//node_path + 'jquery/dist/jquery.js',
|
||||
//node_path + 'bootstrap-sass/assets/javascripts/bootstrap.js',
|
||||
'web/static/js/*.js'
|
||||
])
|
||||
|
||||
//.pipe(concat('app.js'))
|
||||
.pipe(sourcemaps.init())
|
||||
.pipe(size({title: 'Original JS'}))
|
||||
.pipe(uglify({ preserveComments: false }).on('error', console.error.bind(console)))
|
||||
.pipe(size({title: 'Minified JS'}))
|
||||
.pipe(sourcemaps.write('./'))
|
||||
.pipe(gulp.dest(dest_path + 'js/'));
|
||||
});
|
||||
|
||||
// Compiles SASS files. Will create one output file for each input file, but usually you
|
||||
// only have app.scss which will be output as app.css
|
||||
gulp.task('css', function() {
|
||||
return gulp.src([
|
||||
'web/static/css/*.scss'
|
||||
])
|
||||
.pipe(sass().on('error', sass.logError))
|
||||
.pipe(sourcemaps.init())
|
||||
.pipe(size({title: 'Compiled CSS'}))
|
||||
.pipe(minify())
|
||||
.pipe(size({title: 'Minified CSS'}))
|
||||
.pipe(sourcemaps.write('./'))
|
||||
.pipe(gulp.dest(dest_path + 'css/'));
|
||||
});
|
||||
|
||||
// Copies fonts
|
||||
gulp.task('fonts', function() {
|
||||
return gulp.src(node_path + 'bootstrap-sass/assets/fonts/bootstrap/*')
|
||||
.pipe(gulp.dest(dest_path + 'fonts/'));
|
||||
});
|
||||
|
||||
// Removes all files from dest_path
|
||||
gulp.task('clean', function() {
|
||||
return gulp.src(dest_path + '**/*', { read: false })
|
||||
.pipe(vinylPaths(del));
|
||||
});
|
||||
|
||||
gulp.task('default', ['js', 'css', 'fonts'], function(callback) {
|
||||
callback();
|
||||
console.log('\nPlaced optimized files in ' + chalk.magenta(dest_path));
|
||||
});
|
|
@ -9,6 +9,7 @@ defmodule MebeWeb.Endpoint do
|
|||
# Code reloading can be explicitly enabled under the
|
||||
# :code_reloader configuration of your endpoint.
|
||||
if code_reloading? do
|
||||
socket "/phoenix/live_reload/socket", Phoenix.LiveReloader.Socket
|
||||
plug Phoenix.LiveReloader
|
||||
plug Phoenix.CodeReloader
|
||||
end
|
||||
|
|
72
lib/mix/tasks/apprunner.exs
Normal file
72
lib/mix/tasks/apprunner.exs
Normal file
|
@ -0,0 +1,72 @@
|
|||
defmodule AppRunner do
|
||||
def wait_for_eof() do
|
||||
case IO.getn("", 1024) do
|
||||
:eof -> nil
|
||||
_ -> wait_for_eof()
|
||||
end
|
||||
end
|
||||
|
||||
def exec(program, args) do
|
||||
Port.open(
|
||||
{:spawn_executable, program},
|
||||
[
|
||||
:exit_status,
|
||||
:stderr_to_stdout, # Redirect stderr to stdout to log properly
|
||||
args: args,
|
||||
line: 1024
|
||||
]
|
||||
)
|
||||
end
|
||||
|
||||
def get_pid(port) do
|
||||
case Port.info(port) do
|
||||
nil ->
|
||||
nil
|
||||
|
||||
info when is_list(info) ->
|
||||
case Keyword.get(info, :os_pid) do
|
||||
nil -> nil
|
||||
pid -> Integer.to_string(pid)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def kill(pid) do
|
||||
System.find_executable("kill") |> System.cmd([pid])
|
||||
end
|
||||
|
||||
def wait_loop(port) do
|
||||
receive do
|
||||
{_, {:data, {:eol, msg}}} ->
|
||||
msg |> :unicode.characters_to_binary(:unicode) |> IO.puts()
|
||||
|
||||
{_, {:data, {:noeol, msg}}} ->
|
||||
msg |> :unicode.characters_to_binary(:unicode) |> IO.write()
|
||||
|
||||
{_, :eof_received} ->
|
||||
get_pid(port) |> kill()
|
||||
:erlang.halt(0)
|
||||
|
||||
{_, :closed} ->
|
||||
:erlang.halt(0)
|
||||
|
||||
{_, {:exit_status, status}} ->
|
||||
:erlang.halt(status)
|
||||
|
||||
{:EXIT, _, _} ->
|
||||
:erlang.halt(1)
|
||||
end
|
||||
|
||||
wait_loop(port)
|
||||
end
|
||||
end
|
||||
|
||||
[program | args] = System.argv()
|
||||
port = AppRunner.exec(program, args)
|
||||
|
||||
Task.async(fn ->
|
||||
AppRunner.wait_for_eof()
|
||||
:eof_received
|
||||
end)
|
||||
|
||||
AppRunner.wait_loop(port)
|
14
lib/mix/tasks/frontend.build.assets.ex
Normal file
14
lib/mix/tasks/frontend.build.assets.ex
Normal file
|
@ -0,0 +1,14 @@
|
|||
defmodule Mix.Tasks.Frontend.Build.Assets do
|
||||
use Mix.Task
|
||||
import MebeWeb.{FrontendConfs}
|
||||
|
||||
@shortdoc "Copy other assets to target dir"
|
||||
|
||||
def run(_) do
|
||||
# Ensure target path exists
|
||||
out_path = "#{dist_path()}/fonts"
|
||||
|
||||
File.mkdir_p!(out_path)
|
||||
File.cp_r!("#{node_path()}/bootstrap-sass/assets/fonts/bootstrap", out_path)
|
||||
end
|
||||
end
|
31
lib/mix/tasks/frontend.build.css.compile.ex
Normal file
31
lib/mix/tasks/frontend.build.css.compile.ex
Normal file
|
@ -0,0 +1,31 @@
|
|||
defmodule Mix.Tasks.Frontend.Build.Css.Compile do
|
||||
use Mix.Task
|
||||
import MebeWeb.{TaskUtils, FrontendConfs}
|
||||
|
||||
@shortdoc "Build the SCSS sources"
|
||||
|
||||
def bin(), do: node_bin("node-sass")
|
||||
|
||||
def out_path(:prod), do: "#{tmp_path()}/compiled/css"
|
||||
def out_path(_), do: "#{dist_path()}/css"
|
||||
|
||||
def args(), do: [
|
||||
"-o",
|
||||
out_path(Mix.env()),
|
||||
"--source-map",
|
||||
"true",
|
||||
"--include-path",
|
||||
"#{node_path()}/bootstrap-sass/assets/stylesheets",
|
||||
"--precision",
|
||||
"8"
|
||||
]
|
||||
|
||||
def scss_file(), do: "#{src_path()}/css/app.scss"
|
||||
|
||||
def run(_) do
|
||||
exec(
|
||||
bin(),
|
||||
args() ++ [scss_file()]
|
||||
) |> listen()
|
||||
end
|
||||
end
|
15
lib/mix/tasks/frontend.build.css.ex
Normal file
15
lib/mix/tasks/frontend.build.css.ex
Normal file
|
@ -0,0 +1,15 @@
|
|||
defmodule Mix.Tasks.Frontend.Build.Css do
|
||||
use Mix.Task
|
||||
import MebeWeb.TaskUtils
|
||||
|
||||
@shortdoc "Build the frontend CSS"
|
||||
|
||||
def run(_) do
|
||||
js_task = case Mix.env() do
|
||||
:prod -> "frontend.build.css.minify"
|
||||
_ -> "frontend.build.css.compile"
|
||||
end
|
||||
|
||||
run_task(js_task)
|
||||
end
|
||||
end
|
30
lib/mix/tasks/frontend.build.css.minify.ex
Normal file
30
lib/mix/tasks/frontend.build.css.minify.ex
Normal file
|
@ -0,0 +1,30 @@
|
|||
defmodule Mix.Tasks.Frontend.Build.Css.Minify do
|
||||
use Mix.Task
|
||||
import MebeWeb.{TaskUtils, FrontendConfs}
|
||||
|
||||
@shortdoc "Minify built CSS files"
|
||||
@preferred_cli_env :prod
|
||||
|
||||
def run(_) do
|
||||
run_task("frontend.build.css.compile")
|
||||
|
||||
in_path = "#{tmp_path()}/compiled/css"
|
||||
in_file = "#{in_path}/app.css"
|
||||
out_path = "#{dist_path()}/css"
|
||||
out_file = "#{out_path}/app.css"
|
||||
|
||||
File.mkdir_p!(out_path)
|
||||
|
||||
exec(
|
||||
node_bin("cssnano"),
|
||||
[
|
||||
in_file,
|
||||
out_file,
|
||||
"--sourcemap",
|
||||
"#{out_path}/app.css.map"
|
||||
]
|
||||
) |> listen()
|
||||
|
||||
print_size(out_file, in_file)
|
||||
end
|
||||
end
|
16
lib/mix/tasks/frontend.build.ex
Normal file
16
lib/mix/tasks/frontend.build.ex
Normal file
|
@ -0,0 +1,16 @@
|
|||
defmodule Mix.Tasks.Frontend.Build do
|
||||
use Mix.Task
|
||||
import MebeWeb.TaskUtils
|
||||
|
||||
@shortdoc "Build the frontend"
|
||||
|
||||
def run(_) do
|
||||
run_task("frontend.clean")
|
||||
|
||||
run_tasks([
|
||||
"frontend.build.js",
|
||||
"frontend.build.css",
|
||||
"frontend.build.assets"
|
||||
])
|
||||
end
|
||||
end
|
37
lib/mix/tasks/frontend.build.js.bundle.ex
Normal file
37
lib/mix/tasks/frontend.build.js.bundle.ex
Normal file
|
@ -0,0 +1,37 @@
|
|||
defmodule Mix.Tasks.Frontend.Build.Js.Bundle do
|
||||
use Mix.Task
|
||||
import MebeWeb.{TaskUtils, FrontendConfs}
|
||||
|
||||
@shortdoc "Bundle the JavaScript sources into app.js"
|
||||
|
||||
def bin(), do: node_bin("rollup")
|
||||
|
||||
def out_path(:prod), do: "#{tmp_path()}/bundled/js"
|
||||
def out_path(_), do: "#{dist_path()}/js"
|
||||
|
||||
def args() do
|
||||
op = out_path(Mix.env())
|
||||
|
||||
[
|
||||
"--config",
|
||||
"rollup.config.js",
|
||||
"--input",
|
||||
"#{tmp_path()}/transpiled/js/app.js",
|
||||
"--output",
|
||||
"#{op}/app.js",
|
||||
"--format",
|
||||
"cjs",
|
||||
"--sourcemap",
|
||||
"#{op}/app.js.map"
|
||||
]
|
||||
end
|
||||
|
||||
def run(_) do
|
||||
run_task("frontend.build.js.transpile")
|
||||
|
||||
exec(
|
||||
bin(),
|
||||
args()
|
||||
) |> listen()
|
||||
end
|
||||
end
|
15
lib/mix/tasks/frontend.build.js.ex
Normal file
15
lib/mix/tasks/frontend.build.js.ex
Normal file
|
@ -0,0 +1,15 @@
|
|||
defmodule Mix.Tasks.Frontend.Build.Js do
|
||||
use Mix.Task
|
||||
import MebeWeb.TaskUtils
|
||||
|
||||
@shortdoc "Build the frontend JavaScript"
|
||||
|
||||
def run(_) do
|
||||
js_task = case Mix.env() do
|
||||
:prod -> "frontend.build.js.minify"
|
||||
_ -> "frontend.build.js.bundle"
|
||||
end
|
||||
|
||||
run_task(js_task)
|
||||
end
|
||||
end
|
38
lib/mix/tasks/frontend.build.js.minify.ex
Normal file
38
lib/mix/tasks/frontend.build.js.minify.ex
Normal file
|
@ -0,0 +1,38 @@
|
|||
defmodule Mix.Tasks.Frontend.Build.Js.Minify do
|
||||
use Mix.Task
|
||||
import MebeWeb.{TaskUtils, FrontendConfs}
|
||||
|
||||
@shortdoc "Minify built JS files"
|
||||
@preferred_cli_env :prod
|
||||
|
||||
def run(_) do
|
||||
run_task("frontend.build.js.bundle")
|
||||
|
||||
in_path = "#{tmp_path()}/bundled/js"
|
||||
in_file = "#{in_path}/app.js"
|
||||
out_path = "#{dist_path()}/js"
|
||||
out_file = "#{out_path}/app.js"
|
||||
|
||||
File.mkdir_p!(out_path)
|
||||
|
||||
exec(
|
||||
node_bin("uglifyjs"),
|
||||
[
|
||||
"--in-source-map",
|
||||
"#{in_path}/app.js.map",
|
||||
"--source-map",
|
||||
"#{out_path}/app.js.map",
|
||||
"--source-map-url",
|
||||
"app.js.map",
|
||||
"--screw-ie8",
|
||||
"-m",
|
||||
"-o",
|
||||
out_file,
|
||||
"--",
|
||||
in_file
|
||||
]
|
||||
) |> listen()
|
||||
|
||||
print_size(out_file, in_file)
|
||||
end
|
||||
end
|
23
lib/mix/tasks/frontend.build.js.transpile.ex
Normal file
23
lib/mix/tasks/frontend.build.js.transpile.ex
Normal file
|
@ -0,0 +1,23 @@
|
|||
defmodule Mix.Tasks.Frontend.Build.Js.Transpile do
|
||||
use Mix.Task
|
||||
import MebeWeb.{TaskUtils, FrontendConfs}
|
||||
|
||||
@shortdoc "Transpile JS sources to ES5"
|
||||
|
||||
def bin(), do: node_bin("babel")
|
||||
|
||||
def args(), do: [
|
||||
"#{src_path()}/js",
|
||||
"--out-dir",
|
||||
"#{tmp_path()}/transpiled/js",
|
||||
"--source-maps",
|
||||
"inline"
|
||||
]
|
||||
|
||||
def run(_) do
|
||||
exec(
|
||||
bin(),
|
||||
args()
|
||||
) |> listen()
|
||||
end
|
||||
end
|
11
lib/mix/tasks/frontend.clean.ex
Normal file
11
lib/mix/tasks/frontend.clean.ex
Normal file
|
@ -0,0 +1,11 @@
|
|||
defmodule Mix.Tasks.Frontend.Clean do
|
||||
use Mix.Task
|
||||
import MebeWeb.{FrontendConfs}
|
||||
|
||||
@shortdoc "Clean build artifacts"
|
||||
|
||||
def run(_) do
|
||||
File.rm_rf!(tmp_path())
|
||||
File.rm_rf!(dist_path())
|
||||
end
|
||||
end
|
33
lib/mix/tasks/frontend.watch.ex
Normal file
33
lib/mix/tasks/frontend.watch.ex
Normal file
|
@ -0,0 +1,33 @@
|
|||
defmodule Mix.Tasks.Frontend.Watch do
|
||||
use Mix.Task
|
||||
import MebeWeb.{TaskUtils, FrontendConfs}
|
||||
alias Mix.Tasks.Frontend.Build.Js.Transpile, as: TranspileJS
|
||||
alias Mix.Tasks.Frontend.Build.Js.Bundle, as: BundleJS
|
||||
alias Mix.Tasks.Frontend.Build.Css.Compile, as: CompileCSS
|
||||
|
||||
@shortdoc "Watch frontend and rebuild when necessary"
|
||||
|
||||
def run(_) do
|
||||
run_task("frontend.build")
|
||||
|
||||
[
|
||||
exec(
|
||||
TranspileJS.bin(),
|
||||
TranspileJS.args() ++ ["-w"]
|
||||
),
|
||||
|
||||
exec(
|
||||
BundleJS.bin(),
|
||||
BundleJS.args() ++ ["-w"]
|
||||
),
|
||||
|
||||
exec(
|
||||
CompileCSS.bin(),
|
||||
CompileCSS.args() ++ [
|
||||
"-w",
|
||||
CompileCSS.scss_file()
|
||||
]
|
||||
)
|
||||
] |> watch()
|
||||
end
|
||||
end
|
34
lib/mix/tasks/frontend_confs.ex
Normal file
34
lib/mix/tasks/frontend_confs.ex
Normal file
|
@ -0,0 +1,34 @@
|
|||
defmodule MebeWeb.FrontendConfs do
|
||||
import MebeWeb.TaskUtils
|
||||
|
||||
@moduledoc """
|
||||
Project specific paths and other stuff for Mebe frontend.
|
||||
"""
|
||||
|
||||
@doc """
|
||||
Get absolute path to node_modules.
|
||||
"""
|
||||
def node_path() do
|
||||
"#{proj_path()}/node_modules"
|
||||
end
|
||||
|
||||
@doc """
|
||||
Get absolute path to binary installed with npm.
|
||||
"""
|
||||
def node_bin(executable), do: "#{node_path()}/.bin/#{executable}"
|
||||
|
||||
@doc """
|
||||
Get absolute path to source directory for frontend build.
|
||||
"""
|
||||
def src_path(), do: "#{proj_path()}/web/static"
|
||||
|
||||
@doc """
|
||||
Get absolute path to temp directory for build artifacts.
|
||||
"""
|
||||
def tmp_path(), do: "#{proj_path()}/.tmp"
|
||||
|
||||
@doc """
|
||||
Get absolute path to target directory for frontend build.
|
||||
"""
|
||||
def dist_path(), do: "#{proj_path()}/priv/static"
|
||||
end
|
311
lib/mix/tasks/utils.ex
Normal file
311
lib/mix/tasks/utils.ex
Normal file
|
@ -0,0 +1,311 @@
|
|||
defmodule MebeWeb.TaskUtils do
|
||||
@moduledoc """
|
||||
Utilities for project build tasks.
|
||||
"""
|
||||
|
||||
require Logger
|
||||
|
||||
@elixir System.find_executable("elixir")
|
||||
|
||||
@default_task_timeout 60000
|
||||
|
||||
defmodule Program do
|
||||
@moduledoc """
|
||||
Program to execute with arguments. Name is used for prefixing logs.
|
||||
"""
|
||||
defstruct [
|
||||
name: "",
|
||||
port: nil,
|
||||
pending_output: ""
|
||||
]
|
||||
end
|
||||
|
||||
@doc """
|
||||
Get configuration value.
|
||||
"""
|
||||
def conf(key) when is_atom(key) do
|
||||
Application.get_env(:code_stats, key)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Get path to project root directory.
|
||||
"""
|
||||
def proj_path() do
|
||||
Path.expand("../../..", __DIR__)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Get absolute path to a program in $PATH.
|
||||
"""
|
||||
def exec_path(program) do
|
||||
System.find_executable(program)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Run the given Mix task and wait for it to stop before returning.
|
||||
|
||||
See run_tasks/2 for the argument description.
|
||||
"""
|
||||
def run_task(task, timeout \\ @default_task_timeout) do
|
||||
run_tasks([task], timeout)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Run the given Mix tasks in parallel and wait for them all to stop
|
||||
before returning.
|
||||
|
||||
Tasks should be tuples {task_name, args} or binaries (no args).
|
||||
"""
|
||||
def run_tasks(tasks, timeout \\ 60000) do
|
||||
run_and_log = fn task, args ->
|
||||
Logger.info("[Started] #{task}")
|
||||
Mix.Task.run(task, args)
|
||||
Logger.info("[Finished] #{task}")
|
||||
end
|
||||
|
||||
tasks
|
||||
|> Enum.map(fn
|
||||
task when is_binary(task) ->
|
||||
fn -> run_and_log.(task, []) end
|
||||
{task, args} ->
|
||||
fn -> run_and_log.(task, args) end
|
||||
end)
|
||||
|> run_funs(timeout)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Run the given functions in parallel and wait for them all to stop
|
||||
before returning.
|
||||
|
||||
Functions can either be anonymous functions or tuples of
|
||||
{module, fun, args}.
|
||||
"""
|
||||
def run_funs(funs, timeout \\ 60000) when is_list(funs) do
|
||||
funs
|
||||
|> Enum.map(fn
|
||||
fun when is_function(fun) ->
|
||||
Task.async(fun)
|
||||
{module, fun, args} ->
|
||||
Task.async(module, fun, args)
|
||||
end)
|
||||
|> Enum.map(fn task ->
|
||||
Task.await(task, timeout)
|
||||
end)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Start an external program with apprunner.
|
||||
|
||||
Apprunner handles killing the program if BEAM is abruptly shut down.
|
||||
Name is used as a prefix for logging output.
|
||||
|
||||
Options that can be given:
|
||||
|
||||
- name: Use as name for logging, otherwise name of binary is used.
|
||||
- cd: Directory to change to before executing.
|
||||
"""
|
||||
def exec(executable, args, opts \\ []) do
|
||||
name = Keyword.get(
|
||||
opts,
|
||||
:name,
|
||||
(executable |> Path.rootname() |> Path.basename())
|
||||
)
|
||||
|
||||
options = [
|
||||
:exit_status, # Send msg with status when command stops
|
||||
args: ["#{proj_path()}/lib/mix/tasks/apprunner.exs" | [executable | args]],
|
||||
line: 1024 # Send command output as lines of 1k length
|
||||
]
|
||||
|
||||
options = case Keyword.get(opts, :cd) do
|
||||
nil -> options
|
||||
cd -> Keyword.put(options, :cd, cd)
|
||||
end
|
||||
|
||||
Logger.debug("[Spawned] #{name}")
|
||||
|
||||
%Program{
|
||||
name: name,
|
||||
pending_output: "",
|
||||
port: Port.open(
|
||||
{:spawn_executable, @elixir},
|
||||
options
|
||||
)
|
||||
}
|
||||
end
|
||||
|
||||
@doc """
|
||||
Listen to messages from programs and print them to the screen.
|
||||
|
||||
Also listens to input from user and returns if input is given.
|
||||
"""
|
||||
def listen(programs, task \\ nil, opts \\ [])
|
||||
|
||||
def listen([], _, _), do: nil
|
||||
|
||||
def listen(%Program{} = program, task, opts) do
|
||||
listen([program], task, opts)
|
||||
end
|
||||
|
||||
def listen(programs, task, opts) do
|
||||
# Start another task to ask for user input if we are in watch mode
|
||||
task = with \
|
||||
true <- Keyword.get(opts, :watch, false),
|
||||
nil <- task,
|
||||
{:ok, task} <- Task.start_link(__MODULE__, :wait_for_input, [self()])
|
||||
do
|
||||
task
|
||||
end
|
||||
|
||||
programs = receive do
|
||||
:user_input_received ->
|
||||
[]
|
||||
|
||||
{port, {:data, {:eol, msg}}} ->
|
||||
program = Enum.find(programs, get_port_checker(port))
|
||||
msg = :unicode.characters_to_binary(msg, :unicode)
|
||||
|
||||
prefix = "[#{program.name}] #{program.pending_output}"
|
||||
|
||||
Logger.debug(prefix <> msg)
|
||||
|
||||
programs
|
||||
|> Enum.reject(get_port_checker(port))
|
||||
|> Enum.concat([
|
||||
%{program | pending_output: ""}
|
||||
])
|
||||
|
||||
{port, {:data, {:noeol, msg}}} ->
|
||||
program = Enum.find(programs, get_port_checker(port))
|
||||
msg = :unicode.characters_to_binary(msg, :unicode)
|
||||
|
||||
programs
|
||||
|> Enum.reject(get_port_checker(port))
|
||||
|> Enum.concat([
|
||||
%{program | pending_output: "#{program.pending_output}#{msg}"}
|
||||
])
|
||||
|
||||
# Port was closed normally after being told to close
|
||||
{port, :closed} ->
|
||||
handle_closed(programs, port)
|
||||
|
||||
# Port closed because the program closed by itself
|
||||
{port, {:exit_status, 0}} ->
|
||||
handle_closed(programs, port)
|
||||
|
||||
# Program closed with error status
|
||||
{port, {:exit_status, status}} ->
|
||||
program = Enum.find(programs, get_port_checker(port))
|
||||
Logger.error("Program #{program.name} returned status #{status}.")
|
||||
raise "Failed status #{status} from #{program.name}!"
|
||||
|
||||
# Port crashed
|
||||
{:EXIT, port, _} ->
|
||||
handle_closed(programs, port)
|
||||
end
|
||||
|
||||
if not Enum.empty?(programs) do
|
||||
listen(programs, task, opts)
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Kill a running program returned by exec().
|
||||
"""
|
||||
def kill(%Program{name: name, port: port}) do
|
||||
if name != nil do
|
||||
Logger.debug("[Killing] #{name}")
|
||||
end
|
||||
|
||||
send(port, {self(), :close})
|
||||
end
|
||||
|
||||
@doc """
|
||||
Print output from given programs to screen until user input is given.
|
||||
|
||||
When user input is given, kill programs and return.
|
||||
"""
|
||||
def watch(%Program{} = program), do: watch([program])
|
||||
|
||||
def watch(programs) when is_list(programs) do
|
||||
Logger.info("Programs started, press ENTER to exit.")
|
||||
|
||||
listen(programs, nil, watch: true)
|
||||
|
||||
Logger.info("ENTER received, killing tasks.")
|
||||
|
||||
Enum.each(programs, &kill/1)
|
||||
:ok
|
||||
end
|
||||
|
||||
@doc """
|
||||
Print file size of given file in human readable form.
|
||||
|
||||
If old file is given as second argument, print the old file's size
|
||||
and the diff also.
|
||||
"""
|
||||
def print_size(new_file, old_file \\ nil) do
|
||||
new_size = get_size(new_file)
|
||||
|
||||
{prefix, postfix} = if old_file != nil do
|
||||
old_size = get_size(old_file)
|
||||
|
||||
{
|
||||
"#{human_size(old_size)} -> ",
|
||||
" Diff: #{human_size(old_size - new_size)}"
|
||||
}
|
||||
else
|
||||
{"", ""}
|
||||
end
|
||||
|
||||
Logger.debug("#{Path.basename(new_file)}: #{prefix}#{human_size(new_size)}.#{postfix}")
|
||||
end
|
||||
|
||||
def wait_for_input(target) do
|
||||
IO.gets("")
|
||||
send(target, :user_input_received)
|
||||
end
|
||||
|
||||
defp get_size(file) do
|
||||
%File.Stat{size: size} = File.stat!(file)
|
||||
size
|
||||
end
|
||||
|
||||
defp human_size(size) do
|
||||
size_units = ["B", "kiB", "MiB", "GiB", "TiB", "PiB", "EiB", "ZiB", "YiB"] # You never know
|
||||
|
||||
human_size(size, size_units)
|
||||
end
|
||||
|
||||
defp human_size(size, [unit | []]), do: size_with_unit(size, unit)
|
||||
|
||||
defp human_size(size, [unit | rest]) do
|
||||
if size > 1024 do
|
||||
human_size(size / 1024, rest)
|
||||
else
|
||||
size_with_unit(size, unit)
|
||||
end
|
||||
end
|
||||
|
||||
defp size_with_unit(size, unit), do: "#{Float.round(size, 2)} #{unit}"
|
||||
|
||||
defp get_port_checker(port) do
|
||||
fn %Program{port: program_port} ->
|
||||
program_port == port
|
||||
end
|
||||
end
|
||||
|
||||
defp handle_closed(programs, port) do
|
||||
case Enum.find(programs, get_port_checker(port)) do
|
||||
%Program{} = program ->
|
||||
Logger.debug("[Stopped] #{program.name}")
|
||||
|
||||
programs
|
||||
|> Enum.reject(get_port_checker(port))
|
||||
|
||||
nil ->
|
||||
# Program was already removed
|
||||
programs
|
||||
end
|
||||
end
|
||||
end
|
35
package.json
35
package.json
|
@ -1,29 +1,24 @@
|
|||
{
|
||||
"name": "mebe_web",
|
||||
"version": "1.0.0",
|
||||
"version": "1.1.0",
|
||||
"repository": {},
|
||||
"license": "MIT",
|
||||
"description": "Minimalistic Elixir Blog Engine",
|
||||
"private": true,
|
||||
"devDependencies": {
|
||||
"bower": "~1.4.1",
|
||||
"chalk": "^1.0.0",
|
||||
"del": "^1.1.1",
|
||||
"end-of-stream": "~0.1.5",
|
||||
"event-stream": "~3.3.1",
|
||||
"gulp": "^3.8.11",
|
||||
"gulp-concat": "~2.5.2",
|
||||
"gulp-html-replace": "~1.4.5",
|
||||
"gulp-minify-css": "~1.1.1",
|
||||
"gulp-replace": "~0.5.3",
|
||||
"gulp-sass": "~2.0.0",
|
||||
"gulp-size": "~1.2.1",
|
||||
"gulp-sourcemaps": "^1.5.2",
|
||||
"gulp-uglify": "~1.2.0",
|
||||
"vinyl-paths": "^1.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"bootstrap-sass": "^3.3.5",
|
||||
"jquery": "^2.1.4"
|
||||
"babel-cli": "~6.22.2",
|
||||
"babel-core": "~6.22.1",
|
||||
"babel-polyfill": "~6.22.0",
|
||||
"babel-preset-es2015": "~6.22.0",
|
||||
"babel-preset-es2016": "~6.22.0",
|
||||
"babel-preset-es2017": "~6.22.0",
|
||||
"bootstrap-sass": "~3.3.7",
|
||||
"cssnano": "~3.10.0",
|
||||
"cssnano-cli": "~1.0.5",
|
||||
"node-sass": "~4.3.0",
|
||||
"rollup": "~0.41.4",
|
||||
"rollup-plugin-sourcemaps": "~0.4.1",
|
||||
"rollup-watch": "~3.2.2",
|
||||
"uglify-js": "~2.7.5"
|
||||
}
|
||||
}
|
||||
|
|
7
rollup.config.js
Normal file
7
rollup.config.js
Normal file
|
@ -0,0 +1,7 @@
|
|||
import sourcemaps from 'rollup-plugin-sourcemaps';
|
||||
|
||||
export default {
|
||||
plugins: [
|
||||
sourcemaps()
|
||||
]
|
||||
};
|
|
@ -1 +1,8 @@
|
|||
import checkForLaineHash from './old_hash_redirector';
|
||||
|
||||
// Check hash even before site is loaded since it doesn't need to
|
||||
// wait for loading
|
||||
checkForLaineHash();
|
||||
|
||||
window.onload = () => {
|
||||
};
|
||||
|
|
|
@ -4,8 +4,8 @@
|
|||
* This is done in JS because the hash is not sent to the server.
|
||||
*/
|
||||
|
||||
(function () {
|
||||
var RE = [
|
||||
function checkForLaineHash() {
|
||||
const RE = [
|
||||
[
|
||||
/^\#\!\/(\d{4})\/(\d\d)\/(\d\d)\/(.*)$/,
|
||||
'/$1/$2/$3/$4'
|
||||
|
@ -40,11 +40,11 @@
|
|||
]
|
||||
];
|
||||
|
||||
var currentHash = window.location.hash;
|
||||
const currentHash = window.location.hash;
|
||||
|
||||
if (currentHash) {
|
||||
for (var i = 0; i < RE.length; ++i) {
|
||||
var results = RE[i][0].exec(currentHash);
|
||||
for (let i = 0; i < RE.length; ++i) {
|
||||
const results = RE[i][0].exec(currentHash);
|
||||
|
||||
if (results !== null) {
|
||||
window.location.replace(currentHash.replace(RE[i][0], RE[i][1]));
|
||||
|
@ -52,4 +52,6 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
}());
|
||||
}
|
||||
|
||||
export default checkForLaineHash;
|
||||
|
|
|
@ -76,7 +76,6 @@
|
|||
|
||||
</div> <!-- /container -->
|
||||
<script src="<%= static_path(@conn, "/js/app.js") %>"></script>
|
||||
<script src="<%= static_path(@conn, "/js/old_hash_redirector.js") %>"></script>
|
||||
|
||||
<%= raw extra_html() %>
|
||||
</body>
|
||||
|
|
Reference in a new issue