@tutorialkit-rb/runtime 1.5.2-rb.0.1.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 (46) hide show
  1. package/README.md +18 -0
  2. package/dist/index.d.ts +3 -0
  3. package/dist/index.js +2 -0
  4. package/dist/lesson-files.d.ts +22 -0
  5. package/dist/lesson-files.js +126 -0
  6. package/dist/store/editor.d.ts +29 -0
  7. package/dist/store/editor.js +127 -0
  8. package/dist/store/index.d.ts +113 -0
  9. package/dist/store/index.js +316 -0
  10. package/dist/store/previews.d.ts +19 -0
  11. package/dist/store/previews.js +80 -0
  12. package/dist/store/terminal.d.ts +24 -0
  13. package/dist/store/terminal.js +128 -0
  14. package/dist/store/tutorial-runner.d.ts +147 -0
  15. package/dist/store/tutorial-runner.js +564 -0
  16. package/dist/tasks.d.ts +22 -0
  17. package/dist/tasks.js +26 -0
  18. package/dist/utils/multi-counter.d.ts +5 -0
  19. package/dist/utils/multi-counter.js +19 -0
  20. package/dist/utils/promises.d.ts +8 -0
  21. package/dist/utils/promises.js +29 -0
  22. package/dist/utils/support.d.ts +1 -0
  23. package/dist/utils/support.js +23 -0
  24. package/dist/utils/terminal.d.ts +17 -0
  25. package/dist/utils/terminal.js +13 -0
  26. package/dist/webcontainer/command.d.ts +28 -0
  27. package/dist/webcontainer/command.js +67 -0
  28. package/dist/webcontainer/editor-config.d.ts +12 -0
  29. package/dist/webcontainer/editor-config.js +60 -0
  30. package/dist/webcontainer/index.d.ts +4 -0
  31. package/dist/webcontainer/index.js +4 -0
  32. package/dist/webcontainer/on-demand-boot.d.ts +15 -0
  33. package/dist/webcontainer/on-demand-boot.js +39 -0
  34. package/dist/webcontainer/port-info.d.ts +6 -0
  35. package/dist/webcontainer/port-info.js +10 -0
  36. package/dist/webcontainer/preview-info.d.ts +21 -0
  37. package/dist/webcontainer/preview-info.js +56 -0
  38. package/dist/webcontainer/shell.d.ts +14 -0
  39. package/dist/webcontainer/shell.js +46 -0
  40. package/dist/webcontainer/steps.d.ts +15 -0
  41. package/dist/webcontainer/steps.js +38 -0
  42. package/dist/webcontainer/terminal-config.d.ts +59 -0
  43. package/dist/webcontainer/terminal-config.js +230 -0
  44. package/dist/webcontainer/utils/files.d.ts +10 -0
  45. package/dist/webcontainer/utils/files.js +76 -0
  46. package/package.json +53 -0
