repl-sdk 1.6.0 → 1.6.1

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "repl-sdk",
3
- "version": "1.6.0",
3
+ "version": "1.6.1",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": {
@@ -96,9 +96,9 @@
96
96
  "unified": "^11.0.5",
97
97
  "unist-util-visit": "^5.0.0",
98
98
  "vfile": "^6.0.3",
99
- "codemirror-lang-glimdown": "^2.0.4",
100
99
  "codemirror-lang-glimmer": "^2.0.4",
101
- "codemirror-lang-glimmer-js": "^2.0.4"
100
+ "codemirror-lang-glimmer-js": "^2.0.4",
101
+ "codemirror-lang-glimdown": "^2.0.4"
102
102
  },
103
103
  "volta": {
104
104
  "extends": "../../package.json"
@@ -2,6 +2,8 @@
2
2
  * @typedef {import('../types.ts').CompilerConfig} CompilerConfig
3
3
  */
4
4
 
5
+ import { errorMessage } from '../utils.js';
6
+
5
7
  /**
6
8
  * Other `@ember` (and `@glimmer`) packages are bundled in ember-source,
7
9
  * and typecilaly use a build plugin to resolve from `@ember/*` imports.
@@ -91,7 +93,7 @@ function resolve(id) {
91
93
  function onUnhandled(e, handle) {
92
94
  if (!e.reason?.message) return;
93
95
 
94
- let reason = e.reason.message;
96
+ let reason = errorMessage(e.reason);
95
97
 
96
98
  if (reason.includes('Stack trace for the update:')) {
97
99
  reason += ' (see console)';
@@ -18,6 +18,8 @@ export const jsx = {
18
18
  return `https://esm.sh/react@19.2.3/es2022/react.development.mjs`;
19
19
  case 'react/jsx-dev-runtime':
20
20
  return `https://esm.sh/react@19.2.3/es2022/jsx-dev-runtime.development.mjs`;
21
+ case 'react/jsx-runtime':
22
+ return `https://esm.sh/react@19.2.3/es2022/jsx-runtime.mjs`;
21
23
  case 'react-dom/client':
22
24
  return `https://esm.sh/react-dom@19.2.3/es2022/client.development.mjs`;
23
25
  case '@babel/standalone':
@@ -37,6 +39,18 @@ export const jsx = {
37
39
  [
38
40
  babel.availablePresets.react,
39
41
  {
42
+ /**
43
+ * The production automatic runtime (jsx/jsxs from
44
+ * 'react/jsx-runtime') works under both dev- and
45
+ * production-built hosts.
46
+ *
47
+ * The default (development) transform emits jsxDEV from
48
+ * 'react/jsx-dev-runtime', which a production build of react
49
+ * deliberately exports as undefined — every compiled demo
50
+ * then throws "_jsxDEV is not a function" at evaluation.
51
+ */
52
+ runtime: 'automatic',
53
+ development: false,
40
54
  // useBuiltIns: true,
41
55
  },
42
56
  ],
@@ -53,6 +67,15 @@ export const jsx = {
53
67
 
54
68
  // Wait for react-dom to render
55
69
  await new Promise((resolve) => requestAnimationFrame(resolve));
70
+
71
+ // The return value is this render's destroy handle (see Compiler#render).
72
+ // Without unmounting, every rendered demo leaves a live FiberRootNode
73
+ // pinning its container element -- and through DOM parent/child links,
74
+ // the entire (detached) tree the demo was rendered into. In dev,
75
+ // react-refresh additionally roots every FiberRootNode globally
76
+ // (helpersByRoot), so nothing about an abandoned render is ever
77
+ // garbage-collected.
78
+ return () => root.unmount();
56
79
  },
57
80
  };
58
81
  },
@@ -99,17 +99,20 @@ export const svelte = {
99
99
 
100
100
  element.appendChild(div);
101
101
 
102
- requestAnimationFrame(() => {
103
- // @ts-ignore
104
- svelte.mount(component, {
105
- target: element,
106
- props: {
107
- /* no props */
108
- },
109
- });
102
+ await new Promise((resolve) => requestAnimationFrame(resolve));
110
103
 
111
- api.announce('info', 'Done');
104
+ // @ts-ignore
105
+ const instance = svelte.mount(component, {
106
+ target: element,
107
+ props: {
108
+ /* no props */
109
+ },
112
110
  });
111
+
112
+ api.announce('info', 'Done');
113
+
114
+ // The return value is this render's destroy handle (see Compiler#render).
115
+ return () => svelte.unmount(instance);
113
116
  },
114
117
  };
115
118
  },
