This commit is contained in:
Wout De Puysseleir 2023-02-19 17:46:05 -08:00
commit 9102bcfe2f
No known key found for this signature in database
GPG key ID: 3DE9371B50FEC46A
23 changed files with 755 additions and 0 deletions

4
.formatter.exs Normal file
View file

@ -0,0 +1,4 @@
# Used by "mix format"
[
inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"]
]

29
.gitignore vendored Normal file
View file

@ -0,0 +1,29 @@
# The directory Mix will write compiled artifacts to.
/_build/
# If you run "mix test --cover", coverage assets end up here.
/cover/
# The directory Mix downloads your dependencies sources to.
/deps/
# Where third-party dependencies like ExDoc output generated docs.
/doc/
# Ignore .fetch files in case you like to edit your project deps locally.
/.fetch
# If the VM crashes, it generates a dump, let's ignore it too.
erl_crash.dump
# Also ignore archive artifacts (built via "mix archive.build").
*.ez
# Ignore package tarball (built via "mix hex.build").
live_svelte-*.tar
# Temporary files, for example, from tests.
/tmp/
# NPM dependencies
node_module/

9
LICENSE.MD Normal file
View file

@ -0,0 +1,9 @@
# MIT License
Copyright (c) 2023 Wout De Puysseleir
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

110
README.md Normal file
View file

