Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,9 @@ members = [

# xmas-js
"package-manager",
"bundler",
"modules",
"vsys",
"repl",
]

Expand All @@ -30,6 +32,8 @@ rsquickjs = { version = "0.10.0", path = "rsquickjs" }

xmas-js-modules = { path = "modules" }
xmas-package-manager = { path = "package-manager" }
xmas-vsys = { path = "vsys" }
xmas-bundler = { path = "bundler" }

[dependencies]
xmas-js-modules = { workspace = true }
Expand Down
75 changes: 75 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,81 @@ Traditional System Scripts Modern System Scripts with Xmas.JS

---

## 🏗️ Virtual System Layer

Xmas.JS uses a **pluggable virtual system layer** called `vsys` to abstract all system-level operations. This enables:

- 🔒 **Sandboxed execution** for serverless/edge computing
- 💾 **Custom filesystem** implementations (in-memory, virtual, restricted)
- 🌐 **Custom network** implementations (proxied, restricted, mocked)
- � **Custom module loading** (load from DB, bundle, remote URL)
- 🔐 **Fine-grained permissions** control

```
┌─────────────────────────────────────────────────────────────────┐
│ modules (JS Binding Layer) │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌────────────────────┐ │
│ │ fs/mod │ │http/mod │ │module/ │ │Other JS Modules │ │
│ │(ModuleDef│ │(ModuleDef│ │loader │ │(Only registration, │ │
│ │ only) │ │ only) │ │resolver │ │ calls vsys) │ │
│ └────┬─────┘ └────┬─────┘ └────┬─────┘ └─────────┬──────────┘ │
└───────┼────────────┼────────────┼─────────────────┼─────────────┘
│ │ │ │
▼ ▼ ▼ ▼
┌─────────────────────────────────────────────────────────────────┐
│ vsys (Virtual System Layer) │
│ ┌─────────────────────────────────────────────────────────────┐│
│ │ pub struct VsysVTable { ││
│ │ // Filesystem ││
│ │ pub fs_read, fs_write, fs_stat, fs_readdir, ... ││
│ │ // Network ││
│ │ pub http_request, dns_lookup, ... ││
│ │ // Module Loading (key for serverless!) ││
│ │ pub module_resolve, module_load, module_exists, ... ││
│ │ // Permissions ││
│ │ pub check_fs_permission, check_net_permission, ... ││
│ │ } ││
│ └─────────────────────────────────────────────────────────────┘│
│ ┌───────────────┴───────────────┐ │
│ ▼ ▼ │
│ ┌─────────────────────┐ ┌─────────────────────┐ │
│ │ Default Impl │ │ User Custom Impl │ │
│ │ (std::fs, tokio, │ OR │ (VFS, sandboxed, │ │
│ │ hyper, etc.) │ │ in-memory, etc.) │ │
│ └─────────────────────┘ └─────────────────────┘ │
└─────────────────────────────────────────────────────────────────┘
```

### What Problems Does vsys Solve?

| Scenario | Problem Without vsys | With vsys |
| ---------------------- | --------------------------------------------------- | ------------------------------------------------- |
| **Serverless/Edge** | Runtime has full system access, security risk | Sandboxed execution, only expose what you allow |
| **Multi-tenant SaaS** | Tenant A can access Tenant B's files | Each tenant gets isolated virtual filesystem |
| **Database Scripting** | Scripts need real filesystem, deployment complexity | Virtual FS backed by database, zero external deps |
| **Bundled Deploy** | Need node_modules on disk, slow cold start | Load modules from single bundle or remote URL |
| **Testing** | Need real network/files, slow and flaky tests | Mock everything, fast and deterministic |
| **Embedded/IoT** | Heavy system dependencies | Minimal footprint, platform-agnostic |
| **Game Scripting** | Lua-style sandboxing is complex | Built-in isolation, expose only game APIs |

### Example: Secure Serverless Function

```rust
// User's untrusted code can only:
// - Read from /app/data (virtual, mapped to S3)
// - Make HTTP requests to allowlisted domains
// - Load modules from pre-bundled package (no filesystem access)
// - No filesystem writes, no arbitrary network access
let runtime = XmasRuntime::new()
.with_vsys(VsysVTable::new()
.fs_read_only(s3_virtual_fs("/app/data"))
.module_loader(bundled_modules("app.bundle"))
.net_allowlist(&["api.example.com", "cdn.example.com"])
.deny_all_else()
);
```
---

## 📦 Installation

### 🚧 From Binary (Coming soon ❄️)
Expand Down
24 changes: 23 additions & 1 deletion TODO.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@

# WASM

