repl-sdk 0.0.0 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. package/dist/assets/tar-worker-kdkltuRC.js +598 -0
  2. package/dist/assets/tar-worker-kdkltuRC.js.map +1 -0
  3. package/dist/codemirror-D4aIVflZ.js +110 -0
  4. package/dist/codemirror-D4aIVflZ.js.map +1 -0
  5. package/dist/gjs-CzFzkEFv.js +173 -0
  6. package/dist/gjs-CzFzkEFv.js.map +1 -0
  7. package/dist/gmd-D9OXs2v3.js +166 -0
  8. package/dist/gmd-D9OXs2v3.js.map +1 -0
  9. package/dist/hbs-CuhWjffM.js +62 -0
  10. package/dist/hbs-CuhWjffM.js.map +1 -0
  11. package/dist/index-CUWCqMoD.js +2133 -0
  12. package/dist/index-CUWCqMoD.js.map +1 -0
  13. package/dist/index.js +4 -104
  14. package/dist/index.js.map +1 -1
  15. package/dist/parse-aBKk9rfS.js +328 -0
  16. package/dist/parse-aBKk9rfS.js.map +1 -0
  17. package/dist/render-app-island-B-i8rvGi.js +61 -0
  18. package/dist/render-app-island-B-i8rvGi.js.map +1 -0
  19. package/package.json +82 -9
  20. package/src/cache.js +138 -0
  21. package/src/cdn.js +93 -0
  22. package/src/codemirror.js +161 -0
  23. package/src/compilers/ember/gjs.js +212 -0
  24. package/src/compilers/ember/gmd.js +190 -0
  25. package/src/compilers/ember/hbs.js +98 -0
  26. package/src/compilers/ember/render-app-island.js +83 -0
  27. package/src/compilers/ember.js +166 -0
  28. package/src/compilers/js.js +32 -0
  29. package/src/compilers/markdown/build-compiler.js +151 -0
  30. package/src/compilers/markdown/const.js +2 -0
  31. package/src/compilers/markdown/heading-id.js +75 -0
  32. package/src/compilers/markdown/live-code-extraction.js +198 -0
  33. package/src/compilers/markdown/parse.js +22 -0
  34. package/src/compilers/markdown/parse.test.ts +363 -0
  35. package/src/compilers/markdown/sanitize-for-glimmer.js +26 -0
  36. package/src/compilers/markdown/types.ts +21 -0
  37. package/src/compilers/markdown/utils.js +78 -0
  38. package/src/compilers/markdown.js +125 -0
  39. package/src/compilers/mermaid.js +35 -0
  40. package/src/compilers/react.js +47 -0
  41. package/src/compilers/svelte.js +116 -0
  42. package/src/compilers/vue.js +58 -0
  43. package/src/compilers.js +108 -0
  44. package/src/es-module-shim.js +53 -0
  45. package/src/index.d.ts +53 -4
  46. package/src/index.js +744 -89
  47. package/src/npm.js +58 -0
  48. package/src/request.Request.test.ts +59 -0
  49. package/src/request.js +140 -0
  50. package/src/resolve.fromImports.test.ts +35 -0
  51. package/src/resolve.fromInternalImport.test.ts +69 -0
  52. package/src/resolve.js +352 -0
  53. package/src/resolve.resolvePath.test.ts +24 -0
  54. package/src/resolve.test.ts +23 -0
  55. package/src/specifier.js +71 -0
  56. package/src/specifier.test.ts +90 -0
  57. package/src/tar-worker.js +61 -0
  58. package/src/tar.js +76 -0
  59. package/src/types.ts +335 -58
  60. package/src/utils.js +28 -1
  61. package/declarations/index.d.ts +0 -73
package/src/index.js CHANGED
@@ -1,155 +1,781 @@
1
1
  /**
2
2
  * @typedef {import("./types.ts").Options} Options
3
+ * @typedef {import('./types.ts').CompilerConfig} CompilerConfig
3
4
  */
4
- import { assert } from './utils.js';
5
+
6
+ import mime from 'mime/lite';
7
+
8
+ import { cache, secretKey } from './cache.js';
9
+ import { compilers } from './compilers.js';
10
+ import { STABLE_REFERENCE } from './es-module-shim.js';
11
+ import { getTarRequestId } from './request.js';
12
+ import { getFromTarball } from './tar.js';
13
+ import { assert, nextId, prefix_tgz, tgzPrefix, unzippedPrefix } from './utils.js';
5
14
 
