It's been a little over 6 weeks since v0.19 removing V8 from the trail binary. This change now removes the code.

I wasn't sure how long to keep the code around as an optional feature. However, the latest wasmtime release pins serde to a version, which is incompatible with the latest (somewhat stale) deno/V8/SWC dependencies. In other words, updating wasmtime right now would break the V8 build :/. The state of the rustyscript/deno dependency chain had certainly been one of the driving factors to move to WASM. While it feels validating, it's a little sad to see V8 go... but at the least we want to make the removal eplicit (as opposed to a broken build) with this change.
This commit is contained in:
Sebastian Jeltsch
2025-10-28 14:59:47 +01:00
parent 0c15272bae
commit c35c2acc41
28 changed files with 230 additions and 6884 deletions

3183
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -9,7 +9,6 @@ members = [
"crates/client",
"crates/core",
"crates/extension",
"crates/js-runtime",
"crates/qs",
"crates/refinery",
"crates/schema",
@@ -95,7 +94,6 @@ trailbase-assets = { path = "crates/assets", version = "0.2.0" }
trailbase-build = { path = "crates/build", version = "0.1.1" }
trailbase-client = { path = "crates/client", version = "0.5.0" }
trailbase-extension = { path = "crates/extension", version = "0.3.0" }
trailbase-js = { path = "crates/js-runtime", version = "0.2.0" }
trailbase-qs = { path = "crates/qs", version = "0.1.0" }
trailbase-refinery = { path = "crates/refinery", version = "0.1.0" }
trailbase-schema = { path = "crates/schema", version = "0.1.0" }

View File

@@ -13,8 +13,6 @@ name = "trail"
[features]
default = []
# Conditionally enable "v8" feature of dep:trailbase.
v8 = ["trailbase/v8"]
swagger = ["dep:utoipa-swagger-ui"]
vendor-ssl = ["dep:openssl"]

View File

@@ -25,7 +25,6 @@ harness = false
[features]
default = ["wasm"]
v8 = ["dep:trailbase-js"]
wasm = ["dep:trailbase-wasm-runtime-host"]
otel = ["dep:axum-tracing-opentelemetry", "dep:init-tracing-opentelemetry"]
@@ -93,7 +92,6 @@ tracing-subscriber = { workspace = true }
trailbase-assets = { workspace = true }
trailbase-build = { workspace = true }
trailbase-extension = { workspace = true }
trailbase-js = { workspace = true, optional = true }
trailbase-qs = { workspace = true }
trailbase-refinery = { workspace = true }
trailbase-schema = { workspace = true }

View File

@@ -48,9 +48,6 @@ struct InternalState {
subscription_manager: SubscriptionManager,
object_store: Arc<dyn ObjectStore + Send + Sync>,
#[cfg(feature = "v8")]
runtime: crate::js::RuntimeHandle,
/// Actual WASM runtimes.
wasm_runtimes: Vec<Arc<RwLock<Runtime>>>,
/// WASM runtime builders needed to rebuild above runtimes, e.g. when hot-reloading.
@@ -204,8 +201,6 @@ impl AppState {
),
connection_metadata,
object_store,
#[cfg(feature = "v8")]
runtime: build_js_runtime(args.conn.clone(), args.runtime_threads),
wasm_runtimes: build_wasm_runtime()
.expect("startup")
.into_iter()
@@ -400,11 +395,6 @@ impl AppState {
.await;
}
#[cfg(feature = "v8")]
pub(crate) fn script_runtime(&self) -> crate::js::RuntimeHandle {
return self.state.runtime.clone();
}
pub(crate) fn wasm_runtimes(&self) -> &[Arc<RwLock<Runtime>>] {
return &self.state.wasm_runtimes;
}
@@ -641,8 +631,6 @@ pub async fn test_state(options: Option<TestStateOptions>) -> anyhow::Result<App
),
connection_metadata,
object_store,
#[cfg(feature = "v8")]
runtime: build_js_runtime(conn, None),
wasm_runtimes: vec![],
build_wasm_runtimes: Box::new(|| Ok(vec![])),
test_cleanup: vec![Box::new(temp_dir)],
@@ -668,33 +656,6 @@ where
return derived;
}
#[cfg(feature = "v8")]
fn build_js_runtime(
conn: trailbase_sqlite::Connection,
threads: Option<usize>,
) -> crate::js::RuntimeHandle {
use crate::js::{RuntimeHandle, register_database_functions};
let runtime = if let Some(threads) = threads {
RuntimeHandle::singleton_or_init_with_threads(threads)
} else {
RuntimeHandle::singleton()
};
if cfg!(test) {
lazy_static::lazy_static! {
static ref START: std::sync::Once = std::sync::Once::new();
}
START.call_once(|| {
register_database_functions(&runtime, conn);
});
} else {
register_database_functions(&runtime, conn);
}
return runtime;
}
fn build_record_api(
conn: trailbase_sqlite::Connection,
connection_metadata: &ConnectionMetadata,

View File

@@ -1,5 +0,0 @@
#[cfg(feature = "v8")]
pub(crate) mod runtime;
#[cfg(feature = "v8")]
pub use trailbase_js::runtime::{RuntimeHandle, register_database_functions};

View File

@@ -1,385 +0,0 @@
use axum::Router;
use axum::body::Body;
use axum::extract::{RawPathParams, Request};
use axum::http::{HeaderName, HeaderValue, request::Parts};
use axum::http::{StatusCode, header::CONTENT_TYPE};
use axum::response::{IntoResponse, Response};
use futures_util::FutureExt;
use log::*;
use serde::Deserialize;
use std::path::PathBuf;
use std::str::FromStr;
use thiserror::Error;
use tokio::sync::oneshot;
use trailbase_js::runtime::{
JsUser, LargeRSError, Message, Module, Runtime, RuntimeHandle,
build_call_async_js_function_message, get_arg,
};
use crate::AppState;
use crate::auth::user::User;
type AnyError = Box<dyn std::error::Error + Send + Sync>;
pub struct DispatchArgs {
pub method: String,
pub route_path: String,
pub uri: String,
pub path_params: Vec<(String, String)>,
pub headers: Vec<(String, String)>,
pub user: Option<JsUser>,
pub body: bytes::Bytes,
pub reply: oneshot::Sender<Result<JsHttpResponse, Box<LargeRSError>>>,
}
#[derive(Deserialize, Default, Debug)]
pub struct JsHttpResponse {
pub headers: Option<Vec<(String, String)>>,
pub status: Option<u16>,
pub body: Option<bytes::Bytes>,
}
#[derive(Debug, Error)]
pub enum JsHttpResponseError {
#[error("Precondition: {0}")]
Precondition(String),
#[error("Internal: {0}")]
Internal(Box<dyn std::error::Error + Send + Sync>),
#[error("Runtime: {0}")]
Runtime(#[from] Box<LargeRSError>),
}
impl IntoResponse for JsHttpResponseError {
fn into_response(self) -> Response {
let (status, body): (StatusCode, Option<String>) = match self {
Self::Precondition(err) => (StatusCode::PRECONDITION_FAILED, Some(err.to_string())),
Self::Internal(err) => (StatusCode::INTERNAL_SERVER_ERROR, Some(err.to_string())),
Self::Runtime(err) => (StatusCode::INTERNAL_SERVER_ERROR, Some(err.to_string())),
};
if let Some(body) = body {
return Response::builder()
.status(status)
.header(CONTENT_TYPE, "text/plain")
.body(Body::new(body))
.unwrap_or_default();
}
return Response::builder()
.status(status)
.body(Body::empty())
.unwrap_or_default();
}
}
/// Get's called from JS during `addRoute` and installs an axum HTTP handler.
///
/// The axum HTTP handler will then call back into the registered callback in JS.
fn add_route_to_router(
runtime_handle: RuntimeHandle,
method: String,
route: String,
) -> Result<Router<AppState>, AnyError> {
let method_uppercase = method.to_uppercase();
let route_path = route.clone();
let handler = move |params: RawPathParams, user: Option<User>, req: Request| async move {
let (parts, body) = req.into_parts();
let Ok(body_bytes) = axum::body::to_bytes(body, usize::MAX).await else {
return Err(JsHttpResponseError::Precondition(
"request deserialization failed".to_string(),
));
};
let Parts { uri, headers, .. } = parts;
let path_params: Vec<(String, String)> = params
.iter()
.map(|(k, v)| (k.to_string(), v.to_string()))
.collect();
let headers: Vec<(String, String)> = headers
.into_iter()
.filter_map(|(key, value)| {
if let Some(key) = key
&& let Ok(value) = value.to_str()
{
return Some((key.to_string(), value.to_string()));
}
return None;
})
.collect();
let js_user: Option<JsUser> = user.map(|u| JsUser {
id: u.id,
email: u.email,
csrf: u.csrf_token,
});
let (sender, receiver) = oneshot::channel::<Result<JsHttpResponse, Box<LargeRSError>>>();
debug!("dispatch {method} {uri}");
runtime_handle
.send_to_any_isolate(build_http_dispatch_message(DispatchArgs {
method,
route_path,
uri: uri.to_string(),
path_params,
headers,
user: js_user,
body: body_bytes,
reply: sender,
}))
.await
.map_err(|_err| JsHttpResponseError::Internal("send failed".into()))?;
let js_response = receiver
.await
.map_err(|_err| JsHttpResponseError::Internal("receive failed".into()))??;
let mut http_response = Response::builder()
.status(js_response.status.unwrap_or(200))
.body(Body::from(js_response.body.unwrap_or_default()))
.map_err(|err| JsHttpResponseError::Internal(err.into()))?;
if let Some(headers) = js_response.headers {
for (key, value) in headers {
http_response.headers_mut().insert(
HeaderName::from_str(key.as_str())
.map_err(|err| JsHttpResponseError::Internal(err.into()))?,
HeaderValue::from_str(value.as_str())
.map_err(|err| JsHttpResponseError::Internal(err.into()))?,
);
}
}
return Ok(http_response);
};
return Ok(Router::<AppState>::new().route(
&route,
match method_uppercase.as_str() {
"DELETE" => axum::routing::delete(handler),
"GET" => axum::routing::get(handler),
"HEAD" => axum::routing::head(handler),
"OPTIONS" => axum::routing::options(handler),
"PATCH" => axum::routing::patch(handler),
"POST" => axum::routing::post(handler),
"PUT" => axum::routing::put(handler),
"TRACE" => axum::routing::trace(handler),
_ => {
return Err(format!("method: {method_uppercase}").into());
}
},
));
}
async fn install_routes_and_jobs(
state: &AppState,
module: Module,
) -> Result<Option<Router<AppState>>, AnyError> {
let runtime_handle = state.script_runtime();
let jobs = state.jobs();
// For all the isolates/worker-threads.
let receivers: Vec<_> = runtime_handle
.state()
.iter()
.enumerate()
.map(async |(index, state)| {
let module = module.clone();
let runtime_handle = runtime_handle.clone();
let jobs = jobs.clone();
let (router_sender, router_receiver) = kanal::unbounded::<Router<AppState>>();
if let Err(err) = state
.send_privately(Message::Run(
None,
Box::new(move |_m, runtime: &mut Runtime| {
// First install a native callbacks.
//
// Register native callback for building axum router.
let runtime_handle_clone = runtime_handle.clone();
runtime
.register_function("install_route", move |args: &[serde_json::Value]| {
let method: String = get_arg(args, 0)?;
let route: String = get_arg(args, 1)?;
let router = add_route_to_router(runtime_handle_clone.clone(), method, route)
.map_err(|err| LargeRSError::Runtime(err.to_string()))?;
router_sender.send(router).expect("send");
return Ok(serde_json::Value::Null);
})
.expect("Failed to register 'install_route' function");
// Register native callback for registering cron jobs.
runtime
.register_function(
"install_job",
move |args: &[serde_json::Value]| -> Result<serde_json::Value, _> {
let name: String = get_arg(args, 0)?;
let default_spec: String = get_arg(args, 1)?;
let schedule = cron::Schedule::from_str(&default_spec).map_err(|err| {
return LargeRSError::Runtime(err.to_string());
})?;
let runtime_handle = runtime_handle.clone();
let (id_sender, id_receiver) = oneshot::channel::<i64>();
let id_receiver = id_receiver.shared();
let Some(job) = jobs.new_job(
None,
name,
schedule,
crate::scheduler::build_callback(move || {
let runtime_handle = runtime_handle.clone();
let id_receiver = id_receiver.clone();
return async move {
let Some(first_isolate) = runtime_handle.state().first() else {
return Err("Missing isolate".into());
};
let (sender, receiver) =
oneshot::channel::<Result<Option<String>, Box<LargeRSError>>>();
let id = id_receiver.await?;
first_isolate
.send_privately(build_call_async_js_function_message::<Option<String>>(
None,
"__dispatchCron",
[id],
sender,
))
.await?;
match receiver.await? {
Err(err) => debug!("cron failed: {err}"),
Ok(Some(err)) => debug!("cron failed: {err}"),
_ => {}
};
Ok::<_, AnyError>(())
};
}),
) else {
return Err(LargeRSError::Runtime("Failed to add job".to_string()));
};
if let Err(err) = id_sender.send(job.id as i64) {
return Err(LargeRSError::Runtime(err.to_string()));
}
job.start();
return Ok(job.id.into());
},
)
.expect("Failed to register 'install_job' function");
return None;
}),
))
.await
{
panic!("Failed to comm with v8 rt'{index}': {err}");
}
// Then execute the script/module, i.e. statements in the file scope.
if let Err(err) = state.load_module(module).await {
error!("Failed to load module: {err}");
return None;
}
// Now all module-level calls to `install_route` should have happened. Let's drain the
// registered routes. Note, we cannot `collect()` since the sender side never hangs up.
let mut installed_routers: Vec<Router<AppState>> = vec![];
match router_receiver.drain_into(&mut installed_routers) {
Ok(n) => debug!("Got {n} routers from JS"),
Err(err) => {
error!("Failed to get routers from JS: {err}");
return None;
}
};
let mut merged_router = Router::<AppState>::new();
for router in installed_routers {
if router.has_routes() {
merged_router = merged_router.merge(router);
}
}
return Some(merged_router);
})
.collect();
// Await function registration and module loading for all isolates/worker-threads.
let mut receivers = futures_util::future::join_all(receivers).await;
// Note: We only return the first router assuming that JS route registration is consistent across
// all isolates.
return Ok(receivers.swap_remove(0));
}
pub(crate) async fn load_routes_and_jobs_from_js_modules(
state: &AppState,
scripts_dir: PathBuf,
) -> Result<Option<Router<AppState>>, AnyError> {
let runtime_handle = state.script_runtime();
if runtime_handle.num_threads() == 0 {
info!("JS threads set to zero. Skipping initialization for JS modules");
return Ok(None);
}
let modules = match Module::load_dir(scripts_dir.clone()) {
Ok(modules) => modules,
Err(err) => {
debug!("Skip loading js modules from '{scripts_dir:?}': {err}");
return Ok(None);
}
};
if !modules.is_empty() {
warn!(
"Found JS/TS scripts. The V8 runtime is deprecated and will likely be \
removed in the next major release. Please migrate to WASM. If you have \
concerns or encounter any issues, don't hesitate to reach out."
);
}
let mut js_router = Router::new();
for module in modules {
let fname = module.filename().to_owned();
if let Some(router) = install_routes_and_jobs(state, module).await? {
js_router = js_router.merge(router);
} else {
debug!("Skipping js module '{fname:?}': no routes");
}
}
if js_router.has_routes() {
return Ok(Some(js_router));
}
return Ok(None);
}
pub fn build_http_dispatch_message(args: DispatchArgs) -> Message {
return build_call_async_js_function_message(
None,
"__dispatch",
serde_json::json!([
args.method,
args.route_path,
args.uri,
args.path_params,
args.headers,
args.user,
args.body
]),
args.reply,
);
}

View File

@@ -17,7 +17,6 @@ mod data_dir;
mod email;
mod encryption;
mod extract;
mod js;
mod listing;
mod migrations;
mod scheduler;

View File

@@ -143,10 +143,6 @@ pub async fn init_app_state(args: InitArgs) -> Result<(bool, AppState), InitErro
let object_store = build_objectstore(&args.data_dir, config.server.s3_storage_config.as_ref())?;
// Write out the latest .js/.d.ts runtime files.
#[cfg(feature = "v8")]
trailbase_js::runtime::write_js_runtime_files(args.data_dir.root()).await;
let app_state = AppState::new(AppStateArgs {
data_dir: args.data_dir.clone(),
public_url: args.public_url,

View File

@@ -151,17 +151,6 @@ impl Server {
let mut custom_routers: Vec<Router<AppState>> = vec![];
#[cfg(feature = "v8")]
if let Some(js_router) = crate::js::runtime::load_routes_and_jobs_from_js_modules(
&state,
state.data_dir().root().join("scripts"),
)
.await
.map_err(|err| InitError::ScriptError(err.to_string()))?
{
custom_routers.push(js_router);
}
for rt in state.wasm_runtimes() {
if let Some(wasm_router) = crate::wasm::install_routes_and_jobs(&state, rt.clone())
.await
@@ -643,11 +632,6 @@ async fn start_listen(
}
};
#[cfg(not(feature = "v8"))]
tokio_rustls::rustls::crypto::ring::default_provider()
.install_default()
.expect("Failed to install TLS provider");
let server_config = ServerConfig::builder()
.with_no_client_auth()
.with_single_cert(vec![cert], key)

View File

@@ -1,35 +0,0 @@
[package]
name = "trailbase-js"
version = "0.2.0"
edition = "2024"
license = "OSL-3.0"
description = "JS runtime for the TrailBase framework"
homepage = "https://trailbase.io"
readme = "../README.md"
exclude = [
"**/node_modules/",
"**/dist/",
]
[lib]
doctest = false
[dependencies]
bytes = { version = "1.8.0", features = ["serde"] }
futures-util = { version = "0.3", default-features = false, features = ["alloc"] }
kanal = "0.1.1"
log = { version = "^0.4.21", default-features = false }
parking_lot = { workspace = true }
rusqlite = { workspace = true }
rust-embed = { workspace = true }
rustyscript = { version = "^0.12.0", features = ["web", "fs"] }
self_cell = "1.2.0"
serde = { workspace = true }
serde_json = { workspace = true }
tokio = { workspace = true }
tracing-subscriber = { workspace = true }
trailbase-schema = { workspace = true }
trailbase-sqlite = { workspace = true }
[build-dependencies]
trailbase-build = { workspace = true }

View File

@@ -1 +0,0 @@
dist/

View File

@@ -1,30 +0,0 @@
import pluginJs from "@eslint/js";
import tseslint from "typescript-eslint";
export default [
pluginJs.configs.recommended,
...tseslint.configs.recommended,
{
ignores: ["dist/", "node_modules/"],
},
{
files: ["src/**/*.{js,mjs,cjs,mts,ts,tsx,jsx}"],
rules: {
// https://typescript-eslint.io/rules/no-explicit-any/
"@typescript-eslint/no-explicit-any": "warn",
"@typescript-eslint/no-wrapper-object-types": "warn",
"@typescript-eslint/no-namespace": "off",
"no-var": "off",
// http://eslint.org/docs/rules/no-unused-vars
"@typescript-eslint/no-unused-vars": [
"error",
{
vars: "all",
args: "after-used",
argsIgnorePattern: "^_",
varsIgnorePattern: "^_",
},
],
},
},
];

View File

@@ -1,24 +0,0 @@
{
"name": "trailbase-js-runtime",
"description": "Runtime for JS/TS execution within TrailBase",
"version": "0.1.0",
"license": "OSL-3.0",
"type": "module",
"homepage": "https://trailbase.io",
"scripts": {
"build": "vite build",
"check": "tsc --noEmit --skipLibCheck && eslint && vitest run",
"format": "prettier -w src",
"test": "vitest run"
},
"devDependencies": {
"@eslint/js": "^9.39.1",
"eslint": "^9.39.1",
"prettier": "^3.6.2",
"typescript": "^5.9.3",
"typescript-eslint": "^8.46.4",
"vite": "^7.2.2",
"vite-plugin-dts": "^4.5.4",
"vitest": "^3.2.4"
}
}

View File

@@ -1,808 +0,0 @@
declare namespace Deno {
export interface ReadFileOptions {
/**
* An abort signal to allow cancellation of the file read operation.
* If the signal becomes aborted the readFile operation will be stopped
* and the promise returned will be rejected with an AbortError.
*/
signal?: AbortSignal;
}
export interface WriteFileOptions {
/** If set to `true`, will append to a file instead of overwriting previous
* contents.
*
* @default {false} */
append?: boolean;
/** Sets the option to allow creating a new file, if one doesn't already
* exist at the specified path.
*
* @default {true} */
create?: boolean;
/** If set to `true`, no file, directory, or symlink is allowed to exist at
* the target location. When createNew is set to `true`, `create` is ignored.
*
* @default {false} */
createNew?: boolean;
/** Permissions always applied to file. */
mode?: number;
/** An abort signal to allow cancellation of the file write operation.
*
* If the signal becomes aborted the write file operation will be stopped
* and the promise returned will be rejected with an {@linkcode AbortError}.
*/
signal?: AbortSignal;
}
/**
* Options which can be set when using {@linkcode Deno.makeTempDir},
* {@linkcode Deno.makeTempDirSync}, {@linkcode Deno.makeTempFile}, and
* {@linkcode Deno.makeTempFileSync}.
*
* @category File System */
export interface MakeTempOptions {
/** Directory where the temporary directory should be created (defaults to
* the env variable `TMPDIR`, or the system's default, usually `/tmp`).
*
* Note that if the passed `dir` is relative, the path returned by
* `makeTempFile()` and `makeTempDir()` will also be relative. Be mindful of
* this when changing working directory. */
dir?: string;
/** String that should precede the random portion of the temporary
* directory's name. */
prefix?: string;
/** String that should follow the random portion of the temporary
* directory's name. */
suffix?: string;
}
/**
* Options which can be set when using {@linkcode Deno.mkdir} and
* {@linkcode Deno.mkdirSync}.
*
* @category File System */
export interface MkdirOptions {
/** If set to `true`, means that any intermediate directories will also be
* created (as with the shell command `mkdir -p`).
*
* Intermediate directories are created with the same permissions.
*
* When recursive is set to `true`, succeeds silently (without changing any
* permissions) if a directory already exists at the path, or if the path
* is a symlink to an existing directory.
*
* @default {false} */
recursive?: boolean;
/** Permissions to use when creating the directory (defaults to `0o777`,
* before the process's umask).
*
* Ignored on Windows. */
mode?: number;
}
/**
* Information about a directory entry returned from {@linkcode Deno.readDir}
* and {@linkcode Deno.readDirSync}.
*
* @category File System */
export interface DirEntry {
/** The file name of the entry. It is just the entity name and does not
* include the full path. */
name: string;
/** True if this is info for a regular file. Mutually exclusive to
* `DirEntry.isDirectory` and `DirEntry.isSymlink`. */
isFile: boolean;
/** True if this is info for a regular directory. Mutually exclusive to
* `DirEntry.isFile` and `DirEntry.isSymlink`. */
isDirectory: boolean;
/** True if this is info for a symlink. Mutually exclusive to
* `DirEntry.isFile` and `DirEntry.isDirectory`. */
isSymlink: boolean;
}
/**
* Options which can be set when doing {@linkcode Deno.open} and
* {@linkcode Deno.openSync}.
*
* @category File System */
export interface OpenOptions {
/** Sets the option for read access. This option, when `true`, means that
* the file should be read-able if opened.
*
* @default {true} */
read?: boolean;
/** Sets the option for write access. This option, when `true`, means that
* the file should be write-able if opened. If the file already exists,
* any write calls on it will overwrite its contents, by default without
* truncating it.
*
* @default {false} */
write?: boolean;
/** Sets the option for the append mode. This option, when `true`, means
* that writes will append to a file instead of overwriting previous
* contents.
*
* Note that setting `{ write: true, append: true }` has the same effect as
* setting only `{ append: true }`.
*
* @default {false} */
append?: boolean;
/** Sets the option for truncating a previous file. If a file is
* successfully opened with this option set it will truncate the file to `0`
* size if it already exists. The file must be opened with write access
* for truncate to work.
*
* @default {false} */
truncate?: boolean;
/** Sets the option to allow creating a new file, if one doesn't already
* exist at the specified path. Requires write or append access to be
* used.
*
* @default {false} */
create?: boolean;
/** If set to `true`, no file, directory, or symlink is allowed to exist at
* the target location. Requires write or append access to be used. When
* createNew is set to `true`, create and truncate are ignored.
*
* @default {false} */
createNew?: boolean;
/** Permissions to use if creating the file (defaults to `0o666`, before
* the process's umask).
*
* Ignored on Windows. */
mode?: number;
}
/**
* Options which can be set when using {@linkcode Deno.remove} and
* {@linkcode Deno.removeSync}.
*
* @category File System */
export interface RemoveOptions {
/** If set to `true`, path will be removed even if it's a non-empty directory.
*
* @default {false} */
recursive?: boolean;
}
/** Options that can be used with {@linkcode symlink} and
* {@linkcode symlinkSync}.
*
* @category File System */
export interface SymlinkOptions {
/** Specify the symbolic link type as file, directory or NTFS junction. This
* option only applies to Windows and is ignored on other operating systems. */
type: "file" | "dir" | "junction";
}
export function writeFile(
path: string | URL,
data: Uint8Array | ReadableStream<Uint8Array>,
options?: WriteFileOptions,
): Promise<void>;
export function writeTextFile(
path: string | URL,
data: string | ReadableStream<string>,
options?: WriteFileOptions,
): Promise<void>;
export function readTextFile(
path: string | URL,
options?: ReadFileOptions,
): Promise<string>;
export function readFile(
path: string | URL,
options?: ReadFileOptions,
): Promise<Uint8Array>;
export function chmod(path: string | URL, mode: number): Promise<void>;
export function chown(
path: string | URL,
uid: number | null,
gid: number | null,
): Promise<void>;
export function cwd(): string;
export function makeTempDir(options?: MakeTempOptions): Promise<string>;
export function makeTempFile(options?: MakeTempOptions): Promise<string>;
export function mkdir(
path: string | URL,
options?: MkdirOptions,
): Promise<void>;
export function chdir(directory: string | URL): void;
export function copyFile(
fromPath: string | URL,
toPath: string | URL,
): Promise<void>;
export function readDir(path: string | URL): AsyncIterable<DirEntry>;
export function readLink(path: string | URL): Promise<string>;
export function realPath(path: string | URL): Promise<string>;
export function remove(
path: string | URL,
options?: RemoveOptions,
): Promise<void>;
export function rename(
oldpath: string | URL,
newpath: string | URL,
): Promise<void>;
export function stat(path: string | URL): Promise<FileInfo>;
export function lstat(path: string | URL): Promise<FileInfo>;
export function truncate(name: string, len?: number): Promise<void>;
export function open(
path: string | URL,
options?: OpenOptions,
): Promise<FsFile>;
export function create(path: string | URL): Promise<FsFile>;
export function symlink(
oldpath: string | URL,
newpath: string | URL,
options?: SymlinkOptions,
): Promise<void>;
export function link(oldpath: string, newpath: string): Promise<void>;
export function utime(
path: string | URL,
atime: number | Date,
mtime: number | Date,
): Promise<void>;
export function umask(mask?: number): number;
/** Provides information about a file and is returned by
* {@linkcode Deno.stat}, {@linkcode Deno.lstat}, {@linkcode Deno.statSync},
* and {@linkcode Deno.lstatSync} or from calling `stat()` and `statSync()`
* on an {@linkcode Deno.FsFile} instance.
*
* @category File System
*/
export interface FileInfo {
/** True if this is info for a regular file. Mutually exclusive to
* `FileInfo.isDirectory` and `FileInfo.isSymlink`. */
isFile: boolean;
/** True if this is info for a regular directory. Mutually exclusive to
* `FileInfo.isFile` and `FileInfo.isSymlink`. */
isDirectory: boolean;
/** True if this is info for a symlink. Mutually exclusive to
* `FileInfo.isFile` and `FileInfo.isDirectory`. */
isSymlink: boolean;
/** The size of the file, in bytes. */
size: number;
/** The last modification time of the file. This corresponds to the `mtime`
* field from `stat` on Linux/Mac OS and `ftLastWriteTime` on Windows. This
* may not be available on all platforms. */
mtime: Date | null;
/** The last access time of the file. This corresponds to the `atime`
* field from `stat` on Unix and `ftLastAccessTime` on Windows. This may not
* be available on all platforms. */
atime: Date | null;
/** The creation time of the file. This corresponds to the `birthtime`
* field from `stat` on Mac/BSD and `ftCreationTime` on Windows. This may
* not be available on all platforms. */
birthtime: Date | null;
/** The last change time of the file. This corresponds to the `ctime`
* field from `stat` on Mac/BSD and `ChangeTime` on Windows. This may
* not be available on all platforms. */
ctime: Date | null;
/** ID of the device containing the file. */
dev: number;
/** Inode number.
*
* _Linux/Mac OS only._ */
ino: number | null;
/** The underlying raw `st_mode` bits that contain the standard Unix
* permissions for this file/directory.
*/
mode: number | null;
/** Number of hard links pointing to this file.
*
* _Linux/Mac OS only._ */
nlink: number | null;
/** User ID of the owner of this file.
*
* _Linux/Mac OS only._ */
uid: number | null;
/** Group ID of the owner of this file.
*
* _Linux/Mac OS only._ */
gid: number | null;
/** Device ID of this file.
*
* _Linux/Mac OS only._ */
rdev: number | null;
/** Blocksize for filesystem I/O.
*
* _Linux/Mac OS only._ */
blksize: number | null;
/** Number of blocks allocated to the file, in 512-byte units.
*
* _Linux/Mac OS only._ */
blocks: number | null;
/** True if this is info for a block device.
*
* _Linux/Mac OS only._ */
isBlockDevice: boolean | null;
/** True if this is info for a char device.
*
* _Linux/Mac OS only._ */
isCharDevice: boolean | null;
/** True if this is info for a fifo.
*
* _Linux/Mac OS only._ */
isFifo: boolean | null;
/** True if this is info for a socket.
*
* _Linux/Mac OS only._ */
isSocket: boolean | null;
}
/**
* A enum which defines the seek mode for IO related APIs that support
* seeking.
*
* @category I/O */
export enum SeekMode {
/* Seek from the start of the file/resource. */
Start = 0,
/* Seek from the current position within the file/resource. */
Current = 1,
/* Seek from the end of the current file/resource. */
End = 2,
}
/** @category I/O */
export interface SetRawOptions {
/**
* The `cbreak` option can be used to indicate that characters that
* correspond to a signal should still be generated. When disabling raw
* mode, this option is ignored. This functionality currently only works on
* Linux and Mac OS.
*/
cbreak: boolean;
}
export class FsFile implements Disposable {
/** A {@linkcode ReadableStream} instance representing to the byte contents
* of the file. This makes it easy to interoperate with other web streams
* based APIs.
*
* ```ts
* using file = await Deno.open("my_file.txt", { read: true });
* const decoder = new TextDecoder();
* for await (const chunk of file.readable) {
* console.log(decoder.decode(chunk));
* }
* ```
*/
readonly readable: ReadableStream<Uint8Array>;
/** A {@linkcode WritableStream} instance to write the contents of the
* file. This makes it easy to interoperate with other web streams based
* APIs.
*
* ```ts
* const items = ["hello", "world"];
* using file = await Deno.open("my_file.txt", { write: true });
* const encoder = new TextEncoder();
* const writer = file.writable.getWriter();
* for (const item of items) {
* await writer.write(encoder.encode(item));
* }
* ```
*/
readonly writable: WritableStream<Uint8Array>;
/** Write the contents of the array buffer (`p`) to the file.
*
* Resolves to the number of bytes written.
*
* **It is not guaranteed that the full buffer will be written in a single
* call.**
*
* ```ts
* const encoder = new TextEncoder();
* const data = encoder.encode("Hello world");
* using file = await Deno.open("/foo/bar.txt", { write: true });
* const bytesWritten = await file.write(data); // 11
* ```
*
* @category I/O
*/
write(p: Uint8Array): Promise<number>;
/** Synchronously write the contents of the array buffer (`p`) to the file.
*
* Returns the number of bytes written.
*
* **It is not guaranteed that the full buffer will be written in a single
* call.**
*
* ```ts
* const encoder = new TextEncoder();
* const data = encoder.encode("Hello world");
* using file = Deno.openSync("/foo/bar.txt", { write: true });
* const bytesWritten = file.writeSync(data); // 11
* ```
*/
writeSync(p: Uint8Array): number;
/** Truncates (or extends) the file to reach the specified `len`. If `len`
* is not specified, then the entire file contents are truncated.
*
* ### Truncate the entire file
*
* ```ts
* using file = await Deno.open("my_file.txt", { write: true });
* await file.truncate();
* ```
*
* ### Truncate part of the file
*
* ```ts
* // if "my_file.txt" contains the text "hello world":
* using file = await Deno.open("my_file.txt", { write: true });
* await file.truncate(7);
* const buf = new Uint8Array(100);
* await file.read(buf);
* const text = new TextDecoder().decode(buf); // "hello w"
* ```
*/
truncate(len?: number): Promise<void>;
/** Synchronously truncates (or extends) the file to reach the specified
* `len`. If `len` is not specified, then the entire file contents are
* truncated.
*
* ### Truncate the entire file
*
* ```ts
* using file = Deno.openSync("my_file.txt", { write: true });
* file.truncateSync();
* ```
*
* ### Truncate part of the file
*
* ```ts
* // if "my_file.txt" contains the text "hello world":
* using file = Deno.openSync("my_file.txt", { write: true });
* file.truncateSync(7);
* const buf = new Uint8Array(100);
* file.readSync(buf);
* const text = new TextDecoder().decode(buf); // "hello w"
* ```
*/
truncateSync(len?: number): void;
/** Read the file into an array buffer (`p`).
*
* Resolves to either the number of bytes read during the operation or EOF
* (`null`) if there was nothing more to read.
*
* It is possible for a read to successfully return with `0` bytes. This
* does not indicate EOF.
*
* **It is not guaranteed that the full buffer will be read in a single
* call.**
*
* ```ts
* // if "/foo/bar.txt" contains the text "hello world":
* using file = await Deno.open("/foo/bar.txt");
* const buf = new Uint8Array(100);
* const numberOfBytesRead = await file.read(buf); // 11 bytes
* const text = new TextDecoder().decode(buf); // "hello world"
* ```
*/
read(p: Uint8Array): Promise<number | null>;
/** Synchronously read from the file into an array buffer (`p`).
*
* Returns either the number of bytes read during the operation or EOF
* (`null`) if there was nothing more to read.
*
* It is possible for a read to successfully return with `0` bytes. This
* does not indicate EOF.
*
* **It is not guaranteed that the full buffer will be read in a single
* call.**
*
* ```ts
* // if "/foo/bar.txt" contains the text "hello world":
* using file = Deno.openSync("/foo/bar.txt");
* const buf = new Uint8Array(100);
* const numberOfBytesRead = file.readSync(buf); // 11 bytes
* const text = new TextDecoder().decode(buf); // "hello world"
* ```
*/
readSync(p: Uint8Array): number | null;
/** Seek to the given `offset` under mode given by `whence`. The call
* resolves to the new position within the resource (bytes from the start).
*
* ```ts
* // Given the file contains "Hello world" text, which is 11 bytes long:
* using file = await Deno.open(
* "hello.txt",
* { read: true, write: true, truncate: true, create: true },
* );
* await file.write(new TextEncoder().encode("Hello world"));
*
* // advance cursor 6 bytes
* const cursorPosition = await file.seek(6, Deno.SeekMode.Start);
* console.log(cursorPosition); // 6
* const buf = new Uint8Array(100);
* await file.read(buf);
* console.log(new TextDecoder().decode(buf)); // "world"
* ```
*
* The seek modes work as follows:
*
* ```ts
* // Given the file contains "Hello world" text, which is 11 bytes long:
* const file = await Deno.open(
* "hello.txt",
* { read: true, write: true, truncate: true, create: true },
* );
* await file.write(new TextEncoder().encode("Hello world"));
*
* // Seek 6 bytes from the start of the file
* console.log(await file.seek(6, Deno.SeekMode.Start)); // "6"
* // Seek 2 more bytes from the current position
* console.log(await file.seek(2, Deno.SeekMode.Current)); // "8"
* // Seek backwards 2 bytes from the end of the file
* console.log(await file.seek(-2, Deno.SeekMode.End)); // "9" (i.e. 11-2)
* ```
*/
seek(offset: number | bigint, whence: SeekMode): Promise<number>;
/** Synchronously seek to the given `offset` under mode given by `whence`.
* The new position within the resource (bytes from the start) is returned.
*
* ```ts
* using file = Deno.openSync(
* "hello.txt",
* { read: true, write: true, truncate: true, create: true },
* );
* file.writeSync(new TextEncoder().encode("Hello world"));
*
* // advance cursor 6 bytes
* const cursorPosition = file.seekSync(6, Deno.SeekMode.Start);
* console.log(cursorPosition); // 6
* const buf = new Uint8Array(100);
* file.readSync(buf);
* console.log(new TextDecoder().decode(buf)); // "world"
* ```
*
* The seek modes work as follows:
*
* ```ts
* // Given the file contains "Hello world" text, which is 11 bytes long:
* using file = Deno.openSync(
* "hello.txt",
* { read: true, write: true, truncate: true, create: true },
* );
* file.writeSync(new TextEncoder().encode("Hello world"));
*
* // Seek 6 bytes from the start of the file
* console.log(file.seekSync(6, Deno.SeekMode.Start)); // "6"
* // Seek 2 more bytes from the current position
* console.log(file.seekSync(2, Deno.SeekMode.Current)); // "8"
* // Seek backwards 2 bytes from the end of the file
* console.log(file.seekSync(-2, Deno.SeekMode.End)); // "9" (i.e. 11-2)
* ```
*/
seekSync(offset: number | bigint, whence: SeekMode): number;
/** Resolves to a {@linkcode Deno.FileInfo} for the file.
*
* ```ts
* import { assert } from "jsr:@std/assert";
*
* using file = await Deno.open("hello.txt");
* const fileInfo = await file.stat();
* assert(fileInfo.isFile);
* ```
*/
stat(): Promise<FileInfo>;
/** Synchronously returns a {@linkcode Deno.FileInfo} for the file.
*
* ```ts
* import { assert } from "jsr:@std/assert";
*
* using file = Deno.openSync("hello.txt")
* const fileInfo = file.statSync();
* assert(fileInfo.isFile);
* ```
*/
statSync(): FileInfo;
/**
* Flushes any pending data and metadata operations of the given file
* stream to disk.
*
* ```ts
* const file = await Deno.open(
* "my_file.txt",
* { read: true, write: true, create: true },
* );
* await file.write(new TextEncoder().encode("Hello World"));
* await file.truncate(1);
* await file.sync();
* console.log(await Deno.readTextFile("my_file.txt")); // H
* ```
*
* @category I/O
*/
sync(): Promise<void>;
/**
* Synchronously flushes any pending data and metadata operations of the given
* file stream to disk.
*
* ```ts
* const file = Deno.openSync(
* "my_file.txt",
* { read: true, write: true, create: true },
* );
* file.writeSync(new TextEncoder().encode("Hello World"));
* file.truncateSync(1);
* file.syncSync();
* console.log(Deno.readTextFileSync("my_file.txt")); // H
* ```
*
* @category I/O
*/
syncSync(): void;
/**
* Flushes any pending data operations of the given file stream to disk.
* ```ts
* using file = await Deno.open(
* "my_file.txt",
* { read: true, write: true, create: true },
* );
* await file.write(new TextEncoder().encode("Hello World"));
* await file.syncData();
* console.log(await Deno.readTextFile("my_file.txt")); // Hello World
* ```
*
* @category I/O
*/
syncData(): Promise<void>;
/**
* Synchronously flushes any pending data operations of the given file stream
* to disk.
*
* ```ts
* using file = Deno.openSync(
* "my_file.txt",
* { read: true, write: true, create: true },
* );
* file.writeSync(new TextEncoder().encode("Hello World"));
* file.syncDataSync();
* console.log(Deno.readTextFileSync("my_file.txt")); // Hello World
* ```
*
* @category I/O
*/
syncDataSync(): void;
/**
* Changes the access (`atime`) and modification (`mtime`) times of the
* file stream resource. Given times are either in seconds (UNIX epoch
* time) or as `Date` objects.
*
* ```ts
* using file = await Deno.open("file.txt", { create: true, write: true });
* await file.utime(1556495550, new Date());
* ```
*
* @category File System
*/
utime(atime: number | Date, mtime: number | Date): Promise<void>;
/**
* Synchronously changes the access (`atime`) and modification (`mtime`)
* times of the file stream resource. Given times are either in seconds
* (UNIX epoch time) or as `Date` objects.
*
* ```ts
* using file = Deno.openSync("file.txt", { create: true, write: true });
* file.utime(1556495550, new Date());
* ```
*
* @category File System
*/
utimeSync(atime: number | Date, mtime: number | Date): void;
/** **UNSTABLE**: New API, yet to be vetted.
*
* Checks if the file resource is a TTY (terminal).
*
* ```ts
* // This example is system and context specific
* using file = await Deno.open("/dev/tty6");
* file.isTerminal(); // true
* ```
*/
isTerminal(): boolean;
/** **UNSTABLE**: New API, yet to be vetted.
*
* Set TTY to be under raw mode or not. In raw mode, characters are read and
* returned as is, without being processed. All special processing of
* characters by the terminal is disabled, including echoing input
* characters. Reading from a TTY device in raw mode is faster than reading
* from a TTY device in canonical mode.
*
* ```ts
* using file = await Deno.open("/dev/tty6");
* file.setRaw(true, { cbreak: true });
* ```
*/
setRaw(mode: boolean, options?: SetRawOptions): void;
/**
* Acquire an advisory file-system lock for the file.
*
* @param [exclusive=false]
*/
lock(exclusive?: boolean): Promise<void>;
/**
* Synchronously acquire an advisory file-system lock synchronously for the file.
*
* @param [exclusive=false]
*/
lockSync(exclusive?: boolean): void;
/**
* Release an advisory file-system lock for the file.
*/
unlock(): Promise<void>;
/**
* Synchronously release an advisory file-system lock for the file.
*/
unlockSync(): void;
/** Close the file. Closing a file when you are finished with it is
* important to avoid leaking resources.
*
* ```ts
* using file = await Deno.open("my_file.txt");
* // do work with "file" object
* ```
*/
close(): void;
[Symbol.dispose](): void;
}
}
// NOTE: Ideally we'd pull in Deno types from https://github.com/denoland/deno/blob/main/cli/tsc/dts/lib.deno.ns.d.ts but haven't found a good way.
export namespace fs {
export const writeFile = Deno.writeFile;
export const writeTextFile = Deno.writeTextFile;
export const readTextFile = Deno.readTextFile;
export const readFile = Deno.readFile;
export const chmod = Deno.chmod;
export const chown = Deno.chown;
export const cwd = Deno.cwd;
export const makeTempDir = Deno.makeTempDir;
export const makeTempFile = Deno.makeTempFile;
export const mkdir = Deno.mkdir;
export const chdir = Deno.chdir;
export const copyFile = Deno.copyFile;
export const readDir = Deno.readDir;
export const readLink = Deno.readLink;
export const realPath = Deno.realPath;
export const remove = Deno.remove;
export const rename = Deno.rename;
export const stat = Deno.stat;
export const lstat = Deno.lstat;
export const truncate = Deno.truncate;
export const FsFile = Deno.FsFile;
export const open = Deno.open;
export const create = Deno.create;
export const symlink = Deno.symlink;
export const link = Deno.link;
export const utime = Deno.utime;
export const umask = Deno.umask;
}

View File

@@ -1,51 +0,0 @@
export { fs } from "./deno";
// Redirect console output to stderr, to keep stdout for request logs.
declare global {
var Deno: {
core: {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
print: any;
};
};
}
const _logStderr = function (...args: unknown[]) {
globalThis.Deno.core.print(`${args.join(" ")}\n`, /* to stderr = */ true);
};
globalThis.console.log = _logStderr;
globalThis.console.info = _logStderr;
globalThis.console.debug = _logStderr;
export {
HttpError,
StatusCodes,
addCronCallback,
addPeriodicCallback,
addRoute,
execute,
htmlHandler,
jsonHandler,
parsePath,
query,
stringHandler,
transaction,
Transaction,
} from "./trailbase";
export type {
Blob,
CallbackType,
HeaderMapType,
HtmlResponseType,
JsonRequestType,
JsonResponseType,
MaybeResponse,
Method,
ParsedPath,
PathParamsType,
RequestType,
ResponseType,
StringRequestType,
StringResponseType,
UserType,
} from "./trailbase";

View File

@@ -1,791 +0,0 @@
import { decodeFallback, encodeFallback } from "./util";
declare global {
function __dispatch(
m: Method,
route: string,
uri: string,
path: [string, string][],
headers: [string, string][],
user: UserType | undefined,
body: Uint8Array,
): Promise<ResponseType>;
function __dispatchCron(id: number): Promise<string | undefined>;
var rustyscript: {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
functions: any;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
async_functions: any;
};
}
export type HeaderMapType = { [key: string]: string };
export type PathParamsType = { [key: string]: string };
export type UserType = {
/// Base64 encoded UUIDv7 user id.
id: string;
/// The user's email address.
email: string;
/// The user's CSRF token.
csrf: string;
};
export type RequestType = {
uri: string;
params: PathParamsType;
headers: HeaderMapType;
user?: UserType;
body?: Uint8Array;
};
export type ResponseType = {
headers?: [string, string][];
status?: number;
body?: Uint8Array;
};
export type MaybeResponse<T> = Promise<T | undefined> | T | undefined;
export type CallbackType = (req: RequestType) => MaybeResponse<ResponseType>;
export type Method =
| "DELETE"
| "GET"
| "HEAD"
| "OPTIONS"
| "PATCH"
| "POST"
| "PUT"
| "TRACE";
/// HTTP status codes.
///
// source: https://github.com/prettymuchbryce/http-status-codes/blob/master/src/status-codes.ts
export enum StatusCodes {
/// Official Documentation @ https://tools.ietf.org/html/rfc7231#section-6.2.1
///
/// This interim response indicates that everything so far is OK and that the
/// client should continue with the request or ignore it if it is already
/// finished.
CONTINUE = 100,
/// Official Documentation @ https://tools.ietf.org/html/rfc7231#section-6.2.2
///
/// This code is sent in response to an Upgrade request header by the client,
/// and indicates the protocol the server is switching too.
SWITCHING_PROTOCOLS = 101,
/// Official Documentation @ https://tools.ietf.org/html/rfc2518#section-10.1
///
/// This code indicates that the server has received and is processing the
/// request, but no response is available yet.
PROCESSING = 102,
/// Official Documentation @ https://www.rfc-editor.org/rfc/rfc8297#page-3
///
/// This code indicates to the client that the server is likely to send a
/// final response with the header fields included in the informational
/// response.
EARLY_HINTS = 103,
/// Official Documentation @ https://tools.ietf.org/html/rfc7231#section-6.3.1
///
/// The request has succeeded. The meaning of a success varies depending on the HTTP method:
/// GET: The resource has been fetched and is transmitted in the message body.
/// HEAD: The entity headers are in the message body.
/// POST: The resource describing the result of the action is transmitted in the message body.
/// TRACE: The message body contains the request message as received by the server
OK = 200,
/// Official Documentation @ https://tools.ietf.org/html/rfc7231#section-6.3.2
///
/// The request has succeeded and a new resource has been created as a result
/// of it. This is typically the response sent after a PUT request.
CREATED = 201,
/// Official Documentation @ https://tools.ietf.org/html/rfc7231#section-6.3.3
///
/// The request has been received but not yet acted upon. It is
/// non-committal, meaning that there is no way in HTTP to later send an
/// asynchronous response indicating the outcome of processing the request. It
/// is intended for cases where another process or server handles the request,
/// or for batch processing.
ACCEPTED = 202,
/// Official Documentation @ https://tools.ietf.org/html/rfc7231#section-6.3.4
///
/// This response code means returned meta-information set is not exact set
/// as available from the origin server, but collected from a local or a third
/// party copy. Except this condition, 200 OK response should be preferred
/// instead of this response.
NON_AUTHORITATIVE_INFORMATION = 203,
/// Official Documentation @ https://tools.ietf.org/html/rfc7231#section-6.3.5
///
/// There is no content to send for this request, but the headers may be
/// useful. The user-agent may update its cached headers for this resource with
/// the new ones.
NO_CONTENT = 204,
/// Official Documentation @ https://tools.ietf.org/html/rfc7231#section-6.3.6
///
/// This response code is sent after accomplishing request to tell user agent
/// reset document view which sent this request.
RESET_CONTENT = 205,
/// Official Documentation @ https://tools.ietf.org/html/rfc7233#section-4.1
///
/// This response code is used because of range header sent by the client to
/// separate download into multiple streams.
PARTIAL_CONTENT = 206,
/// Official Documentation @ https://tools.ietf.org/html/rfc2518#section-10.2
///
/// A Multi-Status response conveys information about multiple resources in
/// situations where multiple status codes might be appropriate.
MULTI_STATUS = 207,
/// Official Documentation @ https://tools.ietf.org/html/rfc7231#section-6.4.1
///
/// The request has more than one possible responses. User-agent or user
/// should choose one of them. There is no standardized way to choose one of
/// the responses.
MULTIPLE_CHOICES = 300,
/// Official Documentation @ https://tools.ietf.org/html/rfc7231#section-6.4.2
///
/// This response code means that URI of requested resource has been changed.
/// Probably, new URI would be given in the response.
MOVED_PERMANENTLY = 301,
/// Official Documentation @ https://tools.ietf.org/html/rfc7231#section-6.4.3
///
/// This response code means that URI of requested resource has been changed
/// temporarily. New changes in the URI might be made in the future. Therefore,
/// this same URI should be used by the client in future requests.
MOVED_TEMPORARILY = 302,
/// Official Documentation @ https://tools.ietf.org/html/rfc7231#section-6.4.4
///
/// Server sent this response to directing client to get requested resource
/// to another URI with an GET request.
SEE_OTHER = 303,
/// Official Documentation @ https://tools.ietf.org/html/rfc7232#section-4.1
///
/// This is used for caching purposes. It is telling to client that response
/// has not been modified. So, client can continue to use same cached version
/// of response.
NOT_MODIFIED = 304,
/// @deprecated
/// Official Documentation @ https://tools.ietf.org/html/rfc7231#section-6.4.6
///
/// Was defined in a previous version of the HTTP specification to indicate
/// that a requested response must be accessed by a proxy. It has been
/// deprecated due to security concerns regarding in-band configuration of a
/// proxy.
USE_PROXY = 305,
/// Official Documentation @ https://tools.ietf.org/html/rfc7231#section-6.4.7
///
/// Server sent this response to directing client to get requested resource
/// to another URI with same method that used prior request. This has the same
/// semantic than the 302 Found HTTP response code, with the exception that the
/// user agent must not change the HTTP method used: if a POST was used in the
/// first request, a POST must be used in the second request.
TEMPORARY_REDIRECT = 307,
/// Official Documentation @ https://tools.ietf.org/html/rfc7538#section-3
///
/// This means that the resource is now permanently located at another URI,
/// specified by the Location: HTTP Response header. This has the same
/// semantics as the 301 Moved Permanently HTTP response code, with the
/// exception that the user agent must not change the HTTP method used: if a
/// POST was used in the first request, a POST must be used in the second
/// request.
PERMANENT_REDIRECT = 308,
/// Official Documentation @ https://tools.ietf.org/html/rfc7231#section-6.5.1
///
/// This response means that server could not understand the request due to invalid syntax.
BAD_REQUEST = 400,
/// Official Documentation @ https://tools.ietf.org/html/rfc7235#section-3.1
///
/// Although the HTTP standard specifies "unauthorized", semantically this
/// response means "unauthenticated". That is, the client must authenticate
/// itself to get the requested response.
UNAUTHORIZED = 401,
/// Official Documentation @ https://tools.ietf.org/html/rfc7231#section-6.5.2
///
/// This response code is reserved for future use. Initial aim for creating
/// this code was using it for digital payment systems however this is not used
/// currently.
PAYMENT_REQUIRED = 402,
/// Official Documentation @ https://tools.ietf.org/html/rfc7231#section-6.5.3
///
/// The client does not have access rights to the content, i.e. they are
/// unauthorized, so server is rejecting to give proper response. Unlike 401,
/// the client's identity is known to the server.
FORBIDDEN = 403,
/// Official Documentation @ https://tools.ietf.org/html/rfc7231#section-6.5.4
///
/// The server can not find requested resource. In the browser, this means
/// the URL is not recognized. In an API, this can also mean that the endpoint
/// is valid but the resource itself does not exist. Servers may also send this
/// response instead of 403 to hide the existence of a resource from an
/// unauthorized client. This response code is probably the most famous one due
/// to its frequent occurence on the web.
NOT_FOUND = 404,
/// Official Documentation @ https://tools.ietf.org/html/rfc7231#section-6.5.5
///
/// The request method is known by the server but has been disabled and
/// cannot be used. For example, an API may forbid DELETE-ing a resource. The
/// two mandatory methods, GET and HEAD, must never be disabled and should not
/// return this error code.
METHOD_NOT_ALLOWED = 405,
/// Official Documentation @ https://tools.ietf.org/html/rfc7231#section-6.5.6
///
/// This response is sent when the web server, after performing server-driven
/// content negotiation, doesn't find any content following the criteria given
/// by the user agent.
NOT_ACCEPTABLE = 406,
/// Official Documentation @ https://tools.ietf.org/html/rfc7235#section-3.2
///
/// This is similar to 401 but authentication is needed to be done by a proxy.
PROXY_AUTHENTICATION_REQUIRED = 407,
/// Official Documentation @ https://tools.ietf.org/html/rfc7231#section-6.5.7
///
/// This response is sent on an idle connection by some servers, even without
/// any previous request by the client. It means that the server would like to
/// shut down this unused connection. This response is used much more since
/// some browsers, like Chrome, Firefox 27+, or IE9, use HTTP pre-connection
/// mechanisms to speed up surfing. Also note that some servers merely shut
/// down the connection without sending this message.
REQUEST_TIMEOUT = 408,
/// Official Documentation @ https://tools.ietf.org/html/rfc7231#section-6.5.8
///
/// This response is sent when a request conflicts with the current state of the server.
CONFLICT = 409,
///
/// Official Documentation @ https://tools.ietf.org/html/rfc7231#section-6.5.9
///
/// This response would be sent when the requested content has been
/// permenantly deleted from server, with no forwarding address. Clients are
/// expected to remove their caches and links to the resource. The HTTP
/// specification intends this status code to be used for "limited-time,
/// promotional services". APIs should not feel compelled to indicate resources
/// that have been deleted with this status code.
GONE = 410,
/// Official Documentation @ https://tools.ietf.org/html/rfc7231#section-6.5.10
///
/// The server rejected the request because the Content-Length header field
/// is not defined and the server requires it.
LENGTH_REQUIRED = 411,
/// Official Documentation @ https://tools.ietf.org/html/rfc7232#section-4.2
///
/// The client has indicated preconditions in its headers which the server
/// does not meet.
PRECONDITION_FAILED = 412,
/// Official Documentation @ https://tools.ietf.org/html/rfc7231#section-6.5.11
///
/// Request entity is larger than limits defined by server; the server might
/// close the connection or return an Retry-After header field.
REQUEST_TOO_LONG = 413,
/// Official Documentation @ https://tools.ietf.org/html/rfc7231#section-6.5.12
///
/// The URI requested by the client is longer than the server is willing to interpret.
REQUEST_URI_TOO_LONG = 414,
/// Official Documentation @ https://tools.ietf.org/html/rfc7231#section-6.5.13
///
/// The media format of the requested data is not supported by the server, so
/// the server is rejecting the request.
UNSUPPORTED_MEDIA_TYPE = 415,
/// Official Documentation @ https://tools.ietf.org/html/rfc7233#section-4.4
///
/// The range specified by the Range header field in the request can't be
/// fulfilled; it's possible that the range is outside the size of the target
/// URI's data.
REQUESTED_RANGE_NOT_SATISFIABLE = 416,
/// Official Documentation @ https://tools.ietf.org/html/rfc7231#section-6.5.14
///
/// This response code means the expectation indicated by the Expect request
/// header field can't be met by the server.
EXPECTATION_FAILED = 417,
/// Official Documentation @ https://tools.ietf.org/html/rfc2324#section-2.3.2
///
/// Any attempt to brew coffee with a teapot should result in the error code
/// "418 I'm a teapot". The resulting entity body MAY be short and stout.
IM_A_TEAPOT = 418,
/// Official Documentation @ https://tools.ietf.org/html/rfc2518#section-10.6
///
/// The 507 (Insufficient Storage) status code means the method could not be
/// performed on the resource because the server is unable to store the
/// representation needed to successfully complete the request. This condition
/// is considered to be temporary. If the request which received this status
/// code was the result of a user action, the request MUST NOT be repeated
/// until it is requested by a separate user action.
INSUFFICIENT_SPACE_ON_RESOURCE = 419,
/// @deprecated
/// Official Documentation @ https://tools.ietf.org/rfcdiff?difftype=--hwdiff&url2=draft-ietf-webdav-protocol-06.txt
///
/// A deprecated response used by the Spring Framework when a method has failed.
METHOD_FAILURE = 420,
/// Official Documentation @ https://datatracker.ietf.org/doc/html/rfc7540#section-9.1.2
///
/// Defined in the specification of HTTP/2 to indicate that a server is not
/// able to produce a response for the combination of scheme and authority that
/// are included in the request URI.
MISDIRECTED_REQUEST = 421,
/// Official Documentation @ https://tools.ietf.org/html/rfc2518#section-10.3
///
/// The request was well-formed but was unable to be followed due to semantic errors.
UNPROCESSABLE_ENTITY = 422,
/// Official Documentation @ https://tools.ietf.org/html/rfc2518#section-10.4
///
/// The resource that is being accessed is locked.
LOCKED = 423,
/// Official Documentation @ https://tools.ietf.org/html/rfc2518#section-10.5
///
/// The request failed due to failure of a previous request.
FAILED_DEPENDENCY = 424,
/// Official Documentation @ https://datatracker.ietf.org/doc/html/rfc7231#section-6.5.15
///
/// The server refuses to perform the request using the current protocol but
/// might be willing to do so after the client upgrades to a different
/// protocol.
UPGRADE_REQUIRED = 426,
/// Official Documentation @ https://tools.ietf.org/html/rfc6585#section-3
///
/// The origin server requires the request to be conditional. Intended to
/// prevent the 'lost update' problem, where a client GETs a resource's state,
/// modifies it, and PUTs it back to the server, when meanwhile a third party
/// has modified the state on the server, leading to a conflict.
PRECONDITION_REQUIRED = 428,
/// Official Documentation @ https://tools.ietf.org/html/rfc6585#section-4
///
/// The user has sent too many requests in a given amount of time ("rate limiting").
TOO_MANY_REQUESTS = 429,
/// Official Documentation @ https://tools.ietf.org/html/rfc6585#section-5
///
/// The server is unwilling to process the request because its header fields
/// are too large. The request MAY be resubmitted after reducing the size of
/// the request header fields.
REQUEST_HEADER_FIELDS_TOO_LARGE = 431,
/// Official Documentation @ https://tools.ietf.org/html/rfc7725
///
/// The user-agent requested a resource that cannot legally be provided, such
/// as a web page censored by a government.
UNAVAILABLE_FOR_LEGAL_REASONS = 451,
/// Official Documentation @ https://tools.ietf.org/html/rfc7231#section-6.6.1
///
/// The server encountered an unexpected condition that prevented it from
/// fulfilling the request.
INTERNAL_SERVER_ERROR = 500,
/// Official Documentation @ https://tools.ietf.org/html/rfc7231#section-6.6.2
///
/// The request method is not supported by the server and cannot be handled.
/// The only methods that servers are required to support (and therefore that
/// must not return this code) are GET and HEAD.
NOT_IMPLEMENTED = 501,
/// Official Documentation @ https://tools.ietf.org/html/rfc7231#section-6.6.3
///
/// This error response means that the server, while working as a gateway to
/// get a response needed to handle the request, got an invalid response.
BAD_GATEWAY = 502,
/// Official Documentation @ https://tools.ietf.org/html/rfc7231#section-6.6.4
///
/// The server is not ready to handle the request. Common causes are a server
/// that is down for maintenance or that is overloaded. Note that together with
/// this response, a user-friendly page explaining the problem should be sent.
/// This responses should be used for temporary conditions and the Retry-After:
/// HTTP header should, if possible, contain the estimated time before the
/// recovery of the service. The webmaster must also take care about the
/// caching-related headers that are sent along with this response, as these
/// temporary condition responses should usually not be cached.
SERVICE_UNAVAILABLE = 503,
/// Official Documentation @ https://tools.ietf.org/html/rfc7231#section-6.6.5
///
/// This error response is given when the server is acting as a gateway and
/// cannot get a response in time.
GATEWAY_TIMEOUT = 504,
/// Official Documentation @ https://tools.ietf.org/html/rfc7231#section-6.6.6
///
/// The HTTP version used in the request is not supported by the server.
HTTP_VERSION_NOT_SUPPORTED = 505,
/// Official Documentation @ https://tools.ietf.org/html/rfc2518#section-10.6
///
/// The server has an internal configuration error: the chosen variant
/// resource is configured to engage in transparent content negotiation itself,
/// and is therefore not a proper end point in the negotiation process.
INSUFFICIENT_STORAGE = 507,
/// Official Documentation @ https://tools.ietf.org/html/rfc6585#section-6
///
/// The 511 status code indicates that the client needs to authenticate to
/// gain network access.
NETWORK_AUTHENTICATION_REQUIRED = 511,
}
export class HttpError extends Error {
readonly statusCode: number;
readonly headers: [string, string][] | undefined;
constructor(
statusCode: number,
message?: string,
headers?: [string, string][],
) {
super(message);
this.statusCode = statusCode;
this.headers = headers;
}
public override toString(): string {
return `HttpError(${this.statusCode}, ${this.message})`;
}
toResponse(): ResponseType {
const m = this.message;
return {
headers: this.headers,
status: this.statusCode,
body: m !== "" ? encodeFallback(m) : undefined,
};
}
}
export type StringRequestType = {
uri: string;
params: PathParamsType;
headers: HeaderMapType;
user?: UserType;
body?: string;
};
export type StringResponseType = {
headers?: [string, string][];
status?: number;
body: string;
};
export function stringHandler(
f: (req: StringRequestType) => MaybeResponse<StringResponseType | string>,
): CallbackType {
return async (req: RequestType): Promise<ResponseType | undefined> => {
try {
const body = req.body;
const resp: StringResponseType | string | undefined = await f({
uri: req.uri,
params: req.params,
headers: req.headers,
user: req.user,
body: body && decodeFallback(body),
});
if (resp === undefined) {
return undefined;
}
if (typeof resp === "string") {
return {
status: StatusCodes.OK,
body: encodeFallback(resp),
};
}
const respBody = resp.body;
return {
headers: resp.headers,
status: resp.status,
body: respBody ? encodeFallback(respBody) : undefined,
};
} catch (err) {
if (err instanceof HttpError) {
return err.toResponse();
}
return {
status: StatusCodes.INTERNAL_SERVER_ERROR,
body: encodeFallback(`Uncaught error: ${err}`),
};
}
};
}
export type HtmlResponseType = {
headers?: [string, string][];
status?: number;
body: string;
};
export function htmlHandler(
f: (req: StringRequestType) => MaybeResponse<HtmlResponseType | string>,
): CallbackType {
return async (req: RequestType): Promise<ResponseType | undefined> => {
try {
const body = req.body;
const resp: HtmlResponseType | string | undefined = await f({
uri: req.uri,
params: req.params,
headers: req.headers,
user: req.user,
body: body && decodeFallback(body),
});
if (resp === undefined) {
return undefined;
}
if (typeof resp === "string") {
return {
headers: [["content-type", "text/html"]],
status: StatusCodes.OK,
body: encodeFallback(resp),
};
}
const respBody = resp.body;
return {
headers: [["content-type", "text/html"], ...(resp.headers ?? [])],
status: resp.status,
body: respBody ? encodeFallback(respBody) : undefined,
};
} catch (err) {
if (err instanceof HttpError) {
return err.toResponse();
}
return {
status: StatusCodes.INTERNAL_SERVER_ERROR,
body: encodeFallback(`Uncaught error: ${err}`),
};
}
};
}
export type JsonRequestType = {
uri: string;
params: PathParamsType;
headers: HeaderMapType;
user?: UserType;
body?: object | string;
};
export interface JsonResponseType {
headers?: [string, string][];
status?: number;
body: object;
}
export function jsonHandler(
f: (req: JsonRequestType) => MaybeResponse<JsonRequestType | object>,
): CallbackType {
return async (req: RequestType): Promise<ResponseType | undefined> => {
try {
const body = req.body;
const resp: JsonResponseType | object | undefined = await f({
uri: req.uri,
params: req.params,
headers: req.headers,
user: req.user,
body: body && decodeFallback(body),
});
if (resp === undefined) {
return undefined;
}
if ("body" in resp) {
const r = resp as JsonResponseType;
const rBody = r.body;
return {
headers: [["content-type", "application/json"], ...(r.headers ?? [])],
status: r.status,
body: rBody ? encodeFallback(JSON.stringify(rBody)) : undefined,
};
}
return {
headers: [["content-type", "application/json"]],
status: StatusCodes.OK,
body: encodeFallback(JSON.stringify(resp)),
};
} catch (err) {
if (err instanceof HttpError) {
return err.toResponse();
}
return {
headers: [["content-type", "application/json"]],
status: StatusCodes.INTERNAL_SERVER_ERROR,
body: encodeFallback(`Uncaught error: ${err}`),
};
}
};
}
const routerCallbacks = new Map<string, CallbackType>();
function isolateId(): number {
return rustyscript.functions.isolate_id();
}
export function addRoute(
method: Method,
route: string,
callback: CallbackType,
) {
if (isolateId() === 0) {
rustyscript.functions.install_route(method, route);
console.debug("JS: Added route:", method, route);
}
routerCallbacks.set(`${method}:${route}`, callback);
}
export async function dispatch(
method: Method,
route: string,
uri: string,
pathParams: [string, string][],
headers: [string, string][],
user: UserType | undefined,
body: Uint8Array,
): Promise<ResponseType> {
const key = `${method}:${route}`;
const cb: CallbackType | undefined = routerCallbacks.get(key);
if (!cb) {
throw Error(`Missing callback: ${key}`);
}
return (
(await cb({
uri,
params: Object.fromEntries(pathParams),
headers: Object.fromEntries(headers),
user: user,
body,
})) ?? { status: StatusCodes.OK }
);
}
globalThis.__dispatch = dispatch;
const cronCallbacks = new Map<number, () => void | Promise<void>>();
/// Installs a Cron job that is registered to be orchestrated from native code.
export function addCronCallback(
name: string,
schedule: string,
cb: () => void | Promise<void>,
) {
const cronRegex =
/^(@(yearly|monthly|weekly|daily|hourly|))|((((\d+,)+\d+|(\d+(\/|-)\d+)|\d+|\*)\s*){6,7})$/;
const matches = cronRegex.test(schedule);
if (!matches) {
throw Error(`Not a valid 6/7-component cron schedule: ${schedule}`);
}
if (isolateId() === 0) {
const id = rustyscript.functions.install_job(name, schedule);
console.debug(`JS: Added cron job (id=${id}): "${name}"`);
cronCallbacks.set(id, cb);
}
}
async function dispatchCron(id: number): Promise<string | undefined> {
const cb: (() => void | Promise<void>) | undefined = cronCallbacks.get(id);
if (!cb) {
throw Error(`Missing cron callback: ${id}`);
}
try {
await cb();
} catch (err) {
return `${err}`;
}
}
globalThis.__dispatchCron = dispatchCron;
/// Installs a periodic callback in a single isolate and returns a cleanup function.
export function addPeriodicCallback(
milliseconds: number,
cb: (cancel: () => void) => void,
): () => void {
// Note: right now we run periodic tasks only on the first isolate. This is
// very simple but doesn't use other workers. This has nice properties in
// terms of state management and hopefully work-stealing will alleviate the
// issue, i.e. workers will pick up the slack in terms of incoming requests.
if (isolateId() !== 0) {
return () => {};
}
const handle = setInterval(() => {
cb(() => clearInterval(handle));
}, milliseconds);
return () => clearInterval(handle);
}
export type Blob = {
blob: string;
};
export type SqlType = number | string | Blob | null;
/// Queries the SQLite database.
export async function query(
sql: string,
params: SqlType[],
): Promise<SqlType[][]> {
return await rustyscript.async_functions.query(sql, params);
}
/// Executes given query against the SQLite database.
export async function execute(sql: string, params: SqlType[]): Promise<number> {
return await rustyscript.async_functions.execute(sql, params);
}
export class Transaction {
finalized: boolean;
constructor() {
this.finalized = false;
}
public query(queryStr: string, params: SqlType[]): SqlType[][] {
return rustyscript.functions.transaction_query(queryStr, params);
}
public execute(queryStr: string, params: SqlType[]): SqlType {
return rustyscript.functions.transaction_execute(queryStr, params);
}
public commit(): void {
this.finalized = true;
rustyscript.functions.transaction_commit();
}
public rollback(): void {
this.finalized = true;
rustyscript.functions.transaction_rollback();
}
}
/// Commit a SQLite transaction.
///
/// NOTE: The API is async while the implementation is not. This is for
/// future-proofing. This means that calling transaction() will block the
/// event-loop until a write-lock on the underlying database connection can be
/// acquired. In most scenarios this should be fine but may become a bottleneck
/// when there's a lot of write congestion. In the future, we should update the
/// implementation to be async.
export async function transaction<T>(f: (tx: Transaction) => T): Promise<T> {
await rustyscript.async_functions.transaction_begin();
const tx = new Transaction();
try {
const r = f(tx);
if (!tx.finalized) {
rustyscript.functions.transaction_rollback();
}
return r;
} catch (e) {
rustyscript.functions.transaction_rollback();
throw e;
}
}
export type ParsedPath = {
path: string;
query: URLSearchParams;
};
export function parsePath(path: string): ParsedPath {
const queryIndex = path.indexOf("?");
if (queryIndex >= 0) {
return {
path: path.slice(0, queryIndex),
query: new URLSearchParams(path.slice(queryIndex + 1)),
};
}
return {
path,
query: new URLSearchParams(),
};
}

View File

@@ -1,148 +0,0 @@
/// @param {Uint8Array} bytes
/// @return {string}
///
/// source: https://github.com/samthor/fast-text-encoding
export function decodeFallback(bytes: Uint8Array): string {
var inputIndex = 0;
// Create a working buffer for UTF-16 code points, but don't generate one
// which is too large for small input sizes. UTF-8 to UCS-16 conversion is
// going to be at most 1:1, if all code points are ASCII. The other extreme
// is 4-byte UTF-8, which results in two UCS-16 points, but this is still 50%
// fewer entries in the output.
var pendingSize = Math.min(256 * 256, bytes.length + 1);
var pending = new Uint16Array(pendingSize);
var chunks = [];
var pendingIndex = 0;
for (;;) {
var more = inputIndex < bytes.length;
// If there's no more data or there'd be no room for two UTF-16 values,
// create a chunk. This isn't done at the end by simply slicing the data
// into equal sized chunks as we might hit a surrogate pair.
if (!more || pendingIndex >= pendingSize - 1) {
// nb. .apply and friends are *really slow*. Low-hanging fruit is to
// expand this to literally pass pending[0], pending[1], ... etc, but
// the output code expands pretty fast in this case.
// These extra vars get compiled out: they're just to make TS happy.
// Turns out you can pass an ArrayLike to .apply().
var subarray = pending.subarray(0, pendingIndex);
var arraylike = subarray as unknown as number[];
chunks.push(String.fromCharCode.apply(null, arraylike));
if (!more) {
return chunks.join("");
}
// Move the buffer forward and create another chunk.
bytes = bytes.subarray(inputIndex);
inputIndex = 0;
pendingIndex = 0;
}
// The native TextDecoder will generate "REPLACEMENT CHARACTER" where the
// input data is invalid. Here, we blindly parse the data even if it's
// wrong: e.g., if a 3-byte sequence doesn't have two valid continuations.
var byte1 = bytes[inputIndex++];
if ((byte1 & 0x80) === 0) {
// 1-byte or null
pending[pendingIndex++] = byte1;
} else if ((byte1 & 0xe0) === 0xc0) {
// 2-byte
var byte2 = bytes[inputIndex++] & 0x3f;
pending[pendingIndex++] = ((byte1 & 0x1f) << 6) | byte2;
} else if ((byte1 & 0xf0) === 0xe0) {
// 3-byte
var byte2 = bytes[inputIndex++] & 0x3f;
var byte3 = bytes[inputIndex++] & 0x3f;
pending[pendingIndex++] = ((byte1 & 0x1f) << 12) | (byte2 << 6) | byte3;
} else if ((byte1 & 0xf8) === 0xf0) {
// 4-byte
var byte2 = bytes[inputIndex++] & 0x3f;
var byte3 = bytes[inputIndex++] & 0x3f;
var byte4 = bytes[inputIndex++] & 0x3f;
// this can be > 0xffff, so possibly generate surrogates
var codepoint =
((byte1 & 0x07) << 0x12) | (byte2 << 0x0c) | (byte3 << 0x06) | byte4;
if (codepoint > 0xffff) {
// codepoint &= ~0x10000;
codepoint -= 0x10000;
pending[pendingIndex++] = ((codepoint >>> 10) & 0x3ff) | 0xd800;
codepoint = 0xdc00 | (codepoint & 0x3ff);
}
pending[pendingIndex++] = codepoint;
} else {
// invalid initial byte
}
}
}
/// @param {string} string
/// @return {Uint8Array}
////
/// source: https://github.com/samthor/fast-text-encoding
export function encodeFallback(string: string): Uint8Array {
var pos = 0;
var len = string.length;
var at = 0; // output position
var tlen = Math.max(32, len + (len >>> 1) + 7); // 1.5x size
var target = new Uint8Array((tlen >>> 3) << 3); // ... but at 8 byte offset
while (pos < len) {
var value = string.charCodeAt(pos++);
if (value >= 0xd800 && value <= 0xdbff) {
// high surrogate
if (pos < len) {
var extra = string.charCodeAt(pos);
if ((extra & 0xfc00) === 0xdc00) {
++pos;
value = ((value & 0x3ff) << 10) + (extra & 0x3ff) + 0x10000;
}
}
if (value >= 0xd800 && value <= 0xdbff) {
continue; // drop lone surrogate
}
}
// expand the buffer if we couldn't write 4 bytes
if (at + 4 > target.length) {
tlen += 8; // minimum extra
tlen *= 1.0 + (pos / string.length) * 2; // take 2x the remaining
tlen = (tlen >>> 3) << 3; // 8 byte offset
var update = new Uint8Array(tlen);
update.set(target);
target = update;
}
if ((value & 0xffffff80) === 0) {
// 1-byte
target[at++] = value; // ASCII
continue;
} else if ((value & 0xfffff800) === 0) {
// 2-byte
target[at++] = ((value >>> 6) & 0x1f) | 0xc0;
} else if ((value & 0xffff0000) === 0) {
// 3-byte
target[at++] = ((value >>> 12) & 0x0f) | 0xe0;
target[at++] = ((value >>> 6) & 0x3f) | 0x80;
} else if ((value & 0xffe00000) === 0) {
// 4-byte
target[at++] = ((value >>> 18) & 0x07) | 0xf0;
target[at++] = ((value >>> 12) & 0x3f) | 0x80;
target[at++] = ((value >>> 6) & 0x3f) | 0x80;
} else {
continue; // out of range
}
target[at++] = (value & 0x3f) | 0x80;
}
// Use subarray if slice isn't supported (IE11). This will use more memory
// because the original array still exists.
return target.slice ? target.slice(0, at) : target.subarray(0, at);
}

View File

@@ -1,132 +0,0 @@
import { test, expect } from "vitest";
import { addPeriodicCallback, parsePath, query, execute, stringHandler, htmlHandler, jsonHandler, addRoute, dispatch, HttpError } from "../src/trailbase";
import type { Method, RequestType, StringRequestType, CallbackType } from "../src/trailbase";
import { decodeFallback, encodeFallback } from "../src/util";
globalThis.rustyscript = {
async_functions: {},
functions: {
isolate_id: () => 0,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
install_route: (_method: Method, _route: string) => { },
},
};
test("periodic callback", async () => {
const promise = new Promise((resolve) => {
let count = 0;
const result: number[] = [];
addPeriodicCallback(1, (cancel) => {
result.push(count++);
if (result.length > 2) {
resolve(result);
cancel();
}
});
});
expect(await promise).toEqual([0, 1, 2]);
});
test("binary encode/decode", () => {
const a = `1234567890-=qwertyuiop[]asdfghjkl;'zxcvbnm,./~!@#$%^&*()_+ `;
expect(decodeFallback(encodeFallback(a))).toEqual(a);
});
test("parse path", () => {
const parsedPath = parsePath("/p0/p1/p2?a=x&a=y&b=z");
expect(parsedPath.path).toEqual("/p0/p1/p2");
const q = parsedPath.query;
expect(q.getAll("a")).toEqual(["x", "y"]);
expect(q.get("b")).toEqual("z");
});
test("db functions", async () => {
type Args = {
query: string;
params: unknown[];
};
let queryArgs: Args = { query: "", params: [] };
let executeArgs: Args = { query: "", params: [] };
{
const query = async (query: string, params: unknown[]) => queryArgs = { query, params };
const execute = async (query: string, params: unknown[]) => executeArgs = { query, params };
globalThis.rustyscript = {
...globalThis.rustyscript,
async_functions: {
query,
execute,
},
};
}
const executeStr = "INSERT INTO table (col) VALUES (?1)";
await execute(executeStr, ["test"]);
expect(executeArgs.query).toEqual(executeStr);
expect(executeArgs.params).toEqual(["test"]);
const queryStr = "SELECT * FROM table WHERE col = ?1";
await query(queryStr, ["test"]);
expect(queryArgs.query).toEqual(queryStr);
expect(queryArgs.params).toEqual(["test"]);
});
test("routes functions", async () => {
const promise = new Promise<StringRequestType>((resolve) => {
addRoute("GET", "/test", stringHandler(async (req: StringRequestType) => {
resolve(req);
return "response";
}));
});
const uri = "http://127.0.0.1";
dispatch("GET", "/test", uri, [], [], undefined, encodeFallback("test"));
const result: StringRequestType = await promise;
expect(result.uri).toEqual(uri);
});
test("string handler", async () => {
const req = {
uri: "http://test.gov",
params: {},
headers: {},
} satisfies RequestType;
{
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const handler: CallbackType = stringHandler((_req) => "test");
const response = (await handler(req))!;
expect(decodeFallback(response.body!)).toEqual("test");
}
{
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const handler: CallbackType = stringHandler((_req) => {
throw new HttpError(418);
});
expect((await handler(req))!.status).toEqual(418);
}
{
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const handler: CallbackType = htmlHandler((_req) => {
throw new HttpError(418);
});
expect((await handler(req))!.status).toEqual(418);
}
{
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const handler: CallbackType = jsonHandler((_req) => {
throw new HttpError(418);
});
expect((await handler(req))!.status).toEqual(418);
}
});

View File

@@ -1,22 +0,0 @@
{
"compilerOptions": {
"lib": ["esnext", "DOM"],
"target": "es2022",
"module": "es2022",
"declaration": true,
"strict": true,
"strictNullChecks": true,
"forceConsistentCasingInFileNames": true,
"esModuleInterop": false,
"moduleResolution": "bundler",
"outDir": "dist",
"paths": {
"@/*": ["./src/*"]
}
},
"exclude": [
"dist/**",
"node_modules/",
"types/**"
]
}

View File

@@ -1,19 +0,0 @@
import { defineConfig } from "vite";
import dts from 'vite-plugin-dts'
export default defineConfig({
plugins: [
dts({ rollupTypes: true }),
],
build: {
outDir: "./dist",
minify: false,
lib: {
entry: "./src/index.ts",
name: "runtime",
fileName: "index",
formats: ["es"],
},
},
})

View File

@@ -1,14 +0,0 @@
#![allow(clippy::needless_return)]
use std::{io::Result, path::PathBuf};
fn main() -> Result<()> {
trailbase_build::init_env_logger();
let path = PathBuf::from("assets").join("runtime");
trailbase_build::rerun_if_changed(path.join("src"));
trailbase_build::build_js(path)?;
return Ok(());
}

View File

@@ -1,55 +0,0 @@
use rustyscript::deno_core::{
ModuleSpecifier, RequestedModuleType, ResolutionKind, error::ModuleLoaderError,
};
use rustyscript::module_loader::ImportProvider as RustyScriptImportProvider;
use crate::util::cow_to_string;
#[derive(Default)]
pub struct ImportProvider;
impl RustyScriptImportProvider for ImportProvider {
fn resolve(
&mut self,
specifier: &ModuleSpecifier,
_referrer: &str,
_kind: ResolutionKind,
) -> Option<Result<ModuleSpecifier, ModuleLoaderError>> {
log::trace!("resolve: {specifier:?}");
// Specifier is just a URL.
match specifier.scheme() {
"file" | "trailbase" => {
return Some(Ok(specifier.clone()));
}
scheme => {
return Some(Err(ModuleLoaderError::uri_error(format!(
"Unsupported schema: '{scheme}'"
))));
}
};
}
fn import(
&mut self,
specifier: &ModuleSpecifier,
_referrer: Option<&ModuleSpecifier>,
_is_dyn_import: bool,
_requested_module_type: RequestedModuleType,
) -> Option<Result<String, ModuleLoaderError>> {
log::trace!("import: {specifier:?}");
match specifier.scheme() {
"trailbase" => {
return Some(Ok(cow_to_string(
crate::JsRuntimeAssets::get("index.js")
.expect("Failed to read rt/index.js")
.data,
)));
}
_ => {
return None;
}
}
}
}

View File

@@ -1,13 +0,0 @@
#![forbid(unsafe_code, clippy::unwrap_used)]
#![allow(clippy::needless_return)]
#![warn(clippy::await_holding_lock, clippy::inefficient_to_string)]
pub mod import_provider;
pub mod runtime;
mod util;
pub use crate::import_provider::ImportProvider;
#[derive(rust_embed::RustEmbed, Clone)]
#[folder = "assets/runtime/dist/"]
pub struct JsRuntimeAssets;

File diff suppressed because it is too large Load Diff

View File

@@ -1,8 +0,0 @@
use std::borrow::Cow;
pub(crate) fn cow_to_string(cow: Cow<'static, [u8]>) -> String {
match cow {
Cow::Borrowed(x) => String::from_utf8_lossy(x).to_string(),
Cow::Owned(x) => String::from_utf8_lossy(&x).to_string(),
}
}

129
pnpm-lock.yaml generated
View File

@@ -382,33 +382,6 @@ importers:
specifier: ^8.46.4
version: 8.46.4(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3)
crates/js-runtime/assets/runtime:
devDependencies:
'@eslint/js':
specifier: ^9.39.1
version: 9.39.1
eslint:
specifier: ^9.39.1
version: 9.39.1(jiti@2.6.1)
prettier:
specifier: ^3.6.2
version: 3.6.2
typescript:
specifier: ^5.9.3
version: 5.9.3
typescript-eslint:
specifier: ^8.46.4
version: 8.46.4(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3)
vite:
specifier: ^7.2.2
version: 7.2.2(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(yaml@2.8.1)
vite-plugin-dts:
specifier: ^4.5.4
version: 4.5.4(@types/node@24.10.1)(rollup@4.53.2)(typescript@5.9.3)(vite@7.2.2(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(yaml@2.8.1))
vitest:
specifier: ^3.2.4
version: 3.2.4(@types/debug@4.1.12)(@types/node@24.10.1)(happy-dom@15.11.7)(jiti@2.6.1)(jsdom@27.2.0)(lightningcss@1.30.2)(terser@5.44.1)(yaml@2.8.1)
docs:
dependencies:
'@astrojs/check':
@@ -3128,8 +3101,8 @@ packages:
resolution: {integrity: sha512-8WB3Jcas3swSvjIeA2yvCJ+Miyz5l1ZmB6HFb9R1317dt9LCQoswg/BGrmAmkWVEszSrrg4RwmO46qIm2OEnSA==}
engines: {node: '>=16'}
caniuse-lite@1.0.30001755:
resolution: {integrity: sha512-44V+Jm6ctPj7R52Na4TLi3Zri4dWUljJd+RDm+j8LtNCc/ihLCT+X1TzoOAkRETEWqjuLnh9581Tl80FvK7jVA==}
caniuse-lite@1.0.30001754:
resolution: {integrity: sha512-x6OeBXueoAceOmotzx3PO4Zpt4rzpeIFsSr6AAePTZxSkXiYDUmpypEl7e2+8NCd9bD7bXjqyef8CJYPC1jfxg==}
case-anything@2.1.13:
resolution: {integrity: sha512-zlOQ80VrQ2Ue+ymH5OuM/DlDq64mEm+B9UTdHULv5osUMD6HalNTblf2b1u/m6QecjsnOkBpqVZ+XPwIVsy7Ng==}
@@ -3386,8 +3359,8 @@ packages:
resolution: {integrity: sha512-OytmFH+13/QXONJcC75QNdMtKpceNk3u8ThBjyyYjkEcy/ekBwR1mMAuNvi3gdBPW3N5TlCzQ0WZw8H0lN/bDw==}
engines: {node: '>=20'}
csstype@3.2.3:
resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==}
csstype@3.2.0:
resolution: {integrity: sha512-si++xzRAY9iPp60roQiFta7OFbhrgvcthrhlNAGeQptSY25uJjkfUV8OArC3KLocB8JT8ohz+qgxWCmz8RhjIg==}
csv-parse@5.6.0:
resolution: {integrity: sha512-l3nz3euub2QMg5ouu5U09Ew9Wf6/wQ8I++ch1loQ0ljmzhmfZYrH9fflS22i/PQEvsPvxCwxgz5q7UB8K1JO4Q==}
@@ -3524,8 +3497,8 @@ packages:
ee-first@1.1.1:
resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==}
electron-to-chromium@1.5.254:
resolution: {integrity: sha512-DcUsWpVhv9svsKRxnSCZ86SjD+sp32SGidNB37KpqXJncp1mfUgKbHvBomE89WJDbfVKw1mdv5+ikrvd43r+Bg==}
electron-to-chromium@1.5.252:
resolution: {integrity: sha512-53uTpjtRgS7gjIxZ4qCgFdNO2q+wJt/Z8+xAvxbCqXPJrY6h7ighUkadQmNMXH96crtpa6gPFNP7BF4UBGDuaA==}
emmet@2.4.11:
resolution: {integrity: sha512-23QPJB3moh/U9sT4rQzGgeyyGIrcM+GH5uVYg2C6wZIxAIJq7Ng3QLT79tl8FUwDXhyq9SusfknOrofAKqvgyQ==}
@@ -3840,8 +3813,8 @@ packages:
fontkit@2.0.4:
resolution: {integrity: sha512-syetQadaUEDNdxdugga9CpEYVaQIxOwk7GlwZWWZ19//qW4zE5bknOKeMBDYAASwnpaSHKJITRLMF9m1fp3s6g==}
form-data@4.0.5:
resolution: {integrity: sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==}
form-data@4.0.4:
resolution: {integrity: sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==}
engines: {node: '>= 6'}
forwarded@0.2.0:
@@ -4152,8 +4125,8 @@ packages:
inherits@2.0.4:
resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==}
inline-style-parser@0.2.7:
resolution: {integrity: sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA==}
inline-style-parser@0.2.6:
resolution: {integrity: sha512-gtGXVaBdl5mAes3rPcMedEBm12ibjt1kDMFfheul1wUAOVEJW60voNdMVzVkfLN06O7ZaD/rxhfKgtlgtTbMjg==}
ipaddr.js@1.9.1:
resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==}
@@ -4262,8 +4235,8 @@ packages:
jju@1.4.0:
resolution: {integrity: sha512-8wb9Yw966OSxApiCt0K3yNJL8pnNeIv+OEq2YMidz4FKP6nonSRoOXc80iXY4JaN2FC11B9qsNmDsm+ZOfMROA==}
jose@6.1.2:
resolution: {integrity: sha512-MpcPtHLE5EmztuFIqB0vzHAWJPpmN1E6L4oo+kze56LIs3MyXIj9ZHMDxqOvkP38gBR7K1v3jqd4WU2+nrfONQ==}
jose@6.1.1:
resolution: {integrity: sha512-GWSqjfOPf4cWOkBzw5THBjtGPhXKqYnfRBzh4Ni+ArTrQQ9unvmsA3oFLqaYKoKe5sjWmGu5wVKg9Ft1i+LQfg==}
js-base64@3.7.8:
resolution: {integrity: sha512-hNngCeKxIUQiEUN3GPJOkz4wF/YvdUdbNL9hsBcMQTkKzboD7T/q3OYOuuPZLUE6dBxSGpwhk5mwuDud7JVAow==}
@@ -5487,8 +5460,8 @@ packages:
deprecated: 'SECURITY: Multiple vulnerabilities fixed in 8.0.1 (XML injection, path traversal, command injection, protocol injection). Upgrade immediately: npm install sitemap@8.0.1'
hasBin: true
smol-toml@1.5.2:
resolution: {integrity: sha512-QlaZEqcAH3/RtNyet1IPIYPsEWAaYyXXv1Krsi+1L/QHppjX4Ifm8MQsBISz9vE8cHicIq3clogsheili5vhaQ==}
smol-toml@1.5.0:
resolution: {integrity: sha512-Jjsa8LZ+DyLbZ7gVi9d18bS8oxq0PQrTlVDfvYXgh7gxLwbW9QWgvakHD+hBLUtr5NahfStd8LQLGSPchaEJ8Q==}
engines: {node: '>= 18'}
solid-devtools@0.30.1:
@@ -5645,11 +5618,11 @@ packages:
style-mod@4.1.3:
resolution: {integrity: sha512-i/n8VsZydrugj3Iuzll8+x/00GH2vnYsk1eomD8QiRrSAeW6ItbCQDtfXCeJHd0iwiNagqjQkvpvREEPtW3IoQ==}
style-to-js@1.1.21:
resolution: {integrity: sha512-RjQetxJrrUJLQPHbLku6U/ocGtzyjbJMP9lCNK7Ag0CNh690nSH8woqWH9u16nMjYBAok+i7JO1NP2pOy8IsPQ==}
style-to-js@1.1.19:
resolution: {integrity: sha512-Ev+SgeqiNGT1ufsXyVC5RrJRXdrkRJ1Gol9Qw7Pb72YCKJXrBvP0ckZhBeVSrw2m06DJpei2528uIpjMb4TsoQ==}
style-to-object@1.0.14:
resolution: {integrity: sha512-LIN7rULI0jBscWQYaSswptyderlarFkjQ+t79nzty8tcIAceVomEVlLzH5VP4Cmsv6MtKhs7qaAiwlcp+Mgaxw==}
style-to-object@1.0.12:
resolution: {integrity: sha512-ddJqYnoT4t97QvN2C95bCgt+m7AAgXjVnkk/jxAfmp7EAB8nnqqZYEbMd3em7/vEomDb2LAQKAy1RFfv41mdNw==}
suf-log@2.5.3:
resolution: {integrity: sha512-KvC8OPjzdNOe+xQ4XWJV2whQA0aM1kGVczMQ8+dStAO6KfEB140JEVQ9dE76ONZ0/Ylf67ni4tILPJB41U0eow==}
@@ -5755,11 +5728,11 @@ packages:
resolution: {integrity: sha512-azl+t0z7pw/z958Gy9svOTuzqIk6xq+NSheJzn5MMWtWTFywIacg2wUlzKFGtt3cthx0r2SxMK0yzJOR0IES7Q==}
engines: {node: '>=14.0.0'}
tldts-core@7.0.18:
resolution: {integrity: sha512-jqJC13oP4FFAahv4JT/0WTDrCF9Okv7lpKtOZUGPLiAnNbACcSg8Y8T+Z9xthOmRBqi/Sob4yi0TE0miRCvF7Q==}
tldts-core@7.0.17:
resolution: {integrity: sha512-DieYoGrP78PWKsrXr8MZwtQ7GLCUeLxihtjC1jZsW1DnvSMdKPitJSe8OSYDM2u5H6g3kWJZpePqkp43TfLh0g==}
tldts@7.0.18:
resolution: {integrity: sha512-lCcgTAgMxQ1JKOWrVGo6E69Ukbnx4Gc1wiYLRf6J5NN4HRYJtCby1rPF8rkQ4a6qqoFBK5dvjJ1zJ0F7VfDSvw==}
tldts@7.0.17:
resolution: {integrity: sha512-Y1KQBgDd/NUc+LfOtKS6mNsC9CCaH+m2P1RoIZy7RAPo3C3/t8X45+zgut31cRZtZ3xKPjfn3TkGTrctC2TQIQ==}
hasBin: true
to-regex-range@5.0.1:
@@ -6629,7 +6602,7 @@ snapshots:
remark-rehype: 11.1.2
remark-smartypants: 3.0.2
shiki: 3.15.0
smol-toml: 1.5.2
smol-toml: 1.5.0
unified: 11.0.5
unist-util-remove-position: 5.0.0
unist-util-visit: 5.0.0
@@ -8381,7 +8354,7 @@ snapshots:
'@types/react@19.2.5':
dependencies:
csstype: 3.2.3
csstype: 3.2.0
'@types/sax@1.2.7':
dependencies:
@@ -8930,7 +8903,7 @@ snapshots:
rehype: 13.0.2
semver: 7.7.3
shiki: 3.15.0
smol-toml: 1.5.2
smol-toml: 1.5.0
tinyexec: 1.0.2
tinyglobby: 0.2.15
tsconfck: 3.1.6(typescript@4.9.4)
@@ -9031,7 +9004,7 @@ snapshots:
rehype: 13.0.2
semver: 7.7.3
shiki: 3.15.0
smol-toml: 1.5.2
smol-toml: 1.5.0
tinyexec: 1.0.2
tinyglobby: 0.2.15
tsconfck: 3.1.6(typescript@5.9.3)
@@ -9095,7 +9068,7 @@ snapshots:
autoprefixer@10.4.22(postcss@8.5.6):
dependencies:
browserslist: 4.28.0
caniuse-lite: 1.0.30001755
caniuse-lite: 1.0.30001754
fraction.js: 5.3.4
normalize-range: 0.1.2
picocolors: 1.1.1
@@ -9105,7 +9078,7 @@ snapshots:
axios@1.13.2:
dependencies:
follow-redirects: 1.15.11
form-data: 4.0.5
form-data: 4.0.4
proxy-from-env: 1.1.0
transitivePeerDependencies:
- debug
@@ -9205,8 +9178,8 @@ snapshots:
browserslist@4.28.0:
dependencies:
baseline-browser-mapping: 2.8.28
caniuse-lite: 1.0.30001755
electron-to-chromium: 1.5.254
caniuse-lite: 1.0.30001754
electron-to-chromium: 1.5.252
node-releases: 2.0.27
update-browserslist-db: 1.1.4(browserslist@4.28.0)
@@ -9237,7 +9210,7 @@ snapshots:
camelcase@8.0.0: {}
caniuse-lite@1.0.30001755: {}
caniuse-lite@1.0.30001754: {}
case-anything@2.1.13: {}
@@ -9507,7 +9480,7 @@ snapshots:
'@csstools/css-syntax-patches-for-csstree': 1.0.16
css-tree: 3.1.0
csstype@3.2.3: {}
csstype@3.2.0: {}
csv-parse@5.6.0: {}
@@ -9610,7 +9583,7 @@ snapshots:
ee-first@1.1.1: {}
electron-to-chromium@1.5.254: {}
electron-to-chromium@1.5.252: {}
emmet@2.4.11:
dependencies:
@@ -9766,7 +9739,7 @@ snapshots:
is-html: 2.0.0
kebab-case: 1.0.2
known-css-properties: 0.30.0
style-to-object: 1.0.14
style-to-object: 1.0.12
typescript: 4.9.4
transitivePeerDependencies:
- supports-color
@@ -9779,7 +9752,7 @@ snapshots:
is-html: 2.0.0
kebab-case: 1.0.2
known-css-properties: 0.30.0
style-to-object: 1.0.14
style-to-object: 1.0.12
typescript: 5.9.3
transitivePeerDependencies:
- supports-color
@@ -10059,7 +10032,7 @@ snapshots:
unicode-properties: 1.4.1
unicode-trie: 2.0.0
form-data@4.0.5:
form-data@4.0.4:
dependencies:
asynckit: 0.4.0
combined-stream: 1.0.8
@@ -10313,7 +10286,7 @@ snapshots:
mdast-util-mdxjs-esm: 2.0.1
property-information: 7.1.0
space-separated-tokens: 2.0.2
style-to-js: 1.1.21
style-to-js: 1.1.19
unist-util-position: 5.0.0
zwitch: 2.0.4
transitivePeerDependencies:
@@ -10347,7 +10320,7 @@ snapshots:
mdast-util-mdxjs-esm: 2.0.1
property-information: 7.1.0
space-separated-tokens: 2.0.2
style-to-js: 1.1.21
style-to-js: 1.1.19
unist-util-position: 5.0.0
vfile-message: 4.0.3
transitivePeerDependencies:
@@ -10492,7 +10465,7 @@ snapshots:
inherits@2.0.4: {}
inline-style-parser@0.2.7: {}
inline-style-parser@0.2.6: {}
ipaddr.js@1.9.1: {}
@@ -10565,7 +10538,7 @@ snapshots:
jju@1.4.0: {}
jose@6.1.2: {}
jose@6.1.1: {}
js-base64@3.7.8: {}
@@ -11361,7 +11334,7 @@ snapshots:
cors: 2.8.5
express: 5.1.0
is-plain-obj: 4.1.0
jose: 6.1.2
jose: 6.1.1
transitivePeerDependencies:
- supports-color
@@ -12180,7 +12153,7 @@ snapshots:
arg: 5.0.2
sax: 1.4.3
smol-toml@1.5.2: {}
smol-toml@1.5.0: {}
solid-devtools@0.30.1(solid-js@1.9.10)(vite@6.4.1(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(yaml@2.8.1)):
dependencies:
@@ -12230,7 +12203,7 @@ snapshots:
solid-js@1.9.10:
dependencies:
csstype: 3.2.3
csstype: 3.2.0
seroval: 1.3.2
seroval-plugins: 1.3.3(seroval@1.3.2)
@@ -12369,13 +12342,13 @@ snapshots:
style-mod@4.1.3: {}
style-to-js@1.1.21:
style-to-js@1.1.19:
dependencies:
style-to-object: 1.0.14
style-to-object: 1.0.12
style-to-object@1.0.14:
style-to-object@1.0.12:
dependencies:
inline-style-parser: 0.2.7
inline-style-parser: 0.2.6
suf-log@2.5.3:
dependencies:
@@ -12473,11 +12446,11 @@ snapshots:
tinyspy@4.0.4: {}
tldts-core@7.0.18: {}
tldts-core@7.0.17: {}
tldts@7.0.18:
tldts@7.0.17:
dependencies:
tldts-core: 7.0.18
tldts-core: 7.0.17
to-regex-range@5.0.1:
dependencies:
@@ -12489,7 +12462,7 @@ snapshots:
tough-cookie@6.0.0:
dependencies:
tldts: 7.0.18
tldts: 7.0.17
tr46@0.0.3: {}

View File

@@ -3,7 +3,6 @@ packages:
- 'crates/assets/js/admin'
- 'crates/assets/js/client'
- 'crates/auth-ui/ui'
- 'crates/js-runtime/assets/runtime'
- 'docs'
- 'docs/examples/record_api_ts'
- 'examples/blog/web'