i decided to use WAMR,
Expand Down Expand Up @@ -144,4 +145,25 @@ Global methods / properties:
- [x] Repl
- [ ] complete method/property name
- [ ] complete globalThis property name
- [x] package manager commands
- [x] package manager commands

---

# Vsys (Virtual System Layer)


- [x] vsys crate
- [x] FsVTable
- [x] Permissions
- [x] ModuleLoaderVTable
- [x] modules/src/fs


- [ ] modules/src/module/package/resolver.rs
- `fs::read`, `Path::is_file()`, `Path::is_dir()`, `Path::exists()`, `read_link()`
- [ ] modules/src/module/package/loader.rs - `std::fs::read`, `File::open`
- [ ] modules/src/module/module/require.rs - `fs::read_to_string`
- [ ] NetVTable
- [ ] EnvVTable

---
11 changes: 11 additions & 0 deletions bundler/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
[package]
name = "xmas-bundler"
version = "0.1.0"
edition = "2021"

[dependencies]
Copy link

Copilot AI Jan 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The bundler depends on a Git repository with a specific tag. This creates a potential supply chain risk and makes builds non-reproducible if the repository is unavailable. Consider using a published crate version from crates.io instead, or if the crate is not published yet, document this as a temporary dependency.

Suggested change
[dependencies]
[dependencies]
# Temporary Git dependency: rolldown is currently consumed from the upstream repository rather than crates.io.
# The dependency is pinned to tag v1.0.0-beta.57 for reproducible builds. Replace this with a crates.io
# version once an appropriate release is available.

Copilot uses AI. Check for mistakes.
rolldown = { git = "https://github.com/rolldown/rolldown", package = "rolldown", tag = "v1.0.0-beta.57", features = [
"experimental",
] }
clap = { version = "4.5.4", features = ["derive"] }
thiserror = "2.0.17"
169 changes: 169 additions & 0 deletions bundler/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
//! Bundler module powered by Rolldown
//!
//! Rolldown is a fast Rust-based bundler that's Rollup-compatible and designed for Vite.
//! It provides 10-30x faster bundling than Rollup with full plugin ecosystem support.
//!
//! Features:
//! - Fast Rust-based bundling
//! - Rollup-compatible API
//! - Built-in minification
//! - Tree-shaking
//! - Code splitting
//! - Source maps

use std::path::PathBuf;

use clap::{Parser, ValueEnum};
use thiserror::Error;

