repl-sdk 1.5.2 → 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.5.2",
3
+ "version": "1.6.1",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": {
@@ -50,7 +50,7 @@
50
50
  "prettier": "^3.7.4",
51
51
  "publint": "^0.3.16",
52
52
  "typescript": "^5.9.3",
53
- "vite": "^7.3.1",
53
+ "vite": "^8.0.8",
54
54
  "vite-plugin-dts": "4.5.4",
55
55
  "vitest": "^4.0.18"
56
56
  },
@@ -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.3",
100
- "codemirror-lang-glimmer": "^2.0.3",
101
- "codemirror-lang-glimmer-js": "^2.0.3"
99
+ "codemirror-lang-glimmer": "^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"
@@ -170,7 +170,18 @@ export async function compiler(config, api) {
170
170
  const { renderComponent } = await compiler.tryResolve('@ember/renderer');
171
171
 
172
172
  const owner = makeOwner(config.owner);
173
- const result = renderComponent(compiled, { into: element, owner });
173
+ const args = /** @type {Record<string, unknown> | undefined} */ (
174
+ extra && typeof extra === 'object' && 'args' in extra
175
+ ? /** @type {Record<string, unknown>} */ (extra).args
176
+ : undefined
177
+ );
178
+ const result = renderComponent(compiled, {
179
+ into: element,
180
+ owner,
181
+ ...(args ? { args } : {}),
182
+ });
183
+
184
+ compiler.announce('info', 'Ember Island Rendered');
174
185
 
175
186
  return () => result.destroy();
176
187
  },
@@ -98,9 +98,16 @@ export async function compiler(config, api) {
98
98
 
99
99
  const { renderComponent } = await compiler.tryResolve('@ember/renderer');
100
100
 
101
+ const args = /** @type {Record<string, unknown> | undefined} */ (
102
+ extra && typeof extra === 'object' && 'args' in extra
103
+ ? /** @type {Record<string, unknown>} */ (extra).args
104
+ : undefined
105
+ );
106
+
101
107
  const result = renderComponent(compiled, {
102
108
  into: element,
103
109
  owner: userOptions.owner,
110
+ ...(args ? { args } : {}),
104
111
  });
105
112
 
106
113
  const destroy = () => result.destroy();
@@ -67,7 +67,18 @@ export async function compiler(config, api) {
67
67
 
68
68
  const { renderComponent } = await compiler.tryResolve('@ember/renderer');
69
69
  const owner = makeOwner(config.owner);
70
- const result = renderComponent(compiled, { into: element, owner });
70
+ const args = /** @type {Record<string, unknown> | undefined} */ (
71
+ extra && typeof extra === 'object' && 'args' in extra
72
+ ? /** @type {Record<string, unknown>} */ (extra).args
73
+ : undefined
74
+ );
75
+ const result = renderComponent(compiled, {
76
+ into: element,
77
+ owner,
78
+ ...(args ? { args } : {}),
79
+ });
80
+
81
+ compiler.announce('info', 'Ember Island Rendered');
71
82
 
72
83
  return () => result.destroy();
73
84
  },
@@ -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
  /**
@@ -345,7 +347,7 @@ export class Compiler {
345
347
  /**
346
348
  * @param {string} format
347
349
  * @param {string} text
348
- * @param {{ fileName?: string, flavor?: string, [key: string]: unknown }} [ options ]
350
+ * @param {{ fileName?: string, flavor?: string, args?: Record<string, unknown>, [key: string]: unknown }} [ options ]
349
351
  * @returns {Promise<{ element: HTMLElement, destroy: () => void }>}
350
352
  */
351
353
  async compile(format, text, options = {}) {
@@ -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);
@@ -411,6 +411,7 @@ export class Compiler {
411
411
  return this.#render(compiler, value, {
412
412
  ...extras,
413
413
  compiled: value,
414
+ ...(opts.args ? { args: opts.args } : {}),
414
415
  });
415
416
  }
416
417
 
@@ -421,7 +422,10 @@ export class Compiler {
421
422
 
422
423
  this.#log('[compile] preparing to render', defaultExport, extras);
423
424
 
424
- return this.#render(compiler, defaultExport, extras);
425
+ return this.#render(compiler, defaultExport, {
426
+ ...extras,
427
+ ...(opts.args ? { args: opts.args } : {}),
428
+ });
425
429
  }
426
430
 
427
431
  #compilerCache = new WeakMap();
@@ -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
+ }