From d6e3667a7e2d60fd8863742cf4c0c9d1877859e9 Mon Sep 17 00:00:00 2001 From: munenick Date: Mon, 11 May 2026 12:17:06 +0900 Subject: [PATCH 1/2] Add Solid plugin --- lib/volt/plugin/solid.ex | 349 +++++++++++++++++++++++++++++++++++++++ mix.exs | 9 +- priv/ts/solid-runtime.ts | 93 +++++++++++ test/builder_test.exs | 71 ++++++++ test/plugin_test.exs | 84 ++++++++++ 5 files changed, 605 insertions(+), 1 deletion(-) create mode 100644 lib/volt/plugin/solid.ex create mode 100644 priv/ts/solid-runtime.ts diff --git a/lib/volt/plugin/solid.ex b/lib/volt/plugin/solid.ex new file mode 100644 index 0000000..04b9cf8 --- /dev/null +++ b/lib/volt/plugin/solid.ex @@ -0,0 +1,349 @@ +defmodule Volt.Plugin.Solid do + @moduledoc """ + Solid JSX/TSX support for Volt. + + Solid JSX requires the Solid compiler rather than a generic JSX runtime + transform. This plugin runs `babel-preset-solid` through Volt's JavaScript + runtime and leaves normal module resolution to Volt. Add `Volt.Plugin.Solid` + to `config :volt, :plugins` when using Solid TSX, since `.jsx` and `.tsx` + files are also used by React and generic JSX builds. + """ + + @behaviour Volt.Plugin + + @runtime_packages %{ + "@babel/standalone" => "^7.0.0", + "babel-preset-solid" => "^1.9.0" + } + + @runtime_name __MODULE__.Runtime + @jsx_exts ~w(.jsx .tsx) + @web_imports ~w( + Aliases + Assets + ChildProperties + DOMElements + DelegatedEvents + Dynamic + ErrorBoundary + For + Hydration + HydrationScript + Index + Match + NoHydration + Portal + Properties + RequestContext + SVGElements + SVGNamespace + Show + Suspense + SuspenseList + Switch + addEventListener + assign + classList + className + clearDelegatedEvents + createComponent + createDynamic + delegateEvents + dynamicProperty + effect + escape + generateHydrationScript + getAssets + getHydrationKey + getNextElement + getNextMarker + getNextMatch + getOwner + getPropAlias + getRequestEvent + hydrate + innerHTML + insert + isDev + isServer + memo + mergeProps + render + renderToStream + renderToString + renderToStringAsync + resolveSSRNode + runHydrationEvents + setAttribute + setAttributeNS + setBoolAttribute + setProperty + setStyleProperty + spread + ssr + ssrAttribute + ssrClassList + ssrElement + ssrHydrationKey + ssrSpread + ssrStyle + style + template + untrack + use + useAssets + ) + + @impl true + def name, do: "solid" + + @impl true + def compile(path, source, opts), do: compile(path, source, opts, []) + + def compile(path, source, opts, plugin_opts) do + filename = compile_filename(path, opts) + + if solid_file?(filename) do + do_compile(source, filename, opts, plugin_opts) + end + end + + @impl true + def extract_imports(path, source, opts), do: extract_imports(path, source, opts, []) + + def extract_imports(path, source, opts, _plugin_opts) do + filename = compile_filename(path, opts) + + if solid_file?(filename) do + with {:ok, %{imports: imports} = result} <- extract_typed_imports(source, filename) do + {:ok, %{result | imports: add_compiler_imports(imports)}} + end + end + end + + @impl true + def prebundle_alias("solid-js/web"), do: "solid-js" + def prebundle_alias(_specifier), do: nil + + @impl true + def prebundle_entry("solid-js") do + web_exports = Enum.map(@web_imports, &{&1, &1}) + + {:proxy, "solid-js.js", + exports: [ + %{all_from: "solid-js"}, + %{named_from: "solid-js/web", names: web_exports} + ]} + end + + def prebundle_entry(_specifier), do: nil + + def runtime_packages, do: @runtime_packages + + defp do_compile(source, filename, opts, plugin_opts) do + runtime = + Volt.JS.Runtime.ensure!( + name: @runtime_name, + packages: @runtime_packages, + apis: [:browser, :node], + entry: {:volt_asset, "solid-runtime.ts"}, + bundle: true, + max_stack_size: 32 * 1024 * 1024 + ) + + compile_options = compile_options(filename, opts, plugin_opts) + + case Volt.JS.Runtime.call(runtime, "compileSolid", [source, compile_options]) do + {:ok, %{"code" => code, "map" => map}} -> + with {:ok, code, downlevelled?} <- maybe_downlevel(code, filename, opts) do + {:ok, + %{ + code: code, + sourcemap: encode_sourcemap(map, downlevelled?), + css: nil, + hashes: %{template: nil, style: nil, script: hash(source)} + }} + end + + {:ok, other} -> + {:error, {:unexpected_solid_result, other}} + + {:error, _} = error -> + error + end + end + + defp compile_options(filename, opts, plugin_opts) do + solid_options = + %{ + "generate" => option(opts, plugin_opts, :generate, "dom"), + "hydratable" => option(opts, plugin_opts, :hydratable, false), + "dev" => option(opts, plugin_opts, :dev, false) + } + |> Map.merge(stringify_keys(Keyword.get(plugin_opts, :solid_options, %{}))) + |> Map.merge(stringify_keys(Keyword.get(opts, :solid_options, %{}))) + + options = %{ + "filename" => filename, + "typescript" => Path.extname(filename) in ~w(.ts .tsx .mts), + "sourcemap" => Keyword.get(opts, :sourcemap, true), + "solidOptions" => solid_options + } + + case stringify_keys(Keyword.get(plugin_opts, :typescript_options, %{})) do + map when map_size(map) == 0 -> options + map -> Map.put(options, "typescriptOptions", map) + end + end + + defp maybe_downlevel(code, filename, opts) do + case Keyword.get(opts, :target) do + nil -> + {:ok, code, false} + + "" -> + {:ok, code, false} + + target -> + case OXC.transform(code, js_filename(filename), + target: to_string(target), + sourcemap: false + ) do + {:ok, transformed} when is_binary(transformed) -> + {:ok, transformed, transformed != code} + + {:ok, %{code: transformed}} -> + {:ok, transformed, transformed != code} + + {:error, _} = error -> + error + end + end + end + + defp js_filename(filename), do: Path.rootname(filename) <> ".js" + + defp compile_filename(path, opts) do + path + |> Path.basename() + |> Volt.JS.Extensions.apply_loader(Keyword.get(opts, :loaders, %{})) + end + + defp solid_file?(filename), do: Path.extname(filename) in @jsx_exts + + defp add_compiler_imports(imports) do + imports + |> Enum.concat([{:static, "solid-js/web"}]) + |> Enum.uniq() + end + + defp extract_typed_imports(source, filename) do + if needs_postwalk?(source) do + extract_typed_imports_slow(source, filename) + else + case OXC.collect_imports(source, filename) do + {:ok, imports} -> + typed = Enum.map(imports, &{&1.type, &1.specifier}) + {:ok, %{imports: typed, workers: []}} + + {:error, _} = error -> + error + end + end + end + + defp needs_postwalk?(source) do + String.contains?(source, "require(") or String.contains?(source, "new Worker") or + String.contains?(source, "new SharedWorker") + end + + defp extract_typed_imports_slow(source, filename) do + case OXC.parse(source, filename) do + {:ok, ast} -> + {_ast, acc} = + OXC.postwalk(ast, %{imports: [], workers: []}, fn + %{type: :import_declaration, source: %{value: spec}} = node, acc -> + if type_only_import?(node) do + {node, acc} + else + {node, update_in(acc.imports, &[{:static, spec} | &1])} + end + + %{type: :export_named_declaration, source: %{value: spec}} = node, acc + when is_binary(spec) -> + if type_only_export?(node) do + {node, acc} + else + {node, update_in(acc.imports, &[{:static, spec} | &1])} + end + + %{type: :export_all_declaration, source: %{value: spec}} = node, acc -> + {node, update_in(acc.imports, &[{:static, spec} | &1])} + + %{type: :import_expression, source: %{type: :literal, value: spec}} = node, acc + when is_binary(spec) -> + {node, update_in(acc.imports, &[{:dynamic, spec} | &1])} + + %{ + type: :call_expression, + callee: %{type: :identifier, name: "require"}, + arguments: [%{type: :literal, value: spec} | _] + } = node, + acc + when is_binary(spec) -> + {node, update_in(acc.imports, &[{:static, spec} | &1])} + + %{ + type: :new_expression, + callee: %{type: :identifier, name: worker_type}, + arguments: [first_arg | _] + } = node, + acc + when worker_type in ["Worker", "SharedWorker"] -> + case Volt.JS.WorkerRewriter.extract_specifier(first_arg) do + {:ok, spec, _s, _e} -> {node, update_in(acc.workers, &[spec | &1])} + nil -> {node, acc} + end + + node, acc -> + {node, acc} + end) + + {:ok, %{imports: Enum.reverse(acc.imports), workers: Enum.reverse(acc.workers)}} + + {:error, _} -> + case OXC.imports(source, filename) do + {:ok, specs} -> {:ok, %{imports: Enum.map(specs, &{:static, &1}), workers: []}} + error -> error + end + end + end + + defp type_only_import?(%{importKind: "type"}), do: true + + defp type_only_import?(%{specifiers: specifiers}) when is_list(specifiers) do + specifiers != [] and Enum.all?(specifiers, &(Map.get(&1, :importKind) == "type")) + end + + defp type_only_import?(_node), do: false + + defp type_only_export?(%{exportKind: "type"}), do: true + defp type_only_export?(_node), do: false + + defp option(opts, plugin_opts, key, default) do + Keyword.get(opts, key, Keyword.get(plugin_opts, key, default)) + end + + defp stringify_keys(map) when is_map(map) do + Map.new(map, fn {key, value} -> {to_string(key), value} end) + end + + defp encode_sourcemap(_map, true), do: nil + defp encode_sourcemap(nil, false), do: nil + defp encode_sourcemap(map, false) when is_map(map), do: Jason.encode!(map) + defp encode_sourcemap(value, false) when is_binary(value), do: value + + defp hash(nil), do: nil + defp hash(""), do: nil + defp hash(value), do: :erlang.phash2(value) |> Integer.to_string(16) +end diff --git a/mix.exs b/mix.exs index deccc01..82014a4 100644 --- a/mix.exs +++ b/mix.exs @@ -117,7 +117,14 @@ defmodule Volt.MixProject do "Production Build": [Volt.Builder, Volt.ChunkGraph, Volt.Preload], "Tailwind CSS": [Volt.Tailwind], "CSS": [Volt.CSS.Modules], - "Plugins": [Volt.Plugin, Volt.Plugin.Vue, Volt.Plugin.Svelte, Volt.Plugin.React, Volt.PluginRunner], + "Plugins": [ + Volt.Plugin, + Volt.Plugin.Vue, + Volt.Plugin.Svelte, + Volt.Plugin.React, + Volt.Plugin.Solid, + Volt.PluginRunner + ], "JavaScript": [Volt.JS.Runtime, Volt.JS.GlobImport, Volt.JS.PackageResolver, Volt.Env, Volt.Assets], "Formatting": [Volt.Formatter], "Mix Tasks": [Mix.Tasks.Volt.Build, Mix.Tasks.Volt.Dev, Mix.Tasks.Volt.Lint, Mix.Tasks.Volt.Js.Format, Mix.Tasks.Volt.Js.Check, Mix.Tasks.Volt.Install] diff --git a/priv/ts/solid-runtime.ts b/priv/ts/solid-runtime.ts new file mode 100644 index 0000000..576f828 --- /dev/null +++ b/priv/ts/solid-runtime.ts @@ -0,0 +1,93 @@ +function assert(value, message) { + if (!value) { + throw new Error(message || 'assertion failed') + } +} + +assert.ok = assert +assert.equal = (actual, expected, message) => { + if (actual != expected) { + throw new Error(message || 'assert equal failed') + } +} +assert.strictEqual = (actual, expected, message) => { + if (actual !== expected) { + throw new Error(message || 'assert strictEqual failed') + } +} + +const path = { + sep: '/', + basename(value) { + return String(value).split('/').pop() || '' + }, + dirname(value) { + const parts = String(value).split('/') + parts.pop() + return parts.join('/') || '.' + }, + extname(value) { + const base = path.basename(value) + const index = base.lastIndexOf('.') + return index > 0 ? base.slice(index) : '' + }, + join(...parts) { + return parts.join('/') + }, + resolve(...parts) { + return parts.join('/') + } +} + +globalThis.require = (name) => { + if (name === 'assert') return assert + if (name === 'path') return path + throw new Error(`unsupported require ${name}`) +} + +let compiler + +async function loadCompiler() { + if (compiler) return compiler + + const Babel = await import('@babel/standalone') + const solidPreset = await import('babel-preset-solid') + Babel.registerPreset('solid', solidPreset.default) + compiler = Babel + return compiler +} + +async function compileSolid(source, options = {}) { + const Babel = await loadCompiler() + const filename = options.filename ?? 'component.tsx' + const typescript = options.typescript ?? /\.[cm]?tsx?$/.test(filename) + + const presets = [] + + if (typescript) { + presets.push([ + 'typescript', + { + isTSX: /\.tsx$/.test(filename), + allExtensions: true, + allowDeclareFields: true, + ...(options.typescriptOptions ?? {}) + } + ]) + } + + presets.push(['solid', options.solidOptions ?? {}]) + + const result = Babel.transform(source, { + filename, + sourceMaps: options.sourcemap ?? true, + presets + }) + + return { + code: result.code ?? '', + map: result.map ?? null + } +} + +globalThis.compileSolid = compileSolid diff --git a/test/builder_test.exs b/test/builder_test.exs index a03b6a2..dd8f9d1 100644 --- a/test/builder_test.exs +++ b/test/builder_test.exs @@ -258,6 +258,77 @@ defmodule Volt.BuilderTest do assert File.read!(result.css.path) =~ "#639" end + @tag :integration + test "bundles Solid TSX through configured plugin" do + solid_dir = Path.join(@fixture_dir, "node_modules/solid-js") + File.mkdir_p!(solid_dir) + + File.write!( + Path.join(solid_dir, "package.json"), + :json.encode(%{ + "name" => "solid-js", + "type" => "module", + "exports" => %{ + "." => "./index.js", + "./web" => "./web.js" + } + }) + ) + + File.write!(Path.join(solid_dir, "index.js"), """ + export function createSignal(value) { + return [() => value, (next) => { value = typeof next === 'function' ? next(value) : next }] + } + """) + + File.write!(Path.join(solid_dir, "web.js"), """ + export function template(html) { return () => ({ marker: 'solid-web-template', html }) } + export function insert() {} + export function delegateEvents() {} + export function render(fn) { return fn() } + export function createComponent(Component, props) { return Component(props) } + """) + + File.write!( + Path.join(@fixture_dir, "src/solid_worker.ts"), + "self.postMessage('solid-worker-ready')" + ) + + File.write!(Path.join(@fixture_dir, "src/solid_app.tsx"), """ + import type { Label } from './solid_types' + import { createSignal } from 'solid-js' + import { render } from 'solid-js/web' + + const worker = new Worker(new URL('./solid_worker.ts', import.meta.url), { type: 'module' }) + console.log(worker) + + type Props = { name: Label } + + function App(props: Props) { + const [count, setCount] = createSignal(0) + return + } + + render(() => ) + """) + + {:ok, result} = + Volt.Builder.build( + entry: Path.join(@fixture_dir, "src/solid_app.tsx"), + outdir: @outdir, + minify: false, + sourcemap: false, + node_modules: Path.join(@fixture_dir, "node_modules"), + plugins: [Volt.Plugin.Solid] + ) + + js = File.read!(result.js.path) + assert js =~ "solid-web-template" + assert js =~ "delegateEvents" + assert js =~ ~r/solid_worker-[a-f0-9]{8}\.js/ + refute js =~ "jsx-runtime" + end + test "builds standalone CSS entries from HTML manifests" do File.write!(Path.join(@fixture_dir, "src/site.css"), ".site { color: blue }") diff --git a/test/plugin_test.exs b/test/plugin_test.exs index 0f3f47f..36bc642 100644 --- a/test/plugin_test.exs +++ b/test/plugin_test.exs @@ -167,6 +167,90 @@ defmodule Volt.PluginTest do end end + describe "Volt.Plugin.Solid" do + @tag :integration + test "compiles Solid TSX through the Solid compiler" do + source = """ + type Props = { name: string } + + export function App(props: Props) { + return

Hello {props.name}

+ } + """ + + assert {:ok, %{code: code, sourcemap: sourcemap}} = + Volt.Plugin.Solid.compile("App.tsx", source, []) + + assert code =~ "solid-js/web" + assert code =~ "template" + refute code =~ "jsx-runtime" + assert is_binary(sourcemap) + end + + test "adds Solid compiler runtime imports during import extraction" do + source = """ + import { createSignal } from 'solid-js' + const [count] = createSignal(0) + export const App = () => + """ + + assert {:ok, %{imports: imports, workers: []}} = + Volt.Plugin.Solid.extract_imports("App.tsx", source, []) + + assert {:static, "solid-js"} in imports + assert {:static, "solid-js/web"} in imports + end + + @tag :integration + test "removes imports that are only used as TypeScript types" do + source = """ + import { createSignal } from 'solid-js' + import { Label } from './types' + + type Props = { label: Label } + + export function App(props: Props) { + const [count] = createSignal(0) + return + } + """ + + assert {:ok, %{code: code}} = Volt.Plugin.Solid.compile("App.tsx", source, []) + + refute code =~ "./types" + assert code =~ "solid-js" + end + + test "extracts require imports and worker specifiers" do + source = """ + import { render } from 'solid-js/web' + const helper = require('./helper') + const worker = new Worker(new URL('./worker.ts', import.meta.url), { type: 'module' }) + console.log(worker) + export const App = () => + """ + + assert {:ok, %{imports: imports, workers: workers}} = + Volt.Plugin.Solid.extract_imports("App.tsx", source, []) + + assert {:static, "./helper"} in imports + assert {:static, "solid-js/web"} in imports + assert "./worker.ts" in workers + end + + @tag :integration + test "drops sourcemap when target downleveling rewrites compiled output" do + source = """ + export const App = () => + """ + + assert {:ok, %{code: code, sourcemap: nil}} = + Volt.Plugin.Solid.compile("App.tsx", source, target: :es2019) + + refute code =~ "??" + end + end + describe "Pipeline integration" do test "plugins can compile custom file types" do defmodule CustomCompilerPlugin do From eb07e670656c57d296d81f84a44727a3ac524dfb Mon Sep 17 00:00:00 2001 From: munenick Date: Mon, 11 May 2026 12:17:14 +0900 Subject: [PATCH 2/2] Add Solid example --- examples/solid/.env | 1 + examples/solid/.formatter.exs | 9 ++++ examples/solid/.gitignore | 6 +++ examples/solid/README.md | 10 ++++ examples/solid/assets/css/app.css | 3 ++ examples/solid/assets/images/volt.svg | 3 ++ examples/solid/assets/js/Card.tsx | 10 ++++ examples/solid/assets/js/Counter.tsx | 19 +++++++ examples/solid/assets/js/app.tsx | 53 ++++++++++++++++++ examples/solid/assets/js/config.json | 4 ++ examples/solid/assets/js/pages/about.ts | 2 + examples/solid/assets/js/pages/home.ts | 2 + examples/solid/assets/tsconfig.json | 14 +++++ examples/solid/config/config.exs | 43 +++++++++++++++ examples/solid/config/dev.exs | 14 +++++ examples/solid/config/prod.exs | 7 +++ examples/solid/config/runtime.exs | 8 +++ examples/solid/config/test.exs | 9 ++++ examples/solid/lib/solid_example.ex | 3 ++ .../solid/lib/solid_example/application.ex | 22 ++++++++ examples/solid/lib/solid_example_web.ex | 54 +++++++++++++++++++ .../solid_example_web/components/layouts.ex | 17 ++++++ .../components/layouts/root.html.heex | 14 +++++ .../controllers/error_html.ex | 5 ++ .../solid/lib/solid_example_web/endpoint.ex | 41 ++++++++++++++ .../lib/solid_example_web/live/home_live.ex | 11 ++++ .../solid/lib/solid_example_web/router.ex | 17 ++++++ .../solid/lib/solid_example_web/telemetry.ex | 14 +++++ examples/solid/mix.exs | 51 ++++++++++++++++++ examples/solid/mix.lock | 45 ++++++++++++++++ examples/solid/package.json | 9 ++++ examples/solid/test/test_helper.exs | 1 + 32 files changed, 521 insertions(+) create mode 100644 examples/solid/.env create mode 100644 examples/solid/.formatter.exs create mode 100644 examples/solid/.gitignore create mode 100644 examples/solid/README.md create mode 100644 examples/solid/assets/css/app.css create mode 100644 examples/solid/assets/images/volt.svg create mode 100644 examples/solid/assets/js/Card.tsx create mode 100644 examples/solid/assets/js/Counter.tsx create mode 100644 examples/solid/assets/js/app.tsx create mode 100644 examples/solid/assets/js/config.json create mode 100644 examples/solid/assets/js/pages/about.ts create mode 100644 examples/solid/assets/js/pages/home.ts create mode 100644 examples/solid/assets/tsconfig.json create mode 100644 examples/solid/config/config.exs create mode 100644 examples/solid/config/dev.exs create mode 100644 examples/solid/config/prod.exs create mode 100644 examples/solid/config/runtime.exs create mode 100644 examples/solid/config/test.exs create mode 100644 examples/solid/lib/solid_example.ex create mode 100644 examples/solid/lib/solid_example/application.ex create mode 100644 examples/solid/lib/solid_example_web.ex create mode 100644 examples/solid/lib/solid_example_web/components/layouts.ex create mode 100644 examples/solid/lib/solid_example_web/components/layouts/root.html.heex create mode 100644 examples/solid/lib/solid_example_web/controllers/error_html.ex create mode 100644 examples/solid/lib/solid_example_web/endpoint.ex create mode 100644 examples/solid/lib/solid_example_web/live/home_live.ex create mode 100644 examples/solid/lib/solid_example_web/router.ex create mode 100644 examples/solid/lib/solid_example_web/telemetry.ex create mode 100644 examples/solid/mix.exs create mode 100644 examples/solid/mix.lock create mode 100644 examples/solid/package.json create mode 100644 examples/solid/test/test_helper.exs diff --git a/examples/solid/.env b/examples/solid/.env new file mode 100644 index 0000000..2513045 --- /dev/null +++ b/examples/solid/.env @@ -0,0 +1 @@ +VOLT_APP_NAME=Volt Solid Example diff --git a/examples/solid/.formatter.exs b/examples/solid/.formatter.exs new file mode 100644 index 0000000..a0877aa --- /dev/null +++ b/examples/solid/.formatter.exs @@ -0,0 +1,9 @@ +[ + plugins: [Volt.Formatter], + import_deps: [:phoenix], + inputs: [ + "*.{heex,ex,exs}", + "{config,lib,test}/**/*.{heex,ex,exs}", + "assets/**/*.{js,ts,jsx,tsx}" + ] +] diff --git a/examples/solid/.gitignore b/examples/solid/.gitignore new file mode 100644 index 0000000..caa6331 --- /dev/null +++ b/examples/solid/.gitignore @@ -0,0 +1,6 @@ +/_build/ +/cover/ +/deps/ +/node_modules/ +/priv/static/assets/ +/npm.lock diff --git a/examples/solid/README.md b/examples/solid/README.md new file mode 100644 index 0000000..ec12c83 --- /dev/null +++ b/examples/solid/README.md @@ -0,0 +1,10 @@ +# Volt Solid Example + +A minimal Phoenix app using Volt with Solid. + +```sh +mix setup +mix phx.server +``` + +Open . diff --git a/examples/solid/assets/css/app.css b/examples/solid/assets/css/app.css new file mode 100644 index 0000000..fb71c09 --- /dev/null +++ b/examples/solid/assets/css/app.css @@ -0,0 +1,3 @@ +@import "tailwindcss" source(none); +@source "../js"; +@source "../../lib"; diff --git a/examples/solid/assets/images/volt.svg b/examples/solid/assets/images/volt.svg new file mode 100644 index 0000000..81ce3aa --- /dev/null +++ b/examples/solid/assets/images/volt.svg @@ -0,0 +1,3 @@ + + + diff --git a/examples/solid/assets/js/Card.tsx b/examples/solid/assets/js/Card.tsx new file mode 100644 index 0000000..19c7eee --- /dev/null +++ b/examples/solid/assets/js/Card.tsx @@ -0,0 +1,10 @@ +import type { JSX } from 'solid-js' + +export default function Card(props: { title: string; children: JSX.Element }) { + return ( +
+