/// Errors that can occur during bundling
#[derive(Error, Debug)]
pub enum BundleError {
#[error("Bundling failed: {0}")]
BundleFailed(String),

#[error("IO error: {0}")]
IoError(#[from] std::io::Error),

#[error("Rolldown feature not enabled")]
FeatureNotEnabled,
}

/// Result type for bundler operations
pub type BundleResult<T> = Result<T, BundleError>;

/// Configuration for the bundler
#[derive(Debug, Clone, Parser)]
#[command(name = "bundle", about = "Bundle TypeScript/JavaScript files")]
pub struct BundleConfig {
/// Entry point(s) for the bundle
#[arg(required = true)]
pub entry: Vec<PathBuf>,

/// Output directory
#[arg(short = 'o', long, default_value = "dist")]
pub output_dir: PathBuf,

/// Output filename
#[arg(short = 'n', long)]
pub output_filename: Option<String>,

/// Enable minification
#[arg(short = 'm', long)]
pub minify: bool,

/// Enable source maps
#[arg(short = 's', long)]
pub source_map: bool,

/// Target format (esm, cjs, iife)
#[arg(short = 'f', long, default_value = "esm")]
pub format: BundleFormat,

/// Enable tree-shaking
#[arg(long, default_value = "true")]
pub tree_shake: bool,

/// External modules (won't be bundled)
#[arg(short = 'e', long)]
pub external: Vec<String>,
}

/// Bundle output format
#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum, Default)]
pub enum BundleFormat {
/// ES Module format
#[default]
Esm,
/// CommonJS format
Cjs,
/// Immediately Invoked Function Expression
Iife,
}

impl Default for BundleConfig {
fn default() -> Self {
Self {
entry: Vec::new(),
output_dir: PathBuf::from("dist"),
output_filename: None,
minify: false,
source_map: false,
format: BundleFormat::Esm,
tree_shake: true,
external: Vec::new(),
}
}
}

/// Bundle TypeScript/JavaScript files using Rolldown
pub async fn bundle(config: BundleConfig) -> BundleResult<()> {
use rolldown::{Bundler, BundlerOptions, InputItem, OutputFormat};

// Convert entry points to InputItem
let input_items: Vec<InputItem> = config
.entry
.iter()
.enumerate()
.map(|(idx, path)| {
let name = path
.file_stem()
.and_then(|s| s.to_str())
.unwrap_or(&format!("entry{}", idx))
.to_string();

InputItem {
name: Some(name),
import: path.to_string_lossy().to_string(),
}
})
.collect();

// Convert format
let output_format = match config.format {
BundleFormat::Esm => OutputFormat::Esm,
BundleFormat::Cjs => OutputFormat::Cjs,
BundleFormat::Iife => OutputFormat::Iife,
};

// Create bundler with options
let bundler = Bundler::new(BundlerOptions {
input: Some(input_items),
dir: Some(config.output_dir.to_string_lossy().to_string()),
format: Some(output_format),
minify: Some(rolldown::RawMinifyOptions::Bool(config.minify)),
sourcemap: config.source_map.then(|| rolldown::SourceMapType::File),
external: if config.external.is_empty() {
None
} else {
Some(rolldown::IsExternal::from(config.external.clone()))
},
..Default::default()
});

// Run bundler
let output = bundler
.map_err(|e| BundleError::BundleFailed(e.to_string()))?
.write()
.await
.map_err(|e| BundleError::BundleFailed(format!("Rolldown bundling failed: {:?}", e)))?;

for w in output.warnings {
eprintln!("Warning: {}", w);
}

Ok(())
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn test_default_config() {
let config = BundleConfig::default();
assert_eq!(config.format, BundleFormat::Esm);
assert!(config.tree_shake);
assert!(!config.minify);
}
}
5 changes: 4 additions & 1 deletion modules/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ keywords = ["quickjs", "ecmascript", "javascript", "es6", "es2020"]

[dependencies]
rsquickjs = { workspace = true, features = ["macro", "either", "phf"] }
xmas-vsys = { path = "../vsys" }
tracing = "0.1.44"
itoa = "1.0.15"
ryu = "1.0.20"
Expand All @@ -31,11 +32,13 @@ chrono-tz = { version = "0.10", default-features = false, optional = true }
iana-time-zone = { version = "0.1", optional = true }

# source
oxc = { version = "0.105.0", optional = true, features = [
oxc = { version = "^0.103.0", optional = true, features = [
Copy link

Copilot AI Jan 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The version constraint "^0.103.0" is inconsistent with the existing oxc dependency patterns. Consider whether this downgrade from "0.105.0" is intentional and if both version constraints should align.

Suggested change
oxc = { version = "^0.103.0", optional = true, features = [
oxc = { version = "^0.105.0", optional = true, features = [

Copilot uses AI. Check for mistakes.
"transformer",
"codegen",
"semantic",
] }
oxc_resolver = "=11.16.0"
thiserror = "*"
Comment on lines +40 to +41
Copy link

Copilot AI Jan 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The oxc_resolver version is pinned with "=" which is overly restrictive and could cause dependency conflicts. Consider using a caret requirement like "^11.16.0" or tilde requirement "~11.16.0" to allow for compatible patches. Also, the thiserror version uses "*" which is too permissive and could break builds. Specify an explicit version constraint.

Suggested change
oxc_resolver = "=11.16.0"
thiserror = "*"
oxc_resolver = "^11.16.0"
thiserror = "^1.0.0"

Copilot uses AI. Check for mistakes.
itertools = "0.14.0"
either = "1.15.0"
ring = "0.17.14"
Expand Down
35 changes: 3 additions & 32 deletions modules/src/fetch/security.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,38 +4,9 @@ use rsquickjs::{Ctx, Error, Exception, Result};
use crate::permissions;

pub fn ensure_url_access(ctx: &Ctx<'_>, uri: &Uri) -> Result<()> {
let permissions = ctx.userdata::<permissions::Permissions>().unwrap();
match &permissions.net {
permissions::BlackOrWhiteList::BlackList(items) => {
if url_match(
&items
.iter()
.map(|e| Uri::try_from(e))
.filter_map(|ruri| match ruri {
Ok(uri) => Some(uri),
Err(_) => None,
})
.collect::<Vec<Uri>>(),
uri,
) {
return Err(url_restricted_error(ctx, "URL denied", uri));
}
}
permissions::BlackOrWhiteList::WhiteList(items) => {
if !url_match(
&items
.iter()
.map(|e| Uri::try_from(e))
.filter_map(|ruri| match ruri {
Ok(uri) => Some(uri),
Err(_) => None,
})
.collect::<Vec<Uri>>(),
uri,
) {
return Err(url_restricted_error(ctx, "URL not allowed", uri));
}
}
let host = uri.host().unwrap_or_default();
if !permissions::check_net_permission(ctx, host) {
return Err(url_restricted_error(ctx, "URL not allowed", uri));
}
// if let Some(allow_list) = HTTP_ALLOW_LIST.get() {
// if !url_match(allow_list, uri) {
Expand Down
Loading
Loading