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.
- package/dist/assets/tar-worker-kdkltuRC.js +598 -0
- package/dist/assets/tar-worker-kdkltuRC.js.map +1 -0
- package/dist/codemirror-D4aIVflZ.js +110 -0
- package/dist/codemirror-D4aIVflZ.js.map +1 -0
- package/dist/gjs-CzFzkEFv.js +173 -0
- package/dist/gjs-CzFzkEFv.js.map +1 -0
- package/dist/gmd-D9OXs2v3.js +166 -0
- package/dist/gmd-D9OXs2v3.js.map +1 -0
- package/dist/hbs-CuhWjffM.js +62 -0
- package/dist/hbs-CuhWjffM.js.map +1 -0
- package/dist/index-CUWCqMoD.js +2133 -0
- package/dist/index-CUWCqMoD.js.map +1 -0
- package/dist/index.js +4 -104
- package/dist/index.js.map +1 -1
- package/dist/parse-aBKk9rfS.js +328 -0
- package/dist/parse-aBKk9rfS.js.map +1 -0
- package/dist/render-app-island-B-i8rvGi.js +61 -0
- package/dist/render-app-island-B-i8rvGi.js.map +1 -0
- package/package.json +82 -9
- package/src/cache.js +138 -0
- package/src/cdn.js +93 -0
- package/src/codemirror.js +161 -0
- package/src/compilers/ember/gjs.js +212 -0
- package/src/compilers/ember/gmd.js +190 -0
- package/src/compilers/ember/hbs.js +98 -0
- package/src/compilers/ember/render-app-island.js +83 -0
- package/src/compilers/ember.js +166 -0
- package/src/compilers/js.js +32 -0
- package/src/compilers/markdown/build-compiler.js +151 -0
- package/src/compilers/markdown/const.js +2 -0
- package/src/compilers/markdown/heading-id.js +75 -0
- package/src/compilers/markdown/live-code-extraction.js +198 -0
- package/src/compilers/markdown/parse.js +22 -0
- package/src/compilers/markdown/parse.test.ts +363 -0
- package/src/compilers/markdown/sanitize-for-glimmer.js +26 -0
- package/src/compilers/markdown/types.ts +21 -0
- package/src/compilers/markdown/utils.js +78 -0
- package/src/compilers/markdown.js +125 -0
- package/src/compilers/mermaid.js +35 -0
- package/src/compilers/react.js +47 -0
- package/src/compilers/svelte.js +116 -0
- package/src/compilers/vue.js +58 -0
- package/src/compilers.js +108 -0
- package/src/es-module-shim.js +53 -0
- package/src/index.d.ts +53 -4
- package/src/index.js +744 -89
- package/src/npm.js +58 -0
- package/src/request.Request.test.ts +59 -0
- package/src/request.js +140 -0
- package/src/resolve.fromImports.test.ts +35 -0
- package/src/resolve.fromInternalImport.test.ts +69 -0
- package/src/resolve.js +352 -0
- package/src/resolve.resolvePath.test.ts +24 -0
- package/src/resolve.test.ts +23 -0
- package/src/specifier.js +71 -0
- package/src/specifier.test.ts +90 -0
- package/src/tar-worker.js +61 -0
- package/src/tar.js +76 -0
- package/src/types.ts +335 -58
- package/src/utils.js +28 -1
- 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
|
-
|
|
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:
|
|
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
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
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
|
-
*
|
|
60
|
-
*
|
|
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
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
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
|
-
*
|
|
264
|
+
* This is semi-trying to polyfill modules
|
|
265
|
+
* that aren't proper ESM. very annoying.
|
|
71
266
|
*/
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
267
|
+
`${Object.keys(result)
|
|
268
|
+
.map((exportName) => {
|
|
269
|
+
if (exportName === 'default') {
|
|
270
|
+
return `export default mod.default ?? mod;`;
|
|
271
|
+
}
|
|
76
272
|
|
|
77
|
-
|
|
273
|
+
return `export const ${exportName} = mod.${exportName};`;
|
|
274
|
+
})
|
|
275
|
+
.join('\n')}
|
|
276
|
+
`;
|
|
78
277
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
//
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
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
|
|
380
|
+
const opts = { ...options };
|
|
381
|
+
|
|
382
|
+
opts.fileName ||= `dynamic.${format}`;
|
|
383
|
+
|
|
384
|
+
this.#log('[compile] compiling');
|
|
98
385
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
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
|
-
|
|
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
|
|
460
|
+
return compiler;
|
|
108
461
|
}
|
|
109
462
|
|
|
110
|
-
|
|
111
|
-
|
|
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
|
-
|
|
477
|
+
if (flavor && flavor in config) {
|
|
478
|
+
config = /** @type {{ [flavor: string]: CompilerConfig}} */ (config)[flavor];
|
|
479
|
+
}
|
|
120
480
|
|
|
121
|
-
|
|
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
|
-
|
|
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
|
-
|
|
517
|
+
assert(`Cannot render falsey values. Did compilation succeed?`, whatToRender);
|
|
128
518
|
|
|
129
|
-
|
|
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
|
-
|
|
728
|
+
const div = document.createElement('div');
|
|
729
|
+
|
|
134
730
|
div.setAttribute('data-repl-output', '');
|
|
135
|
-
div.id =
|
|
731
|
+
div.id = nextId();
|
|
732
|
+
|
|
136
733
|
return div;
|
|
137
734
|
}
|
|
138
|
-
}
|
|
139
735
|
|
|
140
|
-
|
|
141
|
-
|
|
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
|
-
|
|
144
|
-
// Shim is already present
|
|
145
|
-
return;
|
|
743
|
+
this.#options.on.log(type, message);
|
|
146
744
|
}
|
|
147
745
|
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
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
|
-
|
|
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
|
+
}
|