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
Current section
221 Versions
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…