* Set up Escript packaging

* Use MD5 digest sa ETAG

* Make sure changes to the static files recompile the relevant module

* Manually start the application in Escript

* Set up basic CLI

* Run formatter

* Start Elixir app before anything else

* Improve version output

* Build Escript to project root directory

* Improve assets handling

* Move plug related modules under plugs directory

* Include bundled assets in the repository

* Use the same plug with different static providers in prod and dev

* Refactor providers

* Rename StaticProvidedPlug to StaticPlug
This commit is contained in:
Jonatan Kłosko 2021-03-17 01:53:44 +01:00 committed by GitHub
parent 7fa2b44666
commit 8b37e32e3a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
50 changed files with 7398 additions and 62 deletions

14
.gitignore vendored
View file

@ -28,10 +28,16 @@ npm-debug.log
# The directory NPM downloads your dependencies sources to.
/assets/node_modules/
# Since we are building assets from assets/,
# we ignore priv/static. You may want to comment
# this depending on your deployment strategy.
/priv/static/
# During development we build assets from assets/
# and they end up in priv/static_dev/, from where they are served.
#
# For deployment we build the assets to priv/static/
# and commit to the reposity, because they have to be accessible
# when building Escript locally.
/priv/static_dev/
# The directory used by ExUnit :tmp_dir
/tmp/
# The built Escript
/livebook

View file

