├── .github
├── FUNDING.yml
└── workflows
│ └── static.yml
├── .gitignore
├── .vscode
└── settings.json
├── README.md
├── RELEASE.md
├── biome.json
├── bun.lockb
├── lib
├── .swcrc
├── README.md
├── package-lock.json
├── package.json
├── src
│ ├── brotli_stream.ts
│ ├── get_llvm_wasm.ts
│ ├── get_rustc_wasm.ts
│ ├── get_wasm.ts
│ ├── index.ts
│ ├── parse_tar.ts
│ └── sysroot.ts
├── tsconfig.json
└── vite.config.ts
├── package-lock.json
├── package.json
└── page
├── .gitignore
├── README.md
├── index.html
├── package-lock.json
├── package.json
├── postcss.config.js
├── src
├── App.tsx
├── btn.tsx
├── cat.ts
├── cmd_parser.ts
├── compile_and_run.ts
├── config.ts
├── ctx.ts
├── index.css
├── index.tsx
├── monaco_worker.ts
├── solid_xterm.tsx
├── sysroot.ts
├── wasm
│ ├── lsr.wasm
│ └── tre.wasm
├── worker_process
│ ├── llvm.ts
│ ├── rustc.ts
│ ├── thread_spawn.ts
│ ├── util_cmd.ts
│ └── worker.ts
└── xterm.tsx
├── tailwind.config.ts
├── tsconfig.json
└── vite.config.ts
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | # These are supported funding model platforms
2 |
3 | github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g.,
4 | [oligamiq, bjorn3, whitequark]
5 | # patreon: # Replace with a single Patreon username
6 | # open_collective: # Replace with a single Open Collective username
7 | # ko_fi: # Replace with a single Ko-fi username
8 | # tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
9 | # community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
10 | # liberapay: # Replace with a single Liberapay username
11 | # issuehunt: # Replace with a single IssueHunt username
12 | # lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry
13 | # polar: # Replace with a single Polar username
14 | # buy_me_a_coffee: # Replace with a single Buy Me a Coffee username
15 | # thanks_dev: # Replace with a single thanks.dev username
16 | custom: # Replace with up to 4 custom sponsorship URLs e.g.,
17 | [https://www.amazon.co.jp/hz/wishlist/ls/3KDVR70NQ0DRQ?ref_=list_d_wl_lfu_nav_2]
18 |
--------------------------------------------------------------------------------
/.github/workflows/static.yml:
--------------------------------------------------------------------------------
1 | # Simple workflow for deploying static content to GitHub Pages
2 | name: Deploy static content to Pages
3 |
4 | on:
5 | # Runs on pushes targeting the default branch
6 | push:
7 | branches: ["main"]
8 |
9 | # Allows you to run this workflow manually from the Actions tab
10 | workflow_dispatch:
11 |
12 | # Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages
13 | permissions:
14 | contents: read
15 | pages: write
16 | id-token: write
17 |
18 | # Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued.
19 | # However, do NOT cancel in-progress runs as we want to allow these production deployments to complete.
20 | concurrency:
21 | group: "pages"
22 | cancel-in-progress: false
23 |
24 | jobs:
25 | # Single deploy job since we're just deploying
26 | deploy:
27 | environment:
28 | name: github-pages
29 | url: ${{ steps.deployment.outputs.page_url }}
30 | runs-on: ubuntu-latest
31 | steps:
32 | - name: Checkout
33 | uses: actions/checkout@v4
34 | - name: Setup Pages
35 | uses: actions/configure-pages@v5
36 | - name: Setup Node
37 | uses: actions/setup-node@v3
38 | with:
39 | node-version: 20
40 | - name: install bun
41 | run: |
42 | curl -fsSL https://bun.sh/install | bash
43 | source /home/runner/.bashrc
44 | echo '~/.bun/bin' >> $GITHUB_PATH
45 |
46 | - name: Build
47 | run: |
48 | bun i
49 | cd page
50 | bun run build
51 | bunx mini-coi -sw dist/mini-coi.js
52 | sed -i '/
/a \ ' dist/index.html
53 | - name: Upload artifact
54 | uses: actions/upload-pages-artifact@v3
55 | with:
56 | # Upload entire repository
57 | path: './page/dist'
58 | - name: Deploy to GitHub Pages
59 | id: deployment
60 | uses: actions/deploy-pages@v4
61 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | **/dist
2 | **/node_modules
3 |
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "editor.tabSize": 2,
3 | "prettier.enable": false,
4 | "cSpell.words": [
5 | "nanotar",
6 | "Preopen",
7 | "rustc",
8 | "rustlib",
9 | "uuidv4",
10 | "wasip"
11 | ]
12 | }
13 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Rubrc
2 | Rubrc is a rustc that runs in the browser.
3 |
4 | It is a port of the rustc compiler to WebAssembly. It is a work in progress and is not yet ready for general use.
5 |
6 | This have some bottlenecks, like the lack of thread spawn is very slow.
7 |
8 | Currently, the targets for which executable files can be generated are `wasm32-wasip1` and `x86_64-unknown-linux-musl`. Other targets fail during the linking process. If you have any information, we would greatly appreciate it if you could share it in an issue.
9 |
10 | Demo: [Rubrc](https://oligamiq.github.io/rubrc/)
11 |
12 | # Special Thanks
13 | ## Projects
14 | - [rubri](https://github.com/LyonSyonII/rubri) by [LyonSyonII](https://github.com/LyonSyonII) - At first, I was using this project to run it on the browser.
15 | - [browser_wasi_shim](https://github.com/bjorn3/browser_wasi_shim) by [bjorn3](https://github.com/bjorn3) - This project is used to run the WASI on the browser.
16 | - [browser_wasi_shim-threads](https://github.com/bjorn3/browser_wasi_shim/tree/main/threads#README) by [oligamiq](https://github.com/oligamiq) - This project is used to run the WASI with threads on the browser.
17 | - [rust_wasm](https://github.com/oligamiq/rust_wasm) by [oligamiq](https://github.com/oligamiq) - This is a project that hosts files and sysroots compiled from Rustc, supporting from Tier 1 to Tier 2 with host in this project, and compiled to wasm.
18 |
19 | ## People
20 | - [bjorn3](https://github.com/bjorn3) - He created the foundation for compiling Rustc to WASI and managing linker relations.
21 | - [oligamiq](https://github.com/oligamiq) - He created Rustc compiled with LLVM Backend to WASI.
22 | - [whitequark](https://github.com/whitequark) - He created the LLVM to WASI.
23 | - [rust-lang](https://github.com/rust-lang) - They created the Rust language.
24 |
25 | ## Related Page
26 | - https://github.com/rust-lang/miri/issues/722#issuecomment-1960849880
27 | - https://discourse.llvm.org/t/rfc-building-llvm-for-webassembly/79073/27
28 |
29 | # Issues
30 | This has been created in a rather haphazard manner, but as the creator, I will be busy for a while, so it’s been left in this state for now. There are numerous bugs, such as commands throwing errors and subsequently becoming unusable, but feel free to open issues if necessary. Minor pull requests to improve usability are also welcome, so feel free to tweak it as you like.
31 |
32 | # Features
33 | ! This project require coop coep headers to work, so you need to run it on a server or use a browser extension to allow it.
34 | - [x] Run rustc on the browser
35 | - [x] Ctrl+V
36 |
37 | # Funding
38 | The projects that this project depends on, namely [browser_wasi_shim-threads](https://www.npmjs.com/package/@oligami/browser_wasi_shim-threads), [rust_wasm](https://github.com/oligamiq/rust_wasm), and [shared-object](https://www.npmjs.com/package/@oligami/shared-object), are all my projects. The [toolchain-for-building-rustc](https://github.com/oligamiq/toolchain-for-building-rustc) that rust_wasm depends on is also my project. I was the one who enabled the LLVM backend for rustc, and ultimately, I aim to make rustc executable in browsers that support wasm and allow cargo to run seamlessly on the web.
39 |
40 | If you like or want to use this series of projects, I would appreciate it if you could contribute financially via the sponsor button.
41 |
42 | Please note that coding has temporarily stopped due to being busy, and there may be missing or incorrect documentation. Although it works, it is currently in a state with various issues, so I do not recommend using it for production.
43 |
44 | # License
45 | This project is licensed under the MIT OR Apache-2.0 License.
46 |
--------------------------------------------------------------------------------
/RELEASE.md:
--------------------------------------------------------------------------------
1 | # Release notes
2 | ## v1.0.0 (2024-11-26)
3 | - Initial release
4 |
5 | ## v1.1.0 (2024-12-8)
6 | - load time optimization
7 | 18s -> 10s
8 | Finer-grained asynchronous fetch.
9 | File size reduction due to brotli compression.
10 | Change wasm compile method from compile to compileStreaming.
11 | - lazy loading monaco editor
12 |
13 | ## v1.1.1 (2024-12-18)
14 | - change default code.
15 | - moved the file with the code that runs on the worker to another directory.
16 |
17 | ## v1.1.2 (2024-12-27)
18 | - enable pasting
19 |
--------------------------------------------------------------------------------
/biome.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://biomejs.dev/schemas/1.9.1/schema.json",
3 | "vcs": {
4 | "enabled": false,
5 | "clientKind": "git",
6 | "useIgnoreFile": false
7 | },
8 | "files": {
9 | "ignoreUnknown": false,
10 | "ignore": []
11 | },
12 | "formatter": {
13 | "enabled": true,
14 | "indentStyle": "space",
15 | "indentWidth": 2
16 | },
17 | "organizeImports": {
18 | "enabled": true
19 | },
20 | "linter": {
21 | "enabled": true,
22 | "rules": {
23 | "recommended": true
24 | }
25 | },
26 | "javascript": {
27 | "formatter": {
28 | "quoteStyle": "double"
29 | }
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/bun.lockb:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oligamiq/rubrc/268f73e0c07353624148567c5262e2a66e4fcca4/bun.lockb
--------------------------------------------------------------------------------
/lib/.swcrc:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://json.schemastore.org/swcrc",
3 | "jsc": {
4 | "parser": {
5 | "syntax": "typescript",
6 | "tsx": false,
7 | "dynamicImport": false,
8 | "decorators": false,
9 | "dts": true
10 | },
11 | "transform": {},
12 | "target": "esnext",
13 | "loose": false,
14 | "externalHelpers": false,
15 | "keepClassNames": true
16 | },
17 | "minify": true
18 | }
19 |
--------------------------------------------------------------------------------
/lib/README.md:
--------------------------------------------------------------------------------
1 | # A pure javascript shim for WASI Preview 1 threads
2 |
3 | > [!WARNING]
4 | > The code in this directory is less production ready than the main browser_wasi_shim code.
5 | > This code requires `SharedArrayBuffer`, `waitAsync` and `Atomics` to be enabled in the browser, so it may not work in all browsers.
6 | > For example, Firefox failed to run the demo in this directory.
7 | > Chrome worked fine.
8 | > This library require `cross-origin isolation` to be enabled in the browser.
9 |
10 | This project implement threads on browser_wasi_shim
11 |
12 | # Features
13 | - [x] thread creation
14 | - [x] Filesystem wrapper accessible by multiple workers
15 | - [ ] thread pool
16 |
17 | # Building
18 | ```sh
19 | $ npm install
20 | $ npm run build
21 | ```
22 |
23 | # Running the demo
24 | ```sh
25 | $ git submodule update --init
26 | $ cd examples && npm install && npm run dev
27 | ```
28 | And visit http://localhost
29 |
--------------------------------------------------------------------------------
/lib/package-lock.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@oligami/rustc-browser-wasi_shim",
3 | "version": "1.1.2",
4 | "lockfileVersion": 3,
5 | "requires": true,
6 | "packages": {
7 | "": {
8 | "name": "@oligami/rustc-browser-wasi_shim",
9 | "version": "1.1.2",
10 | "license": "MIT OR Apache-2.0",
11 | "dependencies": {
12 | "@bjorn3/browser_wasi_shim": "^0.3.0",
13 | "@oligami/browser_wasi_shim-threads": "^0.1.1",
14 | "@oligami/rustc-browser-wasi_shim": "file:",
15 | "brotli-dec-wasm": "^2.3.0"
16 | }
17 | },
18 | "node_modules/@bjorn3/browser_wasi_shim": {
19 | "version": "0.3.0",
20 | "resolved": "https://registry.npmjs.org/@bjorn3/browser_wasi_shim/-/browser_wasi_shim-0.3.0.tgz",
21 | "integrity": "sha512-FlRBYttPRLcWORzBe6g8nmYTafBkOEFeOqMYM4tAHJzFsQy4+xJA94z85a9BCs8S+Uzfh9LrkpII7DXr2iUVFg==",
22 | "license": "MIT OR Apache-2.0"
23 | },
24 | "node_modules/@oligami/browser_wasi_shim-threads": {
25 | "version": "0.1.1",
26 | "resolved": "https://registry.npmjs.org/@oligami/browser_wasi_shim-threads/-/browser_wasi_shim-threads-0.1.1.tgz",
27 | "integrity": "sha512-j47+NIQr10DojLfJvsMYYP0OQTbsuEa0sZaLLSPMyTgfUfE5eFPUJtf8SrUR6666nYIL0RoES720f9jWk+n7aQ==",
28 | "license": "MIT OR Apache-2.0",
29 | "dependencies": {
30 | "@bjorn3/browser_wasi_shim": "^0.3.0",
31 | "@oligami/browser_wasi_shim-threads": "file:"
32 | },
33 | "peerDependencies": {
34 | "@bjorn3/browser_wasi_shim": "^0.3.0"
35 | }
36 | },
37 | "node_modules/@oligami/browser_wasi_shim-threads/node_modules/@oligami/browser_wasi_shim-threads": {
38 | "resolved": "node_modules/@oligami/browser_wasi_shim-threads",
39 | "link": true
40 | },
41 | "node_modules/@oligami/rustc-browser-wasi_shim": {
42 | "resolved": "",
43 | "link": true
44 | },
45 | "node_modules/brotli-dec-wasm": {
46 | "version": "2.3.0",
47 | "resolved": "https://registry.npmjs.org/brotli-dec-wasm/-/brotli-dec-wasm-2.3.0.tgz",
48 | "integrity": "sha512-CNck+1A1ofvHk1oyqsKCuoIHLgD2FYy9KTVGHQlV1AKr/v/7N/Owh62nBKEcJxS3YOk+iwWhCi2rcaLhz9VN5g==",
49 | "license": "MIT OR Apache-2.0",
50 | "dependencies": {
51 | "prettier": "^3.2.5"
52 | },
53 | "peerDependencies": {
54 | "typescript": "^5.0.0"
55 | }
56 | },
57 | "node_modules/prettier": {
58 | "version": "3.4.2",
59 | "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.4.2.tgz",
60 | "integrity": "sha512-e9MewbtFo+Fevyuxn/4rrcDAaq0IYxPGLvObpQjiZBMAzB9IGmzlnG9RZy3FFas+eBMu2vA0CszMeduow5dIuQ==",
61 | "license": "MIT",
62 | "bin": {
63 | "prettier": "bin/prettier.cjs"
64 | },
65 | "engines": {
66 | "node": ">=14"
67 | },
68 | "funding": {
69 | "url": "https://github.com/prettier/prettier?sponsor=1"
70 | }
71 | },
72 | "node_modules/typescript": {
73 | "version": "5.7.2",
74 | "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.2.tgz",
75 | "integrity": "sha512-i5t66RHxDvVN40HfDd1PsEThGNnlMCMT3jMUuoh9/0TaqWevNontacunWyN02LA9/fIbEWlcHZcgTKb9QoaLfg==",
76 | "license": "Apache-2.0",
77 | "peer": true,
78 | "bin": {
79 | "tsc": "bin/tsc",
80 | "tsserver": "bin/tsserver"
81 | },
82 | "engines": {
83 | "node": ">=14.17"
84 | }
85 | }
86 | }
87 | }
88 |
--------------------------------------------------------------------------------
/lib/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@oligami/rustc-browser-wasi_shim",
3 | "version": "1.1.2",
4 | "type": "module",
5 | "scripts": {
6 | "dev": "vite",
7 | "build": "vite build",
8 | "prepare": "npm run build",
9 | "fmt": "biome format --write .",
10 | "lint": "biome lint src examples import-module-test",
11 | "check": "biome check && tsc --noEmit"
12 | },
13 | "dependencies": {
14 | "@bjorn3/browser_wasi_shim": "^0.3.0",
15 | "@oligami/browser_wasi_shim-threads": "^0.1.1",
16 | "@oligami/rustc-browser-wasi_shim": "file:",
17 | "brotli-dec-wasm": "^2.3.0"
18 | },
19 | "private": false,
20 | "publishConfig": {
21 | "access": "public"
22 | },
23 | "author": "oligami (https://github.com/oligamiq)",
24 | "license": "MIT OR Apache-2.0",
25 | "description": "Rust compiler on web",
26 | "homepage": "https://github.com/oligamiq/rubrc",
27 | "repository": {
28 | "type": "git",
29 | "url": "git+https://github.com/oligamiq/rubrc.git"
30 | },
31 | "bugs": {
32 | "url": "https://github.com/oligamiq/rubrc/issues"
33 | },
34 | "main": "./dist/rustc-browser-wasi_shim.es.js",
35 | "types": "./dist/index.d.ts",
36 | "exports": {
37 | ".": {
38 | "import": {
39 | "types": "./dist/index.d.ts",
40 | "default": "./dist/rustc-browser-wasi_shim.es.js"
41 | },
42 | "require": {
43 | "types": "./dist/index.d.ts",
44 | "default": "./dist/rustc-browser-wasi_shim.cjs.js"
45 | },
46 | "node": {
47 | "types": "./dist/index.d.ts",
48 | "default": "./dist/rustc-browser-wasi_shim.cjs.js"
49 | },
50 | "types": "./dist/index.d.ts",
51 | "default": "./dist/rustc-browser-wasi_shim.es.js"
52 | }
53 | },
54 | "files": [
55 | "dist",
56 | "src"
57 | ],
58 | "keywords": []
59 | }
60 |
--------------------------------------------------------------------------------
/lib/src/brotli_stream.ts:
--------------------------------------------------------------------------------
1 | import init, {
2 | BrotliDecStream,
3 | BrotliStreamResultCode,
4 | } from "brotli-dec-wasm/web"; // Import the default export
5 | // @ts-ignore
6 | import brotli_dec_wasm_bg from "brotli-dec-wasm/web/bg.wasm?wasm&url"; // Import the wasm file
7 |
8 | const promise = init(brotli_dec_wasm_bg); // Import is async in browsers due to wasm requirements!
9 |
10 | // 1MB output buffer
11 | const OUTPUT_SIZE = 1024 * 1024;
12 |
13 | export const get_brotli_decompress_stream = async (): Promise<
14 | TransformStream
15 | > => {
16 | await promise;
17 |
18 | const decompressStream = new BrotliDecStream();
19 | const decompressionStream = new TransformStream({
20 | transform(chunk, controller) {
21 | let resultCode: number;
22 | let inputOffset = 0;
23 |
24 | // Decompress this chunk, producing up to OUTPUT_SIZE output bytes at a time, until the
25 | // entire input has been decompressed.
26 |
27 | do {
28 | const input = chunk.slice(inputOffset);
29 | const result = decompressStream.decompress(input, OUTPUT_SIZE);
30 | controller.enqueue(result.buf);
31 | resultCode = result.code;
32 | inputOffset += result.input_offset;
33 | } while (resultCode === BrotliStreamResultCode.NeedsMoreOutput);
34 | if (
35 | resultCode !== BrotliStreamResultCode.NeedsMoreInput &&
36 | resultCode !== BrotliStreamResultCode.ResultSuccess
37 | ) {
38 | controller.error(`Brotli decompression failed with code ${resultCode}`);
39 | }
40 | },
41 | flush(controller) {
42 | controller.terminate();
43 | },
44 | });
45 | return decompressionStream;
46 | };
47 |
48 | export const fetch_compressed_stream = async (
49 | url: string | URL | globalThis.Request,
50 | ): Promise> => {
51 | const compressed_stream = await fetch(url);
52 | if (!compressed_stream.ok) {
53 | throw new Error("Failed to fetch wasm");
54 | }
55 | if (!compressed_stream.body) {
56 | throw new Error("No body in response");
57 | }
58 |
59 | return compressed_stream.body.pipeThrough(
60 | await get_brotli_decompress_stream(),
61 | );
62 | };
63 |
--------------------------------------------------------------------------------
/lib/src/get_llvm_wasm.ts:
--------------------------------------------------------------------------------
1 | import { get_wasm } from "./get_wasm";
2 |
3 | export const get_llvm_wasm = () =>
4 | get_wasm("https://oligamiq.github.io/rust_wasm/v0.2.0/llvm_opt.wasm.br");
5 |
--------------------------------------------------------------------------------
/lib/src/get_rustc_wasm.ts:
--------------------------------------------------------------------------------
1 | import { get_wasm } from "./get_wasm";
2 |
3 | export const get_rustc_wasm = () =>
4 | get_wasm("https://oligamiq.github.io/rust_wasm/v0.2.0/rustc_opt.wasm.br");
5 |
--------------------------------------------------------------------------------
/lib/src/get_wasm.ts:
--------------------------------------------------------------------------------
1 | import { fetch_compressed_stream } from "./brotli_stream";
2 |
3 | export const get_wasm = async (
4 | url: string | URL | globalThis.Request,
5 | ): Promise => {
6 | const response = new Response(await fetch_compressed_stream(url), {
7 | headers: { "Content-Type": "application/wasm" },
8 | });
9 |
10 | const wasm = await WebAssembly.compileStreaming(response);
11 |
12 | return wasm;
13 | };
14 |
--------------------------------------------------------------------------------
/lib/src/index.ts:
--------------------------------------------------------------------------------
1 | import "./sysroot";
2 | import "./get_rustc_wasm";
3 | import "./get_llvm_wasm";
4 |
--------------------------------------------------------------------------------
/lib/src/parse_tar.ts:
--------------------------------------------------------------------------------
1 | import type { TarFileItem } from "nanotar";
2 |
3 | // https://github.com/unjs/nanotar/blob/c1247bdec97163b487c8ca55003e291dfea755ab/src/parse.ts
4 | // MIT License
5 |
6 | // Copyright (c) Pooya Parsa
7 |
8 | // Permission is hereby granted, free of charge, to any person obtaining a copy
9 | // of this software and associated documentation files (the "Software"), to deal
10 | // in the Software without restriction, including without limitation the rights
11 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
12 | // copies of the Software, and to permit persons to whom the Software is
13 | // furnished to do so, subject to the following conditions:
14 |
15 | // The above copyright notice and this permission notice shall be included in all
16 | // copies or substantial portions of the Software.
17 |
18 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
19 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
20 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
21 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
22 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
23 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
24 | // SOFTWARE.
25 |
26 | export async function parseTar(
27 | readable_stream: ReadableStream,
28 | callback: (file: TarFileItem) => void,
29 | ) {
30 | const reader = readable_stream.getReader();
31 |
32 | let buffer = new Uint8Array(0);
33 | let done = false;
34 |
35 | const check_stream = async () => {
36 | const { done: _done, value } = await reader.read();
37 |
38 | if (value) {
39 | const new_buffer = new Uint8Array(buffer.length + value.length);
40 |
41 | new_buffer.set(buffer);
42 | new_buffer.set(value, buffer.length);
43 |
44 | buffer = new_buffer;
45 | }
46 |
47 | done = _done;
48 | };
49 |
50 | while (true) {
51 | while (buffer.length < 512 && !done) {
52 | await check_stream();
53 | }
54 |
55 | // File name (offset: 0 - length: 100)
56 | const name = _readString(buffer, 0, 100);
57 | if (name.length === 0) {
58 | break;
59 | }
60 |
61 | // File mode (offset: 100 - length: 8)
62 | const mode = _readString(buffer, 100, 8);
63 |
64 | // File uid (offset: 108 - length: 8)
65 | const uid = Number.parseInt(_readString(buffer, 108, 8));
66 |
67 | // File gid (offset: 116 - length: 8)
68 | const gid = Number.parseInt(_readString(buffer, 116, 8));
69 |
70 | // File size (offset: 124 - length: 12)
71 | const size = _readNumber(buffer, 124, 12);
72 |
73 | // File mtime (offset: 136 - length: 12)
74 | const mtime = _readNumber(buffer, 136, 12);
75 |
76 | // File type (offset: 156 - length: 1)
77 | const _type = _readNumber(buffer, 156, 1);
78 | const type = _type === 0 ? "file" : _type === 5 ? "directory" : _type; // prettier-ignore
79 |
80 | // Ustar indicator (offset: 257 - length: 6)
81 | // Ignore
82 |
83 | // Ustar version (offset: 263 - length: 2)
84 | // Ignore
85 |
86 | // File owner user (offset: 265 - length: 32)
87 | const user = _readString(buffer, 265, 32);
88 |
89 | // File owner group (offset: 297 - length: 32)
90 | const group = _readString(buffer, 297, 32);
91 |
92 | // File data (offset: 512 - length: size)
93 | while (buffer.length < 512 + size) {
94 | await check_stream();
95 | }
96 |
97 | const data = buffer.slice(512, 512 + size);
98 |
99 | const file = {
100 | name,
101 | type,
102 | size,
103 | data,
104 | get text() {
105 | return new TextDecoder().decode(this.data);
106 | },
107 | attrs: {
108 | mode,
109 | uid,
110 | gid,
111 | mtime,
112 | user,
113 | group,
114 | },
115 | };
116 |
117 | callback(file);
118 |
119 | let adjusted_size = 512 + 512 * Math.trunc(size / 512);
120 | if (size % 512) {
121 | adjusted_size += 512;
122 | }
123 |
124 | while (buffer.length < adjusted_size && !done) {
125 | await check_stream();
126 | }
127 |
128 | if (done && buffer.length < adjusted_size) {
129 | break;
130 | }
131 |
132 | buffer = buffer.slice(adjusted_size);
133 | }
134 | }
135 |
136 | function _readString(buffer: Uint8Array, offset: number, size: number) {
137 | const view = buffer.slice(offset, offset + size);
138 | const i = view.indexOf(0);
139 | const td = new TextDecoder();
140 | return td.decode(view.slice(0, i));
141 | }
142 |
143 | function _readNumber(buffer: Uint8Array, offset: number, size: number) {
144 | const view = buffer.slice(offset, offset + size);
145 | let str = "";
146 | for (let i = 0; i < size; i++) {
147 | str += String.fromCodePoint(view[i]);
148 | }
149 | return Number.parseInt(str, 8);
150 | }
151 |
--------------------------------------------------------------------------------
/lib/src/sysroot.ts:
--------------------------------------------------------------------------------
1 | import {
2 | type Inode,
3 | PreopenDirectory,
4 | File,
5 | Directory,
6 | } from "@bjorn3/browser_wasi_shim";
7 | import { WASIFarm } from "@oligami/browser_wasi_shim-threads";
8 |
9 | import { fetch_compressed_stream } from "./brotli_stream";
10 | import { parseTar } from "./parse_tar";
11 |
12 | export const load_sysroot_part = async (triple: string): Promise => {
13 | const decompressed_stream = await fetch_compressed_stream(
14 | `https://oligamiq.github.io/rust_wasm/v0.2.0/${triple}.tar.br`,
15 | );
16 |
17 | const dir = new Map();
18 | console.group("Loading sysroot");
19 |
20 | await parseTar(decompressed_stream, (file) => {
21 | if (!file.data) {
22 | throw new Error("File data not found");
23 | }
24 | if (file.name.includes("/")) {
25 | const parts = file.name.split("/");
26 | const created_dir = dir.get(parts[0]);
27 | if (created_dir instanceof Directory) {
28 | created_dir.contents.set(parts.slice(1).join("/"), new File(file.data));
29 | } else {
30 | dir.set(
31 | parts[0],
32 | new Directory([[parts.slice(1).join("/"), new File(file.data)]]),
33 | );
34 | }
35 | } else {
36 | dir.set(file.name, new File(file.data));
37 | }
38 |
39 | console.log(file.name);
40 | });
41 | console.groupEnd();
42 | return new Directory(dir);
43 | };
44 |
45 | const toMap = (arr: Array<[string, Inode]>) => {
46 | const map = new Map();
47 | for (const [key, value] of arr) {
48 | map.set(key, value);
49 | }
50 | return map;
51 | };
52 |
53 | let rustlib_dir: Directory | undefined;
54 |
55 | export const load_default_sysroot = async (): Promise => {
56 | const sysroot_part = await load_sysroot_part("wasm32-wasip1");
57 | rustlib_dir = new Directory([
58 | ["wasm32-wasip1", new Directory([["lib", sysroot_part]])],
59 | ]);
60 | const sysroot = new PreopenDirectory(
61 | "/sysroot",
62 | toMap([["lib", new Directory([["rustlib", rustlib_dir]])]]),
63 | );
64 | loaded_triples.add("wasm32-wasip1");
65 | return sysroot;
66 | };
67 |
68 | const loaded_triples: Set = new Set();
69 |
70 | export const load_additional_sysroot = async (triple: string) => {
71 | if (loaded_triples.has(triple)) {
72 | return;
73 | }
74 | const sysroot_part = await load_sysroot_part(triple);
75 | if (!rustlib_dir) {
76 | throw new Error("Default sysroot not loaded");
77 | }
78 | rustlib_dir.contents.set(triple, new Directory([["lib", sysroot_part]]));
79 | loaded_triples.add(triple);
80 | };
81 |
82 | export const get_default_sysroot_wasi_farm = async (): Promise => {
83 | const fds = [await load_default_sysroot()];
84 | const farm = new WASIFarm(undefined, undefined, undefined, fds, {
85 | allocator_size: 1024 * 1024 * 1024,
86 | });
87 | return farm;
88 | };
89 |
--------------------------------------------------------------------------------
/lib/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "ESNext",
4 | "useDefineForClassFields": true,
5 | "module": "ESNext",
6 | "lib": ["ESNext", "DOM"],
7 | "skipLibCheck": true,
8 | /* Bundler mode */
9 | "incremental": true,
10 | "moduleResolution": "bundler",
11 | "allowImportingTsExtensions": true,
12 | "isolatedModules": true,
13 | "moduleDetection": "force",
14 | "emitDeclarationOnly": true,
15 | "declaration": true,
16 | "declarationMap": true,
17 | "outDir": "./dist",
18 | "declarationDir": "./types",
19 | "esModuleInterop": true,
20 | /* Linting */
21 | "strict": true,
22 | "noUnusedLocals": true,
23 | "noUnusedParameters": true,
24 | "noFallthroughCasesInSwitch": true
25 | },
26 | "include": ["./src"]
27 | }
28 |
--------------------------------------------------------------------------------
/lib/vite.config.ts:
--------------------------------------------------------------------------------
1 | // https://zenn.dev/seapolis/articles/3605c4befc8465
2 |
3 | import { resolve } from "node:path";
4 | import { defineConfig } from "vite";
5 | // import dts from "vite-plugin-dts";
6 | import swc from "unplugin-swc";
7 |
8 | export default defineConfig({
9 | server: {
10 | headers: {
11 | "Cross-Origin-Embedder-Policy": "require-corp",
12 | "Cross-Origin-Opener-Policy": "same-origin",
13 | },
14 | },
15 | build: {
16 | lib: {
17 | entry: resolve(__dirname, "src/index.ts"),
18 | name: "wasi-shim-threads",
19 | formats: ["es", "umd", "cjs"],
20 | fileName: (format) => `rustc-browser-wasi-shim.${format}.js`,
21 | },
22 | sourcemap: true,
23 | minify: true,
24 | copyPublicDir: false,
25 | },
26 | // plugins: [dts({ rollupTypes: true })],
27 | // plugins: [swc.vite(), swc.rollup(), dts({ rollupTypes: true })],
28 | plugins: [swc.vite(), swc.rollup()],
29 | });
30 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "rubrc",
3 | "version": "1.1.2",
4 | "description": "rubrc root",
5 | "private": true,
6 | "directories": {
7 | "lib": "lib",
8 | "page": "page"
9 | },
10 | "author": "oligami (https://github.com/oligamiq)",
11 | "license": "MIT OR Apache-2.0",
12 | "type": "module",
13 | "devDependencies": {
14 | "@biomejs/biome": "1.9.4",
15 | "@swc/cli": "^0.5.2",
16 | "@swc/core": "^1.10.2",
17 | "@types/tar-stream": "^3.1.3",
18 | "autoprefixer": "^10.4.20",
19 | "better-typescript-lib": "^2.10.0",
20 | "npm-watch": "^0.13.0",
21 | "postcss": "^8.4.49",
22 | "solid-devtools": "^0.31.6",
23 | "tailwindcss": "^3.4.17",
24 | "typescript": "^5.7.2",
25 | "unplugin-swc": "^1.5.1",
26 | "vite": "^6.0.6",
27 | "vite-plugin-dts": "^4.4.0",
28 | "vite-plugin-solid": "^2.11.0"
29 | },
30 | "workspaces": [
31 | "./lib",
32 | "./page"
33 | ],
34 | "dependencies": {
35 | "rubrc": "^1.1.2"
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/page/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | dist
--------------------------------------------------------------------------------
/page/README.md:
--------------------------------------------------------------------------------
1 | ## Usage
2 |
3 | Those templates dependencies are maintained via [pnpm](https://pnpm.io) via `pnpm up -Lri`.
4 |
5 | This is the reason you see a `pnpm-lock.yaml`. That being said, any package manager will work. This file can be safely be removed once you clone a template.
6 |
7 | ```bash
8 | $ npm install # or pnpm install or yarn install
9 | ```
10 |
11 | ### Learn more on the [Solid Website](https://solidjs.com) and come chat with us on our [Discord](https://discord.com/invite/solidjs)
12 |
13 | ## Available Scripts
14 |
15 | In the project directory, you can run:
16 |
17 | ### `npm run dev` or `npm start`
18 |
19 | Runs the app in the development mode.
20 | Open [http://localhost:3000](http://localhost:3000) to view it in the browser.
21 |
22 | The page will reload if you make edits.
23 |
24 | ### `npm run build`
25 |
26 | Builds the app for production to the `dist` folder.
27 | It correctly bundles Solid in production mode and optimizes the build for the best performance.
28 |
29 | The build is minified and the filenames include the hashes.
30 | Your app is ready to be deployed!
31 |
32 | ## Deployment
33 |
34 | You can deploy the `dist` folder to any static host provider (netlify, surge, now, etc.)
35 |
--------------------------------------------------------------------------------
/page/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Rubrc
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/page/package-lock.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "rubrc",
3 | "version": "1.1.2",
4 | "lockfileVersion": 3,
5 | "requires": true,
6 | "packages": {
7 | "": {
8 | "name": "rubrc",
9 | "version": "1.1.2",
10 | "license": "MIT OR Apache-2.0",
11 | "dependencies": {
12 | "@bjorn3/browser_wasi_shim": "^0.3.0",
13 | "@oligami/browser_wasi_shim-threads": "^0.1.1",
14 | "@oligami/shared-object": "0.1.1",
15 | "@thisbeyond/solid-select": "^0.15.0",
16 | "@xterm/addon-fit": "^0.10.0",
17 | "@xterm/addon-search": "^0.15.0",
18 | "@xterm/xterm": "^5.5.0",
19 | "nanotar": "^0.1.1",
20 | "rubrc": "file:",
21 | "solid-js": "^1.9.3",
22 | "solid-monaco": "^0.3.0"
23 | }
24 | },
25 | "node_modules/@bjorn3/browser_wasi_shim": {
26 | "version": "0.3.0",
27 | "resolved": "https://registry.npmjs.org/@bjorn3/browser_wasi_shim/-/browser_wasi_shim-0.3.0.tgz",
28 | "integrity": "sha512-FlRBYttPRLcWORzBe6g8nmYTafBkOEFeOqMYM4tAHJzFsQy4+xJA94z85a9BCs8S+Uzfh9LrkpII7DXr2iUVFg==",
29 | "license": "MIT OR Apache-2.0"
30 | },
31 | "node_modules/@monaco-editor/loader": {
32 | "version": "1.4.0",
33 | "resolved": "https://registry.npmjs.org/@monaco-editor/loader/-/loader-1.4.0.tgz",
34 | "integrity": "sha512-00ioBig0x642hytVspPl7DbQyaSWRaolYie/UFNjoTdvoKPzo6xrXLhTk9ixgIKcLH5b5vDOjVNiGyY+uDCUlg==",
35 | "license": "MIT",
36 | "dependencies": {
37 | "state-local": "^1.0.6"
38 | },
39 | "peerDependencies": {
40 | "monaco-editor": ">= 0.21.0 < 1"
41 | }
42 | },
43 | "node_modules/@oligami/browser_wasi_shim-threads": {
44 | "version": "0.1.1",
45 | "resolved": "https://registry.npmjs.org/@oligami/browser_wasi_shim-threads/-/browser_wasi_shim-threads-0.1.1.tgz",
46 | "integrity": "sha512-j47+NIQr10DojLfJvsMYYP0OQTbsuEa0sZaLLSPMyTgfUfE5eFPUJtf8SrUR6666nYIL0RoES720f9jWk+n7aQ==",
47 | "license": "MIT OR Apache-2.0",
48 | "dependencies": {
49 | "@bjorn3/browser_wasi_shim": "^0.3.0",
50 | "@oligami/browser_wasi_shim-threads": "file:"
51 | },
52 | "peerDependencies": {
53 | "@bjorn3/browser_wasi_shim": "^0.3.0"
54 | }
55 | },
56 | "node_modules/@oligami/browser_wasi_shim-threads/node_modules/@oligami/browser_wasi_shim-threads": {
57 | "resolved": "node_modules/@oligami/browser_wasi_shim-threads",
58 | "link": true
59 | },
60 | "node_modules/@oligami/shared-object": {
61 | "version": "0.1.1",
62 | "resolved": "https://registry.npmjs.org/@oligami/shared-object/-/shared-object-0.1.1.tgz",
63 | "integrity": "sha512-1N5k8IL+JjD0lPiLR+NXT7BFzvnlPaBioMry9RZOL+4YI54TpW4fmkgys9rlyVo4hZB0DL5t8pgzXxq+IIO7fg==",
64 | "license": "MIT",
65 | "dependencies": {
66 | "shared-object": "file:"
67 | }
68 | },
69 | "node_modules/@thisbeyond/solid-select": {
70 | "version": "0.15.0",
71 | "resolved": "https://registry.npmjs.org/@thisbeyond/solid-select/-/solid-select-0.15.0.tgz",
72 | "integrity": "sha512-L3QnA5vm09JWNqLR4QqEIi7cgfs9/1sJbFjyEnPskjKpxEvb4G+Iy5cyd1qhZ2vAIZ9vaLd3udts3kefayPsMg==",
73 | "license": "MIT",
74 | "engines": {
75 | "node": ">=18.0.0",
76 | "pnpm": ">=9.0.0"
77 | },
78 | "peerDependencies": {
79 | "solid-js": "^1.8"
80 | }
81 | },
82 | "node_modules/@xterm/addon-fit": {
83 | "version": "0.10.0",
84 | "resolved": "https://registry.npmjs.org/@xterm/addon-fit/-/addon-fit-0.10.0.tgz",
85 | "integrity": "sha512-UFYkDm4HUahf2lnEyHvio51TNGiLK66mqP2JoATy7hRZeXaGMRDr00JiSF7m63vR5WKATF605yEggJKsw0JpMQ==",
86 | "license": "MIT",
87 | "peerDependencies": {
88 | "@xterm/xterm": "^5.0.0"
89 | }
90 | },
91 | "node_modules/@xterm/addon-search": {
92 | "version": "0.15.0",
93 | "resolved": "https://registry.npmjs.org/@xterm/addon-search/-/addon-search-0.15.0.tgz",
94 | "integrity": "sha512-ZBZKLQ+EuKE83CqCmSSz5y1tx+aNOCUaA7dm6emgOX+8J9H1FWXZyrKfzjwzV+V14TV3xToz1goIeRhXBS5qjg==",
95 | "license": "MIT",
96 | "peerDependencies": {
97 | "@xterm/xterm": "^5.0.0"
98 | }
99 | },
100 | "node_modules/@xterm/xterm": {
101 | "version": "5.5.0",
102 | "resolved": "https://registry.npmjs.org/@xterm/xterm/-/xterm-5.5.0.tgz",
103 | "integrity": "sha512-hqJHYaQb5OptNunnyAnkHyM8aCjZ1MEIDTQu1iIbbTD/xops91NB5yq1ZK/dC2JDbVWtF23zUtl9JE2NqwT87A==",
104 | "license": "MIT"
105 | },
106 | "node_modules/csstype": {
107 | "version": "3.1.3",
108 | "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
109 | "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
110 | "license": "MIT"
111 | },
112 | "node_modules/monaco-editor": {
113 | "version": "0.48.0",
114 | "resolved": "https://registry.npmjs.org/monaco-editor/-/monaco-editor-0.48.0.tgz",
115 | "integrity": "sha512-goSDElNqFfw7iDHMg8WDATkfcyeLTNpBHQpO8incK6p5qZt5G/1j41X0xdGzpIkGojGXM+QiRQyLjnfDVvrpwA==",
116 | "license": "MIT",
117 | "peer": true
118 | },
119 | "node_modules/nanotar": {
120 | "version": "0.1.1",
121 | "resolved": "https://registry.npmjs.org/nanotar/-/nanotar-0.1.1.tgz",
122 | "integrity": "sha512-AiJsGsSF3O0havL1BydvI4+wR76sKT+okKRwWIaK96cZUnXqH0uNBOsHlbwZq3+m2BR1VKqHDVudl3gO4mYjpQ==",
123 | "license": "MIT"
124 | },
125 | "node_modules/rubrc": {
126 | "resolved": "",
127 | "link": true
128 | },
129 | "node_modules/seroval": {
130 | "version": "1.1.1",
131 | "resolved": "https://registry.npmjs.org/seroval/-/seroval-1.1.1.tgz",
132 | "integrity": "sha512-rqEO6FZk8mv7Hyv4UCj3FD3b6Waqft605TLfsCe/BiaylRpyyMC0b+uA5TJKawX3KzMrdi3wsLbCaLplrQmBvQ==",
133 | "license": "MIT",
134 | "engines": {
135 | "node": ">=10"
136 | }
137 | },
138 | "node_modules/seroval-plugins": {
139 | "version": "1.1.1",
140 | "resolved": "https://registry.npmjs.org/seroval-plugins/-/seroval-plugins-1.1.1.tgz",
141 | "integrity": "sha512-qNSy1+nUj7hsCOon7AO4wdAIo9P0jrzAMp18XhiOzA6/uO5TKtP7ScozVJ8T293oRIvi5wyCHSM4TrJo/c/GJA==",
142 | "license": "MIT",
143 | "engines": {
144 | "node": ">=10"
145 | },
146 | "peerDependencies": {
147 | "seroval": "^1.0"
148 | }
149 | },
150 | "node_modules/shared-object": {
151 | "resolved": "node_modules/@oligami/shared-object",
152 | "link": true
153 | },
154 | "node_modules/solid-js": {
155 | "version": "1.9.3",
156 | "resolved": "https://registry.npmjs.org/solid-js/-/solid-js-1.9.3.tgz",
157 | "integrity": "sha512-5ba3taPoZGt9GY3YlsCB24kCg0Lv/rie/HTD4kG6h4daZZz7+yK02xn8Vx8dLYBc9i6Ps5JwAbEiqjmKaLB3Ag==",
158 | "license": "MIT",
159 | "dependencies": {
160 | "csstype": "^3.1.0",
161 | "seroval": "^1.1.0",
162 | "seroval-plugins": "^1.1.0"
163 | }
164 | },
165 | "node_modules/solid-monaco": {
166 | "version": "0.3.0",
167 | "resolved": "https://registry.npmjs.org/solid-monaco/-/solid-monaco-0.3.0.tgz",
168 | "integrity": "sha512-MsJLrCWysv5ONdOjC4kShNoBXJWuwjP6JplJLQWG7pL00XSfvqBEKUF0wLVdaKfntf50Qz8NAd7Yx7dq67bjPA==",
169 | "license": "MIT",
170 | "dependencies": {
171 | "@monaco-editor/loader": "^1.4.0"
172 | },
173 | "engines": {
174 | "node": ">=18",
175 | "pnpm": ">=8.6.0"
176 | },
177 | "peerDependencies": {
178 | "monaco-editor": "^0.48.0",
179 | "solid-js": "^1.8.0"
180 | }
181 | },
182 | "node_modules/state-local": {
183 | "version": "1.0.7",
184 | "resolved": "https://registry.npmjs.org/state-local/-/state-local-1.0.7.tgz",
185 | "integrity": "sha512-HTEHMNieakEnoe33shBYcZ7NX83ACUjCu8c40iOGEZsngj9zRnkqS9j1pqQPXwobB0ZcVTk27REb7COQ0UR59w==",
186 | "license": "MIT"
187 | }
188 | }
189 | }
190 |
--------------------------------------------------------------------------------
/page/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "rubrc",
3 | "version": "1.1.2",
4 | "description": "rubrc example page",
5 | "private": true,
6 | "author": "oligami (https://github.com/oligamiq)",
7 | "license": "MIT OR Apache-2.0",
8 | "type": "module",
9 | "scripts": {
10 | "start": "vite",
11 | "dev": "vite",
12 | "build": "vite build",
13 | "serve": "vite preview",
14 | "fmt": "biome format --write ."
15 | },
16 | "dependencies": {
17 | "@bjorn3/browser_wasi_shim": "^0.3.0",
18 | "@oligami/browser_wasi_shim-threads": "^0.1.1",
19 | "@oligami/shared-object": "0.1.1",
20 | "@thisbeyond/solid-select": "^0.15.0",
21 | "@xterm/addon-fit": "^0.10.0",
22 | "@xterm/addon-search": "^0.15.0",
23 | "@xterm/xterm": "^5.5.0",
24 | "nanotar": "^0.1.1",
25 | "rubrc": "file:",
26 | "solid-js": "^1.9.3",
27 | "solid-monaco": "^0.3.0",
28 | "uuid": "^11.0.3"
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/page/postcss.config.js:
--------------------------------------------------------------------------------
1 | export default {
2 | purge: ["./index.html", "./src/**/*.{vue,js,ts,jsx,tsx}"],
3 | plugins: {
4 | tailwindcss: {},
5 | autoprefixer: {},
6 | },
7 | };
8 |
--------------------------------------------------------------------------------
/page/src/App.tsx:
--------------------------------------------------------------------------------
1 | import { createSignal, lazy, Suspense, type Component } from "solid-js";
2 | import { SetupMyTerminal } from "./xterm";
3 | import type { WASIFarmRef } from "@oligami/browser_wasi_shim-threads";
4 | import type { Ctx } from "./ctx";
5 | import { default_value, rust_file } from "./config";
6 | import { DownloadButton, RunButton } from "./btn";
7 | import { triples } from "./sysroot";
8 |
9 | const Select = lazy(async () => {
10 | const selector = import("@thisbeyond/solid-select");
11 | const css_load = import("@thisbeyond/solid-select/style.css");
12 |
13 | const [mod] = await Promise.all([selector, css_load]);
14 |
15 | return { default: mod.Select };
16 | });
17 |
18 | import { SharedObjectRef } from "@oligami/shared-object";
19 | const MonacoEditor = lazy(() =>
20 | import("solid-monaco").then((mod) => ({ default: mod.MonacoEditor })),
21 | );
22 |
23 | const App = (props: {
24 | ctx: Ctx;
25 | callback: (wasi_ref: WASIFarmRef) => void;
26 | }) => {
27 | const handleMount = (monaco, editor) => {
28 | // Use monaco and editor instances here
29 | };
30 | const handleEditorChange = (value) => {
31 | // Handle editor value change
32 | rust_file.data = new TextEncoder().encode(value);
33 | };
34 | let load_additional_sysroot: (string) => void;
35 |
36 | const [triple, setTriple] = createSignal("wasm32-wasip1");
37 |
38 | return (
39 |
40 |
46 | Loading editor...
47 |
48 | }
49 | >
50 |
57 |
58 | {/* Hello tailwind!
*/}
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
82 |
83 |
84 |
85 |
86 |
87 | );
88 | };
89 |
90 | export default App;
91 |
--------------------------------------------------------------------------------
/page/src/btn.tsx:
--------------------------------------------------------------------------------
1 | import { compile_and_run, download } from "./compile_and_run";
2 |
3 | export const RunButton = (props: {
4 | triple: string;
5 | }) => {
6 | return (
7 |
17 | );
18 | };
19 |
20 | export const DownloadButton = () => {
21 | return (
22 |
32 | );
33 | };
34 |
--------------------------------------------------------------------------------
/page/src/cat.ts:
--------------------------------------------------------------------------------
1 | import type { WASIFarmAnimal } from "@oligami/browser_wasi_shim-threads";
2 | import { wasi } from "@bjorn3/browser_wasi_shim";
3 |
4 | export const get_data = (
5 | path__: string,
6 | _animal: WASIFarmAnimal,
7 | ): Uint8Array => {
8 | // biome-ignore lint/suspicious/noExplicitAny:
9 | const animal = _animal as any;
10 |
11 | // path is absolute
12 | let path = path__;
13 | if (!path.startsWith("/")) {
14 | path = `/${path}`;
15 | }
16 |
17 | // first: get opened fd dir name
18 | let root_fd: number;
19 | const dir_names: Map = new Map();
20 | for (let fd = 0; fd < animal.fd_map.length; fd++) {
21 | const [mapped_fd, wasi_farm_ref] = animal.get_fd_and_wasi_ref(fd);
22 | if (mapped_fd === undefined || wasi_farm_ref === undefined) {
23 | continue;
24 | }
25 | const [prestat, ret] = wasi_farm_ref.fd_prestat_get(mapped_fd);
26 | if (ret !== wasi.ERRNO_SUCCESS) {
27 | continue;
28 | }
29 | if (prestat) {
30 | const [tag, name_len] = prestat;
31 | if (tag === wasi.PREOPENTYPE_DIR) {
32 | const [path, ret] = wasi_farm_ref.fd_prestat_dir_name(
33 | mapped_fd,
34 | name_len,
35 | );
36 | if (ret !== wasi.ERRNO_SUCCESS) {
37 | continue;
38 | }
39 | if (path) {
40 | const decoded_path = new TextDecoder().decode(path);
41 | dir_names.set(fd, decoded_path);
42 | if (decoded_path === "/") {
43 | root_fd = fd;
44 | }
45 | }
46 | }
47 | }
48 | }
49 |
50 | console.log("dir_names", dir_names);
51 |
52 | // second: most match path
53 | let matched_fd = root_fd;
54 | let matched_dir_len = 1;
55 | const parts_path = path.split("/");
56 | for (const [fd, dir_name] of dir_names) {
57 | const parts_dir_name = dir_name.split("/");
58 | let dir_len = 0;
59 | for (let i = 0; i < parts_dir_name.length; i++) {
60 | if (parts_dir_name[i] === parts_path[i]) {
61 | dir_len++;
62 | } else {
63 | break;
64 | }
65 | }
66 | if (dir_len > matched_dir_len) {
67 | matched_fd = fd;
68 | matched_dir_len = dir_len;
69 | }
70 | }
71 |
72 | console.log("matched_fd", matched_fd);
73 |
74 | if (matched_dir_len === 0) {
75 | throw new Error("no matched dir");
76 | }
77 |
78 | console.log("matched_dir_name", dir_names.get(matched_fd));
79 |
80 | // third: tale the rest of path
81 | const rest_path = parts_path.slice(matched_dir_len).join("/");
82 | console.log("rest_path", rest_path);
83 |
84 | // fourth: open file
85 | const [mapped_fd, wasi_farm_ref_n] = animal.get_fd_and_wasi_ref_n(matched_fd);
86 | const [opened_fd, ret] = animal.wasi_farm_refs[wasi_farm_ref_n].path_open(
87 | mapped_fd,
88 | 0,
89 | new TextEncoder().encode(rest_path),
90 | 0,
91 | 0n,
92 | 0n,
93 | 0,
94 | );
95 |
96 | // eslint-disable-next-line @typescript-eslint/no-unused-vars
97 | const mapped_opened_fd = animal.map_new_fd_and_notify(
98 | opened_fd,
99 | wasi_farm_ref_n,
100 | );
101 |
102 | if (ret !== wasi.ERRNO_SUCCESS) {
103 | throw new Error("failed to open file");
104 | }
105 |
106 | // fifth: read file
107 | let file_data: Uint8Array = new Uint8Array();
108 | let offset = 0n;
109 |
110 | // eslint-disable-next-line no-constant-condition
111 | while (true) {
112 | console.log("offset", offset);
113 |
114 | const iovs = new Uint32Array(2);
115 | // buf_ptr so any value
116 | iovs[0] = 0;
117 | // buf_len
118 | iovs[1] = 1024;
119 | const [nread_and_buf, ret] = animal.wasi_farm_refs[
120 | wasi_farm_ref_n
121 | ].fd_pread(opened_fd, iovs, offset);
122 | if (ret !== wasi.ERRNO_SUCCESS) {
123 | throw new Error("failed to read file");
124 | }
125 | if (!nread_and_buf) {
126 | throw new Error("failed to read file");
127 | }
128 | const [nread, buf] = nread_and_buf;
129 | if (nread === 0) {
130 | break;
131 | }
132 | const new_data = new Uint8Array(file_data.length + nread);
133 | new_data.set(file_data);
134 | new_data.set(buf, file_data.length);
135 | file_data = new_data;
136 | offset += BigInt(nread);
137 | if (nread < 1024) {
138 | break;
139 | }
140 | }
141 |
142 | animal.wasi_farm_refs[wasi_farm_ref_n].fd_close(opened_fd);
143 |
144 | return file_data;
145 | };
146 |
--------------------------------------------------------------------------------
/page/src/cmd_parser.ts:
--------------------------------------------------------------------------------
1 | import { SharedObject, SharedObjectRef } from "@oligami/shared-object";
2 | import type { Ctx } from "./ctx";
3 | import { get_data } from "./cat";
4 |
5 | let waiter: SharedObject;
6 | let is_all_done = false;
7 | let is_cmd_run_end = true;
8 | let end_of_exec = false;
9 | let is_rustc_fetch_end = false;
10 |
11 | export const parser_setup = async (ctx: Ctx) => {
12 | const n = 1;
13 |
14 | const resolvers: PromiseWithResolvers[] = [];
15 | for (let i = 0; i < n; i++) {
16 | resolvers.push(Promise.withResolvers());
17 | }
18 |
19 | waiter = new SharedObject(
20 | {
21 | rustc: () => {
22 | resolvers[0].resolve();
23 | },
24 | end_rustc_fetch: () => {
25 | is_rustc_fetch_end = true;
26 | },
27 | is_rustc_fetch_end: () => {
28 | return is_rustc_fetch_end;
29 | },
30 | is_all_done: (): boolean => {
31 | return is_all_done;
32 | },
33 | is_cmd_run_end: () => {
34 | return is_cmd_run_end;
35 | },
36 | set_end_of_exec: (_end_of_exec: boolean) => {
37 | end_of_exec = _end_of_exec;
38 | },
39 | },
40 | ctx.waiter_id,
41 | );
42 |
43 | await Promise.all(resolvers.map((r) => r.promise));
44 |
45 | is_all_done = true;
46 |
47 | await all_done(ctx);
48 | };
49 |
50 | let cmd_parser: SharedObject;
51 |
52 | const all_done = async (ctx: Ctx) => {
53 | const rustc = new SharedObjectRef(ctx.rustc_id).proxy<
54 | (...string) => Promise
55 | >();
56 | const terminal = new SharedObjectRef(ctx.terminal_id).proxy<
57 | (string) => Promise
58 | >();
59 | const ls = new SharedObjectRef(ctx.ls_id).proxy<
60 | (...string) => Promise
61 | >();
62 | const tree = new SharedObjectRef(ctx.tree_id).proxy<
63 | (...string) => Promise
64 | >();
65 | const exec_file = new SharedObjectRef(ctx.exec_file_id).proxy<
66 | (...string) => Promise
67 | >();
68 | const download = new SharedObjectRef(ctx.download_id).proxy<
69 | (string) => Promise
70 | >();
71 | const clang = new SharedObjectRef(ctx.llvm_id).proxy<
72 | (...string) => Promise
73 | >();
74 |
75 | cmd_parser = new SharedObject((...args) => {
76 | is_cmd_run_end = false;
77 | (async (args: string[]) => {
78 | console.log(args);
79 |
80 | const cmd = args[0];
81 |
82 | console.log(cmd);
83 |
84 | const llvm_tools = [
85 | "symbolizer",
86 | "addr2line",
87 | "size",
88 | "objdump",
89 | "otool",
90 | "objcopy",
91 | "install-name-tool",
92 | "bitcode-strip",
93 | "strip",
94 | "cxxfilt",
95 | "c++filt",
96 | "ar",
97 | "ranlib",
98 | "lib",
99 | "dlltool",
100 | "lld",
101 | "lld-link",
102 | "ld.lld",
103 | "ld64.lld",
104 | "wasm-ld",
105 | "ld",
106 | "clang",
107 | "clang",
108 | "clang++",
109 | ];
110 |
111 | if (cmd === "rustc") {
112 | console.log("rustc");
113 | await terminal("executing rustc...\r\n");
114 | await rustc(...args.slice(1));
115 | } else if (cmd === "clang") {
116 | console.log("clang");
117 | await terminal("executing clang...\r\n");
118 | await clang(...args.slice());
119 | } else if (cmd === "llvm") {
120 | console.log("llvm");
121 | await terminal("executing llvm...\r\n");
122 | await clang(...args.slice());
123 | } else if (llvm_tools.includes(cmd)) {
124 | console.log("llvm");
125 | await terminal("executing llvm...\r\n");
126 | await clang(...["llvm", ...args.slice()]);
127 | } else if (cmd === "echo") {
128 | console.log("echo");
129 | await terminal(`${args.slice(1).join(" ")}\r\n`);
130 | } else if (cmd === "ls") {
131 | console.log("ls");
132 | await terminal("executing ls...\r\n");
133 | await ls(...args.slice(1));
134 | } else if (cmd === "tree") {
135 | console.log("tree");
136 | await terminal("executing tree...\r\n");
137 | await tree(...args.slice(1));
138 | } else if (cmd === "download") {
139 | console.log("download: ", args[1]);
140 | if (args[1].includes("/")) {
141 | await terminal("executing download...\r\n");
142 | end_of_exec = false;
143 | await download(args[1]);
144 | while (!end_of_exec) {
145 | await new Promise((resolve) => setTimeout(resolve, 100));
146 | }
147 | } else {
148 | await terminal("download require absolute path\r\n");
149 | }
150 | } else if (cmd.includes("/")) {
151 | console.log("cmd includes /");
152 | await terminal("executing file...\r\n");
153 | end_of_exec = false;
154 | await exec_file(...args);
155 | while (!end_of_exec) {
156 | await new Promise((resolve) => setTimeout(resolve, 100));
157 | }
158 | } else {
159 | const cmd_list = [
160 | "rustc",
161 | "clang",
162 | "llvm",
163 | "echo",
164 | "ls",
165 | "tree",
166 | "download",
167 | ...llvm_tools,
168 | ];
169 | await terminal(
170 | `command not found: ${cmd}\r\navailable commands: ${cmd_list.join(", ")}\r\n`,
171 | );
172 | }
173 | await terminal(">");
174 | is_cmd_run_end = true;
175 | })(args);
176 | }, ctx.cmd_parser_id);
177 |
178 | await terminal("rustc -h\r\n");
179 | await rustc("-h");
180 | await terminal(">");
181 | };
182 |
--------------------------------------------------------------------------------
/page/src/compile_and_run.ts:
--------------------------------------------------------------------------------
1 | import { SharedObject, SharedObjectRef } from "@oligami/shared-object";
2 | import type { Ctx } from "./ctx";
3 |
4 | let ctx: Ctx;
5 | let cmd_parser: (...string) => Promise;
6 | let waiter: {
7 | is_all_done: () => Promise;
8 | is_cmd_run_end: () => Promise;
9 | };
10 | let terminal: ((string) => Promise) & {
11 | reset_err_buff: () => Promise;
12 | get_err_buff: () => Promise;
13 | reset_out_buff: () => Promise;
14 | get_out_buff: () => Promise;
15 | };
16 | let shared_downloader: SharedObject;
17 | let exec_ref: (...string) => Promise;
18 |
19 | export const compile_and_run_setup = (_ctx: Ctx) => {
20 | ctx = _ctx;
21 |
22 | waiter = new SharedObjectRef(ctx.waiter_id).proxy();
23 |
24 | shared_downloader = new SharedObject((url: string, name: string) => {
25 | (async () => {
26 | const a = document.createElement("a");
27 | a.href = url;
28 | a.download = name; // ダウンロード時のファイル名を指定
29 | document.body.appendChild(a); // DOM に追加
30 | a.click(); // クリックしてダウンロードを開始
31 | document.body.removeChild(a); // すぐに削除
32 | })();
33 | }, ctx.download_by_url_id);
34 |
35 | exec_ref = new SharedObjectRef(ctx.cmd_parser_id).proxy();
36 | };
37 |
38 | let can_setup = false;
39 |
40 | export const compile_and_run = async (triple: string) => {
41 | if (!can_setup) {
42 | if (await waiter.is_all_done()) {
43 | terminal = new SharedObjectRef(ctx.terminal_id).proxy();
44 |
45 | cmd_parser = new SharedObjectRef(ctx.cmd_parser_id).proxy();
46 | can_setup = true;
47 | } else {
48 | terminal = new SharedObjectRef(ctx.terminal_id).proxy();
49 |
50 | await terminal("this is not done yet\r\n");
51 | }
52 | }
53 |
54 | if (can_setup) {
55 | const exec = [
56 | "rustc",
57 | "/main.rs",
58 | "--sysroot",
59 | "/sysroot",
60 | "--target",
61 | triple,
62 | "--out-dir",
63 | "/tmp",
64 | "-Ccodegen-units=1",
65 | ];
66 | if (triple === "wasm32-wasip1") {
67 | exec.push("-Clinker-flavor=wasm-ld");
68 | exec.push("-Clinker=wasm-ld");
69 | } else {
70 | // exec.push("-Zunstable-options");
71 | // exec.push("-Clinker-flavor=gnu");
72 | exec.push("-Clinker=lld");
73 |
74 | await terminal.reset_err_buff();
75 | }
76 | await terminal(`${exec.join(" ")}\r\n`);
77 | await cmd_parser(...exec);
78 | while (!(await waiter.is_cmd_run_end())) {
79 | await new Promise((resolve) => setTimeout(resolve, 100));
80 | }
81 |
82 | if (triple === "wasm32-wasip1") {
83 | await terminal("/tmp/main.wasm\r\n");
84 | await cmd_parser("/tmp/main.wasm");
85 | while (!(await waiter.is_cmd_run_end())) {
86 | await new Promise((resolve) => setTimeout(resolve, 100));
87 | }
88 | } else if (triple === "x86_64-pc-windows-gnu") {
89 | const err_msg = await terminal.get_out_buff();
90 | console.log("err_msg: ", err_msg);
91 |
92 | const lld_args_and_etc = err_msg
93 | .split("\r\n")
94 | .find((line) => line.includes("Linking using"));
95 | if (!lld_args_and_etc) {
96 | throw new Error("cannot get lld arguments");
97 | }
98 |
99 | // split by space
100 | const lld_args_str = lld_args_and_etc
101 | .split(' "')
102 | ?.slice(1)
103 | .map((arg) => arg.slice(0, -1));
104 |
105 | // first args to lld-link
106 | const clang_args = lld_args_str;
107 | clang_args[0] = "lld-link";
108 |
109 | // // add -fuse-ld=lld
110 | // clang_args.push("-fuse-ld=lld");
111 |
112 | await terminal(`${clang_args.join(" ")}\r\n`);
113 | await cmd_parser(...clang_args);
114 | } else {
115 | await terminal("download /tmp/main\r\n");
116 | await cmd_parser("download", "/tmp/main");
117 | while (!(await waiter.is_cmd_run_end())) {
118 | await new Promise((resolve) => setTimeout(resolve, 100));
119 | }
120 | }
121 | }
122 | };
123 |
124 | export const download = async (file: string) => {
125 | console.log("download");
126 | await terminal(`download ${file}\r\n`);
127 | exec_ref("download", file);
128 | };
129 |
--------------------------------------------------------------------------------
/page/src/config.ts:
--------------------------------------------------------------------------------
1 | import { File } from "@bjorn3/browser_wasi_shim";
2 |
3 | export const default_value = `// /main.rs
4 | fn main() {
5 | let first_time = std::time::SystemTime::now();
6 |
7 | let count = std::env::args()
8 | .nth(1)
9 | .map(|arg| arg.parse::().ok())
10 | .flatten()
11 | .unwrap_or(10);
12 |
13 | (0..=count).for_each(|i| println!("{i}"));
14 |
15 | println!("Time: {:?}", first_time.elapsed().unwrap());
16 | }
17 | `;
18 |
19 | export const rust_file: File = new File(
20 | new TextEncoder().encode(default_value),
21 | );
22 |
--------------------------------------------------------------------------------
/page/src/ctx.ts:
--------------------------------------------------------------------------------
1 | import { v4 as uuidv4 } from 'uuid';
2 |
3 | export type Ctx = {
4 | terminal_id: string;
5 | rustc_id: string;
6 | waiter_id: string;
7 | cmd_parser_id: string;
8 | tree_id: string;
9 | ls_id: string;
10 | exec_file_id: string;
11 | download_id: string;
12 | download_by_url_id: string;
13 | load_additional_sysroot_id: string;
14 | llvm_id: string;
15 | };
16 |
17 | export const gen_ctx = (): Ctx => {
18 | return {
19 | terminal_id: uuidv4(),
20 | rustc_id: uuidv4(),
21 | waiter_id: uuidv4(),
22 | cmd_parser_id: uuidv4(),
23 | tree_id: uuidv4(),
24 | ls_id: uuidv4(),
25 | exec_file_id: uuidv4(),
26 | download_id: uuidv4(),
27 | download_by_url_id: uuidv4(),
28 | load_additional_sysroot_id: uuidv4(),
29 | llvm_id: uuidv4(),
30 | };
31 | };
32 |
--------------------------------------------------------------------------------
/page/src/index.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
4 |
--------------------------------------------------------------------------------
/page/src/index.tsx:
--------------------------------------------------------------------------------
1 | /* @refresh reload */
2 | import "./index.css";
3 | import { render } from "solid-js/web";
4 |
5 | import App from "./App";
6 | import { gen_ctx } from "./ctx";
7 | import MainWorker from "./worker_process/worker?worker";
8 | import { parser_setup } from "./cmd_parser";
9 | import "./monaco_worker";
10 | import { compile_and_run_setup } from "./compile_and_run";
11 |
12 | const root = document.getElementById("root");
13 |
14 | if (import.meta.env.DEV && !(root instanceof HTMLElement)) {
15 | throw new Error(
16 | "Root element not found. Did you forget to add it to your index.html? Or maybe the id attribute got misspelled?",
17 | );
18 | }
19 |
20 | const ctx = gen_ctx();
21 |
22 | // create worker
23 | const worker = new MainWorker();
24 |
25 | parser_setup(ctx);
26 | compile_and_run_setup(ctx);
27 |
28 | // send message to worker
29 | worker.postMessage({ ctx });
30 |
31 | render(
32 | () => (
33 |
36 | worker.postMessage({
37 | wasi_ref,
38 | })
39 | }
40 | />
41 | ),
42 | // biome-ignore lint/style/noNonNullAssertion:
43 | root!,
44 | );
45 |
--------------------------------------------------------------------------------
/page/src/monaco_worker.ts:
--------------------------------------------------------------------------------
1 | import * as monaco from "monaco-editor";
2 | import editorWorker from "monaco-editor/esm/vs/editor/editor.worker?worker";
3 | import jsonWorker from "monaco-editor/esm/vs/language/json/json.worker?worker";
4 | import cssWorker from "monaco-editor/esm/vs/language/css/css.worker?worker";
5 | import htmlWorker from "monaco-editor/esm/vs/language/html/html.worker?worker";
6 | import tsWorker from "monaco-editor/esm/vs/language/typescript/ts.worker?worker";
7 |
8 | // @ts-ignore
9 | self.MonacoEnvironment = {
10 | // biome-ignore lint/suspicious/noExplicitAny:
11 | getWorker(_: any, label: string) {
12 | if (label === "json") {
13 | return new jsonWorker();
14 | }
15 | if (label === "css" || label === "scss" || label === "less") {
16 | return new cssWorker();
17 | }
18 | if (label === "html" || label === "handlebars" || label === "razor") {
19 | return new htmlWorker();
20 | }
21 | if (label === "typescript" || label === "javascript") {
22 | return new tsWorker();
23 | }
24 | return new editorWorker();
25 | },
26 | };
27 |
--------------------------------------------------------------------------------
/page/src/solid_xterm.tsx:
--------------------------------------------------------------------------------
1 | // solid-xterm's code fork
2 | // https://github.com/WVAviator/solid-xterm
3 |
4 | import { createEffect, createSignal, onCleanup } from "solid-js";
5 | import {
6 | type ITerminalAddon,
7 | type ITerminalInitOnlyOptions,
8 | type ITerminalOptions,
9 | Terminal,
10 | } from "@xterm/xterm";
11 | import "@xterm/xterm/css/xterm.css";
12 |
13 | export type OnMountCleanup = () => void | (() => Promise) | undefined;
14 |
15 | // biome-ignore lint/suspicious/noExplicitAny:
16 | export type ITerminalAddonConstructor = new (...args: any[]) => ITerminalAddon;
17 | export interface XTermProps {
18 | /**
19 | * The CSS classes that will be applied to the terminal container.
20 | */
21 | class?: string;
22 |
23 | /**
24 | * A set of options for the terminal that will be provided on loading.
25 | * A list of all available properties can be found at https://xtermjs.org/docs/api/terminal/interfaces/iterminaloptions/
26 | */
27 | options?: ITerminalOptions & ITerminalInitOnlyOptions;
28 |
29 | /**
30 | * An array of addons that will be loaded into XTerm. Addons can be passed as either an instance or a constructor.
31 | * @see https://xtermjs.org/docs/api/addons/
32 | * @example
33 | * ```tsx
34 | * import { SearchAddon } from 'xterm-addon-search';
35 | * import { FitAddon } from 'xterm-addon-fit';
36 | *
37 | * ...
38 | *
39 | * const searchAddon = createMemo(() => new SearchAddon());
40 | *
41 | *
42 | * ```
43 | */
44 | addons?: (ITerminalAddonConstructor | ITerminalAddon)[];
45 |
46 | /**
47 | * On mount, this callback will be called with the terminal instance.
48 | * @param terminal The terminal object emitting the event.
49 | * @returns A function that will be called when the component is unmounted.
50 | */
51 | onMount?: (terminal: Terminal) => OnMountCleanup | Promise;
52 |
53 | /**
54 | * A callback that will be called when the bell is triggered.
55 | * @param terminal The terminal object emitting the event.
56 | */
57 | onBell?: (terminal: Terminal) => void;
58 |
59 | /**
60 | * A callback that will be called when a binary event fires. This is used to enable non UTF-8 conformant binary messages to be sent to the backend. Currently this is only used for a certain type of mouse reports that happen to be not UTF-8 compatible. The event value is a JS string, pass it to the underlying pty as binary data, e.g. `pty.write(Buffer.from(data, 'binary'))`.
61 | * @see https://xtermjs.org/docs/api/terminal/classes/terminal/#onbinary
62 | * @param data
63 | * @param terminal The terminal object emitting the event.
64 | */
65 | onBinary?: (data: string, terminal: Terminal) => void;
66 |
67 | /**
68 | * A callback that will be called when the cursor moves.
69 | * @see https://xtermjs.org/docs/api/terminal/classes/terminal/#oncursormove
70 | * @param cursorPosition An object containing x and y properties representing the new cursor position.
71 | * @param terminal The terminal object emitting the event.
72 | */
73 | onCursorMove?: (
74 | cursorPosition: { x: number; y: number },
75 | terminal: Terminal,
76 | ) => void;
77 |
78 | /**
79 | * A callback that will be called when a data event fires. This happens for example when the user types or pastes into the terminal. The event value is whatever string results, in a typical setup, this should be passed on to the backing pty.
80 | * @see https://xtermjs.org/docs/api/terminal/classes/terminal/#ondata
81 | * @param data
82 | * @param terminal The terminal object emitting the event.
83 | */
84 | onData?: (data: string, terminal: Terminal) => void;
85 |
86 | /**
87 | * A callback that will be called when a key is pressed. The event value contains the string that will be sent in the data event as well as the DOM event that triggered it.
88 | * @see https://xtermjs.org/docs/api/terminal/classes/terminal/#onkey
89 | * @param event An object containing a key property representing the string sent to the data event, and a domEvent property containing the DOM event that triggered the keypress.
90 | * @param terminal The terminal object emitting the event.
91 | */
92 | onKey?: (
93 | event: { key: string; domEvent: KeyboardEvent },
94 | terminal: Terminal,
95 | ) => void;
96 |
97 | /**
98 | * A callback that will be called when a line feed is added.
99 | * @see https://xtermjs.org/docs/api/terminal/classes/terminal/#onlinefeed
100 | * @param terminal The terminal object emitting the event.
101 | */
102 | onLineFeed?: (terminal: Terminal) => void;
103 |
104 | /**
105 | * A callback that will be called when rows are rendered. The event value contains the start row and end rows of the rendered area (ranges from 0 to Terminal.rows - 1).
106 | * @see https://xtermjs.org/docs/api/terminal/classes/terminal/#onrender
107 | * @param event An object containing start and end properties which represent the start and end rows (inclusive) of the rendered area.
108 | * @param terminal The terminal object emitting the event.
109 | */
110 | onRender?: (
111 | event: { start: number; end: number },
112 | terminal: Terminal,
113 | ) => void;
114 |
115 | /**
116 | * A callback that will be called when the terminal is resized.
117 | * @see https://xtermjs.org/docs/api/terminal/classes/terminal/#onresize
118 | * @param size An object containing cols and rows properties representing the new size.
119 | * @param terminal The terminal object emitting the event.
120 | */
121 | onResize?: (size: { cols: number; rows: number }, terminal: Terminal) => void;
122 |
123 | /**
124 | * A callback that will be called when a scroll occurs.
125 | * @see https://xtermjs.org/docs/api/terminal/classes/terminal/#onscroll
126 | * @param yPos The new y-position of the viewport.
127 | * @param terminal The terminal object emitting the event.
128 | */
129 | onScroll?: (yPos: number, terminal: Terminal) => void;
130 |
131 | /**
132 | * A callback that will be called when a selection change occurs.
133 | * @see https://xtermjs.org/docs/api/terminal/classes/terminal/#onselectionchange
134 | * @param terminal The terminal object emitting the event.
135 | */
136 | onSelectionChange?: (terminal: Terminal) => void;
137 |
138 | /**
139 | * A callback that will be called when an OSC 0 or OSC 2 title change occurs.
140 | * @see https://xtermjs.org/docs/api/terminal/classes/terminal/#ontitlechange
141 | * @param title The new title.
142 | * @param terminal The terminal object emitting the event.
143 | */
144 | onTitleChange?: (title: string, terminal: Terminal) => void;
145 |
146 | /**
147 | * A callback that will be called when data has been parsed by the terminal, after write is called. This event is useful to listen for any changes in the buffer.
148 | * This fires at most once per frame, after data parsing completes. Note that this can fire when there are still writes pending if there is a lot of data.
149 | * @see https://xtermjs.org/docs/api/terminal/classes/terminal/#onwriteparsed
150 | * @param terminal The terminal object emitting the event.
151 | */
152 | onWriteParsed?: (terminal: Terminal) => void;
153 | }
154 |
155 | const XTerm = ({
156 | class: className = "",
157 | options = {},
158 | addons = [],
159 | onMount,
160 | onBell,
161 | onBinary,
162 | onCursorMove,
163 | onData,
164 | onKey,
165 | onLineFeed,
166 | onRender,
167 | onResize,
168 | onScroll,
169 | onSelectionChange,
170 | onTitleChange,
171 | onWriteParsed,
172 | }: XTermProps) => {
173 | const [terminal, setTerminal] = createSignal();
174 |
175 | const handleRef = (terminalContainerRef: HTMLDivElement) => {
176 | const newTerminal = new Terminal(options);
177 | newTerminal.open(terminalContainerRef);
178 |
179 | // biome-ignore lint/complexity/noForEach:
180 | addons.forEach((addon) => {
181 | if (typeof addon === "function") {
182 | newTerminal?.loadAddon(new addon());
183 | } else {
184 | newTerminal?.loadAddon(addon);
185 | }
186 | });
187 |
188 | setTerminal(newTerminal);
189 | };
190 |
191 | onCleanup(() => {
192 | const currentTerminal = terminal();
193 | if (!currentTerminal) return;
194 | currentTerminal.dispose();
195 | setTerminal(undefined);
196 | });
197 |
198 | createEffect(async () => {
199 | const currentTerminal = terminal();
200 | if (!currentTerminal || !onMount) return;
201 | const onMountCleanup = await onMount(currentTerminal);
202 | onCleanup(() => {
203 | onMountCleanup();
204 | });
205 | });
206 |
207 | createEffect(() => {
208 | const currentTerminal = terminal();
209 | if (!currentTerminal || !onBell) return;
210 | const onBellListener = currentTerminal.onBell(() =>
211 | onBell(currentTerminal),
212 | );
213 | onCleanup(() => {
214 | onBellListener.dispose();
215 | });
216 | });
217 |
218 | createEffect(() => {
219 | const currentTerminal = terminal();
220 | if (!currentTerminal || !onBinary) return;
221 | const onBinaryListener = currentTerminal.onBinary((data) =>
222 | onBinary(data, currentTerminal),
223 | );
224 | onCleanup(() => {
225 | onBinaryListener.dispose();
226 | });
227 | });
228 |
229 | createEffect(() => {
230 | const currentTerminal = terminal();
231 | if (!currentTerminal || !onCursorMove) return;
232 | const onCursorMoveListener = currentTerminal.onCursorMove(() => {
233 | if (!currentTerminal) return;
234 | const cursorX = currentTerminal.buffer.active.cursorX;
235 | const cursorY = currentTerminal.buffer.active.cursorY;
236 | const cursorPosition = { x: cursorX, y: cursorY };
237 | onCursorMove(cursorPosition, currentTerminal);
238 | });
239 | onCleanup(() => {
240 | onCursorMoveListener.dispose();
241 | });
242 | });
243 |
244 | createEffect(() => {
245 | const currentTerminal = terminal();
246 | if (!currentTerminal || !onData) return;
247 | const onDataListener = currentTerminal.onData((data) =>
248 | onData(data, currentTerminal),
249 | );
250 | onCleanup(() => {
251 | onDataListener.dispose();
252 | });
253 | });
254 |
255 | createEffect(() => {
256 | const currentTerminal = terminal();
257 | if (!currentTerminal || !onKey) return;
258 | const onKeyListener = currentTerminal.onKey((event) =>
259 | onKey(event, currentTerminal),
260 | );
261 | onCleanup(() => {
262 | onKeyListener.dispose();
263 | });
264 | });
265 |
266 | createEffect(() => {
267 | const currentTerminal = terminal();
268 | if (!currentTerminal || !onLineFeed) return;
269 | const onLineFeedListener = currentTerminal.onLineFeed(() =>
270 | onLineFeed(currentTerminal),
271 | );
272 | onCleanup(() => {
273 | onLineFeedListener.dispose();
274 | });
275 | });
276 |
277 | createEffect(() => {
278 | const currentTerminal = terminal();
279 | if (!currentTerminal || !onRender) return;
280 | const onRenderListener = currentTerminal.onRender((event) =>
281 | onRender(event, currentTerminal),
282 | );
283 | onCleanup(() => {
284 | onRenderListener.dispose();
285 | });
286 | });
287 |
288 | createEffect(() => {
289 | const currentTerminal = terminal();
290 | if (!currentTerminal || !onResize) return;
291 | const onResizeListener = currentTerminal.onResize((size) =>
292 | onResize(size, currentTerminal),
293 | );
294 | onCleanup(() => {
295 | onResizeListener.dispose();
296 | });
297 | });
298 |
299 | createEffect(() => {
300 | const currentTerminal = terminal();
301 | if (!currentTerminal || !onScroll) return;
302 | const onScrollListener = currentTerminal.onScroll((yPos) =>
303 | onScroll(yPos, currentTerminal),
304 | );
305 | onCleanup(() => {
306 | onScrollListener.dispose();
307 | });
308 | });
309 |
310 | createEffect(() => {
311 | const currentTerminal = terminal();
312 | if (!currentTerminal || !onSelectionChange) return;
313 | const onSelectionChangeListener = currentTerminal.onSelectionChange(() =>
314 | onSelectionChange(currentTerminal),
315 | );
316 | onCleanup(() => {
317 | onSelectionChangeListener.dispose();
318 | });
319 | });
320 |
321 | createEffect(() => {
322 | const currentTerminal = terminal();
323 | if (!currentTerminal || !onTitleChange) return;
324 | const onTitleChangeListener = currentTerminal.onTitleChange((title) =>
325 | onTitleChange(title, currentTerminal),
326 | );
327 | onCleanup(() => {
328 | onTitleChangeListener.dispose();
329 | });
330 | });
331 |
332 | createEffect(() => {
333 | const currentTerminal = terminal();
334 | if (!currentTerminal || !onWriteParsed) return;
335 | const onWriteParsedListener = currentTerminal.onWriteParsed(() =>
336 | onWriteParsed(currentTerminal),
337 | );
338 | onCleanup(() => {
339 | onWriteParsedListener.dispose();
340 | });
341 | });
342 |
343 | return ;
344 | };
345 |
346 | export default XTerm;
347 |
--------------------------------------------------------------------------------
/page/src/sysroot.ts:
--------------------------------------------------------------------------------
1 | export const triples = [
2 | "aarch64-unknown-linux-gnu",
3 | "aarch64-unknown-linux-musl",
4 | "arm-unknown-linux-gnueabi",
5 | "arm-unknown-linux-gnueabihf",
6 | "arm-unknown-linux-musleabi",
7 | "arm-unknown-linux-musleabihf",
8 | "armv7-unknown-linux-gnueabihf",
9 | "i586-unknown-linux-gnu",
10 | "i686-unknown-linux-gnu",
11 | "i686-unknown-linux-musl",
12 | "loongarch64-unknown-linux-gnu",
13 | "loongarch64-unknown-linux-musl",
14 | "powerpc-unknown-linux-gnu",
15 | "powerpc64-unknown-linux-gnu",
16 | "powerpc64le-unknown-linux-gnu",
17 | "riscv64gc-unknown-linux-gnu",
18 | "riscv64gc-unknown-linux-musl",
19 | "s390x-unknown-linux-gnu",
20 | "sparcv9-sun-solaris",
21 | "wasm32-unknown-emscripten",
22 | "wasm32-unknown-unknown",
23 | "wasm32-wasip1-threads",
24 | "wasm32-wasip1",
25 | "x86_64-pc-windows-gnu",
26 | "x86_64-unknown-freebsd",
27 | "x86_64-unknown-illumos",
28 | "x86_64-unknown-linux-gnu",
29 | "x86_64-unknown-linux-musl",
30 | "x86_64-unknown-netbsd",
31 | "aarch64-pc-windows-msvc",
32 | "i686-pc-windows-gnu",
33 | "i686-pc-windows-msvc",
34 | "x86_64-pc-windows-msvc",
35 | "aarch64-apple-darwin",
36 | "x86_64-apple-darwin",
37 | ];
38 |
--------------------------------------------------------------------------------
/page/src/wasm/lsr.wasm:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oligamiq/rubrc/268f73e0c07353624148567c5262e2a66e4fcca4/page/src/wasm/lsr.wasm
--------------------------------------------------------------------------------
/page/src/wasm/tre.wasm:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oligamiq/rubrc/268f73e0c07353624148567c5262e2a66e4fcca4/page/src/wasm/tre.wasm
--------------------------------------------------------------------------------
/page/src/worker_process/llvm.ts:
--------------------------------------------------------------------------------
1 | import { SharedObject, SharedObjectRef } from "@oligami/shared-object";
2 | import { WASIFarmAnimal } from "@oligami/browser_wasi_shim-threads";
3 | import type { Ctx } from "../ctx";
4 |
5 | const shared: SharedObject[] = [];
6 |
7 | globalThis.addEventListener("message", async (event) => {
8 | const {
9 | wasi_refs,
10 | ctx,
11 | }: {
12 | // WASIFarmRefObject is not export
13 | // biome-ignore lint/suspicious/noExplicitAny:
14 | wasi_refs: any[];
15 | ctx: Ctx;
16 | } = event.data;
17 |
18 | const waiter = new SharedObjectRef(ctx.waiter_id).proxy<{
19 | is_rustc_fetch_end: () => Promise;
20 | }>();
21 |
22 | while (!(await waiter.is_rustc_fetch_end())) {
23 | await new Promise((resolve) => setTimeout(resolve, 100));
24 | }
25 |
26 | console.log("loading llvm");
27 |
28 | await ready_llvm_wasm(wasi_refs, ctx);
29 | });
30 |
31 | import { get_llvm_wasm } from "../../../lib/src/get_llvm_wasm";
32 | import { strace } from "@bjorn3/browser_wasi_shim";
33 | let linker: WebAssembly.Instance & {
34 | exports: { memory: WebAssembly.Memory; _start: () => unknown };
35 | };
36 | let wasi: WASIFarmAnimal;
37 |
38 | const ready_llvm_wasm = async (
39 | // biome-ignore lint/suspicious/noExplicitAny:
40 | wasi_refs: any[],
41 | ctx: Ctx,
42 | ) => {
43 | const linker_wasm = await get_llvm_wasm();
44 |
45 | console.log("linker_wasm", linker_wasm);
46 |
47 | wasi = new WASIFarmAnimal(
48 | wasi_refs,
49 | ["llvm"], // args
50 | [], // env
51 | // {
52 | // debug: true,
53 | // can_thread_spawn: true,
54 | // thread_spawn_worker_url: new URL(thread_spawn_path, import.meta.url)
55 | // .href,
56 | // thread_spawn_wasm: linker,
57 | // },
58 | );
59 |
60 | linker = (await WebAssembly.instantiate(linker_wasm, {
61 | wasi_snapshot_preview1: strace(wasi.wasiImport, []),
62 | })) as unknown as {
63 | exports: { memory: WebAssembly.Memory; _start: () => unknown };
64 | };
65 |
66 | const memory_reset = linker.exports.memory.buffer;
67 | const memory_reset_view = new Uint8Array(memory_reset).slice();
68 |
69 | shared.push(
70 | new SharedObject((...args) => {
71 | try {
72 | if (args[0] !== "llvm") {
73 | wasi.args = ["llvm", ...args];
74 | } else {
75 | wasi.args = args;
76 | }
77 | console.log(`wasi.start: ${wasi.args}`);
78 | console.log(wasi);
79 | const memory_view = new Uint8Array(linker.exports.memory.buffer);
80 | memory_view.set(memory_reset_view);
81 | wasi.start(linker);
82 | console.log("wasi.start done");
83 | } catch (e) {
84 | console.error(e);
85 | }
86 | }, ctx.llvm_id),
87 | );
88 |
89 | console.log("llvm loaded");
90 | };
91 |
--------------------------------------------------------------------------------
/page/src/worker_process/rustc.ts:
--------------------------------------------------------------------------------
1 | import { SharedObject, SharedObjectRef } from "@oligami/shared-object";
2 | import { get_rustc_wasm } from "../../../lib/src/get_rustc_wasm";
3 | import { WASIFarmAnimal } from "@oligami/browser_wasi_shim-threads";
4 | import type { Ctx } from "../ctx";
5 |
6 | import thread_spawn_path from "./thread_spawn.ts?worker&url";
7 |
8 | let terminal: (string) => void;
9 | let compiler: WebAssembly.Module;
10 | const wasi_refs = [];
11 | let ctx: Ctx;
12 | let rustc_shared: SharedObject;
13 | let waiter: {
14 | rustc: () => Promise;
15 | end_rustc_fetch: () => Promise;
16 | };
17 |
18 | globalThis.addEventListener("message", async (event) => {
19 | if (event.data.ctx) {
20 | ctx = event.data.ctx;
21 | terminal = new SharedObjectRef(ctx.terminal_id).proxy<
22 | (string) => Promise
23 | >();
24 | await terminal("loading rustc\r\n");
25 | waiter = new SharedObjectRef(ctx.waiter_id).proxy<{
26 | rustc: () => Promise;
27 | end_rustc_fetch: () => Promise;
28 | }>();
29 | compiler = await get_rustc_wasm();
30 |
31 | await waiter.end_rustc_fetch();
32 | } else if (event.data.wasi_ref) {
33 | const { wasi_ref } = event.data;
34 |
35 | wasi_refs.push(wasi_ref);
36 |
37 | // wait for the compiler to load
38 | while (!compiler) {
39 | await new Promise((resolve) => setTimeout(resolve, 100));
40 | }
41 |
42 | await terminal("loaded rustc\r\n");
43 |
44 | while (wasi_refs.length === 1) {
45 | await new Promise((resolve) => setTimeout(resolve, 100));
46 | }
47 |
48 | await terminal("loaded wasi\r\n");
49 |
50 | const wasi = new WASIFarmAnimal(
51 | wasi_refs,
52 | [], // args
53 | ["RUST_MIN_STACK=16777216"], // env
54 | {
55 | // debug: true,
56 | can_thread_spawn: true,
57 | thread_spawn_worker_url: new URL(thread_spawn_path, import.meta.url)
58 | .href,
59 | thread_spawn_wasm: compiler,
60 | },
61 | );
62 |
63 | await wasi.wait_worker_background_worker();
64 |
65 | wasi.get_share_memory().grow(200);
66 |
67 | rustc_shared = new SharedObject((...args) => {
68 | try {
69 | wasi.args = ["rustc", ...args];
70 | console.log("wasi.start");
71 | wasi.block_start_on_thread();
72 | console.log("wasi.start done");
73 | } catch (e) {
74 | terminal(`${e.toString()}\r\n`);
75 | }
76 | }, ctx.rustc_id);
77 |
78 | waiter.rustc();
79 | } else if (event.data.wasi_ref_ui) {
80 | wasi_refs.push(event.data.wasi_ref_ui);
81 | }
82 | });
83 |
--------------------------------------------------------------------------------
/page/src/worker_process/thread_spawn.ts:
--------------------------------------------------------------------------------
1 | import { thread_spawn_on_worker } from "@oligami/browser_wasi_shim-threads";
2 |
3 | self.onmessage = async (event) => {
4 | await thread_spawn_on_worker(event.data);
5 | };
6 |
--------------------------------------------------------------------------------
/page/src/worker_process/util_cmd.ts:
--------------------------------------------------------------------------------
1 | import { SharedObject, SharedObjectRef } from "@oligami/shared-object";
2 | import { WASIFarmAnimal } from "@oligami/browser_wasi_shim-threads";
3 | import type { Ctx } from "../ctx";
4 | import lsr from "../wasm/lsr.wasm?url";
5 | import tre from "../wasm/tre.wasm?url";
6 | import { get_data } from "../cat";
7 |
8 | const shared: SharedObject[] = [];
9 |
10 | globalThis.addEventListener("message", async (event) => {
11 | const {
12 | wasi_refs,
13 | ctx,
14 | }: {
15 | // WASIFarmRefObject is not export
16 | // biome-ignore lint/suspicious/noExplicitAny:
17 | wasi_refs: any[];
18 | ctx: Ctx;
19 | } = event.data;
20 |
21 | console.log("loading lsr and tre");
22 |
23 | const terminal = new SharedObjectRef(ctx.terminal_id).proxy<
24 | (string) => Promise
25 | >();
26 | const waiter = new SharedObjectRef(ctx.waiter_id).proxy<{
27 | set_end_of_exec: (_end_of_exec: boolean) => Promise;
28 | }>();
29 | const download_by_url = new SharedObjectRef(ctx.download_by_url_id).proxy<
30 | (url: string, name: string) => Promise
31 | >();
32 |
33 | const ls_wasm = await WebAssembly.compile(
34 | await (await fetch(lsr)).arrayBuffer(),
35 | );
36 |
37 | const ls_wasi = new WASIFarmAnimal(
38 | wasi_refs,
39 | [], // args
40 | [], // env
41 | );
42 |
43 | const ls_inst = (await WebAssembly.instantiate(ls_wasm, {
44 | wasi_snapshot_preview1: ls_wasi.wasiImport,
45 | })) as unknown as {
46 | exports: { memory: WebAssembly.Memory; _start: () => unknown };
47 | };
48 |
49 | const ls_memory_reset = ls_inst.exports.memory.buffer;
50 | const ls_memory_reset_view = new Uint8Array(ls_memory_reset).slice();
51 |
52 | shared.push(
53 | new SharedObject((...args) => {
54 | // If I don't reset memory, I get some kind of error.
55 | const memory_view = new Uint8Array(ls_inst.exports.memory.buffer);
56 | memory_view.set(ls_memory_reset_view);
57 | ls_wasi.args = ["lsr", ...args];
58 | // biome-ignore lint/suspicious/noExplicitAny:
59 | ls_wasi.start(ls_inst as any);
60 | }, ctx.ls_id),
61 | );
62 |
63 | const tree_wasm = await WebAssembly.compile(
64 | await (await fetch(tre)).arrayBuffer(),
65 | );
66 |
67 | const tree_wasi = new WASIFarmAnimal(
68 | wasi_refs,
69 | [], // args
70 | [], // env
71 | );
72 |
73 | const tree_inst = (await WebAssembly.instantiate(tree_wasm, {
74 | wasi_snapshot_preview1: tree_wasi.wasiImport,
75 | })) as unknown as {
76 | exports: { memory: WebAssembly.Memory; _start: () => unknown };
77 | };
78 |
79 | console.log("tree_inst", tree_inst);
80 |
81 | const tree_memory_reset = tree_inst.exports.memory.buffer;
82 | const tree_memory_reset_view = new Uint8Array(tree_memory_reset).slice();
83 |
84 | shared.push(
85 | new SharedObject((...args) => {
86 | // If I don't reset memory, I get some kind of error.
87 | tree_wasi.args = ["tre", ...args];
88 | const memory_view = new Uint8Array(tree_inst.exports.memory.buffer);
89 | memory_view.set(tree_memory_reset_view);
90 | // biome-ignore lint/suspicious/noExplicitAny:
91 | tree_wasi.start(tree_inst as any);
92 | }, ctx.tree_id),
93 | );
94 |
95 | console.log("lsr_inst", ls_inst);
96 |
97 | console.log("lsr and tre loaded");
98 |
99 | const animal = new WASIFarmAnimal(
100 | wasi_refs,
101 | [], // args
102 | [], // env
103 | );
104 |
105 | shared.push(
106 | new SharedObject((...args) => {
107 | (async (args: string[]) => {
108 | const exec_file = args[0];
109 | const exec_args = args.slice(1);
110 | try {
111 | const file = get_data(exec_file, animal);
112 | const compiled_wasm = await WebAssembly.compile(file);
113 | const inst = (await WebAssembly.instantiate(compiled_wasm, {
114 | wasi_snapshot_preview1: animal.wasiImport,
115 | })) as unknown as {
116 | exports: { memory: WebAssembly.Memory; _start: () => unknown };
117 | };
118 | animal.args = [exec_file, ...exec_args];
119 | // biome-ignore lint/suspicious/noExplicitAny:
120 | animal.start(inst as any);
121 | } catch (e) {
122 | terminal(`Error: ${e}\r\n`);
123 | }
124 | waiter.set_end_of_exec(true);
125 | })(args);
126 | }, ctx.exec_file_id),
127 | );
128 |
129 | shared.push(
130 | new SharedObject((file) => {
131 | (async (file) => {
132 | console.log("exec_file", file);
133 | try {
134 | const file_data = get_data(file, animal);
135 | const blob = new Blob([file_data]);
136 | const url = URL.createObjectURL(blob);
137 | await download_by_url(url, file.split("/").pop());
138 | URL.revokeObjectURL(url);
139 | } catch (e) {
140 | terminal(`Error: ${e}\r\n`);
141 | }
142 | waiter.set_end_of_exec(true);
143 | })(file);
144 | }, ctx.download_id),
145 | );
146 | });
147 |
--------------------------------------------------------------------------------
/page/src/worker_process/worker.ts:
--------------------------------------------------------------------------------
1 | import { SharedObject, SharedObjectRef } from "@oligami/shared-object";
2 | import {
3 | get_default_sysroot_wasi_farm,
4 | load_additional_sysroot,
5 | } from "../../../lib/src/sysroot";
6 | import type { Ctx } from "../ctx";
7 |
8 | let terminal: (string) => Promise;
9 | let rustc_worker: Worker;
10 | let ctx: Ctx;
11 | import RustcWorker from "./rustc?worker";
12 | let shared: SharedObject;
13 |
14 | const wasi_refs = [];
15 |
16 | globalThis.addEventListener("message", async (event) => {
17 | if (event.data.ctx) {
18 | rustc_worker = new RustcWorker();
19 | ctx = event.data.ctx;
20 | rustc_worker.postMessage({ ctx });
21 |
22 | terminal = new SharedObjectRef(ctx.terminal_id).proxy<
23 | (string) => Promise
24 | >();
25 |
26 | await terminal("loading sysroot\r\n");
27 |
28 | const farm = await get_default_sysroot_wasi_farm();
29 |
30 | await terminal("loaded sysroot\r\n");
31 |
32 | const wasi_ref = farm.get_ref();
33 |
34 | rustc_worker.postMessage({ wasi_ref });
35 |
36 | shared = new SharedObject((triple) => {
37 | (async () => {
38 | terminal(`loading sysroot ${triple}\r\n`);
39 | await load_additional_sysroot(triple);
40 | terminal(`loaded sysroot ${triple}\r\n`);
41 | })();
42 | }, ctx.load_additional_sysroot_id);
43 |
44 | wasi_refs.push(wasi_ref);
45 | if (wasi_refs.length === 2) {
46 | setup_util_worker(wasi_refs, ctx);
47 | }
48 | } else if (event.data.wasi_ref) {
49 | const { wasi_ref } = event.data;
50 |
51 | rustc_worker.postMessage({ wasi_ref_ui: wasi_ref });
52 | wasi_refs.push(wasi_ref);
53 | if (wasi_refs.length === 2) {
54 | setup_util_worker(wasi_refs, ctx);
55 | }
56 | }
57 | });
58 |
59 | import util_cmd_worker from "./util_cmd?worker";
60 | import run_llvm_worker from "./llvm?worker";
61 |
62 | const setup_util_worker = (
63 | // biome-ignore lint/suspicious/noExplicitAny:
64 | wasi_refs: any[],
65 | ctx: Ctx,
66 | ) => {
67 | const util_worker = new util_cmd_worker();
68 | const llvm_worker = new run_llvm_worker();
69 |
70 | util_worker.postMessage({
71 | wasi_refs,
72 | ctx,
73 | });
74 |
75 | llvm_worker.postMessage({
76 | wasi_refs,
77 | ctx,
78 | });
79 | };
80 |
--------------------------------------------------------------------------------
/page/src/xterm.tsx:
--------------------------------------------------------------------------------
1 | import { SharedObject, SharedObjectRef } from "@oligami/shared-object";
2 | import { FitAddon } from "@xterm/addon-fit";
3 | import type { Terminal } from "@xterm/xterm";
4 | import XTerm from "./solid_xterm";
5 | import { WASIFarm, type WASIFarmRef } from "@oligami/browser_wasi_shim-threads";
6 | import {
7 | Directory,
8 | Fd,
9 | type Inode,
10 | PreopenDirectory,
11 | File,
12 | } from "@bjorn3/browser_wasi_shim";
13 | import type { Ctx } from "./ctx";
14 | import { rust_file } from "./config";
15 |
16 | let shared_xterm: SharedObject;
17 |
18 | let error_buff = "";
19 | let out_buff = "";
20 |
21 | export const SetupMyTerminal = (props: {
22 | ctx: Ctx;
23 | callback: (wasi_ref: WASIFarmRef) => void;
24 | }) => {
25 | let xterm: Terminal | undefined = undefined;
26 |
27 | const fit_addon = new FitAddon();
28 |
29 | const terminal_queue = [];
30 | const write_terminal = (str: string) => {
31 | if (xterm) {
32 | xterm.write(str);
33 | } else {
34 | terminal_queue.push(str);
35 | }
36 | };
37 | write_terminal.reset_err_buff = () => {
38 | error_buff = "";
39 | };
40 | write_terminal.get_err_buff = () => {
41 | console.log("called get_err_buff");
42 | return error_buff;
43 | };
44 | write_terminal.get_out_buff = () => {
45 | console.log("called get_out_buff");
46 | return out_buff;
47 | };
48 | write_terminal.reset_out_buff = () => {
49 | out_buff = "";
50 | };
51 | shared_xterm = new SharedObject(write_terminal, props.ctx.terminal_id);
52 |
53 | const handleMount = (terminal: Terminal) => {
54 | xterm = terminal;
55 | xterm.write(terminal_queue.join(""));
56 | terminal_queue.length = 0;
57 | get_ref(terminal, props.callback);
58 | fit_addon.fit();
59 |
60 | return () => {
61 | console.log("Terminal unmounted.");
62 | };
63 | };
64 |
65 | let keys = "";
66 |
67 | const waiter = new SharedObjectRef(props.ctx.waiter_id).proxy<{
68 | is_all_done: () => boolean;
69 | }>();
70 | let cmd_parser: (...string) => void;
71 |
72 | let before_cmd = "";
73 | const on_enter = async (terminal) => {
74 | if (keys) {
75 | before_cmd = keys;
76 | }
77 | terminal.write("\r\n");
78 | if (await waiter.is_all_done()) {
79 | cmd_parser = new SharedObjectRef(props.ctx.cmd_parser_id).proxy<
80 | (...string) => void
81 | >();
82 | const parsed = keys.split(" ");
83 | await cmd_parser(...parsed);
84 | } else {
85 | terminal.write("this is not done yet\r\n");
86 | }
87 | keys = "";
88 | };
89 | const keydown = (
90 | event: { key: string; domEvent: KeyboardEvent },
91 | terminal,
92 | ) => {
93 | // console.log(event);
94 |
95 | if (event.key === "\r") {
96 | terminal.write("\r\n");
97 | on_enter(terminal);
98 | } else if (event.domEvent.code === "Backspace") {
99 | terminal.write("\b \b");
100 | keys = keys.slice(0, -1);
101 | } else if (event.domEvent.code === "ArrowUp") {
102 | keys = before_cmd;
103 | terminal.write(`\r>${keys}`);
104 | } else if (
105 | // Ctrl + V
106 | event.domEvent.ctrlKey &&
107 | event.domEvent.code === "KeyV"
108 | ) {
109 | navigator.clipboard.readText().then((text) => {
110 | keys += text;
111 | terminal.write(text);
112 | });
113 | }
114 | };
115 | const onData = (data: string) => {
116 | // if Backspace, ArrowUp: do nothing
117 | if (data === "\x7F" || data === "\u001b[A" || data === "\r") {
118 | return;
119 | }
120 |
121 | keys += data;
122 |
123 | if (xterm) {
124 | xterm.write(data);
125 | } else {
126 | terminal_queue.push(data);
127 | }
128 | // console.log(`data received: ${data}`);
129 | };
130 |
131 | // You can pass either an ITerminalAddon constructor or an instance, depending on whether you need to access it later.
132 | return (
133 |
140 | );
141 | };
142 |
143 | const get_ref = (term, callback) => {
144 | class XtermStdio extends Fd {
145 | term: Terminal;
146 |
147 | constructor(term: Terminal) {
148 | super();
149 | this.term = term;
150 | }
151 | fd_write(data: Uint8Array) /*: {ret: number, nwritten: number}*/ {
152 | const decoded = new TextDecoder().decode(data);
153 | // \n to \r\n
154 | const fixed = decoded.replace(/\n/g, "\r\n");
155 | this.term.write(fixed);
156 |
157 | out_buff += fixed;
158 |
159 | return { ret: 0, nwritten: data.byteLength };
160 | }
161 | fd_seek() {
162 | // wasi.ERRNO_BADF 8
163 | return { ret: 8, offset: 0n };
164 | }
165 | fd_filestat_get() {
166 | // wasi.ERRNO_BADF 8
167 | return { ret: 8, filestat: null };
168 | }
169 | }
170 |
171 | class XtermStderr extends Fd {
172 | term: Terminal;
173 |
174 | constructor(term: Terminal) {
175 | super();
176 | this.term = term;
177 | }
178 | fd_seek() {
179 | // wasi.ERRNO_BADF 8
180 | return { ret: 8, offset: 0n };
181 | }
182 | fd_write(data: Uint8Array) /*: {ret: number, nwritten: number}*/ {
183 | const decoded = new TextDecoder().decode(data);
184 | // \n to \r\n
185 | const fixed = decoded.replace(/\n/g, "\r\n");
186 | // ansi colors
187 | this.term.write(`\x1b[31m${fixed}\x1b[0m`);
188 |
189 | error_buff += fixed;
190 |
191 | return { ret: 0, nwritten: data.byteLength };
192 | }
193 | fd_filestat_get() {
194 | // wasi.ERRNO_BADF 8
195 | return { ret: 8, filestat: null };
196 | }
197 | }
198 |
199 | const toMap = (arr: Array<[string, Inode]>) => {
200 | const map = new Map();
201 | for (const [key, value] of arr) {
202 | map.set(key, value);
203 | }
204 | return map;
205 | };
206 |
207 | const root_dir = new PreopenDirectory(
208 | "/",
209 | toMap([
210 | ["sysroot", new Directory([])],
211 | ["main.rs", rust_file],
212 | ]),
213 | );
214 |
215 | const farm = new WASIFarm(
216 | new XtermStdio(term),
217 | new XtermStdio(term),
218 | new XtermStderr(term),
219 | [root_dir],
220 | );
221 |
222 | callback(farm.get_ref());
223 | };
224 |
--------------------------------------------------------------------------------
/page/tailwind.config.ts:
--------------------------------------------------------------------------------
1 | import type { Config } from "tailwindcss";
2 |
3 | const config: Config = {
4 | content: [
5 | "./index.html",
6 | "./src/**/*.{js,ts,jsx,tsx,css,md,mdx,html,json,scss}",
7 | ],
8 | darkMode: "class",
9 | theme: {
10 | extend: {},
11 | },
12 | plugins: [],
13 | };
14 |
15 | export default config;
16 |
--------------------------------------------------------------------------------
/page/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "ESNext",
4 | "module": "ESNext",
5 | "moduleResolution": "node",
6 | "allowSyntheticDefaultImports": true,
7 | "esModuleInterop": true,
8 | "jsx": "preserve",
9 | "jsxImportSource": "solid-js",
10 | "types": ["vite/client"],
11 | "noEmit": true,
12 | "isolatedModules": true
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/page/vite.config.ts:
--------------------------------------------------------------------------------
1 | import { defineConfig } from "vite";
2 | import solidPlugin from "vite-plugin-solid";
3 | // import devtools from 'solid-devtools/vite';
4 |
5 | export default defineConfig({
6 | plugins: [
7 | /*
8 | Uncomment the following line to enable solid-devtools.
9 | For more info see https://github.com/thetarnav/solid-devtools/tree/main/packages/extension#readme
10 | */
11 | // devtools(),
12 | solidPlugin(),
13 | ],
14 | server: {
15 | port: 3000,
16 | headers: {
17 | "Cross-Origin-Embedder-Policy": "require-corp",
18 | "Cross-Origin-Opener-Policy": "same-origin",
19 | },
20 | },
21 | build: {
22 | target: "esnext",
23 | },
24 | base: "./",
25 | });
26 |
--------------------------------------------------------------------------------