@swmansion/popcorn 0.3.3 → 0.4.0-next.2

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.
@@ -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;
@@ -1,54 +1,53 @@
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 } from './shared.mjs';
4
+ import 'node:child_process';
5
+ import 'node:os';
6
+ import 'node:util';
7
+ import 'node:url';
4
8
 
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, "..");
9
+ /**
10
+ * Copies the worker, VM runtime, and `otp/` assets into the output directory after a successful esbuild build.
11
+ *
12
+ * Requires `format: "esm"` and either `outdir` or `outfile`.
13
+ * If the application bundle is elsewhere, set `PopcornOpts.workerUrl` to the copied `worker.mjs`.
14
+ *
15
+ * @see {@link Options} for application packaging and server requirements.
16
+ */
8
17
  function popcorn(options) {
9
- const bundles = options.bundlePaths.map((p) => ({
10
- name: basename(p),
11
- dir: dirname(p),
12
- }));
13
18
  let outputDir;
14
19
  return {
15
- name: "popcorn",
20
+ name: "popcorn-otp",
16
21
  setup(build) {
17
22
  build.onStart(() => {
18
23
  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;
24
+ const outdir = opts.outdir ??
25
+ (opts.outfile === undefined ? undefined : dirname(opts.outfile));
26
+ assert(opts.format === "esm", "Popcorn OTP works only with esm builds.");
27
+ assert(outdir !== undefined, "outdir is not specified, cannot copy files");
28
+ outputDir = resolve(outdir);
29
29
  });
30
- build.onEnd(async () => {
31
- await mkdir(outputDir, { recursive: true });
30
+ build.onEnd(async (result) => {
31
+ if (result.errors.length > 0)
32
+ return;
33
+ assert(outputDir !== undefined, "outdir was not resolved");
34
+ const outDir = outputDir;
35
+ const prepared = await popcorn$1(options);
32
36
  try {
33
- 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 }),
41
- ]);
37
+ await mkdir(outDir, { recursive: true });
38
+ await cp(prepared.dir, outDir, { recursive: true });
42
39
  }
43
- catch (err) {
44
- throw new Error("[popcorn] Failed to copy files", { cause: err });
40
+ finally {
41
+ await rm(prepared.dir, { recursive: true, force: true });
45
42
  }
46
43
  });
47
44
  },
48
45
  };
49
46
  }
50
- async function copy(name, { inDir, outDir }) {
51
- return copyFile(join(inDir, name), join(outDir, name));
47
+ function assert(ok, message) {
48
+ if (!ok) {
49
+ throw new Error(`[popcorn-otp] ${message}`);
50
+ }
52
51
  }
53
52
 
54
53
  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,50 @@
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 { dirname, resolve } from 'node:path';
3
+ import { p as popcorn$1 } from './shared.mjs';
4
+ import 'node:child_process';
5
+ import 'node:os';
6
+ import 'node:util';
7
+ import 'node:url';
4
8
 
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, "..");
9
+ /**
10
+ * Copies the worker, VM runtime, and `otp/` assets into the output directory after Rollup writes the bundle.
11
+ *
12
+ * Requires `output.format: "es"` and either `output.dir` or `output.file`.
13
+ * If the application bundle is elsewhere, set `PopcornOpts.workerUrl` to the copied `worker.mjs`.
14
+ *
15
+ * @see {@link Options} for application packaging and server requirements.
16
+ */
8
17
  function popcorn(options) {
9
- const bundles = options.bundlePaths.map((p) => ({
10
- path: p,
11
- name: basename(p),
12
- }));
18
+ let outputDir;
13
19
  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
- });
20
+ name: "popcorn-otp",
21
+ renderStart(outputOptions) {
22
+ assert(outputOptions.format === "es", "Popcorn OTP works only with esm builds.");
23
+ let dir = outputOptions.dir;
24
+ if (dir === undefined && outputOptions.file !== undefined) {
25
+ dir = dirname(outputOptions.file);
23
26
  }
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 });
27
+ assert(dir !== undefined, "output dir is not specified, cannot copy files");
28
+ outputDir = resolve(dir);
29
+ },
30
+ async writeBundle() {
31
+ assert(outputDir !== undefined, "output dir was not resolved");
32
+ const outDir = outputDir;
33
+ const prepared = await popcorn$1(options);
34
+ try {
35
+ await mkdir(outDir, { recursive: true });
36
+ await cp(prepared.dir, outDir, { recursive: true });
37
+ }
38
+ finally {
39
+ await rm(prepared.dir, { recursive: true, force: true });
30
40
  }
31
41
  },
32
42
  };
33
43
  }
44
+ function assert(ok, message) {
45
+ if (!ok) {
46
+ throw new Error(`[popcorn-otp] ${message}`);
47
+ }
48
+ }
34
49
 
35
50
  export { popcorn };