6
15
  assert(`There is no document. repl-sdk is meant to be ran in a browser`, globalThis.document);
7
16
 
8
- export const defaultFormats = {
9
- mermaid: {
10
- compiler: async () => {
11
- return {
12
- compile: async (text) => {},
13
- };
14
- },
15
- },
16
- };
17
+ export const defaultFormats = Object.keys(compilers);
17
18
 
18
19
  export const defaults = {
19
- formats: defaultFormats,
20
+ formats: compilers,
20
21
  };
21
22
 
22
- const secret = Symbol.for('__repl-sdk__compiler__');
23
-
24
- assert(
25
- `There is already an instance of repl-sdk, and there can only be one. Make sure that your dependency graph is correct.`,
26
- !globalThis[secret]
27
- );
28
-
29
23
  export class Compiler {
30
24
  /** @type {Options} */
31
25
  #options;
32
26
 
33
27
  /**
34
28
  * Options may be passed to the compiler to add to its behavior.
29
+ * @param {Partial<Options>} options
35
30
  */
36
31
  constructor(options = defaults) {
37
- this.#options = options;
38
-
39
- globalThis[secret] = this;
40
- globalThis.window.esmsInitOptions = {
41
- shimMode: true,
42
- skip: `https://esm.sh/`,
43
- revokeBlobURLs: true, // default false
44
- // Permit overrides to import maps
45
- mapOverrides: true, // default false
46
- // Hook all module resolutions
47
- resolve: (id, parentUrl, resolve) => {
48
- if (id.startsWith('blob:')) return id;
49
- if (id.startsWith('https://')) return id;
50
-
51
- console.log('Resolving', id);
52
- /**
53
- * TODO: locally defined scope
54
- * vs
55
- * proxy to esm.sh
56
- */
32
+ this.#options = Object.assign({}, defaults, options);
33
+
34
+ STABLE_REFERENCE.resolve = this.#resolve;
35
+ STABLE_REFERENCE.fetch = this.#fetch;
36
+
37
+ window.addEventListener('unhandledrejection', this.#handleUnhandledRejection);
38
+ }
39
+
40
+ /**
41
+ *
42
+ * @param {HTMLElement} element
43
+ * @param {any} options
44
+ */
45
+ async createEditor(element, { text, format, handleUpdate, extensions }) {
46
+ // Only one instance is allowed
47
+ return cache.cachedPromise('codemirror', async () => {
48
+ const { buildCodemirror } = await import('./codemirror.js');
49
+
50
+ return buildCodemirror({
51
+ element,
52
+ text,
53
+ format,
54
+ extensions,
55
+ handleUpdate,
56
+ getLang: async (format) => {
57
+ const [lang, flavor] = format.split('|');
58
+
59
+ assert(`Could not determine 'lang' from format: ${format}`, lang);
60
+
61
+ const compiler = this.#resolveFormat(lang, flavor);
62
+ const loadLang = compiler.codemirror?.lang;
63
+
64
+ assert(
65
+ `The compiler for '${format}' is missing its configuration for 'codemirror.lang'`,
66
+ loadLang
67
+ );
68
+
69
+ return await loadLang();
70
+ },
71
+ getSupport: async (format) => {
72
+ const [lang, flavor] = format.split('|');
73
+
74
+ assert(`Could not determine 'lang' from format: ${format}`, lang);
75
+
76
+ const compiler = this.#resolveFormat(lang, flavor);
77
+ const loadSupport = compiler.codemirror.support;
78
+
79
+ return await loadSupport?.();
80
+ },
81
+ });
82
+ });
83
+ }
84
+
85
+ /**
86
+ * @param {PromiseRejectionEvent} e
87
+ */
88
+ #handleUnhandledRejection = (e) => {
89
+ let handled = false;
90
+
91
+ for (const onUnhandled of this.#compilerOnUnhandled) {
92
+ onUnhandled(e, (message) => {
93
+ this.#announce('error', message);
94
+ handled = true;
95
+ });
96
+ if (handled) break;
97
+ }
98
+
99
+ if (handled) return;
100
+
101
+ this.#announce('error', e.reason);
102
+ };
103
+
104
+ /**
105
+ * Order of preference
106
+ * 1. manually resolved (from the caller)
107
+ * 2. specified in the compiler config (to use CDN)
108
+ * 3. download tarball from npm
109
+ * or resolve from already downloaded tarball
110
+ *
111
+ * NOTE: when we return a new URL, we want to collapse the parentURI
112
+ * so that we don't get compound query params in nested requests.
113
+ *
114
+ * @param {string} id
115
+ * @param {string} parentUrl
116
+ * @param {(id: string, parentUrl: string) => string} resolve
117
+ * @returns {string}
118
+ */
119
+ #resolve = (id, parentUrl, resolve) => {
120
+ /**
121
+ * We have to strip the query params because our manual resolving
122
+ * doesn't use them -- but CDNs do
123
+ */
124
+ const vanilla = deCDN(id);
125
+
126
+ this.#announce('info', `Loading ${vanilla}`);
127
+ this.#log('[resolve]', id, 'from', parentUrl);
57
128
 
129
+ if (this.#options.resolve?.[vanilla]) {
130
+ this.#log(`[resolve] ${vanilla} found in manually specified resolver`);
131
+
132
+ return `manual:${vanilla}`;
133
+ }
134
+
135
+ for (const compilerResolve of this.#compilerResolvers) {
136
+ const result = compilerResolve(vanilla);
137
+
138
+ if (result) {
139
+ this.#log(`[resolve] ${vanilla} found in compiler config at ${result}.`);
140
+
141
+ if (typeof result === 'function') {
142
+ return `configured:${vanilla}`;
143
+ }
144
+
145
+ return result;
146
+ }
147
+ }
148
+
149
+ if (parentUrl.startsWith(tgzPrefix) && (id.startsWith('.') || id.startsWith('#'))) {
150
+ const answer = getTarRequestId({ to: id, from: parentUrl });
151
+
152
+ return answer;
153
+ }
154
+
155
+ if (id.startsWith('https://')) return resolve(id, parentUrl);
156
+ if (id.startsWith('blob:')) return resolve(id, parentUrl);
157
+ if (id.startsWith('.')) return resolve(id, parentUrl);
158
+ if (parentUrl.startsWith('https://') && parentUrl !== location.href)
159
+ return resolve(id, parentUrl);
160
+ if (parentUrl.startsWith('https://') && parentUrl.startsWith('/'))
161
+ return resolve(id, parentUrl);
162
+
163
+ if (id.startsWith('node:')) {
164
+ this.#log(`Is known node module: ${id}. Grabbing polyfill`);
165
+
166
+ if (id === 'node:process') return prefix_tgz(`process`);
167
+ if (id === 'node:buffer') return prefix_tgz(`buffer`);
168
+ if (id === 'node:events') return prefix_tgz(`events`);
169
+ if (id === 'node:path') return prefix_tgz(`path-browser`);
170
+ if (id === 'node:util') return prefix_tgz(`util-browser`);
171
+ if (id === 'node:crypto') return prefix_tgz(`crypto-browserify`);
172
+ if (id === 'node:stream') return prefix_tgz(`stream-browserify`);
173
+ if (id === 'node:fs') return prefix_tgz(`browserify-fs`);
174
+ }
175
+
176
+ this.#log(`[resolve] ${id} not found, deferring to npmjs.com's provided tarball`);
177
+
178
+ return getTarRequestId({ to: id, from: parentUrl });
179
+ };
180
+ /**
181
+ * @param {string} url
182
+ * @param {RequestInit} options
183
+ * @returns {Promise<Response>}
184
+ */
185
+ #fetch = async (url, options) => {
186
+ const mimeType = mime.getType(url) ?? 'application/javascript';
187
+
188
+ this.#log(`[fetch] attempting to fetch: ${url}. Assuming ${mimeType}`);
189
+
190
+ if (url.startsWith('manual:')) {
191
+ const name = url.replace(/^manual:/, '');
192
+
193
+ this.#log('[fetch] resolved url in manually specified resolver', url);
194
+
195
+ const result = await this.#resolveManually(name);
196
+
197
+ assert(`Failed to resolve ${name}`, result);
198
+
199
+ const blobContent =
200
+ `const mod = window[Symbol.for('${secretKey}')].resolves?.['${name}'];\n` +
201
+ `\n\n` +
202
+ `if (!mod) { throw new Error('Could not resolve \`${name}\`. Does the module exist? ( checked ${url} )') }` +
203
+ `\n\n` +
58
204
  /**
59
- * For esm.sh, we want all imports to declare they don't want
60
- * their dependencies bundled. We want to have every import go
61
- * through this resolve/fetch combo of things so we have a chance
62
- * to compile if we need to.
205
+ * This is semi-trying to polyfill modules
206
+ * that aren't proper ESM. very annoying.
63
207
  */
64
- return `https://esm.sh/*${id}`;
65
- },
66
- // Hook source fetch function
67
- fetch: async (url, options) => {
68
- console.log(`Fetching`, url);
208
+ `${Object.keys(result)
209
+ .map((exportName) => {
210
+ if (exportName === 'default') {
211
+ return `export default mod.default ?? mod;`;
212
+ }
213
+
214
+ return `export const ${exportName} = mod.${exportName};`;
215
+ })
216
+ .join('\n')}
217
+ `;
218
+
219
+ const blob = new Blob(Array.from(blobContent), { type: mimeType });
220
+
221
+ this.#log(
222
+ `[fetch] returning blob mapping to manually resolved import for ${name}`
223
+ // blobContent
224
+ );
225
+
226
+ this.#announce('info', `Loaded ${name}`);
227
+
228
+ return new Response(blob);
229
+ }
230
+
231
+ if (url.startsWith('configured:')) {
232
+ const name = url.replace(/^configured:/, '');
233
+
234
+ this.#log(
235
+ '[fetch] resolved url in a preconfigured (in the compiler config) specified resolver',
236
+ url
237
+ );
238
+
239
+ let result;
240
+
241
+ /**
242
+ * Unlike the manual resolver, these are just functions per
243
+ * id, they represent a way to get a module
244
+ */
245
+ for (const compilerResolve of this.#compilerResolvers) {
246
+ const fn = compilerResolve(name);
247
+
248
+ if (fn) {
249
+ this.#log(`[fetch] ${name} found in compiler config at ${result}.`);
250
+
251
+ result = await fn();
252
+ }
253
+ }
254
+
255
+ assert(`Failed to resolve ${name}`, result);
256
+ cache.resolves[name] = result;
257
+
258
+ const blobContent =
259
+ `const mod = window[Symbol.for('${secretKey}')].resolves?.['${name}'];\n` +
260
+ `\n\n` +
261
+ `if (!mod) { throw new Error('Could not resolve \`${name}\`. Does the module exist? ( checked ${url} )') }` +
262
+ `\n\n` +
69
263
  /**
70
- * Do transformations here based on file extension
264
+ * This is semi-trying to polyfill modules
265
+ * that aren't proper ESM. very annoying.
71
266
  */
72
- if (url.endsWith('example.js')) {
73
- const transformed = `export const js = 'transformed'`;
74
- return new Response(new Blob([transformed], { type: 'application/javascript' }));
75
- }
267
+ `${Object.keys(result)
268
+ .map((exportName) => {
269
+ if (exportName === 'default') {
270
+ return `export default mod.default ?? mod;`;
271
+ }
76
272
 
77
- const response = await fetch(url, options);
273
+ return `export const ${exportName} = mod.${exportName};`;
274
+ })
275
+ .join('\n')}
276
+ `;
78
277
 
79
- // if (response.url.endsWith('.ts')) {
80
- // const source = await response.body();
81
- // const transformed = tsCompile(source);
82
- // return new Response(new Blob([transformed], { type: 'application/javascript' }));
83
- // }
84
- return response;
85
- },
86
- };
87
- // addShim();
278
+ const blob = new Blob(Array.from(blobContent), { type: mimeType });
279
+
280
+ this.#log(
281
+ `[fetch] returning blob mapping to configured resolved import for ${name}`
282
+ // blobContent
283
+ );
284
+
285
+ this.#announce('info', `Loaded ${name}`);
286
+
287
+ return new Response(blob);
288
+ }
289
+
290
+ if (url.startsWith(unzippedPrefix)) {
291
+ this.#log('[fetch] resolved url via tgz resolver', url, options);
292
+
293
+ const tarInfo = await getFromTarball(url);
294
+
295
+ assert(`Could not find file for ${url}`, tarInfo);
296
+
297
+ const { code, ext } = tarInfo;
298
+
299
+ /**
300
+ * We don't know if this code is completely ready to run in the browser yet, so we might need to run in through the compiler again
301
+ */
302
+ const file = await this.#postProcess(code, ext);
303
+ const type = mime.getType(ext);
304
+
305
+ return new Response(new Blob([file], { type: type ?? 'application/javascript' }));
306
+ }
307
+
308
+ if (url.startsWith('https://')) {
309
+ return fetch(url, options);
310
+ }
311
+
312
+ this.#log('[fetch] fetching url', url, options);
313
+
314
+ const response = await fetch(url, options);
315
+
316
+ if (!response.ok) return response;
317
+
318
+ const source = await response.text();
319
+
320
+ this.#announce('info', `Loaded ${url}`);
321
+
322
+ return new Response(new Blob([source], { type: 'application/javascript' }));
323
+ };
324
+
325
+ /**
326
+ * NOTE: this does not resolve compilers that are not loaded yet.
327
+ * So there would be a bit of a race condition here if different compilers
328
+ * were to have incompatible post-processing handlers.
329
+ *
330
+ * @param {string} text
331
+ * @param {string} ext
332
+ */
333
+ async #postProcess(text, ext) {
334
+ let code = text;
335
+
336
+ for (const compiler of this.#compilers) {
337
+ if (compiler.handlers?.[ext]) {
338
+ code = await compiler.handlers[ext](code);
339
+ }
340
+ }
341
+
342
+ return code;
88
343
  }
