@swmansion/popcorn 0.4.0-next.0 → 0.4.0-next.3

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.
package/README.md CHANGED
@@ -3,17 +3,22 @@
3
3
  Run Elixir in a browser with OTP/BEAM compiled to WebAssembly.
4
4
  This prerelease replaces the AtomVM runtime used by Popcorn 0.3.x.
5
5
 
6
+ Start with the
7
+ [versioned Popcorn guide](https://popcorn.hexdocs.pm/0.4.0-next.3/introduction.html).
8
+ It includes a complete Mix and Vite tutorial.
9
+
6
10
  ## Install
7
11
 
8
12
  ```sh
9
13
  npm install @swmansion/popcorn@next
10
14
  ```
11
15
 
12
- Add `{:popcorn, "0.4.0-next.0"}` to your Elixir application's dependencies.
13
- Run `mix deps.get` and `mix compile` before building the JavaScript application.
14
- The bundler plugins invoke Mix locally to package application and standard-library code.
16
+ Add `{:popcorn, "0.4.0-next.3"}` to your Elixir application's dependencies.
17
+ Run `mix deps.get` before building the JavaScript application. The bundler
18
+ plugins invoke Mix locally to compile and package application and
19
+ standard-library code.
15
20
 
16
- Use the toolchain pinned in [popcorn/mise.toml](https://github.com/software-mansion/popcorn/blob/v0.4.0-next.0/popcorn/mise.toml).
21
+ Use the toolchain pinned in [mise.toml](https://github.com/software-mansion/popcorn/blob/v0.4.0-next.3/mise.toml).
17
22
  The packager checks host OTP compatibility against the selected runtime's manifest.
18
23
 
19
24
  ## Configure Vite
@@ -26,13 +31,13 @@ export default defineConfig({
26
31
  plugins: [
27
32
  popcorn({
28
33
  rootDir: "../",
29
- app: "my_app",
30
34
  }),
31
35
  ],
32
36
  });
33
37
  ```
34
38
 
35
- Set `rootDir` to the compiled Mix project's directory and `app` to its OTP application name.
39
+ Set `rootDir` to the Mix project directory. The current Mix application is the
40
+ default entrypoint. Set `app` to select another OTP application explicitly.
36
41
  Use `app: null` to package the base runtime without starting an application.
37
42
 
38
43
  The npm package contains two variants:
@@ -54,7 +59,7 @@ Use ESM output with those bundlers.
54
59
  ```ts
55
60
  import { Popcorn } from "@swmansion/popcorn";
56
61
 
57
- const result = await Popcorn.init({});
62
+ const result = await Popcorn.init();
58
63
  if (!result.ok) throw result.error;
59
64
 
60
65
  const vm = result.data;
@@ -62,6 +67,10 @@ const vm = result.data;
62
67
  vm.deinit();
63
68
  ```
64
69
 
70
+ `Popcorn.init()` waits for the OTP application tree to start. Use
71
+ `popcorn.genserver.call()` to call a supervised GenServer from JavaScript.
72
+ Use `popcorn.onEvent()` to receive events from BEAM processes.
73
+
65
74
  ## Serve in production
66
75
 
67
76
  Serve over HTTPS or localhost. Set these headers on the application and runtime responses:
@@ -73,7 +82,7 @@ Cross-Origin-Embedder-Policy: require-corp
73
82
 
74
83
  Vite sets these headers for development and preview. Configure your production server separately.
75
84
  Serve `.wasm` as `application/wasm`. Serve compressed `.tar.gz` files with `Content-Encoding: gzip` for `.tar` requests.
76
- The package also emits uncompressed tar files. Brotli variants require `brotli: true` and the corresponding server configuration.
85
+ The package also emits uncompressed tar files and Brotli variants. Brotli uses standard effort by default. Set `brotliEffort: "max"` for maximum compression in release builds.
77
86
 
78
87
  The JavaScript bridge currently requires a Content Security Policy that permits `unsafe-eval`.
79
- See the [versioned Elixir API](https://popcorn.hexdocs.pm/0.4.0-next.0/) for interoperability details.
88
+ See the [versioned Elixir API](https://popcorn.hexdocs.pm/0.4.0-next.3/) for interoperability details.
package/dist/events.d.ts CHANGED
@@ -3,7 +3,7 @@ import { type Mapper } from "./etf";
3
3
  import type { AnyValue, BeamBootOptions, BeamEvent, BeamSendPayload, BeamTarget } from "./types";
4
4
  type BootEvent = {
5
5
  type: "popcorn:boot";
6
- payload: Pick<BeamBootOptions, "emulatorArgs" | "extraArgs" | "env" | "ttySize"> & {
6
+ payload: Pick<BeamBootOptions, "emulatorArgs" | "extraArgs" | "env" | "ttySize" | "noshell"> & {
7
7
  otpAssetsRoot?: string;
8
8
  };
9
9
  };
@@ -24,12 +24,12 @@ type SendEvent = {
24
24
  type: "popcorn:send";
25
25
  payload: SendRequestPayload;
26
26
  };
27
- export type RunJsReplyPayload = {
27
+ export type BridgeReplyPayload = {
28
28
  message: BeamSendPayload;
29
29
  };
30
- type RunJsReplyEvent = {
31
- type: "popcorn:run-js-reply";
32
- payload: RunJsReplyPayload;
30
+ type BridgeReplyEvent = {
31
+ type: "popcorn:bridge-reply";
32
+ payload: BridgeReplyPayload;
33
33
  };
34
34
  export type SendRequestPayload = {
35
35
  id: string;
@@ -60,7 +60,7 @@ type BootEndEvent = {
60
60
  type: "popcorn:boot-fail";
61
61
  payload: SerializedError;
62
62
  };
63
- export type MainToVmEvent = BootEvent | SendEvent | RunJsReplyEvent | StdinEvent | TtyResizeEvent;
63
+ export type MainToVmEvent = BootEvent | SendEvent | BridgeReplyEvent | StdinEvent | TtyResizeEvent;
64
64
  /**
65
65
  * A decoded BEAM message payload.
66
66
  *
@@ -73,7 +73,7 @@ export declare function readMainEvent(value: unknown): MainToVmEvent;
73
73
  export declare function readWorkerEvent(value: unknown): VmToMainEvent;
74
74
  export declare function serializeSendPayload(target: BeamTarget, payload: AnyValue, mapper?: Mapper): Result<BeamSendPayload, "bridge:unserializable">;
75
75
  export declare function deserializeBridgeMessage(text: string): Extract<BeamEvent, {
76
- type: "otp:message" | "otp:error" | "otp:run_js";
76
+ type: "otp:message" | "otp:error" | "otp:run_js" | "otp:await-ready";
77
77
  }> | null;
78
78
  /** Usable only from main context. */
79
79
  export declare function toVm(worker: Worker, event: MainToVmEvent, transfer?: Transferable[]): void;
package/dist/index.mjs CHANGED
@@ -480,6 +480,7 @@ function readWorkerEvent(value) {
480
480
  case "otp:error":
481
481
  case "otp:message":
482
482
  case "otp:run_js":
483
+ case "otp:await-ready":
483
484
  case "otp:tracked-value-delete":
484
485
  case "popcorn:boot-vm-ready":
485
486
  case "popcorn:boot-end":
@@ -553,6 +554,7 @@ class Popcorn {
553
554
  eventHandlers = new Set();
554
555
  pendingSends = new Map();
555
556
  pendingCalls = new Map();
557
+ readyWaiters = [];
556
558
  callSeq = 0;
557
559
  trackedValues = new Map();
558
560
  trackedKeySeq = 0;
@@ -585,6 +587,9 @@ class Popcorn {
585
587
  this.vmReady = true;
586
588
  this.runJs(data.payload);
587
589
  return;
590
+ case "otp:await-ready":
591
+ this.handleReadyRequest(data.payload.replyTo);
592
+ return;
588
593
  case "otp:tracked-value-delete":
589
594
  this.deleteTrackedValue(data.payload);
590
595
  return;
@@ -614,7 +619,7 @@ class Popcorn {
614
619
  *
615
620
  * Call {@link boot} to start the VM.
616
621
  **/
617
- constructor(opts) {
622
+ constructor(opts = {}) {
618
623
  const ttySize = opts.tty?.size ?? DEFAULT_TTY_SIZE;
619
624
  check(isValidTtySize(ttySize));
620
625
  check(opts.beam?.otpAssetsRoot === undefined ||
@@ -647,7 +652,7 @@ class Popcorn {
647
652
  *
648
653
  * @returns Ok tuple or `runtime:eval-unavailable` if the page blocks JavaScript evaluation.
649
654
  */
650
- static async init(opts) {
655
+ static async init(opts = {}) {
651
656
  if (!canEval()) {
652
657
  return { ok: false, error: err$1("runtime:eval-unavailable", {}) };
653
658
  }
@@ -693,6 +698,7 @@ class Popcorn {
693
698
  this.Pid = createPidClass();
694
699
  this.io = createIoState();
695
700
  this.output = resolveOutputHandlers(this.opts);
701
+ check(this.readyWaiters.length === 0);
696
702
  this.state = { status: "booting" };
697
703
  return await new Promise((resolve) => {
698
704
  const timeoutsMs = { ...DEFAULT_TIMEOUTS_MS, ...this.opts.timeoutsMs };
@@ -724,6 +730,7 @@ class Popcorn {
724
730
  break;
725
731
  case "popcorn:boot-end":
726
732
  this.state = { status: "booted" };
733
+ this.flushReadyWaiters();
727
734
  settle({ ok: true, data: this });
728
735
  break;
729
736
  case "popcorn:boot-fail": {
@@ -740,7 +747,11 @@ class Popcorn {
740
747
  this.vmWorker.addEventListener("message", onBootMessage);
741
748
  toVm(this.vmWorker, {
742
749
  type: "popcorn:boot",
743
- payload: { ...this.opts.beam, ttySize: this.ttySize },
750
+ payload: {
751
+ ...this.opts.beam,
752
+ ttySize: this.ttySize,
753
+ noshell: this.opts.tty === undefined,
754
+ },
744
755
  });
745
756
  });
746
757
  }
@@ -888,6 +899,7 @@ class Popcorn {
888
899
  }
889
900
  this.state = { status: "closed", error };
890
901
  this.vmReady = false;
902
+ this.readyWaiters.length = 0;
891
903
  for (const resolve of this.pendingSends.values()) {
892
904
  resolve({ ok: false, error });
893
905
  }
@@ -1054,7 +1066,7 @@ class Popcorn {
1054
1066
  for (const { key, value, cleanup } of tracked) {
1055
1067
  this.trackedValues.set(key, { value, cleanup });
1056
1068
  }
1057
- this.sendRunJsReply(command.data);
1069
+ this.sendBridgeReply(command.data);
1058
1070
  return;
1059
1071
  }
1060
1072
  const failure = serializeSendPayload(target, {
@@ -1062,15 +1074,33 @@ class Popcorn {
1062
1074
  error: { unserializable: command.error.data.reason },
1063
1075
  });
1064
1076
  check(failure.ok);
1065
- this.sendRunJsReply(failure.data);
1077
+ this.sendBridgeReply(failure.data);
1078
+ }
1079
+ handleReadyRequest(replyTo) {
1080
+ if (this.state.status === "booted") {
1081
+ this.sendReadyReply(replyTo);
1082
+ return;
1083
+ }
1084
+ check(this.state.status === "booting");
1085
+ this.readyWaiters.push(replyTo);
1086
+ }
1087
+ flushReadyWaiters() {
1088
+ for (const replyTo of this.readyWaiters)
1089
+ this.sendReadyReply(replyTo);
1090
+ this.readyWaiters.length = 0;
1091
+ }
1092
+ sendReadyReply(replyTo) {
1093
+ const message = serializeSendPayload({ pid: replyTo }, true);
1094
+ check(message.ok);
1095
+ this.sendBridgeReply(message.data);
1066
1096
  }
1067
1097
  asRef(value) {
1068
1098
  if (value instanceof this.TrackedValue)
1069
1099
  return value;
1070
1100
  return new this.TrackedValue(value);
1071
1101
  }
1072
- sendRunJsReply(message) {
1073
- toVm(this.vmWorker, { type: "popcorn:run-js-reply", payload: { message } }, [message.etf.buffer]);
1102
+ sendBridgeReply(message) {
1103
+ toVm(this.vmWorker, { type: "popcorn:bridge-reply", payload: { message } }, [message.etf.buffer]);
1074
1104
  }
1075
1105
  jsWithCurrentEnv(code) {
1076
1106
  const make = new Function("TrackedValue", `"use strict"; return (${code});`);
@@ -1,11 +1,10 @@
1
1
  import { mkdir, cp, rm } from 'node:fs/promises';
2
2
  import { dirname, resolve } from 'node:path';
3
- import { p as popcorn$1, c as copyRuntime } from './shared.mjs';
3
+ import { p as popcorn$1 } from './shared.mjs';
4
4
  import 'node:child_process';
5
5
  import 'node:os';
6
6
  import 'node:util';
7
7
  import 'node:url';
8
- import 'node:zlib';
9
8
 
10
9
  /**
11
10
  * Copies the worker, VM runtime, and `otp/` assets into the output directory after a successful esbuild build.
@@ -36,12 +35,7 @@ function popcorn(options) {
36
35
  const prepared = await popcorn$1(options);
37
36
  try {
38
37
  await mkdir(outDir, { recursive: true });
39
- await Promise.all([
40
- copyRuntime(outDir, prepared.runtimeVariant),
41
- cp(resolve(prepared.dir, "otp"), resolve(outDir, "otp"), {
42
- recursive: true,
43
- }),
44
- ]);
38
+ await cp(prepared.dir, outDir, { recursive: true });
45
39
  }
46
40
  finally {
47
41
  await rm(prepared.dir, { recursive: true, force: true });
@@ -1,11 +1,10 @@
1
1
  import { mkdir, cp, rm } from 'node:fs/promises';
2
- import { resolve, dirname } from 'node:path';
3
- import { p as popcorn$1, c as copyRuntime } from './shared.mjs';
2
+ import { dirname, resolve } from 'node:path';
3
+ import { p as popcorn$1 } from './shared.mjs';
4
4
  import 'node:child_process';
5
5
  import 'node:os';
6
6
  import 'node:util';
7
7
  import 'node:url';
8
- import 'node:zlib';
9
8
 
10
9
  /**
11
10
  * Copies the worker, VM runtime, and `otp/` assets into the output directory after Rollup writes the bundle.
@@ -34,12 +33,7 @@ function popcorn(options) {
34
33
  const prepared = await popcorn$1(options);
35
34
  try {
36
35
  await mkdir(outDir, { recursive: true });
37
- await Promise.all([
38
- copyRuntime(outDir, prepared.runtimeVariant),
39
- cp(resolve(prepared.dir, "otp"), resolve(outDir, "otp"), {
40
- recursive: true,
41
- }),
42
- ]);
36
+ await cp(prepared.dir, outDir, { recursive: true });
43
37
  }
44
38
  finally {
45
39
  await rm(prepared.dir, { recursive: true, force: true });
@@ -1,57 +1,37 @@
1
+ type RuntimeVariant = "core" | "crypto";
1
2
  /**
2
3
  * Shared options for the Vite, Rollup, and esbuild plugins.
3
4
  *
4
- * Compile the project before the plugin runs.
5
- * The plugins use the local `mix` executable to package applications and dependencies.
6
- *
7
- * On the production server, set `Cross-Origin-Opener-Policy: same-origin` and `Cross-Origin-Embedder-Policy: require-corp`.
8
- * Serve compressed variants with the matching `Content-Encoding` header.
5
+ * The plugins invoke `mix popcorn.cook` in the project directory, which
6
+ * compiles the project before packaging it.
9
7
  */
10
- type RuntimeVariant = "core" | "crypto";
11
8
  export type Options = {
12
- /**
13
- * Runtime variant override.
14
- *
15
- * By default, the plugin selects `crypto` when an application requires it and `core` otherwise.
16
- */
9
+ /** Runtime variant override. */
17
10
  runtimeVariant?: RuntimeVariant;
18
- /**
19
- * Mix project directory.
20
- *
21
- * Reads compiled apps from `_build/$MIX_ENV/lib`, with `MIX_ENV` defaulting to `dev`.
22
- */
11
+ /** Mix project directory. */
23
12
  rootDir: string;
24
13
  /**
25
14
  * OTP application to start after VM boot.
26
- *
27
- * Use `null` to boot without an entrypoint application.
28
- */
29
- app: string | null;
30
- /**
31
- * Additional applications to package with their dependencies.
32
- *
33
- * Does not start them automatically. Defaults to `[]`.
15
+ * Defaults to the current Mix application.
34
16
  */
17
+ app?: string | null;
18
+ /** Additional applications to package with their dependencies. */
35
19
  extraApps?: string[];
36
- /**
37
- * Adds Brotli tarball variants beside the gzip and uncompressed files.
38
- *
39
- * Defaults to `false`.
40
- */
41
- brotli?: boolean;
42
- /**
43
- * Removes nonessential BEAM chunks.
44
- *
45
- * Experimental. Defaults to `true`.
46
- */
20
+ /** Brotli compression effort. Defaults to `standard`. */
21
+ brotliEffort?: "standard" | "max";
22
+ /** Removes nonessential BEAM chunks. Defaults to `true`. */
47
23
  strip?: boolean;
24
+ /** Controls removal of unreachable modules and functions. Defaults to `none`. */
25
+ treeshake?: TreeshakeOptions;
26
+ };
27
+ export type TreeshakeOptions = {
28
+ preservedApps?: string[];
48
29
  };
49
30
  export type Prepared = {
50
31
  dir: string;
51
32
  runtimeVariant: RuntimeVariant;
52
33
  notes: unknown[];
53
34
  };
54
- export declare function popcorn(opts: Options): Promise<Prepared>;
35
+ export declare function popcorn(options: Options): Promise<Prepared>;
55
36
  export declare function runtimeDirectory(variant: RuntimeVariant): string;
56
- export declare function copyRuntime(targetDir: string, variant: RuntimeVariant): Promise<void>;
57
37
  export {};
@@ -1,207 +1,61 @@
1
1
  import { execFile } from 'node:child_process';
2
- import { mkdtemp, rm, mkdir, writeFile, copyFile, readFile } from 'node:fs/promises';
3
- import { resolve, dirname, normalize, basename } from 'node:path';
2
+ import { mkdtemp, readFile, rm } from 'node:fs/promises';
3
+ import { resolve, dirname } from 'node:path';
4
4
  import { tmpdir } from 'node:os';
5
5
  import { promisify } from 'node:util';
6
6
  import { fileURLToPath } from 'node:url';
7
- import { brotliCompress, gzip, constants } from 'node:zlib';
8
7
 
9
8
  const execFileAsync = promisify(execFile);
10
- const brotliCompressAsync = promisify(brotliCompress);
11
- const gzipAsync = promisify(gzip);
12
- async function popcorn(opts) {
13
- const useBrotli = opts.brotli ?? false;
14
- const strip = opts.strip ?? true;
15
- const assetVariants = [
16
- "uncompressed",
17
- "gzip",
18
- useBrotli && "brotli",
19
- ];
20
- const preparedDir = await mkdtemp(p `${tmpdir()}/popcorn-otp-`);
9
+ async function popcorn(options) {
10
+ const dir = await mkdtemp(`${tmpdir()}/popcorn-otp-`);
11
+ const args = ["popcorn.cook", "--out-dir", dir];
12
+ if (options.app === null)
13
+ args.push("--no-app");
14
+ else if (options.app !== undefined)
15
+ args.push("--app", options.app);
16
+ for (const app of options.extraApps ?? [])
17
+ args.push("--extra-app", app);
18
+ if (options.runtimeVariant !== undefined) {
19
+ args.push("--runtime-variant", options.runtimeVariant);
20
+ }
21
+ if (options.brotliEffort !== undefined) {
22
+ args.push("--brotli-effort", options.brotliEffort);
23
+ }
24
+ if (options.strip === false)
25
+ args.push("--no-strip");
26
+ if (options.treeshake !== undefined) {
27
+ args.push("--treeshake");
28
+ for (const app of options.treeshake.preservedApps ?? []) {
29
+ args.push("--preserved-app", app);
30
+ }
31
+ }
21
32
  try {
22
- const report = await withTmp(async (packedDir) => {
23
- const report = await packTarballs({
24
- rootDir: resolve(opts.rootDir),
25
- outDir: packedDir,
26
- runtimeVariant: opts.runtimeVariant,
27
- app: opts.app,
28
- extraApps: opts.extraApps ?? [],
29
- strip,
30
- });
31
- if (!report.ok) {
32
- throw new Error(`[popcorn-otp] ${formatPackError(report.error)}`);
33
- }
34
- await Promise.all([
35
- copy(report.manifestPath, p `${preparedDir}/otp/manifest.json`),
36
- copy(report.bootPath, p `${preparedDir}/otp/bin/vm.boot`),
37
- copy(report.tarPaths, p `${preparedDir}/otp/lib`, {
38
- variants: assetVariants,
39
- }),
40
- ]);
41
- return report;
33
+ await execFileAsync("mix", args, {
34
+ cwd: resolve(options.rootDir),
35
+ env: { ...process.env, MIX_QUIET: "1" },
42
36
  });
37
+ const manifestPath = resolve(dir, "otp/manifest.json");
38
+ const manifest = JSON.parse(await readFile(manifestPath, "utf8"));
39
+ assert(manifest.runtimeVariant === "core" ||
40
+ manifest.runtimeVariant === "crypto", "popcorn.cook did not report a runtime variant");
43
41
  return {
44
- dir: preparedDir,
45
- runtimeVariant: report.runtimeVariant,
46
- notes: report.notes ?? [],
42
+ dir,
43
+ runtimeVariant: manifest.runtimeVariant,
44
+ notes: manifest.notes ?? [],
47
45
  };
48
46
  }
49
47
  catch (error) {
50
- await rm(preparedDir, { recursive: true, force: true });
48
+ await rm(dir, { recursive: true, force: true });
51
49
  throw error;
52
50
  }
53
51
  }
54
52
  function runtimeDirectory(variant) {
55
- if (variant !== "core" && variant !== "crypto") {
56
- throw new Error(`[popcorn-otp] Unknown runtime variant: ${variant}`);
57
- }
58
- return p `${dirname(fileURLToPath(import.meta.url))}/../runtimes/${variant}`;
59
- }
60
- async function copyRuntime(targetDir, variant) {
61
- const distDir = p `${dirname(fileURLToPath(import.meta.url))}/..`;
62
- const runtimeDir = runtimeDirectory(variant);
63
- await Promise.all(["worker.mjs", "beam.mjs", "beam.emu.mjs", "beam.wasm"].map((file) => copy(p `${file === "worker.mjs" ? distDir : runtimeDir}/${file}`, p `${targetDir}/${file}`)));
64
- }
65
- async function packTarballs(opts) {
66
- const { rootDir, outDir, runtimeVariant, app, extraApps, strip } = opts;
67
- const toolDir = p `${dirname(fileURLToPath(import.meta.url))}/beam_tools`;
68
- const packerArgs = [
69
- "run",
70
- "--no-start",
71
- "-e",
72
- "Popcorn.BeamTools.CLI.main(System.argv())",
73
- "--",
74
- "--root-dir",
75
- rootDir,
76
- "--out-dir",
77
- outDir,
78
- "--runtimes-dir",
79
- p `${toolDir}/../../runtimes`,
80
- ];
81
- if (runtimeVariant !== undefined) {
82
- packerArgs.push("--runtime-variant", runtimeVariant);
83
- }
84
- if (app !== null) {
85
- packerArgs.push("--entrypoint-app", app);
86
- }
87
- for (const extraApp of extraApps) {
88
- packerArgs.push("--extra-app", extraApp);
89
- }
90
- if (strip) {
91
- packerArgs.push("--strip");
92
- }
93
- const env = {
94
- ...process.env,
95
- MIX_BUILD_PATH: p `${outDir}/beam_tools_build`,
96
- MIX_QUIET: "1",
97
- };
98
- const { stdout } = await execFileAsync("mix", packerArgs, {
99
- cwd: toolDir,
100
- env,
101
- });
102
- return JSON.parse(stdout);
103
- }
104
- function hasCode(error, code) {
105
- return (typeof error === "object" &&
106
- error !== null &&
107
- error.code === code);
108
- }
109
- function isMissingDepError(error) {
110
- return hasCode(error, "missing_dep");
53
+ const distDir = resolve(dirname(fileURLToPath(import.meta.url)), "..");
54
+ return resolve(distDir, "runtimes", variant);
111
55
  }
112
- function isUnsupportedAppsError(error) {
113
- return hasCode(error, "unsupported_apps");
114
- }
115
- function isMissingExtraAppsError(error) {
116
- return hasCode(error, "missing_extra_apps");
117
- }
118
- function toolchainOf(error) {
119
- if (typeof error !== "object" || error === null) {
120
- return undefined;
121
- }
122
- return error.toolchain;
123
- }
124
- function errorLines(error) {
125
- if (isMissingDepError(error)) {
126
- return [
127
- `${error.app} depends on ${error.dep}, which isn't available.`,
128
- `BEAM applications come from your project build and your active`,
129
- `Erlang/Elixir installation; nothing is bundled with the package.`,
130
- `Apps built by your project: ${error.available_apps.join(", ")}.`,
131
- ];
132
- }
133
- if (isUnsupportedAppsError(error)) {
134
- const apps = error.apps
135
- .map(({ app, capability }) => `${app} (needs ${capability})`)
136
- .join(", ");
137
- return [
138
- `These applications need native support the Wasm runtime wasn't built`,
139
- `with: ${apps}.`,
140
- `Drop them from your dependencies, or use a runtime built with it.`,
141
- ];
142
- }
143
- if (isMissingExtraAppsError(error)) {
144
- return [
145
- `Extra apps not found: ${error.apps.join(", ")}.`,
146
- `They have to come from your project build or your Erlang/Elixir install.`,
147
- ];
148
- }
149
- return [`packaging failed: ${JSON.stringify(error)}`];
150
- }
151
- function formatPackError(error) {
152
- const lines = errorLines(error);
153
- const toolchain = toolchainOf(error);
154
- if (toolchain !== undefined) {
155
- lines.push(`Using ${toolchain.executable} (Erlang/OTP ${toolchain.otp}, Elixir ${toolchain.elixir}).`);
156
- }
157
- return lines.join("\n ");
158
- }
159
- async function copy(source, target, { variants = ["uncompressed"] } = {}) {
160
- const sources = typeof source === "string" ? [source] : source;
161
- const targetIsDir = typeof source !== "string";
162
- await Promise.all(sources.map(async (sourcePath) => {
163
- const targetPath = targetIsDir
164
- ? p `${target}/${basename(sourcePath)}`
165
- : target;
166
- await mkdir(dirname(targetPath), { recursive: true });
167
- let content;
168
- const read = () => (content ??= readFile(sourcePath));
169
- await Promise.all(variants
170
- .filter((variant) => Boolean(variant))
171
- .map(async (variant) => {
172
- switch (variant) {
173
- case "uncompressed":
174
- await copyFile(sourcePath, targetPath);
175
- break;
176
- case "gzip": {
177
- const input = await read();
178
- const buffer = await gzipAsync(input, { level: 9 });
179
- await writeFile(`${targetPath}.gz`, buffer);
180
- break;
181
- }
182
- case "brotli": {
183
- const Q = constants.BROTLI_PARAM_QUALITY;
184
- const opts = { params: { [Q]: 11 } };
185
- const input = await read();
186
- const buffer = await brotliCompressAsync(input, opts);
187
- await writeFile(`${targetPath}.br`, buffer);
188
- break;
189
- }
190
- }
191
- }));
192
- }));
193
- }
194
- function p(strings, ...values) {
195
- return normalize(String.raw(strings, ...values));
196
- }
197
- async function withTmp(f) {
198
- const dir = await mkdtemp(p `${tmpdir()}/popcorn-otp-`);
199
- try {
200
- return await f(dir);
201
- }
202
- finally {
203
- await rm(dir, { recursive: true, force: true });
204
- }
56
+ function assert(ok, message) {
57
+ if (!ok)
58
+ throw new Error(`[popcorn-otp] ${message}`);
205
59
  }
206
60
 
207
- export { copyRuntime as c, popcorn as p, runtimeDirectory as r };
61
+ export { popcorn as p, runtimeDirectory as r };
@@ -1,5 +1,10 @@
1
1
  import type { Plugin } from "vite";
2
2
  import { type Options } from "./shared";
3
+ /** Headers required by the threaded WebAssembly runtime. */
4
+ export declare const crossOriginIsolationHeaders: {
5
+ "Cross-Origin-Opener-Policy": string;
6
+ "Cross-Origin-Embedder-Policy": string;
7
+ };
3
8
  /**
4
9
  * Serves application assets during developmens.
5
10
  *
@@ -9,7 +14,7 @@ import { type Options } from "./shared";
9
14
  * import { popcorn } from "@swmansion/popcorn/vite";
10
15
  *
11
16
  * export default defineConfig({
12
- * plugins: [popcorn({ rootDir: "../my_app", app: "my_app" })],
17
+ * plugins: [popcorn({ rootDir: "../my_app" })],
13
18
  * });
14
19
  * ```
15
20
  *