@@ -50,8 +50,13 @@ export const vue = {
50
50
  element.appendChild(div);
51
51
  element.appendChild(style);
52
52
 
53
- createApp(component).mount(div);
53
+ const app = createApp(component);
54
+
55
+ app.mount(div);
54
56
  compiler.announce('info', 'Done');
57
+
58
+ // The return value is this render's destroy handle (see Compiler#render).
59
+ return () => app.unmount();
55
60
  },
56
61
  };
57
62
  },
@@ -0,0 +1,58 @@
1
+ import { describe, expect, it } from 'vitest';
2
+
3
+ import { gjs } from './compilers/ember.js';
4
+
5
+ function handleUnhandled(reason: unknown) {
6
+ let handled: string | undefined;
7
+
8
+ gjs.onUnhandled?.({ reason } as PromiseRejectionEvent, (message) => (handled = message));
9
+
10
+ return handled;
11
+ }
12
+
13
+ /**
14
+ * In apps, nothing awaits the compile state's promise, so compile errors
15
+ * also arrive via unhandledrejection — and this handler's announcement is
16
+ * the last one, i.e. the one the UI's error bubble shows.
17
+ */
18
+ describe('gjs onUnhandled', () => {
19
+ it('ignores reasons without a message', () => {
20
+ expect(handleUnhandled(undefined)).toBe(undefined);
21
+ expect(handleUnhandled('boom')).toBe(undefined);
22
+ });
23
+
24
+ it('uses the message of a normal Error', () => {
25
+ expect(handleUnhandled(new Error('boom'))).toBe('boom');
26
+ });
27
+
28
+ it('includes the source_code of an SWC-style error (as thrown by content-tag)', () => {
29
+ const swcError = Object.assign(new Error('Parse Error at dynamic-repl.js:3:16: 3:33'), {
30
+ source_code:
31
+ " × Expected 'from', got 'string literal'\n" +
32
+ ' ╭─[dynamic-repl.js:3:1]\n' +
33
+ " 2 │ import { tracked } from '@glimmer/tracking';\n" +
34
+ " 3 │ import { on } '@ember/modifier';\n" +
35
+ ' · ─────────────────\n' +
36
+ ' ╰────',
37
+ });
38
+
39
+ expect(handleUnhandled(swcError)).toMatchInlineSnapshot(`
40
+ "Parse Error at dynamic-repl.js:3:16: 3:33
41
+
42
+ × Expected 'from', got 'string literal'
43
+ ╭─[dynamic-repl.js:3:1]
44
+ 2 │ import { tracked } from '@glimmer/tracking';
45
+ 3 │ import { on } '@ember/modifier';
46
+ · ─────────────────
47
+ ╰────"
48
+ `);
49
+ });
50
+
51
+ it('points at the console for backtracking-rerender asserts', () => {
52
+ const message = handleUnhandled(
53
+ new Error('Assertion Failed: ...\n\nStack trace for the update:\n...')
54
+ );
55
+
56
+ expect(message).toContain('(see console)');
57
+ });
58
+ });
package/src/index.d.ts CHANGED
@@ -3,6 +3,13 @@ import type { EditorView } from 'codemirror';
3
3
 
4
4
  export type { ErrorMessage, InfoMessage, Message, Options } from './types.ts';
5
5
 
6
+ /**
7
+ * Builds the most useful human-readable message from a thrown error,
8
+ * including the explanation and code-frame from SWC / content-tag
9
+ * parse errors (which hide those on a non-standard `source_code` property).
10
+ */
11
+ export function errorMessage(error: unknown): string;
12
+
6
13
  export const defaultFormats: keyof Options['formats'];
7
14
  export const defaults: Options;
8
15
 
package/src/index.js CHANGED
@@ -10,10 +10,12 @@ import { compilers } from './compilers.js';
10
10
  import { STABLE_REFERENCE } from './es-module-shim.js';
11
11
  import { getTarRequestId } from './request.js';
12
12
  import { getFromTarball } from './tar.js';