89
344
 
90
345
  /**
91
346
  * @param {string} format
92
347
  * @param {string} text
348
+ * @param {{ fileName?: string, flavor?: string, [key: string]: unknown }} [ options ]
349
+ * @returns {Promise<{ element: HTMLElement, destroy: () => void }>}
93
350
  */
94
- async compile(format, text) {
351
+ async compile(format, text, options = {}) {
352
+ this.#announce('info', `Compiling ${format}`);
353
+
354
+ try {
355
+ return await this.#compile(format, text, options);
356
+ } catch (e) {
357
+ // for on.log usage
358
+ const message = e instanceof Error ? e.message : e;
359
+
360
+ this.#announce('error', String(message));
361
+
362
+ // Don't hide errors!
363
+ this.#error(e);
364
+ throw e;
365
+ }
366
+ }
367
+
368
+ /**
369
+ * @param {string} format
370
+ * @param {string} text
371
+ * @param {{ fileName?: string, flavor?: string, [key: string]: unknown }} [ options ]
372
+ * @returns {Promise<{ element: HTMLElement, destroy: () => void }>}
373
+ */
374
+ async #compile(format, text, options) {
375
+ this.#log('[compile] idempotently installing es-module-shim');
376
+
377
+ // @ts-ignore
95
378
  await import('es-module-shims');
96
379
 
97
- const compiler = await this.#getCompiler(format);
380
+ const opts = { ...options };
381
+
382
+ opts.fileName ||= `dynamic.${format}`;
383
+
384
+ this.#log('[compile] compiling');
98
385
 
99
- // TODO: pass this through es-module-shims
100
- // for getting the actual module back
101
- const compiledText = await compiler.compile(text);
386
+ const compiler = await this.#getCompiler(format, opts.flavor);
387
+ const compiled = await compiler.compile(text, opts);
388
+
389
+ let compiledText = 'export default "failed to compile"';
390
+ let extras = { compiled: '' };
391
+
392
+ if (typeof compiled === 'string') {
393
+ compiledText = compiled;
394
+ extras = { compiled: compiledText };
395
+ } else if (typeof compiled.compiled === 'string') {
396
+ const { compiled: text } = compiled;
397
+
398
+ compiledText = text;
399
+ extras = compiled;
400
+ } else {
401
+ /**
402
+ * the compiler didn't return text, so we can skip import shimming
403
+ */
404
+ let value = compiled;
405
+
406
+ if ('compiled' in compiled) {
407
+ value = compiled.compiled;
408
+ extras = compiled;
409
+ }
410
+
411
+ return this.#render(compiler, value, {
412
+ ...extras,
413
+ compiled: value,
414
+ });
415
+ }
102
416
 
