Move js runtime out with offline transpilation.

This commit is contained in:
Sebastian Jeltsch
2024-11-13 12:03:38 +01:00
parent bce0f9c2c0
commit 5929582168
8 changed files with 124 additions and 91 deletions

9
pnpm-lock.yaml generated
View File

@@ -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':

View File

@@ -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

View File

@@ -25,7 +25,7 @@ fn copy_dir(src: impl AsRef<Path>, dst: impl AsRef<Path>) -> Result<()> {
return Ok(());
}
fn build_ui(path: &str) -> Result<()> {
fn build_js(path: &str) -> Result<()> {
let pnpm_run = |args: &[&str]| -> Result<std::process::Output> {
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(());

1
trailbase-core/js/.gitignore vendored Normal file
View File

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

View File

@@ -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"
}
}

View File

@@ -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<string, CbType>();
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<unknown[][]> {
return await rustyscript.async_functions.query(queryStr, params);
}
export async function execute(
queryStr: string,
params: unknown[],
): Promise<number> {
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;

View File

@@ -0,0 +1,13 @@
{
"compilerOptions": {
"declaration": true,
"strict": true,
"strictNullChecks": true,
"forceConsistentCasingInFileNames": true,
"esModuleInterop": false,
"moduleResolution": "bundler",
"target": "ESNext",
"module": "ESNext",
"outDir": "dist"
}
}

View File

@@ -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<dyn std::error::Error + Send + Sync>;
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<string, CbType>();
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<HashMap<String, String>>,
@@ -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<libsql::Value, ru
use rustyscript::Error;
return Ok(match value {
serde_json::Value::Object(ref _map) => {
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<unknown[][]> {
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<number> {
return await execute(queryStr, []);
}
"#,
@@ -648,3 +572,7 @@ mod tests {
assert_eq!(0, count);
}
}
#[derive(RustEmbed, Clone)]
#[folder = "js/dist/"]
struct JsRuntimeAssets;