@swmansion/popcorn 0.4.0-next.4 → 0.4.0-next.6

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
@@ -4,7 +4,7 @@ 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
6
  Start with the
7
- [versioned Popcorn guide](https://popcorn.hexdocs.pm/0.4.0-next.4/introduction.html).
7
+ [versioned Popcorn guide](https://popcorn.hexdocs.pm/0.4.0-next.6/introduction.html).
8
8
  It includes a complete Mix and Vite tutorial.
9
9
 
10
10
  ## Install
@@ -13,12 +13,12 @@ It includes a complete Mix and Vite tutorial.
13
13
  npm install @swmansion/popcorn@next
14
14
  ```
15
15
 
16
- Add `{:popcorn, "0.4.0-next.4"}` to your Elixir application's dependencies.
16
+ Add `{:popcorn, "0.4.0-next.6"}` to your Elixir application's dependencies.
17
17
  Run `mix deps.get` before building the JavaScript application. The bundler
18
18
  plugins invoke Mix locally to compile and package application and
19
19
  standard-library code.
20
20
 
21
- Use the toolchain pinned in [mise.toml](https://github.com/software-mansion/popcorn/blob/v0.4.0-next.4/mise.toml).
21
+ Use the toolchain pinned in [mise.toml](https://github.com/software-mansion/popcorn/blob/v0.4.0-next.6/mise.toml).
22
22
  The packager checks host OTP compatibility against the selected runtime's manifest.
23
23
 
24
24
  ## Configure Vite
@@ -37,8 +37,20 @@ export default defineConfig({
37
37
  ```
38
38
 
39
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.
41
- Use `app: null` to package the base runtime without starting an application.
40
+ default entrypoint. Configure cooking in the Mix project's `config/config.exs`:
41
+
42
+ ```elixir
43
+ import Config
44
+
45
+ config :popcorn,
46
+ extra_apps: [:eex],
47
+ treeshake: [preserved_apps: [:eex]]
48
+ ```
49
+
50
+ Set `app: :other_app` to select another OTP application, or `app: nil` to
51
+ package the base runtime without starting an application. Cooking settings
52
+ belong exclusively to Elixir config; the JS plugins accept only `rootDir`.
53
+ See `mix help popcorn.cook` for all settings.
42
54
 
43
55
  The npm package contains two variants:
44
56
 
@@ -47,8 +59,8 @@ The npm package contains two variants:
47
59
 
48
60
  The plugin emits only the selected variant. The browser does not download both.
49
61
  Both variants share one JavaScript API and one Hex package.
50
- The plugin selects `crypto` when the application's dependencies or `extraApps` require it; otherwise it selects `core`.
51
- Set `runtimeVariant: "core"` or `runtimeVariant: "crypto"` to override this choice.
62
+ The cook task selects `crypto` when the application's dependencies or configured `extra_apps` require it; otherwise it selects `core`.
63
+ Set `runtime_variant: :core` or `runtime_variant: :crypto` in Elixir config to override this choice.
52
64
  An explicit `"core"` selection produces a build error if the application requires crypto.
53
65
 
54
66
  Rollup and esbuild plugins accept the same options through `@swmansion/popcorn/rollup` and `@swmansion/popcorn/esbuild`.
@@ -59,18 +71,18 @@ Use ESM output with those bundlers.
59
71
  ```ts
60
72
  import { Popcorn } from "@swmansion/popcorn";
61
73
 
62
- const result = await Popcorn.init();
63
- if (!result.ok) throw result.error;
64
-
65
- const vm = result.data;
74
+ const vm = await Popcorn.init();
66
75
  // Stop the runtime when the application no longer needs it.
67
76
  vm.deinit();
68
77
  ```
69
78
 
70
79
  `Popcorn.init()` waits for the OTP application tree to start. Use
71
- `popcorn.genserver.call()` to call a supervised GenServer from JavaScript.
80
+ `popcorn.call()` to call a supervised GenServer from JavaScript.
72
81
  Use `popcorn.onEvent()` to receive events from BEAM processes.
73
82
 
83
+ Methods throw or reject on failure. Only `send`, `call`, and `cast` also have
84
+ `trySend`, `tryCall`, and `tryCast` variants that return `Result` objects.
85
+
74
86
  Each VM starts with 64 MiB of Wasm linear memory and can grow to 256 MiB on
75
87
  phones/tablets or 2 GiB on desktop. Device detection uses the browser's user
76
88
  agent and touch support. Configure the limits with
@@ -91,7 +103,7 @@ Cross-Origin-Embedder-Policy: require-corp
91
103
 
92
104
  Vite sets these headers for development and preview. Configure your production server separately.
93
105
  Serve `.wasm` as `application/wasm`. Serve compressed `.tar.gz` files with `Content-Encoding: gzip` for `.tar` requests.
94
- 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.
106
+ The package also emits uncompressed tar files and Brotli variants. Brotli uses standard effort by default. Set `config :popcorn, brotli_effort: :max` in `config/config.exs` for maximum compression in release builds.
95
107
 
96
108
  The JavaScript bridge currently requires a Content Security Policy that permits `unsafe-eval`.
97
- See the [versioned Elixir API](https://popcorn.hexdocs.pm/0.4.0-next.4/) for interoperability details.
109
+ See the [versioned Elixir API](https://popcorn.hexdocs.pm/0.4.0-next.6/) for interoperability details.
package/dist/errors.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * An operation result. Check `ok` before accessing `data` or `error`.
3
- * Expected failures use the error branch. Invalid usage can still throw.
3
+ * Expected failures from `try*` methods use the error branch. Invalid usage can still throw.
4
4
  */
5
5
  export type Result<T, E extends Tag = Tag> = {
6
6
  ok: true;
@@ -99,4 +99,5 @@ export declare class PopcornError<T extends Tag = Tag> extends Error {
99
99
  static deserialize(value: unknown): PopcornError;
100
100
  }
101
101
  export declare function isUnserializableReason(value: unknown): value is UnserializableReason;
102
+ export declare function unwrap<T>(result: Result<T>): T;
102
103
  export {};
package/dist/index.mjs CHANGED
@@ -172,6 +172,11 @@ function check$1(ok, msg) {
172
172
  throw err$1("internal:check", { detail: msg });
173
173
  }
174
174
  }
175
+ function unwrap(result) {
176
+ if (!result.ok)
177
+ throw result.error;
178
+ return result.data;
179
+ }
175
180
 
176
181
  function base64ToBytes(b64) {
177
182
  const binary = atob(b64);
@@ -191,7 +196,7 @@ function unreachable() {
191
196
  function objectWithKeys(value, keys) {
192
197
  if (value === null || typeof value !== "object")
193
198
  return null;
194
- if (value.constructor !== Object)
199
+ if (Object.getPrototypeOf(value) !== Object.prototype)
195
200
  return null;
196
201
  const hasAllKeys = keys.every((k) => Object.hasOwn(value, k));
197
202
  if (!hasAllKeys)
@@ -208,7 +213,9 @@ const LARGE_TUPLE_EXT = 0x69;
208
213
  const NIL_EXT = 0x6a;
209
214
  const LIST_EXT = 0x6c;
210
215
  const BINARY_EXT = 0x6d;
216
+ const BINARY_HEADER_SIZE = 1 + 4;
211
217
  const SMALL_BIG_EXT = 0x6e;
218
+ const LARGE_BIG_EXT = 0x6f;
212
219
  const MAP_EXT = 0x74;
213
220
  const ATOM_UTF8_EXT = 0x76;
214
221
  const SMALL_ATOM_UTF8_EXT = 0x77;
@@ -309,6 +316,16 @@ class Encoder {
309
316
  case "number":
310
317
  this.number(value);
311
318
  return;
319
+ case "bigint":
320
+ this.integer(value);
321
+ return;
322
+ case "symbol": {
323
+ const name = Symbol.keyFor(value) ?? value.description;
324
+ if (name === undefined)
325
+ throw err("unsupported", value);
326
+ this.atom(name);
327
+ return;
328
+ }
312
329
  case "object":
313
330
  this.object(value);
314
331
  return;
@@ -328,29 +345,31 @@ class Encoder {
328
345
  if (!Number.isSafeInteger(value)) {
329
346
  throw err("lossy-int", value);
330
347
  }
331
- if (value >= 0 && value < 2 ** 8) {
348
+ this.integer(BigInt(value));
349
+ }
350
+ integer(value) {
351
+ if (value >= 0n && value < 256n) {
332
352
  this.byte(SMALL_INTEGER_EXT);
333
- this.byte(value);
353
+ this.byte(Number(value));
334
354
  return;
335
355
  }
336
- if (value >= -2147483648 && value < 2 ** 31) {
356
+ if (value >= -2147483648n && value < 2147483648n) {
337
357
  this.byte(INTEGER_EXT);
338
- this.int32(value);
358
+ this.int32(Number(value));
339
359
  return;
340
360
  }
341
- this.smallBigInt(value);
342
- }
343
- // https://www.erlang.org/doc/apps/erts/erl_ext_dist.html#small_big_ext
344
- smallBigInt(value) {
345
- let magnitude = BigInt(Math.abs(value));
361
+ let magnitude = value < 0n ? -value : value;
346
362
  const digits = [];
347
363
  while (magnitude > 0n) {
348
364
  digits.push(Number(magnitude & 0xffn));
349
365
  magnitude >>= 8n;
350
366
  }
351
- this.byte(SMALL_BIG_EXT);
352
- this.byte(digits.length);
353
- this.byte(value < 0 ? 1 : 0);
367
+ this.byte(digits.length < 256 ? SMALL_BIG_EXT : LARGE_BIG_EXT);
368
+ if (digits.length < 256)
369
+ this.byte(digits.length);
370
+ else
371
+ this.uint32(digits.length);
372
+ this.byte(value < 0n ? 1 : 0);
354
373
  this.bytes(digits);
355
374
  }
356
375
  object(rawValue) {
@@ -376,12 +395,34 @@ class Encoder {
376
395
  this.array(value);
377
396
  return;
378
397
  }
398
+ if (value instanceof ArrayBuffer) {
399
+ const bytes = new Uint8Array(value);
400
+ this.binary(bytes);
401
+ return;
402
+ }
403
+ if (ArrayBuffer.isView(value)) {
404
+ const bytes = new Uint8Array(value.buffer, value.byteOffset, value.byteLength);
405
+ this.binary(bytes);
406
+ return;
407
+ }
408
+ if (value instanceof Date) {
409
+ this.date(value);
410
+ return;
411
+ }
412
+ if (value instanceof Map) {
413
+ this.entries(value.entries(), value);
414
+ return;
415
+ }
416
+ if (value instanceof Set) {
417
+ this.set(value);
418
+ return;
419
+ }
379
420
  const prototype = Object.getPrototypeOf(value);
380
421
  const isObject = prototype === Object.prototype || prototype === null;
381
422
  if (!isObject) {
382
423
  throw err("non-plain-object", value);
383
424
  }
384
- this.map(value);
425
+ this.entries(Object.entries(value), value);
385
426
  }
386
427
  finally {
387
428
  this.ancestors.delete(value);
@@ -412,18 +453,84 @@ class Encoder {
412
453
  }
413
454
  this.byte(NIL_EXT);
414
455
  }
415
- map(value) {
416
- const keys = Object.keys(value).sort();
456
+ set(value) {
457
+ // Elixir 1.20 uses the version-2 Erlang sets map representation.
458
+ const members = [];
459
+ for (const member of value)
460
+ members.push([member, []]);
461
+ const memberBytes = this.mapBytes(members, value, true);
462
+ const map = new RawTerm(memberBytes);
463
+ const fields = [
464
+ [atom("__struct__"), atom("Elixir.MapSet")],
465
+ [atom("map"), map],
466
+ ];
467
+ this.entries(fields, value);
468
+ }
469
+ date(value) {
470
+ const year = value.getUTCFullYear();
471
+ const validDate = Number.isFinite(value.getTime());
472
+ const supportedYear = year >= -9999 && year <= 9999;
473
+ if (!validDate || !supportedYear) {
474
+ throw err("unsupported", value);
475
+ }
476
+ const fields = {
477
+ __struct__: atom("Elixir.DateTime"),
478
+ calendar: atom("Elixir.Calendar.ISO"),
479
+ year,
480
+ month: value.getUTCMonth() + 1,
481
+ day: value.getUTCDate(),
482
+ hour: value.getUTCHours(),
483
+ minute: value.getUTCMinutes(),
484
+ second: value.getUTCSeconds(),
485
+ microsecond: tuple(value.getUTCMilliseconds() * 1000, 3),
486
+ time_zone: "Etc/UTC",
487
+ zone_abbr: "UTC",
488
+ utc_offset: 0,
489
+ std_offset: 0,
490
+ };
491
+ const entries = [];
492
+ for (const [key, item] of Object.entries(fields)) {
493
+ entries.push([atom(key), item]);
494
+ }
495
+ this.entries(entries, value);
496
+ }
497
+ termBytes(value) {
498
+ const start = this.output.length;
499
+ this.value(value);
500
+ return this.output.splice(start);
501
+ }
502
+ mapBytes(entries, source, merge = false) {
503
+ const encoded = [...entries].map(([key, value]) => ({
504
+ key: this.termBytes(key),
505
+ value: this.termBytes(value),
506
+ }));
507
+ encoded.sort((left, right) => compareKeys(left.key, right.key));
508
+ const unique = [];
509
+ for (const entry of encoded) {
510
+ const previous = unique.at(-1);
511
+ const duplicate = previous !== undefined && compareBytes(entry.key, previous.key) === 0;
512
+ if (duplicate) {
513
+ if (!merge)
514
+ throw err("unsupported", source);
515
+ continue;
516
+ }
517
+ unique.push(entry);
518
+ }
519
+ const start = this.output.length;
417
520
  this.byte(MAP_EXT);
418
- this.uint32(keys.length);
419
- for (const key of keys) {
420
- this.binary(key);
421
- this.value(value[key]);
521
+ this.uint32(unique.length);
522
+ for (const entry of unique) {
523
+ this.bytes(entry.key);
524
+ this.bytes(entry.value);
422
525
  }
526
+ return new Uint8Array(this.output.splice(start));
527
+ }
528
+ entries(entries, source) {
529
+ this.bytes(this.mapBytes(entries, source));
423
530
  }
424
531
  atom(atom) {
425
532
  const bytes = UTF8$1.encode(atom);
426
- if (bytes.length >= 2 ** 16) {
533
+ if ([...atom].length > 255) {
427
534
  throw err("unsupported", atom);
428
535
  }
429
536
  if (bytes.length < 2 ** 8) {
@@ -437,7 +544,7 @@ class Encoder {
437
544
  this.bytes(bytes);
438
545
  }
439
546
  binary(value) {
440
- const bytes = UTF8$1.encode(value);
547
+ const bytes = typeof value === "string" ? UTF8$1.encode(value) : value;
441
548
  this.byte(BINARY_EXT);
442
549
  this.uint32(bytes.length);
443
550
  this.bytes(bytes);
@@ -470,6 +577,21 @@ class Encoder {
470
577
  function err(reason, part) {
471
578
  return new TypeError(reason, { cause: part });
472
579
  }
580
+ function compareBytes(a, b, start = 0) {
581
+ const length = Math.min(a.length, b.length);
582
+ for (let i = start; i < length; i++) {
583
+ if (a[i] !== b[i])
584
+ return a[i] - b[i];
585
+ }
586
+ return a.length - b.length;
587
+ }
588
+ function compareKeys(left, right) {
589
+ const leftTag = left[0];
590
+ const rightTag = right[0];
591
+ const binaryKeys = leftTag === BINARY_EXT && rightTag === BINARY_EXT;
592
+ const offset = binaryKeys ? BINARY_HEADER_SIZE : 0;
593
+ return compareBytes(left, right, offset);
594
+ }
473
595
 
474
596
  function readWorkerEvent(value) {
475
597
  const data = objectWithKeys(value, ["type", "payload"]);
@@ -544,6 +666,10 @@ function assertRunJsFn(value) {
544
666
  * Use {@link Popcorn.init} to create and start an instance.
545
667
  **/
546
668
  class Popcorn {
669
+ /** Receives stdout. Can be replaced at any time; `undefined` restores the default output handler. */
670
+ onStdout;
671
+ /** Receives stderr. Can be replaced at any time; `undefined` restores the default output handler. */
672
+ onStderr;
547
673
  vmWorker;
548
674
  state = { status: "created" };
549
675
  opts;
@@ -561,10 +687,14 @@ class Popcorn {
561
687
  trackedKeySeq = 0;
562
688
  io = createIoState();
563
689
  vmReady = false;
564
- genserver = {
565
- call: (target, request, opts) => this.call(target, request, opts),
566
- cast: (target, request, opts) => this.cast(target, request, opts),
567
- };
690
+ /** Calls a GenServer and returns its reply. See {@link tryCall} for a result instead of throwing. */
691
+ async call(target, request, opts) {
692
+ return unwrap(await this.tryCall(target, request, opts));
693
+ }
694
+ /** Sends a cast through the proxy. See {@link tryCast} for a result instead of throwing. */
695
+ async cast(target, request, opts) {
696
+ unwrap(await this.tryCast(target, request, opts));
697
+ }
568
698
  TrackedValue = class {
569
699
  value;
570
700
  cleanup;
@@ -635,7 +765,11 @@ class Popcorn {
635
765
  },
636
766
  };
637
767
  this.ttySize = { ...ttySize };
638
- this.output = resolveOutputHandlers(opts);
768
+ this.onStdout = opts.onStdout;
769
+ this.onStderr = opts.onStderr;
770
+ this.output = resolveOutputHandlers(this.opts, this);
771
+ if (opts.onEvent)
772
+ this.onEvent(opts.onEvent);
639
773
  this.spawnWorker();
640
774
  }
641
775
  spawnWorker() {
@@ -650,20 +784,18 @@ class Popcorn {
650
784
  /**
651
785
  * Creates an instance and waits for {@link boot}.
652
786
  *
653
- * For startup messages, use the constructor and register {@link onEvent} before boot.
787
+ * Use `opts.onEvent` to receive messages sent during startup.
788
+ *
789
+ * @returns The instance. Rejects on failure.
654
790
  *
655
- * @returns Ok tuple or `runtime:eval-unavailable` if the page blocks JavaScript evaluation.
791
+ * @see {@link PopcornOpts}
656
792
  */
657
793
  static async init(opts = {}) {
658
794
  if (!canEval()) {
659
- return { ok: false, error: err$1("runtime:eval-unavailable", {}) };
795
+ throw err$1("runtime:eval-unavailable", {});
660
796
  }
661
797
  const popcorn = new Popcorn(opts);
662
- const result = await popcorn.boot();
663
- if (!result.ok) {
664
- return result;
665
- }
666
- return { ok: true, data: popcorn };
798
+ return await popcorn.boot();
667
799
  }
668
800
  /**
669
801
  * Starts the VM and waits for its bridge and entrypoint application.
@@ -672,37 +804,29 @@ class Popcorn {
672
804
  *
673
805
  * After shutdown, starts a fresh VM with the original options.
674
806
  *
675
- * @returns Ok tuple with `this` if boot completes or error tuple.
807
+ * @returns This instance. Rejects on failure.
676
808
  *
677
809
  * @example
678
810
  * ```ts
679
811
  * const popcorn = new Popcorn({});
680
812
  * popcorn.onEvent((message) => console.log(message));
681
- * const result = await popcorn.boot();
682
- * if (!result.ok) throw result.error;
813
+ * await popcorn.boot();
683
814
  * ```
684
815
  */
685
816
  async boot() {
686
- if (this.state.status === "booted") {
687
- return { ok: true, data: this };
688
- }
689
- if (this.state.status === "booting") {
690
- // TODO(jgonet): make it easier to construct check() errors without throwing
691
- const error = err$1("internal:check", {
692
- detail: "Boot already in progress",
693
- });
694
- return { ok: false, error };
695
- }
817
+ if (this.state.status === "booted")
818
+ return this;
819
+ check(this.state.status !== "booting", "Boot already in progress");
696
820
  const reboot = this.state.status === "closed";
697
821
  if (reboot) {
698
822
  this.spawnWorker();
699
823
  }
700
824
  this.Pid = createPidClass();
701
825
  this.io = createIoState();
702
- this.output = resolveOutputHandlers(this.opts);
826
+ this.output = resolveOutputHandlers(this.opts, this);
703
827
  check(this.readyWaiters.length === 0);
704
828
  this.state = { status: "booting" };
705
- return await new Promise((resolve) => {
829
+ return await new Promise((resolve, reject) => {
706
830
  const timeoutsMs = { ...DEFAULT_TIMEOUTS_MS, ...this.opts.timeoutsMs };
707
831
  const settle = (result) => {
708
832
  if (this.settleBoot === null)
@@ -712,7 +836,10 @@ class Popcorn {
712
836
  if (!result.ok) {
713
837
  this.deinit();
714
838
  }
715
- resolve(result);
839
+ if (result.ok)
840
+ resolve(result.data);
841
+ else
842
+ reject(result.error);
716
843
  };
717
844
  this.settleBoot = settle;
718
845
  const startPhase = (timeoutMs) => setTimeout(() => {
@@ -763,13 +890,12 @@ class Popcorn {
763
890
  *
764
891
  * Encodes strings as UTF-8 and copies byte arrays. Does not append a newline.
765
892
  *
766
- * Returns `stdio:overflow` if the chunk exceeds the remaining 64 KiB queue capacity.
893
+ * Throws `stdio:overflow` if the chunk exceeds the remaining 64 KiB queue capacity.
767
894
  * An overflow leaves the queue unchanged.
768
895
  */
769
896
  writeStdin(chunk) {
770
- if (this.state.status === "closed") {
771
- return { ok: false, error: this.state.error };
772
- }
897
+ if (this.state.status === "closed")
898
+ throw this.state.error;
773
899
  check(this.state.status === "booted");
774
900
  const bytes = toBytes(chunk);
775
901
  check(bytes.byteLength > 0);
@@ -779,29 +905,28 @@ class Popcorn {
779
905
  capacityBytes: STDIN_QUEUE_CAPACITY_BYTES,
780
906
  attemptedBytes,
781
907
  });
782
- return { ok: false, error };
908
+ throw error;
783
909
  }
784
910
  this.io.stdin.reservedBytes = attemptedBytes;
785
911
  const event = { type: "popcorn:stdin", payload: { chunk: bytes } };
786
912
  toVm(this.vmWorker, event, [bytes.buffer]);
787
- return { ok: true, data: null };
788
913
  }
789
914
  /**
790
915
  * Sends new terminal dimensions to a booted VM.
791
916
  *
792
917
  * Each dimension must be between 1 and 65,535.
918
+ *
919
+ * Throws on failure.
793
920
  */
794
921
  resizeTty(columns, rows) {
795
- if (this.state.status === "closed") {
796
- return { ok: false, error: this.state.error };
797
- }
922
+ if (this.state.status === "closed")
923
+ throw this.state.error;
798
924
  check(this.state.status === "booted");
799
925
  check(isValidTtySize({ columns, rows }));
800
926
  toVm(this.vmWorker, {
801
927
  type: "popcorn:tty-resize",
802
928
  payload: { columns, rows },
803
929
  });
804
- return { ok: true, data: null };
805
930
  }
806
931
  /**
807
932
  * Sends a payload to a registered process name or a {@link Pid} from this VM boot.
@@ -810,21 +935,26 @@ class Popcorn {
810
935
  * A send timeout does not cancel delivery.
811
936
  * Uses the value conversions in {@link AnyValue}. An omitted, `null`, or `undefined` payload becomes an empty map.
812
937
  *
813
- * @returns Ok tuple or `bridge:not-started` before boot and `vm:exited` after shutdown.
938
+ * @returns Rejects on failure.
814
939
  *
815
940
  * @example
816
941
  * Send an Erlang `{ok, <<"value">>}` tuple to a registered `receiver` process.
817
942
  *
818
943
  * ```ts
819
- * const result = await popcorn.send("receiver", tuple(atom("ok"), "value"));
820
- * if (!result.ok) throw result.error;
944
+ * await popcorn.send("receiver", tuple(atom("ok"), "value"));
821
945
  * ```
822
946
  *
823
947
  * @see {@link AnyValue}
824
948
  * @see {@link atom}
825
949
  * @see {@link tuple}
950
+ *
951
+ * @see {@link trySend} for a result instead of throwing on expected failures.
826
952
  */
827
953
  async send(rawTarget, payload) {
954
+ unwrap(await this.trySend(rawTarget, payload));
955
+ }
956
+ /** See {@link send}. */
957
+ async trySend(rawTarget, payload) {
828
958
  if (this.state.status !== "booted") {
829
959
  if (this.state.status === "closed") {
830
960
  return { ok: false, error: this.state.error };
@@ -978,7 +1108,16 @@ class Popcorn {
978
1108
  unreachable();
979
1109
  }
980
1110
  }
981
- async call(rawTarget, request, opts) {
1111
+ /**
1112
+ * Calls a GenServer by registered name or {@link Pid} and waits for its reply.
1113
+ *
1114
+ * @param opts - `timeoutMs` defaults to 5 000 ms. `proxy` allows to select another registered proxy (see `Popcorn.Proxy`).
1115
+ * @returns A {@link Result} with the reply or a bridge, VM, timeout, or GenServer error.
1116
+ *
1117
+ * GenServer failures use `genserver:noproc`, `genserver:exit`, or `genserver:unserializable`.
1118
+ * A call timeout does not cancel server work.
1119
+ */
1120
+ async tryCall(rawTarget, request, opts) {
982
1121
  if (this.state.status !== "booted") {
983
1122
  if (this.state.status === "closed") {
984
1123
  return { ok: false, error: this.state.error };
@@ -1021,7 +1160,14 @@ class Popcorn {
1021
1160
  }
1022
1161
  return result;
1023
1162
  }
1024
- async cast(rawTarget, request, opts) {
1163
+ /**
1164
+ * Sends a cast through the proxy.
1165
+ *
1166
+ * Success confirms delivery to the proxy
1167
+ * @see {@link tryCall}
1168
+ * @param opts - `proxy` allows to select another registered proxy.
1169
+ */
1170
+ async tryCast(rawTarget, request, opts) {
1025
1171
  if (this.state.status !== "booted") {
1026
1172
  if (this.state.status === "closed") {
1027
1173
  return { ok: false, error: this.state.error };
@@ -1050,9 +1196,16 @@ class Popcorn {
1050
1196
  const args = this.reviveHandles(request.args);
1051
1197
  check(this.vmReady);
1052
1198
  const actions = {
1053
- send: (target, payload) => this.sendBridge(target, payload),
1054
- call: (target, payload, opts) => this.callBridge(target, payload, opts),
1055
- cast: (target, payload, opts) => this.castBridge(target, payload, opts),
1199
+ send: async (target, payload) => {
1200
+ unwrap(await this.sendBridge(target, payload));
1201
+ },
1202
+ call: async (target, payload, opts) => unwrap(await this.callBridge(target, payload, opts)),
1203
+ cast: async (target, payload, opts) => {
1204
+ unwrap(await this.castBridge(target, payload, opts));
1205
+ },
1206
+ trySend: (target, payload) => this.sendBridge(target, payload),
1207
+ tryCall: (target, payload, opts) => this.callBridge(target, payload, opts),
1208
+ tryCast: (target, payload, opts) => this.castBridge(target, payload, opts),
1056
1209
  };
1057
1210
  const result = await fn(args, actions);
1058
1211
  const value = request.return === "ref" ? this.asRef(result) : result;
@@ -1110,6 +1263,10 @@ class Popcorn {
1110
1263
  return make(this.TrackedValue);
1111
1264
  }
1112
1265
  reviveHandles(value) {
1266
+ const marker = objectWithKeys(value, ["popcorn_value"]);
1267
+ if (marker !== null && Object.keys(marker).length === 1) {
1268
+ return this.reviveValueTag(marker.popcorn_value);
1269
+ }
1113
1270
  const key = trackedRefKey(value);
1114
1271
  if (key !== null) {
1115
1272
  const entry = this.trackedValues.get(key);
@@ -1125,14 +1282,55 @@ class Popcorn {
1125
1282
  }
1126
1283
  const obj = objectWithKeys(value, []);
1127
1284
  if (obj !== null) {
1128
- const revived = {};
1129
- for (const [k, v] of Object.entries(obj)) {
1130
- revived[k] = this.reviveHandles(v);
1131
- }
1132
- return revived;
1285
+ const entries = Object.entries(obj).map(([key, item]) => [
1286
+ key,
1287
+ this.reviveHandles(item),
1288
+ ]);
1289
+ return Object.fromEntries(entries);
1133
1290
  }
1134
1291
  return value;
1135
1292
  }
1293
+ reviveValueTag(payload) {
1294
+ check(Array.isArray(payload) && payload.length === 2);
1295
+ const [tag, data] = payload;
1296
+ switch (tag) {
1297
+ case "binary":
1298
+ check(typeof data === "string");
1299
+ return base64ToBytes(data);
1300
+ case "integer": {
1301
+ const integer = readInteger(data);
1302
+ const safe = integer >= BigInt(Number.MIN_SAFE_INTEGER) &&
1303
+ integer <= BigInt(Number.MAX_SAFE_INTEGER);
1304
+ if (safe)
1305
+ return Number(integer);
1306
+ return integer;
1307
+ }
1308
+ case "datetime": {
1309
+ const milliseconds = readInteger(data);
1310
+ const inRange = milliseconds >= -377705116800000n && milliseconds <= 253402300799999n;
1311
+ check(inRange);
1312
+ return new Date(Number(milliseconds));
1313
+ }
1314
+ case "set":
1315
+ check(Array.isArray(data));
1316
+ return new Set(data.map((item) => this.reviveHandles(item)));
1317
+ case "object": {
1318
+ check(Array.isArray(data));
1319
+ const entries = [];
1320
+ for (const entry of data) {
1321
+ check(Array.isArray(entry) && entry.length === 2);
1322
+ const [key, value] = entry;
1323
+ check(typeof key === "string");
1324
+ entries.push([key, this.reviveHandles(value)]);
1325
+ }
1326
+ const object = Object.fromEntries(entries);
1327
+ check(Object.keys(object).length === entries.length);
1328
+ return object;
1329
+ }
1330
+ default:
1331
+ check(false, "unknown popcorn value tag");
1332
+ }
1333
+ }
1136
1334
  /** Maps pids and `TrackedValue`s during encoding, collecting handles into
1137
1335
  * `tracked` for the caller to register once encoding succeeds. */
1138
1336
  handleMapper(tracked) {
@@ -1228,22 +1426,20 @@ function isValidTtySize({ columns, rows }) {
1228
1426
  const rowInRange = 0 < rows && rows <= 0xffff;
1229
1427
  return colInRange && rowInRange;
1230
1428
  }
1231
- function resolveOutputHandlers(opts) {
1429
+ function resolveOutputHandlers(opts, callbacks) {
1232
1430
  if (opts.tty?.output === "bytes") {
1233
- const onStdout = opts.onStdout;
1234
- const onStderr = opts.onStderr;
1235
1431
  return {
1236
- stdout: onStdout ?? defaultOnStdoutBytes,
1237
- stderr: onStderr ?? defaultOnStderrBytes,
1432
+ stdout: (chunk) => (callbacks.onStdout ??
1433
+ defaultOnStdoutBytes)(chunk),
1434
+ stderr: (chunk) => (callbacks.onStderr ??
1435
+ defaultOnStderrBytes)(chunk),
1238
1436
  };
1239
1437
  }
1240
1438
  const stdoutDecoder = new TextDecoder();
1241
1439
  const stderrDecoder = new TextDecoder();
1242
- const onStdout = opts.onStdout ?? defaultOnStdout;
1243
- const onStderr = opts.onStderr ?? defaultOnStderr;
1244
1440
  return {
1245
- stdout: (chunk) => decodeOutput(stdoutDecoder, onStdout, chunk),
1246
- stderr: (chunk) => decodeOutput(stderrDecoder, onStderr, chunk),
1441
+ stdout: (chunk) => decodeOutput(stdoutDecoder, callbacks.onStdout ?? defaultOnStdout, chunk),
1442
+ stderr: (chunk) => decodeOutput(stderrDecoder, callbacks.onStderr ?? defaultOnStderr, chunk),
1247
1443
  };
1248
1444
  }
1249
1445
  function decodeOutput(decoder, onOutput, chunk) {
@@ -1333,5 +1529,10 @@ function defaultOnError(payload) {
1333
1529
  unreachable();
1334
1530
  }
1335
1531
  }
1532
+ function readInteger(value) {
1533
+ check(typeof value === "string");
1534
+ check(/^-?(0|[1-9][0-9]*)$/.test(value));
1535
+ return BigInt(value);
1536
+ }
1336
1537
 
1337
1538
  export { Popcorn, PopcornError, a, atom, schedulers, t, tuple };
@@ -6,26 +6,8 @@ type RuntimeVariant = "core" | "crypto";
6
6
  * compiles the project before packaging it.
7
7
  */
8
8
  export type Options = {
9
- /** Runtime variant override. */
10
- runtimeVariant?: RuntimeVariant;
11
- /** Mix project directory. */
9
+ /** Mix project directory. Cooking options belong in its config/config.exs. */
12
10
  rootDir: string;
13
- /**
14
- * OTP application to start after VM boot.
15
- * Defaults to the current Mix application.
16
- */
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;
26
- };
27
- export type TreeshakeOptions = {
28
- preservedApps?: string[];
29
11
  };
30
12
  export type Prepared = {
31
13
  dir: string;
@@ -9,26 +9,6 @@ const execFileAsync = promisify(execFile);
9
9
  async function popcorn(options) {
10
10
  const dir = await mkdtemp(`${tmpdir()}/popcorn-otp-`);
11
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
12
  try {
33
13
  await execFileAsync("mix", args, {
34
14
  cwd: resolve(options.rootDir),
@@ -43,6 +43,7 @@ function popcorn(options) {
43
43
  let dirty = false;
44
44
  let outDir;
45
45
  let assetsDir = "assets";
46
+ let workerDerivedOtpRoot = `/@fs${distDir}/otp/`;
46
47
  // Dev/preview: pack once at server startup, then re-pack lazily after the
47
48
  // app's compiled beams change. A single in-flight promise dedupes concurrent
48
49
  // requests; a change during a re-pack re-sets `dirty` so it isn't lost.
@@ -78,7 +79,7 @@ function popcorn(options) {
78
79
  return;
79
80
  }
80
81
  const requestPath = decodeURIComponent(new URL(url, "http://localhost").pathname);
81
- const requestRoot = [`/${assetsDir}/otp/`, `/@fs${distDir}/otp/`].find((root) => requestPath.startsWith(root));
82
+ const requestRoot = [`/${assetsDir}/otp/`, workerDerivedOtpRoot].find((root) => requestPath.startsWith(root));
82
83
  if (requestRoot === undefined) {
83
84
  next();
84
85
  return;
@@ -146,6 +147,10 @@ function popcorn(options) {
146
147
  config.server.fs.allow.push(distDir);
147
148
  outDir = resolve(config.root, config.build.outDir);
148
149
  assetsDir = config.build.assetsDir;
150
+ const servedDistDir = isUnder(config.root, distDir)
151
+ ? `/${relative(config.root, distDir)}`
152
+ : `/@fs${distDir}`;
153
+ workerDerivedOtpRoot = `${servedDistDir}/otp/`;
149
154
  },
150
155
  async configureServer(server) {
151
156
  const rootDir = resolve(options.rootDir);
package/dist/popcorn.d.ts CHANGED
@@ -54,14 +54,14 @@ export type PopcornOpts<Output extends TtyOutput = "text"> = {
54
54
  * Receives stdout.
55
55
  *
56
56
  * Defaults to `console.log`.
57
- * When `tty.output` is "bytes", we pass an `ArrayBuffer` as an argument and `string` otherwise
57
+ * Receives `Uint8Array` when `tty.output` is "bytes", and `string` otherwise.
58
58
  */
59
59
  onStdout?: (chunk: OutputChunk<Output>) => void;
60
60
  /**
61
61
  * Receives stderr.
62
62
  *
63
63
  * Defaults to `console.error`.
64
- * When `tty.output` is "bytes", we pass an `ArrayBuffer` as an argument and `string` otherwise
64
+ * Receives `Uint8Array` when `tty.output` is "bytes", and `string` otherwise.
65
65
  */
66
66
  onStderr?: (chunk: OutputChunk<Output>) => void;
67
67
  /**
@@ -70,6 +70,8 @@ export type PopcornOpts<Output extends TtyOutput = "text"> = {
70
70
  * Defaults to console output.
71
71
  */
72
72
  onError?: (event: OtpErrorPayload) => void;
73
+ /** Receives BEAM message payloads, including messages sent during startup. */
74
+ onEvent?: (event: PopcornEvent) => void;
73
75
  /**
74
76
  * Module worker URL.
75
77
  *
@@ -93,36 +95,17 @@ type CallOpts = {
93
95
  timeoutMs?: number;
94
96
  proxy?: string;
95
97
  };
96
- export type GenServer = {
97
- /**
98
- * Calls a GenServer by registered name or {@link Pid} and waits for its reply.
99
- *
100
- * Requires a running `Popcorn.Proxy`, registered as `popcorn_proxy` by default.
101
- *
102
- * @param opts - `timeoutMs` defaults to 5 000 ms. `proxy` allows to select another registered proxy.
103
- * @returns A {@link Result} with the reply or a bridge, VM, timeout, or GenServer error.
104
- *
105
- * GenServer failures use `genserver:noproc`, `genserver:exit`, or `genserver:unserializable`.
106
- * A call timeout does not cancel server work.
107
- */
108
- call(target: string | Pid, request?: AnyValue, opts?: CallOpts): Promise<Result<AnyValue>>;
109
- /**
110
- * Sends a cast through the proxy.
111
- *
112
- * Success confirms delivery to the proxy
113
- * @see {@link call}
114
- * @param opts - `proxy` allows to select another registered proxy.
115
- */
116
- cast(target: string | Pid, request?: AnyValue, opts?: {
117
- proxy?: string;
118
- }): Promise<Result<null>>;
119
- };
98
+ export type GenServer = Pick<Popcorn, "call" | "cast" | "tryCall" | "tryCast">;
120
99
  /**
121
100
  * A BEAM VM in a browser worker.
122
101
  *
123
102
  * Use {@link Popcorn.init} to create and start an instance.
124
103
  **/
125
104
  export declare class Popcorn<Output extends TtyOutput = "text"> {
105
+ /** Receives stdout. Can be replaced at any time; `undefined` restores the default output handler. */
106
+ onStdout?: (chunk: OutputChunk<Output>) => void;
107
+ /** Receives stderr. Can be replaced at any time; `undefined` restores the default output handler. */
108
+ onStderr?: (chunk: OutputChunk<Output>) => void;
126
109
  private vmWorker;
127
110
  private state;
128
111
  private readonly opts;
@@ -140,7 +123,12 @@ export declare class Popcorn<Output extends TtyOutput = "text"> {
140
123
  private trackedKeySeq;
141
124
  private io;
142
125
  private vmReady;
143
- readonly genserver: GenServer;
126
+ /** Calls a GenServer and returns its reply. See {@link tryCall} for a result instead of throwing. */
127
+ call(target: string | Pid, request?: AnyValue, opts?: CallOpts): Promise<AnyValue>;
128
+ /** Sends a cast through the proxy. See {@link tryCast} for a result instead of throwing. */
129
+ cast(target: string | Pid, request?: AnyValue, opts?: {
130
+ proxy?: string;
131
+ }): Promise<void>;
144
132
  private readonly TrackedValue;
145
133
  private Pid;
146
134
  private readonly onWorkerMessage;
@@ -154,11 +142,13 @@ export declare class Popcorn<Output extends TtyOutput = "text"> {
154
142
  /**
155
143
  * Creates an instance and waits for {@link boot}.
156
144
  *
157
- * For startup messages, use the constructor and register {@link onEvent} before boot.
145
+ * Use `opts.onEvent` to receive messages sent during startup.
146
+ *
147
+ * @returns The instance. Rejects on failure.
158
148
  *
159
- * @returns Ok tuple or `runtime:eval-unavailable` if the page blocks JavaScript evaluation.
149
+ * @see {@link PopcornOpts}
160
150
  */
161
- static init<Output extends TtyOutput = "text">(opts?: PopcornOpts<Output>): Promise<Result<Popcorn<Output>>>;
151
+ static init<Output extends TtyOutput = "text">(opts?: PopcornOpts<Output>): Promise<Popcorn<Output>>;
162
152
  /**
163
153
  * Starts the VM and waits for its bridge and entrypoint application.
164
154
  *
@@ -166,32 +156,33 @@ export declare class Popcorn<Output extends TtyOutput = "text"> {
166
156
  *
167
157
  * After shutdown, starts a fresh VM with the original options.
168
158
  *
169
- * @returns Ok tuple with `this` if boot completes or error tuple.
159
+ * @returns This instance. Rejects on failure.
170
160
  *
171
161
  * @example
172
162
  * ```ts
173
163
  * const popcorn = new Popcorn({});
174
164
  * popcorn.onEvent((message) => console.log(message));
175
- * const result = await popcorn.boot();
176
- * if (!result.ok) throw result.error;
165
+ * await popcorn.boot();
177
166
  * ```
178
167
  */
179
- boot(): Promise<Result<Popcorn<Output>>>;
168
+ boot(): Promise<Popcorn<Output>>;
180
169
  /**
181
170
  * Queues terminal input.
182
171
  *
183
172
  * Encodes strings as UTF-8 and copies byte arrays. Does not append a newline.
184
173
  *
185
- * Returns `stdio:overflow` if the chunk exceeds the remaining 64 KiB queue capacity.
174
+ * Throws `stdio:overflow` if the chunk exceeds the remaining 64 KiB queue capacity.
186
175
  * An overflow leaves the queue unchanged.
187
176
  */
188
- writeStdin(chunk: string | Uint8Array): Result<null>;
177
+ writeStdin(chunk: string | Uint8Array): void;
189
178
  /**
190
179
  * Sends new terminal dimensions to a booted VM.
191
180
  *
192
181
  * Each dimension must be between 1 and 65,535.
182
+ *
183
+ * Throws on failure.
193
184
  */
194
- resizeTty(columns: number, rows: number): Result<null>;
185
+ resizeTty(columns: number, rows: number): void;
195
186
  /**
196
187
  * Sends a payload to a registered process name or a {@link Pid} from this VM boot.
197
188
  *
@@ -199,21 +190,24 @@ export declare class Popcorn<Output extends TtyOutput = "text"> {
199
190
  * A send timeout does not cancel delivery.
200
191
  * Uses the value conversions in {@link AnyValue}. An omitted, `null`, or `undefined` payload becomes an empty map.
201
192
  *
202
- * @returns Ok tuple or `bridge:not-started` before boot and `vm:exited` after shutdown.
193
+ * @returns Rejects on failure.
203
194
  *
204
195
  * @example
205
196
  * Send an Erlang `{ok, <<"value">>}` tuple to a registered `receiver` process.
206
197
  *
207
198
  * ```ts
208
- * const result = await popcorn.send("receiver", tuple(atom("ok"), "value"));
209
- * if (!result.ok) throw result.error;
199
+ * await popcorn.send("receiver", tuple(atom("ok"), "value"));
210
200
  * ```
211
201
  *
212
202
  * @see {@link AnyValue}
213
203
  * @see {@link atom}
214
204
  * @see {@link tuple}
205
+ *
206
+ * @see {@link trySend} for a result instead of throwing on expected failures.
215
207
  */
216
- send(rawTarget: string | Pid, payload?: AnyValue): Promise<Result<null>>;
208
+ send(rawTarget: string | Pid, payload?: AnyValue): Promise<void>;
209
+ /** See {@link send}. */
210
+ trySend(rawTarget: string | Pid, payload?: AnyValue): Promise<Result<null>>;
217
211
  private sendBridge;
218
212
  /**
219
213
  * Registers a handler for BEAM message payloads.
@@ -235,9 +229,27 @@ export declare class Popcorn<Output extends TtyOutput = "text"> {
235
229
  private emit;
236
230
  private completeCall;
237
231
  private parseCallReply;
238
- private call;
232
+ /**
233
+ * Calls a GenServer by registered name or {@link Pid} and waits for its reply.
234
+ *
235
+ * @param opts - `timeoutMs` defaults to 5 000 ms. `proxy` allows to select another registered proxy (see `Popcorn.Proxy`).
236
+ * @returns A {@link Result} with the reply or a bridge, VM, timeout, or GenServer error.
237
+ *
238
+ * GenServer failures use `genserver:noproc`, `genserver:exit`, or `genserver:unserializable`.
239
+ * A call timeout does not cancel server work.
240
+ */
241
+ tryCall(rawTarget: string | Pid, request?: AnyValue, opts?: CallOpts): Promise<Result<AnyValue>>;
239
242
  private callBridge;
240
- private cast;
243
+ /**
244
+ * Sends a cast through the proxy.
245
+ *
246
+ * Success confirms delivery to the proxy
247
+ * @see {@link tryCall}
248
+ * @param opts - `proxy` allows to select another registered proxy.
249
+ */
250
+ tryCast(rawTarget: string | Pid, request?: AnyValue, opts?: {
251
+ proxy?: string;
252
+ }): Promise<Result<null>>;
241
253
  private castBridge;
242
254
  private nextCallId;
243
255
  private runJs;
@@ -248,6 +260,7 @@ export declare class Popcorn<Output extends TtyOutput = "text"> {
248
260
  private sendBridgeReply;
249
261
  private jsWithCurrentEnv;
250
262
  private reviveHandles;
263
+ private reviveValueTag;
251
264
  /** Maps pids and `TrackedValue`s during encoding, collecting handles into
252
265
  * `tracked` for the caller to register once encoding succeeds. */
253
266
  private handleMapper;
Binary file
Binary file
package/dist/types.d.ts CHANGED
@@ -1,15 +1,8 @@
1
1
  type CreateModuleFn<Mod> = (overrides?: Partial<Mod>) => Promise<Mod>;
2
2
  /**
3
- * A message value. JavaScript sends use these BEAM conversions:
3
+ * A value exchanged with BEAM. Containers convert their contents recursively.
4
4
  *
5
- * - Strings become UTF-8 binaries. Booleans become `true` and `false` atoms.
6
- * - Integers become integers. Other finite numbers become floats.
7
- * - Arrays become lists.
8
- * - Plain objects become maps with binary string keys.
9
- * - `null` and `undefined` become the `nil` atom.
10
- * - `atom()` and `tuple()` create atoms and tuples. PIDs retain their BEAM identity.
11
- *
12
- * Cycles, class instances, functions, symbols, bigints, unsafe integers, and non-finite numbers cause `bridge:unserializable`.
5
+ * See [Sharing values across languages](values.html) for supported types and conversions.
13
6
  */
14
7
  export type AnyValue = unknown;
15
8
  declare const pidBrand: unique symbol;
package/dist/worker.mjs CHANGED
@@ -222,7 +222,7 @@ function unreachable() {
222
222
  function objectWithKeys(value, keys) {
223
223
  if (value === null || typeof value !== "object")
224
224
  return null;
225
- if (value.constructor !== Object)
225
+ if (Object.getPrototypeOf(value) !== Object.prototype)
226
226
  return null;
227
227
  const hasAllKeys = keys.every((k) => Object.hasOwn(value, k));
228
228
  if (!hasAllKeys)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@swmansion/popcorn",
3
- "version": "0.4.0-next.4",
3
+ "version": "0.4.0-next.6",
4
4
  "description": "JS bindings for BEAM/OTP in WebAssembly",
5
5
  "type": "module",
6
6
  "license": "Apache-2.0",