├── .editorconfig
├── .gitignore
├── .idea
├── .gitignore
├── jsLibraryMappings.xml
├── modules.xml
├── vcs.xml
└── webcontainer-runtime.iml
├── LICENSE
├── README.md
├── package.json
├── public
├── index.html
├── index.js
└── tsconfig.json
├── rollup.config.js
├── src
├── command
│ ├── bash.ts
│ ├── cat.ts
│ ├── curl.ts
│ ├── echo.ts
│ ├── env.ts
│ ├── forkbomb.ts
│ ├── help.ts
│ ├── ls.ts
│ ├── rm.ts
│ ├── sleep.ts
│ ├── tee.ts
│ └── tsconfig.json
├── kernelspace
│ ├── fs
│ │ ├── empty.ts
│ │ ├── http.ts
│ │ ├── index.ts
│ │ ├── memfs.ts
│ │ ├── native.d.ts
│ │ ├── native.ts
│ │ ├── null.ts
│ │ └── overlay.ts
│ └── index.ts
├── rpc.ts
├── services.ts
├── userspace
│ ├── index.d.ts
│ └── index.ts
└── utils.ts
├── tsconfig.json
└── yarn.lock
/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 |
3 | [*]
4 | charset = utf-8
5 | end_of_line = lf
6 | indent_size = 2
7 | indent_style = space
8 | insert_final_newline = false
9 | max_line_length = 120
10 | tab_width = 2
11 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules/
2 | /dist/
3 | /public/command/
4 | /public/lib/
5 | .vercel
6 | *~
7 |
--------------------------------------------------------------------------------
/.idea/.gitignore:
--------------------------------------------------------------------------------
1 | # Default ignored files
2 | /shelf/
3 | /workspace.xml
4 | # Editor-based HTTP Client requests
5 | /httpRequests/
6 |
--------------------------------------------------------------------------------
/.idea/jsLibraryMappings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/.idea/modules.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/.idea/vcs.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/.idea/webcontainer-runtime.iml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2021 Minigugus
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # [WIP] WebContainer Shell
2 |
3 | > Your viewing the v2 branch, a complete rewrite more future-proof
4 |
5 | > WebContainer-inspired shell, in the browser
6 |
7 | *This is a proof-of-concept and work in progress - code is messy and serve demonstration purpose only*
8 |
9 | Checkout the demo at https://bash-js.vercel.app
10 |
11 | This projects aims to contribute to the discussion about [WebContainer specification](https://github.com/stackblitz/webcontainer-core).
12 |
13 | This is a bash-like shell but that runs in the browser. It comes with a lightweight kernel implementation (supports process and filesystem management):
14 | * Every process runs in its own dedicated worker
15 | * [Extensible filesystem](public/index.js#L56-L62)
16 | * Performant (heavily rely on postMessage and Transferable objects - reduce minimize the amount of copy)
17 | * Supports commands pipes (eg. `echo Hello world! | tee README`)
18 |
19 | Interesting files:
20 | * [`public/index.js`](public/index.js): Example of how to use the proposed Webcontainer API
21 | * [`src/kernelspace/fs/`](src/kernelspace/fs/): Supported filesystems implementation
22 | * [`src/command/`](src/command/): Commands implementations
23 |
24 | ## TODO
25 |
26 | * [ ] Serve filesystem via Service Worker
27 | * [ ] Let the app works offline with a Service Worker
28 | * [X] Move shell features into a dedicated a process (enable nested shells)
29 | * [ ] Add signals support (for SIGINT and SIGKILL)
30 | * [ ] Add jobs support (enables detached commands)
31 | * [ ] Add network support (TCP, UNIX socket, UDP)
32 | * [ ] Add multi-tabs support (one container per tab)
33 | * [ ] Add a [WASI](https://wasi.dev) runtime (a `wasi [wasm-file]` command for instance)
34 | * [ ] Add integration with [WAPM](https://wapm.io/interface/wasi)
35 | * [ ] Add `ps` and `kill` commands
36 | * [ ] Add docs about APIs and kernel design
37 | * [ ] Add a `deno` command (shim the Rust part with a wrapper around this lib's API)
38 | * [ ] `iframe`-based process ? (enable `electron`-like apps)
39 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "version": "0.1.0",
3 | "private": true,
4 | "main": "dist/index.js",
5 | "author": "Minigugus <43109623+Minigugus@users.noreply.github.com>",
6 | "scripts": {
7 | "build": "rollup -c"
8 | },
9 | "name": "webcontainer-runtime",
10 | "description": "WebContainer-like runtime for OS-like features in the browser",
11 | "license": "MIT",
12 | "devDependencies": {
13 | "rollup": "^2.56.3",
14 | "rollup-plugin-typescript2": "^0.30.0",
15 | "typescript": "^4.4.2"
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | WebContainer Shell
9 |
19 |
20 |
21 |
22 | Loading...
23 |
24 |
25 | If you get stuck on Loading..., it probably means your browser doesn't supports required features to show this
26 | site.
27 | Your browser must supports top-level-await and transferable streams for this site to work. You should retry
28 | with a
29 | recent version of Chrome/Chromium.
30 |
31 |
32 |
33 |
34 |
38 |
39 |
40 |
41 |
42 |
--------------------------------------------------------------------------------
/public/index.js:
--------------------------------------------------------------------------------
1 | // @ts-ignore
2 | let term = new Terminal();
3 | // @ts-ignore
4 | const fitAddon = new FitAddon.FitAddon();
5 | term.loadAddon(fitAddon);
6 | term.open(document.body);
7 | term.focus();
8 |
9 | /** @type {number} */
10 | let timeout;
11 | document.body.style.height = `${window.innerHeight}px`;
12 | fitAddon.fit();
13 | addEventListener('resize', () => {
14 | clearTimeout(timeout);
15 | timeout = setTimeout(() => {
16 | document.body.style.height = `${window.innerHeight}px`;
17 | fitAddon.fit();
18 | }, 200);
19 | });
20 |
21 | document.body.addEventListener('dragover', (e) => e.preventDefault()); // TODO complete drag-and-drop (import user directory/file)
22 |
23 | import { create, fs } from "./lib/kernel.js";
24 |
25 | export const webcontainer = await create(new URL('./lib/process.js', import.meta.url).href);
26 | export const filesystem = new fs.OverlayFS();
27 |
28 | Object.assign(window, { webcontainer, filesystem });
29 |
30 | let root;
31 | try {
32 | // @ts-ignore
33 | const rootEntry = await navigator.storage.getDirectory();
34 | root = new fs.NativeFS(rootEntry);
35 | } catch (err) {
36 | console.warn('No private native file system access', err);
37 | }
38 | if (!root)
39 | root = new fs.MemFS();
40 | try {
41 | if (!await root.access(['etc', 'motd']))
42 | await new Response(`Welcome to bash.js!
43 | This is an exemple of what could be possible with the upcoming WebContainer specification.
44 |
45 | Checkout \x1B[4mhttps://github.com/Minigugus/webcontainer-shell/tree/v2\x1B[0m
46 |
47 | Type \x1B[1;3mhelp\x1B[0m to get started\n`)
48 | .body
49 | .pipeTo(await root.writeFile(['etc', 'motd'], 0, true));
50 | } catch (err) {
51 | console.warn('could not create default /etc/motd', err);
52 | }
53 |
54 | filesystem
55 | .mount([], root)
56 | .mount(['root'], filesystem)
57 | .mount(['mnt'], new fs.EmptyFS())
58 | .mount(['dev'], new fs.NullFS()) // TODO dedicated driver
59 | .mount(['sys'], new fs.NullFS()) // TODO dedicated driver
60 | .mount(['proc'], new fs.NullFS()) // TODO dedicated driver
61 | .mount(['bin'], new fs.HTTPFS(new URL('./command/', import.meta.url).href))
62 | .mount(['tmp'], new fs.MemFS());
63 |
64 | document.body.addEventListener('drop', async (e) => {
65 | e.preventDefault();
66 | const dirs = [];
67 | // @ts-ignore
68 | for (const item of e.dataTransfer.items) {
69 | if (item.kind === 'file') {
70 | const entry = await item.getAsFileSystemHandle();
71 | if (entry.kind === 'directory') {
72 | try {
73 | filesystem.mount(['mnt', entry.name], new fs.NativeFS(entry));
74 | dirs.push(` - /mnt/${entry.name}: OK`);
75 | } catch (err) {
76 | dirs.push(` - /mnt/${entry.name}: ERROR: ${(err instanceof Error && err.message) || err}`);
77 | }
78 | }
79 | }
80 | }
81 | alert(`Imported directories:\n${dirs.join('\n')}`)
82 | });
83 |
84 | try {
85 | const bash = await webcontainer.run({
86 | entrypoint: 'bash', // will be resolved using the PATH environment variable
87 | cwd: '/',
88 | argv: ['bash'],
89 | env: {
90 | 'PATH': '/bin',
91 | 'HOST': location.host,
92 | 'USER': localStorage.getItem('USER') || 'nobody'
93 | }
94 | }, filesystem); // TODO networking
95 |
96 | const decoder = new TextDecoder();
97 | /** @param {ReadableStream} stream */
98 | const pipeToTerm = async stream => {
99 | const reader = stream.getReader();
100 | let result;
101 | while (!(result = await reader.read()).done)
102 | term.write(decoder.decode(result.value).replace(/\r?\n/g, '\r\n'));
103 | };
104 | Promise.all([
105 | pipeToTerm(bash.stdout).catch(err => err),
106 | pipeToTerm(bash.stderr).catch(err => err)
107 | ])
108 | .then(console.warn, console.error)
109 | .then(() => bash.status)
110 | .then(status => {
111 | disposable.dispose();
112 | term.write('\r\nbash.js exited with status \x1B[1;' + (status ? 31 : 32) + 'm' + status + '\x1B[0;3m\r\nPress any key to restart\r\n');
113 | term.onKey(() => window.location.reload());
114 | });
115 |
116 | /** @type {WritableStreamDefaultWriter} */
117 | const bashStdin = bash.stdin.getWriter();
118 |
119 | const encoder = new TextEncoder();
120 | const disposable = term.onKey(async ({ key }) => {
121 | await bashStdin.ready;
122 | await bashStdin.write(encoder.encode(key));
123 | });
124 | } catch (err) {
125 | let msg = err.message || err;
126 | switch (msg) {
127 | case 'ENOTFOUND':
128 | msg = 'command \x1B[3mbash\x1B[0;1;31m not found';
129 | }
130 | term.write(`\x1B[31mFailed to start bash.js: \x1B[1m${msg}\x1B[0m`);
131 | term.write('\r\n\r\n\x1B[3mPress any key to retry\r\n');
132 | term.onKey(() => window.location.reload());
133 | }
--------------------------------------------------------------------------------
/public/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "module": "ESNext",
4 | "target": "ESNext",
5 | "outDir": "dist",
6 | "moduleResolution": "classic",
7 | "allowJs": true,
8 | "checkJs": true,
9 | "strict": false,
10 | "noImplicitAny": false,
11 | "lib": [
12 | "ES2019",
13 | "DOM"
14 | ]
15 | },
16 | "include": [
17 | "*.js",
18 | "lib/*.mjs"
19 | ]
20 | }
21 |
--------------------------------------------------------------------------------
/rollup.config.js:
--------------------------------------------------------------------------------
1 | import { readdirSync, readFileSync, writeFileSync } from 'fs';
2 | import typescript from 'rollup-plugin-typescript2';
3 |
4 | const commands = Object.create(null);
5 |
6 | export default [
7 | {
8 | input: {
9 | 'kernel': 'src/kernelspace/index.ts'
10 | },
11 | output: {
12 | chunkFileNames: '[name].js',
13 | entryFileNames: '[name].js',
14 | format: 'esm',
15 | dir: 'public/lib'
16 | },
17 | plugins: [
18 | typescript()
19 | ]
20 | },
21 | {
22 | input: {
23 | 'process': 'src/userspace/index.ts'
24 | },
25 | output: {
26 | exports: 'named',
27 | format: 'iife',
28 | dir: 'public/lib'
29 | },
30 | plugins: [
31 | typescript()
32 | ]
33 | },
34 | ...readdirSync(__dirname + '/src/command')
35 | .filter(name => name.endsWith('.ts'))
36 | .map(name =>
37 | [`${name.slice(0, -3)}`, `src/command/${name}`]
38 | )
39 | .map(([name, from]) => {
40 | /** @type {string} */
41 | const content = readFileSync(from, 'utf8');
42 | const [, description = '', usage = ''] = /\/\*\*((?:.|\n)+?)(?:@usage\s([^\n]+?)\s+)?\*\//.exec(content) ?? [];
43 | commands[name] = {
44 | usage: usage.split(' ').filter(x => x),
45 | description: description.replace(/^(?:\n|\s\*\s)/gm, '').trim()
46 | };
47 | return ({
48 | input: {
49 | [name]: from
50 | },
51 | output: {
52 | exports: 'named',
53 | name: 'webcontainer',
54 | format: 'iife',
55 | dir: 'public/command'
56 | },
57 | plugins: [
58 | typescript()
59 | ]
60 | });
61 | })
62 | ];
63 |
64 | writeFileSync(__dirname + '/public/command/help.json', JSON.stringify(commands));
65 |
--------------------------------------------------------------------------------
/src/command/bash.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
3 | /**
4 | * The shell that powers this terminal
5 | * Builtin commands:
6 | * - cd: change current directory
7 | * - pwd: print current directory
8 | * - exit: close the session
9 | * - user: prints/change the current user (same as USER=username)
10 | * - hostname: prints/change the current user (same as HOST=hostname)
11 | * - unset: delete an environment variable
12 | */
13 |
14 | if (typeof webcontainer !== 'function')
15 | throw new Error('Missing webcontainer runtime');
16 |
17 | const ENCODER = new TextEncoder();
18 |
19 | const BUILTIN: Record Promise> =
20 | Object.assign(Object.create(null), {
21 | 'cd': async (process: LocalProcess, argv: [string, ...string[]]) => {
22 | switch (argv.length) {
23 | case 1:
24 | argv[1] = '/';
25 | case 2:
26 | process.cwd = argv[1]!;
27 | break;
28 | default:
29 | await process.write(2, ENCODER.encode(`bash: ${argv[0]}: too many arguments\r\n`));
30 | }
31 | },
32 | 'pwd': async (process: LocalProcess) => {
33 | await process.write(1, ENCODER.encode(process.cwd + '\r\n'));
34 | },
35 | 'exit': async (process: LocalProcess, argv: [string, ...string[]]) => {
36 | process.exit(Number(argv[1]!));
37 | },
38 | 'user': async (process: LocalProcess, argv: [string, ...string[]]) => {
39 | if (argv.length > 1)
40 | process.setenv('USER', argv[1] || 'nobody');
41 | else
42 | await process.write(1, ENCODER.encode(process.getenv('USER') + '\r\n'));
43 | },
44 | 'hostname': async (process: LocalProcess, argv: [string, ...string[]]) => {
45 | if (argv.length > 1)
46 | process.setenv('HOST', argv[1] || new URL(location.origin).host);
47 | else
48 | await process.write(1, ENCODER.encode(process.getenv('HOST') + '\r\n'));
49 | },
50 | 'unset': async (process: LocalProcess, argv: [string, ...string[]]) => {
51 | argv.slice(1).forEach(n => process.setenv(n, null));
52 | },
53 | });
54 |
55 | function mapError(err: Error) {
56 | switch (err.message) {
57 | case 'ENOTFOUND':
58 | return 'not found';
59 | }
60 | return err.message;
61 | }
62 |
63 | webcontainer(async process => {
64 | const prompt = () => ENCODER.encode(
65 | `\x1B[1;32m${process.getenv('USER') || 'nobody'
66 | }@${process.getenv('HOST') || new URL(location.origin).host
67 | }\x1B[0m:\x1B[1;34m${process.cwd
68 | }\x1B[0m$ `
69 | );
70 | const pump = async (input: ReadableStream, rid: number) => {
71 | const decoder = new TextDecoder();
72 | const reader = input.getReader();
73 | let read;
74 | while (!(read = await reader.read()).done) {
75 | const decoded = decoder.decode(read.value, { stream: true });
76 | await process.write(rid, ENCODER.encode(decoded.replace(/\r?\n/, '\r\n')));
77 | }
78 | }
79 | const rid = 0;
80 | try {
81 | try {
82 | const rid = await process.openRead('/etc/motd');
83 | let buffer;
84 | while ((buffer = await process.read(rid, )) !== null)
85 | await process.write(1, buffer);
86 | await process.close(rid);
87 | } catch (err) {
88 | const msg = (err instanceof Error && err.message) || String(err);
89 | if (msg !== 'ENOTFOUND')
90 | await process.write(2, ENCODER.encode(`/etc/motd: ${msg}`));
91 | }
92 | await process.write(2, new Uint8Array(prompt()));
93 | let line: number[] = [];
94 | let running: Promise | null = null;
95 | let writer: WritableStreamDefaultWriter | null = null;
96 | let buffer;
97 | while ((buffer = await process.read(rid)) !== null) {
98 | // console.debug('', buffer, line);
99 | let output: number[] = [];
100 | for (let i = 0; i < buffer.byteLength; i++) {
101 | const c = buffer[i]!;
102 | switch (c) {
103 | case 4: // ^D
104 | if (!line.length)
105 | if (writer)
106 | await writer.close();
107 | else {
108 | await process.write(2, new Uint8Array([101, 120, 105, 116, 13, 10]));
109 | return 0;
110 | }
111 | break;
112 | case 12: // ^L
113 | if (!running) {
114 | output.push(27, 99); // '\ec'
115 | output = output.concat([...prompt(), ...line]);
116 | } else {
117 | output.push(94, 76); // '^L'
118 | }
119 | break;
120 | case 10: // \r
121 | break;
122 | case 13: // \n
123 | output.push(13, 10);
124 | if (writer) {
125 | line.push(10);
126 | await writer.ready;
127 | await writer.write(new Uint8Array(line));
128 | line = []
129 | break;
130 | }
131 | // const argv = new TextDecoder()
132 | // .decode(new Uint8Array(line))
133 | // .split(' ')
134 | // .filter(a => a);
135 | let cmd: Cmd | null = null;
136 | try {
137 | cmd = parseCmd(new TextDecoder().decode(new Uint8Array(line)));
138 | console.debug('cmd', cmd);
139 | line = [];
140 | if (!cmd.arg0) {
141 | Object.entries(cmd.env)
142 | .forEach(([k, v]) => process.setenv(k, v))
143 | output = output.concat([...prompt(), ...line]);
144 | break;
145 | }
146 | const env = Object.assign(process.env, cmd.env);
147 | if (cmd.arg0 in BUILTIN) {
148 | const c = cmd;
149 | running = Promise.resolve()
150 | .then(() => BUILTIN[c.arg0!]!(process, [c.arg0!, ...c.args]));
151 | } else {
152 | let c: Cmd | null = cmd, child, promises: Promise[] = [];
153 | do {
154 | let prev = child;
155 | child = await process.spawn(c.arg0!, {
156 | args: c.args,
157 | env
158 | });
159 | promises.push(pump(child.stderr, 2).catch(() => null));
160 | if (writer === null) {
161 | writer = child.stdin.getWriter();
162 | writer.closed.then(() => (writer = null));
163 | } else if (prev) {
164 | promises.push(prev.stdout.pipeTo(child.stdin));
165 | }
166 | } while ((c = c.pipeTo) !== null);
167 | // const child = await process.spawn(cmd.arg0, {
168 | // args: cmd.args,
169 | // env
170 | // });
171 | // writer = child.stdin.getWriter();
172 | // writer.closed.then(() => (writer = null));
173 | promises.push(pump(child.stdout, 1).catch(() => null));
174 | running = Promise.all(promises);
175 | }
176 | running.finally(async () => {
177 | await writer?.close().catch(() => null);
178 | writer = null;
179 | await process.write(2, new Uint8Array([...prompt(), ...line]));
180 | running = null;
181 | });
182 | } catch (err) {
183 | output = output.concat([
184 | ...ENCODER.encode(`bash: ${cmd ? `${cmd.arg0}: ` : ''}${((err instanceof Error && mapError(err)) || err)}\r\n`),
185 | ...prompt(),
186 | ...line
187 | ]);
188 | }
189 | break;
190 | case 3: // ^C
191 | if (!running) {
192 | line = [];
193 | output.push(13, 10);
194 | output = output.concat([...prompt(), ...line]);
195 | } else {
196 | output.push(94, 67); // '^C'
197 | }
198 | break;
199 | case 0x1b: // \e
200 | let d;
201 | while ((d = buffer[++i]) && (d < 97 || d > 122));
202 | break;
203 | case 127: // (backspace)
204 | if (line.length) {
205 | const [removed = 0] = line.splice(line.length - 1, 1);
206 | if (String.fromCharCode(removed).length)
207 | output.push(8, 32, 8); // '\b \b'
208 | }
209 | break;
210 | default:
211 | if (c >= 32) {
212 | line.push(c);
213 | if (String.fromCharCode(c).length)
214 | output.push(c);
215 | }
216 | break;
217 | }
218 | }
219 | await process.write(2, new Uint8Array(output));
220 | }
221 | await writer?.close().catch(() => null);
222 | await running;
223 | } catch (err) {
224 | await process.write(2, ENCODER.encode(`${process.argv[0]}: ${((err instanceof Error && err.message) || err)}\n`));
225 | }
226 | await process.write(2, new Uint8Array([101, 120, 105, 116, 13, 10]));
227 | });
228 |
229 | interface Cmd {
230 | env: Record;
231 | arg0: string | null;
232 | args: string[];
233 | pipeTo: Cmd | null; //number | string |
234 | }
235 |
236 | function parseCmd(input: string): Cmd {
237 | const cmd = {
238 | env: Object.create(null),
239 | arg0: null as string | null,
240 | args: [] as string[],
241 | pipeTo: null as Cmd | null,
242 | };
243 | let match;
244 | while ((input = input.trim()).length > 0) {
245 | if ((match = /^[^\s\|\>\<]+/.exec(input)) !== null) {
246 | const arg = match[0]!;
247 | input = input.slice(arg.length);
248 | let index;
249 | if (cmd.arg0 === null && (index = arg.indexOf('=')) !== -1) {
250 | const key = arg.slice(0, index);
251 | const value = arg.slice(index + 1);
252 | cmd.env[key] = value;
253 | } else if (cmd.arg0 === null) {
254 | cmd.arg0 = arg;
255 | } else {
256 | cmd.args.push(arg);
257 | }
258 | } else {
259 | if (cmd.arg0)
260 | if (input[0] === '|') {
261 | cmd.pipeTo = parseCmd(input.slice(1));
262 | input = '';
263 | if (cmd.pipeTo.arg0)
264 | continue;
265 | }
266 | throw new Error('malformed command line');
267 | }
268 | }
269 | return cmd;
270 | }
271 |
--------------------------------------------------------------------------------
/src/command/cat.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
3 | /**
4 | * Prints one or more file sequentially into stdout
5 | * @usage ...
6 | */
7 |
8 | if (typeof webcontainer !== 'function')
9 | throw new Error('Missing webcontainer runtime');
10 |
11 | webcontainer(async process => {
12 | let pendingWrite = Promise.resolve();
13 | const encoder = new TextEncoder();
14 | const paths = process.argv.slice(1);
15 | if (paths.length === 0)
16 | paths.push('-');
17 | for (const path of paths) {
18 | let rid = null;
19 | try {
20 | rid = path === '-' ? 0 : await process.openRead(path);
21 | let buffer;
22 | while ((buffer = await process.read(rid)) !== null) {
23 | await pendingWrite;
24 | pendingWrite = process.write(1, buffer);
25 | }
26 | await pendingWrite;
27 | } catch (err) {
28 | await pendingWrite.catch(() => null);
29 | await process.write(2, encoder.encode(`${path}: ${((err instanceof Error && err.message) || err)}\n`));
30 | } finally {
31 | pendingWrite = Promise.resolve();
32 | if (rid !== null)
33 | await process.close(rid);
34 | }
35 | }
36 | });
37 |
--------------------------------------------------------------------------------
/src/command/curl.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
3 | /**
4 | * Download a file while printing its content to stdout
5 | * @usage (-u) [url]
6 | */
7 |
8 | if (typeof webcontainer !== 'function')
9 | throw new Error('Missing webcontainer runtime');
10 |
11 | webcontainer(async function curl(process) {
12 | debugger;
13 | const args = process.argv.slice(1);
14 | let upload = args.findIndex(x => x === '-u');
15 | if (args.length < (~upload ? 2 : 1))
16 | return 1;
17 | const options: RequestInit = {};
18 | if (upload !== -1) {
19 | options.method = 'POST';
20 | options.body = process.stdin();
21 | args.splice(upload, 1);
22 | }
23 | try {
24 | const response = await fetch(args[0]!, options);
25 | await response.body?.pipeTo(process.stdout());
26 | } catch (err) {
27 | await process.write(2, new TextEncoder().encode(`curl: cannot fetch: ${(err instanceof Error && err.message) || err}\n`));
28 | }
29 | });
30 |
--------------------------------------------------------------------------------
/src/command/echo.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
3 | /**
4 | * Prints command line arguments concatenated with spaces to stdout
5 | * @usage ...
6 | */
7 |
8 | if (typeof webcontainer !== 'function')
9 | throw new Error('Missing webcontainer runtime');
10 |
11 | webcontainer(async process => {
12 | await process.write(1, new TextEncoder().encode(process.argv.slice(1).join(' ') + '\n'));
13 | });
14 |
--------------------------------------------------------------------------------
/src/command/env.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
3 | /**
4 | * Prints environment variables passed to this process
5 | */
6 |
7 | if (typeof webcontainer !== 'function')
8 | throw new Error('Missing webcontainer runtime');
9 |
10 | webcontainer(async process => {
11 | await process.write(1, new TextEncoder().encode(
12 | Object.entries(process.env)
13 | .map(([k, v]) => `${k}=${v}`)
14 | .join('\n') + '\n')
15 | );
16 | });
17 |
--------------------------------------------------------------------------------
/src/command/forkbomb.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
3 | /**
4 | * Spawn 2^N processes - WARNING! There is no limit over how many processes are spawn (can crash your browser!)
5 | * @usage [N]
6 | */
7 |
8 | if (typeof webcontainer !== 'function')
9 | throw new Error('Missing webcontainer runtime');
10 |
11 | webcontainer(async process => {
12 | const left = Number(process.argv[1]);
13 | if (!isFinite(left))
14 | return process.write(1, new TextEncoder().encode('Expected a positive number as argument, received ' + process.argv[1] + '\n'));
15 | if (left <= 0)
16 | return process.write(1, new TextEncoder().encode(' '.repeat(left) + left + '\n'));
17 | const entrypoint = !process.entrypoint.startsWith('blob:')
18 | ? URL.createObjectURL(await new Response(await process.createReadStream(process.entrypoint)).blob())
19 | : process.entrypoint
20 | try {
21 | const [p1, p2] = await Promise.all([
22 | process.spawn(entrypoint, {
23 | args: [String(left - 1)],
24 | arg0: process.argv[0] + ' L',
25 | }),
26 | process.spawn(entrypoint, {
27 | args: [String(left - 1)],
28 | arg0: process.argv[0] + ' R',
29 | })
30 | ]);
31 | {
32 | const reader = p1.stdout.getReader();
33 | let read;
34 | while (!(read = await reader.read()).done)
35 | await process.write(1, read.value);
36 | }
37 | await process.write(1, new TextEncoder().encode(' '.repeat(left) + left + '\n'));
38 | {
39 | const reader = p2.stdout.getReader();
40 | let read;
41 | while (!(read = await reader.read()).done)
42 | await process.write(1, read.value);
43 | }
44 | } finally {
45 | if (entrypoint !== process.entrypoint)
46 | URL.revokeObjectURL(entrypoint);
47 | }
48 | });
49 |
--------------------------------------------------------------------------------
/src/command/help.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
3 | /**
4 | * Prints this help message
5 | */
6 |
7 | if (typeof webcontainer !== 'function')
8 | throw new Error('Missing webcontainer runtime');
9 |
10 | webcontainer(async function help(process) {
11 | const encoder = new TextEncoder();
12 | const print = (line: string) => process.write(1, encoder.encode(line + '\n'));
13 | const printErr = (line: string) => process.write(2, encoder.encode(line + '\n'));
14 | try {
15 | /** @type {Record} */
16 | const cmds = await new Response(await process.createReadStream('/bin/help.json'), {
17 | headers: {
18 | 'Content-Type': 'application/json'
19 | }
20 | }).json();
21 | for (const [name, { usage, description }] of Object.entries<{ usage: string[], description: string }>(cmds)) {
22 | let desc = description || '(no help available)';
23 | desc = desc.split('\n').map(x => '\t' + x).join('\n');
24 | await print(`${[`\x1B[1m${name}\x1B[0m`, ...usage.map(a => `\x1B[4m${a}\x1B[0m`)].join(' ')}\n${desc}\n`);
25 | }
26 | } catch (err) {
27 | await printErr(`\x1B[1;31mCommands list not available (${(err instanceof Error && err.message) ?? err})\x1B[0m`);
28 | await print('');
29 | }
30 | await print('\x1B[4mShortcuts\x1B[0m');
31 | await print(' \x1B[1mCtrl + D\x1B[0m : Close stdin (stop commands like \x1B[1mcat\x1B[0m and \x1B[1mtee\x1B[0m)');
32 | await print(' \x1B[1mCtrl + L\x1B[0m : Clear the terminal');
33 | await print(' \x1B[1mCtrl + C\x1B[0m : DOES NOTHING (should kill a command but it is not supported yet)');
34 | await print(' \x1B[1m TAB\x1B[0m : DOES NOTHING (no auto-compelte yet)');
35 | await print('');
36 | await print("Commands can be piped together with a | character (eg. \x1B[4mecho Hello world! | tee README\x1B[0m). && and || doesn't work yet.");
37 | await print("Environment variables are also supported. Play with \x1B[4menv\x1B[0m command for more.");
38 | await print('');
39 | await print('You can also drop a directory from your computer here to make it accessible via this terminal.');
40 | });
41 |
--------------------------------------------------------------------------------
/src/command/ls.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
3 | /**
4 | * Prints files and directories in the current directory or in thoses passed as parameter
5 | * @usage ...
6 | */
7 |
8 | if (typeof webcontainer !== 'function')
9 | throw new Error('Missing webcontainer runtime');
10 |
11 | webcontainer(async process => {
12 | const encoder = new TextEncoder();
13 | const print = (line: string) => process.write(1, encoder.encode(line + '\n'));
14 | const printErr = (line: string) => process.write(2, encoder.encode(line + '\n'));
15 | const noColor = !!process.getenv('NOCOLOR');
16 | let paths = process.argv;
17 | if (paths.length === 1)
18 | paths.push(process.cwd);
19 | for (let i = 1; i < paths.length; i++) {
20 | let path = paths[i]!;
21 | if (paths.length !== 2)
22 | await print(`${i - 1 ? '\n' : ''}${path}:`);
23 | try {
24 | for await (let entry of await process.readDir(path))
25 | switch (entry.type) {
26 | case 'directory':
27 | await print(`drwxrwxrwx 1 nobody nobody 0 1970/01/01 00:00 ${noColor
28 | ? entry.name
29 | : `\x1B[1;34m${entry.name}\x1B[0m`}`);
30 | break;
31 | case 'file':
32 | if (entry.name.endsWith('.js'))
33 | await print(`-rwxrwxrwx 1 nobody nobody 0 1970/01/01 00:00 ${noColor
34 | ? entry.name
35 | : `\x1B[1;32m${entry.name}\x1B[0m`}`);
36 | else
37 | await print(`-rw-rw-rw- 1 nobody nobody 0 1970/01/01 00:00 ${entry.name}`);
38 | }
39 | } catch (err) {
40 | await printErr(`${process.argv[0]}: ${path}: ${((err instanceof Error && err.message) || err)}`);
41 | }
42 | }
43 | });
44 |
--------------------------------------------------------------------------------
/src/command/rm.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
3 | /**
4 | * Delete recursively files and directories passed as parameter
5 | * @usage ...
6 | */
7 |
8 | if (typeof webcontainer !== 'function')
9 | throw new Error('Missing webcontainer runtime');
10 |
11 | webcontainer(async process => {
12 | const encoder = new TextEncoder();
13 | const printErr = (line: string) => process.write(2, encoder.encode(line + '\n'));
14 | let paths = process.argv;
15 | if (paths.length === 1)
16 | paths.push(process.cwd);
17 | for (let i = 1; i < paths.length; i++) {
18 | let path = paths[i]!;
19 | try {
20 | await process.unlink(path, true);
21 | } catch (err) {
22 | await printErr(`${process.argv[0]}: ${path}: ${((err instanceof Error && err.message) || err)}`);
23 | }
24 | }
25 | });
26 |
--------------------------------------------------------------------------------
/src/command/sleep.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
3 | /**
4 | * Spawn a process that wait for at least N milliseconds
5 | * @usage [N]
6 | */
7 |
8 | if (typeof webcontainer !== 'function')
9 | throw new Error('Missing webcontainer runtime');
10 |
11 | webcontainer(async process => {
12 | const duration = Number(process.argv[1]);
13 | if (isFinite(duration) && duration > 0)
14 | await new Promise(res => setTimeout(res, duration));
15 | });
16 |
--------------------------------------------------------------------------------
/src/command/tee.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
3 | /**
4 | * Redirects stdin to stdout and to all files passed as parameters
5 | * @usage ...
6 | */
7 |
8 | if (typeof webcontainer !== 'function')
9 | throw new Error('Missing webcontainer runtime');
10 |
11 | webcontainer(async process => {
12 | const encoder = new TextEncoder();
13 | let rids = new Set((await Promise.all([...new Set(process.argv.slice(1))].map(path =>
14 | process
15 | .openWrite(path, 'override', true)
16 | .catch(err =>
17 | process.write(
18 | 2,
19 | encoder.encode(`${process.argv[0]}: ${path}: ${((err instanceof Error && err.message) || err)}\n`)
20 | ).then(() => null)
21 | )
22 | ))).filter((rid: number | null): rid is number => rid !== null));
23 | rids.add(1);
24 | let buffer: Uint8Array | null;
25 | while ((buffer = await process.read(0)) !== null)
26 | await Promise.all(
27 | [...rids].map(r =>
28 | process.write(r, buffer!)
29 | .catch(err => {
30 | rids.delete(r);
31 | process.write(
32 | 2,
33 | encoder.encode(`${process.argv[0]}: ${process.getResourceURI(r)!}: ${((err instanceof Error && err.message) || err)}\n`)
34 | )
35 | })
36 | )
37 | );
38 | });
39 |
--------------------------------------------------------------------------------
/src/command/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../../tsconfig.json",
3 | "compilerOptions": {
4 | "removeComments": true
5 | },
6 | }
7 |
--------------------------------------------------------------------------------
/src/kernelspace/fs/empty.ts:
--------------------------------------------------------------------------------
1 | import type { FileSystemDriver, FileSystemNode } from './index';
2 |
3 | export class EmptyFS implements FileSystemDriver {
4 | async resolveUri(path: string[]): Promise {
5 | throw new Error(path.length ? 'ENOTFOUND' : 'EISDIR');
6 | }
7 | async access(path: string[]): Promise {
8 | return !path.length;
9 | }
10 | async readDir(path: string[]): Promise> {
11 | if (path.length)
12 | throw new Error('ENOTFOUND');
13 | return new ReadableStream({
14 | start(c) {
15 | c.close();
16 | }
17 | });
18 | }
19 | async readFile(path: string[], offset?: number, length?: number): Promise> {
20 | throw new Error(path.length ? 'ENOTFOUND' : 'EISDIR');
21 | }
22 | async writeFile(path: string[], offset: 'before' | 'after' | 'override', create: boolean): Promise> {
23 | throw new Error(path.length ? 'EACCESS' : 'EISDIR');
24 | }
25 | async deleteNode(path: string[], recursive: boolean): Promise {
26 | throw new Error(path.length ? 'ENOTFOUND' : 'EBUSY');
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/src/kernelspace/fs/http.ts:
--------------------------------------------------------------------------------
1 | import type { FileSystemDriver, FileSystemNode } from './index';
2 |
3 | export class HTTPFS implements FileSystemDriver {
4 | #root: string;
5 |
6 | constructor(root: string) {
7 | this.#root = root;
8 | }
9 |
10 | async resolveUri(path: string[]): Promise {
11 | const url = new URL(path.join('/'), this.#root);
12 | url.hash = '';
13 | url.search = '';
14 | return url.href;
15 | }
16 | async access(path: string[]): Promise {
17 | const url = new URL(path.join('/'), this.#root);
18 | url.hash = '';
19 | url.search = '';
20 | const response = await fetch(url.href, { method: 'HEAD', cache: 'force-cache' });
21 | switch (response.status) {
22 | case 200:
23 | case 201:
24 | return true;
25 | case 404:
26 | case 403:
27 | return false;
28 | default:
29 | throw new Error(`EHTTP ${response.status} ${response.statusText}`);
30 | }
31 | }
32 | async readDir(path: string[]): Promise> {
33 | throw new Error('EACCESS');
34 | }
35 | async readFile(path: string[], offset = 0, length?: number): Promise> {
36 | if (path.length === 0)
37 | throw new Error('EISDIR');
38 | const url = new URL(path.join('/'), this.#root);
39 | url.hash = '';
40 | url.search = '';
41 | const response = await fetch(url.href, { cache: 'force-cache' });
42 | switch (response.status) {
43 | case 200:
44 | break;
45 | case 404:
46 | throw new Error('ENOTFOUND');
47 | case 403:
48 | throw new Error('EACCESS');
49 | default:
50 | throw new Error(`EHTTP ${response.status} ${response.statusText}`);
51 | }
52 | return response.body || new ReadableStream({
53 | start(c) {
54 | c.close();
55 | }
56 | });
57 | }
58 | async writeFile(path: string[], offset: 'before' | 'after' | 'override', create: boolean): Promise> {
59 | throw new Error('EACCESS');
60 | }
61 | async deleteNode(path: string[], recursive: boolean): Promise {
62 | throw new Error('EACCESS');
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/src/kernelspace/fs/index.ts:
--------------------------------------------------------------------------------
1 | export type FileSystemNode = FileSystemFile | FileSystemDirectory;
2 | export interface FileSystemFile {
3 | type: 'file',
4 | name: string;
5 | }
6 | export interface FileSystemDirectory {
7 | type: 'directory',
8 | name: string;
9 | }
10 |
11 | export interface FileSystemDriver {
12 | resolveUri(path: string[]): Promise;
13 | access(path: string[]): Promise;
14 | readDir(path: string[]): Promise>;
15 | readFile(path: string[], offset?: number, length?: number): Promise>;
16 | writeFile(path: string[], offset: 'before' | 'after' | 'override', create: boolean): Promise>;
17 | deleteNode(path: string[], recursive: boolean): Promise;
18 | }
19 |
--------------------------------------------------------------------------------
/src/kernelspace/fs/memfs.ts:
--------------------------------------------------------------------------------
1 | import type { FileSystemDriver, FileSystemNode } from './index';
2 |
3 | import { asyncIterator2ReadableStream } from '../../utils';
4 |
5 | type MemNode = MemFile | MemDirectory;
6 |
7 | interface MemFile {
8 | type: 'file';
9 | content: Blob;
10 | url: string | null;
11 | lock: Promise;
12 | }
13 |
14 | interface MemDirectory {
15 | type: 'directory';
16 | content: Record;
17 | }
18 |
19 | function expectedExists(node: MemNode | null): asserts node is MemNode {
20 | if (!node)
21 | throw new Error('ENOTFOUND');
22 | }
23 |
24 | function expectedDirectory(node: MemNode): asserts node is MemDirectory {
25 | if (node.type !== 'directory')
26 | throw new Error('ENOTADIR');
27 | }
28 |
29 | function expectedFile(node: MemNode): asserts node is MemFile {
30 | if (node.type !== 'file')
31 | throw new Error('EISDIR');
32 | }
33 |
34 | export class MemFS implements FileSystemDriver {
35 | #filetree: MemDirectory = { type: 'directory', content: Object.create(null) };
36 |
37 | async resolveUri(path: string[]): Promise {
38 | let resolved: MemNode = this.#filetree;
39 | for (const segment of path) {
40 | expectedDirectory(resolved);
41 | const found: MemNode | null = resolved.content[segment] || null;
42 | expectedExists(found);
43 | resolved = found;
44 | }
45 | expectedFile(resolved);
46 | return resolved.url ??= URL.createObjectURL(resolved.content);
47 | }
48 | async access(path: string[]): Promise {
49 | let resolved: MemNode = this.#filetree;
50 | for (const segment of path) {
51 | expectedDirectory(resolved);
52 | const found: MemNode | null = resolved.content[segment] || null;
53 | if (!found)
54 | return false;
55 | resolved = found;
56 | }
57 | return true;
58 | }
59 | async readDir(path: string[]): Promise> {
60 | let resolved: MemNode = this.#filetree;
61 | for (const segment of path) {
62 | const found: MemNode | null = resolved.content[segment] || null;
63 | expectedExists(found);
64 | expectedDirectory(found);
65 | resolved = found;
66 | }
67 | return asyncIterator2ReadableStream(
68 | Object
69 | .entries(resolved.content)
70 | .map(([name, { type }]) => ({
71 | type,
72 | name
73 | }))[Symbol.iterator]()
74 | )
75 | }
76 | async readFile(path: string[], offset = 0, length?: number): Promise> {
77 | let resolved: MemNode = this.#filetree;
78 | for (const segment of path) {
79 | expectedDirectory(resolved);
80 | const found: MemNode | null = resolved.content[segment] || null;
81 | expectedExists(found);
82 | resolved = found;
83 | }
84 | expectedFile(resolved);
85 | const file = resolved;
86 | const result = resolved.lock
87 | .catch(() => (new Error().stack, null))
88 | .then(() => file.content.slice(offset, length ? offset + length : undefined).stream());
89 | resolved.lock = result;
90 | return result;
91 | }
92 | async writeFile(path: string[], offset: 'before' | 'after' | 'override', create: boolean): Promise> {
93 | let parent = this.#filetree, resolved: MemNode | null = parent;
94 | for (let i = 0; i < path.length; i++) {
95 | const segment = path[i]!;
96 | if (resolved)
97 | expectedDirectory(resolved);
98 | else if (create)
99 | resolved = parent.content[path[i - 1]!] = { type: 'directory', content: Object.create(null) };
100 | else
101 | expectedExists(resolved);
102 | const found: MemNode | null = resolved.content[segment] || null;
103 | parent = resolved;
104 | resolved = found;
105 | }
106 | if (resolved)
107 | expectedFile(resolved);
108 | else if (create)
109 | resolved = parent.content[path[path.length - 1]!] = {
110 | type: 'file',
111 | content: new Blob([]),
112 | url: null,
113 | lock: Promise.resolve()
114 | };
115 | else
116 | expectedExists(resolved);
117 | const strategy = new ByteLengthQueuingStrategy({ highWaterMark: 65535 });
118 | const { readable, writable } = new TransformStream({}, strategy, strategy);
119 | const file = resolved;
120 | const result = Promise.all([
121 | resolved.lock.catch(() => null),
122 | new Response(readable).blob()
123 | ])
124 | .then(([, blob]) => {
125 | let content;
126 | switch (offset) {
127 | case 'override':
128 | content = blob;
129 | break;
130 | case 'before':
131 | content = new Blob([blob, file.content]);
132 | break;
133 | case 'after':
134 | content = new Blob([file.content, blob]);
135 | break;
136 | }
137 | if (file.url)
138 | URL.revokeObjectURL(file.url);
139 | file.url = null;
140 | file.content = content;
141 | new Error().stack;
142 | });
143 | resolved.lock = result;
144 | return Promise.resolve(writable);
145 | }
146 | async deleteNode(path: string[], recursive: boolean): Promise {
147 | let parent = this.#filetree, resolved: MemNode | null = this.#filetree;
148 | for (const segment of path) {
149 | if (!resolved)
150 | resolved = parent.content[segment] = { type: 'directory', content: Object.create(null) };
151 | else
152 | expectedDirectory(resolved);
153 | const found: MemNode | null = resolved.content[segment] || null;
154 | parent = resolved;
155 | resolved = found;
156 | }
157 | if (resolved)
158 | if (parent === resolved)
159 | throw new Error('EBUSY'); // cannot delete root
160 | else
161 | delete parent.content[path[path.length - 1]!];
162 | }
163 | }
164 |
--------------------------------------------------------------------------------
/src/kernelspace/fs/native.d.ts:
--------------------------------------------------------------------------------
1 | declare const enum ChooseFileSystemEntriesType {
2 | 'open-file',
3 | 'save-file',
4 | 'open-directory'
5 | }
6 |
7 | interface ChooseFileSystemEntriesOptionsAccepts {
8 | description?: string;
9 | mimeTypes?: string;
10 | extensions?: string;
11 | }
12 |
13 | interface ChooseFileSystemEntriesOptions {
14 | type?: ChooseFileSystemEntriesType;
15 | multiple?: boolean;
16 | accepts?: ChooseFileSystemEntriesOptionsAccepts[];
17 | excludeAcceptAllOption?: boolean;
18 | }
19 |
20 | interface FileSystemHandlePermissionDescriptor {
21 | mode: 'read' | 'readwrite';
22 | }
23 |
24 | interface FileSystemCreateWriterOptions {
25 | keepExistingData?: boolean;
26 | }
27 |
28 | interface FileSystemGetFileOptions {
29 | create?: boolean;
30 | }
31 |
32 | interface FileSystemGetDirectoryOptions {
33 | create?: boolean;
34 | }
35 |
36 | interface FileSystemRemoveOptions {
37 | recursive?: boolean;
38 | }
39 |
40 | declare const enum SystemDirectoryType {
41 | 'sandbox'
42 | }
43 |
44 | interface GetSystemDirectoryOptions {
45 | type: SystemDirectoryType;
46 | }
47 |
48 | interface FileSystemHandle {
49 | readonly isFile: boolean;
50 | readonly isDirectory: boolean;
51 | readonly name: string;
52 |
53 | queryPermission(descriptor?: FileSystemHandlePermissionDescriptor): Promise;
54 |
55 | requestPermission(descriptor?: FileSystemHandlePermissionDescriptor): Promise;
56 | }
57 |
58 | interface FileSystemHandleConstructor {
59 | new(): FileSystemHandle;
60 | }
61 |
62 | declare const enum WriteCommandType {
63 | write = 0,
64 | seek = 1,
65 | truncate = 2,
66 | }
67 |
68 | type WriteParams = {
69 | type: WriteCommandType.write;
70 | data: BufferSource | Blob | string;
71 | } | {
72 | type: WriteCommandType.seek;
73 | position: number;
74 | } | {
75 | type: WriteCommandType.truncate;
76 | size: number;
77 | };
78 |
79 | type FileSystemWriteChunkType = BufferSource | Blob | string | WriteParams;
80 |
81 | interface FileSystemWritableFileStream extends WritableStream {
82 | write(data: FileSystemWriteChunkType): Promise;
83 |
84 | seek(position: number): Promise;
85 |
86 | truncate(size: number): Promise;
87 |
88 | close(): Promise;
89 | }
90 |
91 | interface FileSystemWritableFileStreamConstructor {
92 | new(): FileSystemWritableFileStream;
93 | }
94 |
95 | interface FileSystemFileHandle extends FileSystemHandle {
96 | readonly kind: 'file';
97 |
98 | getFile(): Promise;
99 |
100 | createWritable(options?: FileSystemCreateWriterOptions): Promise;
101 | }
102 |
103 | interface FileSystemFileHandleConstructor {
104 | new(): FileSystemFileHandle;
105 | }
106 |
107 | interface FileSystemDirectoryHandle extends FileSystemHandle {
108 | readonly kind: 'directory';
109 |
110 | getFileHandle(name: string, options?: FileSystemGetFileOptions): Promise;
111 |
112 | getDirectoryHandle(name: string, options?: FileSystemGetDirectoryOptions): Promise;
113 |
114 | removeEntry(name: string, options?: FileSystemRemoveOptions): Promise;
115 |
116 | keys(): AsyncIterable;
117 |
118 | values(): AsyncIterable;
119 |
120 | entries(): AsyncIterable<[string, FileSystemFileHandle | FileSystemDirectoryHandle]>;
121 | }
122 |
123 | interface FileSystemDirectoryHandleConstructor {
124 | new(): FileSystemDirectoryHandle;
125 |
126 | getSystemDirectory(options: GetSystemDirectoryOptions): Promise;
127 | }
128 |
129 | interface WorkerGlobalScope {
130 | FileSystemHandle: FileSystemHandleConstructor;
131 | FileSystemFileHandle: FileSystemFileHandleConstructor;
132 | FileSystemDirectoryHandle: FileSystemDirectoryHandleConstructor;
133 | FileSystemWritableFileStream: FileSystemWritableFileStreamConstructor;
134 | }
135 |
136 | interface StorageManager {
137 | getDirectory(): Promise;
138 | }
139 |
140 |
--------------------------------------------------------------------------------
/src/kernelspace/fs/native.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
3 | import { asyncIterator2ReadableStream } from '../../utils';
4 | import type { FileSystemDriver, FileSystemNode } from './index';
5 |
6 | const processError = (expectDirectory: boolean, err: any) => {
7 | if (err instanceof Error && typeof (err.name as any) === 'string')
8 | switch (err.name) {
9 | case 'NotFoundError':
10 | return 'ENOTFOUND';
11 | case 'TypeMismatchError':
12 | return expectDirectory
13 | ? 'ENOTADIR'
14 | : 'EISDIR';
15 | case 'InvalidModificationError':
16 | return 'ENOTEMPTY';
17 | case 'SecurityError':
18 | case 'TypeError':
19 | case 'NotAllowedError':
20 | return 'EACCESS';
21 | }
22 | throw err;
23 | }
24 |
25 | export class NativeFS implements FileSystemDriver {
26 | #root: FileSystemDirectoryHandle;
27 |
28 | constructor(root: FileSystemDirectoryHandle) {
29 | this.#root = root;
30 | }
31 |
32 | async resolveUri(path: string[]): Promise {
33 | const file = path.pop();
34 | if (!file)
35 | throw new Error('EISDIR');
36 | let parent = this.#root;
37 | try {
38 | for (const segment of path)
39 | parent = await parent.getDirectoryHandle(segment, { create: false });
40 | } catch (err) {
41 | throw new Error(processError(true, err));
42 | }
43 | try {
44 | const found = await parent.getFileHandle(file, { create: false });
45 | return URL.createObjectURL(await found.getFile());
46 | } catch (err) {
47 | throw new Error(processError(false, err));
48 | }
49 | }
50 | async access(path: string[]): Promise {
51 | const file = path.pop();
52 | if (!file)
53 | throw new Error('EISDIR');
54 | let parent = this.#root;
55 | try {
56 | for (const segment of path)
57 | parent = await parent.getDirectoryHandle(segment, { create: false });
58 | } catch (err) {
59 | const msg = processError(true, err);
60 | if (msg === 'ENOTFOUND')
61 | return false;
62 | throw new Error(msg);
63 | }
64 | try {
65 | const found = await parent.getFileHandle(file, { create: false });
66 | return (await found.queryPermission({ mode: 'read' })) === 'granted';
67 | } catch (err) {
68 | const msg = processError(false, err);
69 | if (msg === 'ENOTFOUND')
70 | return false;
71 | throw new Error(msg);
72 | }
73 | }
74 | async readDir(path: string[]): Promise> {
75 | let parent = this.#root;
76 | try {
77 | for (const segment of path)
78 | parent = await parent.getDirectoryHandle(segment, { create: false });
79 | } catch (err) {
80 | throw new Error(processError(true, err));
81 | }
82 | try {
83 | return asyncIterator2ReadableStream(parent.entries()[Symbol.asyncIterator](), ([name, entry]) => ({
84 | type: entry.kind,
85 | name
86 | }));
87 | } catch (err) {
88 | throw new Error(processError(false, err));
89 | }
90 | }
91 | async readFile(path: string[], offset?: number, length?: number): Promise> {
92 | const file = path.pop();
93 | if (!file)
94 | throw new Error('EISDIR');
95 | let parent = this.#root;
96 | try {
97 | for (const segment of path)
98 | parent = await parent.getDirectoryHandle(segment, { create: false });
99 | } catch (err) {
100 | throw new Error(processError(true, err));
101 | }
102 | try {
103 | const found = await parent.getFileHandle(file, { create: false });
104 | return (await found.getFile()).stream();
105 | } catch (err) {
106 | throw new Error(processError(false, err));
107 | }
108 | }
109 | async writeFile(path: string[], offset: 'before' | 'after' | 'override', create: boolean): Promise> {
110 | const file = path.pop();
111 | if (!file)
112 | throw new Error('EISDIR');
113 | let parent = this.#root;
114 | try {
115 | for (const segment of path)
116 | parent = await parent.getDirectoryHandle(segment, { create });
117 | } catch (err) {
118 | throw new Error(processError(true, err));
119 | }
120 | try {
121 | const found = await parent.getFileHandle(file, { create });
122 | const buffer = new TransformStream({});
123 | buffer.readable
124 | .pipeTo(await found.createWritable({ keepExistingData: offset !== 'override' }))
125 | .catch(() => null);
126 | return buffer.writable;
127 | } catch (err) {
128 | throw new Error(processError(false, err));
129 | }
130 | }
131 | async deleteNode(path: string[], recursive: boolean): Promise {
132 | const file = path.pop();
133 | if (!file)
134 | throw new Error('EBUSY');
135 | let parent = this.#root;
136 | try {
137 | for (const segment of path)
138 | parent = await parent.getDirectoryHandle(segment, { create: false });
139 | } catch (err) {
140 | const msg = processError(true, err);
141 | if (msg === 'ENOTFOUND')
142 | return;
143 | throw new Error(msg);
144 | }
145 | try {
146 | await parent.removeEntry(file, { recursive });
147 | } catch (err) {
148 | const msg = processError(false, err);
149 | if (msg === 'ENOTFOUND')
150 | return;
151 | throw new Error(msg);
152 | }
153 | }
154 | }
155 |
--------------------------------------------------------------------------------
/src/kernelspace/fs/null.ts:
--------------------------------------------------------------------------------
1 | import type { FileSystemDriver, FileSystemNode } from './index';
2 |
3 | export class NullFS implements FileSystemDriver {
4 | async resolveUri(path: string[]): Promise {
5 | throw new Error('EACCESS');
6 | }
7 | async access(path: string[]): Promise {
8 | return false;
9 | }
10 | async readDir(path: string[]): Promise> {
11 | throw new Error('EACCESS');
12 | }
13 | async readFile(path: string[], offset?: number, length?: number): Promise> {
14 | throw new Error('EACCESS');
15 | }
16 | async writeFile(path: string[], offset: 'before' | 'after' | 'override', create: boolean): Promise> {
17 | throw new Error('EACCESS');
18 | }
19 | async deleteNode(path: string[], recursive: boolean): Promise {
20 | throw new Error('EACCESS');
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/src/kernelspace/fs/overlay.ts:
--------------------------------------------------------------------------------
1 | import type { FileSystemDriver, FileSystemNode } from './index';
2 |
3 | interface OverlayNode {
4 | driver: FileSystemDriver | null;
5 | children: Record;
6 | }
7 |
8 | function prependToStream(items: T[], stream: ReadableStream) {
9 | return stream.pipeThrough(new TransformStream({
10 | start(c) {
11 | items.forEach(i => c.enqueue(i));
12 | }
13 | }));
14 | }
15 |
16 | export class OverlayFS implements FileSystemDriver {
17 | #mount: OverlayNode = { driver: null, children: Object.create(null) };
18 |
19 | mount(path: string[], fs: FileSystemDriver) {
20 | let node = this.#mount;
21 | for (const segment of path)
22 | node = node.children[segment] ??= { driver: null, children: Object.create(null) };
23 | if (node.driver !== null)
24 | throw new Error('A mount point is already exists at /' + path.join('/'));
25 | node.driver = fs;
26 | return this;
27 | }
28 |
29 | resolve(path: string[]): { driver: FileSystemDriver, node: OverlayNode | null } {
30 | let node: OverlayNode | null = this.#mount, found = this.#mount.driver && this.#mount, founddepth = 0, depth = 0;
31 | for (const segment of path) {
32 | node = node!.children[segment] ?? null;
33 | if (!node) {
34 | node = null;
35 | break;
36 | }
37 | depth++;
38 | if (node.driver) {
39 | founddepth = depth;
40 | found = node;
41 | }
42 | }
43 | if (found === null)
44 | throw new Error('ENOTFOUND');
45 | path.splice(0, founddepth);
46 | return found && { driver: found.driver!, node };
47 | }
48 |
49 | resolveUri(path: string[]): Promise {
50 | return this.resolve(path).driver!.resolveUri(path);
51 | }
52 | access(path: string[]): Promise {
53 | return this.resolve(path).driver!.access(path);
54 | }
55 | async readDir(path: string[]): Promise> {
56 | const fs = this.resolve(path);
57 | const stream = await fs.driver!.readDir(path);
58 | if (!fs.node)
59 | return stream;
60 | const mounts = Object.keys(fs.node.children).map(name => ({ type: 'directory', name }))
61 | return prependToStream(mounts, stream);
62 | }
63 | readFile(path: string[], offset?: number, length?: number): Promise> {
64 | return this.resolve(path).driver!.readFile(path, offset, length);
65 | }
66 | writeFile(path: string[], offset: 'before' | 'after' | 'override', create: boolean): Promise> {
67 | return this.resolve(path).driver!.writeFile(path, offset, create);
68 | }
69 | deleteNode(path: string[], recursive: boolean): Promise {
70 | return this.resolve(path).driver!.deleteNode(path, recursive);
71 | }
72 | }
73 |
--------------------------------------------------------------------------------
/src/kernelspace/index.ts:
--------------------------------------------------------------------------------
1 | import type { LocalProcessController } from '../userspace';
2 | import { FileSystemDriver, FileSystemNode } from './fs';
3 |
4 | import { KERNEL_PROCESS_ENDPOINT, LOCAL_PROCESS_CONTROLLER_ENDPOINT } from '../services';
5 | import { deferrable } from '../utils';
6 |
7 | import { OverlayFS } from './fs/overlay';
8 | import { NativeFS } from './fs/native';
9 | import { MemFS } from './fs/memfs';
10 | import { HTTPFS } from './fs/http';
11 | import { EmptyFS } from './fs/empty';
12 | import { NullFS } from './fs/null';
13 | import { CustomTransferable, TO_TRANSFORABLES } from '../rpc';
14 |
15 | export const fs = {
16 | OverlayFS,
17 | NativeFS,
18 | MemFS,
19 | HTTPFS,
20 | EmptyFS,
21 | NullFS,
22 | }
23 |
24 | export async function create(runtimeUrl: string = './process.js'): Promise {
25 | const runtime = await (await fetch(runtimeUrl)).blob();
26 | return new Webcontainer(URL.createObjectURL(runtime));
27 | }
28 |
29 | class Webcontainer {
30 | #runtimeUrl: string;
31 |
32 | constructor(runtimeUrl: string) {
33 | this.#runtimeUrl = runtimeUrl;
34 | }
35 |
36 | get runtime() {
37 | return this.#runtimeUrl;
38 | }
39 |
40 | async run(info: ProcessSpawn, fs: FileSystemDriver = new NullFS()): Promise {
41 | return new Kernel(this.#runtimeUrl, fs).spawn(0, info);
42 | }
43 | }
44 |
45 | class Kernel {
46 | #nextPid = 1;
47 | #processes = new Map();
48 | #runtimeUrl: string;
49 | #fs: FileSystemDriver;
50 |
51 | constructor(
52 | runtimeUrl: string,
53 | fs: FileSystemDriver
54 | ) {
55 | this.#runtimeUrl = runtimeUrl;
56 | this.#fs = fs;
57 | }
58 |
59 | get runtime() {
60 | return this.#runtimeUrl;
61 | }
62 |
63 | get fs() {
64 | return this.#fs;
65 | }
66 |
67 | async spawn(parent: number, info: ProcessSpawn): Promise {
68 | if (!info.entrypoint.includes('/')) {
69 | for (const path of (info.env['PATH'] ?? '').split(':').reverse()) {
70 | let resolved = new URL(path, 'file:///' + info.cwd).pathname;
71 | if (!resolved.endsWith('/'))
72 | resolved += '/';
73 | resolved += info.entrypoint;
74 | if (!info.entrypoint.includes('.'))
75 | resolved += '.js';
76 | if (await this.fs.access(resolved.split('/').filter(s => s)).catch(() => false)) {
77 | info.entrypoint = resolved;
78 | break;
79 | }
80 | }
81 | } else if (/^\.?\.?\//.test(info.entrypoint))
82 | info.entrypoint = new URL(info.entrypoint, 'file://' + info.cwd + '/').pathname;
83 | const process = await KernelProcess.spawn(this, {
84 | ...info,
85 | entrypoint: info.entrypoint,
86 | ppid: parent,
87 | pid: this.#nextPid++
88 | });
89 | this.#processes.set(process.pid, process);
90 | process.status.then(() => this.#processes.delete(process.pid));
91 | return process;
92 | }
93 | }
94 |
95 | export interface ProcessSpawnInfo {
96 | entrypoint: string;
97 | ppid: number;
98 | pid: number;
99 | cwd: string;
100 | argv: [string, ...string[]];
101 | env: Record;
102 | }
103 |
104 | export type ProcessSpawn = Pick;
105 |
106 | type PublicKernelProcess = Pick<
107 | KernelProcess,
108 | keyof KernelProcess extends infer R
109 | ? R extends keyof KernelProcess
110 | ? KernelProcess[R] extends (...args: any) => Promise
111 | ? R
112 | : never
113 | : never
114 | : never
115 | >;
116 |
117 | export type { PublicKernelProcess as KernelProcess };
118 |
119 | class KernelProcess implements FileSystemDriver {
120 | public static async spawn(kernel: Kernel, info: ProcessSpawnInfo): Promise {
121 | const worker = new Worker(kernel.runtime, {
122 | credentials: 'omit',
123 | name: `[PID ${info.pid}] ${info.argv[0]}`,
124 | type: 'classic'
125 | });
126 | try {
127 | const ioBuffer = new ByteLengthQueuingStrategy({ highWaterMark: 65535 });
128 | const stdin = new TransformStream({}, ioBuffer, ioBuffer);
129 | const stdout = new TransformStream({}, ioBuffer, ioBuffer);
130 | const stderr = new TransformStream({}, ioBuffer, ioBuffer);
131 | const controller = LOCAL_PROCESS_CONTROLLER_ENDPOINT.attach(worker);
132 | const process = new KernelProcess(
133 | kernel,
134 | worker,
135 | controller,
136 | info,
137 | stdin.writable,
138 | stdout.readable,
139 | stderr.readable
140 | );
141 | const { port1, port2 } = new MessageChannel();
142 | KERNEL_PROCESS_ENDPOINT.expose(port2, process);
143 | port2.start();
144 | await controller.spawn(
145 | port1,
146 | info,
147 | stdin.readable,
148 | stdout.writable,
149 | stderr.writable
150 | );
151 | return process;
152 | } catch (err) {
153 | worker.terminate();
154 | throw err;
155 | }
156 | }
157 |
158 | #kernel: Kernel;
159 | #worker: Worker;
160 | #controller: LocalProcessController;
161 | #info: ProcessSpawnInfo;
162 |
163 | #stdin: WritableStream;
164 | #stdout: ReadableStream;
165 | #stderr: ReadableStream;
166 |
167 | #exitCode: number | null = null;
168 | #exit = deferrable();
169 |
170 | private constructor(
171 | kernel: Kernel,
172 | worker: Worker,
173 | controller: LocalProcessController,
174 | info: ProcessSpawnInfo,
175 | stdin: WritableStream,
176 | stdout: ReadableStream,
177 | stderr: ReadableStream
178 | ) {
179 | // super();
180 | this.#kernel = kernel;
181 | this.#worker = worker;
182 | this.#controller = controller;
183 | this.#info = info;
184 | this.#stdin = stdin;
185 | this.#stdout = stdout;
186 | this.#stderr = stderr;
187 | }
188 |
189 | access(path: string[]): Promise {
190 | return this.#kernel.fs.access(path);
191 | }
192 |
193 | resolveUri(path: string[]): Promise {
194 | return this.#kernel.fs.resolveUri(path);
195 | }
196 |
197 | readDir(path: string[]): Promise> {
198 | return this.#kernel.fs.readDir(path);
199 | }
200 |
201 | readFile(path: string[], offset?: number, length?: number): Promise> {
202 | return this.#kernel.fs.readFile(path, offset, length);
203 | }
204 |
205 | writeFile(path: string[], offset: 'before' | 'after' | 'override', create: boolean): Promise> {
206 | return this.#kernel.fs.writeFile(path, offset, create);
207 | }
208 |
209 | deleteNode(path: string[], recursive: boolean): Promise {
210 | return this.#kernel.fs.deleteNode(path, recursive);
211 | }
212 |
213 | get kernel() {
214 | return this.#kernel;
215 | }
216 |
217 | get ppid() {
218 | return this.#info.ppid;
219 | }
220 |
221 | get pid() {
222 | return this.#info.pid;
223 | }
224 |
225 | get status() {
226 | return this.#exit.promise;
227 | }
228 |
229 | get stdin() {
230 | return this.#stdin;
231 | }
232 |
233 | get stdout() {
234 | return this.#stdout;
235 | }
236 |
237 | get stderr() {
238 | return this.#stderr;
239 | }
240 |
241 | async spawn(info: ProcessSpawn): Promise<{
242 | pid: number,
243 | stdin: WritableStream,
244 | stdout: ReadableStream,
245 | stderr: ReadableStream,
246 | } & CustomTransferable> {
247 | const process = await this.#kernel.spawn(this.pid, info);
248 | return {
249 | pid: process.pid,
250 | stdin: process.stdin,
251 | stdout: process.stdout,
252 | stderr: process.stderr,
253 |
254 | [TO_TRANSFORABLES]() {
255 | return [this.stdin, this.stdout, this.stderr];
256 | }
257 | }
258 | }
259 |
260 | async exit(code: number): Promise {
261 | this.#exitCode = code;
262 | this.#worker.terminate();
263 | this.#exit.resolve(code);
264 | // this.dispatchEvent(new Event('exit', { cancelable: false }));
265 | return code as never;
266 | }
267 | }
268 |
269 | const decoder = new TextDecoder();
270 |
271 | async function pump(input: ReadableStream, print: (line: Uint8Array) => void) {
272 | let pending: Uint8Array[] = [];
273 | let pendingLength = 0;
274 | const reader = input.getReader();
275 | let read;
276 | while (!(read = await reader.read()).done) {
277 | let buffer = read.value;
278 | let newline = buffer.indexOf(10); // 10 = \n
279 | if (newline !== -1) {
280 | if (newline > 0) {
281 | pending.push(buffer.subarray(0, newline));
282 | pendingLength += buffer.byteLength;
283 | }
284 | let line = new Uint8Array(pendingLength);
285 | let offset = 0;
286 | for (const buffer of pending) {
287 | line.set(buffer, offset);
288 | offset += buffer.byteLength;
289 | }
290 | print(line);
291 | pending = [];
292 | pendingLength = 0;
293 | while ((offset = newline + 1, newline = buffer.indexOf(10, offset + 1)) !== -1)
294 | print(buffer.subarray(offset, newline));
295 | buffer = buffer.subarray(offset);
296 | }
297 | if (buffer.byteLength > 0) {
298 | pending.push(buffer);
299 | pendingLength += buffer.byteLength;
300 | }
301 | }
302 | if (pendingLength > 0) {
303 | let line = new Uint8Array(pendingLength);
304 | let offset = 0;
305 | for (const buffer of pending) {
306 | line.set(buffer, offset);
307 | offset += buffer.byteLength;
308 | }
309 | print(line);
310 | }
311 | }
312 |
313 | export async function pumpToConsole({ pid, stdout, stderr, status }: KernelProcess) {
314 | const print = (line: Uint8Array) => console.info('[PID %s] %s', pid, decoder.decode(line));
315 | const printErr = (line: Uint8Array) => console.error('[PID %s] %s', pid, decoder.decode(line));
316 | await Promise.all([
317 | pump(stdout, print).catch(() => null),
318 | pump(stderr, printErr).catch(() => null),
319 | ]);
320 | return status;
321 | }
322 |
--------------------------------------------------------------------------------
/src/rpc.ts:
--------------------------------------------------------------------------------
1 | import { Deferrable, deferrable } from './utils';
2 |
3 | export interface IService {
4 | call(req: unknown): Promise;
5 | }
6 |
7 | export type ServiceRequest =
8 | T extends { call(req: infer R): any }
9 | ? R
10 | : unknown;
11 |
12 | export type ServiceResponse =
13 | T extends { call(req: any): Promise }
14 | ? R
15 | : unknown;
16 |
17 | export interface IIdentityService {
18 | call(req: ServiceRequest): Promise>;
19 | }
20 |
21 | export type ServiceInterface> = {
22 | [fn in keyof T]: T[fn] extends (...args: any) => Promise ? (...args: any) => Promise : never;
23 | }
24 |
25 | type Interface2ServiceRequest> = {
26 | [name in keyof T]: { name: name, args: Parameters }
27 | }[keyof T];
28 |
29 | type Interface2ServiceResponse> = {
30 | [name in keyof T]: T[name] extends (...args: any) => Promise ? R : never;
31 | };
32 |
33 | export interface IServicified> extends IService {
34 | call>(req: U): Promise[U['name']]>;
35 | }
36 |
37 | class Servicified> implements IServicified {
38 | #interface: T;
39 |
40 | constructor(iface: T) {
41 | this.#interface = iface;
42 | }
43 |
44 | async call>(req: U) {
45 | return this.#interface[req.name].apply(this.#interface, req.args) as Interface2ServiceResponse[U['name']]
46 | }
47 | }
48 |
49 | export function servicify>(iface: T): IServicified {
50 | return new Servicified(iface);
51 | }
52 |
53 | export function interfacify>(service: T) {
54 | return new Proxy ? R : never>(Object.create(null), {
55 | get(target, p, receiver) {
56 | if (typeof p !== 'string')
57 | return Reflect.get(target, p, receiver);
58 | return (...args: any) => service.call({
59 | name: p,
60 | args
61 | });
62 | }
63 | });
64 | }
65 |
66 | type SerizalizedRequest = { id: number, req: any };
67 |
68 | type SerizalizedResponse =
69 | | { id: number, err: any }
70 | | { id: number, ok: any };
71 |
72 | class RemoteService implements IRemote {
73 | #channel: Channel;
74 | #encoding: ServiceEncoding;
75 | #pending = new Map>>();
76 | #count = 0;
77 |
78 | constructor(
79 | channel: Channel,
80 | encoding: ServiceEncoding
81 | ) {
82 | this.#encoding = encoding;
83 | this.#channel = channel;
84 | this.#channel.addEventListener('message', e => {
85 | e.preventDefault();
86 | this.complete(e.data);
87 | }, { capture: true, once: false, passive: false });
88 | }
89 |
90 | private complete(response: SerizalizedResponse) {
91 | if (typeof response !== 'object' || !response || typeof response.id !== 'number') {
92 | console.warn('[RemoteService] Invalid serialized response:', response);
93 | return;
94 | }
95 | const { id } = response;
96 | const pending = this.#pending.get(id);
97 | if (!pending)
98 | console.warn('[RemoteService] No pending request id: %s', id);
99 | else if ('err' in response)
100 | pending.reject(response.err);
101 | else if ('ok' in response) {
102 | Promise.resolve(this.#encoding.decodeResponse(response.ok))
103 | .then(
104 | res => pending.resolve(res),
105 | err => pending.reject(err)
106 | )
107 | } else
108 | console.warn('[RemoteService] Malformed serialized response:', response);
109 | }
110 |
111 | async call(op: ServiceRequest): Promise> {
112 | const prepared = this.#encoding.encodeRequest(op);
113 | const defer = deferrable>();
114 | const id = this.#count++;
115 | this.#pending.set(id, defer);
116 | this.#channel.postMessage({ id, req: prepared[0] } as SerizalizedRequest, prepared[1]);
117 | return defer.promise;
118 | }
119 | }
120 |
121 | class ExposedService {
122 | #channel: Channel;
123 | #service: T;
124 | #encoding: ServiceEncoding;
125 |
126 | constructor(
127 | channel: Channel,
128 | service: T,
129 | encoding: ServiceEncoding
130 | ) {
131 | this.#encoding = encoding;
132 | this.#channel = channel;
133 | this.#service = service;
134 | this.#channel.addEventListener('message', e => {
135 | e.preventDefault();
136 | this.process(e.data);
137 | }, { capture: true, once: false, passive: false });
138 | }
139 |
140 | private process(request: SerizalizedRequest) {
141 | if (typeof request !== 'object' || !request || typeof request.id !== 'number' || !('req' in request)) {
142 | console.warn('[LocalService] Invalid serialized request:', request);
143 | return;
144 | }
145 | const { id } = request;
146 | Promise.resolve(this.#encoding.decodeRequest(request.req))
147 | .then(req => this.#service.call(req))
148 | .then(res => this.#encoding.encodeResponse(res as ServiceResponse))
149 | .then(
150 | res => this.#channel.postMessage({ id, ok: res[0] } as SerizalizedResponse, res[1]),
151 | err => this.#channel.postMessage({ id, err: err } as SerizalizedResponse, [])
152 | );
153 | }
154 | }
155 |
156 | export interface Channel {
157 | addEventListener(type: 'message', handler: (e: MessageEvent) => void, options: AddEventListenerOptions): void;
158 | postMessage(message: any, transfer: Transferable[]): void;
159 | }
160 |
161 | export interface ServiceEncoding {
162 | encodeRequest(request: ServiceRequest): [any, Transferable[]];
163 | encodeResponse(response: ServiceResponse): [any, Transferable[]];
164 | decodeRequest(request: any): ServiceRequest;
165 | decodeResponse(response: any): ServiceResponse;
166 | }
167 |
168 | export interface IRemote extends IIdentityService {
169 | }
170 |
171 | export function attach(
172 | channel: Channel,
173 | encoding: ServiceEncoding
174 | ): IRemote {
175 | return new RemoteService(channel, encoding);
176 | }
177 |
178 | export function expose(
179 | channel: Channel,
180 | encoding: ServiceEncoding,
181 | service: T
182 | ) {
183 | return new ExposedService(channel, service, encoding);
184 | }
185 |
186 | export class ServiceEndPoint {
187 | #encoding: ServiceEncoding;
188 |
189 | constructor(
190 | encoding: ServiceEncoding
191 | ) {
192 | this.#encoding = encoding;
193 | }
194 |
195 | attach(channel: Channel) {
196 | return attach(channel, this.#encoding);
197 | }
198 |
199 | expose(channel: Channel, service: T) {
200 | return expose(channel, this.#encoding, service);
201 | }
202 | }
203 |
204 | const ab = self.ArrayBuffer;
205 | const rs = self.ReadableStream;
206 | const ws = self.WritableStream;
207 | const ts = self.TransformStream;
208 | const mp = self.MessagePort;
209 |
210 | export interface CustomTransferable {
211 | [TO_TRANSFORABLES](this: this): (Transferable | ReadableStream | WritableStream)[];
212 | }
213 |
214 | export const TO_TRANSFORABLES = Symbol();
215 |
216 | const appendTransferable = (acc: Transferable[], a: any) => {
217 | if (typeof a === 'object' && a && (TO_TRANSFORABLES in a)) {
218 | const transferables = a[TO_TRANSFORABLES]();
219 | delete a[TO_TRANSFORABLES];
220 | return acc.concat(transferables);
221 | }
222 | if (
223 | (ab && a instanceof ab) ||
224 | (mp && a instanceof mp) ||
225 | (rs && a instanceof rs) ||
226 | (ws && a instanceof ws) ||
227 | (ts && a instanceof ts)
228 | )
229 | acc.push(a as Transferable);
230 | return acc;
231 | };
232 |
233 | function servicifiedEncoding>(): ServiceEncoding {
234 | return {
235 | encodeRequest: req => [req, req.args.reduce(appendTransferable, [])],
236 | encodeResponse: res => [res, appendTransferable([], res)],
237 | decodeRequest: req => req,
238 | decodeResponse: res => res,
239 | }
240 | }
241 |
242 | export class InterfaceEndPoint> {
243 | #endpoint = new ServiceEndPoint>(servicifiedEncoding());
244 |
245 | constructor() {
246 | }
247 |
248 | attach(channel: Channel) {
249 | return interfacify(this.#endpoint.attach(channel) as IServicified);
250 | }
251 |
252 | expose(channel: Channel, iface: T) {
253 | return this.#endpoint.expose(channel, servicify(iface));
254 | }
255 | }
256 |
--------------------------------------------------------------------------------
/src/services.ts:
--------------------------------------------------------------------------------
1 | import type { KernelProcess } from './kernelspace';
2 | import type { LocalProcessController } from './userspace';
3 |
4 | import { InterfaceEndPoint } from './rpc';
5 |
6 | export const KERNEL_PROCESS_ENDPOINT = new InterfaceEndPoint();
7 | export const LOCAL_PROCESS_CONTROLLER_ENDPOINT = new InterfaceEndPoint();
8 |
--------------------------------------------------------------------------------
/src/userspace/index.d.ts:
--------------------------------------------------------------------------------
1 | declare type LocalProcess = import('./index').LocalProcess;
2 | declare function webcontainer(callback: import('./index').EntrypointFunction): void;
3 |
--------------------------------------------------------------------------------
/src/userspace/index.ts:
--------------------------------------------------------------------------------
1 | import type { Channel } from '../rpc';
2 | import type { KernelProcess, ProcessSpawnInfo } from '../kernelspace';
3 | import type { FileSystemNode } from '../kernelspace/fs';
4 |
5 | import { KERNEL_PROCESS_ENDPOINT, LOCAL_PROCESS_CONTROLLER_ENDPOINT } from '../services';
6 | import { readableStream2AsyncIterator } from '../utils';
7 |
8 | export type EntrypointFunction = (process: LocalProcess) => void | number | Promise;
9 |
10 | export type { LocalProcess, LocalProcessController };
11 |
12 | ///
13 |
14 | function segments(fullpath: string) {
15 | return fullpath.split('/').filter(s => s);
16 | }
17 |
18 | const RELEASE_RESOURCES = Symbol();
19 |
20 | class LocalProcess {
21 | #kernel: KernelProcess;
22 |
23 | #entrypoint: string;
24 |
25 | #ppid: number;
26 | #pid: number;
27 |
28 | #cwd: string;
29 | #argv: [string, ...string[]];
30 | #env: Record;
31 |
32 | #stdin: ReadableStream;
33 | #stdout: WritableStream;
34 | #stderr: WritableStream;
35 |
36 | #nextResourceId = 3;
37 | #resources: Record,
40 | writer?: WritableStreamDefaultWriter,
41 | }> = Object.create(null);
42 |
43 | constructor(
44 | kernel: KernelProcess,
45 | info: ProcessSpawnInfo,
46 | stdin: ReadableStream,
47 | stdout: WritableStream,
48 | stderr: WritableStream,
49 | ) {
50 | Object.freeze(this);
51 | this.#kernel = kernel;
52 | this.#entrypoint = info.entrypoint;
53 | this.#ppid = info.ppid;
54 | this.#pid = info.pid;
55 | this.#cwd = info.cwd;
56 | this.#argv = info.argv;
57 | this.#env = info.env;
58 | this.#stdin = stdin;
59 | this.#stdout = stdout;
60 | this.#stderr = stderr;
61 | this.#resources[0] = { uri: 'stdin:', reader: stdin.getReader() };
62 | this.#resources[1] = { uri: 'stdout:', writer: stdout.getWriter() };
63 | this.#resources[2] = { uri: 'stderr:', writer: stderr.getWriter() };
64 | }
65 |
66 | get entrypoint(): string {
67 | return this.#entrypoint;
68 | }
69 |
70 | get ppid(): number {
71 | return this.#ppid;
72 | }
73 |
74 | get pid(): number {
75 | return this.#pid;
76 | }
77 |
78 | get cwd(): string {
79 | return this.#cwd;
80 | }
81 |
82 | set cwd(path: string) {
83 | this.#cwd = '/' + segments(this.resolve(path)).join('/');
84 | }
85 |
86 | get argv() {
87 | return this.#argv;
88 | }
89 |
90 | get env() {
91 | return Object.assign(Object.create(null), this.#env);
92 | }
93 |
94 | stdin() {
95 | const resource = this.#resources[0];
96 | if (resource) {
97 | resource.reader?.releaseLock();
98 | delete this.#resources[0];
99 | }
100 | return this.#stdin;
101 | }
102 |
103 | stdout() {
104 | const resource = this.#resources[1];
105 | if (resource) {
106 | resource.writer?.releaseLock();
107 | delete this.#resources[1];
108 | }
109 | return this.#stdout;
110 | }
111 |
112 | stderr() {
113 | const resource = this.#resources[2];
114 | if (resource) {
115 | resource.writer?.releaseLock();
116 | delete this.#resources[2];
117 | }
118 | return this.#stderr;
119 | }
120 |
121 | getenv(name: string): string | null {
122 | return this.#env[name] ?? null;
123 | }
124 |
125 | setenv(name: string, value: string | null): string | null {
126 | if (value !== null)
127 | return this.#env[name] = value;
128 | delete this.#env[name];
129 | return null;
130 | }
131 |
132 | resolve(path: string) {
133 | return new URL(path, 'file://' + this.#cwd + '/').pathname;
134 | }
135 |
136 | async createReadStream(path: string): Promise> {
137 | const parts = segments(path = this.resolve(path));
138 | return this.#kernel.readFile(parts);
139 | }
140 |
141 | async createWriteStream(path: string, seek: 'after' | 'before' | 'override', createOrReplace: boolean): Promise> {
142 | const parts = segments(path = this.resolve(path));
143 | return this.#kernel.writeFile(parts, seek, createOrReplace);
144 | }
145 |
146 | async unlink(path: string, recursive = false) {
147 | const parts = segments(path = this.resolve(path));
148 | await this.#kernel.deleteNode(parts, recursive);
149 | }
150 |
151 | getResourceURI(rid: number) {
152 | const { uri } = this.#resources[rid] || {};
153 | if (!uri)
154 | return null;
155 | return uri;
156 | }
157 |
158 | async readDir(path: string): Promise> {
159 | const parts = segments(path = this.resolve(path));
160 | const stream = await this.#kernel.readDir(parts);
161 | return readableStream2AsyncIterator(stream, {
162 | onclose: s => s.cancel(),
163 | onabort: (s, err) => s.cancel(err),
164 | });
165 | }
166 |
167 | async openRead(path: string): Promise {
168 | const parts = segments(path = this.resolve(path));
169 | const readable = await this.#kernel.readFile(parts);
170 | const rid = this.#nextResourceId++;
171 | this.#resources[rid] = { uri: `file://${path}`, reader: readable.getReader() };
172 | return rid;
173 | }
174 |
175 | async openWrite(path: string, seek: 'after' | 'before' | 'override', createOrReplace: boolean): Promise {
176 | const parts = segments(path = this.resolve(path));
177 | const writable = await this.#kernel.writeFile(parts, seek, createOrReplace);
178 | const rid = this.#nextResourceId++;
179 | this.#resources[rid] = { uri: `file://${path}`, writer: writable.getWriter() };
180 | return rid;
181 | }
182 |
183 | async read(rid: number): Promise {
184 | const { reader } = this.#resources[rid] || {};
185 | if (!reader)
186 | throw new Error("EBADF: Resource cannot be read");
187 | const read = await reader.read();
188 | return read.done ? null : read.value;
189 | }
190 |
191 | async write(rid: number, p: Uint8Array): Promise {
192 | const { writer } = this.#resources[rid] || {};
193 | if (!writer)
194 | throw new Error("EBADF: Resource cannot be written");
195 | await writer.ready;
196 | await writer.write(p);
197 | }
198 |
199 | async close(rid: number): Promise {
200 | const ressource = this.#resources[rid];
201 | if (!ressource)
202 | return; // resource already closed
203 | delete this.#resources[rid];
204 | await Promise.all([
205 | ressource.writer?.close().catch(() => null),
206 | ressource.reader?.cancel().catch(() => null)
207 | ]);
208 | }
209 |
210 | async spawn(entrypoint: string, {
211 | cwd = this.#cwd,
212 | arg0 = entrypoint,
213 | args = [],
214 | env = this.#env
215 | }: {
216 | cwd?: string,
217 | arg0?: string,
218 | args?: string[],
219 | env?: Record,
220 | }) {
221 | return this.#kernel.spawn({
222 | entrypoint,
223 | cwd,
224 | argv: [arg0, ...args],
225 | env
226 | })
227 | }
228 |
229 | async exit(code: number | undefined) {
230 | code = Number(code);
231 | if (!isFinite(code))
232 | code = 0;
233 | await this[RELEASE_RESOURCES]();
234 | return this.#kernel.exit(code);
235 | }
236 |
237 | [RELEASE_RESOURCES]() {
238 | return Promise.all(Object
239 | .keys(this.#resources)
240 | .map(rid => this.close(Number(rid)))
241 | );
242 | }
243 | }
244 | Object.freeze(LocalProcess.prototype);
245 |
246 | class LocalProcessController {
247 | constructor(
248 | channel: Channel
249 | ) {
250 | Object.freeze(this);
251 | LOCAL_PROCESS_CONTROLLER_ENDPOINT.expose(channel, this);
252 | }
253 |
254 | async spawn(
255 | channel: MessagePort,
256 | info: ProcessSpawnInfo,
257 | stdin: ReadableStream,
258 | stdout: WritableStream,
259 | stderr: WritableStream,
260 | ): Promise {
261 | const kernel = KERNEL_PROCESS_ENDPOINT.attach(channel);
262 | channel.start();
263 | const resolved = new URL(info.entrypoint, 'file://' + info.cwd + '/');
264 | const entrypointUrl = resolved.protocol === 'file:'
265 | ? await kernel.resolveUri(segments(resolved.pathname))
266 | : resolved.href;
267 | let initFunction: null | EntrypointFunction = null as any;
268 | self.webcontainer = (callback: EntrypointFunction) => (initFunction = callback);
269 | importScripts(entrypointUrl);
270 | // @ts-ignore
271 | delete self.webcontainer;
272 | if (typeof initFunction !== 'function')
273 | throw new Error(`Not an executable`);
274 | const init = initFunction;
275 | const process = new LocalProcess(
276 | kernel,
277 | info,
278 | stdin,
279 | stdout,
280 | stderr
281 | );
282 | Promise.resolve()
283 | .then(() => init(process))
284 | .finally(() => process[RELEASE_RESOURCES]())
285 | .then(code => process.exit(typeof code === 'number' && isFinite(code) ? code : 0))
286 | .catch(err => {
287 | console.error('[PID %s] %s: unhandled exception:', info.pid, info.entrypoint, err);
288 | return kernel.exit(127); // TODO: SIGSEGF
289 | });
290 | }
291 | }
292 | Object.freeze(LocalProcessController.prototype);
293 |
294 | new LocalProcessController(self);
295 |
--------------------------------------------------------------------------------
/src/utils.ts:
--------------------------------------------------------------------------------
1 | export interface Deferrable {
2 | promise: Promise;
3 | reject: (reason?: any) => void;
4 | resolve: (value: T | PromiseLike) => void;
5 | }
6 |
7 | export function deferrable(): Deferrable {
8 | let reject: (reason?: any) => void, resolve: (value: T | PromiseLike) => void;
9 | const promise = new Promise((res, rej) => {
10 | reject = rej;
11 | resolve = res;
12 | });
13 | return {
14 | promise,
15 | reject: reject!,
16 | resolve: resolve!
17 | }
18 | }
19 |
20 | export function asyncIterator2ReadableStream(iterator: Iterator | AsyncIterator): ReadableStream;
21 | export function asyncIterator2ReadableStream(iterator: Iterator | AsyncIterator, map: (i: T) => U): ReadableStream;
22 | export function asyncIterator2ReadableStream(iterator: Iterator | AsyncIterator, map = (i: T): U => i as never) {
23 | return new ReadableStream({
24 | async pull(controller) {
25 | try {
26 | while (controller.desiredSize !== null && controller.desiredSize > 0) {
27 | const result = await iterator.next();
28 | if (result.done)
29 | return controller.close();
30 | controller.enqueue(map(result.value));
31 | }
32 | } catch (err) {
33 | controller.error(err);
34 | }
35 | },
36 | cancel(err) {
37 | iterator.return?.(err);
38 | }
39 | });
40 | }
41 |
42 | export async function* readableStream2AsyncIterator(
43 | stream: ReadableStream,
44 | {
45 | onclose = r => r.cancel(),
46 | onabort = (r, err) => r.cancel(err)
47 | }: {
48 | onclose?: (reader: ReadableStreamReader) => Promise,
49 | onabort?: (reader: ReadableStreamReader, err: any) => Promise
50 | } = {}
51 | ) {
52 | const reader = stream.getReader();
53 | try {
54 | let read;
55 | while (!(read = await reader.read()).done)
56 | yield read.value;
57 | await onclose(reader);
58 | } catch (err) {
59 | await onabort(reader, err);
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "module": "ESNext",
4 | "target": "ESNext",
5 | "outDir": "dist",
6 | "moduleResolution": "node",
7 | "sourceMap": true,
8 | "strict": true,
9 | "strictNullChecks": true,
10 | "noUncheckedIndexedAccess": true,
11 | "alwaysStrict": true,
12 | "declaration": false,
13 | "declarationMap": false,
14 | "esModuleInterop": true,
15 | "lib": [
16 | "ES2019",
17 | "WebWorker"
18 | ]
19 | },
20 | "exclude": [
21 | "node_modules"
22 | ],
23 | "include": [
24 | "src"
25 | ]
26 | }
--------------------------------------------------------------------------------
/yarn.lock:
--------------------------------------------------------------------------------
1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
2 | # yarn lockfile v1
3 |
4 |
5 | "@rollup/pluginutils@^4.1.0":
6 | version "4.1.1"
7 | resolved "https://registry.yarnpkg.com/@rollup/pluginutils/-/pluginutils-4.1.1.tgz#1d4da86dd4eded15656a57d933fda2b9a08d47ec"
8 | integrity sha512-clDjivHqWGXi7u+0d2r2sBi4Ie6VLEAzWMIkvJLnDmxoOhBYOTfzGbOQBA32THHm11/LiJbd01tJUpJsbshSWQ==
9 | dependencies:
10 | estree-walker "^2.0.1"
11 | picomatch "^2.2.2"
12 |
13 | commondir@^1.0.1:
14 | version "1.0.1"
15 | resolved "https://registry.yarnpkg.com/commondir/-/commondir-1.0.1.tgz#ddd800da0c66127393cca5950ea968a3aaf1253b"
16 | integrity sha1-3dgA2gxmEnOTzKWVDqloo6rxJTs=
17 |
18 | estree-walker@^2.0.1:
19 | version "2.0.2"
20 | resolved "https://registry.yarnpkg.com/estree-walker/-/estree-walker-2.0.2.tgz#52f010178c2a4c117a7757cfe942adb7d2da4cac"
21 | integrity sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==
22 |
23 | find-cache-dir@^3.3.1:
24 | version "3.3.2"
25 | resolved "https://registry.yarnpkg.com/find-cache-dir/-/find-cache-dir-3.3.2.tgz#b30c5b6eff0730731aea9bbd9dbecbd80256d64b"
26 | integrity sha512-wXZV5emFEjrridIgED11OoUKLxiYjAcqot/NJdAkOhlJ+vGzwhOAfcG5OX1jP+S0PcjEn8bdMJv+g2jwQ3Onig==
27 | dependencies:
28 | commondir "^1.0.1"
29 | make-dir "^3.0.2"
30 | pkg-dir "^4.1.0"
31 |
32 | find-up@^4.0.0:
33 | version "4.1.0"
34 | resolved "https://registry.yarnpkg.com/find-up/-/find-up-4.1.0.tgz#97afe7d6cdc0bc5928584b7c8d7b16e8a9aa5d19"
35 | integrity sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==
36 | dependencies:
37 | locate-path "^5.0.0"
38 | path-exists "^4.0.0"
39 |
40 | fs-extra@8.1.0:
41 | version "8.1.0"
42 | resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-8.1.0.tgz#49d43c45a88cd9677668cb7be1b46efdb8d2e1c0"
43 | integrity sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==
44 | dependencies:
45 | graceful-fs "^4.2.0"
46 | jsonfile "^4.0.0"
47 | universalify "^0.1.0"
48 |
49 | fsevents@~2.3.2:
50 | version "2.3.2"
51 | resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.2.tgz#8a526f78b8fdf4623b709e0b975c52c24c02fd1a"
52 | integrity sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==
53 |
54 | function-bind@^1.1.1:
55 | version "1.1.1"
56 | resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d"
57 | integrity sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==
58 |
59 | graceful-fs@^4.1.6, graceful-fs@^4.2.0:
60 | version "4.2.8"
61 | resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.8.tgz#e412b8d33f5e006593cbd3cee6df9f2cebbe802a"
62 | integrity sha512-qkIilPUYcNhJpd33n0GBXTB1MMPp14TxEsEs0pTrsSVucApsYzW5V+Q8Qxhik6KU3evy+qkAAowTByymK0avdg==
63 |
64 | has@^1.0.3:
65 | version "1.0.3"
66 | resolved "https://registry.yarnpkg.com/has/-/has-1.0.3.tgz#722d7cbfc1f6aa8241f16dd814e011e1f41e8796"
67 | integrity sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==
68 | dependencies:
69 | function-bind "^1.1.1"
70 |
71 | is-core-module@^2.2.0:
72 | version "2.6.0"
73 | resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.6.0.tgz#d7553b2526fe59b92ba3e40c8df757ec8a709e19"
74 | integrity sha512-wShG8vs60jKfPWpF2KZRaAtvt3a20OAn7+IJ6hLPECpSABLcKtFKTTI4ZtH5QcBruBHlq+WsdHWyz0BCZW7svQ==
75 | dependencies:
76 | has "^1.0.3"
77 |
78 | jsonfile@^4.0.0:
79 | version "4.0.0"
80 | resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-4.0.0.tgz#8771aae0799b64076b76640fca058f9c10e33ecb"
81 | integrity sha1-h3Gq4HmbZAdrdmQPygWPnBDjPss=
82 | optionalDependencies:
83 | graceful-fs "^4.1.6"
84 |
85 | locate-path@^5.0.0:
86 | version "5.0.0"
87 | resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-5.0.0.tgz#1afba396afd676a6d42504d0a67a3a7eb9f62aa0"
88 | integrity sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==
89 | dependencies:
90 | p-locate "^4.1.0"
91 |
92 | make-dir@^3.0.2:
93 | version "3.1.0"
94 | resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-3.1.0.tgz#415e967046b3a7f1d185277d84aa58203726a13f"
95 | integrity sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==
96 | dependencies:
97 | semver "^6.0.0"
98 |
99 | p-limit@^2.2.0:
100 | version "2.3.0"
101 | resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-2.3.0.tgz#3dd33c647a214fdfffd835933eb086da0dc21db1"
102 | integrity sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==
103 | dependencies:
104 | p-try "^2.0.0"
105 |
106 | p-locate@^4.1.0:
107 | version "4.1.0"
108 | resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-4.1.0.tgz#a3428bb7088b3a60292f66919278b7c297ad4f07"
109 | integrity sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==
110 | dependencies:
111 | p-limit "^2.2.0"
112 |
113 | p-try@^2.0.0:
114 | version "2.2.0"
115 | resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.2.0.tgz#cb2868540e313d61de58fafbe35ce9004d5540e6"
116 | integrity sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==
117 |
118 | path-exists@^4.0.0:
119 | version "4.0.0"
120 | resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-4.0.0.tgz#513bdbe2d3b95d7762e8c1137efa195c6c61b5b3"
121 | integrity sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==
122 |
123 | path-parse@^1.0.6:
124 | version "1.0.7"
125 | resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735"
126 | integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==
127 |
128 | picomatch@^2.2.2:
129 | version "2.3.0"
130 | resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.0.tgz#f1f061de8f6a4bf022892e2d128234fb98302972"
131 | integrity sha512-lY1Q/PiJGC2zOv/z391WOTD+Z02bCgsFfvxoXXf6h7kv9o+WmsmzYqrAwY63sNgOxE4xEdq0WyUnXfKeBrSvYw==
132 |
133 | pkg-dir@^4.1.0:
134 | version "4.2.0"
135 | resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-4.2.0.tgz#f099133df7ede422e81d1d8448270eeb3e4261f3"
136 | integrity sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==
137 | dependencies:
138 | find-up "^4.0.0"
139 |
140 | resolve@1.20.0:
141 | version "1.20.0"
142 | resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.20.0.tgz#629a013fb3f70755d6f0b7935cc1c2c5378b1975"
143 | integrity sha512-wENBPt4ySzg4ybFQW2TT1zMQucPK95HSh/nq2CFTZVOGut2+pQvSsgtda4d26YrYcr067wjbmzOG8byDPBX63A==
144 | dependencies:
145 | is-core-module "^2.2.0"
146 | path-parse "^1.0.6"
147 |
148 | rollup-plugin-typescript2@^0.30.0:
149 | version "0.30.0"
150 | resolved "https://registry.yarnpkg.com/rollup-plugin-typescript2/-/rollup-plugin-typescript2-0.30.0.tgz#1cc99ac2309bf4b9d0a3ebdbc2002aecd56083d3"
151 | integrity sha512-NUFszIQyhgDdhRS9ya/VEmsnpTe+GERDMmFo0Y+kf8ds51Xy57nPNGglJY+W6x1vcouA7Au7nsTgsLFj2I0PxQ==
152 | dependencies:
153 | "@rollup/pluginutils" "^4.1.0"
154 | find-cache-dir "^3.3.1"
155 | fs-extra "8.1.0"
156 | resolve "1.20.0"
157 | tslib "2.1.0"
158 |
159 | rollup@^2.56.3:
160 | version "2.56.3"
161 | resolved "https://registry.yarnpkg.com/rollup/-/rollup-2.56.3.tgz#b63edadd9851b0d618a6d0e6af8201955a77aeff"
162 | integrity sha512-Au92NuznFklgQCUcV96iXlxUbHuB1vQMaH76DHl5M11TotjOHwqk9CwcrT78+Tnv4FN9uTBxq6p4EJoYkpyekg==
163 | optionalDependencies:
164 | fsevents "~2.3.2"
165 |
166 | semver@^6.0.0:
167 | version "6.3.0"
168 | resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d"
169 | integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==
170 |
171 | tslib@2.1.0:
172 | version "2.1.0"
173 | resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.1.0.tgz#da60860f1c2ecaa5703ab7d39bc05b6bf988b97a"
174 | integrity sha512-hcVC3wYEziELGGmEEXue7D75zbwIIVUMWAVbHItGPx0ziyXxrOMQx4rQEVEV45Ut/1IotuEvwqPopzIOkDMf0A==
175 |
176 | typescript@^4.4.2:
177 | version "4.4.2"
178 | resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.4.2.tgz#6d618640d430e3569a1dfb44f7d7e600ced3ee86"
179 | integrity sha512-gzP+t5W4hdy4c+68bfcv0t400HVJMMd2+H9B7gae1nQlBzCqvrXX+6GL/b3GAgyTH966pzrZ70/fRjwAtZksSQ==
180 |
181 | universalify@^0.1.0:
182 | version "0.1.2"
183 | resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.1.2.tgz#b646f69be3942dabcecc9d6639c80dc105efaa66"
184 | integrity sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==
185 |
--------------------------------------------------------------------------------