@swmansion/popcorn 0.3.3 → 0.4.0-next.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/LICENSE +201 -0
  2. package/NOTICE +12 -0
  3. package/README.md +78 -2
  4. package/dist/beam.d.ts +10 -0
  5. package/dist/errors.d.ts +96 -39
  6. package/dist/etf.d.ts +38 -0
  7. package/dist/events.d.ts +82 -0
  8. package/dist/index.d.ts +7 -3
  9. package/dist/index.mjs +1287 -2
  10. package/dist/plugins/beam_tools/lib/popcorn/beam_tools/beam_patcher.ex +184 -0
  11. package/dist/plugins/beam_tools/lib/popcorn/beam_tools/cli.ex +74 -0
  12. package/dist/plugins/beam_tools/lib/popcorn/beam_tools/packager.ex +540 -0
  13. package/dist/plugins/beam_tools/mix.exs +16 -0
  14. package/dist/plugins/beam_tools/patches/kernel/prim_tty.erl +13 -0
  15. package/dist/plugins/beam_tools/patches/stdlib/beam_lib.erl +27 -0
  16. package/dist/plugins/esbuild.d.ts +10 -2
  17. package/dist/plugins/esbuild.mjs +39 -34
  18. package/dist/plugins/rollup.d.ts +10 -2
  19. package/dist/plugins/rollup.mjs +46 -25
  20. package/dist/plugins/shared.d.ts +54 -4
  21. package/dist/plugins/shared.mjs +207 -0
  22. package/dist/plugins/vite.d.ts +17 -2
  23. package/dist/plugins/vite.mjs +201 -75
  24. package/dist/popcorn.d.ts +237 -110
  25. package/dist/runtimes/core/beam.emu.mjs +141 -0
  26. package/dist/runtimes/core/beam.mjs +141 -0
  27. package/dist/runtimes/core/beam.wasm +0 -0
  28. package/dist/runtimes/core/manifest.json +1 -0
  29. package/dist/runtimes/crypto/beam.emu.mjs +520 -0
  30. package/dist/runtimes/crypto/beam.mjs +520 -0
  31. package/dist/runtimes/crypto/beam.wasm +0 -0
  32. package/dist/runtimes/crypto/manifest.json +1 -0
  33. package/dist/tar.d.ts +4 -0
  34. package/dist/types.d.ts +108 -63
  35. package/dist/utils.d.ts +7 -0
  36. package/dist/worker.d.ts +1 -0
  37. package/dist/worker.mjs +765 -0
  38. package/package.json +20 -28
  39. package/dist/AtomVM.mjs +0 -7992
  40. package/dist/AtomVM.wasm +0 -0
  41. package/dist/bridge.d.ts +0 -22
  42. package/dist/bridge.mjs +0 -66
  43. package/dist/errors.mjs +0 -55
  44. package/dist/iframe.d.ts +0 -1
  45. package/dist/iframe.mjs +0 -215
  46. package/dist/popcorn.mjs +0 -381
  47. package/dist/types.mjs +0 -25