13
- import { assert, nextId, prefix_tgz, tgzPrefix, unzippedPrefix } from './utils.js';
13
+ import { assert, errorMessage, nextId, prefix_tgz, tgzPrefix, unzippedPrefix } from './utils.js';
14
14
 
15
15
  assert(`There is no document. repl-sdk is meant to be ran in a browser`, globalThis.document);
16
16
 
17
+ export { errorMessage } from './utils.js';
18
+
17
19
  export const defaultFormats = Object.keys(compilers);
18
20
 
19
21
  export const defaults = {
@@ -98,7 +100,7 @@ export class Compiler {
98
100
 
99
101
  if (handled) return;
100
102
 
101
- this.#announce('error', e.reason);
103
+ this.#announce('error', errorMessage(e.reason));
102
104
  };
103
105
 
104
106
  /**
@@ -355,9 +357,7 @@ export class Compiler {
355
357
  return await this.#compile(format, text, options);
356
358
  } catch (e) {
357
359
  // for on.log usage
358
- const message = e instanceof Error ? e.message : e;
359
-
360
- this.#announce('error', String(message));
360
+ this.#announce('error', errorMessage(e));
361
361
 
362
362
  // Don't hide errors!
363
363
  this.#error(e);
@@ -0,0 +1,45 @@
1
+ import { describe, expect, it } from 'vitest';
2
+
3
+ import { errorMessage } from './utils.js';
4
+
5
+ describe('errorMessage', () => {
6
+ it('handles non-object throwables', () => {
7
+ expect(errorMessage('boom')).toBe('boom');
8
+ expect(errorMessage(undefined)).toBe('undefined');
9
+ expect(errorMessage(null)).toBe('null');
10
+ expect(errorMessage(2)).toBe('2');
11
+ });
12
+
13
+ it('uses the message of a normal Error', () => {
14
+ expect(errorMessage(new Error('boom'))).toBe('boom');
15
+ });
16
+
17
+ it('falls back to String() for objects without message or source_code', () => {
18
+ expect(errorMessage({})).toBe('[object Object]');
19
+ });
20
+
21
+ it('includes the source_code of an SWC-style error (as thrown by content-tag)', () => {
22
+ /**
23
+ * Real shape from content-tag's `Preprocessor#process`:
24
+ * an Error with extra own properties: source_code, source_code_color
25
+ */
26
+ const swcError = Object.assign(new Error('Parse Error at dynamic-repl.js:1:9: 1:10'), {
27
+ source_code:
28
+ ' × Expression expected\n' +
29
+ ' ╭─[dynamic-repl.js:1:1]\n' +
30
+ ' 1 │ let y = ;\n' +
31
+ ' · ─\n' +
32
+ ' ╰────',
33
+ });
34
+
35
+ expect(errorMessage(swcError)).toMatchInlineSnapshot(`
36
+ "Parse Error at dynamic-repl.js:1:9: 1:10
37
+
38
+ × Expression expected
39
+ ╭─[dynamic-repl.js:1:1]
40
+ 1 │ let y = ;
41
+ · ─
42
+ ╰────"
43
+ `);
44
+ });
45
+ });
package/src/utils.js CHANGED
@@ -39,3 +39,36 @@ export function prefix_tgz(url) {
39
39
  export function isRecord(x) {
40
40
  return typeof x === 'object' && x !== null && !Array.isArray(x);
41
41
  }
42
+
43
+ /**
44
+ * Builds the most useful human-readable message from a thrown error.
45
+ *
46
+ * SWC (via content-tag) throws Errors whose `message` is only
47
+ * "Parse Error at <file>:<line>:<column>" — the explanation of what's
48
+ * wrong and the code-frame live on a non-standard `source_code` property
49
+ * (and `stack` is nothing but wasm frames).
50
+ *
51
+ * @param {unknown} error
52
+ * @returns {string}
53
+ */
54
+ export function errorMessage(error) {
55
+ if (!isRecord(error)) {
56
+ return String(error);
57
+ }
58
+
59
+ const parts = [];
60
+
61
+ if ('message' in error && error.message) {
62
+ parts.push(String(error.message));
63
+ }
64
+
65
+ if ('source_code' in error && error.source_code) {
66
+ parts.push(String(error.source_code));
67
+ }
68
+
69
+ if (parts.length === 0) {
70
+ return String(error);
71
+ }
72
+
73
+ return parts.join('\n\n');
74
+ }