@@ -0,0 +1,564 @@
1
+ import picomatch from 'picomatch/posix.js';
2
+ import { newTask } from '../tasks.js';
3
+ import { MultiCounter } from '../utils/multi-counter.js';
4
+ import { clearTerminal, escapeCodes } from '../utils/terminal.js';
5
+ import { Command, Commands } from '../webcontainer/command.js';
6
+ import { StepsController } from '../webcontainer/steps.js';
7
+ import { diffFiles, toFileTree } from '../webcontainer/utils/files.js';
8
+ /**
9
+ * The idea behind this class is that it manages the state of WebContainer and exposes
10
+ * an interface that makes sense to every component of TutorialKit.
11
+ *
12
+ * There should be only a single instance of this class.
13
+ */
14
+ export class TutorialRunner {
15
+ _webcontainer;
16
+ _terminalStore;
17
+ _editorStore;
18
+ _stepController;
19
+ _currentLoadTask = undefined;
20
+ _currentProcessTask = undefined;
21
+ _currentCommandProcess = undefined;
22
+ _currentTemplate = undefined;
23
+ _currentFiles = undefined;
24
+ _currentRunCommands = undefined;
25
+ _ignoreFileEvents = new MultiCounter();
26
+ _watcher;
27
+ _watchContentFromWebContainer = false;
28
+ _readyToWatch = false;
29
+ _packageJsonDirty = false;
30
+ _commandsChanged = false;
31
+ // this strongly assumes that there's a single package json which might not be true
32
+ _packageJsonContent = '';
33
+ _packageJsonPath = '';
34
+ constructor(_webcontainer, _terminalStore, _editorStore, _stepController) {
35
+ this._webcontainer = _webcontainer;
36
+ this._terminalStore = _terminalStore;
37
+ this._editorStore = _editorStore;
38
+ this._stepController = _stepController;
39
+ }
40
+ setWatchFromWebContainer(value) {
41
+ this._watchContentFromWebContainer = value;
42
+ if (this._readyToWatch && this._watchContentFromWebContainer) {
43
+ this._webcontainer.then((webcontainer) => this._setupWatcher(webcontainer));
44
+ }
45
+ else if (!this._watchContentFromWebContainer) {
46
+ this._stopWatcher();
47
+ }
48
+ }
49
+ /**
50
+ * Set the commands to run. This updates the reported `steps` if any have changed.
51
+ *
52
+ * This function is safe to call server side.
53
+ *
54
+ * To actually run them in WebContainer see `runCommands`.
55
+ *
56
+ * @param commands The commands schema.
57
+ */
58
+ setCommands(commands) {
59
+ const newCommands = new Commands(commands);
60
+ const anyChange = this._changeDetection(commands);
61
+ // if we already know that there's a change we can update the steps now
62
+ if (anyChange) {
63
+ this._stepController.setFromCommands(Array.from(newCommands));
64
+ this._currentRunCommands = newCommands;
65
+ this._commandsChanged = true;
66
+ }
67
+ }
68
+ onTerminalResize(cols, rows) {
69
+ this._currentCommandProcess?.resize({ cols, rows });
70
+ }
71
+ createFolder(folderPath) {
72
+ const previousLoadPromise = this._currentLoadTask?.promise;
73
+ this._currentLoadTask = newTask(async (signal) => {
74
+ await previousLoadPromise;
75
+ const webcontainer = await this._webcontainer;
76
+ signal.throwIfAborted();
77
+ this._ignoreFileEvents.increment(folderPath);
78
+ await webcontainer.fs.mkdir(folderPath);
79
+ }, { ignoreCancel: true });
80
+ }
81
+ /**
82
+ * Update the content of a single file in WebContainer.
83
+ *
84
+ * @param filePath path of the file
85
+ * @param content new content of the file
86
+ */
87
+ updateFile(filePath, content) {
88
+ const previousLoadPromise = this._currentLoadTask?.promise;
89
+ this._currentLoadTask = newTask(async (signal) => {
90
+ await previousLoadPromise;
91
+ const webcontainer = await this._webcontainer;
92
+ signal.throwIfAborted();
93
+ this._ignoreFileEvents.increment(filePath);
94
+ await webcontainer.fs.writeFile(filePath, content);
95
+ this._updateCurrentFiles({ [filePath]: content });
96
+ }, { ignoreCancel: true });
97
+ }
98
+ /**
99
+ * Update the provided files in WebContainer.
100
+ *
101
+ * @param files Files to update.
102
+ */
103
+ updateFiles(files) {
104
+ const previousLoadPromise = this._currentLoadTask?.promise;
105
+ this._currentLoadTask = newTask(async (signal) => {
106
+ await previousLoadPromise;
107
+ const webcontainer = await this._webcontainer;
108
+ signal.throwIfAborted();
109
+ this._ignoreFileEvents.increment(Object.keys(files));
110
+ await webcontainer.mount(toFileTree(files));
111
+ this._updateCurrentFiles(files);
112
+ }, { ignoreCancel: true });
113
+ }
114
+ async fileExists(filepath) {
115
+ return this._fsExists(filepath, 'file');
116
+ }
117
+ async folderExists(folderPath) {
118
+ return this._fsExists(folderPath, 'folder');
119
+ }
120
+ async _fsExists(filepath, type) {
121
+ if (this._currentFiles?.[filepath] || this._currentTemplate?.[filepath]) {
122
+ return true;
123
+ }
124
+ const previousLoadPromise = this._currentLoadTask?.promise;
125
+ return new Promise((resolve) => {
126
+ this._currentLoadTask = newTask(async () => {
127
+ await previousLoadPromise;
128
+ const webcontainer = await this._webcontainer;
129
+ try {
130
+ if (type === 'file') {
131
+ await webcontainer.fs.readFile(filepath);
132
+ }
133
+ else {
134
+ await webcontainer.fs.readdir(filepath);
135
+ }
136
+ resolve(true);
137
+ }
138
+ catch {
139
+ resolve(false);
140
+ }
141
+ }, { ignoreCancel: true });
142
+ });
143
+ }
144
+ /**
145
+ * Load the provided files into WebContainer and remove any other files that had been loaded previously.
146
+ *
147
+ * This function always waits for any previous `prepareFiles` or `updateFile(s)` call to have completed
148
+ * before sending the next one.
149
+ *
150
+ * Previous load operations will be cancelled if `options.abortPreviousLoad` was set to true (which is the default).
151
+ *
152
+ * @see {LoadFilesOptions}
153
+ */
154
+ prepareFiles({ files, template, signal, abortPreviousLoad = true, removePaths, }) {
155
+ const previousLoadPromise = this._currentLoadTask?.promise;
156
+ if (abortPreviousLoad) {
157
+ this._currentLoadTask?.cancel();
158
+ }
159
+ this._currentLoadTask = newTask(async (signal) => {
160
+ await previousLoadPromise;
161
+ // no watcher should be installed
162
+ this._readyToWatch = false;
163
+ // stop current watcher if they are any
164
+ this._stopWatcher();
165
+ const webcontainer = await this._webcontainer;
166
+ signal.throwIfAborted();
167
+ [template, files] = await Promise.all([template, files]);
168
+ signal.throwIfAborted();
169
+ if (this._currentFiles || this._currentTemplate) {
170
+ if (removePaths) {
171
+ for (const path of removePaths) {
172
+ if (this._currentFiles) {
173
+ for (const filePath of Object.keys(this._currentFiles)) {
174
+ if (filePath.startsWith(path)) {
175
+ delete this._currentFiles[filePath];
176
+ }
177
+ }
178
+ }
179
+ await webcontainer.fs.rm(path, { recursive: true });
180
+ }
181
+ }
182
+ await updateFiles(webcontainer, { ...this._currentTemplate, ...this._currentFiles }, { ...template, ...files });
183
+ }
184
+ else {
185
+ await webcontainer.mount(toFileTree({ ...template, ...files }));
186
+ }
187
+ this._currentTemplate = { ...template };
188
+ this._currentFiles = { ...files };
189
+ this._updateDirtyState({ ...template, ...files });
190
+ }, { ignoreCancel: true, signal });
191
+ return this._currentLoadTask.promise;
192
+ }
193
+ /**
194
+ * Runs the list of commands set with `setCommands`.
195
+ *
196
+ * This function always wait for any previous `runCommands` call to have completed before sending the next one.
197
+ * It will cancel the previous operation if `options.abortPreviousRun` was set to true.
198
+ *
199
+ * Commands are split into two:
200
+ *
201
+ * - `prepareCommands`: For example commands like `npm install`, `mkdir -p src/foobar`, etc.
202
+ * - `mainCommand`: Used to for example run a dev server or equivalent.
203
+ *
204
+ * @see {LoadFilesOptions}
205
+ */
206
+ runCommands({ abortPreviousRun = true } = {}) {
207
+ const previousTask = this._currentProcessTask;
208
+ const loadPromise = this._currentLoadTask?.promise;
209
+ const newCommands = this._currentRunCommands;
210
+ const commandsChanged = this._commandsChanged;
211
+ if (!newCommands) {
212
+ throw new Error('setCommands should be called before runCommands');
213
+ }
214
+ this._currentProcessTask = newTask(async (signal) => {
215
+ /**
216
+ * Make sure we wait for everything to be loaded on the fs before
217
+ * checking for changes. We do this because we want to know if the
218
+ * `package.json` changed.
219
+ */
220
+ await loadPromise;
221
+ if (signal.aborted && abortPreviousRun) {
222
+ previousTask?.cancel();
223
+ }
224
+ signal.throwIfAborted();
225
+ const anyChange = this._packageJsonDirty || commandsChanged;
226
+ if (!anyChange) {
227
+ /**
228
+ * If there are no changes and we have a previous task, then
229
+ * we must link this new task to that previous one, otherwise
230
+ * the link is broken and that task will never ends.
231
+ *
232
+ * We create that link here by awaiting it. Note that this `if`
233
+ * here should always evaluate to true.
234
+ */
235
+ if (previousTask) {
236
+ const abortListener = () => previousTask.cancel();
237
+ signal.addEventListener('abort', abortListener, { once: true });
238
+ return previousTask.promise;
239
+ }
240
+ return undefined;
241
+ }
242
+ // there were changes so we reset the "commands changed"
243
+ this._commandsChanged = false;
244
+ if (abortPreviousRun) {
245
+ previousTask?.cancel();
246
+ }
247
+ await previousTask?.promise;
248
+ const webcontainer = await this._webcontainer;
249
+ signal.throwIfAborted();
250
+ return this._runCommands(webcontainer, newCommands, signal);
251
+ }, { ignoreCancel: true });
252
+ }
253
+ /**
254
+ * Restart the last run commands that were submitted.
255
+ */
256
+ restartLastRunCommands() {
257
+ if (!this._currentRunCommands || !this._currentProcessTask) {
258
+ return;
259
+ }
260
+ const previousRunCommands = this._currentRunCommands;
261
+ const previousProcessPromise = this._currentProcessTask.promise;
262
+ const loadPromise = this._currentLoadTask?.promise;
263
+ this._currentProcessTask.cancel();
264
+ this._currentProcessTask = newTask(async (signal) => {
265
+ await Promise.all([previousProcessPromise, loadPromise]);
266
+ const webcontainer = await this._webcontainer;
267
+ signal.throwIfAborted();
268
+ return this._runCommands(webcontainer, previousRunCommands, signal);
269
+ }, { ignoreCancel: true });
270
+ }
271
+ /**
272
+ * Get snapshot of runner's current files.
273
+ * Also prepares `package.json`'s `stackblitz.startCommand` with runner's commands.
274
+ *
275
+ * Note that file paths do not contain the leading `/`.
276
+ */
277
+ takeSnapshot() {
278
+ const files = {};
279
+ // first add template files
280
+ for (const [filePath, value] of Object.entries(this._currentTemplate || {})) {
281
+ if (typeof value === 'string') {
282
+ files[filePath.slice(1)] = value;
283
+ }
284
+ }
285
+ // next overwrite with files from editor
286
+ for (const [filePath, value] of Object.entries(this._currentFiles || {})) {
287
+ if (typeof value === 'string') {
288
+ files[filePath.slice(1)] = value;
289
+ }
290
+ }
291
+ if (this._packageJsonContent) {
292
+ let packageJson;
293
+ try {
294
+ packageJson = JSON.parse(this._packageJsonContent);
295
+ }
296
+ catch { }
297
+ // add start commands when missing
298
+ if (packageJson && !packageJson.stackblitz?.startCommand) {
299
+ const mainCommand = this._currentRunCommands?.mainCommand?.shellCommand;
300
+ const prepareCommands = (this._currentRunCommands?.prepareCommands || []).map((c) => c.shellCommand);
301
+ const startCommand = [...prepareCommands, mainCommand].filter(Boolean).join(' && ');
302
+ files[this._packageJsonPath.slice(1)] = JSON.stringify({ ...packageJson, stackblitz: { startCommand } }, null, 2);
303
+ }
304
+ }
305
+ return { files };
306
+ }
307
+ async _runCommands(webcontainer, commands, signal) {
308
+ const output = this._terminalStore.getOutputPanel();
309
+ clearTerminal(output);
310
+ const abortListener = () => this._currentCommandProcess?.kill();
311
+ signal.addEventListener('abort', abortListener, { once: true });
312
+ let shouldClearDirtyFlag = true;
313
+ try {
314
+ const commandList = Array.from(commands);
315
+ this._stepController.setFromCommands(commandList);
316
+ // keep track of the current runnable command we are on
317
+ let runnableCommands = 0;
318
+ for (const [index, command] of commandList.entries()) {
319
+ const isMainCommand = index === commandList.length - 1 && !!commands.mainCommand;
320
+ if (!command.isRunnable()) {
321
+ this._stepController.updateStep(index, {
322
+ title: command.title,
323
+ status: 'skipped',
324
+ });
325
+ continue;
326
+ }
327
+ this._stepController.updateStep(index, {
328
+ title: command.title,
329
+ status: 'running',
330
+ });
331
+ // print newlines between commands to visually separate them from one another
332
+ if (runnableCommands > 0) {
333
+ output?.write('\n');
334
+ }
335
+ runnableCommands++;
336
+ this._currentCommandProcess = await this._newProcess(webcontainer, output, command.shellCommand);
337
+ try {
338
+ signal.throwIfAborted();
339
+ }
340
+ catch (error) {
341
+ this._stepController.skipRemaining(index);
342
+ throw error;
343
+ }
344
+ if (isMainCommand) {
345
+ shouldClearDirtyFlag = false;
346
+ this._setupWatcher(webcontainer);
347
+ this._clearDirtyState();
348
+ }
349
+ const exitCode = await this._currentCommandProcess.exit;
350
+ if (exitCode !== 0) {
351
+ this._stepController.updateStep(index, {
352
+ title: command.title,
353
+ status: 'failed',
354
+ });
355
+ this._stepController.skipRemaining(index + 1);
356
+ /**
357
+ * We don't clear the dirty flag in that case as there was an error and re-running all commands
358
+ * is the probably better than not running anything.
359
+ */
360
+ shouldClearDirtyFlag = false;
361
+ break;
362
+ }
363
+ else {
364
+ this._stepController.updateStep(index, {
365
+ title: command.title,
366
+ status: 'completed',
367
+ });
368
+ }
369
+ try {
370
+ signal.throwIfAborted();
371
+ }
372
+ catch (error) {
373
+ this._stepController.skipRemaining(index + 1);
374
+ throw error;
375
+ }
376
+ }
377
+ /**
378
+ * All commands were run but we didn't clear the dirty state.
379
+ * We have to, otherwise we would re-run those commands when moving
380
+ * to a lesson that has the exact same set of commands.
381
+ */
382
+ if (shouldClearDirtyFlag) {
383
+ this._clearDirtyState();
384
+ }
385
+ // make sure the watcher is configured
386
+ this._setupWatcher(webcontainer);
387
+ }
388
+ finally {
389
+ signal.removeEventListener('abort', abortListener);
390
+ }
391
+ }
392
+ async _newProcess(webcontainer, output, shellCommand) {
393
+ const [command, ...args] = shellCommand.split(' ');
394
+ output?.write(`${escapeCodes.magenta('❯')} ${escapeCodes.green(command)} ${args.join(' ')}\n`);
395
+ /**
396
+ * We spawn the process and use a fallback for cols and rows in case the output is not connected to a visible
397
+ * terminal yet.
398
+ */
399
+ const process = await webcontainer.spawn(command, args, {
400
+ terminal: output
401
+ ? {
402
+ cols: output.cols ?? 80,
403
+ rows: output.rows ?? 15,
404
+ }
405
+ : undefined,
406
+ });
407
+ process.output.pipeTo(new WritableStream({ write: (data) => output?.write(data) }));
408
+ return process;
409
+ }
410
+ _updateDirtyState(files) {
411
+ for (const filePath in files) {
412
+ if (filePath.endsWith('/package.json') && files[filePath] != this._packageJsonContent) {
413
+ this._packageJsonContent = files[filePath];
414
+ this._packageJsonPath = filePath;
415
+ this._packageJsonDirty = true;
416
+ return;
417
+ }
418
+ }
419
+ }
420
+ _updateCurrentFiles(files) {
421
+ // if the file was not tracked by the existing list of files, add it
422
+ if (this._currentFiles) {
423
+ for (const filePath in files) {
424
+ this._currentFiles[filePath] = files[filePath];
425
+ }
426
+ }
427
+ else {
428
+ this._currentFiles = { ...files };
429
+ }
430
+ this._updateDirtyState(files);
431
+ }
432
+ _stopWatcher() {
433
+ // if there was a watcher terminate it
434
+ if (this._watcher) {
435
+ this._watcher.close();
436
+ this._watcher = undefined;
437
+ }
438
+ }
439
+ _setupWatcher(webcontainer) {
440
+ // inform that the watcher could be installed if we wanted to
441
+ this._readyToWatch = true;
442
+ // if the watcher is alreay setup or we don't sync content we exit
443
+ if (this._watcher || !this._watchContentFromWebContainer) {
444
+ return;
445
+ }
446
+ const filesToRead = new Map();
447
+ let timeoutId;
448
+ const readFiles = () => {
449
+ const files = [...filesToRead.entries()];
450
+ filesToRead.clear();
451
+ Promise.all(files.map(async ([filePath, encoding]) => {
452
+ // casts could be removed with an `if` but it feels weird
453
+ const content = (await webcontainer.fs.readFile(filePath, encoding));
454
+ return [filePath, content];
455
+ })).then((fileContents) => {
456
+ for (const [filePath, content] of fileContents) {
457
+ this._editorStore.updateFile(filePath, content);
458
+ }
459
+ });
460
+ };
461
+ /**
462
+ * Add a file to the list of files to read and schedule a read for later, effectively debouncing the reads.
463
+ *
464
+ * This does not cancel any existing requests because those are expected to be completed really
465
+ * fast. However every read request allocate memory that needs to be freed. The reason we debounce
466
+ * is to avoid running into OOM issues (which has happened in the past) and give time to the GC to
467
+ * cleanup the allocated buffers.
468
+ */
469
+ const scheduleReadFor = (filePath, encoding) => {
470
+ filesToRead.set(filePath, encoding);
471
+ clearTimeout(timeoutId);
472
+ timeoutId = setTimeout(readFiles, 100);
473
+ };
474
+ this._watcher = webcontainer.fs.watch('.', { recursive: true }, async (eventType, filename) => {
475
+ const filePath = `/${filename}`;
476
+ // events we should ignore because we caused them in the TutorialRunner
477
+ if (!this._ignoreFileEvents.decrement(filePath)) {
478
+ return;
479
+ }
480
+ if (Array.isArray(this._watchContentFromWebContainer) &&
481
+ !this._watchContentFromWebContainer.some((pattern) => picomatch.isMatch(filePath, pattern))) {
482
+ return;
483
+ }
484
+ if (eventType === 'change') {
485
+ /**
486
+ * Update file
487
+ * we ignore all paths that aren't exposed in the `_editorStore`
488
+ */
489
+ const file = this._editorStore.documents.get()[filePath];
490
+ if (!file) {
491
+ return;
492
+ }
493
+ scheduleReadFor(filePath, typeof file.value === 'string' ? 'utf-8' : null);
494
+ }
495
+ else if (eventType === 'rename' && Array.isArray(this._watchContentFromWebContainer)) {
496
+ const file = this._editorStore.documents.get()[filePath];
497
+ if (file) {
498
+ // remove file
499
+ this._editorStore.deleteFile(filePath);
500
+ }
501
+ else {
502
+ // add file
503
+ const segments = filePath.split('/');
504
+ for (let index = 0; index < segments.length - 1; index++) {
505
+ const folderPath = segments.slice(0, index + 1).join('/');
506
+ if (!this._editorStore.documents.get()[folderPath]) {
507
+ const isDirectory = await _isDirectory(webcontainer, folderPath);
508
+ this._editorStore.addFileOrFolder({ path: folderPath, type: isDirectory ? 'folder' : 'file' });
509
+ }
510
+ }
511
+ if (!this._editorStore.documents.get()[filePath]) {
512
+ this._editorStore.addFileOrFolder({ path: filePath, type: 'file' });
513
+ }
514
+ this._updateCurrentFiles({ [filePath]: '' });
515
+ scheduleReadFor(filePath, 'utf-8');
516
+ }
517
+ }
518
+ });
519
+ }
520
+ _clearDirtyState() {
521
+ this._packageJsonDirty = false;
522
+ }
523
+ _changeDetection(newCommands) {
524
+ if (this._packageJsonDirty) {
525
+ return true;
526
+ }
527
+ if (!this._currentRunCommands) {
528
+ return true;
529
+ }
530
+ const prevCommandList = commandsToList(this._currentRunCommands);
531
+ const newCommandList = commandsToList(newCommands);
532
+ if (prevCommandList.length !== newCommandList.length) {
533
+ return true;
534
+ }
535
+ for (let i = 0; i < prevCommandList.length; ++i) {
536
+ if (!Command.equals(prevCommandList[i], newCommandList[i])) {
537
+ return true;
538
+ }
539
+ }
540
+ return false;
541
+ }
542
+ }
543
+ function commandsToList(commands) {
544
+ if (commands instanceof Commands) {
545
+ return Array.from(commands).filter((command) => command.isRunnable());
546
+ }
547
+ return commandsToList(new Commands(commands));
548
+ }
549
+ async function updateFiles(webcontainer, previousFiles, newFiles) {
550
+ const { removed, addedOrModified } = diffFiles(previousFiles, newFiles);
551
+ for (const filePath of removed) {
552
+ await webcontainer.fs.rm(filePath, { force: true });
553
+ }
554
+ await webcontainer.mount(toFileTree(addedOrModified));
555
+ }
556
+ async function _isDirectory(webcontainer, filePath) {
557
+ try {
558
+ await webcontainer.fs.readdir(filePath);
559
+ return true;
560
+ }
561
+ catch {
562
+ return false;
563
+ }
564
+ }
@@ -0,0 +1,22 @@
1
+ export declare const kTaskCancelled: unique symbol;
2
+ export type TaskCancelled = typeof kTaskCancelled;
3
+ export interface Task<T> {
4
+ promise: Promise<T>;
5
+ cancel(): void;
6
+ }
7
+ export declare class AbortError extends Error {
8
+ }
9
+ /**
10
+ * A helper function to easily create "cancellable" promises where
11
+ * once a promise is cancelled it resolves to the "cancel" symbol.
12
+ *
13
+ * @param task - A function that return a promise.
14
+ * @returns The newly created task.
15
+ */
16
+ export declare function newTask<T>(task: (abortSignal: AbortSignal) => Promise<T>, opts: {
17
+ ignoreCancel: true;
18
+ signal?: AbortSignal;
19
+ }): Task<T | TaskCancelled>;
20
+ export declare function newTask<T>(task: (abortSignal: AbortSignal) => Promise<T>, opts?: {
21
+ signal?: AbortSignal;
22
+ }): Task<T>;
package/dist/tasks.js ADDED
@@ -0,0 +1,26 @@
1
+ export const kTaskCancelled = Symbol('kTaskCancelled');
2
+ export class AbortError extends Error {
3
+ }
4
+ export function newTask(task, opts = {}) {
5
+ const abortController = new AbortController();
6
+ const abortListener = () => abortController.abort(new AbortError());
7
+ const signal = opts.signal;
8
+ let runningTask = task(abortController.signal);
9
+ if (signal) {
10
+ runningTask = runningTask.finally(() => signal.removeEventListener('abort', abortListener));
11
+ signal.addEventListener('abort', abortListener, { once: true });
12
+ }
13
+ return {
14
+ promise: !opts.ignoreCancel
15
+ ? runningTask
16
+ : runningTask.catch((reason) => {
17
+ if (!(reason instanceof AbortError)) {
18
+ throw reason;
19
+ }
20
+ return kTaskCancelled;
21
+ }),
22
+ cancel() {
23
+ abortController.abort(new AbortError());
24
+ },
25
+ };
26
+ }
@@ -0,0 +1,5 @@
1
+ export declare class MultiCounter {
2
+ private _counts;
3
+ increment(name: string | string[]): void;
4
+ decrement(name: string): boolean;
5
+ }
@@ -0,0 +1,19 @@
1
+ export class MultiCounter {
2
+ _counts = new Map();
3
+ increment(name) {
4
+ if (typeof name === 'string') {
5
+ const currentValue = this._counts.get(name) ?? 0;
6
+ this._counts.set(name, currentValue + 1);
7
+ return;
8
+ }
9
+ name.forEach((value) => this.increment(value));
10
+ }
11
+ decrement(name) {
12
+ const currentValue = this._counts.get(name) ?? 0;
13
+ if (currentValue === 0) {
14
+ return true;
15
+ }
16
+ this._counts.set(name, currentValue - 1);
17
+ return currentValue - 1 === 0;
18
+ }
19
+ }
@@ -0,0 +1,8 @@
1
+ export declare function withResolvers<T>(): PromiseWithResolvers<T>;
2
+ export declare function wait(ms: number): Promise<void>;
3
+ /**
4
+ * Simulates a single tick of the event loop.
5
+ *
6
+ * @returns A promise that resolves after the tick.
7
+ */
8
+ export declare function tick(): Promise<void>;
@@ -0,0 +1,29 @@
1
+ export function withResolvers() {
2
+ if (typeof Promise.withResolvers === 'function') {
3
+ return Promise.withResolvers();
4
+ }
5
+ let resolve;
6
+ let reject;
7
+ const promise = new Promise((_resolve, _reject) => {
8
+ resolve = _resolve;
9
+ reject = _reject;
10
+ });
11
+ return {
12
+ resolve,
13
+ reject,
14
+ promise,
15
+ };
16
+ }
17
+ export function wait(ms) {
18
+ return new Promise((resolve) => setTimeout(resolve, ms));
19
+ }
20
+ /**
21
+ * Simulates a single tick of the event loop.
22
+ *
23
+ * @returns A promise that resolves after the tick.
24
+ */
25
+ export function tick() {
26
+ return new Promise((resolve) => {
27
+ setTimeout(resolve);
28
+ });
29
+ }
@@ -0,0 +1 @@
1
+ export declare function isWebContainerSupported(): boolean;