@ -15033,6 +15033,7 @@
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.2.13.tgz",
"integrity": "sha512-oWb1Z6mkHIskLzEJ/XWX0srkpkTQ7vaopMQkyaEIoq0fmtFVxOthb8cCxeT+p3ynTdkk/RZwbgG4brR5BeWECw==",
"dev": true,
"hasInstallScript": true,
"optional": true,
"os": [
"darwin"

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

View file

@ -22,7 +22,7 @@ module.exports = (env, options) => {
},
output: {
filename: '[name].js',
path: path.resolve(__dirname, '../priv/static/js'),
path: path.resolve(__dirname, (devMode ? '../priv/static_dev/js' : '../priv/static/js')),
publicPath: '/js/'
},
devtool: devMode ? 'eval-cheap-module-source-map' : undefined,

View file

@ -1,10 +1,3 @@
# This file is responsible for configuring your application
# and its dependencies with the aid of the Mix.Config module.
#
# This configuration file is loaded before any dependency and
# is restricted to this project.
# General application configuration
import Config
# Configures the endpoint

View file

@ -49,7 +49,7 @@ config :livebook, LivebookWeb.Endpoint,
config :livebook, LivebookWeb.Endpoint,
live_reload: [
patterns: [
~r"priv/static/.*(js|css|png|jpeg|jpg|gif|svg)$",
~r"priv/static_dev/.*(js|css|png|jpeg|jpg|gif|svg)$",
~r"lib/livebook_web/(live|views)/.*(ex)$",
~r"lib/livebook_web/templates/.*(eex)$"
]

View file

@ -3,18 +3,11 @@ import Config
# For production, don't forget to configure the url host
# to something meaningful, Phoenix uses this information
# when generating URLs.
#
# 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 :livebook, LivebookWeb.Endpoint,
url: [host: "example.com", port: 80],
cache_static_manifest: "priv/static/cache_manifest.json"
config :livebook, LivebookWeb.Endpoint, http: [ip: {127, 0, 0, 1}, port: 8080]
# Do not print debug messages in production
config :logger, level: :info
# The output is shown to the end user,
# so limit the amount of information we show.
config :logger, level: :notice
# ## SSL Support
#
@ -49,7 +42,3 @@ config :logger, level: :info
# force_ssl: [hsts: true]
#
# Check `Plug.SSL` for all available options in `force_ssl`.
# Finally import the config/prod.secret.exs which loads secrets
# and configuration from environment variables.
import_config "prod.secret.exs"

View file

@ -1,29 +1,19 @@
import Config
if config_env() == :prod do
secret_key_base =
System.get_env("SECRET_KEY_BASE") ||
raise """
environment variable SECRET_KEY_BASE is missing.
You can generate one by calling: mix phx.gen.secret
"""
# secret_key_base =
# System.get_env("SECRET_KEY_BASE") ||
# raise """
# environment variable SECRET_KEY_BASE is missing.
# You can generate one by calling: mix phx.gen.secret
# """
config :livebook, LivebookWeb.Endpoint,
http: [
# Enable IPv6 and bind on all interfaces.
# Set it to {0, 0, 0, 0, 0, 0, 0, 1} for local network only access.
ip: {0, 0, 0, 0, 0, 0, 0, 0},
port: String.to_integer(System.get_env("PORT") || "4000")
],
secret_key_base: secret_key_base
# ## Using releases (Elixir v1.9+)
#
# If you are doing OTP releases, you need to instruct Phoenix
# to start each relevant endpoint:
#
# config :livebook, LivebookWeb.Endpoint, server: true
#
# Then you can assemble a release by calling `mix release`.
# See `mix help release` for more information.
# config :livebook, LivebookWeb.Endpoint,
# http: [
# # Enable IPv6 and bind on all interfaces.
# # Set it to {0, 0, 0, 0, 0, 0, 0, 1} for local network only access.
# ip: {0, 0, 0, 0, 0, 0, 0, 0},
# port: String.to_integer(System.get_env("PORT") || "4000")
# ],
# secret_key_base: secret_key_base
end

66
lib/litebook_cli.ex Normal file
View file

@ -0,0 +1,66 @@
defmodule LivebookCLI do
@moduledoc false
def main(args) do
{:ok, _} = Application.ensure_all_started(:elixir)
:ok = Application.load(:livebook)
case check_for_shortcuts(args) do
:help ->
IO.puts("Livebook is an interactive notebook system for Elixir\n")
display_usage()
:version ->
display_version()
nil ->
proceed(args)
end
end
defp proceed(args) do
case args do
["server" | _args] ->
start_server()
IO.puts("Livebook running at #{LivebookWeb.Endpoint.url()}")
Process.sleep(:infinity)
_ ->
display_usage()
end
end
defp start_server() do
Application.put_env(:phoenix, :serve_endpoints, true, persistent: true)
{:ok, _} = Application.ensure_all_started(:livebook)
end
# Check for --help or --version in the args
defp check_for_shortcuts([arg]) when arg in ["--help", "-h"], do: :help
defp check_for_shortcuts([arg]) when arg in ["--version", "-v"], do: :version
defp check_for_shortcuts(_), do: nil
defp display_usage() do
IO.write("""
Usage: livebook [command]
Available commands:
livebook server - Starts the Livebook web application
The --help and --version options can be given instead of a command for usage and versioning information.
""")
end
defp display_version do
IO.puts(:erlang.system_info(:system_version))
IO.puts("Elixir " <> System.build_info()[:build])
version = Application.spec(:livebook, :vsn)
IO.puts("\nLivebook #{version}")
end
end

View file

@ -13,15 +13,29 @@ defmodule LivebookWeb.Endpoint do
socket "/live", Phoenix.LiveView.Socket,
websocket: [connect_info: [:user_agent, session: @session_options]]
# Serve at "/" the static files from "priv/static" directory.
#
# You should set gzip to true if you are running phx.digest
# when deploying your static files in production.
plug Plug.Static,
# We use Escript for distributing Livebook, so we don't
# have access to the files in priv/static at runtime in the prod environment.
# To overcome this we load contents of those files at compilation time,
# so that they become a part of the executable and can be served
# from memory.
defmodule AssetsMemoryProvider do
use LivebookWeb.MemoryProvider,
from: :livebook,
gzip: true
end
defmodule AssetsFileSystemProvider do
use LivebookWeb.FileSystemProvider,
from: {:livebook, "priv/static_dev"}
end
file_provider = if(Mix.env() == :prod, do: AssetsMemoryProvider, else: AssetsFileSystemProvider)
# Serve static failes at "/"
plug LivebookWeb.StaticPlug,
at: "/",
from: :livebook,
gzip: false,
only: ~w(css fonts images js favicon.ico robots.txt)
file_provider: file_provider,
gzip: true
# Code reloading can be explicitly enabled under the
# :code_reloader configuration of your endpoint.

View file

@ -0,0 +1,38 @@
defmodule LivebookWeb.FileSystemProvider do
@moduledoc false
# Configurable implementation of `LivebookWeb.StaticPlug.Provider` behaviour,
# that loads files directly from the file system.
#
# ## `use` options
#
# * `:from` (**required**) - where to read the static files from. See `Plug.Static` for more details.
defmacro __using__(opts) do
quote bind_quoted: [opts: opts] do
@behaviour LivebookWeb.StaticPlug.Provider
from = Keyword.fetch!(opts, :from)
static_path = LivebookWeb.StaticPlug.Provider.static_path(from)
@impl true
def get_file(segments, compression) do
LivebookWeb.FileSystemProvider.__get_file__(unquote(static_path), segments, compression)
end
end
end
def __get_file__(static_path, segments, nil) do
abs_path = Path.join([static_path | segments])
if File.regular?(abs_path) do
content = File.read!(abs_path)
digest = content |> :erlang.md5() |> Base.encode16(case: :lower)
%LivebookWeb.StaticPlug.File{content: content, digest: digest}
else
nil
end
end
def __get_file__(_static_path, _segments, _compression), do: nil
end

View file

@ -0,0 +1,74 @@
defmodule LivebookWeb.MemoryProvider do
@moduledoc false
@gzippable_exts ~w(.js .css .txt .text .html .json .svg .eot .ttf)
# Configurable implementation of `LivebookWeb.StaticPlug.Provider` behaviour,
# that bundles the files into the module compiled source.
#
# ## `use` options
#
# * `:from` (**required**) - where to read the static files from. See `Plug.Static` for more details.
#
# * `:gzip` - whether to bundle gzipped version of the files,
# in which case the uncompressed files are not included. Defaults to `false`.
defmacro __using__(opts) do
quote bind_quoted: [opts: opts] do
@behaviour LivebookWeb.StaticPlug.Provider
from = Keyword.fetch!(opts, :from)
static_path = LivebookWeb.StaticPlug.Provider.static_path(from)
paths = LivebookWeb.MemoryProvider.__paths__(static_path)
files = LivebookWeb.MemoryProvider.__preload_files__!(static_path, paths, opts)
for path <- paths do
abs_path = Path.join(static_path, path)
@external_resource Path.relative_to_cwd(abs_path)
end
@impl true
def get_file(segments, compression)
for {segments, compression, file} <- files do
def get_file(unquote(segments), unquote(compression)), do: unquote(Macro.escape(file))
end
def get_file(_, _), do: nil
# Force recompilation if the static files change.
def __mix_recompile__? do
current_paths = LivebookWeb.MemoryProvider.__paths__(unquote(static_path))
:erlang.md5(current_paths) != unquote(:erlang.md5(paths))
end
end
end
def __preload_files__!(static_path, paths, opts) do
gzip? = Keyword.get(opts, :gzip, false)
Enum.map(paths, fn path ->
segments = Path.split(path)
abs_path = Path.join(static_path, path)
content = File.read!(abs_path)
digest = content |> :erlang.md5() |> Base.encode16(case: :lower)
if gzip? and Path.extname(path) in @gzippable_exts do
gzipped_content = :zlib.gzip(content)
{segments, :gzip,
%LivebookWeb.StaticPlug.File{content: gzipped_content, digest: digest}}
else
{segments, nil, %LivebookWeb.StaticPlug.File{content: content, digest: digest}}
end
end)
end
def __paths__(static_path) do
Path.join(static_path, "**")
|> Path.wildcard()
|> Enum.reject(&File.dir?/1)
|> Enum.map(&String.replace_leading(&1, static_path <> "/", ""))
|> Enum.sort()
end
end

View file

@ -0,0 +1,164 @@
defmodule LivebookWeb.StaticPlug.File do
@moduledoc false
defstruct [:content, :digest]
@type t :: %__MODULE__{content: binary(), digest: String.t()}
end
defmodule LivebookWeb.StaticPlug.Provider do
@moduledoc false
@type segments :: list(String.t())
@type compression :: :gzip | nil
@doc """
Returns file data for the given path (given as list of segments) and compression type.
"""
@callback get_file(segments(), compression()) :: LivebookWeb.StaticPlug.File.t() | nil
@doc """
Parses static files location usually passed as the `:from` option
when configuring provider.
See `Plug.Static` for more details.
"""
@spec static_path({atom(), binary()} | atom() | binary()) :: binary()
def static_path(from)
def static_path({app, path}) when is_atom(app) and is_binary(path) do
Path.join(Application.app_dir(app), path)
end
def static_path(path) when is_binary(path), do: path
def static_path(app) when is_atom(app), do: static_path({app, "priv/static"})
end
defmodule LivebookWeb.StaticPlug do
@moduledoc false
# This is a simplified version of `Plug.Static` meant
# to serve static files using the given provider.
#
# ## Options
#
# * `:file_provider` (**required**) - a module implementing `LivebookWeb.StaticPlug.Provider`
# behaviour, responsible for resolving file requests
#
# * `:at`, `:gzip` - same as `Plug.Static`
@behaviour Plug
import Plug.Conn
@allowed_methods ~w(GET HEAD)
@impl true
def init(opts) do
file_provider = Keyword.fetch!(opts, :file_provider)
%{
file_provider: file_provider,
at: opts |> Keyword.fetch!(:at) |> Plug.Router.Utils.split(),
gzip?: Keyword.get(opts, :gzip, false)
}
end
@impl true
def call(
%Plug.Conn{method: method} = conn,
%{file_provider: file_provider, at: at, gzip?: gzip?} = options
)
when method in @allowed_methods do
segments = subset(at, conn.path_info)
case encoding_with_file(conn, file_provider, segments, gzip?) do
{encoding, file} ->
serve_static(conn, encoding, file, segments, options)
:error ->
conn
end
end
def call(conn, _options) do
conn
end
defp serve_static(conn, content_encoding, file, segments, options) do
case put_cache_header(conn, file) do
{:stale, conn} ->
filename = List.last(segments)
content_type = MIME.from_path(filename)
conn
|> put_resp_header("content-type", content_type)
|> maybe_add_encoding(content_encoding)
|> maybe_add_vary(options)
|> send_resp(200, file.content)
|> halt()
{:fresh, conn} ->
conn
|> maybe_add_vary(options)
|> send_resp(304, "")
|> halt()
end
end
defp maybe_add_encoding(conn, nil), do: conn
defp maybe_add_encoding(conn, ce), do: put_resp_header(conn, "content-encoding", ce)
# If we serve gzip at any moment, we need to set the proper vary
# header regardless of whether we are serving gzip content right now.
# See: http://www.fastly.com/blog/best-practices-for-using-the-vary-header/
defp maybe_add_vary(conn, %{gzip?: true}) do
update_in(conn.resp_headers, &[{"vary", "Accept-Encoding"} | &1])
end
defp maybe_add_vary(conn, _options), do: conn
defp put_cache_header(conn, file) do
etag = etag_for_file(file)
conn =
conn
|> put_resp_header("cache-control", "public")
|> put_resp_header("etag", etag)
if etag in get_req_header(conn, "if-none-match") do
{:fresh, conn}
else
{:stale, conn}
end
end
defp etag_for_file(file) do
<<?", file.digest::binary, ?">>
end
defp encoding_with_file(conn, file_provider, segments, gzip?) do
cond do
file = gzip? and accept_encoding?(conn, "gzip") && file_provider.get_file(segments, :gzip) ->
{"gzip", file}
file = file_provider.get_file(segments, nil) ->
{nil, file}
true ->
:error
end
end
defp accept_encoding?(conn, encoding) do
encoding? = &String.contains?(&1, [encoding, "*"])
Enum.any?(get_req_header(conn, "accept-encoding"), fn accept ->
accept |> Plug.Conn.Utils.list() |> Enum.any?(encoding?)
end)
end
defp subset([h | expected], [h | actual]), do: subset(expected, actual)
defp subset([], actual), do: actual
defp subset(_, _), do: []
end

22
mix.exs
View file

@ -10,7 +10,9 @@ defmodule Livebook.MixProject do
compilers: [:phoenix] ++ Mix.compilers(),
start_permanent: Mix.env() == :prod,
aliases: aliases(),
deps: deps()
deps: deps(),
escript: escript(),
preferred_cli_env: preferred_cli_env()
]
end
@ -54,7 +56,23 @@ defmodule Livebook.MixProject do
# See the documentation for `Mix` for more info on aliases.
defp aliases do
[
setup: ["deps.get", "cmd npm install --prefix assets"]
setup: ["deps.get", "cmd npm install --prefix assets"],
# Update the assets bundle to be committed into the repository
# and also builds the Escript.
build: ["cmd npm run deploy --prefix ./assets", "escript.build"]
]
end
defp escript() do
[
main_module: LivebookCLI,
app: nil
]
end
defp preferred_cli_env() do
[
build: :prod
]
end
end

1
priv/static/css/app.css Normal file

File diff suppressed because one or more lines are too long

BIN
priv/static/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

15
priv/static/js/0.js Normal file

File diff suppressed because one or more lines are too long

Binary file not shown.

1
priv/static/js/1.js Normal file
View file

@ -0,0 +1 @@
(window.webpackJsonp=window.webpackJsonp||[]).push([[1],{330:function(e,t,n){"use strict";n.r(t),n.d(t,"conf",(function(){return s})),n.d(t,"language",(function(){return o}));var s={comments:{blockComment:["\x3c!--","--\x3e"]},brackets:[["{","}"],["[","]"],["(",")"]],autoClosingPairs:[{open:"{",close:"}"},{open:"[",close:"]"},{open:"(",close:")"},{open:"<",close:">",notIn:["string"]}],surroundingPairs:[{open:"(",close:")"},{open:"[",close:"]"},{open:"`",close:"`"}],folding:{markers:{start:new RegExp("^\\s*\x3c!--\\s*#?region\\b.*--\x3e"),end:new RegExp("^\\s*\x3c!--\\s*#?endregion\\b.*--\x3e")}}},o={defaultToken:"",tokenPostfix:".md",control:/[\\`*_\[\]{}()#+\-\.!]/,noncontrol:/[^\\`*_\[\]{}()#+\-\.!]/,escapes:/\\(?:@control)/,jsescapes:/\\(?:[btnfr\\"']|[0-7][0-7]?|[0-3][0-7]{2})/,empty:["area","base","basefont","br","col","frame","hr","img","input","isindex","link","meta","param"],tokenizer:{root:[[/^\s*\|/,"@rematch","@table_header"],[/^(\s{0,3})(#+)((?:[^\\#]|@escapes)+)((?:#+)?)/,["white","keyword","keyword","keyword"]],[/^\s*(=+|\-+)\s*$/,"keyword"],[/^\s*((\*[ ]?)+)\s*$/,"meta.separator"],[/^\s*>+/,"comment"],[/^\s*([\*\-+:]|\d+\.)\s/,"keyword"],[/^(\t|[ ]{4})[^ ].*$/,"string"],[/^\s*~~~\s*((?:\w|[\/\-#])+)?\s*$/,{token:"string",next:"@codeblock"}],[/^\s*```\s*((?:\w|[\/\-#])+).*$/,{token:"string",next:"@codeblockgh",nextEmbedded:"$1"}],[/^\s*```\s*$/,{token:"string",next:"@codeblock"}],{include:"@linecontent"}],table_header:[{include:"@table_common"},[/[^\|]+/,"keyword.table.header"]],table_body:[{include:"@table_common"},{include:"@linecontent"}],table_common:[[/\s*[\-:]+\s*/,{token:"keyword",switchTo:"table_body"}],[/^\s*\|/,"keyword.table.left"],[/^\s*[^\|]/,"@rematch","@pop"],[/^\s*$/,"@rematch","@pop"],[/\|/,{cases:{"@eos":"keyword.table.right","@default":"keyword.table.middle"}}]],codeblock:[[/^\s*~~~\s*$/,{token:"string",next:"@pop"}],[/^\s*```\s*$/,{token:"string",next:"@pop"}],[/.*$/,"variable.source"]],codeblockgh:[[/```\s*$/,{token:"variable.source",next:"@pop",nextEmbedded:"@pop"}],[/[^`]+/,"variable.source"]],linecontent:[[/&\w+;/,"string.escape"],[/@escapes/,"escape"],[/\b__([^\\_]|@escapes|_(?!_))+__\b/,"strong"],[/\*\*([^\\*]|@escapes|\*(?!\*))+\*\*/,"strong"],[/\b_[^_]+_\b/,"emphasis"],[/\*([^\\*]|@escapes)+\*/,"emphasis"],[/`([^\\`]|@escapes)+`/,"variable"],[/\{+[^}]+\}+/,"string.target"],[/(!?\[)((?:[^\]\\]|@escapes)*)(\]\([^\)]+\))/,["string.link","","string.link"]],[/(!?\[)((?:[^\]\\]|@escapes)*)(\])/,"string.link"],{include:"html"}],html:[[/<(\w+)\/>/,"tag"],[/<(\w+)/,{cases:{"@empty":{token:"tag",next:"@tag.$1"},"@default":{token:"tag",next:"@tag.$1"}}}],[/<\/(\w+)\s*>/,{token:"tag"}],[/<!--/,"comment","@comment"]],comment:[[/[^<\-]+/,"comment.content"],[/-->/,"comment","@pop"],[/<!--/,"comment.content.invalid"],[/[<\-]/,"comment.content"]],tag:[[/[ \t\r\n]+/,"white"],[/(type)(\s*=\s*)(")([^"]+)(")/,["attribute.name.html","delimiter.html","string.html",{token:"string.html",switchTo:"@tag.$S2.$4"},"string.html"]],[/(type)(\s*=\s*)(')([^']+)(')/,["attribute.name.html","delimiter.html","string.html",{token:"string.html",switchTo:"@tag.$S2.$4"},"string.html"]],[/(\w+)(\s*=\s*)("[^"]*"|'[^']*')/,["attribute.name.html","delimiter.html","string.html"]],[/\w+/,"attribute.name.html"],[/\/>/,"tag","@pop"],[/>/,{cases:{"$S2==style":{token:"tag",switchTo:"embeddedStyle",nextEmbedded:"text/css"},"$S2==script":{cases:{$S3:{token:"tag",switchTo:"embeddedScript",nextEmbedded:"$S3"},"@default":{token:"tag",switchTo:"embeddedScript",nextEmbedded:"text/javascript"}}},"@default":{token:"tag",next:"@pop"}}}]],embeddedStyle:[[/[^<]+/,""],[/<\/style\s*>/,{token:"@rematch",next:"@pop",nextEmbedded:"@pop"}],[/</,""]],embeddedScript:[[/[^<]+/,""],[/<\/script\s*>/,{token:"@rematch",next:"@pop",nextEmbedded:"@pop"}],[/</,""]]}}}}]);

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

File diff suppressed because it is too large Load diff

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

2
priv/static/js/app.js Normal file

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,13 @@
/* NProgress, (c) 2013, 2014 Rico Sta. Cruz - http://ricostacruz.com/nprogress
* @license MIT */
/*!
* The buffer module from node.js, for the browser.
*
* @author Feross Aboukhadijeh <http://feross.org>
* @license MIT
*/
/*! @license DOMPurify | (c) Cure53 and other contributors | Released under the Apache license 2.0 and Mozilla Public License 2.0 | github.com/cure53/DOMPurify/blob/2.2.2/LICENSE */
/*! ieee754. BSD-3-Clause License. Feross Aboukhadijeh <https://feross.org/opensource> */

Binary file not shown.

Binary file not shown.

Binary file not shown.

File diff suppressed because one or more lines are too long

Binary file not shown.

5
priv/static/robots.txt Normal file
View file

@ -0,0 +1,5 @@
# See http://www.robotstxt.org/robotstxt.html for documentation on how to use the robots.txt file
#
# To ban all spiders from the entire site uncomment the next two lines:
# User-agent: *
# Disallow: /

View file

@ -0,0 +1,21 @@
defmodule LivebookWeb.FileSystemProviderTest do
use ExUnit.Case, async: true
defmodule MyProvider do
use LivebookWeb.FileSystemProvider,
from: Path.expand("../../support/static", __DIR__)
end
test "includes regular files" do
assert %{content: content} = MyProvider.get_file(["js", "app.js"], nil)
assert content == ~s{console.log("Hello");\n}
end
test "ignores directories" do
assert nil == MyProvider.get_file(["js"], nil)
end
test "ignores non-existent files" do
assert nil == MyProvider.get_file(["nonexistent.js"], nil)
end
end

View file

@ -0,0 +1,22 @@
defmodule LivebookWeb.MemoryProviderTest do
use ExUnit.Case, async: true
defmodule MyProvider do
use LivebookWeb.MemoryProvider,
from: Path.expand("../../support/static", __DIR__),
gzip: true
end
test "includes uncompressed files that are not gzippable" do
assert %{content: ""} = MyProvider.get_file(["icon.ico"], nil)
end
test "includes compressed files which are gzippable" do
assert %{content: content} = MyProvider.get_file(["js", "app.js"], :gzip)
assert :zlib.gunzip(content) == ~s{console.log("Hello");\n}
end
test "does not include uncompressed files that are gzippable" do
assert nil == MyProvider.get_file(["js", "app.js"], nil)
end
end

View file

@ -0,0 +1,66 @@
defmodule LivebookWeb.StaticPlugTest do
use ExUnit.Case, async: true
use Plug.Test
defmodule MyProvider do
@behaviour LivebookWeb.StaticPlug.Provider
@impl true
def get_file(["app.js"], :gzip) do
%LivebookWeb.StaticPlug.File{content: "content", digest: "digest"}
end
def get_file(["icon.ico"], nil) do
%LivebookWeb.StaticPlug.File{content: "content", digest: "digest"}
end
def get_file(_path, _compression), do: nil
end
defmodule MyPlug do
use Plug.Builder
plug LivebookWeb.StaticPlug,
at: "/",
file_provider: MyProvider,
gzip: true
plug :passthrough
defp passthrough(conn, _), do: Plug.Conn.send_resp(conn, 404, "Passthrough")
end
defp call(conn), do: MyPlug.call(conn, [])
test "serves uncompressed file if there is no compressed version" do
conn =
conn(:get, "/icon.ico")
|> put_req_header("accept-encoding", "gzip")
|> call()
assert conn.status == 200
assert conn.resp_body == "content"
assert get_resp_header(conn, "content-type") == ["image/vnd.microsoft.icon"]
assert get_resp_header(conn, "etag") == [~s{"digest"}]
end
test "serves the compressed file if available" do
conn =
conn(:get, "/app.js")
|> put_req_header("accept-encoding", "gzip")
|> call()
assert conn.status == 200
assert get_resp_header(conn, "content-encoding") == ["gzip"]
assert get_resp_header(conn, "content-type") == ["application/javascript"]
assert get_resp_header(conn, "etag") == [~s{"digest"}]
end
test "ignores unavailable paths" do
conn =
conn(:get, "/invalid.js")
|> call()
assert conn.status == 404
end
end

View file

View file

@ -0,0 +1 @@
console.log("Hello");