103
417
  const asBlobUrl = textToBlobUrl(compiledText);
104
418
 
105
- const { default: defaultExport } = await importShim(/* @vite-ignore */ asBlobUrl);
419
+ // @ts-ignore
420
+ const { default: defaultExport } = await shimmedImport(/* @vite-ignore */ asBlobUrl);
421
+
422
+ this.#log('[compile] preparing to render', defaultExport, extras);
423
+
424
+ return this.#render(compiler, defaultExport, extras);
425
+ }
426
+
427
+ #compilerCache = new WeakMap();
428
+ #compilers = new Set();
429
+ #compilerResolvers = new Set();
430
+ /**
431
+ * @type {Set<(e: PromiseRejectionEvent, handle: (message: string) => void) => void>}
432
+ */
433
+ #compilerOnUnhandled = new Set();
434
+
435
+ /**
436
+ * @param {string} format
437
+ * @param {string | undefined} flavor
438
+ */
439
+ async #getCompiler(format, flavor) {
440
+ const config = this.#resolveFormat(format, flavor);
441
+
442
+ if (this.#compilerCache.has(config)) {
443
+ return this.#compilerCache.get(config);
444
+ }
445
+
446
+ if (config.resolve) {
447
+ this.#compilerResolvers.add(config.resolve);
448
+ }
449
+
450
+ if (config.onUnhandled) {
451
+ this.#compilerOnUnhandled.add(config.onUnhandled);
452
+ }
453
+
454
+ const options = this.optionsFor(format, flavor);
455
+ const compiler = await config.compiler(options, this.#nestedPublicAPI);
456
+
457
+ this.#compilerCache.set(config, compiler);
458
+ this.#compilers.add(compiler);
106
459
 
107
- return this.#render(compiler, defaultExport);
460
+ return compiler;
108
461
  }