@@ -1,7 +1,37 @@
1
- export declare const DIST_DIR = "node_modules/@swmansion/popcorn/dist";
2
- export type PopcornPluginOptions = {
1
+ type RuntimeVariant = "core" | "crypto";
2
+ /**
3
+ * Shared options for the Vite, Rollup, and esbuild plugins.
4
+ *
5
+ * The plugins invoke `mix popcorn.cook` in the project directory, which
6
+ * compiles the project before packaging it.
7
+ */
8
+ export type Options = {
9
+ /** Runtime variant override. */
10
+ runtimeVariant?: RuntimeVariant;
11
+ /** Mix project directory. */
12
+ rootDir: string;
3
13
  /**
4
- * Paths to .avm bundle files.
14
+ * OTP application to start after VM boot.
15
+ * Defaults to the current Mix application.
5
16
  */
6
- bundlePaths: string[];
17
+ app?: string | null;
18
+ /** Additional applications to package with their dependencies. */
19
+ extraApps?: string[];
20
+ /** Brotli compression effort. Defaults to `standard`. */
21
+ brotliEffort?: "standard" | "max";
22
+ /** Removes nonessential BEAM chunks. Defaults to `true`. */
23
+ strip?: boolean;
24
+ /** Controls removal of unreachable modules and functions. Defaults to `none`. */
25
+ treeshake?: TreeshakeOptions;
7
26
  };
27
+ export type TreeshakeOptions = {
28
+ preservedApps?: string[];
29
+ };
30
+ export type Prepared = {
31
+ dir: string;
32
+ runtimeVariant: RuntimeVariant;
33
+ notes: unknown[];
34
+ };
35
+ export declare function popcorn(options: Options): Promise<Prepared>;
36
+ export declare function runtimeDirectory(variant: RuntimeVariant): string;
37
+ export {};
@@ -0,0 +1,61 @@
1
+ import { execFile } from 'node:child_process';
2
+ import { mkdtemp, readFile, rm } from 'node:fs/promises';
3
+ import { resolve, dirname } from 'node:path';
4
+ import { tmpdir } from 'node:os';
5
+ import { promisify } from 'node:util';
6
+ import { fileURLToPath } from 'node:url';
7
+
8
+ const execFileAsync = promisify(execFile);
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
+ }
32
+ try {
33
+ await execFileAsync("mix", args, {
34
+ cwd: resolve(options.rootDir),
35
+ env: { ...process.env, MIX_QUIET: "1" },
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");
41
+ return {
42
+ dir,
43
+ runtimeVariant: manifest.runtimeVariant,
44
+ notes: manifest.notes ?? [],
45
+ };
46
+ }
47
+ catch (error) {
48
+ await rm(dir, { recursive: true, force: true });
49
+ throw error;
50
+ }
51
+ }
52
+ function runtimeDirectory(variant) {
53
+ const distDir = resolve(dirname(fileURLToPath(import.meta.url)), "..");
54
+ return resolve(distDir, "runtimes", variant);
55
+ }
56
+ function assert(ok, message) {
57
+ if (!ok)
58
+ throw new Error(`[popcorn-otp] ${message}`);
59
+ }
60
+
61
+ export { popcorn as p, runtimeDirectory as r };
@@ -1,3 +1,23 @@
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
+ /** 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
+ };
8
+ /**
9
+ * Serves application assets during developmens.
10
+ *
11
+ * @example
12
+ * ```ts
13
+ * import { defineConfig } from "vite";
14
+ * import { popcorn } from "@swmansion/popcorn/vite";
15
+ *
16
+ * export default defineConfig({
17
+ * plugins: [popcorn({ rootDir: "../my_app" })],
18
+ * });
19
+ * ```
20
+ *
21
+ * @see {@link Options} for production server requirements.
22
+ */
23
+ export declare function popcorn(options: Options): Plugin;
@@ -1,101 +1,224 @@
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';
4
8
 
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, "..");
9
+ /** Headers required by the threaded WebAssembly runtime. */
10
+ const crossOriginIsolationHeaders = {
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" })],
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 server startup, then re-pack lazily after the
47
+ // app's 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
+ res.end("Forbidden");
99
+ return;
100
+ }
101
+ try {
102
+ const compressible = isCompressible(filePath);
103
+ const encoding = compressible
104
+ ? selectEncoding(req.headers["accept-encoding"]?.toString())
105
+ : { name: null, suffix: "" };
106
+ if (compressible && encoding.name === null) {
107
+ res.statusCode = 406;
108
+ res.setHeader("Vary", "Accept-Encoding");
109
+ res.end("No supported content encoding");
110
+ return;
111
+ }
112
+ const content = await readFile(`${filePath}${encoding.suffix}`);
113
+ setContentType(res, filePath);
114
+ res.setHeader("Vary", "Accept-Encoding");
115
+ if (encoding.name !== null) {
116
+ res.setHeader("Content-Encoding", encoding.name);
117
+ }
118
+ res.end(content);
119
+ }
120
+ catch {
121
+ res.statusCode = 404;
122
+ res.end("Not found");
123
+ }
124
+ };
16
125
  return {
17
- name: "popcorn",
126
+ name: "popcorn-otp",
127
+ enforce: "pre",
128
+ resolveId: resolveRuntime,
18
129
  config() {
19
130
  return {
20
- // Exclude popcorn from prebundling so import.meta.url resolves correctly
21
- optimizeDeps: {
22
- exclude: ["@swmansion/popcorn"],
131
+ // Exclude from prebundling so import.meta.url resolves correctly.
132
+ optimizeDeps: { exclude: ["@swmansion/popcorn"] },
133
+ worker: {
134
+ format: "es",
135
+ plugins: () => [
136
+ {
137
+ name: "popcorn-runtime",
138
+ enforce: "pre",
139
+ resolveId: resolveRuntime,
140
+ },
141
+ ],
23
142
  },
24
143
  };
25
144
  },
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);
145
+ configResolved(config) {
146
+ config.server.fs.allow.push(distDir);
147
+ outDir = resolve(config.root, config.build.outDir);
34
148
  assetsDir = config.build.assetsDir;
35
149
  },
36
- 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();
150
+ async configureServer(server) {
151
+ const rootDir = resolve(options.rootDir);
152
+ server.httpServer?.prependListener("request", setIsolationHeaders);
153
+ server.watcher.add(rootDir);
154
+ server.watcher.on("all", (_event, file) => {
155
+ if (isUnder(rootDir, file))
156
+ dirty = true;
46
157
  });
158
+ server.middlewares.use(serve);
159
+ server.httpServer?.once("close", cleanup);
160
+ await ensurePrepared();
161
+ dirty = false;
47
162
  },
48
- 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
- });
163
+ async configurePreviewServer(server) {
164
+ server.httpServer?.prependListener("request", setIsolationHeaders);
165
+ server.middlewares.use(serve);
166
+ server.httpServer?.once("close", cleanup);
167
+ await ensurePrepared();
59
168
  },
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
- });
169
+ async closeBundle() {
170
+ assert(outDir !== undefined, "outDir was not resolved");
171
+ const built = await ensurePrepared();
172
+ try {
173
+ const targetDir = join(outDir, assetsDir, "otp");
174
+ await mkdir(dirname(targetDir), { recursive: true });
175
+ await cp(join(built.dir, "otp"), targetDir, { recursive: true });
68
176
  }
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
- });
177
+ finally {
178
+ await rm(built.dir, { recursive: true, force: true });
179
+ prepared = undefined;
77
180
  }
