mirror of
https://github.com/livebook-dev/livebook.git
synced 2024-09-20 10:05:57 +08:00
Escript (#77)
* 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:
parent
7fa2b44666
commit
8b37e32e3a
14
.gitignore
vendored
14
.gitignore
vendored
|
@ -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
|
||||
|
|
1
assets/package-lock.json
generated
1
assets/package-lock.json
generated
|
@ -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 |
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)$"
|
||||
]
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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
66
lib/litebook_cli.ex
Normal 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
|
|
@ -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.
|
||||
|
|
38
lib/livebook_web/plugs/file_system_provider.ex
Normal file
38
lib/livebook_web/plugs/file_system_provider.ex
Normal 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
|
74
lib/livebook_web/plugs/memory_provider.ex
Normal file
74
lib/livebook_web/plugs/memory_provider.ex
Normal 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
|
164
lib/livebook_web/plugs/static_plug.ex
Normal file
164
lib/livebook_web/plugs/static_plug.ex
Normal 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
22
mix.exs
|
@ -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
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
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
15
priv/static/js/0.js
Normal file
File diff suppressed because one or more lines are too long
BIN
priv/static/js/016b603e010d5dcae8ea8fa4aa580228.woff
Normal file
BIN
priv/static/js/016b603e010d5dcae8ea8fa4aa580228.woff
Normal file
Binary file not shown.
1
priv/static/js/1.js
Normal file
1
priv/static/js/1.js
Normal 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"}],[/</,""]]}}}}]);
|
BIN
priv/static/js/10f84849b8a69b4844b2925080f81a97.woff
Normal file
BIN
priv/static/js/10f84849b8a69b4844b2925080f81a97.woff
Normal file
Binary file not shown.
BIN
priv/static/js/1b95ebd96a4c3e90b1b08bd187f056b0.woff2
Normal file
BIN
priv/static/js/1b95ebd96a4c3e90b1b08bd187f056b0.woff2
Normal file
Binary file not shown.
BIN
priv/static/js/29df4aa934ecc94620708eea295782a5.woff2
Normal file
BIN
priv/static/js/29df4aa934ecc94620708eea295782a5.woff2
Normal file
Binary file not shown.
BIN
priv/static/js/3164eabdbb931859ae7ca8ca62a3c332.woff2
Normal file
BIN
priv/static/js/3164eabdbb931859ae7ca8ca62a3c332.woff2
Normal file
Binary file not shown.
BIN
priv/static/js/47bb5e1a429a97dcc3b35e434386f912.woff2
Normal file
BIN
priv/static/js/47bb5e1a429a97dcc3b35e434386f912.woff2
Normal file
Binary file not shown.
BIN
priv/static/js/4cdaaa8fe839542200b080dc9b08b7b5.woff2
Normal file
BIN
priv/static/js/4cdaaa8fe839542200b080dc9b08b7b5.woff2
Normal file
Binary file not shown.
BIN
priv/static/js/5431f9f71fc665274a10721c239cd333.woff
Normal file
BIN
priv/static/js/5431f9f71fc665274a10721c239cd333.woff
Normal file
Binary file not shown.
BIN
priv/static/js/54de81abbcf869efd1dc2b595a68fe7e.woff2
Normal file
BIN
priv/static/js/54de81abbcf869efd1dc2b595a68fe7e.woff2
Normal file
Binary file not shown.
BIN
priv/static/js/5cb99e6cba5a4619063f73d47b775f6f.eot
Normal file
BIN
priv/static/js/5cb99e6cba5a4619063f73d47b775f6f.eot
Normal file
Binary file not shown.
BIN
priv/static/js/663c2351f9f20696b3a4cee9f20832f3.woff2
Normal file
BIN
priv/static/js/663c2351f9f20696b3a4cee9f20832f3.woff2
Normal file
Binary file not shown.
BIN
priv/static/js/75a1d3c8eb035b420ce68a0c44757916.woff2
Normal file
BIN
priv/static/js/75a1d3c8eb035b420ce68a0c44757916.woff2
Normal file
Binary file not shown.
BIN
priv/static/js/8d09fa11700ed63cf96e1d1c038368f3.woff
Normal file
BIN
priv/static/js/8d09fa11700ed63cf96e1d1c038368f3.woff
Normal file
Binary file not shown.
BIN
priv/static/js/8e08cd86133caec43c49a139a36424e8.woff
Normal file
BIN
priv/static/js/8e08cd86133caec43c49a139a36424e8.woff
Normal file
Binary file not shown.
BIN
priv/static/js/90668f6f9b3c2c18a090f132d1793c67.woff2
Normal file
BIN
priv/static/js/90668f6f9b3c2c18a090f132d1793c67.woff2
Normal file
Binary file not shown.
BIN
priv/static/js/913520c3f0ae87c1d9aa23fc52b8c0b0.woff2
Normal file
BIN
priv/static/js/913520c3f0ae87c1d9aa23fc52b8c0b0.woff2
Normal file
Binary file not shown.
BIN
priv/static/js/953c8aeb6c2394cec010f8daa27c3fa2.woff2
Normal file
BIN
priv/static/js/953c8aeb6c2394cec010f8daa27c3fa2.woff2
Normal file
Binary file not shown.
6835
priv/static/js/9cedd2150922ead848695530d71a212f.svg
Normal file
6835
priv/static/js/9cedd2150922ead848695530d71a212f.svg
Normal file
File diff suppressed because it is too large
Load diff
After Width: | Height: | Size: 1.1 MiB |
BIN
priv/static/js/a05a0d687a088c4efed303a504befad7.woff2
Normal file
BIN
priv/static/js/a05a0d687a088c4efed303a504befad7.woff2
Normal file
Binary file not shown.
2
priv/static/js/app.js
Normal file
2
priv/static/js/app.js
Normal file
File diff suppressed because one or more lines are too long
13
priv/static/js/app.js.LICENSE.txt
Normal file
13
priv/static/js/app.js.LICENSE.txt
Normal 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> */
|
BIN
priv/static/js/b3726f0165bf67ac68494ee7a1b9f6ce.ttf
Normal file
BIN
priv/static/js/b3726f0165bf67ac68494ee7a1b9f6ce.ttf
Normal file
Binary file not shown.
BIN
priv/static/js/c15997f1d022aff0bd008085732b8d87.woff2
Normal file
BIN
priv/static/js/c15997f1d022aff0bd008085732b8d87.woff2
Normal file
Binary file not shown.
BIN
priv/static/js/c2a24f65668ba8f3fdf78ce8e467889e.woff2
Normal file
BIN
priv/static/js/c2a24f65668ba8f3fdf78ce8e467889e.woff2
Normal file
Binary file not shown.
1
priv/static/js/editor.worker.js
Normal file
1
priv/static/js/editor.worker.js
Normal file
File diff suppressed because one or more lines are too long
BIN
priv/static/js/f2616f597cf98f38d2347c9648bfe049.ttf
Normal file
BIN
priv/static/js/f2616f597cf98f38d2347c9648bfe049.ttf
Normal file
Binary file not shown.
5
priv/static/robots.txt
Normal file
5
priv/static/robots.txt
Normal 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: /
|
21
test/livebook_web/plugs/file_system_provider_test.exs
Normal file
21
test/livebook_web/plugs/file_system_provider_test.exs
Normal 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
|
22
test/livebook_web/plugs/memory_provider_test.exs
Normal file
22
test/livebook_web/plugs/memory_provider_test.exs
Normal 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
|
66
test/livebook_web/plugs/static_plug_test.exs
Normal file
66
test/livebook_web/plugs/static_plug_test.exs
Normal 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
|
0
test/support/static/icon.ico
Normal file
0
test/support/static/icon.ico
Normal file
1
test/support/static/js/app.js
Normal file
1
test/support/static/js/app.js
Normal file
|
@ -0,0 +1 @@
|
|||
console.log("Hello");
|
Loading…
Reference in a new issue