@@ -0,0 +1,540 @@
1
+ defmodule Popcorn.BeamTools.Packager do
2
+ alias Popcorn.BeamTools.BeamPatcher
3
+
4
+ @static_nif_beams MapSet.new(["wasm.beam", "prim_tty.beam", "zstd.beam"])
5
+
6
+ # :beam_lib.significant_chunks/0 is undocumented.
7
+ @retained_chunks :beam_lib.significant_chunks() -- [~c"Type"]
8
+ # Applications that only work when the emulator was built with the matching
9
+ # native support. The runtime manifest declares what the build provides.
10
+ @app_capabilities %{
11
+ "asn1" => "crypto",
12
+ "crypto" => "crypto",
13
+ "public_key" => "crypto",
14
+ "ssl" => "crypto"
15
+ }
16
+
17
+ @boot_name "bin/vm.boot"
18
+
19
+ # Using `__DIR__` is safe – the plugin is compiled on user's machine
20
+ @patches_dir Path.expand("../../../patches", __DIR__)
21
+
22
+ # To run, Beam needs following apps:
23
+ # - kernel
24
+ # - stdlib (kernel dep)
25
+ #
26
+ # We also add elixir support out of the box and need:
27
+ # - compiler (elixir dep)
28
+ # - elixir
29
+ #
30
+ # All of them should be in elixir's transitive dependency closure
31
+ @base_apps ["elixir"]
32
+
33
+ @type options :: %{
34
+ root_dir: Path.t(),
35
+ entrypoint_app: String.t() | nil,
36
+ extra_apps: [String.t()],
37
+ out_dir: Path.t(),
38
+ runtimes_dir: Path.t(),
39
+ runtime_variant: String.t() | nil,
40
+ strip: boolean()
41
+ }
42
+
43
+ defp strip_tarball(path, out_dir) do
44
+ {:ok, entries} = :erl_tar.extract(to_charlist(path), [:memory])
45
+
46
+ stripped =
47
+ entries
48
+ |> Enum.sort_by(fn {name, _content} -> to_string(name) end)
49
+ |> Enum.map(&strip_beam/1)
50
+
51
+ output = Path.join(out_dir, Path.basename(path))
52
+ opts = [mtime: 0, atime: 0, ctime: 0, uid: 0, gid: 0]
53
+
54
+ :ok = :erl_tar.create(to_charlist(output), stripped, opts)
55
+ end
56
+
57
+ defp beam?(path), do: Path.extname(path) == ".beam"
58
+
59
+ # Reimplementation of :beam_lib.strip_files/2.
60
+ # Original also gzip compresses modules.
61
+ defp strip_beam({name, content}) do
62
+ if beam?(to_string(name)) do
63
+ {:ok, _module, chunks} = :beam_lib.all_chunks(content)
64
+ chunks_by_name = Map.new(chunks)
65
+
66
+ chunks =
67
+ Enum.flat_map(@retained_chunks, fn name ->
68
+ case Map.fetch(chunks_by_name, name) do
69
+ {:ok, data} -> [{name, data}]
70
+ :error -> []
71
+ end
72
+ end)
73
+
74
+ {:ok, stripped} = :beam_lib.build_module(chunks)
75
+
76
+ {name, stripped}
77
+ else
78
+ {name, content}
79
+ end
80
+ end
81
+
82
+ @spec run(options()) :: {:ok, map()} | {:error, map()}
83
+ def run(args) do
84
+ %{
85
+ root_dir: root_dir,
86
+ entrypoint_app: entrypoint_app,
87
+ extra_apps: extra_apps,
88
+ out_dir: out_dir,
89
+ runtimes_dir: runtimes_dir,
90
+ runtime_variant: runtime_variant,
91
+ strip: strip
92
+ } = args
93
+
94
+ toolchain = fetch_toolchain_info()
95
+
96
+ with {:ok, project_apps} <- root_dir |> project_build_dir() |> get_apps_info(),
97
+ {:ok, builtin_apps} <- get_builtin_apps(toolchain),
98
+ {:ok, apps_info} <- apps_to_pack(project_apps, builtin_apps, extra_apps, entrypoint_app),
99
+ variant = runtime_variant || required_runtime(apps_info),
100
+ {:ok, manifest} <- read_manifest(Path.join([runtimes_dir, variant, "manifest.json"])),
101
+ :ok <- check_otp_version(toolchain.otp, manifest.version),
102
+ :ok <- check_capabilities(apps_info, manifest.capabilities),
103
+ {:ok, boot_path} <- create_boot(out_dir, toolchain.otp_root, manifest.preloaded),
104
+ staged_apps = stage_apps(Path.join(out_dir, "staging"), apps_info),
105
+ :ok <- patch_apps(staged_apps) do
106
+ vm_version = manifest.version
107
+ toolchain = Map.take(toolchain, ~w(otp elixir)a)
108
+
109
+ File.mkdir_p!(out_dir)
110
+
111
+ packed_apps =
112
+ staged_apps
113
+ |> async_stream(fn {app, info} ->
114
+ version = Keyword.get(info.props, :vsn, ~c"") |> to_string()
115
+ tar_path = create_tarball(out_dir, app, info.ebin_dir)
116
+
117
+ {app, %{tar: tar_path, version: version}}
118
+ end)
119
+ |> Map.new()
120
+
121
+ diagnostics =
122
+ staged_apps
123
+ |> async_stream(fn {app, info} ->
124
+ case loaded_dynamic_nifs(app, info.ebin_dir) do
125
+ [] ->
126
+ []
127
+
128
+ beams ->
129
+ {:error, context} = err(:dynamic_nifs_loading, {app, beams})
130
+ [context]
131
+ end
132
+ end)
133
+ |> Enum.concat()
134
+
135
+ manifest_path = Path.join(out_dir, "manifest.json")
136
+
137
+ packed_tar_paths =
138
+ packed_apps
139
+ |> Map.values()
140
+ |> Enum.map(&Path.expand(Path.join(out_dir, &1.tar)))
141
+
142
+ tar_paths = maybe_strip_tarballs(packed_tar_paths, strip, out_dir)
143
+
144
+ manifest = %{
145
+ entrypoint: entrypoint_app,
146
+ apps: packed_apps,
147
+ notes: diagnostics,
148
+ toolchain: toolchain,
149
+ vm: %{boot: @boot_name, version: vm_version}
150
+ }
151
+
152
+ File.write!(manifest_path, encode_json(manifest))
153
+
154
+ result = %{
155
+ ok: true,
156
+ runtimeVariant: variant,
157
+ entrypoint: entrypoint_app,
158
+ manifestPath: Path.expand(manifest_path),
159
+ bootPath: Path.expand(boot_path),
160
+ tarPaths: tar_paths,
161
+ apps: packed_apps,
162
+ notes: diagnostics,
163
+ toolchain: toolchain
164
+ }
165
+
166
+ {:ok, result}
167
+ end
168
+ end
169
+
170
+ defp create_boot(out_dir, otp_root, runtime_preloaded) do
171
+ boot_path = Path.join([Path.dirname(otp_root), "bin", "no_dot_erlang.boot"])
172
+ {:script, id, commands} = boot_path |> File.read!() |> :erlang.binary_to_term()
173
+
174
+ preloaded =
175
+ Enum.flat_map(commands, fn
176
+ {:preLoaded, modules} -> Enum.map(modules, &to_string/1)
177
+ _ -> []
178
+ end)
179
+
180
+ case preloaded -- runtime_preloaded do
181
+ [] ->
182
+ boot = {:script, id, Enum.map(commands, &drop_app_versions/1)}
183
+ path = Path.join(out_dir, @boot_name)
184
+
185
+ File.mkdir_p!(Path.dirname(path))
186
+
187
+ File.write!(path, :erlang.term_to_binary(boot))
188
+
189
+ {:ok, path}
190
+
191
+ missing ->
192
+ err(:unsupported_boot, {boot_path, missing})
193
+ end
194
+ end
195
+
196
+ # `$ROOT/lib/kernel-10.5/ebin` -> `$ROOT/lib/kernel/ebin`
197
+ defp drop_app_versions({:path, dirs}) do
198
+ {:path, Enum.map(dirs, &drop_version_fragment/1)}
199
+ end
200
+
201
+ defp drop_app_versions(command), do: command
202
+
203
+ defp drop_version_fragment(dir) do
204
+ [root, "lib", app_version, "ebin"] = dir |> to_string() |> Path.split()
205
+ [app, _version] = String.split(app_version, "-", parts: 2)
206
+
207
+ to_charlist(Path.join([root, "lib", app, "ebin"]))
208
+ end
209
+
210
+ # Apps are copied out of the host installation so packing can modify them.
211
+ defp stage_apps(staging_dir, apps_info) do
212
+ apps_info
213
+ |> async_stream(fn {app, info} ->
214
+ ebin_dir = Path.join([staging_dir, app, "ebin"])
215
+
216
+ File.mkdir_p!(Path.dirname(ebin_dir))
217
+ File.cp_r!(info.ebin_dir, ebin_dir)
218
+
219
+ {app, %{info | ebin_dir: ebin_dir}}
220
+ end)
221
+ |> Map.new()
222
+ end
223
+
224
+ # we patch only selected modules from OTP, see patches/
225
+ defp patch_apps(staged_apps) do
226
+ [@patches_dir, "*", "*.erl"]
227
+ |> Path.join()
228
+ |> Path.wildcard()
229
+ |> reduce_while_ok(:ok, fn patch_path, :ok ->
230
+ app = patch_path |> Path.dirname() |> Path.basename()
231
+ name = Path.basename(patch_path, ".erl") <> ".beam"
232
+ ebin_dir = Map.fetch!(staged_apps, app).ebin_dir
233
+ beam_path = Path.join(ebin_dir, name)
234
+
235
+ BeamPatcher.patch_beam(beam_path, patch_path)
236
+ end)
237
+ |> case do
238
+ {:ok, _} -> :ok
239
+ {:error, _} = error -> error
240
+ end
241
+ end
242
+
243
+ defp async_stream(enumerable, fun) do
244
+ enumerable
245
+ |> Task.async_stream(fun, timeout: :infinity)
246
+ |> Enum.map(fn {:ok, result} -> result end)
247
+ end
248
+
249
+ defp maybe_strip_tarballs(paths, false, _out_dir), do: paths
250
+
251
+ defp maybe_strip_tarballs(paths, true, out_dir) do
252
+ Enum.map(paths, fn path ->
253
+ :ok = strip_tarball(path, out_dir)
254
+ Path.expand(Path.join(out_dir, Path.basename(path)))
255
+ end)
256
+ end
257
+
258
+ defp project_build_dir(root_dir) do
259
+ build_env = System.get_env("MIX_ENV", "dev")
260
+ build_lib_dir = Path.join([root_dir, "_build", build_env, "lib"])
261
+
262
+ build_lib_dir
263
+ end
264
+
265
+ defp get_apps_info(root_dir) do
266
+ extract_info = fn app_path ->
267
+ {:ok, [{:application, name, props}]} = :file.consult(app_path)
268
+ dir = Path.dirname(app_path)
269
+
270
+ {to_string(name), %{props: props, ebin_dir: dir, app_path: app_path}}
271
+ end
272
+
273
+ get_name = fn {name, _info} -> name end
274
+ get_app_path = fn {_name, info} -> info.app_path end
275
+ duplicated? = fn {_name, paths} -> match?([_, _ | _], paths) end
276
+
277
+ apps =
278
+ root_dir
279
+ |> Path.join("*/ebin/*.app")
280
+ |> Path.wildcard()
281
+ |> Enum.map(extract_info)
282
+
283
+ duplicates =
284
+ apps
285
+ |> Enum.group_by(get_name, get_app_path)
286
+ |> Enum.filter(duplicated?)
287
+ |> Map.new()
288
+
289
+ if Enum.empty?(duplicates) do
290
+ {:ok, Map.new(apps)}
291
+ else
292
+ err(:duplicated_apps, {root_dir, duplicates})
293
+ end
294
+ end
295
+
296
+ defp get_builtin_apps(toolchain) do
297
+ with {:ok, elixir_apps} <- get_apps_info(toolchain.elixir_root),
298
+ {:ok, otp_apps} <- get_apps_info(toolchain.otp_root) do
299
+ {:ok, Map.merge(elixir_apps, otp_apps)}
300
+ end
301
+ end
302
+
303
+ defp read_manifest(manifest_path) do
304
+ with {:ok, json} <- File.read(manifest_path),
305
+ {:ok, %{"vm" => vm}} <- decode_json(json),
306
+ %{"version" => version, "preloaded" => preloaded, "capabilities" => capabilities} <- vm do
307
+ {:ok, %{version: version, preloaded: preloaded, capabilities: capabilities}}
308
+ else
309
+ _ -> err(:bad_manifest, manifest_path)
310
+ end
311
+ end
312
+
313
+ defp apps_to_pack(project_apps, builtin_apps, extra_apps, entrypoint) do
314
+ all_apps_info = Map.merge(builtin_apps, project_apps)
315
+
316
+ gather_from_root = fn app, selected ->
317
+ gather_required_apps(all_apps_info, project_apps, app, selected)
318
+ end
319
+
320
+ with {:ok, roots} <- root_apps(all_apps_info, extra_apps, entrypoint),
321
+ {:ok, selected_apps} <- reduce_while_ok(roots, MapSet.new(), gather_from_root) do
322
+ all_apps_info
323
+ |> Map.filter(fn {app, _info} -> MapSet.member?(selected_apps, app) end)
324
+ |> Enum.sort()
325
+ |> then(&{:ok, &1})
326
+ end
327
+ end
328
+
329
+ defp required_runtime(apps_info) do
330
+ needs_crypto = Enum.any?(apps_info, fn {app, _info} -> @app_capabilities[app] == "crypto" end)
331
+ if needs_crypto, do: "crypto", else: "core"
332
+ end
333
+
334
+ defp check_capabilities(apps_info, capabilities) do
335
+ unsupported =
336
+ apps_info
337
+ |> Enum.flat_map(fn {app, _info} ->
338
+ case Map.fetch(@app_capabilities, app) do
339
+ {:ok, capability} -> [%{app: app, capability: capability}]
340
+ :error -> []
341
+ end
342
+ end)
343
+ |> Enum.reject(&Map.fetch!(capabilities, &1.capability))
344
+
345
+ if unsupported == [], do: :ok, else: err(:unsupported_apps, unsupported)
346
+ end
347
+
348
+ defp root_apps(all_apps_info, extra_apps, entrypoint) do
349
+ with {:ok, roots} <- entrypoint_roots(all_apps_info, entrypoint),
350
+ {:ok, extra} <- extra_roots(all_apps_info, extra_apps) do
351
+ {:ok, extra ++ roots}
352
+ end
353
+ end
354
+
355
+ defp entrypoint_roots(_all_apps_info, nil), do: {:ok, @base_apps}
356
+
357
+ # The entrypoint may be a builtin app, so a project with no code of its own
358
+ # doesn't need a stub application just to depend on one.
359
+ defp entrypoint_roots(all_apps_info, entrypoint)
360
+ when is_map_key(all_apps_info, entrypoint) do
361
+ {:ok, [entrypoint | @base_apps]}
362
+ end
363
+
364
+ defp entrypoint_roots(_all_apps_info, entrypoint),
365
+ do: err(:missing_entrypoint, entrypoint)
366
+
367
+ defp extra_roots(all_apps_info, extra_apps) do
368
+ case Enum.reject(extra_apps, &is_map_key(all_apps_info, &1)) do
369
+ [] -> {:ok, extra_apps}
370
+ missing -> err(:missing_extra_apps, Enum.sort(missing))
371
+ end
372
+ end
373
+
374
+ defp gather_required_apps(all_apps_info, project_apps, app, selected) do
375
+ if MapSet.member?(selected, app) do
376
+ {:ok, selected}
377
+ else
378
+ info = Map.fetch!(all_apps_info, app)
379
+ selected = MapSet.put(selected, app)
380
+
381
+ info.props
382
+ |> get_required_apps()
383
+ |> reduce_while_ok(selected, fn dep, acc ->
384
+ if Map.has_key?(all_apps_info, dep) do
385
+ gather_required_apps(all_apps_info, project_apps, dep, acc)
386
+ else
387
+ project_app_names = Enum.sort(Map.keys(project_apps))
388
+ err(:missing_dep, {app, dep, project_app_names})
389
+ end
390
+ end)
391
+ end
392
+ end
393
+
394
+ defp reduce_while_ok(enumerable, acc, f) do
395
+ Enum.reduce_while(enumerable, {:ok, acc}, fn value, {:ok, acc} ->
396
+ case f.(value, acc) do
397
+ :ok -> {:cont, {:ok, acc}}
398
+ {:ok, new_acc} -> {:cont, {:ok, new_acc}}
399
+ {:error, _} = error -> {:halt, error}
400
+ end
401
+ end)
402
+ end
403
+
404
+ defp create_tarball(outdir, app, ebin_dir) do
405
+ tar = "lib/#{app}.tar"
406
+ tar_path = Path.join(outdir, tar)
407
+ tar_path_c = to_charlist(tar_path)
408
+ arc_name = ~c"lib/#{app}/ebin"
409
+ ebin_dir_c = to_charlist(ebin_dir)
410
+
411
+ File.mkdir_p!(Path.dirname(tar_path))
412
+ :ok = :erl_tar.create(tar_path_c, [{arc_name, ebin_dir_c}], [])
413
+
414
+ tar
415
+ end
416
+
417
+ defp loaded_dynamic_nifs(app, ebin_dir) do
418
+ Path.join(ebin_dir, "*.beam")
419
+ |> Path.wildcard()
420
+ |> Enum.filter(&imports_load_nif?/1)
421
+ |> Enum.map(&Path.basename/1)
422
+ |> Enum.reject(&MapSet.member?(@static_nif_beams, &1))
423
+ end
424
+
425
+ defp imports_load_nif?(beam_path) do
426
+ case :beam_lib.chunks(to_charlist(beam_path), [:imports]) do
427
+ {:ok, {_mod, [imports: imports]}} -> {:erlang, :load_nif, 2} in imports
428
+ _ -> false
429
+ end
430
+ end
431
+
432
+ defp fetch_toolchain_info() do
433
+ %{
434
+ otp: host_otp_version(),
435
+ elixir: System.version(),
436
+ otp_root: Path.join(to_string(:code.root_dir()), "lib"),
437
+ elixir_root: :elixir |> :code.lib_dir() |> to_string() |> Path.dirname()
438
+ }
439
+ end
440
+
441
+ defp check_otp_version(host_version, runtime_version) do
442
+ # host: computer this runs on
443
+ # runtime: vm compiled to wasm
444
+ host = otp_version(host_version)
445
+ runtime = otp_version(runtime_version)
446
+
447
+ [host_major | _] = host
448
+ [runtime_major | _] = runtime
449
+
450
+ compatible = runtime_major - 2 <= host_major and version_lte?(host, runtime)
451
+
452
+ if compatible do
453
+ :ok
454
+ else
455
+ err(:unsupported_otp, {host_version, runtime_version})
456
+ end
457
+ end
458
+
459
+ defp host_otp_version do
460
+ path =
461
+ Path.join([to_string(:code.root_dir()), "releases", System.otp_release(), "OTP_VERSION"])
462
+
463
+ path
464
+ |> File.read!()
465
+ |> String.trim()
466
+ end
467
+
468
+ defp otp_version(version) do
469
+ version
470
+ |> to_string()
471
+ |> String.split("-", parts: 2)
472
+ |> hd()
473
+ |> String.split(".")
474
+ |> Enum.map(&String.to_integer/1)
475
+ end
476
+
477
+ defp version_lte?(left, right) do
478
+ width = max(length(left), length(right))
479
+ pad = fn version -> version ++ List.duplicate(0, width - length(version)) end
480
+ pad.(left) <= pad.(right)
481
+ end
482
+
483
+ defp get_required_apps(props) do
484
+ prop = &Keyword.get/3
485
+ optional = prop.(props, :optional_applications, []) |> MapSet.new()
486
+ applications = prop.(props, :applications, []) |> MapSet.new()
487
+ included = prop.(props, :included_applications, []) |> MapSet.new()
488
+
489
+ MapSet.union(applications, included)
490
+ |> MapSet.difference(optional)
491
+ |> Enum.map(&to_string/1)
492
+ end
493
+
494
+ defp err(:missing_entrypoint, app) do
495
+ {:error, %{code: "missing_entrypoint", app: app}}
496
+ end
497
+
498
+ defp err(:missing_extra_apps, apps) do
499
+ {:error, %{code: "missing_extra_apps", apps: apps}}
500
+ end
501
+
502
+ defp err(:bad_manifest, path) do
503
+ {:error, %{code: "bad_manifest", path: path}}
504
+ end
505
+
506
+ defp err(:unsupported_boot, {boot_path, missing_preloaded}) do
507
+ missing = Enum.sort(missing_preloaded)
508
+ {:error, %{code: "unsupported_boot", boot: boot_path, missing_preloaded: missing}}
509
+ end
510
+
511
+ defp err(:unsupported_otp, {host, runtime}) do
512
+ {:error, %{code: "unsupported_otp", host: host, runtime: runtime}}
513
+ end
514
+
515
+ defp err(:duplicated_apps, {root_dir, duplicates}) do
516
+ {:error, %{code: "duplicated_apps", root_dir: root_dir, duplicates: duplicates}}
517
+ end
518
+
519
+ defp err(:unsupported_apps, unsupported) do
520
+ {:error, %{code: "unsupported_apps", apps: Enum.sort_by(unsupported, & &1.app)}}
521
+ end
522
+
523
+ defp err(:missing_dep, {app, dep, project_apps}) do
524
+ {:error, %{code: "missing_dep", app: app, dep: dep, available_apps: project_apps}}
525
+ end
526
+
527
+ defp err(:dynamic_nifs_loading, {app, beams}) do
528
+ {:error, %{code: "dynamic_nifs_loading", app: app, beams: beams}}
529
+ end
530
+
531
+ defp encode_json(term) do
532
+ term |> :json.encode() |> IO.iodata_to_binary()
533
+ end
534
+
535
+ defp decode_json(json) do
536
+ {:ok, :json.decode(json)}
537
+ rescue
538
+ _ -> :error
539
+ end
540
+ end
@@ -0,0 +1,16 @@
1
+ defmodule Popcorn.BeamTools.MixProject do
2
+ use Mix.Project
3
+
4
+ def project do
5
+ [
6
+ app: :popcorn_beam_tools,
7
+ version: "0.1.0",
8
+ elixir: "~> 1.17",
9
+ deps: []
10
+ ]
11
+ end
12
+
13
+ def application do
14
+ []
15
+ end
16
+ end
@@ -0,0 +1,13 @@
1
+ %% The emscripten target has no terminal to query, so terminal capability
2
+ %% detection is skipped and the state is returned unchanged.
3
+ -module(prim_tty).
4
+ -compile([{popcorn_patch_private, [{init, 2}]}]).
5
+ -export([init/2]).
6
+
7
+ init(State, {unix, _} = OsType) ->
8
+ case erlang:system_info(system_architecture) of
9
+ "wasm32-unknown-emscripten" -> State;
10
+ _ -> popcorn_module:init(State, OsType)
11
+ end;
12
+ init(State, OsType) ->
13
+ popcorn_module:init(State, OsType).
@@ -0,0 +1,27 @@
1
+ -module(beam_lib).
2
+ -export([checksum/1]).
3
+
4
+ checksum(File0) ->
5
+ case catch checksum_1(File0) of
6
+ {ok, _} = Result -> Result;
7
+ Error -> Error
8
+ end.
9
+
10
+ checksum_1(File0) ->
11
+ File = popcorn_module:beam_filename(File0),
12
+ Beam = read_beam(File),
13
+ {ok, Module, _} = popcorn_module:scan_beam(Beam, []),
14
+ case erts_internal:beamfile_module_checksum(Beam) of
15
+ Digest when is_binary(Digest) ->
16
+ {ok, {Module, {xxh3_128, Digest}}};
17
+ undefined ->
18
+ popcorn_module:error({not_a_beam_file, File})
19
+ end.
20
+
21
+ read_beam(Beam) when is_binary(Beam) ->
22
+ popcorn_module:maybe_uncompress(Beam);
23
+ read_beam(File) ->
24
+ case file:read_file(File) of
25
+ {ok, Beam} -> popcorn_module:maybe_uncompress(Beam);
26
+ Error -> popcorn_module:file_error(File, Error)
27
+ end.
@@ -1,3 +1,11 @@
1
1
  import type { Plugin } from "esbuild";
2
- import { type PopcornPluginOptions } from "./shared";
3
- export declare function popcorn(options: PopcornPluginOptions): Plugin;
2
+ import { type Options } from "./shared";
3
+ /**
4
+ * Copies the worker, VM runtime, and `otp/` assets into the output directory after a successful esbuild build.
5
+ *
6
+ * Requires `format: "esm"` and either `outdir` or `outfile`.
7
+ * If the application bundle is elsewhere, set `PopcornOpts.workerUrl` to the copied `worker.mjs`.
8
+ *
9
+ * @see {@link Options} for application packaging and server requirements.
10
+ */
11
+ export declare function popcorn(options: Options): Plugin;