78
181
  },
79
182
  };
80
183
  }
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;
184
+ function isCompressible(path) {
185
+ return path.endsWith(".tar") || path.endsWith(".wasm");
186
+ }
187
+ function selectEncoding(header) {
188
+ if (header?.includes("br")) {
189
+ return { name: "br", suffix: ".br" };
90
190
  }
91
- catch (err) {
92
- console.error(`[popcorn] Failed to serve bundle:`, err);
93
- throw err;
191
+ if (header?.includes("gzip"))
192
+ return { name: "gzip", suffix: ".gz" };
193
+ return { name: null, suffix: "" };
194
+ }
195
+ function isUnder(dir, file) {
196
+ const rel = relative(dir, file);
197
+ return rel !== "" && !rel.startsWith("..") && !isAbsolute(rel);
198
+ }
199
+ function setIsolationHeaders(_req, res) {
200
+ for (const [key, value] of Object.entries(crossOriginIsolationHeaders)) {
201
+ res.setHeader(key, value);
202
+ }
203
+ }
204
+ function setContentType(res, path) {
205
+ if (path.endsWith(".mjs")) {
206
+ res.setHeader("Content-Type", "text/javascript");
207
+ }
208
+ else if (path.endsWith(".wasm")) {
209
+ res.setHeader("Content-Type", "application/wasm");
210
+ }
211
+ else if (path.endsWith(".json")) {
212
+ res.setHeader("Content-Type", "application/json");
213
+ }
214
+ else {
215
+ res.setHeader("Content-Type", "application/octet-stream");
94
216
  }
95
217
  }
96
- function setSharedArrayBufferHeaders(res) {
97
- res.setHeader("Cross-Origin-Opener-Policy", "same-origin");
98
- res.setHeader("Cross-Origin-Embedder-Policy", "require-corp");
218
+ function assert(ok, message) {
219
+ if (!ok) {
220
+ throw new Error(`[popcorn-otp] ${message}`);
221
+ }
99
222
  }
100
223
 
101
- export { popcorn };
224
+ export { crossOriginIsolationHeaders, popcorn };