109
462
 
110
- async #getCompiler(format) {
111
- const config = this.#options.formats[format];
463
+ /**
464
+ * @param {string} format
465
+ * @param {string | undefined} flavor
466
+ * @returns {import('./types').CompilerConfig}
467
+ */
468
+ #resolveFormat(format, flavor) {
469
+ let config = this.#options.formats[format];
112
470
 
113
471
  assert(
114
- `${format} is not a configured format. ` +
472
+ `${format} is not a configured format / extension. ` +
115
473
  `The currently configured formats are ${Object.keys(this.#options.formats).join(', ')}`,
116
474
  config
117
475
  );
118
476
 
119
- const compiler = await config.compiler();
477
+ if (flavor && flavor in config) {
478
+ config = /** @type {{ [flavor: string]: CompilerConfig}} */ (config)[flavor];
479
+ }
120
480
 
121
- return compiler;
481
+ assert(
482
+ `The config for ${format}${flavor ? ` (using flavor ${flavor})` : ''} is missing the 'compiler' function. It had keys: ${Object.keys(/** @type {any} */ (config))}. If this is a language with multiple flavors, make sure you specify the flavor.`,
483
+ 'compiler' in /** @type {any} */ (config)
484
+ );
485
+
486
+ return /** @type {import('./types').CompilerConfig} */ (config);
122
487
  }
