@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,101 +1,227 @@
1
- import { readFile, stat } from 'fs/promises';
2
- import { dirname, resolve, basename } from 'path';
3
- import { fileURLToPath } from 'url';
1
+ import { mkdir, cp, rm, readFile } from 'node:fs/promises';
2
+ import { resolve, dirname, join, relative, isAbsolute } from 'node:path';
3
+ import { fileURLToPath } from 'node:url';
4
+ import { r as runtimeDirectory, p as popcorn$1 } from './shared.mjs';
5
+ import 'node:child_process';
6
+ import 'node:os';
7
+ import 'node:util';
8
+ import 'node:zlib';
4
9
 
5
- const __dirname$1 = dirname(fileURLToPath(import.meta.url));
6
- // Plugin is at dist/plugins/vite.mjs, dist/ is one level up
7
- const popcornDistDir = resolve(__dirname$1, "..");
10
+ const CORS_HEADERS = {
11
+ "Cross-Origin-Opener-Policy": "same-origin",
12
+ "Cross-Origin-Embedder-Policy": "require-corp",
13
+ };
14
+ // Plugin is at dist/plugins/vite.mjs, dist/ is one level up.
15
+ const distDir = resolve(dirname(fileURLToPath(import.meta.url)), "..");
16
+ /**
17
+ * Serves application assets during developmens.
18
+ *
19
+ * @example
20
+ * ```ts
21
+ * import { defineConfig } from "vite";
22
+ * import { popcorn } from "@swmansion/popcorn/vite";
23
+ *
24
+ * export default defineConfig({
25
+ * plugins: [popcorn({ rootDir: "../my_app", app: "my_app" })],
26
+ * });
27
+ * ```
28
+ *
29
+ * @see {@link Options} for production server requirements.
30
+ */
8
31
  function popcorn(options) {
9
- const bundlePaths = options.bundlePaths;
10
- const bundles = bundlePaths.map((p) => ({
11
- path: p,
12
- name: basename(p),
13
- url: `/${basename(p)}`,
14
- }));
15
- let assetsDir;
32
+ const resolveRuntime = async (source, importer) => {
33
+ if (importer !== undefined &&
34
+ resolve(dirname(importer.split("?")[0]), source) ===
35
+ join(distDir, "beam.mjs")) {
36
+ const { runtimeVariant } = await ensurePrepared();
37
+ const runtimeDir = runtimeDirectory(runtimeVariant);
38
+ return join(runtimeDir, "beam.mjs");
39
+ }
40
+ };
41
+ let prepared;
42
+ let repacking;
43
+ let dirty = false;
44
+ let outDir;
45
+ let assetsDir = "assets";
46
+ // Dev/preview: pack once at first request, re-pack lazily after the app's
47
+ // compiled beams change. A single in-flight promise dedupes concurrent
48
+ // requests; a change during a re-pack re-sets `dirty` so it isn't lost.
49
+ const ensurePrepared = () => {
50
+ if (prepared !== undefined && !dirty)
51
+ return Promise.resolve(prepared);
52
+ if (repacking === undefined) {
53
+ dirty = false;
54
+ repacking = popcorn$1(options)
55
+ .then(async (next) => {
56
+ if (prepared !== undefined) {
57
+ await rm(prepared.dir, { recursive: true, force: true });
58
+ }
59
+ prepared = next;
60
+ return next;
61
+ })
62
+ .finally(() => {
63
+ repacking = undefined;
64
+ });
65
+ }
66
+ return repacking;
67
+ };
68
+ const cleanup = () => {
69
+ if (prepared !== undefined) {
70
+ rm(prepared.dir, { recursive: true, force: true });
71
+ prepared = undefined;
72
+ }
73
+ };
74
+ const serve = async (req, res, next) => {
75
+ const url = req.url;
76
+ if (url === undefined) {
77
+ next();
78
+ return;
79
+ }
80
+ const requestPath = decodeURIComponent(new URL(url, "http://localhost").pathname);
81
+ const requestRoot = [`/${assetsDir}/otp/`, `/@fs${distDir}/otp/`].find((root) => requestPath.startsWith(root));
82
+ if (requestRoot === undefined) {
83
+ next();
84
+ return;
85
+ }
86
+ let dir;
87
+ try {
88
+ ({ dir } = await ensurePrepared());
89
+ }
90
+ catch (error) {
91
+ next(error);
92
+ return;
93
+ }
94
+ const assetsRoot = join(dir, "otp");
95
+ const filePath = resolve(assetsRoot, requestPath.slice(requestRoot.length));
96
+ if (!isUnder(assetsRoot, filePath)) {
97
+ res.statusCode = 403;
98
+ setHeaders(res);
99
+ res.end("Forbidden");
100
+ return;
101
+ }
102
+ try {
103
+ const compressible = isCompressible(filePath);
104
+ const encoding = compressible
105
+ ? selectEncoding(req.headers["accept-encoding"]?.toString(), options.brotli ?? false)
106
+ : { name: null, suffix: "" };
107
+ if (compressible && encoding.name === null) {
108
+ res.statusCode = 406;
109
+ setHeaders(res);
110
+ res.setHeader("Vary", "Accept-Encoding");
111
+ res.end("No supported content encoding");
112
+ return;
113
+ }
114
+ const content = await readFile(`${filePath}${encoding.suffix}`);
115
+ setHeaders(res);
116
+ setContentType(res, filePath);
117
+ res.setHeader("Vary", "Accept-Encoding");
118
+ if (encoding.name !== null) {
119
+ res.setHeader("Content-Encoding", encoding.name);
120
+ }
121
+ res.end(content);
122
+ }
123
+ catch {
124
+ res.statusCode = 404;
125
+ setHeaders(res);
126
+ res.end("Not found");
127
+ }
128
+ };
16
129
  return {
17
- name: "popcorn",
130
+ name: "popcorn-otp",
131
+ enforce: "pre",
132
+ resolveId: resolveRuntime,
18
133
  config() {
19
134
  return {
20
- // Exclude popcorn from prebundling so import.meta.url resolves correctly
21
- optimizeDeps: {
22
- exclude: ["@swmansion/popcorn"],
135
+ // Exclude from prebundling so import.meta.url resolves correctly.
136
+ optimizeDeps: { exclude: ["@swmansion/popcorn"] },
137
+ worker: {
138
+ format: "es",
139
+ plugins: () => [
140
+ {
141
+ name: "popcorn-runtime",
142
+ enforce: "pre",
143
+ resolveId: resolveRuntime,
144
+ },
145
+ ],
23
146
  },
147
+ // COOP/COEP must be on every response (SharedArrayBuffer/pthreads),
148
+ // including the worker/beam files Vite serves from the package itself.
149
+ server: { headers: CORS_HEADERS },
150
+ preview: { headers: CORS_HEADERS },
24
151
  };
25
152
  },
26
- async configResolved(config) {
27
- const results = await Promise.allSettled(bundles.map((b) => stat(b.path)));
28
- for (const [i, result] of results.entries()) {
29
- if (result.status === "rejected") {
30
- this.error(`[popcorn] Bundle doesn't exist at '${bundles[i].path}'`);
31
- }
32
- }
33
- config.server.fs.allow.push(popcornDistDir);
153
+ configResolved(config) {
154
+ config.server.fs.allow.push(distDir);
155
+ outDir = resolve(config.root, config.build.outDir);
34
156
  assetsDir = config.build.assetsDir;
35
157
  },
36
158
  configureServer(server) {
37
- server.middlewares.use(async (req, res, next) => {
38
- setSharedArrayBufferHeaders(res);
39
- for (const bundle of bundles) {
40
- const opts = { bundleUrl: bundle.url, bundlePath: bundle.path };
41
- const served = await serveAvmBundle(req, res, opts);
42
- if (served)
43
- return;
44
- }
45
- next();
159
+ const rootDir = resolve(options.rootDir);
160
+ server.watcher.add(rootDir);
161
+ server.watcher.on("all", (_event, file) => {
162
+ if (isUnder(rootDir, file))
163
+ dirty = true;
46
164
  });
165
+ server.middlewares.use(serve);
166
+ server.httpServer?.once("close", cleanup);
47
167
  },
48
168
  configurePreviewServer(server) {
49
- server.middlewares.use(async (req, res, next) => {
50
- setSharedArrayBufferHeaders(res);
51
- for (const bundle of bundles) {
52
- const opts = { bundleUrl: bundle.url, bundlePath: bundle.path };
53
- const served = await serveAvmBundle(req, res, opts);
54
- if (served)
55
- return;
56
- }
57
- next();
58
- });
169
+ server.middlewares.use(serve);
170
+ server.httpServer?.once("close", cleanup);
59
171
  },
60
- async generateBundle() {
61
- // Emit bundles to assets directory
62
- for (const bundle of bundles) {
63
- this.emitFile({
64
- type: "asset",
65
- fileName: `${assetsDir}/${bundle.name}`,
66
- source: await readFile(bundle.path),
67
- });
172
+ async closeBundle() {
173
+ assert(outDir !== undefined, "outDir was not resolved");
174
+ const built = await ensurePrepared();
175
+ try {
176
+ const targetDir = join(outDir, assetsDir, "otp");
177
+ await mkdir(dirname(targetDir), { recursive: true });
178
+ await cp(join(built.dir, "otp"), targetDir, { recursive: true });
68
179
  }
69
- // Emit AtomVM files to assets directory (same location as iframe.mjs)
70
- // Vite treats iframe.mjs as a static asset and doesn't analyze its imports
71
- for (const name of ["AtomVM.mjs", "AtomVM.wasm"]) {
72
- this.emitFile({
73
- type: "asset",
74
- fileName: `${assetsDir}/${name}`,
75
- source: await readFile(resolve(popcornDistDir, name)),
76
- });
180
+ finally {
181
+ await rm(built.dir, { recursive: true, force: true });
182
+ prepared = undefined;
77
183
  }
78
184
  },
79
185
  };
80
186
  }
81
- async function serveAvmBundle(req, res, { bundleUrl, bundlePath }) {
82
- try {
83
- if (req.url === bundleUrl) {
84
- const content = await readFile(bundlePath);
85
- res.setHeader("Content-Type", "application/octet-stream");
86
- res.end(content);
87
- return true;
88
- }
89
- return false;
187
+ function isCompressible(path) {
188
+ return path.endsWith(".tar") || path.endsWith(".wasm");
189
+ }
190
+ function selectEncoding(header, useBrotli) {
191
+ if (useBrotli && header?.includes("br")) {
192
+ return { name: "br", suffix: ".br" };
90
193
  }
91
- catch (err) {
92
- console.error(`[popcorn] Failed to serve bundle:`, err);
93
- throw err;
194
+ if (header?.includes("gzip"))
195
+ return { name: "gzip", suffix: ".gz" };
196
+ return { name: null, suffix: "" };
197
+ }
198
+ function isUnder(dir, file) {
199
+ const rel = relative(dir, file);
200
+ return rel !== "" && !rel.startsWith("..") && !isAbsolute(rel);
201
+ }
202
+ function setHeaders(res) {
203
+ for (const [key, value] of Object.entries(CORS_HEADERS)) {
204
+ res.setHeader(key, value);
205
+ }
206
+ }
207
+ function setContentType(res, path) {
208
+ if (path.endsWith(".mjs")) {
209
+ res.setHeader("Content-Type", "text/javascript");
210
+ }
211
+ else if (path.endsWith(".wasm")) {
212
+ res.setHeader("Content-Type", "application/wasm");
213
+ }
214
+ else if (path.endsWith(".json")) {
215
+ res.setHeader("Content-Type", "application/json");
216
+ }
217
+ else {
218
+ res.setHeader("Content-Type", "application/octet-stream");
94
219
  }
95
220
  }
96
- function setSharedArrayBufferHeaders(res) {
97
- res.setHeader("Cross-Origin-Opener-Policy", "same-origin");
98
- res.setHeader("Cross-Origin-Embedder-Policy", "require-corp");
221
+ function assert(ok, message) {
222
+ if (!ok) {
223
+ throw new Error(`[popcorn-otp] ${message}`);
224
+ }
99
225
  }
100
226
 
101
227
  export { popcorn };