Packages

phoenix_kit

1.7.146
1.7.207 1.7.206 1.7.205 1.7.204 1.7.203 1.7.202 1.7.201 1.7.200 1.7.199 1.7.198 1.7.197 1.7.196 1.7.194 1.7.193 1.7.192 1.7.191 1.7.190 1.7.189 1.7.187 1.7.186 1.7.185 1.7.184 1.7.183 1.7.182 1.7.181 1.7.180 1.7.179 1.7.178 1.7.177 1.7.176 1.7.175 1.7.174 1.7.173 1.7.172 1.7.171 1.7.170 1.7.169 1.7.168 1.7.167 1.7.166 1.7.165 1.7.164 1.7.162 1.7.161 1.7.160 1.7.159 1.7.157 1.7.156 1.7.155 1.7.154 1.7.153 1.7.152 1.7.151 1.7.150 1.7.149 1.7.146 1.7.145 1.7.144 1.7.143 1.7.138 1.7.133 1.7.132 1.7.131 1.7.130 1.7.128 1.7.126 1.7.125 1.7.121 1.7.120 1.7.119 1.7.118 1.7.117 1.7.116 1.7.115 1.7.114 1.7.113 1.7.112 1.7.111 1.7.110 1.7.109 1.7.108 1.7.107 1.7.106 1.7.105 1.7.104 1.7.103 1.7.102 1.7.101 1.7.100 1.7.99 1.7.98 1.7.97 1.7.96 1.7.95 1.7.94 1.7.93 1.7.92 1.7.91 1.7.90 1.7.89 1.7.88 1.7.87 1.7.86 1.7.85 1.7.84 1.7.83 1.7.82 1.7.81 1.7.80 1.7.79 1.7.78 1.7.77 1.7.76 1.7.75 1.7.74 1.7.71 1.7.70 1.7.69 1.7.66 1.7.65 1.7.64 1.7.63 1.7.62 1.7.61 1.7.59 1.7.58 1.7.57 1.7.56 1.7.55 1.7.54 1.7.53 1.7.52 1.7.51 1.7.49 1.7.44 1.7.43 1.7.42 1.7.41 1.7.39 1.7.38 1.7.37 1.7.36 1.7.34 1.7.33 1.7.31 1.7.30 1.7.29 1.7.28 1.7.27 1.7.26 1.7.25 1.7.24 1.7.23 1.7.22 1.7.21 1.7.20 1.7.19 1.7.18 1.7.17 1.7.16 1.7.15 1.7.14 1.7.13 1.7.12 1.7.11 1.7.10 1.7.9 1.7.8 1.7.7 1.7.6 1.7.5 1.7.4 1.7.3 1.7.2 1.7.1 1.7.0 1.6.20 1.6.19 1.6.18 1.6.17 1.6.16 1.6.15 1.6.14 1.6.13 1.6.12 1.6.11 1.6.10 1.6.9 1.6.8 1.6.7 1.6.6 1.6.5 1.6.4 1.6.3 1.5.2 1.5.1 1.5.0 1.4.9 1.4.8 1.4.7 1.4.6 1.4.5 1.4.4 1.4.3 1.4.2 1.4.1 1.4.0 1.3.2 1.3.1 1.3.0 1.2.10 1.2.9 1.2.8 1.2.7 1.2.5 1.2.4 1.2.2 1.2.1 1.2.0 1.1.0 1.0.0

A foundation for building Elixir Phoenix apps — SaaS, social networks, ERP systems, marketplaces, and more

Current section

221 Versions

Jump to

Compare versions

11 files changed
+492 additions
-16 deletions
unknownCHANGELOG.md
File is too large to be displayed (100 KB limit).
  @@ -1,6 +1,6 @@