@ -0,0 +1,110 @@
# LiveSvelte
LiveSvelte renders Svelte directly into your Phoenix LiveView and enables E2E reactivity.
## Features
- Server-Side Rendered (SSR) Svelte
- End-To-End Reactivity
- Svelte Preprocessing support with [svelte-preprocess](https://github.com/sveltejs/svelte-preprocess)
- Tailwind support
## Docs
<https://hexdocs.pm/live_svelte>
## Installation
Add `live_svelte` to your list of dependencies in `mix.exs`:
```elixir
def deps do
[
{:live_svelte, "~> 0.1.0-rc0"}
]
end
```
Run the following in your terminal
```bash
mix deps.get
mix live_svelte.setup
```
Make sure you have Node installed, you can verify this by running `node --version` in your project directory.
## Usage
### Basic Example
#### Create a Svelte component
```svelte
<script>
export let number = 1
export let pushEvent
function increase() {
pushEvent('set_number', { number: number + 1 }, () => {})
}
function decrease() {
pushEvent('set_number', { number: number - 1 }, () => {})
}
</script>
<p>The number is {number}</p>
<button on:click={increase}>+</button>
<button on:click={decrease}>-</button>
```
#### Create a LiveView
```elixir
# `/lib/app_web/live/live_svelte.ex`
defmodule AppWeb.SvelteLive do
use AppWeb, :live_view
def render(assigns) do
~H"""
<.live_component
module={LiveSvelte.LiveComponent}
id="Example"
name="Example"
props={%{number: @number}}
/>
"""
end
def handle_event("set_number", %{"number" => number}, socket) do
{:noreply, assign(socket, :number, number)}
end
def mount(_params, _session, socket) do
{:ok, assign(socket, :number, 5)}
end
end
```
```elixir
# `/lib/app_web/router.ex`
import Phoenix.LiveView.Router
scope "/", AppWeb do
...
live "/svelte", SvelteLive
...
end
```
###
To use the preprocessor, install the desired preprocessor.
e.g. Typescript
```
cd assets && npm install --save-dev typescript
```
## Credits
- [Ryan Cooke](https://dev.to/debussyman) - [E2E Reactivity using Svelte with Phoenix LiveView](https://dev.to/debussyman/e2e-reactivity-using-svelte-with-phoenix-liveview-38mf)
- [Svonix](https://github.com/nikokozak/svonix)

91
assets/build.js Normal file
View file

@ -0,0 +1,91 @@
const esbuild = require('esbuild')
const sveltePlugin = require('esbuild-svelte')
const importGlobPlugin = require('esbuild-plugin-import-glob').default
const sveltePreprocess = require('svelte-preprocess')
const args = process.argv.slice(2)
const watch = args.includes('--watch')
const deploy = args.includes('--deploy')
let optsClient = {
entryPoints: ['js/app.js'],
mainFields: ['svelte', 'browser', 'module', 'main'],
bundle: true,
minify: false,
target: 'es2017',
outdir: '../priv/static/assets',
logLevel: 'info',
plugins: [
importGlobPlugin(),
sveltePlugin({
preprocess: sveltePreprocess(),
compilerOptions: {hydratable: true},
})
]
}
let optsServer = {
entryPoints: ['js/server.js'],
mainFields: ['svelte', 'module', 'main'],
platform: 'node',
format: 'cjs',
bundle: true,
minify: false,
target: "node19.6.1",
outdir: '../priv/static/assets/server',
logLevel: 'info',
plugins: [
importGlobPlugin(),
sveltePlugin({
preprocess: sveltePreprocess(),
compilerOptions: {hydratable: true, generate: 'ssr', format: 'cjs'},
})
]
}
if (watch) {
optsClient = {
...optsClient,
watch,
sourcemap: 'inline'
}
optsServer = {
...optsServer,
watch,
sourcemap: 'inline'
}
}
if (deploy) {
optsClient = {
...optsClient,
minify: true
}
optsServer = {
...optsServer,
minify: true
}
}
const client = esbuild.build(optsClient)
const server = esbuild.build(optsServer)
if (watch) {
client.then(_result => {
process.stdin.on('close', () => {
process.exit(0)
})
process.stdin.resume()
})
server.then(_result => {
process.stdin.on('close', () => {
process.exit(0)
})
process.stdin.resume()
})
}

42
assets/js/app.js Normal file
View file

@ -0,0 +1,42 @@
// If you want to use Phoenix channels, run `mix help phx.gen.channel`
// to get started and then uncomment the line below.
// import "./user_socket.js"
// You can include dependencies in two ways.
//
// The simplest option is to put them in assets/vendor and
// import them using relative paths:
//
// import "../vendor/some-package.js"
//
// Alternatively, you can `npm install some-package --prefix assets` and import
// them using a path starting with the package name:
//
// import "some-package"
//
// Include phoenix_html to handle method=PUT/DELETE in forms and buttons.
import "phoenix_html"
// Establish Phoenix Socket and LiveView configuration.
import {Socket} from "phoenix"
import {LiveSocket} from "phoenix_live_view"
import topbar from "../vendor/topbar"
import hooks from './hooks'
let csrfToken = document.querySelector("meta[name='csrf-token']").getAttribute("content")
let liveSocket = new LiveSocket("/live", Socket, {hooks, params: {_csrf_token: csrfToken}})
// Show progress bar on live navigation and form submits
topbar.config({barColors: {0: "#29d"}, shadowColor: "rgba(0, 0, 0, .3)"})
window.addEventListener("phx:page-loading-start", _info => topbar.show(300))
window.addEventListener("phx:page-loading-stop", _info => topbar.hide())
// connect if there are any LiveViews on the page
liveSocket.connect()
// expose liveSocket on window for web console debug logs and latency simulation:
// >> liveSocket.enableDebug()
// >> liveSocket.enableLatencySim(1000) // enabled for duration of browser session
// >> liveSocket.disableLatencySim()
window.liveSocket = liveSocket

62
assets/js/hooks.js Normal file
View file

@ -0,0 +1,62 @@
import * as Components from '../svelte/components/**/*'
let { default: modules, filenames } = Components
filenames = filenames
.map(name => name.replace('../svelte/components/', ''))
.map(name => name.replace('.svelte', ''))
components = Object.assign({}, ...modules.map((m, index) => ({[filenames[index]]: m.default})))
function parsedProps(el) {
const props = el.getAttribute('data-props')
return props ? JSON.parse(props) : {}
}
const SvelteComponent = {
mounted() {
const componentName = this.el.getAttribute('data-name')
if (!componentName) {
throw new Error('Component name must be provided')
}
const requiredApp = components[componentName]
if (!requiredApp) {
throw new Error(`Unable to find ${componentName} component. Did you forget to import it into hooks.js?`)
}
const pushEvent = (event, data, callback) => {
this.pushEvent(event, data, callback)
}
const goto = href => {
liveSocket.pushHistoryPatch(href, 'push', this.el)
}
this._instance = new requiredApp({
target: this.el,
props: {...parsedProps(this.el), pushEvent, goto},
hydrate: true
})
},
updated() {
const pushEvent = (event, data, callback) => {
this.pushEvent(event, data, callback)
}
const goto = href => {
liveSocket.pushHistoryPatch(href, 'push', this.el)
}
this._instance.$$set({...parsedProps(this.el), pushEvent, goto})
},
destroyed() {
this._instance?.$destroy()
}
}
export default {
SvelteComponent
}

9
assets/js/server.js Normal file
View file

@ -0,0 +1,9 @@
import * as Components from '../svelte/components/**/*'
let { default: modules, filenames } = Components
filenames = filenames
.map(name => name.replace('../svelte/components/', ''))
.map(name => name.replace('.svelte', ''))
module.exports = Object.assign({}, ...modules.map((m, index) => ({[filenames[index]]: m.default})))

View file

@ -0,0 +1,18 @@
<script>
export let number = 1;
export let pushEvent;
function increase() {
pushEvent('set_number', { number: number + 1 })
}
function decrease() {
pushEvent('set_number', { number: number - 1 })
}
</script>
<h1 class="text-red">Component is working</h1>
<p>The number is {number}</p>
<button on:click={increase}>+</button>
<button on:click={decrease}>-</button>

4
assets/svelte/render.js Normal file
View file

@ -0,0 +1,4 @@
module.exports.render = (name, props={}) => {
const ssrComponent = require('../../priv/static/assets/server/server.js')[name].default
return ssrComponent.render(props)
}

27
assets/tailwind.config.js Normal file
View file

@ -0,0 +1,27 @@
// See the Tailwind configuration guide for advanced usage
// https://tailwindcss.com/docs/configuration
const plugin = require("tailwindcss/plugin")
module.exports = {
content: [
"./js/**/*.js",
"../lib/*_web.ex",
"../lib/*_web/**/*.*ex",
"./svelte/**/*.svelte"
],
theme: {
extend: {
colors: {
brand: "#FD4F00",
}
},
},
plugins: [
require("@tailwindcss/forms"),
plugin(({addVariant}) => addVariant("phx-no-feedback", [".phx-no-feedback&", ".phx-no-feedback &"])),
plugin(({addVariant}) => addVariant("phx-click-loading", [".phx-click-loading&", ".phx-click-loading &"])),
plugin(({addVariant}) => addVariant("phx-submit-loading", [".phx-submit-loading&", ".phx-submit-loading &"])),
plugin(({addVariant}) => addVariant("phx-change-loading", [".phx-change-loading&", ".phx-change-loading &"]))
]
}

37
lib/component.ex Normal file
View file

@ -0,0 +1,37 @@
defmodule LiveSvelte do
use Phoenix.Component
attr(:props, :map, default: %{})
attr(:name, :string)
def render(%{name: name, props: props, __changed__: changed} = assigns) do
assigns |> IO.inspect()
assigns =
assigns
|> assign(:ssr_render, ssr_render(name, props, changed))
|> assign(:id, id(assigns.name))
~H"""
<.live_component
module={LiveSvelte.LiveComponent}
id={@id}
name={@name}
props={@props}
ssr_render={@ssr_render}
/>
"""
end
def ssr_render(name, props, nil) do
"SSR RENDERING" |> IO.inspect()
NodeJS.call!({"svelte/render", "render"}, [name, props])
end
def ssr_render(_name, _props, _changed) do
"NOT SSR RENDERING" |> IO.inspect()
%{"html" => "", "css" => %{"code" => ""}, "head" => ""}
end
defp id(name), do: "#{name}-#{System.unique_integer([:positive])}"
end

57
lib/live_component.ex Normal file
View file

@ -0,0 +1,57 @@
defmodule LiveSvelte.LiveComponent do
use Phoenix.LiveComponent
import Phoenix.HTML
attr(:props, :map, default: %{})
attr(:name, :string)
attr(:rendered, :boolean, default: false)
def render(assigns) do
~H"""
<div>
<%!-- TODO: This can return things like <title> which should be in the head --%>
<%!-- <script><%= raw(@ssr_render["head"]) %></script> --%>
<style><%= raw(@ssr_render["css"]["code"]) %></style>
<div
id={id(@name)}
data-name={@name}
data-props={json(@props)}
phx-update="ignore"
phx-hook="SvelteComponent"
>
<%= raw(@ssr_render["html"]) %>
</div>
</div>
"""
end
def update(assigns, socket) do
# Maybe something like this?
# socket = if not Map.get(assigns, :rendered, false) do
# send self(), {:rendered, true}
# end
socket =
socket
|> assign(assigns)
# TODO: Only render once
|> assign(:ssr_render, ssr_render(assigns.name, assigns.props))
{:ok, socket}
end
def ssr_render(name, props) do
NodeJS.call!({"svelte/render", "render"}, [name, props])
end
defp json(props) do
props
|> Jason.encode()
|> case do
{:ok, encoded} -> encoded
{:error, _} -> ""
end
end
defp id(name), do: "#{name}-#{System.unique_integer([:positive])}"
end

6
lib/logger.ex Normal file
View file

@ -0,0 +1,6 @@
defmodule LiveSvelte.Logger do
def log_info(status), do: Mix.shell().info([status, :reset])
def log_success(status), do: Mix.shell().info([:green, status, :reset])
def log_warning(status), do: Mix.shell().info([:yellow, status, :reset])
def log_error(status), do: Mix.shell().error([status, :reset])
end

View file

@ -0,0 +1,25 @@
defmodule Mix.Tasks.LiveSvelte.ConfigureEsbuild do
import LiveSvelte.Logger
def run(_) do
log_info("-- Configuring esbuild...")
Mix.Project.deps_paths(depth: 1)
|> Map.fetch!(:live_svelte)
|> Path.join("assets/**/*{.svelte,.js}")
|> Path.wildcard()
|> Enum.each(fn file ->
split = Path.split(file)
assets_index = Enum.find_index(split, fn item -> item == "assets" end)
path =
split
|> Stream.with_index()
|> Stream.reject(fn {_item, i} -> assets_index > i end)
|> Enum.map(&elem(&1, 0))
|> Path.join()
Mix.Generator.copy_file(file, path)
end)
end
end

View file

@ -0,0 +1,87 @@
defmodule Mix.Tasks.LiveSvelte.ConfigurePhoenix do
import LiveSvelte.Logger
@watcher_regex ~r/watchers:\s\[(?!\s+node:)/
@esbuild_regex ~r/(?<!# )esbuild: {.*}/
@nodejs_regex ~r/children\s+=\s+\[(?!\s+\{NodeJS)/
def run(_) do
log_info("-- Configuring Phoenix...")
try do
configure_config()
configure_application()
rescue
err -> log_error(err.message)
end
Mix.Task.run("format")
end
defp configure_config() do
config_file = "dev.exs"
config_path = find_file("config/#{config_file}", config_file)
watcher = ~s"""
node: ["build.js", "--watch", cd: Path.expand("../assets", __DIR__)],\
"""
File.read!(config_path)
|> insert(@watcher_regex, watcher, "'#{watcher}' in #{config_file}")
|> comment(@esbuild_regex, "old esbuild watcher in #{config_file}")
|> save(config_path)
end
defp configure_application() do
application_file = "application.ex"
application_path = find_file("lib/**/#{application_file}", application_file)
nodeSupervisor = ~s"""
{NodeJS.Supervisor, [path: "./assets", pool_size: 4]},\
"""
File.read!(application_path)
|> insert(@nodejs_regex, nodeSupervisor, "'#{nodeSupervisor}' in #{application_file}")
|> save(application_path)
end
defp find_file(wildcard, file_name) do
with [path] <- Path.wildcard(wildcard) do
path
else
[] -> raise "Could not find #{file_name}"
[_ | _] -> raise "Found multiple #{file_name} files"
end
end
defp insert(source, regex, to_insert, name) do
case Regex.run(regex, source, return: :index) do
[{pos, len}] ->
log_success("Inserted #{name}")
insert_position(source, pos + len, to_insert)
nil ->
log_error("Could not insert #{name}, please do so yourself")
source
end
end
defp comment(source, regex, name) do
case Regex.run(regex, source, return: :index) do
[{pos, _len}] ->
log_success("Commented out #{name}")
insert_position(source, pos, "# ")
nil ->
log_warning("Could not comment out #{name}")
source
end
end
defp insert_position(source, position, to_insert) do
{head, tail} = String.split_at(source, position)
head <> to_insert <> tail
end
defp save(source, target_file), do: File.write!(target_file, source)
end

View file

@ -0,0 +1,14 @@
defmodule Mix.Tasks.LiveSvelte.InstallNpmDeps do
import LiveSvelte.Logger
def run(_) do
log_info("-- Installing npm dependencies...")
"cd assets &&
npm install --save-dev esbuild@^0.16.17 esbuild-svelte svelte svelte-preprocess esbuild-plugin-import-glob &&
npm install --save ../deps/phoenix ../deps/phoenix_html ../deps/phoenix_live_view"
|> String.to_charlist()
|> :os.cmd()
|> IO.puts()
end
end

15
lib/mix/tasks/setup.ex Normal file
View file

@ -0,0 +1,15 @@
defmodule Mix.Tasks.LiveSvelte.Setup do
import LiveSvelte.Logger
def run(_) do
[
"install_npm_deps",
"configure_phoenix",
"configure_esbuild"
]
|> Enum.map(&Task.async(fn -> Mix.Task.run("live_svelte." <> &1) end))
|> Enum.map(&Task.await(&1, :infinity))
log_success("live_svelte setup finished.")
end
end

52
mix.exs Normal file
View file

@ -0,0 +1,52 @@
defmodule LiveSvelte.MixProject do
use Mix.Project
@version "0.1.0-rc0"
@repo_url "https://github.com/woutdp/live_svelte"
def project do
[
app: :live_svelte,
version: @version,
elixir: "~> 1.12",
start_permanent: Mix.env() == :prod,
deps: deps(),
# Hex
description: "E2E reactivity for Svelte and LiveView",
package: package(),
# Docs
name: "LiveSvelte",
docs: [
source_ref: "v#{@version}",
source_url: @repo_url
]
]
end
defp package() do
[
maintainers: ["Wout De Puysseleir"],
licenses: ["MIT"],
links: %{"GitHub" => @repo_url},
files: ~w(assets lib LICENSE.MD mix.exs package.json README.md .formatter.exs)
]
end
def application do
[
extra_applications: [:logger]
]
end
defp deps do
[
{:ex_doc, "~> 0.19", only: :dev, runtime: false},
{:jason, "~> 1.2"},
{:nodejs, "~> 2.0"},
{:phoenix, "~> 1.6.16"},
{:phoenix_live_view, "~> 0.18.3"}
]
end
end

22
mix.lock Normal file
View file

@ -0,0 +1,22 @@
%{
"castore": {:hex, :castore, "1.0.0", "c25cd0794c054ebe6908a86820c8b92b5695814479ec95eeff35192720b71eec", [:mix], [], "hexpm", "577d0e855983a97ca1dfa33cbb8a3b6ece6767397ffb4861514343b078fc284b"},
"earmark_parser": {:hex, :earmark_parser, "1.4.30", "0b938aa5b9bafd455056440cdaa2a79197ca5e693830b4a982beada840513c5f", [:mix], [], "hexpm", "3b5385c2d36b0473d0b206927b841343d25adb14f95f0110062506b300cd5a1b"},
"ex_doc": {:hex, :ex_doc, "0.29.1", "b1c652fa5f92ee9cf15c75271168027f92039b3877094290a75abcaac82a9f77", [:mix], [{:earmark_parser, "~> 1.4.19", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.14", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1", [hex: :makeup_erlang, repo: "hexpm", optional: false]}], "hexpm", "b7745fa6374a36daf484e2a2012274950e084815b936b1319aeebcf7809574f6"},
"jason": {:hex, :jason, "1.4.0", "e855647bc964a44e2f67df589ccf49105ae039d4179db7f6271dfd3843dc27e6", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "79a3791085b2a0f743ca04cec0f7be26443738779d09302e01318f97bdb82121"},
"makeup": {:hex, :makeup, "1.1.0", "6b67c8bc2882a6b6a445859952a602afc1a41c2e08379ca057c0f525366fc3ca", [:mix], [{:nimble_parsec, "~> 1.2.2 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "0a45ed501f4a8897f580eabf99a2e5234ea3e75a4373c8a52824f6e873be57a6"},
"makeup_elixir": {:hex, :makeup_elixir, "0.16.0", "f8c570a0d33f8039513fbccaf7108c5d750f47d8defd44088371191b76492b0b", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "28b2cbdc13960a46ae9a8858c4bebdec3c9a6d7b4b9e7f4ed1502f8159f338e7"},
"makeup_erlang": {:hex, :makeup_erlang, "0.1.1", "3fcb7f09eb9d98dc4d208f49cc955a34218fc41ff6b84df7c75b3e6e533cc65f", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "174d0809e98a4ef0b3309256cbf97101c6ec01c4ab0b23e926a9e17df2077cbb"},
"mime": {:hex, :mime, "2.0.3", "3676436d3d1f7b81b5a2d2bd8405f412c677558c81b1c92be58c00562bb59095", [:mix], [], "hexpm", "27a30bf0db44d25eecba73755acf4068cbfe26a4372f9eb3e4ea3a45956bff6b"},
"nimble_parsec": {:hex, :nimble_parsec, "1.2.3", "244836e6e3f1200c7f30cb56733fd808744eca61fd182f731eac4af635cc6d0b", [:mix], [], "hexpm", "c8d789e39b9131acf7b99291e93dae60ab48ef14a7ee9d58c6964f59efb570b0"},
"nodejs": {:hex, :nodejs, "2.0.0", "9a00d00eabf84ba7a04269de46863e0f87bdf6bc488d5a20972b38ade9012764", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:poolboy, "~> 1.5.1", [hex: :poolboy, repo: "hexpm", optional: false]}], "hexpm", "3a03df7dbfba435223b4534fbf276db8be5287fbf83c828f2749bf1ffe73e930"},
"phoenix": {:hex, :phoenix, "1.6.16", "e5bdd18c7a06da5852a25c7befb72246de4ddc289182285f8685a40b7b5f5451", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.0", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 1.0 or ~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: false]}, {:plug, "~> 1.10", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.2", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:plug_crypto, "~> 1.2", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "e15989ff34f670a96b95ef6d1d25bad0d9c50df5df40b671d8f4a669e050ac39"},
"phoenix_html": {:hex, :phoenix_html, "3.3.0", "bf451c71ebdaac8d2f40d3b703435e819ccfbb9ff243140ca3bd10c155f134cc", [:mix], [{:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "272c5c1533499f0132309936c619186480bafcc2246588f99a69ce85095556ef"},
"phoenix_live_view": {:hex, :phoenix_live_view, "0.18.15", "58137e648fca9da56d6e931c9c3001f895ff090291052035f395bc958b82f1a5", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix, "~> 1.6.15 or ~> 1.7.0", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 3.3", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "888dd8ea986bebbda741acc65aef788c384d13db91fea416461b2e96aa06a193"},
"phoenix_pubsub": {:hex, :phoenix_pubsub, "2.1.1", "ba04e489ef03763bf28a17eb2eaddc2c20c6d217e2150a61e3298b0f4c2012b5", [:mix], [], "hexpm", "81367c6d1eea5878ad726be80808eb5a787a23dee699f96e72b1109c57cdd8d9"},
"phoenix_template": {:hex, :phoenix_template, "1.0.1", "85f79e3ad1b0180abb43f9725973e3b8c2c3354a87245f91431eec60553ed3ef", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}], "hexpm", "157dc078f6226334c91cb32c1865bf3911686f8bcd6bcff86736f6253e6993ee"},
"phoenix_view": {:hex, :phoenix_view, "2.0.2", "6bd4d2fd595ef80d33b439ede6a19326b78f0f1d8d62b9a318e3d9c1af351098", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}], "hexpm", "a929e7230ea5c7ee0e149ffcf44ce7cf7f4b6d2bfe1752dd7c084cdff152d36f"},
"plug": {:hex, :plug, "1.14.0", "ba4f558468f69cbd9f6b356d25443d0b796fbdc887e03fa89001384a9cac638f", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "bf020432c7d4feb7b3af16a0c2701455cbbbb95e5b6866132cb09eb0c29adc14"},
"plug_crypto": {:hex, :plug_crypto, "1.2.3", "8f77d13aeb32bfd9e654cb68f0af517b371fb34c56c9f2b58fe3df1235c1251a", [:mix], [], "hexpm", "b5672099c6ad5c202c45f5a403f21a3411247f164e4a8fab056e5cd8a290f4a2"},
"poolboy": {:hex, :poolboy, "1.5.2", "392b007a1693a64540cead79830443abf5762f5d30cf50bc95cb2c1aaafa006b", [:rebar3], [], "hexpm", "dad79704ce5440f3d5a3681c8590b9dc25d1a561e8f5a9c995281012860901e3"},
"telemetry": {:hex, :telemetry, "1.2.1", "68fdfe8d8f05a8428483a97d7aab2f268aaff24b49e0f599faa091f1d4e7f61c", [:rebar3], [], "hexpm", "dad9ce9d8effc621708f99eac538ef1cbe05d6a874dd741de2e689c47feafed5"},
}

26
package.json Normal file
View file

@ -0,0 +1,26 @@
{
"name": "live_svelte",
"version": "0.1.0",
"description": "",
"license": "MIT",
"module": "./priv/static/live_svelte.mjs",
"main": "./priv/static/live_svelte.cjs.js",
"unpkg": "./priv/static/live_svelte.min.js",
"jsdelivr": "./priv/static/live_svelte.min.js",
"exports": {
"import": "./priv/static/live_svelte.mjs",
"require": "./priv/static/live_svelte.cjs.js"
},
"repository": {
"type": "git",
"url": "git://github.com/woutdp/live_svelte.git"
},
"author": "Wout De Puysseleir <contact@wout.space>",
"files": [
"README.md",
"LICENSE.md",
"package.json",
"assets/js/phoenix/*"
]
}

View file

@ -0,0 +1,8 @@
defmodule LiveSvelteTest do
use ExUnit.Case
doctest LiveSvelte
test "greets the world" do
assert LiveSvelte.hello() == :world
end
end

1
test/test_helper.exs Normal file
View file

@ -0,0 +1 @@
ExUnit.start()