├── .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 | --------------------------------------------------------------------------------