@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.
- package/README.md +18 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +2 -0
- package/dist/lesson-files.d.ts +22 -0
- package/dist/lesson-files.js +126 -0
- package/dist/store/editor.d.ts +29 -0
- package/dist/store/editor.js +127 -0
- package/dist/store/index.d.ts +113 -0
- package/dist/store/index.js +316 -0
- package/dist/store/previews.d.ts +19 -0
- package/dist/store/previews.js +80 -0
- package/dist/store/terminal.d.ts +24 -0
- package/dist/store/terminal.js +128 -0
- package/dist/store/tutorial-runner.d.ts +147 -0
- package/dist/store/tutorial-runner.js +564 -0
- package/dist/tasks.d.ts +22 -0
- package/dist/tasks.js +26 -0
- package/dist/utils/multi-counter.d.ts +5 -0
- package/dist/utils/multi-counter.js +19 -0
- package/dist/utils/promises.d.ts +8 -0
- package/dist/utils/promises.js +29 -0
- package/dist/utils/support.d.ts +1 -0
- package/dist/utils/support.js +23 -0
- package/dist/utils/terminal.d.ts +17 -0
- package/dist/utils/terminal.js +13 -0
- package/dist/webcontainer/command.d.ts +28 -0
- package/dist/webcontainer/command.js +67 -0
- package/dist/webcontainer/editor-config.d.ts +12 -0
- package/dist/webcontainer/editor-config.js +60 -0
- package/dist/webcontainer/index.d.ts +4 -0
- package/dist/webcontainer/index.js +4 -0
- package/dist/webcontainer/on-demand-boot.d.ts +15 -0
- package/dist/webcontainer/on-demand-boot.js +39 -0
- package/dist/webcontainer/port-info.d.ts +6 -0
- package/dist/webcontainer/port-info.js +10 -0
- package/dist/webcontainer/preview-info.d.ts +21 -0
- package/dist/webcontainer/preview-info.js +56 -0
- package/dist/webcontainer/shell.d.ts +14 -0
- package/dist/webcontainer/shell.js +46 -0
- package/dist/webcontainer/steps.d.ts +15 -0
- package/dist/webcontainer/steps.js +38 -0
- package/dist/webcontainer/terminal-config.d.ts +59 -0
- package/dist/webcontainer/terminal-config.js +230 -0
- package/dist/webcontainer/utils/files.d.ts +10 -0
- package/dist/webcontainer/utils/files.js +76 -0
- package/package.json +53 -0
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
export class TerminalConfig {
|
|
2
|
+
_config;
|
|
3
|
+
constructor(config) {
|
|
4
|
+
const normalized = normalizeTerminalConfig(config);
|
|
5
|
+
this._config = normalized;
|
|
6
|
+
}
|
|
7
|
+
get panels() {
|
|
8
|
+
return this._config.panels;
|
|
9
|
+
}
|
|
10
|
+
get activePanel() {
|
|
11
|
+
return this._config.activePanel;
|
|
12
|
+
}
|
|
13
|
+
get defaultOpen() {
|
|
14
|
+
return this._config.defaultOpen;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
const TERMINAL_PANEL_TITLES = {
|
|
18
|
+
output: 'Output',
|
|
19
|
+
terminal: 'Terminal',
|
|
20
|
+
};
|
|
21
|
+
let globalId = 0;
|
|
22
|
+
/**
|
|
23
|
+
* This class contains the state for a terminal panel. This is a panel which is attached to a process and renders
|
|
24
|
+
* the process output to a screen.
|
|
25
|
+
*/
|
|
26
|
+
export class TerminalPanel {
|
|
27
|
+
type;
|
|
28
|
+
_options;
|
|
29
|
+
static panelCount = {
|
|
30
|
+
output: 0,
|
|
31
|
+
terminal: 0,
|
|
32
|
+
};
|
|
33
|
+
static resetCount() {
|
|
34
|
+
this.panelCount = {
|
|
35
|
+
output: 0,
|
|
36
|
+
terminal: 0,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
id;
|
|
40
|
+
title;
|
|
41
|
+
_terminal;
|
|
42
|
+
_process;
|
|
43
|
+
_data = [];
|
|
44
|
+
_onData;
|
|
45
|
+
constructor(type, _options) {
|
|
46
|
+
this.type = type;
|
|
47
|
+
this._options = _options;
|
|
48
|
+
let title = _options?.title;
|
|
49
|
+
// automatically infer a title if no title is provided
|
|
50
|
+
if (!title) {
|
|
51
|
+
title = TERMINAL_PANEL_TITLES[type];
|
|
52
|
+
// we keep track of all untitled panel and add an index to the title
|
|
53
|
+
const count = TerminalPanel.panelCount[type];
|
|
54
|
+
if (count > 0) {
|
|
55
|
+
title += ` ${count}`;
|
|
56
|
+
}
|
|
57
|
+
TerminalPanel.panelCount[type]++;
|
|
58
|
+
}
|
|
59
|
+
this.title = title;
|
|
60
|
+
this.id = _options?.id ?? (type === 'output' ? 'output' : `${type}-${globalId++}`);
|
|
61
|
+
}
|
|
62
|
+
get terminal() {
|
|
63
|
+
return this._terminal;
|
|
64
|
+
}
|
|
65
|
+
get process() {
|
|
66
|
+
return this._process;
|
|
67
|
+
}
|
|
68
|
+
get processOptions() {
|
|
69
|
+
if (this.type === 'output') {
|
|
70
|
+
return undefined;
|
|
71
|
+
}
|
|
72
|
+
return {
|
|
73
|
+
allowRedirects: this._options?.allowRedirects ?? false,
|
|
74
|
+
allowCommands: this._options?.allowCommands,
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
// #region ITerminal methods
|
|
78
|
+
get cols() {
|
|
79
|
+
// we fallback to a default
|
|
80
|
+
return this._terminal?.cols;
|
|
81
|
+
}
|
|
82
|
+
get rows() {
|
|
83
|
+
return this._terminal?.rows;
|
|
84
|
+
}
|
|
85
|
+
reset() {
|
|
86
|
+
if (this._terminal) {
|
|
87
|
+
this._terminal.reset();
|
|
88
|
+
}
|
|
89
|
+
else {
|
|
90
|
+
this._data = [];
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
/** @internal*/
|
|
94
|
+
write(data) {
|
|
95
|
+
if (this._terminal) {
|
|
96
|
+
this._terminal.write(data);
|
|
97
|
+
}
|
|
98
|
+
else {
|
|
99
|
+
this._data.push({ data, type: 'echo' });
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
input(data) {
|
|
103
|
+
if (this.type !== 'terminal') {
|
|
104
|
+
throw new Error('Cannot write data to output-only terminal');
|
|
105
|
+
}
|
|
106
|
+
if (this._terminal) {
|
|
107
|
+
this._terminal.input(data);
|
|
108
|
+
}
|
|
109
|
+
else {
|
|
110
|
+
this._data.push({ data, type: 'input' });
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
onData(callback) {
|
|
114
|
+
if (this._terminal) {
|
|
115
|
+
this._terminal.onData(callback);
|
|
116
|
+
}
|
|
117
|
+
else {
|
|
118
|
+
this._onData = callback;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
// #endregion
|
|
122
|
+
/**
|
|
123
|
+
* Attach a WebContainer process to this panel.
|
|
124
|
+
*
|
|
125
|
+
* @param process The WebContainer process
|
|
126
|
+
*/
|
|
127
|
+
attachProcess(process) {
|
|
128
|
+
this._process = process;
|
|
129
|
+
if (this.cols != null && this.rows != null) {
|
|
130
|
+
this._process.resize({ cols: this.cols, rows: this.rows });
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Attach a terminal to this panel.
|
|
135
|
+
*
|
|
136
|
+
* @param terminal The terminal.
|
|
137
|
+
*/
|
|
138
|
+
attachTerminal(terminal) {
|
|
139
|
+
for (const { type, data } of this._data) {
|
|
140
|
+
if (type === 'echo') {
|
|
141
|
+
terminal.write(data);
|
|
142
|
+
}
|
|
143
|
+
else {
|
|
144
|
+
terminal.input(data);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
this._data = [];
|
|
148
|
+
this._terminal = terminal;
|
|
149
|
+
if (this._onData) {
|
|
150
|
+
terminal.onData(this._onData);
|
|
151
|
+
}
|
|
152
|
+
if (this.cols != null && this.rows != null) {
|
|
153
|
+
this._process?.resize({ cols: this.cols, rows: this.rows });
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* Normalize the provided configuration to a configuration which is easier to parse.
|
|
159
|
+
*
|
|
160
|
+
* @param config The terminal configuration.
|
|
161
|
+
* @returns A normalized terminal configuration.
|
|
162
|
+
*/
|
|
163
|
+
function normalizeTerminalConfig(config) {
|
|
164
|
+
let activePanel = 0;
|
|
165
|
+
if (config === false) {
|
|
166
|
+
// if the value is `false`, we don't render anything
|
|
167
|
+
return {
|
|
168
|
+
panels: [],
|
|
169
|
+
activePanel,
|
|
170
|
+
defaultOpen: false,
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
// reset the count so that the auto-infered titles are indexed properly
|
|
174
|
+
TerminalPanel.resetCount();
|
|
175
|
+
// if no config is set, or the value is `true`, we just render the output panel
|
|
176
|
+
if (config === undefined || config === true) {
|
|
177
|
+
return {
|
|
178
|
+
panels: [new TerminalPanel('output')],
|
|
179
|
+
activePanel,
|
|
180
|
+
defaultOpen: false,
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
const panels = [];
|
|
184
|
+
const options = {
|
|
185
|
+
allowRedirects: config.allowRedirects,
|
|
186
|
+
allowCommands: config.allowCommands,
|
|
187
|
+
};
|
|
188
|
+
if (config.panels) {
|
|
189
|
+
if (config.panels === 'output') {
|
|
190
|
+
panels.push(new TerminalPanel('output'));
|
|
191
|
+
}
|
|
192
|
+
else if (config.panels === 'terminal') {
|
|
193
|
+
panels.push(new TerminalPanel('terminal', options));
|
|
194
|
+
}
|
|
195
|
+
else if (Array.isArray(config.panels)) {
|
|
196
|
+
for (const panel of config.panels) {
|
|
197
|
+
let terminalPanel;
|
|
198
|
+
if (typeof panel === 'string') {
|
|
199
|
+
terminalPanel = new TerminalPanel(panel, options);
|
|
200
|
+
}
|
|
201
|
+
else if (Array.isArray(panel)) {
|
|
202
|
+
terminalPanel = new TerminalPanel(panel[0], {
|
|
203
|
+
title: panel[1],
|
|
204
|
+
...options,
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
else {
|
|
208
|
+
terminalPanel = new TerminalPanel(panel.type, {
|
|
209
|
+
id: panel.id,
|
|
210
|
+
title: panel.title,
|
|
211
|
+
allowRedirects: panel.allowRedirects ?? config.allowRedirects,
|
|
212
|
+
allowCommands: panel.allowCommands ?? config.allowCommands,
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
panels.push(terminalPanel);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
if (typeof config.activePanel === 'number') {
|
|
220
|
+
activePanel = config.activePanel;
|
|
221
|
+
if (activePanel >= panels.length) {
|
|
222
|
+
activePanel = 0;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
return {
|
|
226
|
+
activePanel,
|
|
227
|
+
panels,
|
|
228
|
+
defaultOpen: config.open || false,
|
|
229
|
+
};
|
|
230
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { Files } from '@tutorialkit-rb/types';
|
|
2
|
+
import type { FileSystemTree } from '@webcontainer/api';
|
|
3
|
+
export declare function areFilesEqual(a: Files, b: Files): boolean;
|
|
4
|
+
interface FilesDiff {
|
|
5
|
+
addedOrModified: Files;
|
|
6
|
+
removed: string[];
|
|
7
|
+
}
|
|
8
|
+
export declare function diffFiles(before: Files, after: Files): FilesDiff;
|
|
9
|
+
export declare function toFileTree(files: Files): FileSystemTree;
|
|
10
|
+
export {};
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
export function areFilesEqual(a, b) {
|
|
2
|
+
const aFilePaths = Object.keys(a);
|
|
3
|
+
const bFilePaths = Object.keys(b);
|
|
4
|
+
if (aFilePaths.length !== bFilePaths.length) {
|
|
5
|
+
return false;
|
|
6
|
+
}
|
|
7
|
+
aFilePaths.sort();
|
|
8
|
+
bFilePaths.sort();
|
|
9
|
+
for (let i = 0; i < aFilePaths.length; ++i) {
|
|
10
|
+
const filePath = aFilePaths[i];
|
|
11
|
+
if (bFilePaths[i] !== filePath) {
|
|
12
|
+
return false;
|
|
13
|
+
}
|
|
14
|
+
if (a[filePath] !== b[filePath]) {
|
|
15
|
+
return false;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
return true;
|
|
19
|
+
}
|
|
20
|
+
export function diffFiles(before, after) {
|
|
21
|
+
const addedOrModified = {};
|
|
22
|
+
const removed = [];
|
|
23
|
+
for (const filePath in before) {
|
|
24
|
+
const beforeFile = before[filePath];
|
|
25
|
+
const afterFile = after[filePath];
|
|
26
|
+
if (typeof afterFile == 'undefined') {
|
|
27
|
+
removed.push(filePath);
|
|
28
|
+
}
|
|
29
|
+
else if (beforeFile !== afterFile) {
|
|
30
|
+
addedOrModified[filePath] = afterFile;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
for (const filePath in after) {
|
|
34
|
+
if (!(filePath in before)) {
|
|
35
|
+
addedOrModified[filePath] = after[filePath];
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return {
|
|
39
|
+
addedOrModified,
|
|
40
|
+
removed,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
export function toFileTree(files) {
|
|
44
|
+
const root = {};
|
|
45
|
+
for (const filePath in files) {
|
|
46
|
+
const segments = filePath.split('/').filter((segment) => segment);
|
|
47
|
+
let currentTree = root;
|
|
48
|
+
for (let i = 0; i < segments.length; ++i) {
|
|
49
|
+
const name = segments[i];
|
|
50
|
+
if (i === segments.length - 1) {
|
|
51
|
+
currentTree[name] = {
|
|
52
|
+
file: {
|
|
53
|
+
contents: files[filePath],
|
|
54
|
+
},
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
else {
|
|
58
|
+
let folder = currentTree[name];
|
|
59
|
+
assertDirectoryNode(folder);
|
|
60
|
+
if (!folder) {
|
|
61
|
+
folder = {
|
|
62
|
+
directory: {},
|
|
63
|
+
};
|
|
64
|
+
currentTree[name] = folder;
|
|
65
|
+
}
|
|
66
|
+
currentTree = folder.directory;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return root;
|
|
71
|
+
}
|
|
72
|
+
function assertDirectoryNode(node) {
|
|
73
|
+
if (node && !('directory' in node)) {
|
|
74
|
+
throw new Error('Expected directory node');
|
|
75
|
+
}
|
|
76
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@tutorialkit-rb/runtime",
|
|
3
|
+
"version": "1.5.2-rb.0.1.0",
|
|
4
|
+
"description": "TutorialKit runtime",
|
|
5
|
+
"author": "StackBlitz Inc.",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"bugs": "https://github.com/stackblitz/tutorialkit/issues",
|
|
8
|
+
"homepage": "https://github.com/stackblitz/tutorialkit",
|
|
9
|
+
"license": "MIT",
|
|
10
|
+
"repository": {
|
|
11
|
+
"type": "git",
|
|
12
|
+
"url": "https://github.com/Bakaface/tutorialkit.rb",
|
|
13
|
+
"directory": "packages/runtime"
|
|
14
|
+
},
|
|
15
|
+
"types": "./dist/index.d.ts",
|
|
16
|
+
"exports": {
|
|
17
|
+
".": "./dist/index.js",
|
|
18
|
+
"./tasks": "./dist/tasks.js"
|
|
19
|
+
},
|
|
20
|
+
"files": [
|
|
21
|
+
"dist"
|
|
22
|
+
],
|
|
23
|
+
"typesVersions": {
|
|
24
|
+
"*": {
|
|
25
|
+
"tasks": [
|
|
26
|
+
"dist/tasks.d.ts"
|
|
27
|
+
]
|
|
28
|
+
}
|
|
29
|
+
},
|
|
30
|
+
"scripts": {
|
|
31
|
+
"build": "tsc -b tsconfig.build.json",
|
|
32
|
+
"dev": "pnpm run build --watch --preserveWatchOutput",
|
|
33
|
+
"test": "vitest"
|
|
34
|
+
},
|
|
35
|
+
"dependencies": {
|
|
36
|
+
"@tutorialkit-rb/types": "1.5.2",
|
|
37
|
+
"@webcontainer/api": "1.5.1",
|
|
38
|
+
"nanostores": "^0.10.3",
|
|
39
|
+
"picomatch": "^4.0.2"
|
|
40
|
+
},
|
|
41
|
+
"devDependencies": {
|
|
42
|
+
"@types/picomatch": "^3.0.1",
|
|
43
|
+
"typescript": "^5.4.5",
|
|
44
|
+
"vite": "^5.3.1",
|
|
45
|
+
"vite-tsconfig-paths": "^4.3.2",
|
|
46
|
+
"vitest": "^3.0.5"
|
|
47
|
+
},
|
|
48
|
+
"keywords": [
|
|
49
|
+
"ruby",
|
|
50
|
+
"rails",
|
|
51
|
+
"wasm"
|
|
52
|
+
]
|
|
53
|
+
}
|