@swmansion/popcorn 0.3.2 → 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 +1 -1
  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
@@ -1,54 +1,59 @@
1
- import { mkdir, copyFile } from 'fs/promises';
2
- import { dirname, resolve, basename, join } from 'path';
3
- import { fileURLToPath } from 'url';
1
+ import { mkdir, cp, rm } from 'node:fs/promises';
2
+ import { dirname, resolve } from 'node:path';
3
+ import { p as popcorn$1, c as copyRuntime } from './shared.mjs';
4
+ import 'node:child_process';
5
+ import 'node:os';
6
+ import 'node:util';
7
+ import 'node:url';
8
+ import 'node:zlib';
4
9
 
5
- const __dirname$1 = dirname(fileURLToPath(import.meta.url));
6
- // Plugin is at dist/plugins/esbuild.mjs, dist/ is one level up
7
- const popcornDistDir = resolve(__dirname$1, "..");
10
+ /**
11
+ * Copies the worker, VM runtime, and `otp/` assets into the output directory after a successful esbuild build.
12
+ *
13
+ * Requires `format: "esm"` and either `outdir` or `outfile`.
14
+ * If the application bundle is elsewhere, set `PopcornOpts.workerUrl` to the copied `worker.mjs`.
15
+ *
16
+ * @see {@link Options} for application packaging and server requirements.
17
+ */
8
18
  function popcorn(options) {
9
- const bundles = options.bundlePaths.map((p) => ({
10
- name: basename(p),
11
- dir: dirname(p),
12
- }));
13
19
  let outputDir;
14
20
  return {
15
- name: "popcorn",
21
+ name: "popcorn-otp",
16
22
  setup(build) {
17
23
  build.onStart(() => {
18
24
  const opts = build.initialOptions;
19
- const isEsm = opts.format === "esm";
20
- const outdirFallback = opts.outfile !== undefined ? dirname(opts.outfile) : undefined;
21
- const outdir = opts.outdir ?? outdirFallback;
22
- if (!isEsm) {
23
- throw new Error("[popcorn] Popcorn works only with esm type builds.");
24
- }
25
- if (outdir === undefined) {
26
- throw new Error("[popcorn] outdir is not specified, cannot copy files");
27
- }
28
- outputDir = outdir;
25
+ const outdir = opts.outdir ??
26
+ (opts.outfile === undefined ? undefined : dirname(opts.outfile));
27
+ assert(opts.format === "esm", "Popcorn OTP works only with esm builds.");
28
+ assert(outdir !== undefined, "outdir is not specified, cannot copy files");
29
+ outputDir = resolve(outdir);
29
30
  });
30
- build.onEnd(async () => {
31
- await mkdir(outputDir, { recursive: true });
31
+ build.onEnd(async (result) => {
32
+ if (result.errors.length > 0)
33
+ return;
34
+ assert(outputDir !== undefined, "outdir was not resolved");
35
+ const outDir = outputDir;
36
+ const prepared = await popcorn$1(options);
32
37
  try {
38
+ await mkdir(outDir, { recursive: true });
33
39
  await Promise.all([
34
- // Copy bundles to output directory
35
- ...bundles.map((b) => copy(b.name, { inDir: b.dir, outDir: outputDir })),
36
- // Copy popcorn runtime files to output directory
37
- // These need to be alongside the bundled code for import.meta.url to work
38
- copy("iframe.mjs", { inDir: popcornDistDir, outDir: outputDir }),
39
- copy("AtomVM.mjs", { inDir: popcornDistDir, outDir: outputDir }),
40
- copy("AtomVM.wasm", { inDir: popcornDistDir, outDir: outputDir }),
40
+ copyRuntime(outDir, prepared.runtimeVariant),
41
+ cp(resolve(prepared.dir, "otp"), resolve(outDir, "otp"), {
42
+ recursive: true,
43
+ }),
41
44
  ]);
42
45
  }
43
- catch (err) {
44
- throw new Error("[popcorn] Failed to copy files", { cause: err });
46
+ finally {
47
+ await rm(prepared.dir, { recursive: true, force: true });
45
48
  }
46
49
  });
47
50
  },
48
51
  };
49
52
  }
50
- async function copy(name, { inDir, outDir }) {
51
- return copyFile(join(inDir, name), join(outDir, name));
53
+ function assert(ok, message) {
54
+ if (!ok) {
55
+ throw new Error(`[popcorn-otp] ${message}`);
56
+ }
52
57
  }
53
58
 
54
59
  export { popcorn };
@@ -1,3 +1,11 @@
1
1
  import type { Plugin } from "rollup";
2
- import { type PopcornPluginOptions } from "./shared";
3
- export declare function popcorn(options: PopcornPluginOptions): Plugin<unknown>;
2
+ import { type Options } from "./shared";
3
+ /**
4
+ * Copies the worker, VM runtime, and `otp/` assets into the output directory after Rollup writes the bundle.
5
+ *
6
+ * Requires `output.format: "es"` and either `output.dir` or `output.file`.
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;
@@ -1,35 +1,56 @@
1
- import { readFile } from 'fs/promises';
2
- import { dirname, resolve, basename } from 'path';
3
- import { fileURLToPath } from 'url';
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';
4
+ import 'node:child_process';
5
+ import 'node:os';
6
+ import 'node:util';
7
+ import 'node:url';
8
+ import 'node:zlib';
4
9
 
5
- const __dirname$1 = dirname(fileURLToPath(import.meta.url));
6
- // Plugin is at dist/plugins/rollup.mjs, dist/ is one level up
7
- const popcornDistDir = resolve(__dirname$1, "..");
10
+ /**
11
+ * Copies the worker, VM runtime, and `otp/` assets into the output directory after Rollup writes the bundle.
12
+ *
13
+ * Requires `output.format: "es"` and either `output.dir` or `output.file`.
14
+ * If the application bundle is elsewhere, set `PopcornOpts.workerUrl` to the copied `worker.mjs`.
15
+ *
16
+ * @see {@link Options} for application packaging and server requirements.
17
+ */
8
18
  function popcorn(options) {
9
- const bundles = options.bundlePaths.map((p) => ({
10
- path: p,
11
- name: basename(p),
12
- }));
19
+ let outputDir;
13
20
  return {
14
- name: "popcorn",
15
- async generateBundle() {
16
- // Emit bundles
17
- for (const bundle of bundles) {
18
- this.emitFile({
19
- type: "asset",
20
- fileName: resolve(popcornDistDir, bundle.name),
21
- source: await readFile(bundle.path),
22
- });
21
+ name: "popcorn-otp",
22
+ renderStart(outputOptions) {
23
+ assert(outputOptions.format === "es", "Popcorn OTP works only with esm builds.");
24
+ let dir = outputOptions.dir;
25
+ if (dir === undefined && outputOptions.file !== undefined) {
26
+ dir = dirname(outputOptions.file);
23
27
  }
24
- // Emit popcorn runtime files to output directory
25
- // These need to be alongside the bundled code for import.meta.url to work
26
- for (const name of ["iframe.mjs", "AtomVM.mjs", "AtomVM.wasm"]) {
27
- const sourcePath = resolve(popcornDistDir, name);
28
- const source = await readFile(sourcePath);
29
- this.emitFile({ type: "asset", fileName: name, source });
28
+ assert(dir !== undefined, "output dir is not specified, cannot copy files");
29
+ outputDir = resolve(dir);
30
+ },
31
+ async writeBundle() {
32
+ assert(outputDir !== undefined, "output dir was not resolved");
33
+ const outDir = outputDir;
34
+ const prepared = await popcorn$1(options);
35
+ try {
36
+ 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
+ ]);
43
+ }
44
+ finally {
45
+ await rm(prepared.dir, { recursive: true, force: true });
30
46
  }
31
47
  },
32
48
  };
33
49
  }
50
+ function assert(ok, message) {
51
+ if (!ok) {
52
+ throw new Error(`[popcorn-otp] ${message}`);
53
+ }
54
+ }
34
55
 
35
56
  export { popcorn };
@@ -1,7 +1,57 @@
1
- export declare const DIST_DIR = "node_modules/@swmansion/popcorn/dist";
2
- export type PopcornPluginOptions = {
1
+ /**
2
+ * Shared options for the Vite, Rollup, and esbuild plugins.
3
+ *
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.
9
+ */
10
+ type RuntimeVariant = "core" | "crypto";
11
+ export type Options = {
3
12
  /**
4
- * Paths to .avm bundle files.
13
+ * Runtime variant override.
14
+ *
15
+ * By default, the plugin selects `crypto` when an application requires it and `core` otherwise.
5
16
  */
6
- bundlePaths: string[];
17
+ runtimeVariant?: RuntimeVariant;
18
+ /**
19
+ * Mix project directory.
20
+ *
21
+ * Reads compiled apps from `_build/$MIX_ENV/lib`, with `MIX_ENV` defaulting to `dev`.
22
+ */
23
+ rootDir: string;
24
+ /**
25
+ * 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 `[]`.
34
+ */
35
+ 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
+ */
47
+ strip?: boolean;
48
+ };
49
+ export type Prepared = {
50
+ dir: string;
51
+ runtimeVariant: RuntimeVariant;
52
+ notes: unknown[];
7
53
  };
54
+ export declare function popcorn(opts: Options): Promise<Prepared>;
55
+ export declare function runtimeDirectory(variant: RuntimeVariant): string;
56
+ export declare function copyRuntime(targetDir: string, variant: RuntimeVariant): Promise<void>;
57
+ export {};
@@ -0,0 +1,207 @@
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';
4
+ import { tmpdir } from 'node:os';
5
+ import { promisify } from 'node:util';
6
+ import { fileURLToPath } from 'node:url';
7
+ import { brotliCompress, gzip, constants } from 'node:zlib';
8
+
9
+ 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-`);
21
+ 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;
42
+ });
43
+ return {
44
+ dir: preparedDir,
45
+ runtimeVariant: report.runtimeVariant,
46
+ notes: report.notes ?? [],
47
+ };
48
+ }
49
+ catch (error) {
50
+ await rm(preparedDir, { recursive: true, force: true });
51
+ throw error;
52
+ }
53
+ }
54
+ 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");
111
+ }
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
+ }
205
+ }
206
+
207
+ export { copyRuntime as c, popcorn as p, runtimeDirectory as r };
@@ -1,3 +1,18 @@
1
- import { type PopcornPluginOptions } from "./shared";
2
1
  import type { Plugin } from "vite";
3
- export declare function popcorn(options: PopcornPluginOptions): Plugin;
2
+ import { type Options } from "./shared";
3
+ /**
4
+ * Serves application assets during developmens.
5
+ *
6
+ * @example
7
+ * ```ts
8
+ * import { defineConfig } from "vite";
9
+ * import { popcorn } from "@swmansion/popcorn/vite";
10
+ *
11
+ * export default defineConfig({
12
+ * plugins: [popcorn({ rootDir: "../my_app", app: "my_app" })],
13
+ * });
14
+ * ```
15
+ *
16
+ * @see {@link Options} for production server requirements.
17
+ */
18
+ export declare function popcorn(options: Options): Plugin;