use std::collections::HashSet;
use std::path::{Path, PathBuf};
use std::sync::Arc;
use std::sync::Mutex;
use oj_js::EngineConfig;
use oj_js::HostModule;
use oj_js::HostModuleType;
use oj_js::HostResolved;
use oj_js::JsEngine;
use oj_js::ModuleHost;
use oj_server::SsrBridge;
use oj_server::StartResolution;
use crate::ssr_dev::normalize_path;
use crate::ssr_host::{importer_id, versioned_id, VersionGraph};
const START_BOOTSTRAP_JS: &str = include_str!("assets/start-bootstrap.mjs");
const STUB_PREFIX: &str = "oj-start-stub:";
const PROBE_EXTS: [&str; 7] = [".ts", ".tsx", ".mts", ".js", ".jsx", ".mjs", ".json"];
fn probe(base: &Path) -> Option<PathBuf> {
if base.is_file() {
return Some(base.to_path_buf());
}
let s = base.to_string_lossy();
for ext in PROBE_EXTS {
let p = PathBuf::from(format!("{s}{ext}"));
if p.is_file() {
return Some(p);
}
}
for ext in PROBE_EXTS {
let p = base.join(format!("index{ext}"));
if p.is_file() {
return Some(p);
}
}
None
}
pub(crate) struct NoExternal {
rules: oj_config::SsrExternals,
regexes: Vec<regex::Regex>,
}
impl NoExternal {
fn new(rules: oj_config::SsrExternals) -> NoExternal {
let regexes = rules
.no_external_regex
.iter()
.filter_map(|src| regex::Regex::new(src).ok())
.collect();
NoExternal { rules, regexes }
}
fn matches_pkg(&self, pkg: &str) -> bool {
if self.rules.external.iter().any(|e| e == pkg) || self.rules.external_all {
return false;
}
if self.rules.no_external_all {
return true;
}
if self.rules.no_external.iter().any(|p| glob_match(p, pkg)) {
return true;
}
self.regexes.iter().any(|re| re.is_match(pkg))
}
pub(crate) fn matches_path(&self, path: &str) -> bool {
match pkg_name_of_path(path) {
Some(pkg) => self.matches_pkg(&pkg),
None => false,
}
}
}
fn glob_match(pattern: &str, value: &str) -> bool {
if !pattern.contains('*') {
return pattern == value;
}
let re = format!(
"^{}$",
pattern
.split('*')
.map(regex::escape)
.collect::<Vec<_>>()
.join(".*")
);
regex::Regex::new(&re)
.map(|r| r.is_match(value))
.unwrap_or(false)
}
fn pkg_name_of_path(path: &str) -> Option<String> {
let i = path.rfind("/node_modules/")?;
let rest = &path[i + "/node_modules/".len()..];
let mut parts = rest.split('/');
let first = parts.next().filter(|p| !p.is_empty())?;
if let Some(scope) = first.strip_prefix('@').map(|_| first) {
let second = parts.next().filter(|p| !p.is_empty())?;
return Some(format!("{scope}/{second}"));
}
Some(first.to_string())
}
fn base64_decode(s: &str) -> Vec<u8> {
use base64::Engine;
base64::engine::general_purpose::STANDARD
.decode(s)
.unwrap_or_default()
}
fn base64url(bytes: &[u8]) -> String {
use base64::Engine;
base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(bytes)
}
pub(crate) fn rewrite_server_fns(code: &str, rel: &str) -> String {
if !code.contains("createServerFn") {
return code.to_string();
}
static RE: std::sync::OnceLock<regex::Regex> = std::sync::OnceLock::new();
let re = RE.get_or_init(|| {
regex::Regex::new(
r"(?:^|[\n;])[ \t]*((?:export\s+)?const\s+([A-Za-z_$][A-Za-z0-9_$]*)\s*=\s*createServerFn)\b",
)
.expect("server-fn rewrite regex")
});
let mut edits: Vec<(usize, String)> = Vec::new();
for caps in re.captures_iter(code) {
let decl = caps.get(1).expect("decl group");
let name = &caps[2];
let Some(handler_args_at) = scan_chain_to_handler(code, decl.end()) else {
continue; };
let id = base64url(format!("{rel}#{name}").as_bytes());
let meta = format!("{{ id: {id:?}, name: {name:?}, filename: {rel:?} }}");
edits.push((
decl.start(),
format!(
"export const {name}_createServerFn_handler = createServerRpc({meta}, (opts) => {name}.__executeServer(opts)); "
),
));
edits.push((handler_args_at, format!("{name}_createServerFn_handler, ")));
}
if edits.is_empty() {
return code.to_string();
}
edits.sort_by_key(|(at, _)| *at);
let mut out =
String::from("import { createServerRpc } from \"@tanstack/react-start/server-rpc\"; ");
let mut last = 0;
for (at, text) in edits {
out.push_str(&code[last..at]);
out.push_str(&text);
last = at;
}
out.push_str(&code[last..]);
out
}
fn scan_chain_to_handler(code: &str, from: usize) -> Option<usize> {
let bytes = code.as_bytes();
let mut i = from;
loop {
i = skip_ws_and_comments(bytes, i)?;
if bytes[i] != b'(' {
return None;
}
i = skip_balanced_parens(bytes, i)?;
i = skip_ws_and_comments(bytes, i)?;
if bytes[i] != b'.' {
return None;
}
i = skip_ws_and_comments(bytes, i + 1)?;
let name_start = i;
while i < bytes.len()
&& (bytes[i] == b'$' || bytes[i] == b'_' || bytes[i].is_ascii_alphanumeric())
{
i += 1;
}
if i == name_start {
return None;
}
let method = &code[name_start..i];
i = skip_ws_and_comments(bytes, i)?;
if bytes[i] != b'(' {
return None;
}
if method == "handler" {
return Some(i + 1);
}
}
}
fn skip_ws_and_comments(bytes: &[u8], mut i: usize) -> Option<usize> {
loop {
while i < bytes.len() && bytes[i].is_ascii_whitespace() {
i += 1;
}
if i + 1 < bytes.len() && bytes[i] == b'/' && bytes[i + 1] == b'/' {
while i < bytes.len() && bytes[i] != b'\n' {
i += 1;
}
continue;
}
if i + 1 < bytes.len() && bytes[i] == b'/' && bytes[i + 1] == b'*' {
i += 2;
while i + 1 < bytes.len() && !(bytes[i] == b'*' && bytes[i + 1] == b'/') {
i += 1;
}
if i + 1 >= bytes.len() {
return None;
}
i += 2;
continue;
}
return if i < bytes.len() { Some(i) } else { None };
}
}
fn skip_balanced_parens(bytes: &[u8], open: usize) -> Option<usize> {
debug_assert_eq!(bytes[open], b'(');
let mut stack: Vec<u8> = vec![b'('];
let mut prev: Option<u8> = Some(b'(');
let mut i = open + 1;
while i < bytes.len() {
if stack.last() == Some(&b'`') {
match bytes[i] {
b'\\' => i += 2,
b'`' => {
stack.pop();
i += 1;
}
b'$' if bytes.get(i + 1) == Some(&b'{') => {
stack.push(b'T');
prev = Some(b'{');
i += 2;
}
_ => i += 1,
}
continue;
}
let c = bytes[i];
match c {
b'\'' | b'"' => {
i = skip_string(bytes, i)?;
prev = Some(b'"');
}
b'`' => {
stack.push(b'`');
i += 1;
}
b'/' if bytes.get(i + 1) == Some(&b'/') => {
while i < bytes.len() && bytes[i] != b'\n' {
i += 1;
}
}
b'/' if bytes.get(i + 1) == Some(&b'*') => {
i += 2;
while i + 1 < bytes.len() && !(bytes[i] == b'*' && bytes[i + 1] == b'/') {
i += 1;
}
if i + 1 >= bytes.len() {
return None;
}
i += 2;
}
b'/' if regex_can_follow(prev) => {
i = skip_regex_literal(bytes, i)?;
prev = Some(b'/');
}
b'(' | b'[' | b'{' => {
stack.push(c);
prev = Some(c);
i += 1;
}
b')' | b']' => {
let want = if c == b')' { b'(' } else { b'[' };
if stack.pop() != Some(want) {
return None;
}
if stack.is_empty() {
return Some(i + 1);
}
prev = Some(c);
i += 1;
}
b'}' => {
match stack.pop() {
Some(b'{') | Some(b'T') => {}
_ => return None,
}
prev = Some(c);
i += 1;
}
_ => {
if !c.is_ascii_whitespace() {
prev = Some(c);
}
i += 1;
}
}
}
None
}
fn regex_can_follow(prev: Option<u8>) -> bool {
match prev {
None => true,
Some(c) => matches!(
c,
b'(' | b'['
| b'{'
| b','
| b';'
| b':'
| b'!'
| b'&'
| b'|'
| b'?'
| b'='
| b'+'
| b'-'
| b'*'
| b'%'
| b'<'
| b'>'
| b'~'
| b'^'
),
}
}
fn skip_string(bytes: &[u8], start: usize) -> Option<usize> {
let quote = bytes[start];
let mut i = start + 1;
while i < bytes.len() {
match bytes[i] {
b'\\' => i += 2,
c if c == quote => return Some(i + 1),
b'\n' => return Some(i + 1), _ => i += 1,
}
}
None
}
fn skip_regex_literal(bytes: &[u8], start: usize) -> Option<usize> {
let mut i = start + 1;
let mut in_class = false;
while i < bytes.len() {
match bytes[i] {
b'\\' => i += 2,
b'[' => {
in_class = true;
i += 1;
}
b']' if in_class => {
in_class = false;
i += 1;
}
b'/' if !in_class => return Some(i + 1),
b'\n' => return None, _ => i += 1,
}
}
None
}
fn ext_of(path: &str) -> &str {
let name = path.rsplit('/').next().unwrap_or(path);
match name.rfind('.') {
Some(i) if i > 0 => &name[i + 1..],
_ => "",
}
}
fn src_lang(path: &str) -> Option<&'static str> {
match ext_of(path) {
"ts" | "mts" | "cts" => Some("ts"),
"tsx" => Some("tsx"),
"jsx" => Some("jsx"),
"js" | "mjs" => Some("js"),
_ => None,
}
}
#[derive(Default)]
pub(crate) struct CjsCache {
pkg_is_module: Mutex<std::collections::HashMap<PathBuf, bool>>,
verdicts: Mutex<std::collections::HashMap<PathBuf, (std::time::SystemTime, bool)>>,
}
impl CjsCache {
fn nearest_pkg_is_module(&self, start: &Path) -> bool {
if let Some(hit) = self.pkg_is_module.lock().unwrap().get(start) {
return *hit;
}
let mut dir = Some(start);
let mut is_module = false;
for _ in 0..40 {
let Some(d) = dir else { break };
let pj = d.join("package.json");
if pj.is_file() {
is_module = std::fs::read_to_string(&pj)
.ok()
.and_then(|s| serde_json::from_str::<serde_json::Value>(&s).ok())
.and_then(|v| {
v.get("type")
.and_then(|t| t.as_str())
.map(|t| t == "module")
})
.unwrap_or(false);
break;
}
dir = d.parent();
}
self.pkg_is_module
.lock()
.unwrap()
.insert(start.to_path_buf(), is_module);
is_module
}
fn is_cjs_file(&self, path: &str) -> bool {
if path.ends_with(".cjs") {
return true;
}
if !path.ends_with(".js") {
return false;
}
let file = Path::new(path);
if let Some(dir) = file.parent() {
if self.nearest_pkg_is_module(dir) {
return false;
}
}
let mtime = std::fs::metadata(file).ok().and_then(|m| m.modified().ok());
if let Some(mtime) = mtime {
if let Some((seen, verdict)) = self.verdicts.lock().unwrap().get(file) {
if *seen == mtime {
return *verdict;
}
}
}
let verdict = match std::fs::read_to_string(file) {
Ok(src) => !has_esm_syntax(&src),
Err(_) => false,
};
if let Some(mtime) = mtime {
self.verdicts
.lock()
.unwrap()
.insert(file.to_path_buf(), (mtime, verdict));
}
verdict
}
}
fn has_esm_syntax(src: &str) -> bool {
static RE: std::sync::OnceLock<regex::Regex> = std::sync::OnceLock::new();
let re = RE.get_or_init(|| {
regex::Regex::new(r"(?:^|[\n;])\s*export\s|(?:^|[\n;])\s*import\s[^(]")
.expect("esm syntax regex")
});
re.is_match(src)
}
fn mime_of(path: &str) -> &'static str {
match ext_of(path).to_ascii_lowercase().as_str() {
"png" => "image/png",
"jpg" | "jpeg" => "image/jpeg",
"gif" => "image/gif",
"webp" => "image/webp",
"avif" => "image/avif",
"ico" => "image/x-icon",
"svg" => "image/svg+xml",
"woff" => "font/woff",
"woff2" => "font/woff2",
"ttf" => "font/ttf",
"otf" => "font/otf",
"eot" => "application/vnd.ms-fontobject",
"mp4" => "video/mp4",
"webm" => "video/webm",
"ogg" => "audio/ogg",
"mp3" => "audio/mpeg",
"wav" => "audio/wav",
"flac" => "audio/flac",
"m4a" => "audio/mp4",
"aac" => "audio/aac",
"mov" => "video/quicktime",
"bmp" => "image/bmp",
"pdf" => "application/pdf",
"webmanifest" => "application/manifest+json",
"wasm" => "application/wasm",
"css" => "text/css",
"json" => "application/json",
"txt" => "text/plain",
_ => "application/octet-stream",
}
}
fn json_str(s: &str) -> String {
serde_json::Value::String(s.to_string()).to_string()
}
fn classify_id(abs: &str, intent: Option<&'static str>, svg_react: bool) -> String {
if svg_react {
return format!("{abs}?react");
}
if let Some(kind) = intent {
return format!("{abs}?ojasset={kind}");
}
let ext = ext_of(abs).to_ascii_lowercase();
if oj_server::is_style_ext(&ext) {
return format!("{abs}?ojasset=css");
}
if oj_server::is_importable_asset_ext(&ext) || ext == "wasm" {
return format!("{abs}?ojasset=url");
}
abs.to_string()
}
fn split_intent(spec: &str) -> (String, Option<&'static str>, bool) {
let Some((clean, query)) = crate::build::split_asset_query(spec) else {
return (spec.to_string(), None, false);
};
let tag: &'static str = match query.as_str() {
"react" if clean.ends_with(".svg") => return (clean, None, true),
"raw" => "raw",
"url" | "no-inline" => "url",
"inline" => "inline",
"worker" | "worker&inline" => "worker",
"worker&url" | "worker&inline&url" => "worker-url",
"sharedworker" | "sharedworker&inline" => "sharedworker",
"sharedworker&url" | "sharedworker&inline&url" => "sharedworker-url",
_ => return (spec.to_string(), None, false),
};
(clean, Some(tag), false)
}
fn framework_aliases(root: &Path, cache: &Path) -> Vec<(String, PathBuf)> {
vec![
("#tanstack-router-entry".into(), root.join("src/router")),
("#tanstack-start-entry".into(), cache.join("start-entry.ts")),
(
"#tanstack-start-plugin-adapters".into(),
cache.join("plugin-adapters.ts"),
),
(
"#tanstack-start-server-fn-resolver".into(),
cache.join("server-fn-resolver.mjs"),
),
(
"tanstack-start-manifest:v".into(),
cache.join("manifest-dev.ts"),
),
(
"tanstack-start-injected-head-scripts:v".into(),
cache.join("injected-head-scripts.ts"),
),
(
"@cloudflare/vite-plugin/server".into(),
cache.join("cf-server.mjs"),
),
("cloudflare:workers".into(), cache.join("cf-workers.mjs")),
(
"@tanstack/react-start/server-entry".into(),
cache.join("server-entry.tsx"),
),
]
}
pub struct StartHost {
bridge: SsrBridge,
root: PathBuf,
public_dir: Option<PathBuf>,
aliases: Vec<(String, PathBuf)>,
no_external: NoExternal,
graph: VersionGraph,
plugins_started: tokio::sync::OnceCell<()>,
warned_schemes: Mutex<HashSet<String>>,
cjs: CjsCache,
}
impl StartHost {
pub fn new(
root: PathBuf,
cache: &Path,
bridge: SsrBridge,
no_external: oj_config::SsrExternals,
public_dir: Option<PathBuf>,
) -> Arc<StartHost> {
let aliases = framework_aliases(&root, cache);
Arc::new(StartHost {
bridge,
root,
public_dir,
aliases,
no_external: NoExternal::new(no_external),
graph: VersionGraph::default(),
plugins_started: tokio::sync::OnceCell::new(),
warned_schemes: Mutex::new(HashSet::new()),
cjs: CjsCache::default(),
})
}
pub(crate) fn graph(&self) -> &VersionGraph {
&self.graph
}
async fn ensure_plugins_started(&self) {
self.plugins_started
.get_or_init(|| async {
let Some(host) = self.bridge.plugin_host().await else {
return;
};
if let Err(e) = host.build_start().await {
let name = e
.split_once("[plugin:")
.and_then(|(_, rest)| rest.split_once(']'))
.map(|(name, _)| name.to_string())
.unwrap_or_else(|| "unknown".into());
let msg = e.lines().next().unwrap_or(&e).to_string();
eprintln!("oj: plugin \"{name}\" buildStart failed (skipped): {msg}");
}
})
.await;
}
fn alias_target(&self, spec: &str) -> Option<&Path> {
self.aliases
.iter()
.find(|(find, _)| find == spec)
.map(|(_, target)| target.as_path())
}
fn stub_specifier(&self, spec: &str) -> String {
let scheme = spec.split(':').next().unwrap_or("").to_string();
if self.warned_schemes.lock().unwrap().insert(scheme.clone()) {
eprintln!(
"oj: no SSR module for the '{scheme}:' scheme (e.g. {spec}); serving an empty module"
);
}
self.graph.specifier_for(&format!("{STUB_PREFIX}{spec}"))
}
fn module_url(&self, id: &str, importer: String) -> HostResolved {
HostResolved::Url(self.graph.edge_specifier(id, importer))
}
async fn resolve_inner(
&self,
importer: &str,
spec: &str,
) -> Result<Option<HostResolved>, String> {
let importer_id = importer_id(importer);
if let Some(target) = self.alias_target(spec) {
let bridge_first = spec.starts_with('#');
if !bridge_first {
if let Some(hit) = probe(target) {
return Ok(Some(self.module_url(&hit.to_string_lossy(), importer_id)));
}
} else {
if let Ok(StartResolution::Module(id)) =
self.bridge.resolve_start(&importer_id, spec).await
{
return Ok(Some(self.module_url(&id, importer_id)));
}
if let Some(hit) = probe(target) {
return Ok(Some(self.module_url(&hit.to_string_lossy(), importer_id)));
}
}
}
let is_plugin_shaped = spec.starts_with("virtual:") || spec.starts_with('\0');
if importer_id.contains("/node_modules/") && !is_plugin_shaped {
let bare = !spec.starts_with('.')
&& !spec.starts_with('/')
&& !spec.starts_with('#')
&& !oj_server::is_node_builtin(spec);
if bare {
if let Ok(StartResolution::Dependency(p)) =
self.bridge.resolve_start(&importer_id, spec).await
{
if let Ok(u) = url::Url::from_file_path(&p) {
return Ok(Some(HostResolved::External(u.to_string())));
}
}
}
if spec.starts_with("./") || spec.starts_with("../") {
if let Some(dir) = Path::new(&importer_id).parent() {
if !normalize_path(&dir.join(spec)).is_file() {
if let Ok(StartResolution::Dependency(p)) =
self.bridge.resolve_start(&importer_id, spec).await
{
if let Ok(u) = url::Url::from_file_path(&p) {
return Ok(Some(HostResolved::External(u.to_string())));
}
}
}
}
}
return Ok(Some(HostResolved::External(spec.to_string())));
}
let (clean, intent, svg_react) = split_intent(spec);
if clean.starts_with('/') {
let rel = clean.trim_start_matches('/');
let public_hit = || {
self.public_dir
.as_ref()
.and_then(|public| probe(&public.join(rel)))
};
if let Some(hit) = probe(&self.root.join(rel)).or_else(public_hit) {
let id = classify_id(&hit.to_string_lossy(), intent, svg_react);
return Ok(Some(self.module_url(&id, importer_id)));
}
}
match self.bridge.resolve_start(&importer_id, &clean).await {
Ok(StartResolution::Module(id)) => {
if Path::new(&id).is_absolute() {
let tagged = classify_id(&id, intent, svg_react);
if tagged == id && self.cjs.is_cjs_file(&id) {
if let Ok(u) = url::Url::from_file_path(&id) {
return Ok(Some(HostResolved::External(u.to_string())));
}
}
return Ok(Some(self.module_url(&tagged, importer_id)));
}
Ok(Some(self.module_url(&id, importer_id)))
}
Ok(StartResolution::Dependency(p)) => {
let abs = p.to_string_lossy().into_owned();
let ext = ext_of(&abs).to_ascii_lowercase();
if svg_react
|| intent.is_some()
|| oj_server::is_style_ext(&ext)
|| oj_server::is_importable_asset_ext(&ext)
{
let id = classify_id(&abs, intent, svg_react);
return Ok(Some(self.module_url(&id, importer_id)));
}
match url::Url::from_file_path(&p) {
Ok(u) => Ok(Some(HostResolved::External(u.to_string()))),
Err(_) => Ok(Some(HostResolved::External(clean))),
}
}
Ok(StartResolution::Bare(spec)) => {
if let Ok(u) = url::Url::parse(&spec) {
if u.scheme().len() > 1 && u.scheme() != "node" {
return Ok(Some(HostResolved::Url(self.stub_specifier(&spec))));
}
}
Ok(Some(HostResolved::External(spec)))
}
Err(e) => Err(format!("{e} (imported from {importer_id})")),
}
}
async fn load_inner(&self, specifier: &str) -> Result<Option<HostModule>, String> {
let Some(id) = versioned_id(specifier) else {
return self.load_unversioned(specifier).await;
};
self.ensure_plugins_started().await;
let module = self.load_id(&id).await?;
self.graph.record_loaded(&id);
Ok(Some(module))
}
async fn load_unversioned(&self, specifier: &str) -> Result<Option<HostModule>, String> {
let Ok(u) = url::Url::parse(specifier) else {
return Ok(None);
};
if u.scheme() != "file" {
return Ok(None);
}
let Ok(path) = u.to_file_path() else {
return Ok(None);
};
let path_str = path.to_string_lossy().into_owned();
if !path_str.contains("/node_modules/") {
return Ok(None);
}
if path_str.ends_with(".json") {
let code = self
.bridge
.load_module(&path_str)
.await
.map_err(|e| e.to_string())?;
return Ok(Some(HostModule {
code,
module_type: HostModuleType::JavaScript,
}));
}
if !self.no_external.matches_path(&path_str) {
return Ok(None);
}
if src_lang(&path_str).is_none() || self.cjs.is_cjs_file(&path_str) {
return Ok(None);
}
self.ensure_plugins_started().await;
let source = std::fs::read_to_string(&path).map_err(|e| format!("read {path_str}: {e}"))?;
let code = self
.bridge
.transform_module(&path_str, source, false, true)
.await
.map_err(|e| e.to_string())?;
Ok(Some(HostModule {
code,
module_type: HostModuleType::JavaScript,
}))
}
async fn load_id(&self, id: &str) -> Result<HostModule, String> {
let js = |code: String| HostModule {
code,
module_type: HostModuleType::JavaScript,
};
if let Some(spec) = id.strip_prefix(STUB_PREFIX) {
let _ = spec;
return Ok(js("export default {};".into()));
}
let (clean, query) = match id.split_once('?') {
Some((c, q)) => (c.to_string(), Some(q.to_string())),
None => (id.to_string(), None),
};
let is_path = Path::new(&clean).is_absolute();
if is_path && clean.ends_with(".svg") {
let plugin_id = match &query {
Some(q) => format!("{clean}?{q}"),
None => clean.clone(),
};
let loaded = match self.bridge.plugin_host().await {
Some(host) if host.hook_wants_load(&plugin_id) => {
host.load(&plugin_id).await.ok().flatten()
}
_ => None,
};
let code = loaded.unwrap_or_else(|| {
format!(
"export default {};",
json_str(&format!("/@oj-start/fs{clean}"))
)
});
return Ok(js(code));
}
if let Some(kind) = query.as_deref().and_then(|q| q.strip_prefix("ojasset=")) {
return Ok(js(self.asset_module(&clean, kind).await?));
}
if !is_path {
return self
.bridge
.load_module(id)
.await
.map(js)
.map_err(|e| e.to_string());
}
if clean.ends_with(".json") {
return self
.bridge
.load_module(&clean)
.await
.map(js)
.map_err(|e| e.to_string());
}
if clean.ends_with(".mdx") {
let raw = std::fs::read_to_string(&clean).map_err(|e| format!("read {clean}: {e}"))?;
let compiled = match self.bridge.plugin_host().await {
Some(host) => host
.transform(&raw, &clean, "{}")
.await
.map(|(code, _, _, _)| code)
.map_err(|e| e.to_string())?,
None => return Err(format!("no plugin compiles {clean}")),
};
let code = self
.bridge
.transform_module(&clean, compiled, true, false)
.await
.map_err(|e| e.to_string())?;
return Ok(js(code));
}
let in_deps = clean.contains("/node_modules/");
let plugin_loaded = match self.bridge.plugin_host().await {
Some(host) if host.hook_wants_load(&clean) => host.load(&clean).await.ok().flatten(),
Some(_) => {
if oj_server::plugins::hook_gate_debug() {
eprintln!("oj: hook gate skipped start load for {clean}");
}
None
}
None => None,
};
let from_plugin = plugin_loaded.is_some();
let raw = match plugin_loaded {
Some(code) => code,
None => match std::fs::read_to_string(&clean) {
Ok(s) => s,
Err(_) => {
return self
.bridge
.load_module(&clean)
.await
.map(js)
.map_err(|e| e.to_string());
}
},
};
let raw = if !in_deps {
let rel = clean
.strip_prefix(&format!("{}/", self.root.to_string_lossy()))
.unwrap_or(&clean)
.to_string();
rewrite_server_fns(&raw, &rel)
} else {
raw
};
let code = self
.bridge
.transform_module(&clean, raw, from_plugin, true)
.await
.map_err(|e| e.to_string())?;
Ok(js(code))
}
fn dev_url(&self, path: &str) -> String {
match Path::new(path).strip_prefix(&self.root) {
Ok(rel) => format!("/{}", rel.to_string_lossy()),
Err(_) => format!("/@fs{path}"),
}
}
async fn asset_module(&self, path: &str, kind: &str) -> Result<String, String> {
match kind {
"raw" => {
let text =
std::fs::read_to_string(path).map_err(|e| format!("read {path}: {e}"))?;
Ok(format!("export default {};", json_str(&text)))
}
"inline" => {
let ext = ext_of(path).to_ascii_lowercase();
if oj_server::is_style_ext(&ext) {
let text =
std::fs::read_to_string(path).map_err(|e| format!("read {path}: {e}"))?;
Ok(format!("export default {};", json_str(&text)))
} else {
let bytes = std::fs::read(path).map_err(|e| format!("read {path}: {e}"))?;
let url = format!(
"data:{};base64,{}",
mime_of(path),
crate::ssr_host::base64(&bytes)
);
Ok(format!("export default {};", json_str(&url)))
}
}
"css" => {
self.bridge
.load_module(path)
.await
.map_err(|e| e.to_string())
}
"worker-url" | "sharedworker-url" => {
Ok(format!("export default {};", json_str(&self.dev_url(path))))
}
"worker" | "sharedworker" => {
let ctor = if kind == "sharedworker" {
"SharedWorker"
} else {
"Worker"
};
Ok(format!(
"export default function WorkerWrapper(options) {{ return new {ctor}({}, {{ type: \"module\", name: options?.name }}); }}",
json_str(&self.dev_url(path))
))
}
_ => Ok(format!(
"export default {};",
json_str(&format!("/@oj-start/fs{path}"))
)),
}
}
}
fn serve_module_host(host: Arc<StartHost>, mut requests: oj_js::HostRequests) {
tokio::spawn(async move {
while let Some(request) = requests.recv().await {
let host = Arc::clone(&host);
tokio::spawn(async move {
match request {
oj_js::HostRequest::Resolve {
importer,
specifier,
reply,
} => {
let _ = reply.send(host.resolve_inner(&importer, &specifier).await);
}
oj_js::HostRequest::Load { specifier, reply } => {
let _ = reply.send(host.load_inner(&specifier).await);
}
}
});
}
});
}
#[derive(Debug)]
pub struct StartRequest {
pub method: String,
pub url: String,
pub host: String,
pub headers: Vec<(String, String)>,
pub body: Option<Vec<u8>>,
}
#[derive(Debug)]
pub struct StartResponse {
pub status: u16,
pub headers: Vec<(String, String)>,
pub set_cookies: Vec<String>,
pub body: Vec<u8>,
}
pub struct StartEngine {
host: Arc<StartHost>,
root: PathBuf,
entry_id: String,
bootstrap: String,
init_env: Vec<(String, String)>,
engine: tokio::sync::RwLock<EngineSlot>,
}
struct EngineSlot {
engine: JsEngine,
inited: tokio::sync::OnceCell<()>,
}
impl EngineSlot {
fn new(engine: JsEngine) -> EngineSlot {
EngineSlot {
engine,
inited: tokio::sync::OnceCell::new(),
}
}
}
impl StartEngine {
pub fn new(
root: PathBuf,
cache: &Path,
bridge: SsrBridge,
entry_abs: PathBuf,
no_external: oj_config::SsrExternals,
public_dir: Option<PathBuf>,
init_env: Vec<(String, String)>,
) -> anyhow::Result<StartEngine> {
let bootstrap = cache.join("start-bootstrap.mjs");
std::fs::write(&bootstrap, START_BOOTSTRAP_JS)?;
let host = StartHost::new(root.clone(), cache, bridge, no_external, public_dir);
let engine = spawn_engine(&root, &host)?;
Ok(StartEngine {
host,
root,
entry_id: entry_abs.to_string_lossy().into_owned(),
bootstrap: bootstrap.to_string_lossy().into_owned(),
init_env,
engine: tokio::sync::RwLock::new(EngineSlot::new(engine)),
})
}
async fn init(&self, engine: &JsEngine) -> Result<(), String> {
let env: serde_json::Map<String, serde_json::Value> = self
.init_env
.iter()
.map(|(k, v)| (k.clone(), serde_json::Value::String(v.clone())))
.collect();
engine
.call(
self.bootstrap.clone(),
"init",
vec![serde_json::Value::Object(env)],
None,
)
.await
.map(|_| ())
.map_err(|e| e.to_string())
}
pub async fn handle(&self, req: StartRequest) -> Result<StartResponse, String> {
let slot = self.engine.read().await;
slot.inited
.get_or_try_init(|| self.init(&slot.engine))
.await?;
let engine = &slot.engine;
let entry = self.host.graph().specifier_for(&self.entry_id);
let payload = serde_json::json!({
"method": req.method,
"url": req.url,
"host": req.host,
"headers": req.headers,
"bodyBase64": req.body.map(|b| crate::ssr_host::base64(&b)),
});
let value = engine
.call(
self.bootstrap.clone(),
"handle",
vec![entry.into(), payload],
None,
)
.await
.map_err(|e| e.to_string())?;
let status = value.get("status").and_then(|s| s.as_u64()).unwrap_or(500) as u16;
let pairs = |key: &str| -> Vec<(String, String)> {
value
.get(key)
.and_then(|h| h.as_array())
.map(|a| {
a.iter()
.filter_map(|p| {
let k = p.get(0)?.as_str()?.to_string();
let v = p.get(1)?.as_str()?.to_string();
Some((k, v))
})
.collect()
})
.unwrap_or_default()
};
let set_cookies = value
.get("setCookies")
.and_then(|c| c.as_array())
.map(|a| {
a.iter()
.filter_map(|v| v.as_str().map(str::to_string))
.collect()
})
.unwrap_or_default();
let body = value
.get("bodyBase64")
.and_then(|b| b.as_str())
.map(base64_decode)
.unwrap_or_default();
Ok(StartResponse {
status,
headers: pairs("headers"),
set_cookies,
body,
})
}
pub async fn reload(&self) {
self.host.graph().invalidate();
self.host.graph().bump(&self.entry_id);
self.maybe_respawn().await;
}
pub async fn revalidate(&self) -> bool {
let dropped = self.host.graph().invalidate();
if dropped > 0 {
self.host.graph().bump(&self.entry_id);
}
self.maybe_respawn().await;
dropped > 0
}
async fn maybe_respawn(&self) {
if !self.host.graph().should_respawn() {
return;
}
let mut engine = self.engine.write().await;
if !self.host.graph().should_respawn() {
return; }
match spawn_engine(&self.root, &self.host) {
Ok(fresh) => {
*engine = EngineSlot::new(fresh);
self.host.graph().reset_after_respawn();
eprintln!("oj start: engine respawned to reclaim stale module instances");
}
Err(e) => eprintln!("oj start: engine respawn failed: {e}"),
}
}
}
pub struct ScriptEngine {
engine: JsEngine,
env_shadowed: tokio::sync::OnceCell<()>,
}
const SCRIPT_ENV_SHADOW_JS: &str = r#"
Object.defineProperty(process, "env", {
value: { ...process.env },
configurable: true,
writable: true,
});
"#;
impl ScriptEngine {
pub fn new(root: &Path) -> anyhow::Result<ScriptEngine> {
let mut config = EngineConfig::new(root);
config.code_cache_dir = Some(oj_server::engine_code_cache_dir(root));
Ok(ScriptEngine {
engine: JsEngine::spawn(config, None, None).map_err(|e| anyhow::anyhow!("{e}"))?,
env_shadowed: tokio::sync::OnceCell::new(),
})
}
pub async fn run_async(
&self,
script: &Path,
env: &[(String, String)],
what: &str,
) -> anyhow::Result<()> {
self.call(script, env)
.await
.map(|_| ())
.map_err(|e| anyhow::anyhow!("{what} failed: {e}"))
}
async fn call(
&self,
script: &Path,
env: &[(String, String)],
) -> Result<serde_json::Value, oj_js::EngineError> {
self.env_shadowed
.get_or_try_init(|| async {
self.engine
.eval(
oj_js::EvalInput::Source(SCRIPT_ENV_SHADOW_JS.to_string()),
None,
)
.await
.map(|_| ())
})
.await?;
let env_obj: serde_json::Map<String, serde_json::Value> = env
.iter()
.map(|(k, v)| (k.clone(), serde_json::Value::String(v.clone())))
.collect();
self.engine
.call(
script.to_string_lossy().into_owned(),
"run",
vec![serde_json::Value::Object(env_obj)],
None,
)
.await
}
}
fn spawn_engine(root: &Path, host: &Arc<StartHost>) -> Result<JsEngine, oj_js::EngineError> {
let mut config = EngineConfig::new(root);
config.code_cache_dir = Some(oj_server::engine_code_cache_dir(root));
config.registry = Some(host.bridge.engine_registry());
let (link, requests) = ModuleHost::channel();
serve_module_host(Arc::clone(host), requests);
JsEngine::spawn(config, Some(link), None)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn server_fn_rewrite_matches_the_node_loader() {
let code = "import { createServerFn } from \"@tanstack/react-start\";\nexport const getGreeting = createServerFn({ method: \"GET\" }).handler(async () => \"hi\");\n";
let out = rewrite_server_fns(code, "src/server/data.ts");
assert!(out
.starts_with("import { createServerRpc } from \"@tanstack/react-start/server-rpc\"; "));
let id = base64url(b"src/server/data.ts#getGreeting");
assert!(
out.contains(&format!(
"export const getGreeting_createServerFn_handler = createServerRpc({{ id: \"{id}\", name: \"getGreeting\", filename: \"src/server/data.ts\" }}, (opts) => getGreeting.__executeServer(opts)); "
)),
"{out}"
);
assert!(out.contains(".handler(getGreeting_createServerFn_handler, async () => \"hi\")"));
assert_eq!(code.lines().count(), out.lines().count());
let plain = "export const x = 1;\n";
assert_eq!(rewrite_server_fns(plain, "src/x.ts"), plain);
}
#[test]
fn split_statement_factories_are_left_untouched() {
let split = "import { createServerFn } from \"@tanstack/react-start\";\nconst factory = createServerFn({ method: \"POST\" });\nexport const doIt = factory.handler(async () => 1);\n";
assert_eq!(rewrite_server_fns(split, "src/split.ts"), split);
let inline = "const factory = createServerFn({ method: \"POST\" }); export const doIt = factory.handler(async () => 1);\n";
assert_eq!(rewrite_server_fns(inline, "src/split.ts"), inline);
let mixed = "const factory = createServerFn({ method: \"POST\" });\nexport const real = createServerFn({ method: \"GET\" }).handler(async () => 2);\nexport const viaFactory = factory.handler(async () => 3);\n";
let out = rewrite_server_fns(mixed, "src/mixed.ts");
assert!(out.contains("export const real_createServerFn_handler = createServerRpc("));
assert!(out.contains(".handler(real_createServerFn_handler, async () => 2)"));
assert!(!out.contains("factory_createServerFn_handler"), "{out}");
assert!(
out.contains(".handler(async () => 3)"),
"the factory's handler call keeps its own arguments: {out}"
);
}
#[test]
fn adjacent_server_fns_rewrite_independently() {
let code = "export const a = createServerFn({ method: \"GET\" }).handler(async () => 1);\nexport const b = createServerFn({ method: \"POST\" }).handler(async () => 2);\n";
let out = rewrite_server_fns(code, "src/two.ts");
for name in ["a", "b"] {
let id = base64url(format!("src/two.ts#{name}").as_bytes());
assert!(
out.contains(&format!(
"export const {name}_createServerFn_handler = createServerRpc({{ id: \"{id}\""
)),
"{out}"
);
}
assert!(out.contains(".handler(a_createServerFn_handler, async () => 1)"));
assert!(out.contains(".handler(b_createServerFn_handler, async () => 2)"));
assert_eq!(code.lines().count(), out.lines().count());
}
#[test]
fn multiline_chains_keep_rewriting() {
let code = r#"export const getUser = createServerFn({
method: "GET",
})
.middleware([authMiddleware]) // trailing comment
.validator((s) => {
if (!/^[(]/.test(s.name) && s.label !== ")((") return s;
throw new Error(`bad ${s.name}; (unbalanced`);
})
.handler(async ({ data }) => data);
"#;
let out = rewrite_server_fns(code, "src/user.ts");
assert!(
out.contains("export const getUser_createServerFn_handler = createServerRpc("),
"{out}"
);
assert!(
out.contains(".handler(getUser_createServerFn_handler, async ({ data }) => data)"),
"{out}"
);
assert_eq!(code.lines().count(), out.lines().count());
}
#[test]
fn asi_split_factories_are_left_untouched() {
let code = "const factory = createServerFn({ method: \"POST\" })\nexport const doIt = factory.handler(async () => 1)\n";
assert_eq!(rewrite_server_fns(code, "src/asi.ts"), code);
}
#[test]
fn base64url_matches_node_buffer() {
assert_eq!(base64url(b"src/a.ts#fn"), "c3JjL2EudHMjZm4");
assert_eq!(base64url(&[0xff, 0xef, 0x01]), "_-8B");
}
#[test]
fn intent_queries_split_like_the_loader() {
assert_eq!(
split_intent("./logo.svg?react"),
("./logo.svg".into(), None, true)
);
assert_eq!(
split_intent("../notes.txt?raw"),
("../notes.txt".into(), Some("raw"), false)
);
assert_eq!(
split_intent("./hero.png?url"),
("./hero.png".into(), Some("url"), false)
);
assert_eq!(
split_intent("./x.css?inline"),
("./x.css".into(), Some("inline"), false)
);
assert_eq!(split_intent("./x.tsx"), ("./x.tsx".into(), None, false));
assert_eq!(
split_intent("./x.ts?react"),
("./x.ts?react".into(), None, false)
);
}
#[test]
fn worker_queries_split_like_vite() {
for (spec, tag) in [
("./w.ts?worker&url", "worker-url"),
("./w.ts?url&worker", "worker-url"),
("./w.ts?worker", "worker"),
("./w.ts?worker&inline", "worker"),
("./w.ts?sharedworker", "sharedworker"),
("./w.ts?sharedworker&url", "sharedworker-url"),
] {
assert_eq!(
split_intent(spec),
("./w.ts".into(), Some(tag), false),
"{spec}"
);
}
assert_eq!(
split_intent("./hero.png?no-inline"),
("./hero.png".into(), Some("url"), false)
);
assert_eq!(
split_intent("./hero.png?url&no-inline"),
("./hero.png".into(), Some("url"), false)
);
assert_eq!(
split_intent("./hero.png?inline&no-inline"),
("./hero.png".into(), Some("url"), false)
);
assert_eq!(
split_intent("./hero.png?url&inline"),
("./hero.png".into(), Some("inline"), false)
);
assert_eq!(
split_intent("./w.ts?worker&inline&url"),
("./w.ts".into(), Some("worker-url"), false)
);
assert_eq!(
split_intent("./r.tsx?tsr-split=component"),
("./r.tsx?tsr-split=component".into(), None, false)
);
}
#[test]
fn classification_tags_assets_styles_and_sources() {
assert_eq!(classify_id("/a/b.css", None, false), "/a/b.css?ojasset=css");
assert_eq!(
classify_id("/a/b.module.scss", None, false),
"/a/b.module.scss?ojasset=css"
);
assert_eq!(
classify_id("/a/hero.png", None, false),
"/a/hero.png?ojasset=url"
);
assert_eq!(
classify_id("/a/n.txt", Some("raw"), false),
"/a/n.txt?ojasset=raw"
);
assert_eq!(classify_id("/a/l.svg", None, true), "/a/l.svg?react");
assert_eq!(classify_id("/a/l.svg", None, false), "/a/l.svg");
assert_eq!(classify_id("/a/x.tsx", None, false), "/a/x.tsx");
assert_eq!(
classify_id("/a/m.wasm", None, false),
"/a/m.wasm?ojasset=url"
);
}
#[test]
fn no_external_matches_names_globs_and_regexes() {
let rules = oj_config::SsrExternals {
no_external: vec!["left-pad".into(), "@acme/*".into()],
no_external_regex: vec!["^weird-".into()],
external: vec!["@acme/keep-out".into()],
..Default::default()
};
let m = NoExternal::new(rules);
assert!(m.matches_path("/x/node_modules/left-pad/index.js"));
assert!(m.matches_path("/x/node_modules/@acme/ui/dist/i.js"));
assert!(m.matches_path("/x/node_modules/weird-lib/i.js"));
assert!(!m.matches_path("/x/node_modules/@acme/keep-out/i.js"));
assert!(!m.matches_path("/x/node_modules/react/index.js"));
assert!(!m.matches_path("/x/src/app.tsx"));
}
#[test]
fn cjs_detection_matches_the_loader() {
let dir = std::env::temp_dir().join(format!("oj-cjsdet-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(dir.join("esm")).unwrap();
std::fs::write(dir.join("package.json"), "{}").unwrap();
std::fs::write(dir.join("esm/package.json"), r#"{"type":"module"}"#).unwrap();
let cache = CjsCache::default();
let cjs = dir.join("plain.js");
std::fs::write(&cjs, "module.exports = { a: 1 };\n").unwrap();
let esmish = dir.join("esmish.js");
std::fs::write(&esmish, "export const a = 1;\n").unwrap();
let typed = dir.join("esm/x.js");
std::fs::write(&typed, "module.exports = 1;\n").unwrap();
assert!(cache.is_cjs_file(&cjs.to_string_lossy()));
assert!(!cache.is_cjs_file(&esmish.to_string_lossy()));
assert!(
!cache.is_cjs_file(&typed.to_string_lossy()),
"type:module wins"
);
assert!(cache.is_cjs_file("/whatever/x.cjs"));
assert!(!cache.is_cjs_file("/whatever/x.mjs"));
assert!(cache.is_cjs_file(&cjs.to_string_lossy()));
assert!(!cache.is_cjs_file(&typed.to_string_lossy()));
}
#[test]
fn cjs_verdict_cache_follows_the_file_mtime() {
let dir = std::env::temp_dir().join(format!("oj-cjsmtime-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(&dir).unwrap();
std::fs::write(dir.join("package.json"), "{}").unwrap();
let cache = CjsCache::default();
let file = dir.join("mut.js");
std::fs::write(&file, "module.exports = 1;\n").unwrap();
assert!(cache.is_cjs_file(&file.to_string_lossy()));
std::fs::write(&file, "export const a = 1;\n").unwrap();
let f = std::fs::OpenOptions::new()
.append(true)
.open(&file)
.unwrap();
f.set_modified(std::time::SystemTime::now() + std::time::Duration::from_secs(5))
.unwrap();
assert!(
!cache.is_cjs_file(&file.to_string_lossy()),
"an mtime move re-reads the file"
);
}
#[test]
fn the_start_host_maps_every_framework_virtual_module() {
let root = Path::new("/app");
let cache = Path::new("/app/.oj-cache/v1/start");
let aliases = framework_aliases(root, cache);
for spec in [
"tanstack-start-manifest:v",
"tanstack-start-injected-head-scripts:v",
"#tanstack-router-entry",
"#tanstack-start-entry",
"#tanstack-start-plugin-adapters",
"#tanstack-start-server-fn-resolver",
"@cloudflare/vite-plugin/server",
"cloudflare:workers",
"@tanstack/react-start/server-entry",
] {
assert!(
aliases.iter().any(|(find, _)| find == spec),
"the start host has no alias for {spec}"
);
}
}
#[tokio::test(flavor = "multi_thread")]
async fn script_engine_env_writes_do_not_leak_into_the_oj_process() {
let dir = std::env::temp_dir().join(format!("oj-scriptenv-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(&dir).unwrap();
let script = dir.join("probe.mjs");
std::fs::write(
&script,
r#"export async function run(env = null) {
if (env) for (const [k, v] of Object.entries(env)) process.env[k] = v;
// The shadow must still BEHAVE like the env: the merged value is visible.
if (process.env.OJ_SCRIPT_MERGE_PROBE !== "merged") {
throw new Error("merged env not visible on the shadow");
}
process.env.NODE_ENV = "development";
process.env.OJ_SCRIPT_WRITE_PROBE = "leaked";
if (process.env.OJ_SCRIPT_WRITE_PROBE !== "leaked") {
throw new Error("own write not visible on the shadow");
}
return null;
}
"#,
)
.unwrap();
let scripts = ScriptEngine::new(&dir).unwrap();
let node_env_before = std::env::var("NODE_ENV").ok();
scripts
.run_async(
&script,
&[("OJ_SCRIPT_MERGE_PROBE".to_string(), "merged".to_string())],
"env shadow probe",
)
.await
.expect("the probe script runs");
assert!(
std::env::var("OJ_SCRIPT_MERGE_PROBE").is_err(),
"the run(env) merge must stay on the isolate's shadow"
);
assert!(
std::env::var("OJ_SCRIPT_WRITE_PROBE").is_err(),
"a script's own env write must stay on the isolate's shadow"
);
assert_eq!(
std::env::var("NODE_ENV").ok(),
node_env_before,
"the script's NODE_ENV write must not alter oj's real env"
);
}
#[test]
fn pkg_names_parse_scoped_and_plain() {
assert_eq!(
pkg_name_of_path("/a/node_modules/react/index.js").as_deref(),
Some("react")
);
assert_eq!(
pkg_name_of_path("/a/node_modules/@acme/ui/dist/x.js").as_deref(),
Some("@acme/ui")
);
assert_eq!(pkg_name_of_path("/a/src/x.ts"), None);
}
}