@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 +18 -9
- package/dist/events.d.ts +7 -7
- package/dist/index.mjs +37 -7
- package/dist/plugins/esbuild.mjs +2 -8
- package/dist/plugins/rollup.mjs +3 -9
- package/dist/plugins/shared.d.ts +17 -37
- package/dist/plugins/shared.mjs +42 -188
- package/dist/plugins/vite.d.ts +6 -1
- package/dist/plugins/vite.mjs +18 -21
- package/dist/popcorn.d.ts +7 -3
- package/dist/runtimes/core/beam.emu.mjs +139 -139
- package/dist/runtimes/core/beam.mjs +139 -139
- package/dist/runtimes/core/beam.wasm +0 -0
- package/dist/runtimes/crypto/beam.emu.mjs +518 -517
- package/dist/runtimes/crypto/beam.mjs +518 -517
- package/dist/runtimes/crypto/beam.wasm +0 -0
- package/dist/types.d.ts +7 -0
- package/dist/worker.mjs +93 -16
- package/package.json +3 -3
- package/dist/plugins/beam_tools/lib/popcorn/beam_tools/beam_patcher.ex +0 -184
- package/dist/plugins/beam_tools/lib/popcorn/beam_tools/cli.ex +0 -74
- package/dist/plugins/beam_tools/lib/popcorn/beam_tools/packager.ex +0 -540
- package/dist/plugins/beam_tools/mix.exs +0 -16
- package/dist/plugins/beam_tools/patches/kernel/prim_tty.erl +0 -13
- package/dist/plugins/beam_tools/patches/stdlib/beam_lib.erl +0 -27
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.
|
|
13
|
-
Run `mix deps.get`
|
|
14
|
-
|
|
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 [
|
|
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
|
|
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
|
|
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.
|
|
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
|
|
27
|
+
export type BridgeReplyPayload = {
|
|
28
28
|
message: BeamSendPayload;
|
|
29
29
|
};
|
|
30
|
-
type
|
|
31
|
-
type: "popcorn:
|
|
32
|
-
payload:
|
|
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 |
|
|
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: {
|
|
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.
|
|
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.
|
|
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
|
-
|
|
1073
|
-
toVm(this.vmWorker, { type: "popcorn:
|
|
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});`);
|
package/dist/plugins/esbuild.mjs
CHANGED
|
@@ -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
|
|
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
|
|
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 });
|
package/dist/plugins/rollup.mjs
CHANGED
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
import { mkdir, cp, rm } from 'node:fs/promises';
|
|
2
|
-
import {
|
|
3
|
-
import { p as popcorn$1
|
|
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
|
|
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 });
|
package/dist/plugins/shared.d.ts
CHANGED
|
@@ -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
|
-
*
|
|
5
|
-
*
|
|
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
|
-
|
|
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(
|
|
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 {};
|
package/dist/plugins/shared.mjs
CHANGED
|
@@ -1,207 +1,61 @@
|
|
|
1
1
|
import { execFile } from 'node:child_process';
|
|
2
|
-
import { mkdtemp,
|
|
3
|
-
import { resolve, dirname
|
|
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
|
-
|
|
11
|
-
const
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
"
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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
|
-
|
|
23
|
-
|
|
24
|
-
|
|
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
|
|
45
|
-
runtimeVariant:
|
|
46
|
-
notes:
|
|
42
|
+
dir,
|
|
43
|
+
runtimeVariant: manifest.runtimeVariant,
|
|
44
|
+
notes: manifest.notes ?? [],
|
|
47
45
|
};
|
|
48
46
|
}
|
|
49
47
|
catch (error) {
|
|
50
|
-
await rm(
|
|
48
|
+
await rm(dir, { recursive: true, force: true });
|
|
51
49
|
throw error;
|
|
52
50
|
}
|
|
53
51
|
}
|
|
54
52
|
function runtimeDirectory(variant) {
|
|
55
|
-
|
|
56
|
-
|
|
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
|
|
113
|
-
|
|
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 {
|
|
61
|
+
export { popcorn as p, runtimeDirectory as r };
|
package/dist/plugins/vite.d.ts
CHANGED
|
@@ -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"
|
|
17
|
+
* plugins: [popcorn({ rootDir: "../my_app" })],
|
|
13
18
|
* });
|
|
14
19
|
* ```
|
|
15
20
|
*
|