123
488
 
124
- async #render(compiler, whatToRender) {
489
+ /**
490
+ * @param {string} format
491
+ * @param {string | undefined} flavor
492
+ * @returns {{ [key: string]: unknown }}
493
+ */
494
+ #resolveUserOptions(format, flavor) {
495
+ let config = /** @type {{ [key: string]: unknown }} */ (this.#options.options?.[format]);
496
+
497
+ if (!config) return {};
498
+
499
+ if (flavor && flavor in config) {
500
+ config = /** @type {{ [key: string]: unknown }} */ (config[flavor]);
501
+ }
502
+
503
+ return config ?? {};
504
+ }
505
+
506
+ /**
507
+ * @param {import('./types.ts').Compiler} compiler
508
+ * @param {string} whatToRender
509
+ * @param {{ compiled: string } & Record<string, unknown>} extras
510
+ * @returns {Promise<{ element: HTMLElement, destroy: () => void }>}
511
+ */
512
+ async #render(compiler, whatToRender, extras) {
513
+ this.#announce('info', 'Rendering');
514
+
125
515
  const div = this.#createDiv();
126
516
 
127
- await compiler.render(div, whatToRender);
517
+ assert(`Cannot render falsey values. Did compilation succeed?`, whatToRender);
128
518
 
129
- return div;
519
+ const destroy = await compiler.render(div, whatToRender, extras, this.#nestedPublicAPI);
520
+
521
+ // Wait for render
522
+ await new Promise((resolve) => requestAnimationFrame(resolve));
523
+
524
+ return {
525
+ element: div,
526
+ destroy: () => {
527
+ if (destroy) {
528
+ return destroy();
529
+ }
530
+ },
531
+ };
532
+ }
533
+
534
+ /**
535
+ * @param {string} format
536
+ * @param {string | undefined} flavor
537
+ */
538
+ optionsFor = (format, flavor) => {
539
+ const { needsLiveMeta } = this.#resolveFormat(format, flavor);
540
+
541
+ return {
542
+ needsLiveMeta: /* @type {boolean | undefined} */ needsLiveMeta ?? true,
543
+ versions: this.#options.versions ?? {},
544
+ ...(this.#resolveUserOptions(format, flavor) ?? {}),
545
+ };
546
+ };
547
+
548
+ static clearCache() {
549
+ cache.clear();
130
550
  }
131
551
 
552
+ /**
553
+ * @param {string} name
554
+ * @param {(name?: string) => Promise<undefined | object>} [fallback]
555
+ * @returns {Promise<undefined | object>}
556
+ */
557
+ #resolveManually = async (name, fallback) => {
558
+ const existing = cache.resolves[name];
559
+
560
+ if (existing) {
561
+ this.#log('[#resolveManually]', name, 'already resolved');
562
+
563
+ return existing;
564
+ }
565
+
566
+ let result =
567
+ /** @type {object | undefined} */
568
+ (await this.#options.resolve?.[name]);
569
+
570
+ if (!result) {
571
+ this.#log(`[#resolveManually] Could not resolve ${name}`);
572
+ }
573
+
574
+ if (typeof result === 'function') {
575
+ if (!result) {
576
+ this.#log(`[#resolveManually] Value for ${name} is a function. Invoking.`);
577
+ }
578
+
579
+ result = await result();
580
+ }
581
+
582
+ /**
583
+ * Compiler-implementation-provided fallback takes precidence over
584
+ * going through the shimmedImport / tgz / npm fallback.
585
+ */
586
+ if (fallback) {
587
+ result = await fallback(name);
588
+ }
589
+
590
+ cache.resolves[name] ||= await result;
591
+
592
+ return result;
593
+ };
594
+
595
+ /**
596
+ * @type {import('./types.ts').PublicMethods}
597
+ */
598
+ #nestedPublicAPI = {
599
+ /**
600
+ * @param {'error' | 'info'} type
601
+ * @param {string} message
602
+ * @returns {void}
603
+ */
604
+ announce: (type, message) => this.#announce(type, message),
605
+
606
+ /**
607
+ * @param {string} name
608
+ * @param {(name?: string) => Promise<object | undefined>} [fallback]
609
+ * @returns {Promise<object | undefined>}
610
+ */
611
+ tryResolve: async (name, fallback) => {
612
+ const existing = await this.#resolveManually(name, fallback);
613
+
614
+ if (existing) {
615
+ this.#log(name, 'already resolved');
616
+
617
+ return existing;
618
+ }
619
+
620
+ // @ts-ignore
621
+ const shimmed = await shimmedImport(/* vite-ignore */ name);
622
+
623
+ return shimmed;
624
+ },
625
+ /**
626
+ * @param {string[]} names
627
+ * @param {(name?: string) => Promise<unknown>} [fallback]
628
+ * @returns {Promise<unknown[]>}
629
+ */
630
+ tryResolveAll: async (names, fallback) => {
631
+ const results = await Promise.all(
632
+ names.map((name) => {
633
+ return this.#nestedPublicAPI.tryResolve(name);
634
+ })
635
+ );
636
+
637
+ if (fallback) {
638
+ /** @type {Record<string, Promise<unknown>>} */
639
+ const morePromises = {};
640
+
641
+ for (let i = 0; i < results.length; i++) {
642
+ const result = results[i];
643
+ const name = names[i];
644
+
645
+ if (!result) {
646
+ this.#warn(`Could not load ${name}. Trying fallback.`);
647
+
648
+ morePromises[i] = fallback(name);
649
+ }
650
+ }
651
+
652
+ await Promise.all(Object.values(morePromises));
653
+
654
+ for (let i = 0; i < results.length; i++) {
655
+ let result = results[i];
656
+
657
+ if (!result && morePromises[i]) {
658
+ result = morePromises[i];
659
+ }
660
+ }
661
+ }
662
+
663
+ return results;
664
+ },
665
+ /**
666
+ * @param {Parameters<Compiler['compile']>} args
667
+ */
668
+ compile: (...args) => this.compile(...args),
669
+ /**
670
+ * @param {Parameters<Compiler['optionsFor']>} args
671
+ */
672
+ optionsFor: (...args) => this.optionsFor(...args),
673
+
674
+ canCompile: (format, flavor) => {
675
+ let config = this.#options.formats[format];
676
+
677
+ if (!config) {
678
+ return {
679
+ result: false,
680
+ reason:
681
+ `${format} is not a configured format / extension. ` +
682
+ `The currently configured formats are ${Object.keys(this.#options.formats).join(', ')}`,
683
+ };
684
+ }
685
+
686
+ if (flavor && flavor in config) {
687
+ config = /** @type {{ [flavor: string]: CompilerConfig}} */ (config)[flavor];
688
+ }
689
+
690
+ if (!config) {
691
+ return {
692
+ result: false,
693
+ reason:
694
+ `${format} for ${flavor} is not a configured format / extension. ` +
695
+ `The currently configured formats are ${Object.keys(this.#options.formats).join(', ')}`,
696
+ };
697
+ }
698
+
699
+ if ('compiler' in config) {
700
+ return { result: true };
701
+ }
702
+
703
+ return {
704
+ result: false,
705
+ reason: `The config for ${format}${flavor ? ` (using flavor ${flavor})` : ''} is missing the 'compiler' function. It had keys: ${Object.keys(config)}. If this is a language with multiple flavors, make sure you specify the flavor.`,
706
+ };
707
+ },
708
+
709
+ getCompiler: (format, flavor) => this.#getCompiler(format, flavor),
710
+
711
+ getAllowedFormats: () => Object.keys(this.#options.formats),
712
+
713
+ getFlavorsFor: (format) => {
714
+ const config = this.#options.formats[format];
715
+
716
+ if (!config) return [];
717
+ if (typeof config === 'function') return [];
718
+
719
+ if (typeof config === 'object') {
720
+ return Object.keys(config);
721
+ }
722
+
723
+ return [];
724
+ },
725
+ };
726
+
132
727
  #createDiv() {
133
- let div = document.createElement('div');
728
+ const div = document.createElement('div');
729
+
134
730
  div.setAttribute('data-repl-output', '');
135
- div.id = 'some-random-string';
731
+ div.id = nextId();
732
+
136
733
  return div;
137
734
  }
