@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,23 @@
1
+ export function isWebContainerSupported() {
2
+ try {
3
+ const hasSharedArrayBuffer = 'SharedArrayBuffer' in globalThis;
4
+ const looksLikeChrome = navigator.userAgent.includes('Chrome');
5
+ const looksLikeFirefox = navigator.userAgent.includes('Firefox');
6
+ const looksLikeSafari = navigator.userAgent.includes('Safari');
7
+ if (hasSharedArrayBuffer && (looksLikeChrome || looksLikeFirefox)) {
8
+ return true;
9
+ }
10
+ if (hasSharedArrayBuffer && looksLikeSafari) {
11
+ // we only support Safari 16.4 and up so we check for the version here
12
+ const match = navigator.userAgent.match(/Version\/(\d+)\.(\d+) (?:Mobile\/.*?)?Safari/);
13
+ const majorVersion = match ? Number(match?.[1]) : 0;
14
+ const minorVersion = match ? Number(match?.[2]) : 0;
15
+ return majorVersion > 16 || (majorVersion === 16 && minorVersion >= 4);
16
+ }
17
+ // allow overriding the support check with localStorage.webcontainer_any_ua = 1
18
+ return Boolean(localStorage.getItem('webcontainer_any_ua'));
19
+ }
20
+ catch {
21
+ return false;
22
+ }
23
+ }
@@ -0,0 +1,17 @@
1
+ export interface ITerminal {
2
+ readonly cols?: number;
3
+ readonly rows?: number;
4
+ reset: () => void;
5
+ write: (data: string) => void;
6
+ input: (data: string) => void;
7
+ onData: (cb: (data: string) => void) => void;
8
+ }
9
+ export declare const escapeCodes: {
10
+ reset: string;
11
+ clear: string;
12
+ red: (text: string) => string;
13
+ gray: (text: string) => string;
14
+ green: (text: string) => string;
15
+ magenta: (text: string) => string;
16
+ };
17
+ export declare function clearTerminal(terminal?: ITerminal): void;
@@ -0,0 +1,13 @@
1
+ const reset = '\x1b[0m';
2
+ export const escapeCodes = {
3
+ reset,
4
+ clear: '\x1b[g',
5
+ red: (text) => `\x1b[1;31m${text}${reset}`,
6
+ gray: (text) => `\x1b[37m${text}${reset}`,
7
+ green: (text) => `\x1b[1;32m${text}${reset}`,
8
+ magenta: (text) => `\x1b[35m${text}${reset}`,
9
+ };
10
+ export function clearTerminal(terminal) {
11
+ terminal?.reset();
12
+ terminal?.write(escapeCodes.clear);
13
+ }
@@ -0,0 +1,28 @@
1
+ import type { CommandSchema, CommandsSchema } from '@tutorialkit-rb/types';
2
+ export declare class Commands implements Iterable<Command> {
3
+ /**
4
+ * List of commands executed before the main command.
5
+ */
6
+ readonly prepareCommands?: Command[];
7
+ /**
8
+ * Main command to run. Typically a dev server, e.g. `npm run start`.
9
+ */
10
+ readonly mainCommand?: Command;
11
+ constructor({ prepareCommands, mainCommand }: CommandsSchema);
12
+ [Symbol.iterator](): Generator<Command, void, unknown>;
13
+ }
14
+ export declare class Command {
15
+ /**
16
+ * The underlying shell command.
17
+ */
18
+ readonly shellCommand: string;
19
+ /**
20
+ * Title describing what this command does, e.g., "Installing dependencies".
21
+ */
22
+ readonly title: string;
23
+ constructor(command: CommandSchema);
24
+ isRunnable(): boolean;
25
+ static equals(a: Command, b: Command): boolean;
26
+ static toTitle(command: CommandSchema): string;
27
+ static toShellCommand(command: CommandSchema): string;
28
+ }
@@ -0,0 +1,67 @@
1
+ export class Commands {
2
+ /**
3
+ * List of commands executed before the main command.
4
+ */
5
+ prepareCommands;
6
+ /**
7
+ * Main command to run. Typically a dev server, e.g. `npm run start`.
8
+ */
9
+ mainCommand;
10
+ constructor({ prepareCommands, mainCommand }) {
11
+ this.prepareCommands = prepareCommands?.map((command) => new Command(command));
12
+ this.mainCommand = mainCommand ? new Command(mainCommand) : undefined;
13
+ }
14
+ [Symbol.iterator]() {
15
+ const _this = this;
16
+ return (function* () {
17
+ for (const command of _this.prepareCommands ?? []) {
18
+ yield command;
19
+ }
20
+ if (_this.mainCommand) {
21
+ yield _this.mainCommand;
22
+ }
23
+ })();
24
+ }
25
+ }
26
+ export class Command {
27
+ /**
28
+ * The underlying shell command.
29
+ */
30
+ shellCommand;
31
+ /**
32
+ * Title describing what this command does, e.g., "Installing dependencies".
33
+ */
34
+ title;
35
+ constructor(command) {
36
+ this.shellCommand = Command.toShellCommand(command);
37
+ this.title = Command.toTitle(command);
38
+ }
39
+ isRunnable() {
40
+ return this.shellCommand !== '';
41
+ }
42
+ static equals(a, b) {
43
+ return a.shellCommand === b.shellCommand;
44
+ }
45
+ static toTitle(command) {
46
+ let title = '';
47
+ if (typeof command === 'string') {
48
+ title = command;
49
+ }
50
+ else if (Array.isArray(command)) {
51
+ title = command[1];
52
+ }
53
+ else {
54
+ title = command.title;
55
+ }
56
+ return title;
57
+ }
58
+ static toShellCommand(command) {
59
+ if (typeof command === 'string') {
60
+ return command;
61
+ }
62
+ if (Array.isArray(command)) {
63
+ return command[0];
64
+ }
65
+ return command.command;
66
+ }
67
+ }
@@ -0,0 +1,12 @@
1
+ import type { EditorSchema } from '@tutorialkit-rb/types';
2
+ export declare class EditorConfig {
3
+ private _config;
4
+ constructor(config?: EditorSchema);
5
+ get visible(): boolean;
6
+ get fileTree(): {
7
+ /** Visibility of file tree */
8
+ visible: boolean;
9
+ /** Whether to allow file and folder editing in file tree */
10
+ allowEdits: false | string[];
11
+ };
12
+ }
@@ -0,0 +1,60 @@
1
+ export class EditorConfig {
2
+ _config;
3
+ constructor(config) {
4
+ this._config = normalizeEditorConfig(config);
5
+ }
6
+ get visible() {
7
+ return this._config.visible;
8
+ }
9
+ get fileTree() {
10
+ return this._config.fileTree;
11
+ }
12
+ }
13
+ function normalizeEditorConfig(config) {
14
+ if (config === false) {
15
+ return {
16
+ visible: false,
17
+ fileTree: {
18
+ visible: false,
19
+ allowEdits: false,
20
+ },
21
+ };
22
+ }
23
+ if (config === undefined || config === true) {
24
+ return {
25
+ visible: true,
26
+ fileTree: {
27
+ visible: true,
28
+ allowEdits: false,
29
+ },
30
+ };
31
+ }
32
+ if (typeof config.fileTree === 'boolean') {
33
+ return {
34
+ visible: true,
35
+ fileTree: {
36
+ visible: config.fileTree,
37
+ allowEdits: false,
38
+ },
39
+ };
40
+ }
41
+ if (typeof config.fileTree?.allowEdits === 'boolean' || !config.fileTree?.allowEdits) {
42
+ return {
43
+ visible: true,
44
+ fileTree: {
45
+ visible: true,
46
+ allowEdits: config.fileTree?.allowEdits ? ['**'] : false,
47
+ },
48
+ };
49
+ }
50
+ return {
51
+ visible: true,
52
+ fileTree: {
53
+ visible: true,
54
+ allowEdits: toArray(config.fileTree.allowEdits),
55
+ },
56
+ };
57
+ }
58
+ function toArray(items) {
59
+ return Array.isArray(items) ? items : [items];
60
+ }
@@ -0,0 +1,4 @@
1
+ export { Command, Commands } from './command.js';
2
+ export { safeBoot } from './on-demand-boot.js';
3
+ export { PreviewInfo } from './preview-info.js';
4
+ export { StepsController, type Step, type Steps } from './steps.js';
@@ -0,0 +1,4 @@
1
+ export { Command, Commands } from './command.js';
2
+ export { safeBoot } from './on-demand-boot.js';
3
+ export { PreviewInfo } from './preview-info.js';
4
+ export { StepsController } from './steps.js';
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Lightweight wrapper around WebContainer.boot
3
+ * to only boot if there's no risk on crashing the browser.
4
+ *
5
+ * This typicall might happen on iOS.
6
+ *
7
+ * When iOS is detected, a call to `unblock()` is required
8
+ * to move forward with the boot.
9
+ */
10
+ import { WebContainer, type BootOptions } from '@webcontainer/api';
11
+ import { type ReadableAtom } from 'nanostores';
12
+ export type BootStatus = 'unknown' | 'blocked' | 'booting' | 'booted';
13
+ export declare const bootStatus: ReadableAtom<BootStatus>;
14
+ export declare function safeBoot(options: BootOptions): Promise<WebContainer>;
15
+ export declare function unblockBoot(): void;
@@ -0,0 +1,39 @@
1
+ /**
2
+ * Lightweight wrapper around WebContainer.boot
3
+ * to only boot if there's no risk on crashing the browser.
4
+ *
5
+ * This typicall might happen on iOS.
6
+ *
7
+ * When iOS is detected, a call to `unblock()` is required
8
+ * to move forward with the boot.
9
+ */
10
+ import { WebContainer } from '@webcontainer/api';
11
+ import { atom } from 'nanostores';
12
+ import { withResolvers } from '../utils/promises.js';
13
+ const localBootStatus = atom('unknown');
14
+ const blockingStatus = withResolvers();
15
+ export const bootStatus = localBootStatus;
16
+ export async function safeBoot(options) {
17
+ if (localBootStatus.get() === 'unknown') {
18
+ localBootStatus.set(isRestricted() ? 'blocked' : 'booting');
19
+ }
20
+ if (localBootStatus.get() === 'blocked') {
21
+ await blockingStatus.promise;
22
+ localBootStatus.set('booting');
23
+ }
24
+ const webcontainer = await WebContainer.boot(options);
25
+ localBootStatus.set('booted');
26
+ return webcontainer;
27
+ }
28
+ export function unblockBoot() {
29
+ if (localBootStatus.get() !== 'blocked') {
30
+ return;
31
+ }
32
+ blockingStatus.resolve();
33
+ }
34
+ function isRestricted() {
35
+ const { userAgent, maxTouchPoints, platform } = navigator;
36
+ const iOS = /iPhone/.test(userAgent) || platform === 'iPhone';
37
+ const iPadOS = (platform === 'MacIntel' && maxTouchPoints > 1) || platform === 'iPad';
38
+ return iOS || iPadOS;
39
+ }
@@ -0,0 +1,6 @@
1
+ export declare class PortInfo {
2
+ readonly port: number;
3
+ origin?: string | undefined;
4
+ ready: boolean;
5
+ constructor(port: number, origin?: string | undefined, ready?: boolean);
6
+ }
@@ -0,0 +1,10 @@
1
+ export class PortInfo {
2
+ port;
3
+ origin;
4
+ ready;
5
+ constructor(port, origin, ready = false) {
6
+ this.port = port;
7
+ this.origin = origin;
8
+ this.ready = ready;
9
+ }
10
+ }
@@ -0,0 +1,21 @@
1
+ import type { PreviewSchema } from '@tutorialkit-rb/types';
2
+ import { PortInfo } from './port-info.js';
3
+ export declare class PreviewInfo {
4
+ readonly portInfo: PortInfo;
5
+ title?: string;
6
+ pathname?: string;
7
+ get url(): string | undefined;
8
+ get port(): number;
9
+ get baseUrl(): string | undefined;
10
+ get ready(): boolean;
11
+ constructor(preview: Omit<Preview, 'port'>, portInfo: PortInfo);
12
+ set path(val: string | undefined);
13
+ static parse(preview: Exclude<PreviewSchema, boolean>[0]): Preview;
14
+ static equals(a: PreviewInfo, b: PreviewInfo): boolean;
15
+ }
16
+ interface Preview {
17
+ port: number;
18
+ pathname?: string;
19
+ title?: string;
20
+ }
21
+ export {};
@@ -0,0 +1,56 @@
1
+ import { PortInfo } from './port-info.js';
2
+ export class PreviewInfo {
3
+ portInfo;
4
+ title;
5
+ pathname;
6
+ get url() {
7
+ if (this.baseUrl) {
8
+ return new URL(this.pathname ?? '/', this.baseUrl).toString();
9
+ }
10
+ return undefined;
11
+ }
12
+ get port() {
13
+ return this.portInfo.port;
14
+ }
15
+ get baseUrl() {
16
+ return this.portInfo.origin;
17
+ }
18
+ get ready() {
19
+ return this.portInfo.ready;
20
+ }
21
+ constructor(preview, portInfo) {
22
+ this.title = preview.title;
23
+ this.pathname = preview.pathname;
24
+ this.portInfo = portInfo;
25
+ }
26
+ set path(val) {
27
+ this.pathname = val;
28
+ }
29
+ static parse(preview) {
30
+ if (typeof preview === 'number') {
31
+ return {
32
+ port: preview,
33
+ };
34
+ }
35
+ else if (typeof preview === 'string') {
36
+ const [port, ...rest] = preview.split('/');
37
+ return {
38
+ port: parseInt(port),
39
+ pathname: rest.join('/'),
40
+ };
41
+ }
42
+ else if (Array.isArray(preview)) {
43
+ return {
44
+ port: preview[0],
45
+ title: preview[1],
46
+ pathname: preview[2],
47
+ };
48
+ }
49
+ else {
50
+ return preview;
51
+ }
52
+ }
53
+ static equals(a, b) {
54
+ return a.portInfo.port === b.portInfo.port && a.pathname === b.pathname && a.title === b.title;
55
+ }
56
+ }
@@ -0,0 +1,14 @@
1
+ import type { WebContainer } from '@webcontainer/api';
2
+ import type { ITerminal } from '../utils/terminal.js';
3
+ interface ProcessOptions {
4
+ /**
5
+ * Set to `true` if you want to allow redirecting output (e.g. `echo foo > bar`).
6
+ */
7
+ allowRedirects: boolean;
8
+ /**
9
+ * List of commands that are allowed by the JSH process.
10
+ */
11
+ allowCommands?: string[];
12
+ }
13
+ export declare function newJSHProcess(webcontainer: WebContainer, terminal: ITerminal, options: ProcessOptions | undefined): Promise<import("@webcontainer/api").WebContainerProcess>;
14
+ export {};
@@ -0,0 +1,46 @@
1
+ import { withResolvers } from '../utils/promises.js';
2
+ export async function newJSHProcess(webcontainer, terminal, options) {
3
+ const args = [];
4
+ if (!options?.allowRedirects) {
5
+ // if redirects are turned off, start JSH with `--no-redirects`
6
+ args.push('--no-redirects');
7
+ }
8
+ if (Array.isArray(options?.allowCommands)) {
9
+ // if only a subset of commands is allowed, pass it down to JSH
10
+ args.push('--allow-commands', options.allowCommands.join(','));
11
+ }
12
+ // we spawn a JSH process with a fallback cols and rows in case the process is not attached yet to a visible terminal
13
+ const process = await webcontainer.spawn('/bin/jsh', ['--osc', ...args], {
14
+ terminal: {
15
+ cols: terminal.cols ?? 80,
16
+ rows: terminal.rows ?? 15,
17
+ },
18
+ env: {
19
+ PATH: `${webcontainer.workdir}/bin:${webcontainer.path}`,
20
+ },
21
+ });
22
+ const input = process.input.getWriter();
23
+ const output = process.output;
24
+ const jshReady = withResolvers();
25
+ let isInteractive = false;
26
+ output.pipeTo(new WritableStream({
27
+ write(data) {
28
+ if (!isInteractive) {
29
+ const [, osc] = data.match(/\x1b\]654;([^\x07]+)\x07/) || [];
30
+ if (osc === 'interactive') {
31
+ // wait until we see the interactive OSC
32
+ isInteractive = true;
33
+ jshReady.resolve();
34
+ }
35
+ }
36
+ terminal.write(data);
37
+ },
38
+ }));
39
+ terminal.onData((data) => {
40
+ if (isInteractive) {
41
+ input.write(data);
42
+ }
43
+ });
44
+ await jshReady.promise;
45
+ return process;
46
+ }
@@ -0,0 +1,15 @@
1
+ import type { Command } from './command.js';
2
+ export type Steps = Step[];
3
+ export interface Step {
4
+ title: string;
5
+ status: 'completed' | 'running' | 'failed' | 'skipped' | 'idle';
6
+ }
7
+ export declare class StepsController {
8
+ /**
9
+ * Steps that the runner is or will be executing.
10
+ */
11
+ steps: import("nanostores").WritableAtom<Steps | undefined>;
12
+ setFromCommands(commands: Command[]): void;
13
+ updateStep(index: number, step: Step): void;
14
+ skipRemaining(index: number): void;
15
+ }
@@ -0,0 +1,38 @@
1
+ import { atom } from 'nanostores';
2
+ export class StepsController {
3
+ /**
4
+ * Steps that the runner is or will be executing.
5
+ */
6
+ steps = atom(undefined);
7
+ setFromCommands(commands) {
8
+ if (commands.length > 0) {
9
+ this.steps.set(commands.map((command) => ({
10
+ title: command.title,
11
+ status: 'idle',
12
+ })));
13
+ }
14
+ else {
15
+ this.steps.set(undefined);
16
+ }
17
+ }
18
+ updateStep(index, step) {
19
+ const currentSteps = this.steps.value;
20
+ if (!currentSteps) {
21
+ return;
22
+ }
23
+ this.steps.set([...currentSteps.slice(0, index), step, ...currentSteps.slice(index + 1)]);
24
+ }
25
+ skipRemaining(index) {
26
+ const currentSteps = this.steps.value;
27
+ if (!currentSteps) {
28
+ return;
29
+ }
30
+ this.steps.set([
31
+ ...currentSteps.slice(0, index),
32
+ ...currentSteps.slice(index).map((step) => ({
33
+ ...step,
34
+ status: 'skipped',
35
+ })),
36
+ ]);
37
+ }
38
+ }
@@ -0,0 +1,59 @@
1
+ import type { TerminalPanelType, TerminalSchema } from '@tutorialkit-rb/types';
2
+ import type { WebContainerProcess } from '@webcontainer/api';
3
+ import type { ITerminal } from '../utils/terminal.js';
4
+ interface TerminalPanelOptions {
5
+ id?: string;
6
+ title?: string;
7
+ allowRedirects?: boolean;
8
+ allowCommands?: string[];
9
+ }
10
+ export declare class TerminalConfig {
11
+ private _config;
12
+ constructor(config?: TerminalSchema);
13
+ get panels(): TerminalPanel[];
14
+ get activePanel(): number;
15
+ get defaultOpen(): boolean;
16
+ }
17
+ /**
18
+ * This class contains the state for a terminal panel. This is a panel which is attached to a process and renders
19
+ * the process output to a screen.
20
+ */
21
+ export declare class TerminalPanel implements ITerminal {
22
+ readonly type: TerminalPanelType;
23
+ private readonly _options?;
24
+ static panelCount: Record<TerminalPanelType, number>;
25
+ static resetCount(): void;
26
+ readonly id: string;
27
+ readonly title: string;
28
+ private _terminal?;
29
+ private _process?;
30
+ private _data;
31
+ private _onData?;
32
+ constructor(type: TerminalPanelType, _options?: TerminalPanelOptions | undefined);
33
+ get terminal(): ITerminal | undefined;
34
+ get process(): WebContainerProcess | undefined;
35
+ get processOptions(): {
36
+ allowRedirects: boolean;
37
+ allowCommands: string[] | undefined;
38
+ } | undefined;
39
+ get cols(): number | undefined;
40
+ get rows(): number | undefined;
41
+ reset(): void;
42
+ /** @internal*/
43
+ write(data: string): void;
44
+ input(data: string): void;
45
+ onData(callback: (data: string) => void): void;
46
+ /**
47
+ * Attach a WebContainer process to this panel.
48
+ *
49
+ * @param process The WebContainer process
50
+ */
51
+ attachProcess(process: WebContainerProcess): void;
52
+ /**
53
+ * Attach a terminal to this panel.
54
+ *
55
+ * @param terminal The terminal.
56
+ */
57
+ attachTerminal(terminal: ITerminal): void;
58
+ }
59
+ export {};