diff --git a/docs/cli-reference.md b/docs/cli-reference.md index e17a371..24302f9 100644 --- a/docs/cli-reference.md +++ b/docs/cli-reference.md @@ -706,6 +706,87 @@ fabricks mortar exec -e DEBUG=1 api ./debug-script.sh These commands inspect services managed by the daemon. Services are created declaratively via `fabricks mortar up`, but these commands let you inspect their state. +### `fabricks service run` + +Run a WASM module via the daemon (resolves and stores in OCI registry if needed). + +**Usage:** +```bash +fabricks service run [OPTIONS] +``` + +**Arguments:** +- `` - Path to Fabrickfile directory or OCI image tag + +**Options:** +``` + -p, --port Publish ports (can be used multiple times) + -e, --env Set environment variables (can be used multiple times) + --env-file Read environment from file + -v, --volume Mount volumes (can be used multiple times) + --name Assign a name to the instance + --network Connect to network + --rm Automatically remove when stopped + -d, --detach Run in background + --restart Restart policy [default: no] [possible: no, on-failure, always] +``` + +**How it Works:** + +1. **Resolution Phase:** + - If `` is a directory path, checks for Fabrickfile + - If Fabrickfile exists and module not built, builds it + - If `` is an OCI tag, pulls from registry if not cached + - Stores/verifies module in local OCI storage + +2. **Execution Phase:** + - Daemon loads module from OCI storage + - For interpreted runtimes (JS/Python), extracts source layer to temp directory + - Mounts source files at `/app` via WASI preopens + - Executes WASM module with specified capabilities and configuration + +**Examples:** +```bash +# Run from Fabrickfile directory (builds if needed) +fabricks service run ./examples/nodejs-hello + +# Run by OCI tag +fabricks service run nodejs-hello:1.0.0 + +# Run with port mapping +fabricks service run -p 8080:8089 ./examples/nodejs-hello + +# Run with environment variables +fabricks service run -e LOG_LEVEL=debug nodejs-hello:1.0.0 + +# Run in background with auto-restart +fabricks service run -d --restart on-failure nodejs-hello:1.0.0 +``` + +**Output:** +``` +Resolving nodejs-hello... +✓ Found Fabrickfile at ./examples/nodejs-hello +✓ Building module (not found in OCI storage) + [1/3] Resolving runtime: javascript:20 + [2/3] Packaging source files + [3/3] Storing in OCI registry as nodejs-hello:1.0.0 +✓ Stored: sha256:abc123... + +Starting service via daemon... +✓ Service ID: srv_xyz789 +✓ Network: listen on 0.0.0.0:8089 +✓ Health check passed +Service running: nodejs-hello (nodejs-hello:1.0.0) +``` + +**Notes:** +- This command always runs through the daemon (unlike `fabricks run` which can run standalone) +- Supports interpreted runtimes (JavaScript, Python) via multi-layer OCI images +- For build failures, see `fabricks build --help` for troubleshooting + +--- + ### `fabricks service ls` List all running services (across all mortar compositions). diff --git a/docs/daemon-api-reference.md b/docs/daemon-api-reference.md index f1960fa..9f6b683 100644 --- a/docs/daemon-api-reference.md +++ b/docs/daemon-api-reference.md @@ -573,6 +573,55 @@ curl --unix-socket /var/run/fabricks.sock \ --- +### Run Module + +**Endpoint:** `POST /v1/services/run-module` + +Runs a module from OCI storage by tag or reference. This is the primary endpoint for running services - it loads the module from local OCI storage, handles multi-layer modules (runtime + source layers for interpreted runtimes), creates a service, and starts it. + +**Request Body:** +```json +{ + "reference": "hello-http:0.1.0", + "args": [], + "env_vars": [], + "no_capabilities": false +} +``` + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `reference` | string | Yes | Module tag (e.g., "my-module:1.0.0") | +| `args` | array | No | Command-line arguments to pass to the module | +| `env_vars` | array | No | Environment variable overrides as `[key, value]` tuples | +| `no_capabilities` | boolean | No | Disable capability enforcement (default: false) | + +**Response:** +```json +{ + "status": "success", + "data": { + "id": "svc-a1b2c3d4", + "name": "hello-http" + } +} +``` + +**cURL Example:** +```bash +curl --unix-socket /var/run/fabricks.sock \ + -X POST \ + -H "Content-Type: application/json" \ + -d '{"reference":"hello-http:0.1.0"}' \ + http://localhost/v1/services/run-module +``` + +**Notes:** +- For interpreted runtimes (Python, JavaScript), the module's runtime layer is used as the WASM binary and source layers are extracted to `/app` +- The module must exist in local OCI storage (use CLI to build and store first) + +--- + ### List Services **Endpoint:** `GET /v1/services` diff --git a/docs/fabrickfile-mortar-reference.md b/docs/fabrickfile-mortar-reference.md index 7076cbd..5a6e842 100644 --- a/docs/fabrickfile-mortar-reference.md +++ b/docs/fabrickfile-mortar-reference.md @@ -492,14 +492,45 @@ Specifies source code location and files to include. ### `[runtime]` (Optional) -Required for interpreted languages (JavaScript, Python). +Required for interpreted languages (JavaScript, Python) that don't compile directly to WASM. The runtime section specifies a base WASM runtime that will load and execute your source code at runtime. + +**When to Use:** +- JavaScript/TypeScript applications (uses SpiderMonkey via jco componentize) +- Python applications (uses CPython via componentize-py) +- Any other interpreted language with a WASM runtime + +**How It Works:** + +For interpreted languages, Fabricks uses a **multi-layer OCI approach**: +- **Layer 0:** Runtime WASM module (e.g., `nodejs-runtime.wasm`, `python-runtime.wasm`) +- **Layer 1+:** User source code files (packaged as tar.gz) + +At runtime: +1. Daemon loads the runtime WASM from Layer 0 +2. Extracts source files from Layer 1+ to temporary directory +3. Mounts source at `/app` via WASI filesystem preopens +4. Runtime reads entrypoint from `/app/.fabricks.toml` +5. Runtime loads and executes your code #### `image` (Required if using runtime) - **Type:** String -- **Examples:** `image = "wasm://node:20"`, `image = "wasm://python:3.11"` +- **Format:** OCI image reference to the runtime +- **Examples:** + - `image = "nodejs-runtime:1.0.0"` - Local tag + - `image = "fabricks.dev/runtimes/javascript:20"` - Registry reference + - `image = "fabricks.dev/runtimes/python:3.12"` - Python runtime + +#### `handler` (Required if using runtime) +- **Type:** String +- **Format:** `"file:function"` - Specifies the entrypoint for your application +- **Examples:** + - `handler = "app.js:handler"` - JavaScript function handler + - `handler = "app:handler"` - Python function handler (imports `handler` from `app.py`) + - `handler = "server:main"` - Alternative entrypoint #### `config` (Optional) - **Type:** Table +- **Purpose:** Runtime-specific configuration options - **Example:** ```toml [runtime.config] @@ -507,6 +538,44 @@ Required for interpreted languages (JavaScript, Python). stack_size = "2Mi" ``` +**Complete Examples:** + +JavaScript/Node.js Runtime: +```toml +[info] +name = "my-api" +version = "1.0.0" +type = "http" + +[runtime] +image = "nodejs-runtime:1.0.0" +handler = "app.js:handler" + +[source] +path = "." + +[capabilities.network] +listen = [8080] +``` + +Python Runtime: +```toml +[info] +name = "my-python-service" +version = "1.0.0" +type = "http" + +[runtime] +image = "python-runtime:3.12" +handler = "app:handler" + +[source] +path = "." + +[capabilities.network] +listen = [8080] +``` + --- ### `[build]` (Required for source builds) @@ -845,7 +914,7 @@ listen = [6379] read_write = ["./data"] ``` -### Minimal Fabrickfile (JavaScript HTTP Service) +### Minimal Fabrickfile (JavaScript HTTP Service with Runtime) ```toml fabrick_version = "1.0" @@ -854,24 +923,40 @@ name = "api-gateway" version = "1.0.0" type = "http" # HTTP handler service -[from] -source = "javascript" - +# Use the JavaScript runtime - no compilation needed! [runtime] -image = "wasm://node:20" +image = "nodejs-runtime:1.0.0" +handler = "app.js:handler" [source] path = "." -[build] -command = "npm run build:wasm" -output = "dist/api.wasm" - [capabilities.network] listen = [3000] connect = ["backend:8080"] ``` +### Minimal Fabrickfile (Python HTTP Service with Runtime) +```toml +fabrick_version = "1.0" + +[info] +name = "python-api" +version = "1.0.0" +type = "http" # HTTP handler service + +# Use the Python runtime - no compilation needed! +[runtime] +image = "python-runtime:3.12" +handler = "app:handler" + +[source] +path = "." + +[capabilities.network] +listen = [8080] +``` + ### Minimal Fabrickfile (Command/CLI Tool) ```toml fabrick_version = "1.0" diff --git a/docs/interpreted-runtimes.md b/docs/interpreted-runtimes.md new file mode 100644 index 0000000..477d231 --- /dev/null +++ b/docs/interpreted-runtimes.md @@ -0,0 +1,825 @@ +# Interpreted Language Runtimes + +Documentation for using interpreted languages (JavaScript, Python) with Fabricks. + +--- + +## Table of Contents + +- [Overview](#overview) +- [How It Works](#how-it-works) +- [JavaScript/Node.js Runtime](#javascriptnodejs-runtime) +- [Python Runtime](#python-runtime) +- [Multi-Layer OCI Architecture](#multi-layer-oci-architecture) +- [Handler Interface](#handler-interface) +- [Building and Running](#building-and-running) +- [Examples](#examples) +- [Creating Custom Runtimes](#creating-custom-runtimes) +- [Limitations](#limitations) + +--- + +## Overview + +Fabricks supports interpreted languages through **pre-built WASM runtimes** that load and execute your source code at runtime. This means you can write JavaScript or Python without needing to understand WebAssembly or compilation toolchains. + +**Supported Runtimes:** + +- **JavaScript/Node.js** - Uses SpiderMonkey engine via `jco componentize` +- **Python** - Uses CPython interpreter via `componentize-py` + +**Key Benefits:** + +- No compilation step for your source code +- Familiar development experience +- Fast iteration (no rebuild needed) +- Standard language features and syntax + +--- + +## How It Works + +Fabricks uses a **multi-layer OCI image approach** for interpreted runtimes: + +``` +┌─────────────────────────────────────────────────────────────┐ +│ OCI Image (2+ layers) │ +├─────────────────────────────────────────────────────────────┤ +│ Layer 0: Runtime WASM Module │ +│ - Pre-compiled language runtime (SpiderMonkey/CPython) │ +│ - WASI HTTP handler framework │ +│ - ~8-15MB (depending on runtime) │ +├─────────────────────────────────────────────────────────────┤ +│ Layer 1: User Source Files (tar.gz) │ +│ - Your .js or .py files │ +│ - .fabricks.toml (entrypoint configuration) │ +│ - Any other application files │ +│ - Typically < 1MB │ +└─────────────────────────────────────────────────────────────┘ +``` + +**At Runtime:** + +1. **Daemon loads module:** + - Daemon reads OCI manifest to identify runtime layer (Layer 0) + - Loads runtime WASM module into wasmtime + +2. **Source mounting:** + - Daemon extracts source files from Layer 1+ to temporary directory + - Mounts source directory at `/app` via WASI filesystem preopens + +3. **Execution:** + - Runtime WASM reads `/app/.fabricks.toml` to find entrypoint + - Runtime loads and executes your source code + - Daemon proxies HTTP requests to the runtime's WASI HTTP handler + +--- + +## JavaScript/Node.js Runtime + +### Overview + +The JavaScript runtime bundles the **SpiderMonkey** JavaScript engine (used by Firefox) into a WASM component that implements the `wasi:http/incoming-handler` interface. + +**Technology Stack:** +- **Engine:** SpiderMonkey (via StarlingMonkey) +- **Componentization:** `jco componentize` from Bytecode Alliance +- **Size:** ~13MB runtime WASM + +### Fabrickfile Configuration + +```toml +fabrick_version = "1.0" + +[info] +name = "my-js-app" +version = "1.0.0" +type = "http" + +[runtime] +image = "nodejs-runtime:1.0.0" # Runtime OCI image +handler = "app.js:handler" # Entrypoint: file:function + +[source] +path = "." # Directory containing your .js files + +[capabilities.network] +listen = [8080] +``` + +### Handler Interface + +Your JavaScript handler receives a request object and returns a response object: + +```javascript +// app.js +function handler(request) { + // Request format: + // { + // method: "GET", + // path: "/hello", + // query: { name: "world" }, + // headers: { "content-type": "application/json" }, + // url: "http://localhost:8080/hello?name=world" + // } + + // Response format: + return { + status: 200, + headers: { "content-type": "text/plain" }, + body: "Hello from JavaScript!" + }; +} +``` + +**Request Object Fields:** +- `method` (string) - HTTP method (GET, POST, etc.) +- `path` (string) - URL path +- `query` (object) - Query parameters as key-value pairs +- `headers` (object) - Request headers (lowercase keys) +- `url` (string) - Full request URL + +**Response Object Fields:** +- `status` (number) - HTTP status code +- `headers` (object) - Response headers +- `body` (string|object) - Response body (objects are JSON-stringified) + +### Complete Example + +```javascript +// app.js +function handler(request) { + const { method, path, query, headers } = request; + + // Route requests + if (path === '/') { + return { + status: 200, + headers: { 'content-type': 'text/plain' }, + body: 'Hello from JavaScript on Fabricks!' + }; + } + + if (path === '/greet') { + const name = query.name || 'World'; + return { + status: 200, + headers: { 'content-type': 'text/plain' }, + body: `Hello, ${name}!` + }; + } + + if (path === '/json') { + // Objects are automatically JSON-stringified + return { + status: 200, + headers: { 'content-type': 'application/json' }, + body: { + message: 'Hello from JavaScript!', + timestamp: Date.now() + } + }; + } + + // 404 for unknown paths + return { + status: 404, + headers: { 'content-type': 'text/plain' }, + body: `Not Found: ${method} ${path}` + }; +} +``` + +### Limitations + +- **No Node.js built-ins:** This is WASI, not Node.js. No `fs`, `http`, `crypto`, etc. +- **No ES modules:** Use function declarations, not `export` +- **No async/await:** Handlers must be synchronous (for now) +- **No dynamic imports:** All code must be in loaded files +- **No npm packages:** Pure JavaScript only (unless bundled) + +--- + +## Python Runtime + +### Overview + +The Python runtime bundles **CPython** (the standard Python interpreter) into a WASM component that implements `wasi:http/incoming-handler`. + +**Technology Stack:** +- **Interpreter:** CPython 3.12 +- **Componentization:** `componentize-py` from Bytecode Alliance +- **Size:** ~15MB runtime WASM + +### Fabrickfile Configuration + +```toml +fabrick_version = "1.0" + +[info] +name = "my-python-app" +version = "1.0.0" +type = "http" + +[runtime] +image = "python-runtime:3.12" # Runtime OCI image +handler = "app:handler" # Entrypoint: module:function + +[source] +path = "." # Directory containing your .py files + +[capabilities.network] +listen = [8080] +``` + +### Handler Interface + +Your Python handler receives a dict and returns a dict: + +```python +# app.py +def handler(request): + # Request format: + # { + # "method": "GET", + # "path": "/hello", + # "query": {"name": "world"}, + # "headers": {"content-type": "application/json"} + # } + + # Response format: + return { + "status": 200, + "headers": {"content-type": "text/plain"}, + "body": "Hello from Python!" + } +``` + +**Request Dict Fields:** +- `method` (str) - HTTP method +- `path` (str) - URL path +- `query` (dict) - Query parameters +- `headers` (dict) - Request headers (lowercase keys) + +**Response Dict Fields:** +- `status` (int) - HTTP status code +- `headers` (dict) - Response headers +- `body` (str) - Response body + +### Complete Example + +```python +# app.py +import json + +def handler(request): + method = request.get("method", "GET") + path = request.get("path", "/") + query = request.get("query", {}) + headers = request.get("headers", {}) + + # Route requests + if path == "/" or path == "": + return { + "status": 200, + "headers": {"content-type": "text/plain"}, + "body": "Hello from Python on Fabricks!" + } + + if path == "/greet": + name = query.get("name", "World") + return { + "status": 200, + "headers": {"content-type": "text/plain"}, + "body": f"Hello, {name}!" + } + + if path == "/json": + data = { + "message": "Hello from Python!", + "service": "python-hello", + "version": "1.0.0" + } + return { + "status": 200, + "headers": {"content-type": "application/json"}, + "body": json.dumps(data) + } + + # 404 for unknown paths + return { + "status": 404, + "headers": {"content-type": "text/plain"}, + "body": f"Not Found: {method} {path}" + } +``` + +### Limitations + +- **Pure Python only:** No C extensions or binary modules +- **Limited stdlib:** Only WASI-compatible standard library modules +- **No async:** Handlers must be synchronous +- **No pip packages:** Pure Python packages only (unless bundled) + +--- + +## Multi-Layer OCI Architecture + +### Why Multi-Layer? + +Traditional WASM builds compile everything into a single `.wasm` file. For interpreted languages, this would mean: +- Recompiling the entire runtime + your code every time you change anything +- Large builds (runtime is 8-15MB) +- Slow iteration + +The multi-layer approach separates: +1. **Runtime layer** (Layer 0) - Built once, reused +2. **Source layer** (Layer 1+) - Rebuilt on every change, tiny size + +### Layer Structure + +``` +OCI Manifest: +{ + "layers": [ + { + "mediaType": "application/vnd.oci.image.layer.v1.tar+gzip", + "digest": "sha256:abc123...", + "size": 13420532, + "annotations": { + "org.opencontainers.image.title": "nodejs-runtime.wasm", + "dev.fabricks.layer.type": "runtime" + } + }, + { + "mediaType": "application/vnd.oci.image.layer.v1.tar+gzip", + "digest": "sha256:def456...", + "size": 2048, + "annotations": { + "org.opencontainers.image.title": "source.tar.gz", + "dev.fabricks.layer.type": "source" + } + } + ] +} +``` + +### Building Multi-Layer Images + +When you run `fabricks build` on a project with a `[runtime]` section: + +1. **Runtime Resolution:** + - Checks if `runtime.image` exists in local OCI storage + - If not found, pulls from registry + - Validates runtime implements required interfaces + +2. **Source Packaging:** + - Packages all files from `[source].path` into tar.gz + - Creates `.fabricks.toml` with entrypoint configuration + - Compresses source layer + +3. **Manifest Creation:** + - Creates OCI manifest with 2+ layers + - Layer 0: Runtime WASM (from runtime.image) + - Layer 1: Source files (newly created) + - Stores in local OCI storage with tag + +### Running Multi-Layer Images + +When you run `fabricks service run` or `fabricks mortar up`: + +1. **Daemon loads manifest:** + - Reads OCI manifest from storage + - Identifies runtime layer (Layer 0) + +2. **Runtime extraction:** + - Loads runtime WASM into memory + - Prepares wasmtime instance + +3. **Source mounting:** + - Extracts source layer(s) to temporary directory + - Sets up WASI preopens: `/app -> /tmp/fabricks/srv_xyz/app` + - Runtime can now read files from `/app` + +4. **Execution:** + - Runtime reads `/app/.fabricks.toml` + - Loads handler from specified entrypoint + - Starts HTTP handler loop + +--- + +## Handler Interface + +### Common Patterns + +#### Routing + +```javascript +// JavaScript +function handler(request) { + const routes = { + '/': homeHandler, + '/users': usersHandler, + '/api/status': statusHandler + }; + + const handler = routes[request.path] || notFoundHandler; + return handler(request); +} +``` + +```python +# Python +def handler(request): + path = request.get("path", "/") + + routes = { + "/": home_handler, + "/users": users_handler, + "/api/status": status_handler + } + + route_handler = routes.get(path, not_found_handler) + return route_handler(request) +``` + +#### JSON Responses + +```javascript +// JavaScript - objects are auto-stringified +function handler(request) { + return { + status: 200, + headers: { 'content-type': 'application/json' }, + body: { message: 'Success', data: [...] } + }; +} +``` + +```python +# Python - must stringify manually +import json + +def handler(request): + return { + "status": 200, + "headers": {"content-type": "application/json"}, + "body": json.dumps({"message": "Success", "data": [...]}) + } +``` + +#### Error Handling + +```javascript +// JavaScript +function handler(request) { + try { + // ... your logic + return { status: 200, body: 'OK' }; + } catch (error) { + return { + status: 500, + headers: { 'content-type': 'text/plain' }, + body: `Error: ${error.message}` + }; + } +} +``` + +```python +# Python +def handler(request): + try: + # ... your logic + return {"status": 200, "body": "OK"} + except Exception as e: + return { + "status": 500, + "headers": {"content-type": "text/plain"}, + "body": f"Error: {str(e)}" + } +``` + +--- + +## Building and Running + +### Quick Start + +1. **Create your handler:** + +```javascript +// app.js +function handler(request) { + return { + status: 200, + headers: { 'content-type': 'text/plain' }, + body: 'Hello from JavaScript!' + }; +} +``` + +2. **Create Fabrickfile:** + +```toml +fabrick_version = "1.0" + +[info] +name = "my-app" +version = "1.0.0" +type = "http" + +[runtime] +image = "nodejs-runtime:1.0.0" +handler = "app.js:handler" + +[source] +path = "." + +[capabilities.network] +listen = [8080] +``` + +3. **Build and run:** + +```bash +# Build (packages source, stores in OCI) +fabricks build + +# Run via daemon +fabricks service run my-app:1.0.0 + +# Or run directly (if daemon not needed) +fabricks run . +``` + +### Development Workflow + +```bash +# Start daemon in background +fabricksd & + +# Initial build +fabricks build + +# Run service +fabricks service run my-app:1.0.0 -p 8080:8080 + +# Make changes to app.js or app.py... + +# Rebuild (only source layer changes, fast!) +fabricks build + +# Restart service (daemon reloads source automatically) +fabricks service restart my-app + +# Or rebuild and restart in one command +fabricks service run my-app:1.0.0 --force-recreate +``` + +### Using fabricks-mortar.toml + +```toml +mortar_version = "1.0" + +[project] +name = "my-app" + +[service.api] +build = "./api" # Directory with Fabrickfile +networks = ["public"] + +environment = { + LOG_LEVEL = "debug" +} + +[service.api.replicas] +min = 2 +max = 10 + +[network.public] +ingress = "0.0.0.0/0" +``` + +```bash +# Build all services +fabricks mortar build + +# Start all services +fabricks mortar up + +# Scale up +fabricks mortar scale api=5 +``` + +--- + +## Examples + +### Example Projects + +The Fabricks repository includes example projects: + +#### Node.js Examples + +- **`examples/runtimes/nodejs/`** - The Node.js runtime itself (for maintainers) +- **`examples/nodejs-hello/`** - Simple Hello World HTTP service + - Demonstrates basic request handling + - Shows routing and query parameter handling + - JSON response examples + +#### Python Examples + +- **`examples/runtimes/python/`** - The Python runtime itself (for maintainers) +- **`examples/python-hello/`** - Simple Hello World HTTP service + - Demonstrates basic request handling + - Shows routing and query parameter handling + - JSON response examples + +### Running Examples + +```bash +# Node.js example +cd examples/nodejs-hello +fabricks build +fabricks service run nodejs-hello:1.0.0 -p 8089:8089 + +# Test it +curl http://localhost:8089/ +curl http://localhost:8089/greet?name=Fabricks +curl http://localhost:8089/json + +# Python example +cd examples/python-hello +fabricks build +fabricks service run python-hello:1.0.0 -p 8088:8088 + +# Test it +curl http://localhost:8088/ +curl http://localhost:8088/greet?name=Fabricks +curl http://localhost:8088/json +``` + +--- + +## Creating Custom Runtimes + +### For Advanced Users and Maintainers + +You can create custom runtimes with additional features or different languages. + +#### JavaScript Runtime (using jco) + +```bash +# 1. Install jco +npm install -g @bytecodealliance/jco + +# 2. Create handler.js that implements wasi:http/incoming-handler +# See examples/runtimes/nodejs/src/handler.js + +# 3. Create WIT definition +# See examples/runtimes/nodejs/wit/world.wit + +# 4. Componentize +jco componentize src/handler.js \ + --wit wit/world.wit \ + --world-name http-handler \ + --out runtime.wasm + +# 5. Tag and publish +fabricks tag runtime.wasm mycompany/my-js-runtime:1.0.0 +fabricks push mycompany/my-js-runtime:1.0.0 +``` + +#### Python Runtime (using componentize-py) + +```bash +# 1. Install componentize-py +pip install componentize-py + +# 2. Fetch WIT dependencies +wkg wit fetch + +# 3. Create handler.py that implements wasi:http/incoming-handler +# See examples/runtimes/python/src/handler.py + +# 4. Componentize +componentize-py \ + -d wit \ + -w wasi:http/proxy@0.2.0 \ + componentize src/handler \ + -o runtime.wasm + +# 5. Tag and publish +fabricks tag runtime.wasm mycompany/my-python-runtime:3.12 +fabricks push mycompany/my-python-runtime:3.12 +``` + +### Custom Runtime Requirements + +Your runtime must: + +1. **Implement WASI HTTP interfaces:** + - `wasi:http/incoming-handler` for HTTP services + - OR `wasi:cli/run` for command-line tools + +2. **Read entrypoint from `/app/.fabricks.toml`:** + ```toml + entrypoint = "app:handler" # module:function format + ``` + +3. **Load and execute user code from `/app`:** + - Add `/app` to module search path + - Import the specified module + - Call the specified function for each request + +4. **Bridge request/response:** + - Convert WASI HTTP request to simple dict/object + - Convert dict/object response to WASI HTTP response + +--- + +## Limitations + +### Current Limitations + +#### JavaScript Runtime + +- **No Node.js APIs:** No `fs`, `http`, `crypto`, `process`, etc. +- **No npm packages:** Cannot import third-party packages (unless bundled) +- **No ES modules:** Must use function declarations +- **Synchronous only:** No `async`/`await` support yet +- **No WebAssembly import:** Cannot import other WASM modules + +#### Python Runtime + +- **No C extensions:** Pure Python only +- **Limited stdlib:** WASI-compatible modules only +- **No pip packages:** Cannot install from PyPI (unless pure Python and bundled) +- **Synchronous only:** No `asyncio` support yet + +#### General Limitations + +- **HTTP only:** Currently only supports HTTP services (type = "http") +- **Single handler:** One entrypoint function per service +- **No state persistence:** No built-in database or persistence (use external services) +- **Limited debugging:** No debugger support in WASI environment + +### Workarounds + +#### Using External Dependencies + +Bundle dependencies into your source code: + +```bash +# For JavaScript - use a bundler +npm install +npm run bundle # outputs single file with dependencies + +# For Python - vendor dependencies +pip install -t ./vendor requests +# Then import from vendor directory +``` + +#### Database Access + +Use network capabilities to connect to external databases: + +```toml +[capabilities.network] +listen = [8080] +connect = ["postgres:5432", "redis:6379"] +``` + +```javascript +// Use HTTP to access databases via network +// Or use WASI sockets when available +``` + +--- + +## Summary + +Interpreted runtimes in Fabricks provide: + +- **Easy onboarding:** Write code in familiar languages +- **Fast iteration:** No compilation step for source changes +- **Multi-layer efficiency:** Runtime layer cached, only source layer rebuilds +- **Standard interfaces:** Same handler pattern across languages +- **Production-ready:** Same security, networking, and orchestration as compiled WASM + +Start building with interpreted runtimes: + +```bash +# Check out the examples +cd examples/nodejs-hello +fabricks build && fabricks service run nodejs-hello:1.0.0 -p 8089:8089 + +cd examples/python-hello +fabricks build && fabricks service run python-hello:1.0.0 -p 8088:8088 +``` + +For more information: +- [Fabrickfile Reference](fabrickfile-mortar-reference.md) +- [CLI Reference](cli-reference.md) +- [Examples](../examples/) diff --git a/examples/nodejs-hello/Fabrickfile b/examples/nodejs-hello/Fabrickfile new file mode 100644 index 0000000..47a2ba1 --- /dev/null +++ b/examples/nodejs-hello/Fabrickfile @@ -0,0 +1,26 @@ +# Node.js Hello World +# +# A simple JavaScript HTTP service - no WASM knowledge required! +# +# Just write JavaScript and run: fabricks build && fabricks run + +fabrick_version = "1.0" + +[info] +name = "nodejs-hello" +version = "1.0.0" +type = "http" +description = "A simple JavaScript HTTP service" + +# Use the JavaScript runtime - Fabricks handles everything else +[from] +source = "javascript" +version = "20" + +# Point to your JavaScript code +[source] +path = "." +entrypoint = "app:handler" + +[capabilities.network] +listen = [8089] diff --git a/examples/nodejs-hello/README.md b/examples/nodejs-hello/README.md new file mode 100644 index 0000000..15f1a1b --- /dev/null +++ b/examples/nodejs-hello/README.md @@ -0,0 +1,87 @@ +# Node.js Hello World + +A simple JavaScript HTTP service running on Fabricks. No WebAssembly knowledge required! + +## Quick Start + +```bash +# Build the service +fabricks build + +# Run it +fabricks run nodejs-hello:1.0.0 + +# Test it +curl http://localhost:8088/ +curl http://localhost:8088/health +curl http://localhost:8088/greet?name=Developer +curl http://localhost:8088/json +``` + +## How It Works + +1. Write your JavaScript handler in `app.js` +2. Configure your service in `Fabrickfile` +3. Run `fabricks build` - Fabricks packages your code with the JavaScript runtime +4. Run `fabricks run` - Your service is now running! + +## Handler Interface + +Your handler receives a request object and returns a response object: + +```javascript +function handler(request) { + // request contains: + // - method: "GET", "POST", etc. + // - path: "/hello" + // - query: { name: "world" } + // - headers: { "content-type": "..." } + + return { + status: 200, + headers: { "content-type": "text/plain" }, + body: "Hello!" + }; +} +``` + +## Endpoints + +This example provides: + +| Endpoint | Description | +|----------|-------------| +| `GET /` | Returns "Hello from JavaScript on Fabricks!" | +| `GET /health` | Health check endpoint | +| `GET /greet?name=X` | Returns personalized greeting | +| `GET /json` | Returns JSON response | + +## Files + +- `app.js` - Your JavaScript handler code +- `Fabrickfile` - Service configuration + +## Requirements + +No local toolchain required! Fabricks handles everything. + +The JavaScript runtime uses SpiderMonkey (Firefox's JS engine) compiled to WebAssembly. + +## Multi-Layer OCI Architecture + +This example uses Fabricks' **multi-layer OCI approach** for interpreted runtimes: + +- **Layer 0:** Pre-built Node.js runtime WASM (~13MB) - SpiderMonkey engine + WASI HTTP framework +- **Layer 1:** Your source code (app.js) - packaged as tar.gz, mounted at `/app` at runtime + +Benefits: +- Runtime built once, reused across all JS projects +- Only source layer rebuilds when you change code (fast iteration) +- No compilation step for your JavaScript! + +## Learn More + +- **[Interpreted Runtimes Documentation](../../docs/interpreted-runtimes.md)** - Complete guide to JS/Python runtimes +- **[Fabrickfile Reference](../../docs/fabrickfile-mortar-reference.md#runtime-optional)** - Runtime configuration options +- **[CLI Reference](../../docs/cli-reference.md#fabricks-service-run)** - Running services via daemon +- **[JavaScript Runtime Details](../runtimes/nodejs/)** - Runtime implementation (for maintainers) diff --git a/examples/nodejs-hello/app.js b/examples/nodejs-hello/app.js new file mode 100644 index 0000000..5adea3f --- /dev/null +++ b/examples/nodejs-hello/app.js @@ -0,0 +1,59 @@ +/** + * Node.js Hello World HTTP Handler + * + * This is a simple HTTP handler that demonstrates JavaScript on Fabricks. + * Just write regular JavaScript - no WASM knowledge required! + */ + +function handler(request) { + const path = request.path || '/'; + const method = request.method || 'GET'; + + // Route requests + if (path === '/' || path === '') { + return { + status: 200, + headers: { 'content-type': 'text/plain' }, + body: 'Hello from JavaScript on Fabricks!', + }; + } + + if (path === '/health') { + return { + status: 200, + headers: { 'content-type': 'text/plain' }, + body: 'OK', + }; + } + + if (path === '/greet') { + // Get name from query string + const query = request.query || {}; + const name = query.name || 'World'; + return { + status: 200, + headers: { 'content-type': 'text/plain' }, + body: `Hello, ${name}!`, + }; + } + + if (path === '/json') { + const data = { + message: 'Hello from JavaScript!', + service: 'nodejs-hello', + version: '1.0.0', + }; + return { + status: 200, + headers: { 'content-type': 'application/json' }, + body: JSON.stringify(data), + }; + } + + // 404 for unknown paths + return { + status: 404, + headers: { 'content-type': 'text/plain' }, + body: `Not Found: ${method} ${path}`, + }; +} diff --git a/examples/python-hello/README.md b/examples/python-hello/README.md index f6284ea..ec2008e 100644 --- a/examples/python-hello/README.md +++ b/examples/python-hello/README.md @@ -159,4 +159,23 @@ fabricks network ls # Remove service from network fabricks network leave public python-hello -``` \ No newline at end of file +``` + +## Multi-Layer OCI Architecture + +This example uses Fabricks' **multi-layer OCI approach** for interpreted runtimes: + +- **Layer 0:** Pre-built Python runtime WASM (~15MB) - CPython 3.12 + WASI HTTP framework +- **Layer 1:** Your source code (app.py) - packaged as tar.gz, mounted at `/app` at runtime + +Benefits: +- Runtime built once, reused across all Python projects +- Only source layer rebuilds when you change code (fast iteration) +- No compilation step for your Python code! + +## Learn More + +- **[Interpreted Runtimes Documentation](../../docs/interpreted-runtimes.md)** - Complete guide to JS/Python runtimes +- **[Fabrickfile Reference](../../docs/fabrickfile-mortar-reference.md#runtime-optional)** - Runtime configuration options +- **[CLI Reference](../../docs/cli-reference.md#fabricks-service-run)** - Running services via daemon +- **[Python Runtime Details](../runtimes/python/)** - Runtime implementation (for maintainers) \ No newline at end of file diff --git a/examples/runtimes/nodejs/Fabrickfile b/examples/runtimes/nodejs/Fabrickfile new file mode 100644 index 0000000..90cd10b --- /dev/null +++ b/examples/runtimes/nodejs/Fabrickfile @@ -0,0 +1,39 @@ +# Node.js Runtime Base Image +# +# This is an official Fabricks runtime image for JavaScript applications. +# End users don't build this - they use it via [from].source = "javascript" +# +# To build this runtime (requires jco): +# fabricks build examples/runtimes/nodejs --tag fabricks.dev/runtimes/javascript:20 +# +# To use this runtime (no toolchain required): +# [from] +# source = "javascript" +# version = "20" + +fabrick_version = "1.0" + +[info] +name = "nodejs-runtime" +version = "20.0.0" +type = "http" +description = "Node.js 20 HTTP runtime for Fabricks - bundles SpiderMonkey JavaScript engine with WASI HTTP support" +authors = ["Fabricks Team"] +license = "Apache-2.0" + +# Note: This runtime is built using jco componentize +# The build command below handles everything + +[build] +# The Node.js runtime uses jco componentize to bundle SpiderMonkey + our HTTP framework +# This requires jco to be installed: npm install -g @bytecodealliance/jco +command = """ +npx jco componentize src/handler.js \ + --wit wit \ + --world-name http-handler \ + --out nodejs-runtime.wasm +""" +output = "nodejs-runtime.wasm" + +[capabilities.network] +listen = [8080] diff --git a/examples/runtimes/nodejs/README.md b/examples/runtimes/nodejs/README.md new file mode 100644 index 0000000..a48c0a9 --- /dev/null +++ b/examples/runtimes/nodejs/README.md @@ -0,0 +1,134 @@ +# Fabricks Node.js Runtime + +This is the official Fabricks runtime for JavaScript applications. It bundles the SpiderMonkey JavaScript engine (via StarlingMonkey) with a WASI HTTP handler framework. + +## Overview + +The Node.js runtime allows users to write simple JavaScript HTTP handlers without needing to understand WebAssembly or the Component Model. The runtime: + +1. Implements `wasi:http/incoming-handler` for HTTP request handling +2. Loads user JavaScript code from `/app` at runtime via WASI filesystem preopens +3. Bridges HTTP requests to a simple function-based interface + +## User Experience + +End users don't build this runtime directly. Instead, they use it via their Fabrickfile: + +```toml +[from] +source = "javascript" +version = "20" + +[source] +path = "." +entrypoint = "app:handler" +``` + +Then write a simple handler function: + +```javascript +// app.js +function handler(request) { + return { + status: 200, + headers: { "content-type": "text/plain" }, + body: "Hello from JavaScript!" + }; +} +``` + +## Handler Interface + +### Request Object + +The handler receives a request object with: + +```javascript +{ + method: "GET", // HTTP method + path: "/hello", // URL path + query: { name: "world" }, // Query parameters as object + headers: { ... }, // Request headers (lowercase keys) + url: "http://..." // Full URL +} +``` + +### Response Object + +The handler returns a response object: + +```javascript +{ + status: 200, // HTTP status code + headers: { "content-type": "text/plain" }, // Response headers + body: "Hello!" // Response body (string or object) +} +``` + +If body is an object, it will be JSON-stringified automatically. + +## Building the Runtime + +This is for Fabricks maintainers only. End users don't need to do this. + +### Requirements + +- Node.js 18+ +- jco: `npm install -g @bytecodealliance/jco` + +### Build Command + +```bash +# From the repository root +fabricks build examples/runtimes/nodejs --tag fabricks.dev/runtimes/javascript:20 + +# Or manually with jco +cd examples/runtimes/nodejs +npx jco componentize src/handler.js \ + --wit wit/world.wit \ + --world-name http-handler \ + --out nodejs-runtime.wasm +``` + +## Architecture + +``` +┌─────────────────────────────────────────────────────────────┐ +│ OCI Image (2 layers) │ +├─────────────────────────────────────────────────────────────┤ +│ Layer 0: nodejs-runtime.wasm │ +│ - SpiderMonkey JS engine │ +│ - WASI HTTP handler framework │ +│ - ~8MB (SpiderMonkey embedding) │ +├─────────────────────────────────────────────────────────────┤ +│ Layer 1: User source files (tar.gz) │ +│ - app.js (user's handler) │ +│ - .fabricks.toml (entrypoint config) │ +│ - Any other JS files │ +└─────────────────────────────────────────────────────────────┘ + +At runtime: +1. wasmtime loads nodejs-runtime.wasm +2. User files are mounted at /app via WASI preopens +3. Runtime reads /app/.fabricks.toml for entrypoint +4. Runtime evaluates user's JavaScript code +5. HTTP requests are routed to user's handler function +``` + +## Files + +- `src/handler.js` - WASI HTTP handler that loads user code from /app +- `wit/world.wit` - WIT interface definition (wasi:http/proxy) +- `Fabrickfile` - Build configuration for the runtime + +## Limitations + +- User code uses `function` declarations (not ES module exports) +- Dynamic imports are not supported +- Node.js built-in modules are not available (this is WASI, not Node.js) +- Async handlers are not yet supported + +## Related + +- [Python Runtime](../python/) - Similar runtime for Python +- [nodejs-hello Example](../../nodejs-hello/) - User-facing example diff --git a/examples/runtimes/nodejs/nodejs-runtime.wasm b/examples/runtimes/nodejs/nodejs-runtime.wasm new file mode 100644 index 0000000..6b9666e Binary files /dev/null and b/examples/runtimes/nodejs/nodejs-runtime.wasm differ diff --git a/examples/runtimes/nodejs/package-lock.json b/examples/runtimes/nodejs/package-lock.json new file mode 100644 index 0000000..8f54232 --- /dev/null +++ b/examples/runtimes/nodejs/package-lock.json @@ -0,0 +1,911 @@ +{ + "name": "nodejs", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "nodejs", + "version": "1.0.0", + "license": "ISC", + "dependencies": { + "@bytecodealliance/componentize-js": "^0.19.3", + "@bytecodealliance/jco": "^1.15.4" + } + }, + "node_modules/@bytecodealliance/componentize-js": { + "version": "0.19.3", + "resolved": "https://registry.npmjs.org/@bytecodealliance/componentize-js/-/componentize-js-0.19.3.tgz", + "integrity": "sha512-ju7Y4WeF0B9uMkSPHJgmT6ouEfSwbe9M1uR/YOnYZjBpxJjH9qzxIkJg/kf8NycVDyFJ2/lscmJ1E1uPiDQVRQ==", + "workspaces": [ + "." + ], + "dependencies": { + "@bytecodealliance/jco": "^1.15.1", + "@bytecodealliance/wizer": "^10.0.0", + "es-module-lexer": "^1.6.0", + "oxc-parser": "^0.76.0" + }, + "bin": { + "componentize-js": "src/cli.js" + } + }, + "node_modules/@bytecodealliance/jco": { + "version": "1.15.4", + "resolved": "https://registry.npmjs.org/@bytecodealliance/jco/-/jco-1.15.4.tgz", + "integrity": "sha512-Lu5ZVPD+ethKrOCbfVe7zHwOH8JmhU1sDXBn/7WN4Zf/KcJc34YQmUQCjxrN5n4h9Q2ep3dcK8Gb7DeV1e2i7w==", + "license": "(Apache-2.0 WITH LLVM-exception)", + "dependencies": { + "@bytecodealliance/componentize-js": "^0.19.3", + "@bytecodealliance/preview2-shim": "^0.17.3", + "binaryen": "^123.0.0", + "commander": "^14", + "mkdirp": "^3", + "ora": "^8", + "terser": "^5" + }, + "bin": { + "jco": "src/jco.js" + } + }, + "node_modules/@bytecodealliance/preview2-shim": { + "version": "0.17.6", + "resolved": "https://registry.npmjs.org/@bytecodealliance/preview2-shim/-/preview2-shim-0.17.6.tgz", + "integrity": "sha512-n3cM88gTen5980UOBAD6xDcNNL3ocTK8keab21bpx1ONdA+ARj7uD1qoFxOWCyKlkpSi195FH+GeAut7Oc6zZw==", + "license": "(Apache-2.0 WITH LLVM-exception)" + }, + "node_modules/@bytecodealliance/wizer": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/@bytecodealliance/wizer/-/wizer-10.0.0.tgz", + "integrity": "sha512-ziWmovyu1jQl9TsKlfC2bwuUZwxVPFHlX4fOqTzxhgS76jITIo45nzODEwPgU+jjmOr8F3YX2V2wAChC5NKujg==", + "license": "Apache-2.0", + "bin": { + "wizer": "wizer.js" + }, + "engines": { + "node": ">=16" + }, + "optionalDependencies": { + "@bytecodealliance/wizer-darwin-arm64": "10.0.0", + "@bytecodealliance/wizer-darwin-x64": "10.0.0", + "@bytecodealliance/wizer-linux-arm64": "10.0.0", + "@bytecodealliance/wizer-linux-s390x": "10.0.0", + "@bytecodealliance/wizer-linux-x64": "10.0.0", + "@bytecodealliance/wizer-win32-x64": "10.0.0" + } + }, + "node_modules/@bytecodealliance/wizer-darwin-arm64": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/@bytecodealliance/wizer-darwin-arm64/-/wizer-darwin-arm64-10.0.0.tgz", + "integrity": "sha512-dhZTWel+xccGTKSJtI9A7oM4yyP20FWflsT+AoqkOqkCY7kCNrj4tmMtZ6GXZFRDkrPY5+EnOh62sfShEibAMA==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "bin": { + "wizer-darwin-arm64": "wizer" + } + }, + "node_modules/@bytecodealliance/wizer-darwin-x64": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/@bytecodealliance/wizer-darwin-x64/-/wizer-darwin-x64-10.0.0.tgz", + "integrity": "sha512-r/LUIZw6Q3Hf4htd46mD+EBxfwjBkxVIrTM1r+B2pTCddoBYQnKVdVsI4UFyy7NoBxzEg8F8BwmTNoSLmFRjpw==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "bin": { + "wizer-darwin-x64": "wizer" + } + }, + "node_modules/@bytecodealliance/wizer-linux-arm64": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/@bytecodealliance/wizer-linux-arm64/-/wizer-linux-arm64-10.0.0.tgz", + "integrity": "sha512-pGSfFWXzeTqHm6z1PtVaEn+7Fm3QGC8YnHrzBV4sQDVS3N1NwmuHZAc8kslmlFPNdu61ycEvdOsSgCny8JPQvg==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "bin": { + "wizer-linux-arm64": "wizer" + } + }, + "node_modules/@bytecodealliance/wizer-linux-s390x": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/@bytecodealliance/wizer-linux-s390x/-/wizer-linux-s390x-10.0.0.tgz", + "integrity": "sha512-O8vHxRTAdb1lUnVXMIMTcp/9q4pq1D4iIKigJCipg2JN15taV9uFAWh0fO88wylXwuSlO7dOE1AwQl54fMKXQg==", + "cpu": [ + "s390x" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "bin": { + "wizer-linux-s390x": "wizer" + } + }, + "node_modules/@bytecodealliance/wizer-linux-x64": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/@bytecodealliance/wizer-linux-x64/-/wizer-linux-x64-10.0.0.tgz", + "integrity": "sha512-fJtM1sy43FBMnp+xpapFX6U1YdTBKA/1T4CYfG/qeE8jn0SXk2EuiYoY/EnC2uyNy9hjTrvfdYO5n4MXW0EIdQ==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "bin": { + "wizer-linux-x64": "wizer" + } + }, + "node_modules/@bytecodealliance/wizer-win32-x64": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/@bytecodealliance/wizer-win32-x64/-/wizer-win32-x64-10.0.0.tgz", + "integrity": "sha512-55BPLfGT7iT7gH5M69NpTM16QknJZ7OxJ0z73VOEoeGA9CT8QPKMRzFKsPIvLs+W8G28fdudFA94nElrdkp3Kg==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "win32" + ], + "bin": { + "wizer-win32-x64": "wizer" + } + }, + "node_modules/@emnapi/core": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.8.1.tgz", + "integrity": "sha512-AvT9QFpxK0Zd8J0jopedNm+w/2fIzvtPKPjqyw9jwvBaReTTqPBk9Hixaz7KbjimP+QNz605/XnjFcDAL2pqBg==", + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.1.0", + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.8.1.tgz", + "integrity": "sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/wasi-threads": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.1.0.tgz", + "integrity": "sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/source-map": { + "version": "0.3.11", + "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.11.tgz", + "integrity": "sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==", + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@napi-rs/wasm-runtime": { + "version": "0.2.12", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.12.tgz", + "integrity": "sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.4.3", + "@emnapi/runtime": "^1.4.3", + "@tybys/wasm-util": "^0.10.0" + } + }, + "node_modules/@oxc-parser/binding-android-arm64": { + "version": "0.76.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-android-arm64/-/binding-android-arm64-0.76.0.tgz", + "integrity": "sha512-1XJW/16CDmF5bHE7LAyPPmEEVnxSadDgdJz+xiLqBrmC4lfAeuAfRw3HlOygcPGr+AJsbD4Z5sFJMkwjbSZlQg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@oxc-parser/binding-darwin-arm64": { + "version": "0.76.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-darwin-arm64/-/binding-darwin-arm64-0.76.0.tgz", + "integrity": "sha512-yoQwSom8xsB+JdGsPUU0xxmxLKiF2kdlrK7I56WtGKZilixuBf/TmOwNYJYLRWkBoW5l2/pDZOhBm2luwmLiLw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@oxc-parser/binding-darwin-x64": { + "version": "0.76.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-darwin-x64/-/binding-darwin-x64-0.76.0.tgz", + "integrity": "sha512-uRIopPLvr3pf2Xj7f5LKyCuqzIU6zOS+zEIR8UDYhcgJyZHnvBkfrYnfcztyIcrGdQehrFUi3uplmI09E7RdiQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@oxc-parser/binding-freebsd-x64": { + "version": "0.76.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-freebsd-x64/-/binding-freebsd-x64-0.76.0.tgz", + "integrity": "sha512-a0EOFvnOd2FqmDSvH6uWLROSlU6KV/JDKbsYDA/zRLyKcG6HCsmFnPsp8iV7/xr9WMbNgyJi6R5IMpePQlUq7Q==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@oxc-parser/binding-linux-arm-gnueabihf": { + "version": "0.76.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-0.76.0.tgz", + "integrity": "sha512-ikRYDHL3fOdZwfJKmcdqjlLgkeNZ3Ez0qM8wAev5zlHZ+lY/Ig7qG5SCqPlvuTu+nNQ6zrFFaKvvt69EBKXU/g==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@oxc-parser/binding-linux-arm-musleabihf": { + "version": "0.76.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-arm-musleabihf/-/binding-linux-arm-musleabihf-0.76.0.tgz", + "integrity": "sha512-dtRv5J5MRCLR7x39K8ufIIW4svIc7gYFUaI0YFXmmeOBhK/K2t/CkguPnDroKtsmXIPHDRtmJ1JJYzNcgJl6Wg==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@oxc-parser/binding-linux-arm64-gnu": { + "version": "0.76.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-0.76.0.tgz", + "integrity": "sha512-IE4iiiggFH2snagQxHrY5bv6dDpRMMat+vdlMN/ibonA65eOmRLp8VLTXnDiNrcla/itJ1L9qGABHNKU+SnE8g==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@oxc-parser/binding-linux-arm64-musl": { + "version": "0.76.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-arm64-musl/-/binding-linux-arm64-musl-0.76.0.tgz", + "integrity": "sha512-wi9zQPMDHrBuRuT7Iurfidc9qlZh7cKa5vfYzOWNBCaqJdgxmNOFzvYen02wVUxSWGKhpiPHxrPX0jdRyJ8Npg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@oxc-parser/binding-linux-riscv64-gnu": { + "version": "0.76.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-riscv64-gnu/-/binding-linux-riscv64-gnu-0.76.0.tgz", + "integrity": "sha512-0tqqu1pqPee2lLGY8vtYlX1L415fFn89e0a3yp4q5N9f03j1rRs0R31qesTm3bt/UK8HYjECZ+56FCVPs2MEMQ==", + "cpu": [ + "riscv64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@oxc-parser/binding-linux-s390x-gnu": { + "version": "0.76.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-0.76.0.tgz", + "integrity": "sha512-y36Hh1a5TA+oIGtlc8lT7N9vdHXBlhBetQJW0p457KbiVQ7jF7AZkaPWhESkjHWAsTVKD2OjCa9ZqfaqhSI0FQ==", + "cpu": [ + "s390x" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@oxc-parser/binding-linux-x64-gnu": { + "version": "0.76.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-x64-gnu/-/binding-linux-x64-gnu-0.76.0.tgz", + "integrity": "sha512-7/acaG9htovp3gp/J0kHgbItQTuHctl+rbqPPqZ9DRBYTz8iV8kv3QN8t8Or8i/hOmOjfZp9McDoSU1duoR4/A==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@oxc-parser/binding-linux-x64-musl": { + "version": "0.76.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-x64-musl/-/binding-linux-x64-musl-0.76.0.tgz", + "integrity": "sha512-AxFt0reY6Q2rfudABmMTFGR8tFFr58NlH2rRBQgcj+F+iEwgJ+jMwAPhXd2y1I2zaI8GspuahedUYQinqxWqjA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@oxc-parser/binding-wasm32-wasi": { + "version": "0.76.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-wasm32-wasi/-/binding-wasm32-wasi-0.76.0.tgz", + "integrity": "sha512-wHdkHdhf6AWBoO8vs5cpoR6zEFY1rB+fXWtq6j/xb9j/lu1evlujRVMkh8IM/M/pOUIrNkna3nzST/mRImiveQ==", + "cpu": [ + "wasm32" + ], + "license": "MIT", + "optional": true, + "dependencies": { + "@napi-rs/wasm-runtime": "^0.2.11" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@oxc-parser/binding-win32-arm64-msvc": { + "version": "0.76.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-0.76.0.tgz", + "integrity": "sha512-G7ZlEWcb2hNwCK3qalzqJoyB6HaTigQ/GEa7CU8sAJ/WwMdG/NnPqiC9IqpEAEy1ARSo4XMALfKbKNuqbSs5mg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@oxc-parser/binding-win32-x64-msvc": { + "version": "0.76.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-win32-x64-msvc/-/binding-win32-x64-msvc-0.76.0.tgz", + "integrity": "sha512-0jLzzmnu8/mqNhKBnNS2lFUbPEzRdj5ReiZwHGHpjma0+ullmmwP2AqSEqx3ssHDK9CpcEMdKOK2LsbCfhHKIA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@oxc-project/types": { + "version": "0.76.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.76.0.tgz", + "integrity": "sha512-CH3THIrSViKal8yV/Wh3FK0pFhp40nzW1MUDCik9fNuid2D/7JJXKJnfFOAvMxInGXDlvmgT6ACAzrl47TqzkQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Boshen" + } + }, + "node_modules/@tybys/wasm-util": { + "version": "0.10.1", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz", + "integrity": "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/acorn": { + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/binaryen": { + "version": "123.0.0", + "resolved": "https://registry.npmjs.org/binaryen/-/binaryen-123.0.0.tgz", + "integrity": "sha512-/hls/a309aZCc0itqP6uhoR+5DsKSlJVfB8Opd2BY9Ndghs84IScTunlyidyF4r2Xe3lQttnfBNIDjaNpj6mTw==", + "license": "Apache-2.0", + "bin": { + "wasm-as": "bin/wasm-as", + "wasm-ctor-eval": "bin/wasm-ctor-eval", + "wasm-dis": "bin/wasm-dis", + "wasm-merge": "bin/wasm-merge", + "wasm-metadce": "bin/wasm-metadce", + "wasm-opt": "bin/wasm-opt", + "wasm-reduce": "bin/wasm-reduce", + "wasm-shell": "bin/wasm-shell", + "wasm2js": "bin/wasm2js" + } + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "license": "MIT" + }, + "node_modules/chalk": { + "version": "5.6.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", + "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", + "license": "MIT", + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/cli-cursor": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-5.0.0.tgz", + "integrity": "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==", + "license": "MIT", + "dependencies": { + "restore-cursor": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-spinners": { + "version": "2.9.2", + "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.9.2.tgz", + "integrity": "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==", + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/commander": { + "version": "14.0.2", + "resolved": "https://registry.npmjs.org/commander/-/commander-14.0.2.tgz", + "integrity": "sha512-TywoWNNRbhoD0BXs1P3ZEScW8W5iKrnbithIl0YH+uCmBd0QpPOA8yc82DS3BIE5Ma6FnBVUsJ7wVUDz4dvOWQ==", + "license": "MIT", + "engines": { + "node": ">=20" + } + }, + "node_modules/emoji-regex": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz", + "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==", + "license": "MIT" + }, + "node_modules/es-module-lexer": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", + "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", + "license": "MIT" + }, + "node_modules/get-east-asian-width": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.4.0.tgz", + "integrity": "sha512-QZjmEOC+IT1uk6Rx0sX22V6uHWVwbdbxf1faPqJ1QhLdGgsRGCZoyaQBm/piRdJy/D2um6hM1UP7ZEeQ4EkP+Q==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-interactive": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-2.0.0.tgz", + "integrity": "sha512-qP1vozQRI+BMOPcjFzrjXuQvdak2pHNUMZoeG2eRbiSqyvbEf/wQtEOTOX1guk6E3t36RkaqiSt8A/6YElNxLQ==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-unicode-supported": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-2.1.0.tgz", + "integrity": "sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-symbols": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-6.0.0.tgz", + "integrity": "sha512-i24m8rpwhmPIS4zscNzK6MSEhk0DUWa/8iYQWxhffV8jkI4Phvs3F+quL5xvS0gdQR0FyTCMMH33Y78dDTzzIw==", + "license": "MIT", + "dependencies": { + "chalk": "^5.3.0", + "is-unicode-supported": "^1.3.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-symbols/node_modules/is-unicode-supported": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-1.3.0.tgz", + "integrity": "sha512-43r2mRvz+8JRIKnWJ+3j8JtjRKZ6GmjzfaE/qiBJnikNnYv/6bagRJ1kUhNk8R5EX/GkobD+r+sfxCPJsiKBLQ==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mimic-function": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/mimic-function/-/mimic-function-5.0.1.tgz", + "integrity": "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mkdirp": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-3.0.1.tgz", + "integrity": "sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg==", + "license": "MIT", + "bin": { + "mkdirp": "dist/cjs/src/bin.js" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/onetime": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-7.0.0.tgz", + "integrity": "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==", + "license": "MIT", + "dependencies": { + "mimic-function": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ora": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/ora/-/ora-8.2.0.tgz", + "integrity": "sha512-weP+BZ8MVNnlCm8c0Qdc1WSWq4Qn7I+9CJGm7Qali6g44e/PUzbjNqJX5NJ9ljlNMosfJvg1fKEGILklK9cwnw==", + "license": "MIT", + "dependencies": { + "chalk": "^5.3.0", + "cli-cursor": "^5.0.0", + "cli-spinners": "^2.9.2", + "is-interactive": "^2.0.0", + "is-unicode-supported": "^2.0.0", + "log-symbols": "^6.0.0", + "stdin-discarder": "^0.2.2", + "string-width": "^7.2.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/oxc-parser": { + "version": "0.76.0", + "resolved": "https://registry.npmjs.org/oxc-parser/-/oxc-parser-0.76.0.tgz", + "integrity": "sha512-l98B2e9evuhES7zN99rb1QGhbzx25829TJFaKi2j0ib3/K/G5z1FdGYz6HZkrU3U8jdH7v2FC8mX1j2l9JrOUg==", + "license": "MIT", + "dependencies": { + "@oxc-project/types": "^0.76.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/Boshen" + }, + "optionalDependencies": { + "@oxc-parser/binding-android-arm64": "0.76.0", + "@oxc-parser/binding-darwin-arm64": "0.76.0", + "@oxc-parser/binding-darwin-x64": "0.76.0", + "@oxc-parser/binding-freebsd-x64": "0.76.0", + "@oxc-parser/binding-linux-arm-gnueabihf": "0.76.0", + "@oxc-parser/binding-linux-arm-musleabihf": "0.76.0", + "@oxc-parser/binding-linux-arm64-gnu": "0.76.0", + "@oxc-parser/binding-linux-arm64-musl": "0.76.0", + "@oxc-parser/binding-linux-riscv64-gnu": "0.76.0", + "@oxc-parser/binding-linux-s390x-gnu": "0.76.0", + "@oxc-parser/binding-linux-x64-gnu": "0.76.0", + "@oxc-parser/binding-linux-x64-musl": "0.76.0", + "@oxc-parser/binding-wasm32-wasi": "0.76.0", + "@oxc-parser/binding-win32-arm64-msvc": "0.76.0", + "@oxc-parser/binding-win32-x64-msvc": "0.76.0" + } + }, + "node_modules/restore-cursor": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-5.1.0.tgz", + "integrity": "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==", + "license": "MIT", + "dependencies": { + "onetime": "^7.0.0", + "signal-exit": "^4.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/stdin-discarder": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/stdin-discarder/-/stdin-discarder-0.2.2.tgz", + "integrity": "sha512-UhDfHmA92YAlNnCfhmq0VeNL5bDbiZGg7sZ2IvPsXubGkiNa9EC+tUTsjBRsYUAz87btI6/1wf4XoVvQ3uRnmQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/strip-ansi": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", + "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/terser": { + "version": "5.46.0", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.46.0.tgz", + "integrity": "sha512-jTwoImyr/QbOWFFso3YoU3ik0jBBDJ6JTOQiy/J2YxVJdZCc+5u7skhNwiOR3FQIygFqVUPHl7qbbxtjW2K3Qg==", + "license": "BSD-2-Clause", + "dependencies": { + "@jridgewell/source-map": "^0.3.3", + "acorn": "^8.15.0", + "commander": "^2.20.0", + "source-map-support": "~0.5.20" + }, + "bin": { + "terser": "bin/terser" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/terser/node_modules/commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "license": "MIT" + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD", + "optional": true + } + } +} diff --git a/examples/runtimes/nodejs/package.json b/examples/runtimes/nodejs/package.json new file mode 100644 index 0000000..bb3a3be --- /dev/null +++ b/examples/runtimes/nodejs/package.json @@ -0,0 +1,17 @@ +{ + "name": "nodejs", + "version": "1.0.0", + "description": "This is the official Fabricks runtime for JavaScript applications. It bundles the SpiderMonkey JavaScript engine (via StarlingMonkey) with a WASI HTTP handler framework.", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "keywords": [], + "author": "", + "license": "ISC", + "type": "commonjs", + "dependencies": { + "@bytecodealliance/componentize-js": "^0.19.3", + "@bytecodealliance/jco": "^1.15.4" + } +} diff --git a/examples/runtimes/nodejs/src/handler.js b/examples/runtimes/nodejs/src/handler.js new file mode 100644 index 0000000..04bc625 --- /dev/null +++ b/examples/runtimes/nodejs/src/handler.js @@ -0,0 +1,463 @@ +/** + * Fabricks Node.js Runtime - WASI HTTP Handler + * + * This runtime implements the wasi:http/incoming-handler interface and loads + * user JavaScript code from /app at runtime via WASI filesystem preopens. + */ + +// Import WASI HTTP types +import { + Fields, + OutgoingResponse, + OutgoingBody, + ResponseOutparam, +} from 'wasi:http/types@0.2.0'; + +// Import WASI filesystem types for loading user code +import { getDirectories } from 'wasi:filesystem/preopens@0.2.0'; +import { Descriptor } from 'wasi:filesystem/types@0.2.0'; + +// Configuration +const APP_DIR = '/app'; +const DEFAULT_MODULE = 'app'; +const DEFAULT_HANDLER = 'handler'; + +// Debug info collected during handler loading +let debugInfo = []; + +// User handler - loaded lazily on first request +let userHandler = null; +let handlerLoaded = false; + +/** + * Find the /app directory descriptor from preopens. + */ +function findAppDirectory() { + try { + const directories = getDirectories(); + debugInfo.push(`Found ${directories.length} preopened directories`); + for (const [descriptor, path] of directories) { + debugInfo.push(` Preopen: "${path}"`); + if (path === APP_DIR || path === APP_DIR + '/') { + debugInfo.push(` -> Matched /app!`); + return descriptor; + } + } + debugInfo.push(`No /app directory found in preopens`); + } catch (e) { + debugInfo.push(`Error getting directories: ${e}`); + debugInfo.push(`Error stack: ${e.stack || 'N/A'}`); + } + return null; +} + +/** + * Read a file from a descriptor. + */ +function readFile(baseDescriptor, filePath) { + debugInfo.push(`readFile: trying to open "${filePath}"`); + + // Open the file for reading + let result; + try { + result = baseDescriptor.openAt( + { symlinkFollow: true }, // path-flags + filePath, // relative path + {}, // open-flags (none needed for read) + { read: true } // descriptor-flags + ); + } catch (e) { + debugInfo.push(`readFile: openAt threw: ${e}`); + return null; + } + + debugInfo.push(`readFile: openAt returned tag=${result?.tag}, val type=${typeof result?.val}`); + + // Check for error result + if (result === null || result === undefined) { + debugInfo.push(`readFile: openAt returned null/undefined`); + return null; + } + + // Handle result type (could be {tag:'ok', val:...} or {tag:'err', val:...}) + let fileDescriptor; + if (result.tag === 'err') { + debugInfo.push(`readFile: openAt error: ${result.val}`); + return null; + } else if (result.tag === 'ok') { + fileDescriptor = result.val; + } else { + // Maybe it returns the descriptor directly? + fileDescriptor = result; + } + + if (!fileDescriptor) { + debugInfo.push(`readFile: no fileDescriptor after openAt`); + return null; + } + + debugInfo.push(`readFile: got fileDescriptor, calling stat...`); + + // Get file size via stat + let statResult; + try { + statResult = fileDescriptor.stat(); + } catch (e) { + debugInfo.push(`readFile: stat threw: ${e}`); + return null; + } + + debugInfo.push(`readFile: stat returned tag=${statResult?.tag}`); + + let fileSize; + if (statResult.tag === 'err') { + debugInfo.push(`readFile: stat error`); + return null; + } else if (statResult.tag === 'ok') { + fileSize = statResult.val.size; + } else { + fileSize = statResult.size; + } + + debugInfo.push(`readFile: fileSize = ${fileSize}`); + + // Read entire file + let readResult; + try { + readResult = fileDescriptor.read(fileSize, BigInt(0)); + } catch (e) { + debugInfo.push(`readFile: read threw: ${e}`); + return null; + } + + debugInfo.push(`readFile: read returned`); + + let bytes; + if (readResult.tag === 'err') { + debugInfo.push(`readFile: read error`); + return null; + } else if (readResult.tag === 'ok') { + [bytes] = readResult.val; + } else if (Array.isArray(readResult)) { + [bytes] = readResult; + } else { + bytes = readResult; + } + + const text = new TextDecoder().decode(new Uint8Array(bytes)); + debugInfo.push(`readFile: successfully read ${text.length} chars`); + return text; +} + +/** + * Check if a file exists. + */ +function fileExists(baseDescriptor, filePath) { + debugInfo.push(`fileExists: checking "${filePath}"`); + try { + // In jco, WASI functions return the result directly when successful + // and throw exceptions on error + baseDescriptor.statAt( + { symlinkFollow: true }, + filePath + ); + debugInfo.push(`fileExists: statAt succeeded`); + return true; + } catch (e) { + debugInfo.push(`fileExists: statAt threw: ${e.message || e}`); + return false; + } +} + +/** + * Load entrypoint configuration from /app/.fabricks.toml. + */ +function loadEntrypoint(appDir) { + let moduleName = DEFAULT_MODULE; + let handlerName = DEFAULT_HANDLER; + + const configContent = readFile(appDir, '.fabricks.toml'); + if (configContent) { + // Simple TOML parsing for entrypoint + for (const line of configContent.split('\n')) { + const trimmed = line.trim(); + if (trimmed.startsWith('entrypoint')) { + const match = trimmed.match(/entrypoint\s*=\s*["']([^"']+)["']/); + if (match) { + const value = match[1]; + if (value.includes(':')) { + [moduleName, handlerName] = value.split(':', 2); + } else { + moduleName = value; + } + } + } + } + } + + return { moduleName, handlerName }; +} + +/** + * Dynamically load the user's handler function. + */ +function loadUserHandler() { + const appDir = findAppDirectory(); + if (!appDir) { + console.log('No /app directory found in preopens'); + return null; + } + + const { moduleName, handlerName } = loadEntrypoint(appDir); + + // Try to find the module file + let modulePath = `${moduleName}.js`; + if (!fileExists(appDir, modulePath)) { + // Try package style + modulePath = `${moduleName}/index.js`; + if (!fileExists(appDir, modulePath)) { + console.log(`User module not found: ${moduleName}.js`); + return null; + } + } + + // Read the module code + const code = readFile(appDir, modulePath); + if (!code) { + console.log(`Failed to read module: ${modulePath}`); + return null; + } + + // Execute the code and extract the handler + // We use Function constructor to create a module-like scope + try { + // Create a namespace object for the module + const moduleExports = {}; + const moduleScope = { + exports: moduleExports, + module: { exports: moduleExports }, + console: console, + JSON: JSON, + parseInt: parseInt, + parseFloat: parseFloat, + encodeURIComponent: encodeURIComponent, + decodeURIComponent: decodeURIComponent, + }; + + // Wrap code to capture function declarations + const wrappedCode = ` + ${code} + if (typeof ${handlerName} === 'function') { + return ${handlerName}; + } + return null; + `; + + // eslint-disable-next-line no-new-func + const factory = new Function('exports', 'module', 'console', 'JSON', 'parseInt', 'parseFloat', 'encodeURIComponent', 'decodeURIComponent', wrappedCode); + const handler = factory( + moduleScope.exports, + moduleScope.module, + moduleScope.console, + moduleScope.JSON, + moduleScope.parseInt, + moduleScope.parseFloat, + moduleScope.encodeURIComponent, + moduleScope.decodeURIComponent + ); + + if (handler && typeof handler === 'function') { + console.log(`Loaded handler '${handlerName}' from ${modulePath}`); + return handler; + } + + console.log(`Handler '${handlerName}' not found in module`); + return null; + } catch (e) { + console.error(`Error loading user handler: ${e}`); + return null; + } +} + +/** + * Ensure the user handler is loaded. + */ +function ensureHandlerLoaded() { + if (!handlerLoaded) { + userHandler = loadUserHandler(); + handlerLoaded = true; + } +} + +/** + * Default handler when no user handler is found. + */ +function defaultHandler(request) { + const path = request.path || '/'; + + if (path === '/' || path === '') { + return { + status: 200, + headers: { 'content-type': 'text/plain' }, + body: `Fabricks Node.js Runtime v20 + +No user handler found. + +Debug info: +${debugInfo.join('\n')} + +Create /app/app.js with a handler function: + + function handler(request) { + return { + status: 200, + body: "Hello!" + }; + } +`, + }; + } + + if (path === '/health') { + return { + status: 200, + headers: { 'content-type': 'text/plain' }, + body: 'OK', + }; + } + + return { + status: 404, + headers: { 'content-type': 'text/plain' }, + body: `Not Found: ${path}`, + }; +} + +/** + * Convert WASI HTTP method variant to string. + */ +function methodToString(methodVariant) { + // In jco, WIT variants are represented as { tag: 'variant_name', val?: value } + if (typeof methodVariant === 'string') { + return methodVariant.toUpperCase(); + } + if (methodVariant && typeof methodVariant === 'object') { + const tag = methodVariant.tag; + if (tag === 'other') { + return String(methodVariant.val || 'OTHER').toUpperCase(); + } + return String(tag || 'UNKNOWN').toUpperCase(); + } + return 'UNKNOWN'; +} + +/** + * Convert WASI HTTP IncomingRequest to simple object format. + */ +function convertRequest(request) { + const pathWithQuery = request.pathWithQuery() || '/'; + + let path = pathWithQuery; + const query = {}; + + const queryIndex = pathWithQuery.indexOf('?'); + if (queryIndex !== -1) { + path = pathWithQuery.substring(0, queryIndex); + const queryString = pathWithQuery.substring(queryIndex + 1); + for (const pair of queryString.split('&')) { + const eqIndex = pair.indexOf('='); + if (eqIndex !== -1) { + const key = pair.substring(0, eqIndex); + const value = decodeURIComponent(pair.substring(eqIndex + 1).replace(/\+/g, ' ')); + query[key] = value; + } + } + } + + const method = methodToString(request.method()); + + return { + method, + path, + query, + headers: {}, + }; +} + +/** + * Send response using WASI HTTP types. + */ +function sendResponse(response, responseOutparam) { + const status = response.status || 200; + const headersDict = response.headers || {}; + let body = response.body || ''; + + if (typeof body === 'object') { + body = JSON.stringify(body); + } + + const bodyBytes = new Uint8Array(new TextEncoder().encode(body)); + + // Build headers list + const headersList = []; + for (const [name, value] of Object.entries(headersDict)) { + const valueBytes = new Uint8Array(new TextEncoder().encode(String(value))); + headersList.push([name, valueBytes]); + } + + // Create response with headers + const fields = Fields.fromList(headersList); + const outgoingResponse = new OutgoingResponse(fields); + outgoingResponse.setStatusCode(status); + + // Write body + const outgoingBody = outgoingResponse.body(); + const outputStream = outgoingBody.write(); + outputStream.blockingWriteAndFlush(bodyBytes); + outputStream[Symbol.dispose](); + + // Finish + OutgoingBody.finish(outgoingBody, undefined); + ResponseOutparam.set(responseOutparam, { tag: 'ok', val: outgoingResponse }); +} + +/** + * WASI HTTP incoming-handler implementation. + */ +export const incomingHandler = { + handle(request, responseOutparam) { + // First, try to load the user handler and capture any errors + let loadError = null; + try { + ensureHandlerLoaded(); + } catch (e) { + loadError = e; + } + + const simpleRequest = convertRequest(request); + + let response; + try { + // If there was a load error, report it + if (loadError) { + response = { + status: 500, + headers: { 'content-type': 'text/plain' }, + body: `User handler load error: ${loadError.message || loadError}\n\nStack: ${loadError.stack || 'N/A'}`, + }; + } else if (userHandler) { + response = userHandler(simpleRequest); + } else { + response = defaultHandler(simpleRequest); + } + } catch (e) { + response = { + status: 500, + headers: { 'content-type': 'text/plain' }, + body: `Handler error: ${e.message || e}\n\nStack: ${e.stack || 'N/A'}`, + }; + } + + sendResponse(response, responseOutparam); + } +}; diff --git a/examples/runtimes/nodejs/test.wasm b/examples/runtimes/nodejs/test.wasm new file mode 100644 index 0000000..4de8ca3 Binary files /dev/null and b/examples/runtimes/nodejs/test.wasm differ diff --git a/examples/runtimes/nodejs/wit/deps/wasi-cli-0.2.0/package.wit b/examples/runtimes/nodejs/wit/deps/wasi-cli-0.2.0/package.wit new file mode 100644 index 0000000..8deef32 --- /dev/null +++ b/examples/runtimes/nodejs/wit/deps/wasi-cli-0.2.0/package.wit @@ -0,0 +1,20 @@ +package wasi:cli@0.2.0; + +interface stdout { + use wasi:io/streams@0.2.0.{output-stream}; + + get-stdout: func() -> output-stream; +} + +interface stderr { + use wasi:io/streams@0.2.0.{output-stream}; + + get-stderr: func() -> output-stream; +} + +interface stdin { + use wasi:io/streams@0.2.0.{input-stream}; + + get-stdin: func() -> input-stream; +} + diff --git a/examples/runtimes/nodejs/wit/deps/wasi-clocks-0.2.0/package.wit b/examples/runtimes/nodejs/wit/deps/wasi-clocks-0.2.0/package.wit new file mode 100644 index 0000000..1ab566b --- /dev/null +++ b/examples/runtimes/nodejs/wit/deps/wasi-clocks-0.2.0/package.wit @@ -0,0 +1,29 @@ +package wasi:clocks@0.2.0; + +interface wall-clock { + record datetime { + seconds: u64, + nanoseconds: u32, + } + + now: func() -> datetime; + + resolution: func() -> datetime; +} + +interface monotonic-clock { + use wasi:io/poll@0.2.0.{pollable}; + + type instant = u64; + + type duration = u64; + + now: func() -> instant; + + resolution: func() -> duration; + + subscribe-instant: func(when: instant) -> pollable; + + subscribe-duration: func(when: duration) -> pollable; +} + diff --git a/examples/runtimes/nodejs/wit/deps/wasi-filesystem-0.2.0/package.wit b/examples/runtimes/nodejs/wit/deps/wasi-filesystem-0.2.0/package.wit new file mode 100644 index 0000000..af4bea2 --- /dev/null +++ b/examples/runtimes/nodejs/wit/deps/wasi-filesystem-0.2.0/package.wit @@ -0,0 +1,535 @@ +package wasi:filesystem@0.2.0; + +/// WASI filesystem is a filesystem API primarily intended to let users run WASI +/// programs that access their files on their existing filesystems, without +/// significant overhead. +/// +/// It is intended to be roughly portable between Unix-family platforms and +/// Windows, though it does not hide many of the major differences. +/// +/// Paths are passed as interface-type `string`s, meaning they must consist of +/// a sequence of Unicode Scalar Values (USVs). Some filesystems may contain +/// paths which are not accessible by this API. +/// +/// The directory separator in WASI is always the forward-slash (`/`). +/// +/// All paths in WASI are relative paths, and are interpreted relative to a +/// `descriptor` referring to a base directory. If a `path` argument to any WASI +/// function starts with `/`, or if any step of resolving a `path`, including +/// `..` and symbolic link steps, reaches a directory outside of the base +/// directory, or reaches a symlink to an absolute or rooted path in the +/// underlying filesystem, the function fails with `error-code::not-permitted`. +/// +/// For more information about WASI path resolution and sandboxing, see +/// [WASI filesystem path resolution]. +/// +/// [WASI filesystem path resolution]: https://github.com/WebAssembly/wasi-filesystem/blob/main/path-resolution.md +interface types { + use wasi:io/streams@0.2.0.{input-stream, output-stream, error}; + use wasi:clocks/wall-clock@0.2.0.{datetime}; + + /// File size or length of a region within a file. + type filesize = u64; + + /// The type of a filesystem object referenced by a descriptor. + /// + /// Note: This was called `filetype` in earlier versions of WASI. + enum descriptor-type { + /// The type of the descriptor or file is unknown or is different from + /// any of the other types specified. + unknown, + /// The descriptor refers to a block device inode. + block-device, + /// The descriptor refers to a character device inode. + character-device, + /// The descriptor refers to a directory inode. + directory, + /// The descriptor refers to a named pipe. + fifo, + /// The file refers to a symbolic link inode. + symbolic-link, + /// The descriptor refers to a regular file inode. + regular-file, + /// The descriptor refers to a socket. + socket, + } + + /// Descriptor flags. + /// + /// Note: This was called `fdflags` in earlier versions of WASI. + flags descriptor-flags { + /// Read mode: Data can be read. + read, + /// Write mode: Data can be written to. + write, + /// Request that writes be performed according to synchronized I/O file + /// integrity completion. The data stored in the file and the file's + /// metadata are synchronized. This is similar to `O_SYNC` in POSIX. + /// + /// The precise semantics of this operation have not yet been defined for + /// WASI. At this time, it should be interpreted as a request, and not a + /// requirement. + file-integrity-sync, + /// Request that writes be performed according to synchronized I/O data + /// integrity completion. Only the data stored in the file is + /// synchronized. This is similar to `O_DSYNC` in POSIX. + /// + /// The precise semantics of this operation have not yet been defined for + /// WASI. At this time, it should be interpreted as a request, and not a + /// requirement. + data-integrity-sync, + /// Requests that reads be performed at the same level of integrety + /// requested for writes. This is similar to `O_RSYNC` in POSIX. + /// + /// The precise semantics of this operation have not yet been defined for + /// WASI. At this time, it should be interpreted as a request, and not a + /// requirement. + requested-write-sync, + /// Mutating directories mode: Directory contents may be mutated. + /// + /// When this flag is unset on a descriptor, operations using the + /// descriptor which would create, rename, delete, modify the data or + /// metadata of filesystem objects, or obtain another handle which + /// would permit any of those, shall fail with `error-code::read-only` if + /// they would otherwise succeed. + /// + /// This may only be set on directories. + mutate-directory, + } + + /// Flags determining the method of how paths are resolved. + flags path-flags { + /// As long as the resolved path corresponds to a symbolic link, it is + /// expanded. + symlink-follow, + } + + /// Open flags used by `open-at`. + flags open-flags { + /// Create file if it does not exist, similar to `O_CREAT` in POSIX. + create, + /// Fail if not a directory, similar to `O_DIRECTORY` in POSIX. + directory, + /// Fail if file already exists, similar to `O_EXCL` in POSIX. + exclusive, + /// Truncate file to size 0, similar to `O_TRUNC` in POSIX. + truncate, + } + + /// Number of hard links to an inode. + type link-count = u64; + + /// File attributes. + /// + /// Note: This was called `filestat` in earlier versions of WASI. + record descriptor-stat { + /// File type. + %type: descriptor-type, + /// Number of hard links to the file. + link-count: link-count, + /// For regular files, the file size in bytes. For symbolic links, the + /// length in bytes of the pathname contained in the symbolic link. + size: filesize, + /// Last data access timestamp. + /// + /// If the `option` is none, the platform doesn't maintain an access + /// timestamp for this file. + data-access-timestamp: option, + /// Last data modification timestamp. + /// + /// If the `option` is none, the platform doesn't maintain a + /// modification timestamp for this file. + data-modification-timestamp: option, + /// Last file status-change timestamp. + /// + /// If the `option` is none, the platform doesn't maintain a + /// status-change timestamp for this file. + status-change-timestamp: option, + } + + /// When setting a timestamp, this gives the value to set it to. + variant new-timestamp { + /// Leave the timestamp set to its previous value. + no-change, + /// Set the timestamp to the current time of the system clock associated + /// with the filesystem. + now, + /// Set the timestamp to the given value. + timestamp(datetime), + } + + /// A directory entry. + record directory-entry { + /// The type of the file referred to by this directory entry. + %type: descriptor-type, + /// The name of the object. + name: string, + } + + /// Error codes returned by functions, similar to `errno` in POSIX. + /// Not all of these error codes are returned by the functions provided by this + /// API; some are used in higher-level library layers, and others are provided + /// merely for alignment with POSIX. + enum error-code { + /// Permission denied, similar to `EACCES` in POSIX. + access, + /// Resource unavailable, or operation would block, similar to `EAGAIN` and `EWOULDBLOCK` in POSIX. + would-block, + /// Connection already in progress, similar to `EALREADY` in POSIX. + already, + /// Bad descriptor, similar to `EBADF` in POSIX. + bad-descriptor, + /// Device or resource busy, similar to `EBUSY` in POSIX. + busy, + /// Resource deadlock would occur, similar to `EDEADLK` in POSIX. + deadlock, + /// Storage quota exceeded, similar to `EDQUOT` in POSIX. + quota, + /// File exists, similar to `EEXIST` in POSIX. + exist, + /// File too large, similar to `EFBIG` in POSIX. + file-too-large, + /// Illegal byte sequence, similar to `EILSEQ` in POSIX. + illegal-byte-sequence, + /// Operation in progress, similar to `EINPROGRESS` in POSIX. + in-progress, + /// Interrupted function, similar to `EINTR` in POSIX. + interrupted, + /// Invalid argument, similar to `EINVAL` in POSIX. + invalid, + /// I/O error, similar to `EIO` in POSIX. + io, + /// Is a directory, similar to `EISDIR` in POSIX. + is-directory, + /// Too many levels of symbolic links, similar to `ELOOP` in POSIX. + loop, + /// Too many links, similar to `EMLINK` in POSIX. + too-many-links, + /// Message too large, similar to `EMSGSIZE` in POSIX. + message-size, + /// Filename too long, similar to `ENAMETOOLONG` in POSIX. + name-too-long, + /// No such device, similar to `ENODEV` in POSIX. + no-device, + /// No such file or directory, similar to `ENOENT` in POSIX. + no-entry, + /// No locks available, similar to `ENOLCK` in POSIX. + no-lock, + /// Not enough space, similar to `ENOMEM` in POSIX. + insufficient-memory, + /// No space left on device, similar to `ENOSPC` in POSIX. + insufficient-space, + /// Not a directory or a symbolic link to a directory, similar to `ENOTDIR` in POSIX. + not-directory, + /// Directory not empty, similar to `ENOTEMPTY` in POSIX. + not-empty, + /// State not recoverable, similar to `ENOTRECOVERABLE` in POSIX. + not-recoverable, + /// Not supported, similar to `ENOTSUP` and `ENOSYS` in POSIX. + unsupported, + /// Inappropriate I/O control operation, similar to `ENOTTY` in POSIX. + no-tty, + /// No such device or address, similar to `ENXIO` in POSIX. + no-such-device, + /// Value too large to be stored in data type, similar to `EOVERFLOW` in POSIX. + overflow, + /// Operation not permitted, similar to `EPERM` in POSIX. + not-permitted, + /// Broken pipe, similar to `EPIPE` in POSIX. + pipe, + /// Read-only file system, similar to `EROFS` in POSIX. + read-only, + /// Invalid seek, similar to `ESPIPE` in POSIX. + invalid-seek, + /// Text file busy, similar to `ETXTBSY` in POSIX. + text-file-busy, + /// Cross-device link, similar to `EXDEV` in POSIX. + cross-device, + } + + /// File or memory access pattern advisory information. + enum advice { + /// The application has no advice to give on its behavior with respect + /// to the specified data. + normal, + /// The application expects to access the specified data sequentially + /// from lower offsets to higher offsets. + sequential, + /// The application expects to access the specified data in a random + /// order. + random, + /// The application expects to access the specified data in the near + /// future. + will-need, + /// The application expects that it will not access the specified data + /// in the near future. + dont-need, + /// The application expects to access the specified data once and then + /// not reuse it thereafter. + no-reuse, + } + + /// A 128-bit hash value, split into parts because wasm doesn't have a + /// 128-bit integer type. + record metadata-hash-value { + /// 64 bits of a 128-bit hash value. + lower: u64, + /// Another 64 bits of a 128-bit hash value. + upper: u64, + } + + /// A descriptor is a reference to a filesystem object, which may be a file, + /// directory, named pipe, special file, or other object on which filesystem + /// calls may be made. + resource descriptor { + /// Return a stream for reading from a file, if available. + /// + /// May fail with an error-code describing why the file cannot be read. + /// + /// Multiple read, write, and append streams may be active on the same open + /// file and they do not interfere with each other. + /// + /// Note: This allows using `read-stream`, which is similar to `read` in POSIX. + read-via-stream: func(offset: filesize) -> result; + /// Return a stream for writing to a file, if available. + /// + /// May fail with an error-code describing why the file cannot be written. + /// + /// Note: This allows using `write-stream`, which is similar to `write` in + /// POSIX. + write-via-stream: func(offset: filesize) -> result; + /// Return a stream for appending to a file, if available. + /// + /// May fail with an error-code describing why the file cannot be appended. + /// + /// Note: This allows using `write-stream`, which is similar to `write` with + /// `O_APPEND` in in POSIX. + append-via-stream: func() -> result; + /// Provide file advisory information on a descriptor. + /// + /// This is similar to `posix_fadvise` in POSIX. + advise: func(offset: filesize, length: filesize, advice: advice) -> result<_, error-code>; + /// Synchronize the data of a file to disk. + /// + /// This function succeeds with no effect if the file descriptor is not + /// opened for writing. + /// + /// Note: This is similar to `fdatasync` in POSIX. + sync-data: func() -> result<_, error-code>; + /// Get flags associated with a descriptor. + /// + /// Note: This returns similar flags to `fcntl(fd, F_GETFL)` in POSIX. + /// + /// Note: This returns the value that was the `fs_flags` value returned + /// from `fdstat_get` in earlier versions of WASI. + get-flags: func() -> result; + /// Get the dynamic type of a descriptor. + /// + /// Note: This returns the same value as the `type` field of the `fd-stat` + /// returned by `stat`, `stat-at` and similar. + /// + /// Note: This returns similar flags to the `st_mode & S_IFMT` value provided + /// by `fstat` in POSIX. + /// + /// Note: This returns the value that was the `fs_filetype` value returned + /// from `fdstat_get` in earlier versions of WASI. + get-type: func() -> result; + /// Adjust the size of an open file. If this increases the file's size, the + /// extra bytes are filled with zeros. + /// + /// Note: This was called `fd_filestat_set_size` in earlier versions of WASI. + set-size: func(size: filesize) -> result<_, error-code>; + /// Adjust the timestamps of an open file or directory. + /// + /// Note: This is similar to `futimens` in POSIX. + /// + /// Note: This was called `fd_filestat_set_times` in earlier versions of WASI. + set-times: func(data-access-timestamp: new-timestamp, data-modification-timestamp: new-timestamp) -> result<_, error-code>; + /// Read from a descriptor, without using and updating the descriptor's offset. + /// + /// This function returns a list of bytes containing the data that was + /// read, along with a bool which, when true, indicates that the end of the + /// file was reached. The returned list will contain up to `length` bytes; it + /// may return fewer than requested, if the end of the file is reached or + /// if the I/O operation is interrupted. + /// + /// In the future, this may change to return a `stream`. + /// + /// Note: This is similar to `pread` in POSIX. + read: func(length: filesize, offset: filesize) -> result, bool>, error-code>; + /// Write to a descriptor, without using and updating the descriptor's offset. + /// + /// It is valid to write past the end of a file; the file is extended to the + /// extent of the write, with bytes between the previous end and the start of + /// the write set to zero. + /// + /// In the future, this may change to take a `stream`. + /// + /// Note: This is similar to `pwrite` in POSIX. + write: func(buffer: list, offset: filesize) -> result; + /// Read directory entries from a directory. + /// + /// On filesystems where directories contain entries referring to themselves + /// and their parents, often named `.` and `..` respectively, these entries + /// are omitted. + /// + /// This always returns a new stream which starts at the beginning of the + /// directory. Multiple streams may be active on the same directory, and they + /// do not interfere with each other. + read-directory: func() -> result; + /// Synchronize the data and metadata of a file to disk. + /// + /// This function succeeds with no effect if the file descriptor is not + /// opened for writing. + /// + /// Note: This is similar to `fsync` in POSIX. + sync: func() -> result<_, error-code>; + /// Create a directory. + /// + /// Note: This is similar to `mkdirat` in POSIX. + create-directory-at: func(path: string) -> result<_, error-code>; + /// Return the attributes of an open file or directory. + /// + /// Note: This is similar to `fstat` in POSIX, except that it does not return + /// device and inode information. For testing whether two descriptors refer to + /// the same underlying filesystem object, use `is-same-object`. To obtain + /// additional data that can be used do determine whether a file has been + /// modified, use `metadata-hash`. + /// + /// Note: This was called `fd_filestat_get` in earlier versions of WASI. + stat: func() -> result; + /// Return the attributes of a file or directory. + /// + /// Note: This is similar to `fstatat` in POSIX, except that it does not + /// return device and inode information. See the `stat` description for a + /// discussion of alternatives. + /// + /// Note: This was called `path_filestat_get` in earlier versions of WASI. + stat-at: func(path-flags: path-flags, path: string) -> result; + /// Adjust the timestamps of a file or directory. + /// + /// Note: This is similar to `utimensat` in POSIX. + /// + /// Note: This was called `path_filestat_set_times` in earlier versions of + /// WASI. + set-times-at: func(path-flags: path-flags, path: string, data-access-timestamp: new-timestamp, data-modification-timestamp: new-timestamp) -> result<_, error-code>; + /// Create a hard link. + /// + /// Note: This is similar to `linkat` in POSIX. + link-at: func(old-path-flags: path-flags, old-path: string, new-descriptor: borrow, new-path: string) -> result<_, error-code>; + /// Open a file or directory. + /// + /// The returned descriptor is not guaranteed to be the lowest-numbered + /// descriptor not currently open/ it is randomized to prevent applications + /// from depending on making assumptions about indexes, since this is + /// error-prone in multi-threaded contexts. The returned descriptor is + /// guaranteed to be less than 2**31. + /// + /// If `flags` contains `descriptor-flags::mutate-directory`, and the base + /// descriptor doesn't have `descriptor-flags::mutate-directory` set, + /// `open-at` fails with `error-code::read-only`. + /// + /// If `flags` contains `write` or `mutate-directory`, or `open-flags` + /// contains `truncate` or `create`, and the base descriptor doesn't have + /// `descriptor-flags::mutate-directory` set, `open-at` fails with + /// `error-code::read-only`. + /// + /// Note: This is similar to `openat` in POSIX. + open-at: func(path-flags: path-flags, path: string, open-flags: open-flags, %flags: descriptor-flags) -> result; + /// Read the contents of a symbolic link. + /// + /// If the contents contain an absolute or rooted path in the underlying + /// filesystem, this function fails with `error-code::not-permitted`. + /// + /// Note: This is similar to `readlinkat` in POSIX. + readlink-at: func(path: string) -> result; + /// Remove a directory. + /// + /// Return `error-code::not-empty` if the directory is not empty. + /// + /// Note: This is similar to `unlinkat(fd, path, AT_REMOVEDIR)` in POSIX. + remove-directory-at: func(path: string) -> result<_, error-code>; + /// Rename a filesystem object. + /// + /// Note: This is similar to `renameat` in POSIX. + rename-at: func(old-path: string, new-descriptor: borrow, new-path: string) -> result<_, error-code>; + /// Create a symbolic link (also known as a "symlink"). + /// + /// If `old-path` starts with `/`, the function fails with + /// `error-code::not-permitted`. + /// + /// Note: This is similar to `symlinkat` in POSIX. + symlink-at: func(old-path: string, new-path: string) -> result<_, error-code>; + /// Unlink a filesystem object that is not a directory. + /// + /// Return `error-code::is-directory` if the path refers to a directory. + /// Note: This is similar to `unlinkat(fd, path, 0)` in POSIX. + unlink-file-at: func(path: string) -> result<_, error-code>; + /// Test whether two descriptors refer to the same filesystem object. + /// + /// In POSIX, this corresponds to testing whether the two descriptors have the + /// same device (`st_dev`) and inode (`st_ino` or `d_ino`) numbers. + /// wasi-filesystem does not expose device and inode numbers, so this function + /// may be used instead. + is-same-object: func(other: borrow) -> bool; + /// Return a hash of the metadata associated with a filesystem object referred + /// to by a descriptor. + /// + /// This returns a hash of the last-modification timestamp and file size, and + /// may also include the inode number, device number, birth timestamp, and + /// other metadata fields that may change when the file is modified or + /// replaced. It may also include a secret value chosen by the + /// implementation and not otherwise exposed. + /// + /// Implementations are encourated to provide the following properties: + /// + /// - If the file is not modified or replaced, the computed hash value should + /// usually not change. + /// - If the object is modified or replaced, the computed hash value should + /// usually change. + /// - The inputs to the hash should not be easily computable from the + /// computed hash. + /// + /// However, none of these is required. + metadata-hash: func() -> result; + /// Return a hash of the metadata associated with a filesystem object referred + /// to by a directory descriptor and a relative path. + /// + /// This performs the same hash computation as `metadata-hash`. + metadata-hash-at: func(path-flags: path-flags, path: string) -> result; + } + + /// A stream of directory entries. + resource directory-entry-stream { + /// Read a single directory entry from a `directory-entry-stream`. + read-directory-entry: func() -> result, error-code>; + } + + /// Attempts to extract a filesystem-related `error-code` from the stream + /// `error` provided. + /// + /// Stream operations which return `stream-error::last-operation-failed` + /// have a payload with more information about the operation that failed. + /// This payload can be passed through to this function to see if there's + /// filesystem-related information about the error to return. + /// + /// Note that this function is fallible because not all stream-related + /// errors are filesystem-related errors. + filesystem-error-code: func(err: borrow) -> option; +} + +interface preopens { + use types.{descriptor}; + + /// Return the set of preopened directories, and their path. + get-directories: func() -> list>; +} + +world imports { + import wasi:io/error@0.2.0; + import wasi:io/poll@0.2.0; + import wasi:io/streams@0.2.0; + import wasi:clocks/wall-clock@0.2.0; + import types; + import preopens; +} diff --git a/examples/runtimes/nodejs/wit/deps/wasi-http-0.2.0/package.wit b/examples/runtimes/nodejs/wit/deps/wasi-http-0.2.0/package.wit new file mode 100644 index 0000000..11f7ff4 --- /dev/null +++ b/examples/runtimes/nodejs/wit/deps/wasi-http-0.2.0/package.wit @@ -0,0 +1,571 @@ +package wasi:http@0.2.0; + +/// This interface defines all of the types and methods for implementing +/// HTTP Requests and Responses, both incoming and outgoing, as well as +/// their headers, trailers, and bodies. +interface types { + use wasi:clocks/monotonic-clock@0.2.0.{duration}; + use wasi:io/streams@0.2.0.{input-stream, output-stream}; + use wasi:io/error@0.2.0.{error as io-error}; + use wasi:io/poll@0.2.0.{pollable}; + + /// This type corresponds to HTTP standard Methods. + variant method { + get, + head, + post, + put, + delete, + connect, + options, + trace, + patch, + other(string), + } + + /// This type corresponds to HTTP standard Related Schemes. + variant scheme { + HTTP, + HTTPS, + other(string), + } + + /// Defines the case payload type for `DNS-error` above: + record DNS-error-payload { + rcode: option, + info-code: option, + } + + /// Defines the case payload type for `TLS-alert-received` above: + record TLS-alert-received-payload { + alert-id: option, + alert-message: option, + } + + /// Defines the case payload type for `HTTP-response-{header,trailer}-size` above: + record field-size-payload { + field-name: option, + field-size: option, + } + + /// These cases are inspired by the IANA HTTP Proxy Error Types: + /// https://www.iana.org/assignments/http-proxy-status/http-proxy-status.xhtml#table-http-proxy-error-types + variant error-code { + DNS-timeout, + DNS-error(DNS-error-payload), + destination-not-found, + destination-unavailable, + destination-IP-prohibited, + destination-IP-unroutable, + connection-refused, + connection-terminated, + connection-timeout, + connection-read-timeout, + connection-write-timeout, + connection-limit-reached, + TLS-protocol-error, + TLS-certificate-error, + TLS-alert-received(TLS-alert-received-payload), + HTTP-request-denied, + HTTP-request-length-required, + HTTP-request-body-size(option), + HTTP-request-method-invalid, + HTTP-request-URI-invalid, + HTTP-request-URI-too-long, + HTTP-request-header-section-size(option), + HTTP-request-header-size(option), + HTTP-request-trailer-section-size(option), + HTTP-request-trailer-size(field-size-payload), + HTTP-response-incomplete, + HTTP-response-header-section-size(option), + HTTP-response-header-size(field-size-payload), + HTTP-response-body-size(option), + HTTP-response-trailer-section-size(option), + HTTP-response-trailer-size(field-size-payload), + HTTP-response-transfer-coding(option), + HTTP-response-content-coding(option), + HTTP-response-timeout, + HTTP-upgrade-failed, + HTTP-protocol-error, + loop-detected, + configuration-error, + /// This is a catch-all error for anything that doesn't fit cleanly into a + /// more specific case. It also includes an optional string for an + /// unstructured description of the error. Users should not depend on the + /// string for diagnosing errors, as it's not required to be consistent + /// between implementations. + internal-error(option), + } + + /// This type enumerates the different kinds of errors that may occur when + /// setting or appending to a `fields` resource. + variant header-error { + /// This error indicates that a `field-key` or `field-value` was + /// syntactically invalid when used with an operation that sets headers in a + /// `fields`. + invalid-syntax, + /// This error indicates that a forbidden `field-key` was used when trying + /// to set a header in a `fields`. + forbidden, + /// This error indicates that the operation on the `fields` was not + /// permitted because the fields are immutable. + immutable, + } + + /// Field keys are always strings. + type field-key = string; + + /// Field values should always be ASCII strings. However, in + /// reality, HTTP implementations often have to interpret malformed values, + /// so they are provided as a list of bytes. + type field-value = list; + + /// This following block defines the `fields` resource which corresponds to + /// HTTP standard Fields. Fields are a common representation used for both + /// Headers and Trailers. + /// + /// A `fields` may be mutable or immutable. A `fields` created using the + /// constructor, `from-list`, or `clone` will be mutable, but a `fields` + /// resource given by other means (including, but not limited to, + /// `incoming-request.headers`, `outgoing-request.headers`) might be be + /// immutable. In an immutable fields, the `set`, `append`, and `delete` + /// operations will fail with `header-error.immutable`. + resource fields { + /// Construct an empty HTTP Fields. + /// + /// The resulting `fields` is mutable. + constructor(); + /// Construct an HTTP Fields. + /// + /// The resulting `fields` is mutable. + /// + /// The list represents each key-value pair in the Fields. Keys + /// which have multiple values are represented by multiple entries in this + /// list with the same key. + /// + /// The tuple is a pair of the field key, represented as a string, and + /// Value, represented as a list of bytes. In a valid Fields, all keys + /// and values are valid UTF-8 strings. However, values are not always + /// well-formed, so they are represented as a raw list of bytes. + /// + /// An error result will be returned if any header or value was + /// syntactically invalid, or if a header was forbidden. + from-list: static func(entries: list>) -> result; + /// Get all of the values corresponding to a key. If the key is not present + /// in this `fields`, an empty list is returned. However, if the key is + /// present but empty, this is represented by a list with one or more + /// empty field-values present. + get: func(name: field-key) -> list; + /// Returns `true` when the key is present in this `fields`. If the key is + /// syntactically invalid, `false` is returned. + has: func(name: field-key) -> bool; + /// Set all of the values for a key. Clears any existing values for that + /// key, if they have been set. + /// + /// Fails with `header-error.immutable` if the `fields` are immutable. + set: func(name: field-key, value: list) -> result<_, header-error>; + /// Delete all values for a key. Does nothing if no values for the key + /// exist. + /// + /// Fails with `header-error.immutable` if the `fields` are immutable. + delete: func(name: field-key) -> result<_, header-error>; + /// Append a value for a key. Does not change or delete any existing + /// values for that key. + /// + /// Fails with `header-error.immutable` if the `fields` are immutable. + append: func(name: field-key, value: field-value) -> result<_, header-error>; + /// Retrieve the full set of keys and values in the Fields. Like the + /// constructor, the list represents each key-value pair. + /// + /// The outer list represents each key-value pair in the Fields. Keys + /// which have multiple values are represented by multiple entries in this + /// list with the same key. + entries: func() -> list>; + /// Make a deep copy of the Fields. Equivelant in behavior to calling the + /// `fields` constructor on the return value of `entries`. The resulting + /// `fields` is mutable. + clone: func() -> fields; + } + + /// Headers is an alias for Fields. + type headers = fields; + + /// Trailers is an alias for Fields. + type trailers = fields; + + /// Represents an incoming HTTP Request. + resource incoming-request { + /// Returns the method of the incoming request. + method: func() -> method; + /// Returns the path with query parameters from the request, as a string. + path-with-query: func() -> option; + /// Returns the protocol scheme from the request. + scheme: func() -> option; + /// Returns the authority from the request, if it was present. + authority: func() -> option; + /// Get the `headers` associated with the request. + /// + /// The returned `headers` resource is immutable: `set`, `append`, and + /// `delete` operations will fail with `header-error.immutable`. + /// + /// The `headers` returned are a child resource: it must be dropped before + /// the parent `incoming-request` is dropped. Dropping this + /// `incoming-request` before all children are dropped will trap. + headers: func() -> headers; + /// Gives the `incoming-body` associated with this request. Will only + /// return success at most once, and subsequent calls will return error. + consume: func() -> result; + } + + /// Represents an outgoing HTTP Request. + resource outgoing-request { + /// Construct a new `outgoing-request` with a default `method` of `GET`, and + /// `none` values for `path-with-query`, `scheme`, and `authority`. + /// + /// * `headers` is the HTTP Headers for the Request. + /// + /// It is possible to construct, or manipulate with the accessor functions + /// below, an `outgoing-request` with an invalid combination of `scheme` + /// and `authority`, or `headers` which are not permitted to be sent. + /// It is the obligation of the `outgoing-handler.handle` implementation + /// to reject invalid constructions of `outgoing-request`. + constructor(headers: headers); + /// Returns the resource corresponding to the outgoing Body for this + /// Request. + /// + /// Returns success on the first call: the `outgoing-body` resource for + /// this `outgoing-request` can be retrieved at most once. Subsequent + /// calls will return error. + body: func() -> result; + /// Get the Method for the Request. + method: func() -> method; + /// Set the Method for the Request. Fails if the string present in a + /// `method.other` argument is not a syntactically valid method. + set-method: func(method: method) -> result; + /// Get the combination of the HTTP Path and Query for the Request. + /// When `none`, this represents an empty Path and empty Query. + path-with-query: func() -> option; + /// Set the combination of the HTTP Path and Query for the Request. + /// When `none`, this represents an empty Path and empty Query. Fails is the + /// string given is not a syntactically valid path and query uri component. + set-path-with-query: func(path-with-query: option) -> result; + /// Get the HTTP Related Scheme for the Request. When `none`, the + /// implementation may choose an appropriate default scheme. + scheme: func() -> option; + /// Set the HTTP Related Scheme for the Request. When `none`, the + /// implementation may choose an appropriate default scheme. Fails if the + /// string given is not a syntactically valid uri scheme. + set-scheme: func(scheme: option) -> result; + /// Get the HTTP Authority for the Request. A value of `none` may be used + /// with Related Schemes which do not require an Authority. The HTTP and + /// HTTPS schemes always require an authority. + authority: func() -> option; + /// Set the HTTP Authority for the Request. A value of `none` may be used + /// with Related Schemes which do not require an Authority. The HTTP and + /// HTTPS schemes always require an authority. Fails if the string given is + /// not a syntactically valid uri authority. + set-authority: func(authority: option) -> result; + /// Get the headers associated with the Request. + /// + /// The returned `headers` resource is immutable: `set`, `append`, and + /// `delete` operations will fail with `header-error.immutable`. + /// + /// This headers resource is a child: it must be dropped before the parent + /// `outgoing-request` is dropped, or its ownership is transfered to + /// another component by e.g. `outgoing-handler.handle`. + headers: func() -> headers; + } + + /// Parameters for making an HTTP Request. Each of these parameters is + /// currently an optional timeout applicable to the transport layer of the + /// HTTP protocol. + /// + /// These timeouts are separate from any the user may use to bound a + /// blocking call to `wasi:io/poll.poll`. + resource request-options { + /// Construct a default `request-options` value. + constructor(); + /// The timeout for the initial connect to the HTTP Server. + connect-timeout: func() -> option; + /// Set the timeout for the initial connect to the HTTP Server. An error + /// return value indicates that this timeout is not supported. + set-connect-timeout: func(duration: option) -> result; + /// The timeout for receiving the first byte of the Response body. + first-byte-timeout: func() -> option; + /// Set the timeout for receiving the first byte of the Response body. An + /// error return value indicates that this timeout is not supported. + set-first-byte-timeout: func(duration: option) -> result; + /// The timeout for receiving subsequent chunks of bytes in the Response + /// body stream. + between-bytes-timeout: func() -> option; + /// Set the timeout for receiving subsequent chunks of bytes in the Response + /// body stream. An error return value indicates that this timeout is not + /// supported. + set-between-bytes-timeout: func(duration: option) -> result; + } + + /// Represents the ability to send an HTTP Response. + /// + /// This resource is used by the `wasi:http/incoming-handler` interface to + /// allow a Response to be sent corresponding to the Request provided as the + /// other argument to `incoming-handler.handle`. + resource response-outparam { + /// Set the value of the `response-outparam` to either send a response, + /// or indicate an error. + /// + /// This method consumes the `response-outparam` to ensure that it is + /// called at most once. If it is never called, the implementation + /// will respond with an error. + /// + /// The user may provide an `error` to `response` to allow the + /// implementation determine how to respond with an HTTP error response. + set: static func(param: response-outparam, response: result); + } + + /// This type corresponds to the HTTP standard Status Code. + type status-code = u16; + + /// Represents an incoming HTTP Response. + resource incoming-response { + /// Returns the status code from the incoming response. + status: func() -> status-code; + /// Returns the headers from the incoming response. + /// + /// The returned `headers` resource is immutable: `set`, `append`, and + /// `delete` operations will fail with `header-error.immutable`. + /// + /// This headers resource is a child: it must be dropped before the parent + /// `incoming-response` is dropped. + headers: func() -> headers; + /// Returns the incoming body. May be called at most once. Returns error + /// if called additional times. + consume: func() -> result; + } + + /// Represents an incoming HTTP Request or Response's Body. + /// + /// A body has both its contents - a stream of bytes - and a (possibly + /// empty) set of trailers, indicating that the full contents of the + /// body have been received. This resource represents the contents as + /// an `input-stream` and the delivery of trailers as a `future-trailers`, + /// and ensures that the user of this interface may only be consuming either + /// the body contents or waiting on trailers at any given time. + resource incoming-body { + /// Returns the contents of the body, as a stream of bytes. + /// + /// Returns success on first call: the stream representing the contents + /// can be retrieved at most once. Subsequent calls will return error. + /// + /// The returned `input-stream` resource is a child: it must be dropped + /// before the parent `incoming-body` is dropped, or consumed by + /// `incoming-body.finish`. + /// + /// This invariant ensures that the implementation can determine whether + /// the user is consuming the contents of the body, waiting on the + /// `future-trailers` to be ready, or neither. This allows for network + /// backpressure is to be applied when the user is consuming the body, + /// and for that backpressure to not inhibit delivery of the trailers if + /// the user does not read the entire body. + %stream: func() -> result; + /// Takes ownership of `incoming-body`, and returns a `future-trailers`. + /// This function will trap if the `input-stream` child is still alive. + finish: static func(this: incoming-body) -> future-trailers; + } + + /// Represents a future which may eventaully return trailers, or an error. + /// + /// In the case that the incoming HTTP Request or Response did not have any + /// trailers, this future will resolve to the empty set of trailers once the + /// complete Request or Response body has been received. + resource future-trailers { + /// Returns a pollable which becomes ready when either the trailers have + /// been received, or an error has occured. When this pollable is ready, + /// the `get` method will return `some`. + subscribe: func() -> pollable; + /// Returns the contents of the trailers, or an error which occured, + /// once the future is ready. + /// + /// The outer `option` represents future readiness. Users can wait on this + /// `option` to become `some` using the `subscribe` method. + /// + /// The outer `result` is used to retrieve the trailers or error at most + /// once. It will be success on the first call in which the outer option + /// is `some`, and error on subsequent calls. + /// + /// The inner `result` represents that either the HTTP Request or Response + /// body, as well as any trailers, were received successfully, or that an + /// error occured receiving them. The optional `trailers` indicates whether + /// or not trailers were present in the body. + /// + /// When some `trailers` are returned by this method, the `trailers` + /// resource is immutable, and a child. Use of the `set`, `append`, or + /// `delete` methods will return an error, and the resource must be + /// dropped before the parent `future-trailers` is dropped. + get: func() -> option, error-code>>>; + } + + /// Represents an outgoing HTTP Response. + resource outgoing-response { + /// Construct an `outgoing-response`, with a default `status-code` of `200`. + /// If a different `status-code` is needed, it must be set via the + /// `set-status-code` method. + /// + /// * `headers` is the HTTP Headers for the Response. + constructor(headers: headers); + /// Get the HTTP Status Code for the Response. + status-code: func() -> status-code; + /// Set the HTTP Status Code for the Response. Fails if the status-code + /// given is not a valid http status code. + set-status-code: func(status-code: status-code) -> result; + /// Get the headers associated with the Request. + /// + /// The returned `headers` resource is immutable: `set`, `append`, and + /// `delete` operations will fail with `header-error.immutable`. + /// + /// This headers resource is a child: it must be dropped before the parent + /// `outgoing-request` is dropped, or its ownership is transfered to + /// another component by e.g. `outgoing-handler.handle`. + headers: func() -> headers; + /// Returns the resource corresponding to the outgoing Body for this Response. + /// + /// Returns success on the first call: the `outgoing-body` resource for + /// this `outgoing-response` can be retrieved at most once. Subsequent + /// calls will return error. + body: func() -> result; + } + + /// Represents an outgoing HTTP Request or Response's Body. + /// + /// A body has both its contents - a stream of bytes - and a (possibly + /// empty) set of trailers, inducating the full contents of the body + /// have been sent. This resource represents the contents as an + /// `output-stream` child resource, and the completion of the body (with + /// optional trailers) with a static function that consumes the + /// `outgoing-body` resource, and ensures that the user of this interface + /// may not write to the body contents after the body has been finished. + /// + /// If the user code drops this resource, as opposed to calling the static + /// method `finish`, the implementation should treat the body as incomplete, + /// and that an error has occured. The implementation should propogate this + /// error to the HTTP protocol by whatever means it has available, + /// including: corrupting the body on the wire, aborting the associated + /// Request, or sending a late status code for the Response. + resource outgoing-body { + /// Returns a stream for writing the body contents. + /// + /// The returned `output-stream` is a child resource: it must be dropped + /// before the parent `outgoing-body` resource is dropped (or finished), + /// otherwise the `outgoing-body` drop or `finish` will trap. + /// + /// Returns success on the first call: the `output-stream` resource for + /// this `outgoing-body` may be retrieved at most once. Subsequent calls + /// will return error. + write: func() -> result; + /// Finalize an outgoing body, optionally providing trailers. This must be + /// called to signal that the response is complete. If the `outgoing-body` + /// is dropped without calling `outgoing-body.finalize`, the implementation + /// should treat the body as corrupted. + /// + /// Fails if the body's `outgoing-request` or `outgoing-response` was + /// constructed with a Content-Length header, and the contents written + /// to the body (via `write`) does not match the value given in the + /// Content-Length. + finish: static func(this: outgoing-body, trailers: option) -> result<_, error-code>; + } + + /// Represents a future which may eventaully return an incoming HTTP + /// Response, or an error. + /// + /// This resource is returned by the `wasi:http/outgoing-handler` interface to + /// provide the HTTP Response corresponding to the sent Request. + resource future-incoming-response { + /// Returns a pollable which becomes ready when either the Response has + /// been received, or an error has occured. When this pollable is ready, + /// the `get` method will return `some`. + subscribe: func() -> pollable; + /// Returns the incoming HTTP Response, or an error, once one is ready. + /// + /// The outer `option` represents future readiness. Users can wait on this + /// `option` to become `some` using the `subscribe` method. + /// + /// The outer `result` is used to retrieve the response or error at most + /// once. It will be success on the first call in which the outer option + /// is `some`, and error on subsequent calls. + /// + /// The inner `result` represents that either the incoming HTTP Response + /// status and headers have recieved successfully, or that an error + /// occured. Errors may also occur while consuming the response body, + /// but those will be reported by the `incoming-body` and its + /// `output-stream` child. + get: func() -> option>>; + } + + /// Attempts to extract a http-related `error` from the wasi:io `error` + /// provided. + /// + /// Stream operations which return + /// `wasi:io/stream/stream-error::last-operation-failed` have a payload of + /// type `wasi:io/error/error` with more information about the operation + /// that failed. This payload can be passed through to this function to see + /// if there's http-related information about the error to return. + /// + /// Note that this function is fallible because not all io-errors are + /// http-related errors. + http-error-code: func(err: borrow) -> option; +} + +/// This interface defines a handler of incoming HTTP Requests. It should +/// be exported by components which can respond to HTTP Requests. +interface incoming-handler { + use types.{incoming-request, response-outparam}; + + /// This function is invoked with an incoming HTTP Request, and a resource + /// `response-outparam` which provides the capability to reply with an HTTP + /// Response. The response is sent by calling the `response-outparam.set` + /// method, which allows execution to continue after the response has been + /// sent. This enables both streaming to the response body, and performing other + /// work. + /// + /// The implementor of this function must write a response to the + /// `response-outparam` before returning, or else the caller will respond + /// with an error on its behalf. + handle: func(request: incoming-request, response-out: response-outparam); +} + +/// This interface defines a handler of outgoing HTTP Requests. It should be +/// imported by components which wish to make HTTP Requests. +interface outgoing-handler { + use types.{outgoing-request, request-options, future-incoming-response, error-code}; + + /// This function is invoked with an outgoing HTTP Request, and it returns + /// a resource `future-incoming-response` which represents an HTTP Response + /// which may arrive in the future. + /// + /// The `options` argument accepts optional parameters for the HTTP + /// protocol's transport layer. + /// + /// This function may return an error if the `outgoing-request` is invalid + /// or not allowed to be made. Otherwise, protocol errors are reported + /// through the `future-incoming-response`. + handle: func(request: outgoing-request, options: option) -> result; +} + +/// The `wasi:http/proxy` world captures a widely-implementable intersection of +/// hosts that includes HTTP forward and reverse proxies. Components targeting +/// this world may concurrently stream in and out any number of incoming and +/// outgoing HTTP requests. +world proxy { + import wasi:random/random@0.2.0; + import wasi:io/error@0.2.0; + import wasi:io/poll@0.2.0; + import wasi:io/streams@0.2.0; + import wasi:cli/stdout@0.2.0; + import wasi:cli/stderr@0.2.0; + import wasi:cli/stdin@0.2.0; + import wasi:clocks/monotonic-clock@0.2.0; + import types; + import outgoing-handler; + import wasi:clocks/wall-clock@0.2.0; + + export incoming-handler; +} diff --git a/examples/runtimes/nodejs/wit/deps/wasi-io-0.2.0/package.wit b/examples/runtimes/nodejs/wit/deps/wasi-io-0.2.0/package.wit new file mode 100644 index 0000000..1840029 --- /dev/null +++ b/examples/runtimes/nodejs/wit/deps/wasi-io-0.2.0/package.wit @@ -0,0 +1,48 @@ +package wasi:io@0.2.0; + +interface error { + resource error { + to-debug-string: func() -> string; + } +} + +interface poll { + resource pollable { + ready: func() -> bool; + block: func(); + } + + poll: func(in: list>) -> list; +} + +interface streams { + use error.{error}; + use poll.{pollable}; + + variant stream-error { + last-operation-failed(error), + closed, + } + + resource input-stream { + read: func(len: u64) -> result, stream-error>; + blocking-read: func(len: u64) -> result, stream-error>; + skip: func(len: u64) -> result; + blocking-skip: func(len: u64) -> result; + subscribe: func() -> pollable; + } + + resource output-stream { + check-write: func() -> result; + write: func(contents: list) -> result<_, stream-error>; + blocking-write-and-flush: func(contents: list) -> result<_, stream-error>; + flush: func() -> result<_, stream-error>; + blocking-flush: func() -> result<_, stream-error>; + subscribe: func() -> pollable; + write-zeroes: func(len: u64) -> result<_, stream-error>; + blocking-write-zeroes-and-flush: func(len: u64) -> result<_, stream-error>; + splice: func(src: borrow, len: u64) -> result; + blocking-splice: func(src: borrow, len: u64) -> result; + } +} + diff --git a/examples/runtimes/nodejs/wit/deps/wasi-random-0.2.0/package.wit b/examples/runtimes/nodejs/wit/deps/wasi-random-0.2.0/package.wit new file mode 100644 index 0000000..4132731 --- /dev/null +++ b/examples/runtimes/nodejs/wit/deps/wasi-random-0.2.0/package.wit @@ -0,0 +1,8 @@ +package wasi:random@0.2.0; + +interface random { + get-random-bytes: func(len: u64) -> list; + + get-random-u64: func() -> u64; +} + diff --git a/examples/runtimes/nodejs/wit/world.wit b/examples/runtimes/nodejs/wit/world.wit new file mode 100644 index 0000000..7bb99e0 --- /dev/null +++ b/examples/runtimes/nodejs/wit/world.wit @@ -0,0 +1,18 @@ +// Fabricks Node.js HTTP Handler World +// +// This defines the interface for JavaScript HTTP applications. +// The runtime implements wasi:http/incoming-handler to serve requests +// and uses WASI filesystem to load user code from /app at runtime. + +package fabricks:nodejs-runtime@20.0.0; + +world http-handler { + // Include the WASI HTTP proxy world which provides: + // - wasi:http/incoming-handler (export) - handle incoming HTTP requests + // - wasi:http/outgoing-handler (import) - make outgoing HTTP requests + include wasi:http/proxy@0.2.0; + + // Import filesystem interfaces to load user code from /app + import wasi:filesystem/types@0.2.0; + import wasi:filesystem/preopens@0.2.0; +} diff --git a/examples/runtimes/nodejs/wkg.lock b/examples/runtimes/nodejs/wkg.lock new file mode 100644 index 0000000..a9c25a2 --- /dev/null +++ b/examples/runtimes/nodejs/wkg.lock @@ -0,0 +1,21 @@ +# This file is automatically generated. +# It is not intended for manual editing. +version = 1 + +[[packages]] +name = "wasi:filesystem" +registry = "wasi.dev" + +[[packages.versions]] +requirement = "=0.2.0" +version = "0.2.0" +digest = "sha256:39c6e0f5618a6b6c8bdf5c39035a048f666c0ed5f44fe04ed172f010ab0c36d4" + +[[packages]] +name = "wasi:http" +registry = "wasi.dev" + +[[packages.versions]] +requirement = "=0.2.0" +version = "0.2.0" +digest = "sha256:5a568e6e2d60c1ce51220e1833cdd5b88db9f615720edc762a9b4a6f36b383bd" diff --git a/fabricks-e2e/src/helpers.rs b/fabricks-e2e/src/helpers.rs index 985d955..e828d35 100644 --- a/fabricks-e2e/src/helpers.rs +++ b/fabricks-e2e/src/helpers.rs @@ -34,7 +34,7 @@ impl TestEnv { config.daemon.data_dir = temp_dir.path().join("data"); config.daemon.socket = temp_dir.path().join("fabricks.sock"); - let state = AppState::new(config)?; + let state = AppState::new(config).await?; state.initialize().await?; Ok(Self { diff --git a/fabricks/src/commands/run.rs b/fabricks/src/commands/run.rs index 0476e54..ba0f150 100644 --- a/fabricks/src/commands/run.rs +++ b/fabricks/src/commands/run.rs @@ -4,13 +4,12 @@ //! daemon to enforce security boundaries, capability restrictions, and network //! isolation. -use std::path::Path; - -use anyhow::{Context, Result, bail}; +use anyhow::{Context, Result}; use tracing::info; use crate::cli::RunArgs; -use crate::daemon_client::{DaemonClient, RunModuleRequest, RunFabrickfileRequest}; +use crate::commands::service::{ModuleSource, resolve_module_reference}; +use crate::daemon_client::{DaemonClient, RunModuleRequest}; use crate::output::{writeln, writeln_stderr}; /// Run the run command. @@ -27,87 +26,10 @@ use crate::output::{writeln, writeln_stderr}; /// - The module cannot be found or loaded /// - The module execution fails pub async fn run(args: &RunArgs) -> Result<()> { - // Check if we're pointing to a directory with a Fabrickfile or to a Fabrickfile directly - if is_fabrickfile_reference(&args.module) { - return run_fabrickfile_via_daemon(&args.module).await; - } - - // Otherwise, it's a module reference (tag or registry reference) - run_module_via_daemon(args).await -} - -/// Checks if the reference points to a Fabrickfile. -fn is_fabrickfile_reference(reference: &str) -> bool { - let path = Path::new(reference); - - // Direct path to a Fabrickfile - if path.is_file() && path.file_name().is_some_and(|n| n == "Fabrickfile") { - return true; - } - - // Directory containing a Fabrickfile - if path.is_dir() && path.join("Fabrickfile").exists() { - return true; - } - - false -} - -/// Run a Fabrickfile through the daemon. -async fn run_fabrickfile_via_daemon(reference: &str) -> Result<()> { - let path = Path::new(reference); - - // Resolve the Fabrickfile path - let fabrickfile_path = if path.is_dir() { - path.join("Fabrickfile") - } else { - path.to_path_buf() - }; - - // Canonicalize the path - let absolute_path = fabrickfile_path - .canonicalize() - .with_context(|| format!("Failed to resolve path: {}", fabrickfile_path.display()))?; - - writeln_stderr(&format!( - "Running {} via daemon...", - fabrickfile_path.display() - ))?; - - let client = DaemonClient::new(); - - let response = client - .run_fabrickfile(RunFabrickfileRequest { - fabrickfile_path: absolute_path, - wasm_path: None, - }) - .await - .context("Failed to run via daemon. Is the daemon running? Start it with: fabricksd")?; + // Resolve the module reference (builds and stores in OCI if needed) + let ModuleSource::Storage { tag, .. } = resolve_module_reference(&args.module).await?; - writeln(&format!( - "Service '{}' started with ID: {}", - response.name, response.id - ))?; - - info!("Service started via daemon: {}", response.id); - - Ok(()) -} - -/// Run a module (by tag or registry reference) through the daemon. -async fn run_module_via_daemon(args: &RunArgs) -> Result<()> { - let reference = &args.module; - - // Check if it looks like a registry reference (contains a slash) - if reference.contains('/') && !Path::new(reference).exists() { - // TODO: Implement pull-on-demand in daemon - bail!( - "Registry references are not yet supported for `run`.\n\ - Use `fabricks pull {reference}` first, then run with the local tag." - ); - } - - writeln_stderr(&format!("Running {reference} via daemon..."))?; + writeln_stderr(&format!("Running {tag} via daemon..."))?; let client = DaemonClient::new(); @@ -124,7 +46,7 @@ async fn run_module_via_daemon(args: &RunArgs) -> Result<()> { let response = client .run_module(RunModuleRequest { - reference: reference.clone(), + reference: tag, args: args.args.clone(), env_vars, no_capabilities: args.no_capabilities, @@ -141,39 +63,3 @@ async fn run_module_via_daemon(args: &RunArgs) -> Result<()> { Ok(()) } - -#[cfg(test)] -mod tests { - use super::*; - use tempfile::TempDir; - - #[test] - fn test_is_fabrickfile_reference_direct_path() { - let temp = TempDir::new().expect("create temp dir"); - let fabrickfile_path = temp.path().join("Fabrickfile"); - std::fs::write(&fabrickfile_path, "fabrick_version = \"1.0\"").expect("write"); - - assert!(is_fabrickfile_reference(fabrickfile_path.to_str().unwrap())); - } - - #[test] - fn test_is_fabrickfile_reference_directory() { - let temp = TempDir::new().expect("create temp dir"); - let fabrickfile_path = temp.path().join("Fabrickfile"); - std::fs::write(&fabrickfile_path, "fabrick_version = \"1.0\"").expect("write"); - - assert!(is_fabrickfile_reference(temp.path().to_str().unwrap())); - } - - #[test] - fn test_is_fabrickfile_reference_tag() { - assert!(!is_fabrickfile_reference("my-module:1.0.0")); - assert!(!is_fabrickfile_reference("python-hello:latest")); - } - - #[test] - fn test_is_fabrickfile_reference_registry() { - assert!(!is_fabrickfile_reference("ghcr.io/user/module:latest")); - assert!(!is_fabrickfile_reference("docker.io/library/nginx:1.0")); - } -} diff --git a/fabricks/src/commands/service.rs b/fabricks/src/commands/service.rs index b2d3b64..750fbfb 100644 --- a/fabricks/src/commands/service.rs +++ b/fabricks/src/commands/service.rs @@ -1,25 +1,28 @@ //! Service command implementation. -use std::io::Write; use std::path::Path; use std::process::Command; use anyhow::{Context, Result, bail}; use fabricks_common::{Fabrickfile, parse_fabrickfile}; use fabricks_oci::{FabricksModule, LocalStorage}; -use tempfile::NamedTempFile; use tracing::{debug, info}; use crate::cli::{OutputFormat, ServiceArgs, ServiceCommands}; -use crate::daemon_client::{DaemonClient, RunFabrickfileRequest}; +use crate::daemon_client::DaemonClient; use crate::output; /// Source of a resolved module. -enum ModuleSource { +pub enum ModuleSource { /// A module loaded from local storage. Storage { + /// The module tag/reference. tag: String, + /// The Fabrickfile configuration (kept for potential future use). + #[allow(dead_code)] fabrickfile: Fabrickfile, + /// The WASM bytes (kept for potential future use). + #[allow(dead_code)] wasm_bytes: Vec, }, } @@ -54,7 +57,7 @@ pub async fn run(args: &ServiceArgs) -> Result<()> { /// - Local file path (exists on filesystem) -> build if needed, store, load from storage /// - Registry reference (contains '/') -> not yet supported /// - Local storage tag (e.g., "hello-http:0.1.0") -> load from storage -async fn resolve_module_reference(reference: &str) -> Result { +pub async fn resolve_module_reference(reference: &str) -> Result { let path = Path::new(reference); // 1. Local file/directory path @@ -333,30 +336,20 @@ fn get_local_storage() -> Result { /// Run a service from a module reference. async fn run_service(client: &DaemonClient, reference: &str, format: OutputFormat) -> Result<()> { - let ModuleSource::Storage { - tag, - fabrickfile, - wasm_bytes, - } = resolve_module_reference(reference).await?; - - // Write WASM to a temp file and create a temp Fabrickfile - // (daemon currently expects file paths) - let wasm_temp = write_temp_wasm(&wasm_bytes)?; - let fabrickfile_temp = write_temp_fabrickfile(&fabrickfile)?; + let ModuleSource::Storage { tag, .. } = resolve_module_reference(reference).await?; output::writeln(&format!("Running module: {tag}"))?; - let req = RunFabrickfileRequest { - fabrickfile_path: fabrickfile_temp.path().to_path_buf(), - wasm_path: Some(wasm_temp.path().to_path_buf()), + // Use run_module endpoint which properly handles multi-layer modules + // (runtime + source layers for interpreted runtimes like Python/JavaScript) + let req = crate::daemon_client::RunModuleRequest { + reference: tag.clone(), + args: Vec::new(), + env_vars: Vec::new(), + no_capabilities: false, }; - // Keep temp files alive during the request - let response = client.run_fabrickfile(req).await?; - - // Temp files are dropped here after daemon has read them - drop(wasm_temp); - drop(fabrickfile_temp); + let response = client.run_module(req).await?; match format { OutputFormat::Json => { @@ -375,26 +368,6 @@ async fn run_service(client: &DaemonClient, reference: &str, format: OutputForma Ok(()) } -/// Write WASM bytes to a temporary file. -fn write_temp_wasm(wasm_bytes: &[u8]) -> Result { - let mut temp = NamedTempFile::with_suffix(".wasm").context("Failed to create temp file")?; - temp.write_all(wasm_bytes) - .context("Failed to write WASM to temp file")?; - temp.flush().context("Failed to flush temp file")?; - Ok(temp) -} - -/// Write Fabrickfile to a temporary file. -fn write_temp_fabrickfile(fabrickfile: &Fabrickfile) -> Result { - let mut temp = NamedTempFile::with_suffix(".toml").context("Failed to create temp file")?; - let toml_content = - toml::to_string_pretty(fabrickfile).context("Failed to serialize Fabrickfile")?; - temp.write_all(toml_content.as_bytes()) - .context("Failed to write Fabrickfile to temp file")?; - temp.flush().context("Failed to flush temp file")?; - Ok(temp) -} - async fn inspect_service(client: &DaemonClient, id: &str, format: OutputFormat) -> Result<()> { let detail = client.get_service(id).await?; diff --git a/fabricks/src/daemon_client.rs b/fabricks/src/daemon_client.rs index 6ab0f40..7b46a6c 100644 --- a/fabricks/src/daemon_client.rs +++ b/fabricks/src/daemon_client.rs @@ -185,16 +185,6 @@ pub struct CreateServiceResponse { pub name: String, } -/// Run Fabrickfile request. -#[derive(Debug, serde::Serialize)] -pub struct RunFabrickfileRequest { - /// Path to Fabrickfile. - pub fabrickfile_path: PathBuf, - /// Optional path to pre-built WASM. - #[serde(skip_serializing_if = "Option::is_none")] - pub wasm_path: Option, -} - /// Run module request (by tag or registry reference). #[derive(Debug, serde::Serialize)] pub struct RunModuleRequest { @@ -464,14 +454,6 @@ impl DaemonClient { self.get(&format!("/v1/services/{id}")).await } - /// Runs a Fabrickfile through the daemon. - pub async fn run_fabrickfile( - &self, - req: RunFabrickfileRequest, - ) -> Result { - self.post("/v1/services/run", &req).await - } - /// Runs a module by tag or registry reference through the daemon. pub async fn run_module(&self, req: RunModuleRequest) -> Result { self.post("/v1/services/run-module", &req).await diff --git a/fabricksd/src/api/handlers/services.rs b/fabricksd/src/api/handlers/services.rs index 23e766e..3aa4734 100644 --- a/fabricksd/src/api/handlers/services.rs +++ b/fabricksd/src/api/handlers/services.rs @@ -10,11 +10,10 @@ use serde::{Deserialize, Serialize}; use crate::api::response::ApiResponse; use crate::error::DaemonError; -use crate::network::NetworkConfig; use crate::service::{ServiceConfig, ServiceDetail, ServiceInfo}; use crate::state::AppState; use crate::volume::VolumeMount; -use fabricks_common::models::Replicas; +use fabricks_common::Fabrickfile; /// Request to create a service. #[derive(Debug, Deserialize)] @@ -47,17 +46,6 @@ pub struct CreateServiceResponse { pub name: String, } -/// Request to run a Fabrickfile. -#[derive(Debug, Deserialize)] -pub struct RunFabrickfileRequest { - /// Path to the Fabrickfile. - pub fabrickfile_path: PathBuf, - - /// Optional path to pre-built WASM. - #[serde(default)] - pub wasm_path: Option, -} - /// Request to run a module by tag. #[derive(Debug, Deserialize)] pub struct RunModuleRequest { @@ -123,36 +111,6 @@ pub async fn create_service( } } -/// POST `/v1/services/run` -/// -/// Runs a Fabrickfile (creates and starts a service). -pub async fn run_fabrickfile( - State(state): State, - Json(req): Json, -) -> Json> { - let manager = state.service_manager.write().await; - - match manager - .run_fabrickfile(&req.fabrickfile_path, req.wasm_path.as_deref()) - .await - { - Ok(id) => { - // Get service info to return name - match manager.get_service(&id).await { - Ok(detail) => Json(ApiResponse::success(CreateServiceResponse { - id, - name: detail.name, - })), - Err(_) => Json(ApiResponse::success(CreateServiceResponse { - id, - name: "unknown".to_string(), - })), - } - } - Err(e) => Json(error_response(&e)), - } -} - /// POST `/v1/services/run-module` /// /// Runs a module from OCI storage by tag/reference. @@ -211,7 +169,7 @@ pub async fn run_module( // Extract each source layer (they stack, later overrides earlier) for source_layer in module.source_layers() { - if let Err(e) = extract_tar_gz(&source_layer.data, &app_dir).await { + if let Err(e) = extract_tar_gz(&source_layer.data, &app_dir) { return Json(error_response(&DaemonError::OciStorageError(format!( "failed to extract source layer: {e}" )))); @@ -223,58 +181,14 @@ pub async fn run_module( None }; - // Build environment from config and overrides - let mut environment = fabrickfile - .config - .as_ref() - .and_then(|c| c.environment.clone()) - .unwrap_or_default(); - - // Apply environment overrides from request - for (key, value) in &req.env_vars { - environment.insert(key.clone(), value.clone()); - } - - // Compute digest - let digest = compute_digest(wasm_bytes); - - // Build volume mounts for interpreted runtimes - let volumes = if let Some(ref app_dir) = source_mount_path { - vec![VolumeMount::read_only( - "app-source".to_string(), - "app-source".to_string(), - app_dir.clone(), - "/app".to_string(), - )] - } else { - Vec::new() - }; - // Build service config - let config = ServiceConfig { - name: fabrickfile.info.name.clone(), - version: fabrickfile.info.version.clone(), - service_type: fabrickfile.info.service_type, + let config = build_service_config( + fabrickfile, wasm_path, - wasm_digest: digest, - capabilities: if req.no_capabilities { - fabricks_common::Capabilities::default() - } else { - fabrickfile.capabilities.clone() - }, - environment, - args: req.args.clone(), - resources: fabrickfile - .config - .as_ref() - .and_then(|c| c.resources.clone()), - replicas: Replicas::default(), - health_check: fabrickfile.health_check.clone(), - depends_on: Vec::new(), - networks: Vec::new(), - volumes, - mortar_project: None, - }; + wasm_bytes, + source_mount_path, + &req, + ); let manager = state.service_manager.write().await; @@ -398,7 +312,7 @@ fn compute_digest(bytes: &[u8]) -> String { } /// Extracts a gzipped tar archive to a directory. -async fn extract_tar_gz(data: &[u8], dest: &std::path::Path) -> std::io::Result<()> { +fn extract_tar_gz(data: &[u8], dest: &std::path::Path) -> std::io::Result<()> { use std::io::Cursor; use flate2::read::GzDecoder; use tar::Archive; @@ -413,6 +327,61 @@ async fn extract_tar_gz(data: &[u8], dest: &std::path::Path) -> std::io::Result< Ok(()) } +/// Builds a service configuration from a Fabrickfile and module data. +fn build_service_config( + fabrickfile: &Fabrickfile, + wasm_path: PathBuf, + wasm_bytes: &[u8], + source_mount_path: Option, + req: &RunModuleRequest, +) -> ServiceConfig { + let digest = compute_digest(wasm_bytes); + + // Merge environment variables from Fabrickfile and request + let mut environment: std::collections::HashMap = fabrickfile + .config + .as_ref() + .and_then(|c| c.environment.clone()) + .unwrap_or_default(); + for (key, value) in &req.env_vars { + environment.insert(key.clone(), value.clone()); + } + + // Add /app volume mount for interpreted modules (read-only source) + let volumes = source_mount_path + .map(|path| { + vec![VolumeMount::read_only( + "app-source".to_string(), + "app-source".to_string(), + path, + "/app".to_string(), + )] + }) + .unwrap_or_default(); + + ServiceConfig { + name: fabrickfile.info.name.clone(), + version: fabrickfile.info.version.clone(), + service_type: fabrickfile.info.service_type, + wasm_path, + wasm_digest: digest, + capabilities: if req.no_capabilities { + fabricks_common::Capabilities::default() + } else { + fabrickfile.capabilities.clone() + }, + environment, + args: req.args.clone(), + resources: fabrickfile.config.as_ref().and_then(|c| c.resources.clone()), + replicas: fabricks_common::models::Replicas::default(), + health_check: fabrickfile.health_check.clone(), + depends_on: Vec::new(), + networks: Vec::new(), + volumes, + mortar_project: None, + } +} + /// Converts a `DaemonError` to an API error response. fn error_response(err: &DaemonError) -> ApiResponse { let (code, message) = match err { diff --git a/fabricksd/src/api/router.rs b/fabricksd/src/api/router.rs index a19967f..d54be33 100644 --- a/fabricksd/src/api/router.rs +++ b/fabricksd/src/api/router.rs @@ -21,10 +21,6 @@ pub fn build_router(state: AppState) -> Router { // Service management .route("/v1/services", get(handlers::services::list_services)) .route("/v1/services", post(handlers::services::create_service)) - .route( - "/v1/services/run", - post(handlers::services::run_fabrickfile), - ) .route( "/v1/services/run-module", post(handlers::services::run_module), diff --git a/fabricksd/src/service/manager.rs b/fabricksd/src/service/manager.rs index 2a97d6f..9770553 100644 --- a/fabricksd/src/service/manager.rs +++ b/fabricksd/src/service/manager.rs @@ -189,87 +189,6 @@ impl ServiceManager { Ok(id) } - /// Creates and starts a service from a Fabrickfile. - /// - /// This is a convenience method that parses the Fabrickfile, creates the service, - /// and starts it in one operation. - /// - /// # Arguments - /// - /// * `fabrickfile_path` - Path to the Fabrickfile - /// * `wasm_path` - Optional path to pre-built WASM (if not specified, uses build.output) - /// - /// # Errors - /// - /// Returns an error if parsing, creation, or startup fails. - pub async fn run_fabrickfile( - &self, - fabrickfile_path: &Path, - wasm_path: Option<&Path>, - ) -> Result { - // Parse Fabrickfile - let content = tokio::fs::read_to_string(fabrickfile_path) - .await - .map_err(|e| DaemonError::FabrickfileParseError(e.to_string()))?; - - let fabrickfile: Fabrickfile = toml::from_str(&content) - .map_err(|e| DaemonError::FabrickfileParseError(e.to_string()))?; - - // Determine WASM path - let wasm_path = if let Some(path) = wasm_path { - path.to_path_buf() - } else if let Some(ref build) = fabrickfile.build { - // Resolve relative to Fabrickfile directory - let base_dir = fabrickfile_path.parent().unwrap_or_else(|| Path::new(".")); - base_dir.join(&build.output) - } else { - return Err(DaemonError::FabrickfileParseError( - "No WASM path specified and no build configuration found".to_string(), - )); - }; - - // Compute digest - let wasm_bytes = - tokio::fs::read(&wasm_path) - .await - .map_err(|_| DaemonError::WasmModuleNotFound { - path: wasm_path.display().to_string(), - })?; - let digest = compute_digest(&wasm_bytes); - - // Build config from Fabrickfile - let config = ServiceConfig { - name: fabrickfile.info.name.clone(), - version: fabrickfile.info.version.clone(), - service_type: fabrickfile.info.service_type, - wasm_path, - wasm_digest: digest, - capabilities: fabrickfile.capabilities.clone(), - environment: fabrickfile - .config - .as_ref() - .and_then(|c| c.environment.clone()) - .unwrap_or_default(), - args: Vec::new(), - resources: fabrickfile - .config - .as_ref() - .and_then(|c| c.resources.clone()), - replicas: fabricks_common::models::Replicas::default(), - health_check: fabrickfile.health_check.clone(), - depends_on: Vec::new(), - networks: Vec::new(), - volumes: Vec::new(), - mortar_project: None, - }; - - // Create and start - let id = self.create_service(config).await?; - self.start_service(&id).await?; - - Ok(id) - } - /// Starts a service. /// /// # Errors diff --git a/fabricksd/src/state.rs b/fabricksd/src/state.rs index 830842f..ee2ccb3 100644 --- a/fabricksd/src/state.rs +++ b/fabricksd/src/state.rs @@ -178,10 +178,9 @@ impl AppState { )))? .join(".fabricks/storage"); let oci_storage = Arc::new(LocalStorage::new(storage_path).await.map_err(|e| { - DaemonError::IoError(std::io::Error::new( - std::io::ErrorKind::Other, - format!("failed to initialize OCI storage: {e}"), - )) + DaemonError::IoError(std::io::Error::other(format!( + "failed to initialize OCI storage: {e}" + ))) })?); Ok(Self {