138
- }
139
735
 
140
- function addShim() {
141
- let url = 'https://ga.jspm.io/npm:es-module-shims@1.10.0/dist/es-module-shims.js';
736
+ /**
737
+ * @param {'error' | 'info'} type
738
+ * @param {string} message
739
+ */
740
+ #announce(type, message) {
741
+ if (!this.#options?.on?.log) return;
142
742
 
143
- if (document.querySelector('script[src="${url}"]')) {
144
- // Shim is already present
145
- return;
743
+ this.#options.on.log(type, message);
146
744
  }
147
745
 
148
- let script = document.createElement('script');
149
- // script.setAttribute('async', '');
150
- script.setAttribute('src', `<script async src="${url}"></script>`);
746
+ /**
747
+ * @param {Parameters<typeof console.debug>} args
748
+ */
749
+ #log(...args) {
750
+ if (this.#options.logging) {
751
+ console.debug(...args);
752
+ }
753
+ }
151
754
 
152
- document.head.appendChild(script);
755
+ /**
756
+ * @param {Parameters<typeof console.warn>} args
757
+ */
758
+ #warn(...args) {
759
+ if (this.#options.logging) {
760
+ console.warn(...args);
761
+ }
762
+ }
763
+
764
+ /**
765
+ * @param {Parameters<typeof console.error>} args
766
+ */
767
+ #error(...args) {
768
+ if (this.#options.logging) {
769
+ console.error(...args);
770
+ }
771
+ }
772
+
773
+ /**
774
+ * @param {string} message
775
+ */
776
+ announceError(message) {
777
+ this.#announce('error', message);
778
+ }
153
779
  }
154
780
 
155
781
  /**
@@ -159,5 +785,34 @@ function textToBlobUrl(text) {
159
785
  const blob = new Blob([text], { type: 'text/javascript' });
160
786
 
161
787
  const blobUrl = URL.createObjectURL(blob);
788
+
162
789
  return blobUrl;
163
790
  }
791
+
792
+ /**
793
+ * This should have happened at the beginning of the compile function.
794
+ * If this error is ever thrown, something goofy has happened, and it would be very unexpected.
795
+
796
+ * @param {...any[]} args
797
+ */
798
+ function shimmedImport(...args) {
799
+ if (!globalThis.importShim) {
800
+ throw new Error(`Could not find importShim. Has the REPL been set up correctly?`);
801
+ }
802
+
803
+ // @ts-ignore
804
+ return globalThis.importShim(/* @vite-ignore */ ...args);
805
+ }
806
+
807
+ /**
808
+ * CDNs will pre-process every file to make sure every import goes through them.
809
+ * We don't want this.
810
+ *
811
+ * @param {string} id
812
+ * @returns {string}
813
+ */
814
+ function deCDN(id) {
815
+ const noQPs = id.split('?')[0];
816
+
817
+ return /** @type {string} */ (noQPs);
818
+ }