1 1 {<<"links">>,[{<<"GitHub">>,<<"https://github.com/BeamLabEU/phoenix_kit">>}]}.
2 2 {<<"name">>,<<"phoenix_kit">>}.
3 - {<<"version">>,<<"1.7.145">>}.
3 + {<<"version">>,<<"1.7.146">>}.
4 4 {<<"description">>,
5 5 <<65,32,102,111,117,110,100,97,116,105,111,110,32,102,111,114,32,98,117,105,
6 6 108,100,105,110,103,32,69,108,105,120,105,114,32,80,104,111,101,110,105,
  @@ -605,6 +605,7 @@
605 605 <<"lib/mix/tasks/phoenix_kit.release_check.ex">>,
606 606 <<"lib/mix/tasks/phoenix_kit.update.ex">>,
607 607 <<"lib/mix/tasks/phoenix_kit.gen.migration.ex">>,
608 + <<"lib/mix/tasks/compile.phoenix_kit_js_sources.ex">>,
608 609 <<"lib/mix/tasks/phoenix_kit.status.ex">>,
609 610 <<"lib/mix/tasks/compile.phoenix_kit_css_sources.ex">>,
610 611 <<"lib/mix/tasks/phoenix_kit.modernize_layouts.ex">>,
  @@ -0,0 +1,225 @@
1 + defmodule Mix.Tasks.Compile.PhoenixKitJsSources do
2 + @moduledoc """
3 + Mix compiler that bundles external-module JavaScript hooks for the host app.
4 +
5 + A LiveView JS hook must be present in the host's single `LiveSocket` at
6 + construction time — a nested LiveView cannot register one at runtime. Modules
7 + declare their prebuilt hook bundles via `js_sources/0` (see `PhoenixKit.Module`);
8 + this compiler discovers them and writes one aggregate file the host loads with a
9 + single `<script>` tag (added once by `mix phoenix_kit.install`).
10 +
11 + ## What it does (every compile)
12 +
13 + 1. Discovers external modules via `PhoenixKit.ModuleDiscovery`, collects their
14 + `js_sources/0` entries.
15 + 2. Resolves each bundle's absolute path via `:code.priv_dir/1` — so it works for
16 + Hex installs and path deps alike, with no `deps/<app>` path arithmetic.
17 + 3. Concatenates the bundles (each wrapped in an IIFE so their top-level scopes
18 + can't collide) into `priv/static/assets/vendor/phoenix_kit_modules.js`,
19 + followed by a merge that folds each bundle's `window.<Global>` into
20 + `window.PhoenixKitHooks` (which the host already spreads into `LiveSocket`).
21 + 4. Writes only when the content changed (diff-before-write) to avoid live-reload
22 + thrash.
23 +
24 + The output tag is **stable**: adding or removing a JS-bearing module only changes
25 + this file's content on the next compile — never the host's layout — so it stays
26 + zero-config like `css_sources/0`.
27 +
28 + ## Setup (one-time, by `mix phoenix_kit.install`)
29 +
30 + # mix.exs
31 + compilers: [:phoenix_kit_js_sources, :phoenix_live_view] ++ Mix.compilers()
32 +
33 + # root.html.heex, before app.js
34 + <script src={~p"/assets/vendor/phoenix_kit_modules.js"}></script>
35 +
36 + Failures are loud: a declared bundle that can't be resolved raises a compile
37 + error rather than silently shipping a chart with "unknown hook" console errors.
38 + """
39 +
40 + use Mix.Task.Compiler
41 +
42 + require Logger
43 +
44 + @output "priv/static/assets/vendor/phoenix_kit_modules.js"
45 +
46 + @impl true
47 + def run(_args) do
48 + # Ensure all dep applications are loaded so module discovery + priv_dir work.
49 + for dep <- Mix.Dep.cached() do
50 + Application.ensure_loaded(dep.app)
51 + end
52 +
53 + specs = collect_specs()
54 + content = build_content(specs)
55 +
56 + path = Path.join(File.cwd!(), @output)
57 + write_if_changed(path, content, length(specs))
58 +
59 + {:ok, []}
60 + end
61 +
62 + # ── Discovery ────────────────────────────────────────────────────
63 +
64 + # Returns a deterministic list of %{app, file, global, source} maps.
65 + defp collect_specs do
66 + PhoenixKit.ModuleDiscovery.discover_external_modules()
67 + |> Enum.flat_map(fn mod ->
68 + if Code.ensure_loaded?(mod) and function_exported?(mod, :js_sources, 0) do
69 + mod.js_sources()
70 + else
71 + []
72 + end
73 + end)
74 + |> Enum.map(&normalize_entry/1)
75 + # Deterministic order; dedupe identical (app, file) declarations.
76 + |> Enum.uniq_by(fn s -> {s.app, s.file} end)
77 + |> Enum.sort_by(fn s -> {to_string(s.app), s.file} end)
78 + |> tap(&check_unique_globals/1)
79 + |> Enum.map(&resolve_source/1)
80 + end
81 +
82 + # Two distinct bundles assigning the same window.<Global> would clobber each
83 + # other (last bundle wins; the merge dedupes the global), silently dropping the
84 + # earlier module's hooks. Fail loud instead.
85 + @doc false
86 + def check_unique_globals(specs) do
87 + dupes =
88 + specs
89 + |> Enum.group_by(& &1.global)
90 + |> Enum.filter(fn {_global, entries} -> length(entries) > 1 end)
91 +
92 + unless dupes == [] do
93 + detail =
94 + Enum.map_join(dupes, "\n", fn {global, entries} ->
95 + apps = Enum.map_join(entries, ", ", fn e -> "#{e.app}:#{e.file}" end)
96 + " window.#{global} <- #{apps}"
97 + end)
98 +
99 + Mix.raise("""
100 + Multiple js_sources/0 bundles declare the same window global, so the later
101 + bundle would clobber the earlier one's hooks:
102 +
103 + #{detail}
104 +
105 + Each module's bundle must assign a unique window.<Global>. Rename one.
106 + """)
107 + end
108 +
109 + :ok
110 + end
111 +
112 + @doc false
113 + def normalize_entry(%{app: app, file: file, global: global})
114 + when is_atom(app) and is_binary(file) and is_binary(global) do
115 + # `global` is emitted as `window.<global>` in the generated JS, so an
116 + # invalid identifier (e.g. "foo-bar") would produce broken JS — fail loud.
117 + unless Regex.match?(~r/^[A-Za-z_$][A-Za-z0-9_$]*$/, global) do
118 + Mix.raise("""
119 + Invalid js_sources/0 :global #{inspect(global)} — must be a valid JavaScript
120 + identifier (it is emitted as window.<global> and folded into PhoenixKitHooks).
121 + """)
122 + end
123 +
124 + %{app: app, file: file, global: global}
125 + end
126 +
127 + def normalize_entry(other) do
128 + Mix.raise("""
129 + Invalid js_sources/0 entry: #{inspect(other)}
130 +
131 + Each entry must be a map with :app (atom), :file (string, relative to the
132 + app's priv/), and :global (string, the window.<Name> the bundle assigns).
133 + """)
134 + end
135 +
136 + # Resolve the bundle's absolute path via the app's priv dir. Fail loud.
137 + defp resolve_source(%{app: app, file: file} = spec) do
138 + priv =
139 + case :code.priv_dir(app) do
140 + {:error, :bad_name} ->
141 + Mix.raise("""
142 + js_sources/0 references app #{inspect(app)}, but it isn't an available
143 + application. Add it as a dependency of the module declaring it.
144 + """)
145 +
146 + dir ->
147 + to_string(dir)
148 + end
149 +
150 + source = Path.join(priv, file)
151 +
152 + unless File.exists?(source) do
153 + Mix.raise("""
154 + js_sources/0 bundle not found: #{source}
155 +
156 + App #{inspect(app)} declared js_sources file #{inspect(file)} (resolved under
157 + its priv/), but no such file exists. The bundle must ship in the app's priv/.
158 + """)
159 + end
160 +
161 + Map.put(spec, :source, source)
162 + end
163 +
164 + # ── Content ──────────────────────────────────────────────────────
165 +
166 + @doc false
167 + def build_content([]) do
168 + """
169 + /* Auto-generated by PhoenixKit — do not edit manually.
170 + No external module declares js_sources/0; this file is intentionally empty. */
171 + """
172 + end
173 +
174 + def build_content(specs) do
175 + bundles =
176 + Enum.map_join(specs, "\n", fn %{app: app, source: source} ->
177 + body = File.read!(source)
178 +
179 + # Wrap each prebuilt bundle in an IIFE so its top-level declarations
180 + # can't collide with another bundle's. Bundles export via window.<Global>,
181 + # which survives the wrapper. Leading `;` guards against ASI hazards.
182 + """
183 + /* #{app} */
184 + ;(function(){
185 + #{body}
186 + })();
187 + """
188 + end)
189 +
190 + globals =
191 + specs
192 + |> Enum.map(& &1.global)
193 + |> Enum.uniq()
194 + |> Enum.map_join(",", fn g -> "window.#{g}||{}" end)
195 +
196 + # NOTE: `check_unique_globals/1` guarantees distinct window.<Global> names,
197 + # but this Object.assign is still last-write-wins on the HOOK NAMES inside
198 + # each bundle — and it folds over the core window.PhoenixKitHooks too. Two
199 + # modules exporting a same-named hook (or one shadowing a core hook) clobber
200 + # silently; we can't detect that without parsing the bundles. js_sources/0's
201 + # docs tell modules to namespace their hook names accordingly.
202 + """
203 + /* Auto-generated by PhoenixKit — do not edit manually.
204 + Concatenated module hook bundles (js_sources/0), loaded before app.js. */
205 + #{bundles}
206 + /* Fold each module's hooks into window.PhoenixKitHooks (spread into LiveSocket by app.js). */
207 + window.PhoenixKitHooks=Object.assign(window.PhoenixKitHooks||{},#{globals});
208 + """
209 + end
210 +
211 + defp write_if_changed(path, content, spec_count) do
212 + existing =
213 + case File.read(path) do
214 + {:ok, data} -> data
215 + _ -> nil
216 + end
217 +
218 + if existing != content do
219 + File.mkdir_p!(Path.dirname(path))
220 + File.write!(path, content)
221 +
222 + Mix.shell().info("[PhoenixKit] Updated #{@output} with #{spec_count} module hook bundle(s)")
223 + end
224 + end
225 + end
  @@ -339,7 +339,7 @@ if Code.ensure_loaded?(Igniter.Mix.Task) do
339 339
340 340 DOCUMENTATION
341 341 For more information, visit:
342 - https://hexdocs.pm/phoenix_kit
342 + https://phoenix-kit.hexdocs.pm
343 343 """)
344 344 end
  @@ -378,6 +378,10 @@ if Code.ensure_loaded?(Igniter.Mix.Task) do
378 378 # Ensure :phoenix_kit_css_sources compiler is registered in mix.exs
379 379 igniter = ensure_css_sources_compiler(igniter)
380 380
381 + # Ensure the external-module JS integration is wired (compiler + aggregate
382 + # script tag). Picks up the feature on hosts installed before it existed.
383 + igniter = JsIntegration.ensure_module_js_integration(igniter)
384 +
381 385 # Check if this is the first pass (config missing) or second pass (config exists)
382 386 config_status = Process.get(:phoenix_kit_config_status, :ok)
383 387
  @@ -816,7 +820,7 @@ if Code.ensure_loaded?(Igniter.Mix.Task) do
816 820
817 821 DOCUMENTATION
818 822 For more information, visit:
819 - https://hexdocs.pm/phoenix_kit
823 + https://phoenix-kit.hexdocs.pm
820 824 """)
821 825 end
822 826
  @@ -940,7 +944,8 @@ if Code.ensure_loaded?(Igniter.Mix.Task) do
940 944 defp ensure_css_sources_compiler(igniter) do
941 945 Igniter.Project.MixProject.update(igniter, :project, [:compilers], fn
942 946 nil ->
943 - {:ok, {:code, [:phoenix_kit_css_sources]}}
947 + # No :compilers key yet — keep the defaults by prepending.
948 + {:ok, {:code, quote(do: [:phoenix_kit_css_sources] ++ Mix.compilers())}}
944 949
945 950 zipper ->
946 951 case Igniter.Code.List.prepend_new_to_list(zipper, :phoenix_kit_css_sources) do
Loading more files…