diff --git a/.babelrc b/.babelrc
new file mode 100644
index 0000000..e77bf0b
--- /dev/null
+++ b/.babelrc
@@ -0,0 +1,7 @@
+{
+ "presets": [
+ ["es2015", {"modules": false}],
+ "es2016",
+ "es2017"
+ ]
+}
diff --git a/.gitignore b/.gitignore
index 81fba74..bdcd46f 100644
--- a/.gitignore
+++ b/.gitignore
@@ -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/
-
diff --git a/README.md b/README.md
index a892a70..75445ef 100644
--- a/README.md
+++ b/README.md
@@ -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.
diff --git a/gulpfile.js b/gulpfile.js
deleted file mode 100644
index 0afee07..0000000
--- a/gulpfile.js
+++ /dev/null
@@ -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));
-});
diff --git a/lib/mebe_web/endpoint.ex b/lib/mebe_web/endpoint.ex
index 81838b4..44af77e 100644
--- a/lib/mebe_web/endpoint.ex
+++ b/lib/mebe_web/endpoint.ex
@@ -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
diff --git a/lib/mix/tasks/apprunner.exs b/lib/mix/tasks/apprunner.exs
new file mode 100644
index 0000000..12d2a7e
--- /dev/null
+++ b/lib/mix/tasks/apprunner.exs
@@ -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)
diff --git a/lib/mix/tasks/frontend.build.assets.ex b/lib/mix/tasks/frontend.build.assets.ex
new file mode 100644
index 0000000..172d444
--- /dev/null
+++ b/lib/mix/tasks/frontend.build.assets.ex
@@ -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
diff --git a/lib/mix/tasks/frontend.build.css.compile.ex b/lib/mix/tasks/frontend.build.css.compile.ex
new file mode 100644
index 0000000..3fe3a79
--- /dev/null
+++ b/lib/mix/tasks/frontend.build.css.compile.ex
@@ -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
diff --git a/lib/mix/tasks/frontend.build.css.ex b/lib/mix/tasks/frontend.build.css.ex
new file mode 100644
index 0000000..9af60fd
--- /dev/null
+++ b/lib/mix/tasks/frontend.build.css.ex
@@ -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
diff --git a/lib/mix/tasks/frontend.build.css.minify.ex b/lib/mix/tasks/frontend.build.css.minify.ex
new file mode 100644
index 0000000..40dc217
--- /dev/null
+++ b/lib/mix/tasks/frontend.build.css.minify.ex
@@ -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
diff --git a/lib/mix/tasks/frontend.build.ex b/lib/mix/tasks/frontend.build.ex
new file mode 100644
index 0000000..4fe3909
--- /dev/null
+++ b/lib/mix/tasks/frontend.build.ex
@@ -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
diff --git a/lib/mix/tasks/frontend.build.js.bundle.ex b/lib/mix/tasks/frontend.build.js.bundle.ex
new file mode 100644
index 0000000..43d4dec
--- /dev/null
+++ b/lib/mix/tasks/frontend.build.js.bundle.ex
@@ -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
diff --git a/lib/mix/tasks/frontend.build.js.ex b/lib/mix/tasks/frontend.build.js.ex
new file mode 100644
index 0000000..9599785
--- /dev/null
+++ b/lib/mix/tasks/frontend.build.js.ex
@@ -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
diff --git a/lib/mix/tasks/frontend.build.js.minify.ex b/lib/mix/tasks/frontend.build.js.minify.ex
new file mode 100644
index 0000000..0832174
--- /dev/null
+++ b/lib/mix/tasks/frontend.build.js.minify.ex
@@ -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
diff --git a/lib/mix/tasks/frontend.build.js.transpile.ex b/lib/mix/tasks/frontend.build.js.transpile.ex
new file mode 100644
index 0000000..811ea8f
--- /dev/null
+++ b/lib/mix/tasks/frontend.build.js.transpile.ex
@@ -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
diff --git a/lib/mix/tasks/frontend.clean.ex b/lib/mix/tasks/frontend.clean.ex
new file mode 100644
index 0000000..ba3b0f0
--- /dev/null
+++ b/lib/mix/tasks/frontend.clean.ex
@@ -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
diff --git a/lib/mix/tasks/frontend.watch.ex b/lib/mix/tasks/frontend.watch.ex
new file mode 100644
index 0000000..dcfdac8
--- /dev/null
+++ b/lib/mix/tasks/frontend.watch.ex
@@ -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
diff --git a/lib/mix/tasks/frontend_confs.ex b/lib/mix/tasks/frontend_confs.ex
new file mode 100644
index 0000000..6a3902f
--- /dev/null
+++ b/lib/mix/tasks/frontend_confs.ex
@@ -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
diff --git a/lib/mix/tasks/utils.ex b/lib/mix/tasks/utils.ex
new file mode 100644
index 0000000..d9c4455
--- /dev/null
+++ b/lib/mix/tasks/utils.ex
@@ -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
diff --git a/package.json b/package.json
index 6835b59..aa24b15 100644
--- a/package.json
+++ b/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"
}
}
diff --git a/rollup.config.js b/rollup.config.js
new file mode 100644
index 0000000..3f32c89
--- /dev/null
+++ b/rollup.config.js
@@ -0,0 +1,7 @@
+import sourcemaps from 'rollup-plugin-sourcemaps';
+
+export default {
+ plugins: [
+ sourcemaps()
+ ]
+};
diff --git a/web/static/js/app.js b/web/static/js/app.js
index 8b13789..37d1035 100644
--- a/web/static/js/app.js
+++ b/web/static/js/app.js
@@ -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 = () => {
+};
diff --git a/web/static/js/old_hash_redirector.js b/web/static/js/old_hash_redirector.js
index d50e0f3..f314f6b 100644
--- a/web/static/js/old_hash_redirector.js
+++ b/web/static/js/old_hash_redirector.js
@@ -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;
diff --git a/web/templates/layout/app.html.eex b/web/templates/layout/app.html.eex
index b248ddc..12a003b 100644
--- a/web/templates/layout/app.html.eex
+++ b/web/templates/layout/app.html.eex
@@ -76,7 +76,6 @@
-
<%= raw extra_html() %>