Skip to content

Commit 08c02b5

Browse files
committed
New features and fixes
* Add drag-and-drop support (import user files) * Improve help * Add an EmptyFS (like NullFS but allows directories listing) * Fix path resolution issue
1 parent 066636c commit 08c02b5

File tree

10 files changed

+78
-13
lines changed

10 files changed

+78
-13
lines changed

README.md

+6-3
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ This projects aims to contribute to the discussion about [WebContainer specifica
1212

1313
This is a bash-like shell but that runs in the browser. It comes with a lightweight kernel implementation (supports process and filesystem management):
1414
* Every process runs in its own dedicated worker
15-
* [Extensible filesystem](public/index.js#L30-48)
15+
* [Extensible filesystem](public/index.js#L56-L62)
1616
* Performant (heavily rely on postMessage and Transferable objects - reduce minimize the amount of copy)
1717
* Supports commands pipes (eg. `echo Hello world! | tee README`)
1818

@@ -26,10 +26,13 @@ Interesting files:
2626
* [ ] Serve filesystem via Service Worker
2727
* [ ] Let the app works offline with a Service Worker
2828
* [X] Move shell features into a dedicated a process (enable nested shells)
29+
* [ ] Add signals support (for SIGINT and SIGKILL)
2930
* [ ] Add jobs support (enables detached commands)
3031
* [ ] Add network support (TCP, UNIX socket, UDP)
31-
* [ ] Add multi-tabs support (cross-tabs kernel)
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)
3235
* [ ] Add `ps` and `kill` commands
3336
* [ ] Add docs about APIs and kernel design
34-
* [ ] Add a `deno` command (redirect ops to the in-browser kernel)
37+
* [ ] Add a `deno` command (shim the Rust part with a wrapper around this lib's API)
3538
* [ ] `iframe`-based process ? (enable `electron`-like apps)

public/index.js

+21
Original file line numberDiff line numberDiff line change
@@ -54,12 +54,33 @@ Type \x1B[1;3mhelp\x1B[0m to get started\n`)
5454
filesystem
5555
.mount([], root)
5656
.mount(['root'], filesystem)
57+
.mount(['mnt'], new fs.EmptyFS())
5758
.mount(['dev'], new fs.NullFS()) // TODO dedicated driver
5859
.mount(['sys'], new fs.NullFS()) // TODO dedicated driver
5960
.mount(['proc'], new fs.NullFS()) // TODO dedicated driver
6061
.mount(['bin'], new fs.HTTPFS(new URL('./command/', import.meta.url).href))
6162
.mount(['tmp'], new fs.MemFS());
6263

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+
6384
try {
6485
const bash = await webcontainer.run({
6586
entrypoint: 'bash', // will be resolved using the PATH environment variable

rollup.config.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ export default [
4242
const [, description = '', usage = ''] = /\/\*\*((?:.|\n)+?)(?:@usage\s([^\n]+?)\s+)?\*\//.exec(content) ?? [];
4343
commands[name] = {
4444
usage: usage.split(' ').filter(x => x),
45-
description: description.replace(/^\s(?:\n|\*\s+)/gm, '').trim()
45+
description: description.replace(/^(?:\n|\s\*\s)/gm, '').trim()
4646
};
4747
return ({
4848
input: {

src/command/bash.ts

+7
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,13 @@
22

33
/**
44
* 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
512
*/
613

714
if (typeof webcontainer !== 'function')

src/command/help.ts

+8-4
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,11 @@ webcontainer(async function help(process) {
1818
'Content-Type': 'application/json'
1919
}
2020
}).json();
21-
for (const [name, { usage, description }] of Object.entries<{ usage: string[], description: string }>(cmds))
22-
await print(`${[`\x1B[1m${name}\x1B[0m`, ...usage.map(a => `\x1B[4m${a}\x1B[0m`)].join(' ')}\n\t${description || '(no help available)'}\n`);
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+
}
2326
} catch (err) {
2427
await printErr(`\x1B[1;31mCommands list not available (${(err instanceof Error && err.message) ?? err})\x1B[0m`);
2528
await print('');
@@ -31,6 +34,7 @@ webcontainer(async function help(process) {
3134
await print(' \x1B[1m TAB\x1B[0m : DOES NOTHING (no auto-compelte yet)');
3235
await print('');
3336
await print("Commands can be piped together with a | character (eg. \x1B[4mecho Hello world! | tee README\x1B[0m). && and || doesn't work yet.");
34-
// await print('');
35-
// await print('TIP: Drop here a directory from your computer to make it accessible via this terminal.');
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.');
3640
});

src/command/tee.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
/// <reference path="../userspace/index.d.ts" />
22

33
/**
4-
* Redirects stdin to stdout and to all files passed at parameters
4+
* Redirects stdin to stdout and to all files passed as parameters
55
* @usage <file1> <file2> ... <fileN>
66
*/
77

src/kernelspace/fs/empty.ts

+28
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import type { FileSystemDriver, FileSystemNode } from './index';
2+
3+
export class EmptyFS implements FileSystemDriver {
4+
async resolveUri(path: string[]): Promise<string> {
5+
throw new Error(path.length ? 'ENOTFOUND' : 'EISDIR');
6+
}
7+
async access(path: string[]): Promise<boolean> {
8+
return !path.length;
9+
}
10+
async readDir(path: string[]): Promise<ReadableStream<FileSystemNode>> {
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<ReadableStream<Uint8Array>> {
20+
throw new Error(path.length ? 'ENOTFOUND' : 'EISDIR');
21+
}
22+
async writeFile(path: string[], offset: 'before' | 'after' | 'override', create: boolean): Promise<WritableStream<Uint8Array>> {
23+
throw new Error(path.length ? 'EACCESS' : 'EISDIR');
24+
}
25+
async deleteNode(path: string[], recursive: boolean): Promise<void> {
26+
throw new Error(path.length ? 'ENOTFOUND' : 'EBUSY');
27+
}
28+
}

src/kernelspace/fs/http.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ export class HTTPFS implements FileSystemDriver {
1717
const url = new URL(path.join('/'), this.#root);
1818
url.hash = '';
1919
url.search = '';
20-
const response = await fetch(url.href, { method: 'HEAD' });
20+
const response = await fetch(url.href, { method: 'HEAD', cache: 'force-cache' });
2121
switch (response.status) {
2222
case 200:
2323
case 201:
@@ -38,7 +38,7 @@ export class HTTPFS implements FileSystemDriver {
3838
const url = new URL(path.join('/'), this.#root);
3939
url.hash = '';
4040
url.search = '';
41-
const response = await fetch(url.href);
41+
const response = await fetch(url.href, { cache: 'force-cache' });
4242
switch (response.status) {
4343
case 200:
4444
break;

src/kernelspace/index.ts

+3-1
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { OverlayFS } from './fs/overlay';
88
import { NativeFS } from './fs/native';
99
import { MemFS } from './fs/memfs';
1010
import { HTTPFS } from './fs/http';
11+
import { EmptyFS } from './fs/empty';
1112
import { NullFS } from './fs/null';
1213
import { CustomTransferable, TO_TRANSFORABLES } from '../rpc';
1314

@@ -16,6 +17,7 @@ export const fs = {
1617
NativeFS,
1718
MemFS,
1819
HTTPFS,
20+
EmptyFS,
1921
NullFS,
2022
}
2123

@@ -77,7 +79,7 @@ class Kernel {
7779
}
7880
}
7981
} else if (/^\.?\.?\//.test(info.entrypoint))
80-
info.entrypoint = new URL(info.entrypoint, 'file://' + info.cwd).pathname;
82+
info.entrypoint = new URL(info.entrypoint, 'file://' + info.cwd + '/').pathname;
8183
const process = await KernelProcess.spawn(this, {
8284
...info,
8385
entrypoint: info.entrypoint,

src/userspace/index.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -260,7 +260,7 @@ class LocalProcessController {
260260
): Promise<void> {
261261
const kernel = KERNEL_PROCESS_ENDPOINT.attach(channel);
262262
channel.start();
263-
const resolved = new URL(info.entrypoint, 'file://' + info.cwd);
263+
const resolved = new URL(info.entrypoint, 'file://' + info.cwd + '/');
264264
const entrypointUrl = resolved.protocol === 'file:'
265265
? await kernel.resolveUri(segments(resolved.pathname))
266266
: resolved.href;

0 commit comments

Comments
 (0)