diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 91b97f5f..50cf51f6 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -208,6 +208,15 @@ importers: specifier: ^8.14.0 version: 8.14.0(eslint@9.14.0(jiti@2.4.0))(typescript@5.6.3) + trailbase-core/js: + devDependencies: + prettier: + specifier: ^3.3.3 + version: 3.3.3 + typescript: + specifier: ^5.6.3 + version: 5.6.3 + ui/admin: dependencies: '@bufbuild/protobuf': diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 1e64cda4..361b5446 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -5,6 +5,7 @@ packages: - 'ui/auth' - 'examples/blog/web' - 'examples/tutorial/scripts' + - 'trailbase-core/js' options: prefer-workspace-packages: true strict-peer-dependencies: true diff --git a/trailbase-core/build.rs b/trailbase-core/build.rs index b1e32bdd..044ed67b 100644 --- a/trailbase-core/build.rs +++ b/trailbase-core/build.rs @@ -25,7 +25,7 @@ fn copy_dir(src: impl AsRef, dst: impl AsRef) -> Result<()> { return Ok(()); } -fn build_ui(path: &str) -> Result<()> { +fn build_js(path: &str) -> Result<()> { let pnpm_run = |args: &[&str]| -> Result { let output = std::process::Command::new("pnpm") .current_dir("..") @@ -45,12 +45,12 @@ fn build_ui(path: &str) -> Result<()> { // NOTE: We don't want to break backend-builds on frontend errors, at least for dev builds. if Ok("release") == env::var("PROFILE").as_deref() { panic!( - "Failed to build ui '{path}': {}", + "Failed to build js '{path}': {}", String::from_utf8_lossy(&output.stderr) ); } warn!( - "Failed to build ui '{path}': {}", + "Failed to build js '{path}': {}", String::from_utf8_lossy(&output.stderr) ); } @@ -97,7 +97,7 @@ fn main() -> Result<()> { let path = "ui/admin"; println!("cargo::rerun-if-changed=../{path}/src/components/"); println!("cargo::rerun-if-changed=../{path}/src/lib/"); - let _ = build_ui(path); + let _ = build_js(path); } { @@ -106,7 +106,12 @@ fn main() -> Result<()> { println!("cargo::rerun-if-changed=../{path}/src/lib/"); println!("cargo::rerun-if-changed=../{path}/src/pages/"); println!("cargo::rerun-if-changed=../{path}/src/layouts/"); - let _ = build_ui("ui/auth"); + let _ = build_js(path); + } + + { + println!("cargo::rerun-if-changed=js/src/"); + let _ = build_js("trailbase-core/js"); } return Ok(()); diff --git a/trailbase-core/js/.gitignore b/trailbase-core/js/.gitignore new file mode 100644 index 00000000..849ddff3 --- /dev/null +++ b/trailbase-core/js/.gitignore @@ -0,0 +1 @@ +dist/ diff --git a/trailbase-core/js/package.json b/trailbase-core/js/package.json new file mode 100644 index 00000000..6991989a --- /dev/null +++ b/trailbase-core/js/package.json @@ -0,0 +1,16 @@ +{ + "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": "tsc", + "format": "prettier -w src" + }, + "devDependencies": { + "prettier": "^3.3.3", + "typescript": "^5.6.3" + } +} diff --git a/trailbase-core/js/src/index.ts b/trailbase-core/js/src/index.ts new file mode 100644 index 00000000..e6e32d06 --- /dev/null +++ b/trailbase-core/js/src/index.ts @@ -0,0 +1,60 @@ +declare var rustyscript: any; +declare var globalThis: any; + +type Headers = { [key: string]: string }; +type Request = { + uri: string; + headers: Headers; + body: string; +}; +type Response = { + headers?: Headers; + status?: number; + body?: string; +}; +type CbType = (req: Request) => Response | undefined; + +const callbacks = new Map(); + +export function addRoute(method: string, route: string, callback: CbType) { + rustyscript.functions.route(method, route); + callbacks.set(`${method}:${route}`, callback); + + console.log("JS: Added route:", method, route); +} + +export async function query( + queryStr: string, + params: unknown[], +): Promise { + return await rustyscript.async_functions.query(queryStr, params); +} + +export async function execute( + queryStr: string, + params: unknown[], +): Promise { + return await rustyscript.async_functions.execute(queryStr, params); +} + +export function dispatch( + method: string, + route: string, + uri: string, + headers: Headers, + body: string, +): Response | undefined { + const key = `${method}:${route}`; + const cb = callbacks.get(key); + if (!cb) { + throw Error(`Missing callback: ${key}`); + } + + return cb({ + uri, + headers, + body, + }); +} + +globalThis.__dispatch = dispatch; diff --git a/trailbase-core/js/tsconfig.json b/trailbase-core/js/tsconfig.json new file mode 100644 index 00000000..f11baa01 --- /dev/null +++ b/trailbase-core/js/tsconfig.json @@ -0,0 +1,13 @@ +{ + "compilerOptions": { + "declaration": true, + "strict": true, + "strictNullChecks": true, + "forceConsistentCasingInFileNames": true, + "esModuleInterop": false, + "moduleResolution": "bundler", + "target": "ESNext", + "module": "ESNext", + "outDir": "dist" + } +} diff --git a/trailbase-core/src/js/mod.rs b/trailbase-core/src/js/mod.rs index 24e496ce..35220a45 100644 --- a/trailbase-core/src/js/mod.rs +++ b/trailbase-core/src/js/mod.rs @@ -5,6 +5,7 @@ use axum::response::{IntoResponse, Response}; use axum::Router; use libsql::Connection; use parking_lot::Mutex; +use rust_embed::RustEmbed; use rustyscript::{json_args, Module, Runtime}; use serde::Deserialize; use serde_json::from_value; @@ -13,6 +14,7 @@ use std::str::FromStr; use std::sync::{Arc, LazyLock}; use thiserror::Error; +use crate::assets::cow_to_string; use crate::records::sql_to_json::rows_to_json_arrays; use crate::AppState; @@ -20,68 +22,6 @@ mod import_provider; type AnyError = Box; -const TRAILBASE_MAIN: &str = r#" -type Headers = { [key: string]: string }; -type Request = { - uri: string; - headers: Headers; - body: string; -}; -type Response = { - headers?: Headers; - status?: number; - body?: string; -}; -type CbType = (req: Request) => Response | undefined; - -const callbacks = new Map(); - -export function addRoute(method: string, route: string, callback: CbType) { - rustyscript.functions.route(method, route); - callbacks.set(`${method}:${route}`, callback); - - console.log("JS: Added route:", method, route); -} - -export async function query(queryStr: string, params: unknown[]) : unknown[][] { - return await rustyscript.async_functions.query(queryStr, params); -} - -export async function execute(queryStr: string, params: unknown[]) : number { - return await rustyscript.async_functions.execute(queryStr, params); -} - -export function dispatch( - method: string, - route: string, - uri: string, - headers: Headers, - body: string, -) : Response | undefined { - console.log("JS: Dispatching:", method, route, body); - - const key = `${method}:${route}`; - const cb = callbacks.get(key); - if (!cb) { - throw Error(`Missing callback: ${key}`); - } - - return cb({ - uri, - headers, - body, - }); -}; - -globalThis.__dispatch = dispatch; -globalThis.__trailbase = { - addRoute, - dispatch, - query, - execute, -}; -"#; - #[derive(Default, Deserialize)] struct JsResponse { headers: Option>, @@ -116,9 +56,6 @@ impl RuntimeSingleton { let handle = std::thread::spawn(move || { let mut runtime = Self::init_runtime().unwrap(); - let module = Module::new("__index.ts", TRAILBASE_MAIN); - let _handle = runtime.load_module(&module).unwrap(); - #[allow(clippy::never_loop)] while let Ok(message) = receiver.recv() { match message { @@ -141,13 +78,7 @@ impl RuntimeSingleton { cache.set( "trailbase:main", - r#" - export const _test = "test0"; - export const addRoute = globalThis.__trailbase.addRoute; - export const query = globalThis.__trailbase.query; - export const execute = globalThis.__trailbase.execute; - "# - .to_string(), + cow_to_string(JsRuntimeAssets::get("index.js").unwrap().data), ); let runtime = rustyscript::Runtime::new(rustyscript::RuntimeOptions { @@ -171,10 +102,10 @@ pub fn json_value_to_param(value: serde_json::Value) -> Result { - return Err(Error::Runtime(format!("Object unsupported"))); + return Err(Error::Runtime("Object unsupported".to_string())); } serde_json::Value::Array(ref _arr) => { - return Err(Error::Runtime(format!("Array unsupported"))); + return Err(Error::Runtime("Array unsupported".to_string())); } serde_json::Value::Null => libsql::Value::Null, serde_json::Value::Bool(b) => libsql::Value::Integer(b as i64), @@ -221,10 +152,8 @@ impl RuntimeHandle { .await .map_err(|err| rustyscript::Error::Runtime(err.to_string()))?; - return Ok( - serde_json::to_value(values) - .map_err(|err| rustyscript::Error::Runtime(err.to_string()))?, - ); + return serde_json::to_value(values) + .map_err(|err| rustyscript::Error::Runtime(err.to_string())); }) }) .unwrap(); @@ -246,8 +175,6 @@ impl RuntimeHandle { .await .map_err(|err| rustyscript::Error::Runtime(err.to_string()))?; - log::error!("ROWS AFF {rows_affected}"); - return Ok(serde_json::Value::Number(rows_affected.into())); }) }) @@ -498,11 +425,8 @@ mod tests { .load_module(&Module::new( "module.js", r#" - import { _test } from "trailbase:main"; - import { dispatch } from "./__index.ts"; - export function test_fun() { - return _test; + return "test0"; } "#, )) @@ -547,7 +471,7 @@ mod tests { r#" import { query } from "trailbase:main"; - export async function test_query(queryStr: string) : unknown[][] { + export async function test_query(queryStr: string) : Promise { return await query(queryStr, []); } "#, @@ -610,7 +534,7 @@ mod tests { r#" import { execute } from "trailbase:main"; - export async function test_execute(queryStr: string) : number { + export async function test_execute(queryStr: string) : Promise { return await execute(queryStr, []); } "#, @@ -648,3 +572,7 @@ mod tests { assert_eq!(0, count); } } + +#[derive(RustEmbed, Clone)] +#[folder = "js/dist/"] +struct JsRuntimeAssets;