{props.title}

+ {props.children} +
+ ) +} diff --git a/examples/solid/assets/js/Counter.tsx b/examples/solid/assets/js/Counter.tsx new file mode 100644 index 0000000..c570c8a --- /dev/null +++ b/examples/solid/assets/js/Counter.tsx @@ -0,0 +1,19 @@ +import { createMemo, createSignal } from 'solid-js' + +export default function Counter() { + const [count, setCount] = createSignal(0) + const doubled = createMemo(() => count() * 2) + + return ( +
+ +

Doubled: {doubled()}

+
+ ) +} diff --git a/examples/solid/assets/js/app.tsx b/examples/solid/assets/js/app.tsx new file mode 100644 index 0000000..2b0566c --- /dev/null +++ b/examples/solid/assets/js/app.tsx @@ -0,0 +1,53 @@ +import { For } from 'solid-js' +import { render } from 'solid-js/web' +import config from './config.json' +import logo from '../images/volt.svg' +import Counter from './Counter' +import Card from './Card' + +const pages = import.meta.glob('./pages/*.ts', { eager: true }) as Record< + string, + { title: string; description: string } +> + +function App() { + return ( +
+
+ +
+

{config.name} + Solid

+

v{config.version}

+
+
+ + + + + + +
    + + {([_path, page]) => ( +
  • + {page.title} + {page.description} +
  • + )} +
    +
+
+ +
Mode: {import.meta.env.MODE}
+
+ ) +} + +const dispose = render(() => , document.getElementById('app')!) + +if (import.meta.hot) { + import.meta.hot.dispose(() => { + dispose() + }) + import.meta.hot.accept() +} diff --git a/examples/solid/assets/js/config.json b/examples/solid/assets/js/config.json new file mode 100644 index 0000000..d29f56f --- /dev/null +++ b/examples/solid/assets/js/config.json @@ -0,0 +1,4 @@ +{ + "name": "Volt", + "version": "1.0.0" +} diff --git a/examples/solid/assets/js/pages/about.ts b/examples/solid/assets/js/pages/about.ts new file mode 100644 index 0000000..2dfee33 --- /dev/null +++ b/examples/solid/assets/js/pages/about.ts @@ -0,0 +1,2 @@ +export const title = 'About' +export const description = 'Built with Volt — Elixir-native frontend tooling' diff --git a/examples/solid/assets/js/pages/home.ts b/examples/solid/assets/js/pages/home.ts new file mode 100644 index 0000000..45cbb3e --- /dev/null +++ b/examples/solid/assets/js/pages/home.ts @@ -0,0 +1,2 @@ +export const title = 'Home' +export const description = 'Welcome to the Volt example app' diff --git a/examples/solid/assets/tsconfig.json b/examples/solid/assets/tsconfig.json new file mode 100644 index 0000000..68e1cbd --- /dev/null +++ b/examples/solid/assets/tsconfig.json @@ -0,0 +1,14 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ESNext", + "moduleResolution": "bundler", + "lib": ["ES2022", "DOM"], + "strict": true, + "jsx": "preserve", + "jsxImportSource": "solid-js", + "allowJs": true, + "noEmit": true + }, + "include": ["js/**/*"] +} diff --git a/examples/solid/config/config.exs b/examples/solid/config/config.exs new file mode 100644 index 0000000..3b3db67 --- /dev/null +++ b/examples/solid/config/config.exs @@ -0,0 +1,43 @@ +import Config + +config :solid_example, + generators: [timestamp_type: :utc_datetime] + +config :solid_example, SolidExampleWeb.Endpoint, + url: [host: "localhost"], + adapter: Bandit.PhoenixAdapter, + render_errors: [formats: [html: SolidExampleWeb.ErrorHTML], layout: false], + pubsub_server: SolidExample.PubSub, + live_view: [signing_salt: "volt-example"] + +config :logger, :default_formatter, + format: "$time $metadata[$level] $message\n", + metadata: [:request_id] + +config :phoenix, :json_library, Jason + +config :volt, + entry: "assets/js/app.tsx", + outdir: "priv/static/assets", + root: "assets", + sources: ["**/*.{js,ts,jsx,tsx}"], + target: :es2022, + minify: false, + hash: false, + resolve_dirs: ["node_modules", "deps"], + plugins: [Volt.Plugin.Solid], + tailwind: [ + css: "assets/css/app.css", + sources: [ + %{base: "lib/", pattern: "**/*.{ex,heex}"}, + %{base: "assets/", pattern: "**/*.{js,ts,jsx,tsx}"} + ] + ] + +config :volt, :format, + semi: false, + single_quote: true + +config :volt, :lint, plugins: [:typescript] + +import_config "#{config_env()}.exs" diff --git a/examples/solid/config/dev.exs b/examples/solid/config/dev.exs new file mode 100644 index 0000000..99c7990 --- /dev/null +++ b/examples/solid/config/dev.exs @@ -0,0 +1,14 @@ +import Config + +config :solid_example, SolidExampleWeb.Endpoint, + http: [ip: {127, 0, 0, 1}, port: String.to_integer(System.get_env("PORT") || "4000")], + check_origin: false, + code_reloader: true, + debug_errors: true, + secret_key_base: + "volt-example-secret-key-base-0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ", + watchers: [] + +config :volt, :server, + prefix: "/assets", + watch_dirs: ["lib/", "assets/"] diff --git a/examples/solid/config/prod.exs b/examples/solid/config/prod.exs new file mode 100644 index 0000000..e495c7e --- /dev/null +++ b/examples/solid/config/prod.exs @@ -0,0 +1,7 @@ +import Config + +config :solid_example, SolidExampleWeb.Endpoint, + cache_static_manifest: "priv/static/cache_manifest.json", + server: true + +config :logger, level: :info diff --git a/examples/solid/config/runtime.exs b/examples/solid/config/runtime.exs new file mode 100644 index 0000000..63c820d --- /dev/null +++ b/examples/solid/config/runtime.exs @@ -0,0 +1,8 @@ +import Config + +if config_env() == :prod do + config :solid_example, SolidExampleWeb.Endpoint, + url: [host: System.get_env("PHX_HOST") || "example.com", port: 443, scheme: "https"], + http: [port: String.to_integer(System.get_env("PORT") || "4000")], + secret_key_base: System.fetch_env!("SECRET_KEY_BASE") +end diff --git a/examples/solid/config/test.exs b/examples/solid/config/test.exs new file mode 100644 index 0000000..b95cb38 --- /dev/null +++ b/examples/solid/config/test.exs @@ -0,0 +1,9 @@ +import Config + +config :solid_example, SolidExampleWeb.Endpoint, + http: [ip: {127, 0, 0, 1}, port: 4002], + secret_key_base: + "volt-example-secret-key-base-0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ", + server: false + +config :logger, level: :warning diff --git a/examples/solid/lib/solid_example.ex b/examples/solid/lib/solid_example.ex new file mode 100644 index 0000000..43368bc --- /dev/null +++ b/examples/solid/lib/solid_example.ex @@ -0,0 +1,3 @@ +defmodule SolidExample do + @moduledoc false +end diff --git a/examples/solid/lib/solid_example/application.ex b/examples/solid/lib/solid_example/application.ex new file mode 100644 index 0000000..21f2383 --- /dev/null +++ b/examples/solid/lib/solid_example/application.ex @@ -0,0 +1,22 @@ +defmodule SolidExample.Application do + @moduledoc false + use Application + + @impl true + def start(_type, _args) do + children = [ + SolidExampleWeb.Telemetry, + {DNSCluster, query: Application.get_env(:solid_example, :dns_cluster_query) || :ignore}, + {Phoenix.PubSub, name: SolidExample.PubSub}, + SolidExampleWeb.Endpoint + ] + + Supervisor.start_link(children, strategy: :one_for_one, name: SolidExample.Supervisor) + end + + @impl true + def config_change(changed, _new, removed) do + SolidExampleWeb.Endpoint.config_change(changed, removed) + :ok + end +end diff --git a/examples/solid/lib/solid_example_web.ex b/examples/solid/lib/solid_example_web.ex new file mode 100644 index 0000000..5b83ca3 --- /dev/null +++ b/examples/solid/lib/solid_example_web.ex @@ -0,0 +1,54 @@ +defmodule SolidExampleWeb do + def static_paths, do: ~w(assets fonts images favicon.ico robots.txt) + + def router do + quote do + use Phoenix.Router, helpers: false + import Plug.Conn + import Phoenix.Controller + import Phoenix.LiveView.Router + end + end + + def controller do + quote do + use Phoenix.Controller, formats: [:html, :json] + import Plug.Conn + unquote(verified_routes()) + end + end + + def live_view do + quote do + use Phoenix.LiveView + unquote(html_helpers()) + end + end + + def html do + quote do + use Phoenix.Component + import Phoenix.Controller, only: [get_csrf_token: 0] + unquote(html_helpers()) + end + end + + defp html_helpers do + quote do + import Phoenix.HTML + alias SolidExampleWeb.Layouts + unquote(verified_routes()) + end + end + + def verified_routes do + quote do + use Phoenix.VerifiedRoutes, + endpoint: SolidExampleWeb.Endpoint, + router: SolidExampleWeb.Router, + statics: SolidExampleWeb.static_paths() + end + end + + defmacro __using__(which) when is_atom(which), do: apply(__MODULE__, which, []) +end diff --git a/examples/solid/lib/solid_example_web/components/layouts.ex b/examples/solid/lib/solid_example_web/components/layouts.ex new file mode 100644 index 0000000..d9fa1b3 --- /dev/null +++ b/examples/solid/lib/solid_example_web/components/layouts.ex @@ -0,0 +1,17 @@ +defmodule SolidExampleWeb.Layouts do + use SolidExampleWeb, :html + + embed_templates "layouts/*" + + attr :flash, :map, required: true + attr :current_scope, :map, default: nil + slot :inner_block, required: true + + def app(assigns) do + ~H""" +
+ {render_slot(@inner_block)} +
+ """ + end +end diff --git a/examples/solid/lib/solid_example_web/components/layouts/root.html.heex b/examples/solid/lib/solid_example_web/components/layouts/root.html.heex new file mode 100644 index 0000000..ec520e6 --- /dev/null +++ b/examples/solid/lib/solid_example_web/components/layouts/root.html.heex @@ -0,0 +1,14 @@ + + + + + + + <.live_title default="Volt Solid Example">Volt Solid Example + + + + + {@inner_content} + + diff --git a/examples/solid/lib/solid_example_web/controllers/error_html.ex b/examples/solid/lib/solid_example_web/controllers/error_html.ex new file mode 100644 index 0000000..fc616a2 --- /dev/null +++ b/examples/solid/lib/solid_example_web/controllers/error_html.ex @@ -0,0 +1,5 @@ +defmodule SolidExampleWeb.ErrorHTML do + use SolidExampleWeb, :html + + def render(template, _assigns), do: Phoenix.Controller.status_message_from_template(template) +end diff --git a/examples/solid/lib/solid_example_web/endpoint.ex b/examples/solid/lib/solid_example_web/endpoint.ex new file mode 100644 index 0000000..c37376e --- /dev/null +++ b/examples/solid/lib/solid_example_web/endpoint.ex @@ -0,0 +1,41 @@ +defmodule SolidExampleWeb.Endpoint do + use Phoenix.Endpoint, otp_app: :solid_example + + @session_options [ + store: :cookie, + key: "_solid_example_key", + signing_salt: "volt-example", + same_site: "Lax" + ] + + socket "/live", Phoenix.LiveView.Socket, + websocket: [connect_info: [session: @session_options]], + longpoll: [connect_info: [session: @session_options]] + + plug Plug.Static, + at: "/", + from: :solid_example, + gzip: not code_reloading?, + only: SolidExampleWeb.static_paths(), + raise_on_missing_only: code_reloading? + + if code_reloading? do + socket "/phoenix/live_reload/socket", Phoenix.LiveReloader.Socket + plug Phoenix.LiveReloader + plug Phoenix.CodeReloader + plug Volt.DevServer, root: "assets" + end + + plug Plug.RequestId + plug Plug.Telemetry, event_prefix: [:phoenix, :endpoint] + + plug Plug.Parsers, + parsers: [:urlencoded, :multipart, :json], + pass: ["*/*"], + json_decoder: Phoenix.json_library() + + plug Plug.MethodOverride + plug Plug.Head + plug Plug.Session, @session_options + plug SolidExampleWeb.Router +end diff --git a/examples/solid/lib/solid_example_web/live/home_live.ex b/examples/solid/lib/solid_example_web/live/home_live.ex new file mode 100644 index 0000000..144e8e7 --- /dev/null +++ b/examples/solid/lib/solid_example_web/live/home_live.ex @@ -0,0 +1,11 @@ +defmodule SolidExampleWeb.HomeLive do + use SolidExampleWeb, :live_view + + def render(assigns) do + ~H""" + +
+
+ """ + end +end diff --git a/examples/solid/lib/solid_example_web/router.ex b/examples/solid/lib/solid_example_web/router.ex new file mode 100644 index 0000000..38bd7ac --- /dev/null +++ b/examples/solid/lib/solid_example_web/router.ex @@ -0,0 +1,17 @@ +defmodule SolidExampleWeb.Router do + use SolidExampleWeb, :router + + pipeline :browser do + plug :accepts, ["html"] + plug :fetch_session + plug :fetch_live_flash + plug :put_root_layout, html: {SolidExampleWeb.Layouts, :root} + plug :protect_from_forgery + plug :put_secure_browser_headers + end + + scope "/", SolidExampleWeb do + pipe_through :browser + live "/", HomeLive + end +end diff --git a/examples/solid/lib/solid_example_web/telemetry.ex b/examples/solid/lib/solid_example_web/telemetry.ex new file mode 100644 index 0000000..a6005e4 --- /dev/null +++ b/examples/solid/lib/solid_example_web/telemetry.ex @@ -0,0 +1,14 @@ +defmodule SolidExampleWeb.Telemetry do + use Supervisor + import Telemetry.Metrics + + def start_link(arg), do: Supervisor.start_link(__MODULE__, arg, name: __MODULE__) + + @impl true + def init(_arg) do + children = [{:telemetry_poller, measurements: [], period: 10_000}] + Supervisor.init(children, strategy: :one_for_one) + end + + def metrics, do: [summary("phoenix.endpoint.stop.duration")] +end diff --git a/examples/solid/mix.exs b/examples/solid/mix.exs new file mode 100644 index 0000000..f7c56b0 --- /dev/null +++ b/examples/solid/mix.exs @@ -0,0 +1,51 @@ +defmodule SolidExample.MixProject do + use Mix.Project + + def project do + [ + app: :solid_example, + version: "0.1.0", + elixir: "~> 1.15", + elixirc_paths: elixirc_paths(Mix.env()), + start_permanent: Mix.env() == :prod, + aliases: aliases(), + deps: deps(), + compilers: [:phoenix_live_view] ++ Mix.compilers(), + listeners: [Phoenix.CodeReloader] + ] + end + + def application do + [ + mod: {SolidExample.Application, []}, + extra_applications: [:logger, :runtime_tools] + ] + end + + defp elixirc_paths(:test), do: ["lib", "test/support"] + defp elixirc_paths(_), do: ["lib"] + + defp deps do + [ + {:phoenix, "~> 1.8.4"}, + {:phoenix_html, "~> 4.1"}, + {:phoenix_live_reload, "~> 1.2", only: :dev}, + {:phoenix_live_view, "~> 1.1.0"}, + {:telemetry_metrics, "~> 1.0"}, + {:telemetry_poller, "~> 1.0"}, + {:jason, "~> 1.2"}, + {:dns_cluster, "~> 0.2.0"}, + {:bandit, "~> 1.5"}, + {:volt, path: "../..", override: true} + ] + end + + defp aliases do + [ + setup: ["deps.get", "npm.install", "assets.build"], + "assets.build": ["volt.build --tailwind --no-hash --no-minify"], + "assets.deploy": ["volt.build --tailwind --no-hash", "phx.digest"], + precommit: ["compile --warnings-as-errors", "format", "test"] + ] + end +end diff --git a/examples/solid/mix.lock b/examples/solid/mix.lock new file mode 100644 index 0000000..13ab2ca --- /dev/null +++ b/examples/solid/mix.lock @@ -0,0 +1,45 @@ +%{ + "bandit": {:hex, :bandit, "1.10.4", "02b9734c67c5916a008e7eb7e2ba68aaea6f8177094a5f8d95f1fb99069aac17", [:mix], [{:hpax, "~> 1.0", [hex: :hpax, repo: "hexpm", optional: false]}, {:plug, "~> 1.18", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:thousand_island, "~> 1.0", [hex: :thousand_island, repo: "hexpm", optional: false]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "a5faf501042ac1f31d736d9d4a813b3db4ef812e634583b6a457b0928798a51d"}, + "castore": {:hex, :castore, "1.0.18", "5e43ef0ec7d31195dfa5a65a86e6131db999d074179d2ba5a8de11fe14570f55", [:mix], [], "hexpm", "f393e4fe6317829b158fb74d86eb681f737d2fe326aa61ccf6293c4104957e34"}, + "dns_cluster": {:hex, :dns_cluster, "0.2.0", "aa8eb46e3bd0326bd67b84790c561733b25c5ba2fe3c7e36f28e88f384ebcb33", [:mix], [], "hexpm", "ba6f1893411c69c01b9e8e8f772062535a4cf70f3f35bcc964a324078d8c8240"}, + "dotenvy": {:hex, :dotenvy, "1.1.1", "00e318f3c51de9fafc4b48598447e386f19204dc18ca69886905bb8f8b08b667", [:mix], [], "hexpm", "c8269471b5701e9e56dc86509c1199ded2b33dce088c3471afcfef7839766d8e"}, + "ex_ast": {:hex, :ex_ast, "0.11.0", "840530d164ae9e937fbb04536eb3a25376b19145d037eca2f99cde5501b0d2f1", [:mix], [{:sourceror, "~> 1.7", [hex: :sourceror, repo: "hexpm", optional: false]}], "hexpm", "f4232f8d37f204ed27b086cb35edf3b681e588642b4bd838141835f654a69f37"}, + "file_system": {:hex, :file_system, "1.1.1", "31864f4685b0148f25bd3fbef2b1228457c0c89024ad67f7a81a3ffbc0bbad3a", [:mix], [], "hexpm", "7a15ff97dfe526aeefb090a7a9d3d03aa907e100e262a0f8f7746b78f8f87a5d"}, + "finch": {:hex, :finch, "0.21.0", "b1c3b2d48af02d0c66d2a9ebfb5622be5c5ecd62937cf79a88a7f98d48a8290c", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.6.2 or ~> 1.7", [hex: :mint, repo: "hexpm", optional: false]}, {:nimble_options, "~> 0.4 or ~> 1.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:nimble_pool, "~> 1.1", [hex: :nimble_pool, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "87dc6e169794cb2570f75841a19da99cfde834249568f2a5b121b809588a4377"}, + "floki": {:hex, :floki, "0.38.1", "f002ccac94b3bcb21d40d9b34cc2cc9fd88a8311879120330075b5dde657ebee", [:mix], [], "hexpm", "e744bf0db7ee34b2c8b62767f04071107af0516a81144b9a2f73fe0494200e5b"}, + "hex_solver": {:hex, :hex_solver, "0.2.3", "0d2ee20fbceb251d573f03ef34852e325529c2874ab66d1b576384021318996c", [:mix], [], "hexpm", "9daeae2ea6b8ad3dc7f51a10484f6bc1d0f705c31063383830a7167ab93b887b"}, + "hpax": {:hex, :hpax, "1.0.3", "ed67ef51ad4df91e75cc6a1494f851850c0bd98ebc0be6e81b026e765ee535aa", [:mix], [], "hexpm", "8eab6e1cfa8d5918c2ce4ba43588e894af35dbd8e91e6e55c817bca5847df34a"}, + "jason": {:hex, :jason, "1.4.5", "2e3a008590b0b8d7388c20293e9dcc9cf3e5d642fd2a114e4cbbb52e595d940a", [:mix], [{:decimal, "~> 1.0 or ~> 2.0 or ~> 3.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "b0c823996102bcd0239b3c2444eb00409b72f6a140c1950bc8b457d836b30684"}, + "libgraph": {:hex, :libgraph, "0.16.0", "3936f3eca6ef826e08880230f806bfea13193e49bf153f93edcf0239d4fd1d07", [:mix], [], "hexpm", "41ca92240e8a4138c30a7e06466acc709b0cbb795c643e9e17174a178982d6bf"}, + "mime": {:hex, :mime, "2.0.7", "b8d739037be7cd402aee1ba0306edfdef982687ee7e9859bee6198c1e7e2f128", [:mix], [], "hexpm", "6171188e399ee16023ffc5b76ce445eb6d9672e2e241d2df6050f3c771e80ccd"}, + "mint": {:hex, :mint, "1.8.0", "b964eaf4416f2dee2ba88968d52239fca5621b0402b9c95f55a08eb9d74803e9", [:mix], [{:castore, "~> 0.1.0 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:hpax, "~> 0.1.1 or ~> 0.2.0 or ~> 1.0", [hex: :hpax, repo: "hexpm", optional: false]}], "hexpm", "f3c572c11355eccf00f22275e9b42463bc17bd28db13be1e28f8e0bb4adbc849"}, + "mint_web_socket": {:hex, :mint_web_socket, "1.0.5", "60354efeb49b1eccf95dfb75f55b08d692e211970fe735a5eb3188b328be2a90", [:mix], [{:mint, ">= 1.4.1 and < 2.0.0-0", [hex: :mint, repo: "hexpm", optional: false]}], "hexpm", "04b35663448fc758f3356cce4d6ac067ca418bbafe6972a3805df984b5f12e61"}, + "nimble_options": {:hex, :nimble_options, "1.1.1", "e3a492d54d85fc3fd7c5baf411d9d2852922f66e69476317787a7b2bb000a61b", [:mix], [], "hexpm", "821b2470ca9442c4b6984882fe9bb0389371b8ddec4d45a9504f00a66f650b44"}, + "nimble_parsec": {:hex, :nimble_parsec, "1.4.2", "8efba0122db06df95bfaa78f791344a89352ba04baedd3849593bfce4d0dc1c6", [:mix], [], "hexpm", "4b21398942dda052b403bbe1da991ccd03a053668d147d53fb8c4e0efe09c973"}, + "nimble_pool": {:hex, :nimble_pool, "1.1.0", "bf9c29fbdcba3564a8b800d1eeb5a3c58f36e1e11d7b7fb2e084a643f645f06b", [:mix], [], "hexpm", "af2e4e6b34197db81f7aad230c1118eac993acc0dae6bc83bac0126d4ae0813a"}, + "npm": {:hex, :npm, "0.6.0", "42ee6ffeff85e502ef9aaf72a096bd1da6ed6cb15cb83758ff23956a58c6f22c", [:mix], [{:hex_solver, "~> 0.2", [hex: :hex_solver, repo: "hexpm", optional: false]}, {:npm_semver, "~> 0.1.0", [hex: :npm_semver, repo: "hexpm", optional: false]}, {:req, "~> 0.5", [hex: :req, repo: "hexpm", optional: false]}], "hexpm", "9183d240d8d28faa28e5d47dde2b91723f7c8e29f400b471689f880a0f17f5ef"}, + "npm_semver": {:hex, :npm_semver, "0.1.0", "3ab2c2a151d8c87c364209b2ca1a4fd2ab98507ed61afbd1ea12c1826e67200a", [:mix], [{:hex_solver, "~> 0.2", [hex: :hex_solver, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.0", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "77afbc4c523c19a572325190bc4c968ec027e1c6ef8538bcddacf835966072fa"}, + "oxc": {:hex, :oxc, "0.12.0", "c954642ee3e0d847ea1c28f335a370f5ff071d96253a1c96e9160cf304a50fe3", [:mix], [{:rustler, "~> 0.36", [hex: :rustler, repo: "hexpm", optional: true]}, {:rustler_precompiled, "~> 0.8", [hex: :rustler_precompiled, repo: "hexpm", optional: false]}], "hexpm", "772406eeb522e0db46a8dd7caf1b3d680fc230c570bdc95110f2d1b21ef19099"}, + "oxide_ex": {:hex, :oxide_ex, "0.2.1", "ff0e1c03f58c151ca87e3b11847801a751853894fd926910f459ef5918cc655a", [:mix], [{:rustler, "~> 0.36", [hex: :rustler, repo: "hexpm", optional: true]}, {:rustler_precompiled, "~> 0.8", [hex: :rustler_precompiled, repo: "hexpm", optional: false]}], "hexpm", "25bebc0d30362dead560e815a1380c266d018924a71a548ea2b1582f8dcd77bb"}, + "phoenix": {:hex, :phoenix, "1.8.5", "919db335247e6d4891764dc3063415b0d2457641c5f9b3751b5df03d8e20bbcf", [:mix], [{:bandit, "~> 1.0", [hex: :bandit, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.1", [hex: :phoenix_pubsub, 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]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.7", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:plug_crypto, "~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:websock_adapter, "~> 0.5.3", [hex: :websock_adapter, repo: "hexpm", optional: false]}], "hexpm", "83b2bb125127e02e9f475c8e3e92736325b5b01b0b9b05407bcb4083b7a32485"}, + "phoenix_html": {:hex, :phoenix_html, "4.3.0", "d3577a5df4b6954cd7890c84d955c470b5310bb49647f0a114a6eeecc850f7ad", [:mix], [], "hexpm", "3eaa290a78bab0f075f791a46a981bbe769d94bc776869f4f3063a14f30497ad"}, + "phoenix_live_reload": {:hex, :phoenix_live_reload, "1.6.2", "b18b0773a1ba77f28c52decbb0f10fd1ac4d3ae5b8632399bbf6986e3b665f62", [:mix], [{:file_system, "~> 0.2.10 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}, {:phoenix, "~> 1.4", [hex: :phoenix, repo: "hexpm", optional: false]}], "hexpm", "d1f89c18114c50d394721365ffb428cce24f1c13de0467ffa773e2ff4a30d5b9"}, + "phoenix_live_view": {:hex, :phoenix_live_view, "1.1.28", "8a8e123d018025f756605a2fb02a4854f0d3cd7b207f710fef1fd5d9d72d0254", [:mix], [{:igniter, ">= 0.6.16 and < 1.0.0-0", [hex: :igniter, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:lazy_html, "~> 0.1.0", [hex: :lazy_html, repo: "hexpm", optional: true]}, {:phoenix, "~> 1.6.15 or ~> 1.7.0 or ~> 1.8.0-rc", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 3.3 or ~> 4.0", [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]}, {:plug, "~> 1.15", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "24faad535b65089642c3a7d84088109dc58f49c1f1c5a978659855d643466353"}, + "phoenix_pubsub": {:hex, :phoenix_pubsub, "2.2.0", "ff3a5616e1bed6804de7773b92cbccfc0b0f473faf1f63d7daf1206c7aeaaa6f", [:mix], [], "hexpm", "adc313a5bf7136039f63cfd9668fde73bba0765e0614cba80c06ac9460ff3e96"}, + "phoenix_template": {:hex, :phoenix_template, "1.0.4", "e2092c132f3b5e5b2d49c96695342eb36d0ed514c5b252a77048d5969330d639", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}], "hexpm", "2c0c81f0e5c6753faf5cca2f229c9709919aba34fab866d3bc05060c9c444206"}, + "plug": {:hex, :plug, "1.19.1", "09bac17ae7a001a68ae393658aa23c7e38782be5c5c00c80be82901262c394c0", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "560a0017a8f6d5d30146916862aaf9300b7280063651dd7e532b8be168511e62"}, + "plug_crypto": {:hex, :plug_crypto, "2.1.1", "19bda8184399cb24afa10be734f84a16ea0a2bc65054e23a62bb10f06bc89491", [:mix], [], "hexpm", "6470bce6ffe41c8bd497612ffde1a7e4af67f36a15eea5f921af71cf3e11247c"}, + "quickbeam": {:hex, :quickbeam, "0.10.9", "6e64722d524658a11a41933ca67eb5a33eb2d686a0542037fb58c7957a837cb2", [:mix], [{:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}, {:mint_web_socket, "~> 1.0", [hex: :mint_web_socket, repo: "hexpm", optional: false]}, {:nimble_pool, "~> 1.1", [hex: :nimble_pool, repo: "hexpm", optional: false]}, {:npm, "~> 0.6.0", [hex: :npm, repo: "hexpm", optional: false]}, {:oxc, "~> 0.11.0 or ~> 0.12.0", [hex: :oxc, repo: "hexpm", optional: false]}, {:zigler, "~> 0.15.2", [hex: :zigler, repo: "hexpm", optional: true]}, {:zigler_precompiled, "~> 0.1.2", [hex: :zigler_precompiled, repo: "hexpm", optional: false]}], "hexpm", "4f59082c96d360518836f46cbfcbb96ed2ed7db952601c74c0a6aea589f550db"}, + "reach": {:hex, :reach, "2.2.0", "bcf49e100f9d7c8f16ae90b85d1f2c562fd7ff140bac489dcd1e28261f40abc7", [:mix], [{:boxart, "~> 0.3.3", [hex: :boxart, repo: "hexpm", optional: true]}, {:ex_ast, "~> 0.10", [hex: :ex_ast, repo: "hexpm", optional: false]}, {:ex_dna, "~> 1.5", [hex: :ex_dna, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:libgraph, "~> 0.16.0", [hex: :libgraph, repo: "hexpm", optional: false]}, {:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: true]}, {:makeup_elixir, "~> 1.0", [hex: :makeup_elixir, repo: "hexpm", optional: true]}, {:makeup_js, "~> 0.1", [hex: :makeup_js, repo: "hexpm", optional: true]}, {:quickbeam, "~> 0.10", [hex: :quickbeam, repo: "hexpm", optional: true]}], "hexpm", "e42dd1dc56e287ff4e76fe1364d6121998a4b364d6d3a3311a519940f9d0d2f3"}, + "req": {:hex, :req, "0.5.17", "0096ddd5b0ed6f576a03dde4b158a0c727215b15d2795e59e0916c6971066ede", [:mix], [{:brotli, "~> 0.3.1", [hex: :brotli, repo: "hexpm", optional: true]}, {:ezstd, "~> 1.0", [hex: :ezstd, repo: "hexpm", optional: true]}, {:finch, "~> 0.17", [hex: :finch, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mime, "~> 2.0.6 or ~> 2.1", [hex: :mime, repo: "hexpm", optional: false]}, {:nimble_csv, "~> 1.0", [hex: :nimble_csv, repo: "hexpm", optional: true]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "0b8bc6ffdfebbc07968e59d3ff96d52f2202d0536f10fef4dc11dc02a2a43e39"}, + "rustler_precompiled": {:hex, :rustler_precompiled, "0.9.0", "3a052eda09f3d2436364645cc1f13279cf95db310eb0c17b0d8f25484b233aa0", [:mix], [{:rustler, "~> 0.23", [hex: :rustler, repo: "hexpm", optional: true]}], "hexpm", "471d97315bd3bf7b64623418b3693eedd8e47de3d1cb79a0ac8f9da7d770d94c"}, + "sourceror": {:hex, :sourceror, "1.12.0", "da354c5f35aad3cc1132f5d5b0d8437d865e2661c263260480bab51b5eedb437", [:mix], [], "hexpm", "755703683bd014ebcd5de9acc24b68fb874a660a568d1d63f8f98cd8a6ef9cd0"}, + "telemetry": {:hex, :telemetry, "1.4.1", "ab6de178e2b29b58e8256b92b382ea3f590a47152ca3651ea857a6cae05ac423", [:rebar3], [], "hexpm", "2172e05a27531d3d31dd9782841065c50dd5c3c7699d95266b2edd54c2dafa1c"}, + "telemetry_metrics": {:hex, :telemetry_metrics, "1.1.0", "5bd5f3b5637e0abea0426b947e3ce5dd304f8b3bc6617039e2b5a008adc02f8f", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "e7b79e8ddfde70adb6db8a6623d1778ec66401f366e9a8f5dd0955c56bc8ce67"}, + "telemetry_poller": {:hex, :telemetry_poller, "1.3.0", "d5c46420126b5ac2d72bc6580fb4f537d35e851cc0f8dbd571acf6d6e10f5ec7", [:rebar3], [{:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "51f18bed7128544a50f75897db9974436ea9bfba560420b646af27a9a9b35211"}, + "thousand_island": {:hex, :thousand_island, "1.4.3", "2158209580f633be38d43ec4e3ce0a01079592b9657afff9080d5d8ca149a3af", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "6e4ce09b0fd761a58594d02814d40f77daff460c48a7354a15ab353bb998ea0b"}, + "vize": {:hex, :vize, "0.10.0", "91b9f704f1736706fc36355bf1507870e5fe84ef403440ff74aa3c93e8b324ea", [:mix], [{:rustler, "~> 0.36", [hex: :rustler, repo: "hexpm", optional: true]}, {:rustler_precompiled, "~> 0.8", [hex: :rustler_precompiled, repo: "hexpm", optional: false]}], "hexpm", "9768d9ff63f42901201a403749fea3b724eb13aca0d6559d70ba69c8b4f08707"}, + "websock": {:hex, :websock, "0.5.3", "2f69a6ebe810328555b6fe5c831a851f485e303a7c8ce6c5f675abeb20ebdadc", [:mix], [], "hexpm", "6105453d7fac22c712ad66fab1d45abdf049868f253cf719b625151460b8b453"}, + "websock_adapter": {:hex, :websock_adapter, "0.5.9", "43dc3ba6d89ef5dec5b1d0a39698436a1e856d000d84bf31a3149862b01a287f", [:mix], [{:bandit, ">= 0.6.0", [hex: :bandit, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.6", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "5534d5c9adad3c18a0f58a9371220d75a803bf0b9a3d87e6fe072faaeed76a08"}, + "zigler_precompiled": {:hex, :zigler_precompiled, "0.1.4", "20a166584b94d860a983ef39c93cb0a9ce40c95c48555fc3cb6532c405558410", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: false]}, {:zigler, "~> 0.13", [hex: :zigler, repo: "hexpm", optional: true]}], "hexpm", "9cd6998a8ddf2f03ded1b6ebc9acb9bfa5e23b614f047583c76c01bf66c4640e"}, +} diff --git a/examples/solid/package.json b/examples/solid/package.json new file mode 100644 index 0000000..8c6a25d --- /dev/null +++ b/examples/solid/package.json @@ -0,0 +1,9 @@ +{ + "private": true, + "dependencies": { + "solid-js": "^1.9.0", + "phoenix": "^1.8.0", + "phoenix_html": "^4.2.1", + "phoenix_live_view": "^1.1.0" + } +} diff --git a/examples/solid/test/test_helper.exs b/examples/solid/test/test_helper.exs new file mode 100644 index 0000000..869559e --- /dev/null +++ b/examples/solid/test/test_helper.exs @@ -0,0 +1 @@ +ExUnit.start()