use crate::{farm, project};
use noxid_deployment_ir::{AdapterTarget, DeploymentPlan};
use std::collections::BTreeMap;
use std::env;
use std::fs;
use std::path::Path;
#[derive(Clone, Copy)]
enum ReservedDeploymentPath {
Literal(&'static str),
AppJson,
}
const RESERVED_DEPLOYMENT_PATHS: &[ReservedDeploymentPath] = &[
ReservedDeploymentPath::Literal("server"),
ReservedDeploymentPath::Literal("public"),
ReservedDeploymentPath::Literal("netlify"),
ReservedDeploymentPath::Literal(".vercel"),
ReservedDeploymentPath::Literal("api-contract.json"),
ReservedDeploymentPath::Literal("api.openapi.json"),
ReservedDeploymentPath::Literal("deployment.plan.json"),
ReservedDeploymentPath::Literal("security.manifest.json"),
ReservedDeploymentPath::Literal("server.mjs"),
ReservedDeploymentPath::Literal("server.ts"),
ReservedDeploymentPath::Literal("package.json"),
ReservedDeploymentPath::Literal("deno.json"),
ReservedDeploymentPath::Literal("pnpm-lock.yaml"),
ReservedDeploymentPath::AppJson,
];
pub(crate) fn is_reserved_deployment_path_segment(value: &str) -> bool {
RESERVED_DEPLOYMENT_PATHS
.iter()
.any(|pattern| match pattern {
ReservedDeploymentPath::Literal(name) => value.eq_ignore_ascii_case(name),
ReservedDeploymentPath::AppJson => {
value
.get(.."app.".len())
.is_some_and(|prefix| prefix.eq_ignore_ascii_case("app."))
&& value
.get(value.len().saturating_sub(".json".len())..)
.is_some_and(|suffix| suffix.eq_ignore_ascii_case(".json"))
}
})
}
pub(crate) fn reserved_deployment_path_description() -> String {
RESERVED_DEPLOYMENT_PATHS
.iter()
.map(|pattern| match pattern {
ReservedDeploymentPath::Literal(name) => format!("`/{name}`"),
ReservedDeploymentPath::AppJson => "`/app.*.json`".to_string(),
})
.collect::<Vec<_>>()
.join(", ")
}
fn reserved_deployment_path_javascript() -> String {
let literals = RESERVED_DEPLOYMENT_PATHS
.iter()
.filter_map(|pattern| match pattern {
ReservedDeploymentPath::Literal(name) => {
Some(format!("\"{}\"", noxid_source::json_escape(name)))
}
ReservedDeploymentPath::AppJson => None,
})
.collect::<Vec<_>>()
.join(", ");
assert!(
RESERVED_DEPLOYMENT_PATHS
.iter()
.any(|pattern| matches!(pattern, ReservedDeploymentPath::AppJson)),
"reserved deployment paths must include compiler-owned app metadata"
);
format!(
"const noxidReservedDeploymentNames = Object.freeze([{literals}]);\nconst isNoxidReservedDeploymentPath = (value) => {{\n const first = String(value).normalize(\"NFC\").replace(/^\\/+/, \"\").split(\"/\", 1)[0].toLowerCase();\n return noxidReservedDeploymentNames.includes(first) || (first.startsWith(\"app.\") && first.endsWith(\".json\"));\n}};"
)
}
pub fn adapt_project(
input: &Path,
out_dir: &Path,
title: Option<String>,
requested: Option<&str>,
) -> Result<DeploymentPlan, String> {
let requested = requested
.map(str::to_string)
.unwrap_or(project::deploy_adapter(input)?)
.parse::<AdapterTarget>()?;
let environment = env::vars().collect::<BTreeMap<_, _>>();
let (selected, detected_by) = if requested == AdapterTarget::Auto {
detect_adapter(&environment)?
} else {
(requested, None)
};
let (server_actions, edge_actions, _, endpoints) = project::execution_counts(input)?;
let task_schedules = project::task_schedules(input)?;
validate_adapter(
selected,
server_actions,
edge_actions,
endpoints,
task_schedules.len(),
)?;
let base_path = project::base_path(input)?;
let public_output = if base_path == "/" {
out_dir.to_path_buf()
} else {
out_dir.join(base_path.trim_start_matches('/'))
};
let server_target = match selected {
AdapterTarget::Node | AdapterTarget::Vercel | AdapterTarget::Netlify => "node",
AdapterTarget::Deno | AdapterTarget::Cloudflare => "web",
_ => "node",
};
let build = farm::bundle_project_for_runtime(input, &public_output, title, server_target)?;
project::copy_static_assets_into(input, &public_output)?;
let vercel_max_duration = project::vercel_max_duration(input)?;
let (queue_drain, queue_drain_budget_ms) = project::queue_drain_settings(input)?;
let shutdown_timeout_ms = project::shutdown_timeout_ms(input)?;
let tracing_export = project::server_tracing_export(input)?;
let emission = AdapterEmissionConfig {
vercel_max_duration,
queue_drain,
queue_drain_budget_ms,
shutdown_timeout_ms,
tracing_export,
node_database_driver: node_database_driver(&environment, &build),
};
emit_adapter_files(
selected,
out_dir,
&base_path,
&build,
&task_schedules,
emission,
)?;
let provider_functions = provider_function_count(selected, &build, task_schedules.len());
let plan = DeploymentPlan {
requested,
selected,
detected_by,
base_path,
output_directory: out_dir.to_string_lossy().to_string(),
routes: build.routes,
ssr_routes: build.ssr_routes,
components: build.components,
prerender_routes: build.prerender_routes,
prerender_entries: build.prerender_entries,
isr_routes: build.isr_routes,
swr_routes: build.swr_routes,
provider_functions,
server_actions: build.server_actions,
edge_actions: build.edge_actions,
worker_actions: build.worker_actions,
warnings: crate::db::deployment_warnings(input),
};
write(&out_dir.join("deployment.plan.json"), &plan.to_json())?;
Ok(plan)
}
fn detect_adapter(
environment: &BTreeMap<String, String>,
) -> Result<(AdapterTarget, Option<String>), String> {
let candidates = [
("VERCEL", AdapterTarget::Vercel),
("CF_PAGES", AdapterTarget::Cloudflare),
("NETLIFY", AdapterTarget::Netlify),
("RAILWAY_ENVIRONMENT", AdapterTarget::Node),
("NOXID_NODE", AdapterTarget::Node),
("DENO_DEPLOYMENT_ID", AdapterTarget::Deno),
]
.into_iter()
.filter(|(name, _)| {
environment
.get(*name)
.is_some_and(|value| !value.is_empty())
})
.collect::<Vec<_>>();
match candidates.as_slice() {
[] => Ok((AdapterTarget::Static, None)),
[(variable, target)] => Ok((*target, Some((*variable).into()))),
_ if candidates
.iter()
.all(|(_, target)| *target == candidates[0].1) =>
{
Ok((candidates[0].1, Some(candidates[0].0.into())))
}
_ => Err(format!(
"error[AMBIGUOUS_DEPLOYMENT_TARGET]: multiple deployment environments detected: {}",
candidates
.iter()
.map(|(name, target)| format!("{name}={target}"))
.collect::<Vec<_>>()
.join(", ")
)),
}
}
fn validate_adapter(
target: AdapterTarget,
server_actions: usize,
edge_actions: usize,
endpoints: usize,
scheduled_tasks: usize,
) -> Result<(), String> {
if scheduled_tasks > 0
&& matches!(
target,
AdapterTarget::Static | AdapterTarget::Cloudflare | AdapterTarget::Deno
)
{
return Err(format!(
"error[ADAPTER_TASK_SCHEDULING_UNSUPPORTED]: adapter `{target}` cannot schedule {scheduled_tasks} declared task(s); choose `node`, `vercel`, or `netlify`, or remove the task schedule declarations",
));
}
let unsupported = match target {
AdapterTarget::Auto => unreachable!("auto is resolved before validation"),
AdapterTarget::Static => server_actions + edge_actions + endpoints,
AdapterTarget::Node
| AdapterTarget::Deno
| AdapterTarget::Vercel
| AdapterTarget::Netlify => edge_actions,
AdapterTarget::Cloudflare => server_actions,
};
if unsupported == 0 {
return Ok(());
}
Err(format!(
"error[ADAPTER_EXECUTION_UNSUPPORTED]: adapter `{target}` cannot host this execution graph (server actions: {}, edge actions: {}, endpoints: {}); choose {} or change the unsupported declaration",
server_actions,
edge_actions,
endpoints,
if edge_actions > 0 {
"`cloudflare` for edge actions"
} else if endpoints > 0 {
"`node`, `deno`, `cloudflare`, `vercel`, or `netlify` for typed endpoints"
} else {
"`node` or `deno` for server actions"
},
))
}
fn has_dynamic_rendering(build: &project::ProjectBuild) -> bool {
build.ssr_routes > 0 || build.server_shell_routes > 0
}
fn has_server_handler(build: &project::ProjectBuild) -> bool {
build.server_actions > 0
|| build.endpoints > 0
|| build.tasks > 0
|| build.queues > 0
|| build.live_resources > 0
|| build.presences > 0
|| build.api_docs
|| build.mcp
|| has_dynamic_rendering(build)
}
fn endpoint_forwarding_patterns(
build: &project::ProjectBuild,
base: &str,
) -> Vec<Vec<Option<String>>> {
let base_segments = base
.trim_matches('/')
.split('/')
.filter(|segment| !segment.is_empty())
.map(|segment| Some(segment.to_string()))
.collect::<Vec<_>>();
let mut patterns = build
.endpoint_paths
.iter()
.map(|path| {
base_segments
.iter()
.cloned()
.chain(
path.trim_matches('/')
.split('/')
.filter(|segment| !segment.is_empty())
.map(|segment| {
if segment.starts_with('[') && segment.ends_with(']') {
None
} else {
Some(segment.to_string())
}
}),
)
.collect::<Vec<_>>()
})
.collect::<Vec<_>>();
if build.live_resources > 0 || build.presences > 0 {
patterns.push(
base_segments
.iter()
.cloned()
.chain([Some("_noxid".into()), Some("live".into())])
.collect(),
);
}
if build.presences > 0 {
patterns.push(
base_segments
.iter()
.cloned()
.chain([Some("_noxid".into()), Some("presence".into())])
.collect(),
);
}
if build.api_docs {
patterns.push(vec![Some("_noxid".into()), Some("openapi.json".into())]);
}
if build.mcp {
patterns.push(vec![Some("_noxid".into()), Some("mcp".into())]);
}
patterns.sort();
patterns.dedup();
patterns
}
fn endpoint_matcher_javascript(build: &project::ProjectBuild, base: &str) -> String {
let patterns = endpoint_forwarding_patterns(build, base);
if patterns.is_empty() {
return String::new();
}
let patterns = patterns
.iter()
.map(|segments| {
let segments = segments
.iter()
.map(|segment| {
segment.as_ref().map_or_else(
|| "null".to_string(),
|segment| format!("\"{}\"", noxid_source::json_escape(segment)),
)
})
.collect::<Vec<_>>()
.join(", ");
format!("Object.freeze([{segments}])")
})
.collect::<Vec<_>>()
.join(",\n ");
format!(
r#"// NOXID_DEPLOYMENT_ENDPOINT_PATTERNS: compiler-derived, exact segment shapes only.
const noxidEndpointPatterns = Object.freeze([
{patterns}
]);
function matchesNoxidEndpointPath(pathname) {{
const actual = pathname.split("/");
if (actual[0] === "") actual.shift();
return noxidEndpointPatterns.some((expected) => expected.length === actual.length && expected.every((segment, index) => {{
if (segment === null) return actual[index].length > 0;
try {{ return decodeURIComponent(actual[index]) === segment; }} catch {{ return false; }}
}}));
}}
"#
)
}
fn endpoint_matcher_condition(build: &project::ProjectBuild, base: &str) -> &'static str {
if endpoint_forwarding_patterns(build, base).is_empty() {
""
} else {
" || matchesNoxidEndpointPath(pathname)"
}
}
fn provider_function_count(
target: AdapterTarget,
build: &project::ProjectBuild,
scheduled_tasks: usize,
) -> usize {
match target {
AdapterTarget::Vercel => usize::from(has_server_handler(build)),
AdapterTarget::Netlify => {
usize::from(has_server_handler(build))
+ scheduled_tasks
+ usize::from(build.queues > 0 && !build.queue_worker)
}
_ => 0,
}
}
fn validate_build_adapter(
target: AdapterTarget,
build: &project::ProjectBuild,
) -> Result<(), String> {
if build.queues > 0
&& matches!(
target,
AdapterTarget::Static | AdapterTarget::Cloudflare | AdapterTarget::Deno
)
{
return Err(format!(
"error[ADAPTER_QUEUE_UNSUPPORTED]: adapter `{target}` cannot run {} declared durable queue(s); choose `node`, `vercel`, or `netlify`",
build.queues,
));
}
if build.queues > 0
&& build.queue_worker
&& matches!(target, AdapterTarget::Vercel | AdapterTarget::Netlify)
{
return Err(format!(
"error[SERVERLESS_QUEUE_WORKER_UNSUPPORTED]: adapter `{target}` cannot run the embedded queue worker; set `[server] queue_worker = false` so the adapter emits a scheduled queue drain, or choose `node`",
));
}
Ok(())
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum NodeDatabaseDriver {
Postgres,
Mysql,
Sqlite,
}
fn node_database_driver(
environment: &BTreeMap<String, String>,
build: &project::ProjectBuild,
) -> Option<NodeDatabaseDriver> {
match environment.get("DATABASE_URL").map(String::as_str) {
Some(url) if url.starts_with("postgres://") => Some(NodeDatabaseDriver::Postgres),
Some(url) if url.starts_with("mysql://") => Some(NodeDatabaseDriver::Mysql),
Some("sqlite::memory:") => Some(NodeDatabaseDriver::Sqlite),
Some(url) if url.starts_with("sqlite://") => Some(NodeDatabaseDriver::Sqlite),
Some(_) => None,
None if build.queues > 0 => Some(NodeDatabaseDriver::Postgres),
None => None,
}
}
fn node_package_json(driver: Option<NodeDatabaseDriver>) -> &'static str {
match driver {
Some(NodeDatabaseDriver::Postgres) => {
"{\n \"private\": true,\n \"type\": \"module\",\n \"scripts\": { \"start\": \"node server.mjs\" },\n \"dependencies\": { \"postgres\": \"3.4.9\" }\n}\n"
}
Some(NodeDatabaseDriver::Mysql) => {
"{\n \"private\": true,\n \"type\": \"module\",\n \"scripts\": { \"start\": \"node server.mjs\" },\n \"dependencies\": { \"mysql2\": \"3.23.4\" }\n}\n"
}
Some(NodeDatabaseDriver::Sqlite) | None => {
"{\n \"private\": true,\n \"type\": \"module\",\n \"scripts\": { \"start\": \"node server.mjs\" }\n}\n"
}
}
}
#[derive(Clone, Copy)]
struct AdapterEmissionConfig {
vercel_max_duration: u64,
queue_drain: bool,
queue_drain_budget_ms: u64,
shutdown_timeout_ms: u64,
tracing_export: noxid_codegen_server_js::ServerTracingExport,
node_database_driver: Option<NodeDatabaseDriver>,
}
fn lifecycle_binding(
export_name: &str,
local_name: &str,
value: &str,
feature: &str,
fallback: Option<&str>,
) -> String {
assert!(
noxid_codegen_server_js::server_lifecycle_exports().any(|name| name == export_name),
"adapter binding `{export_name}` is absent from tools/server-lifecycle-exports.txt"
);
match fallback {
Some(fallback) => {
format!("const {local_name} = typeof {value} === \"function\" ? {value} : {fallback};")
}
None => format!(
"const {local_name} = {value};\nrequireNoxidLifecycleExport(\"{export_name}\", {local_name}, \"{feature}\");"
),
}
}
fn handler_import_prelude(handler: &str, bindings: &[String]) -> String {
format!(
"import * as noxidServerHandler from \"{handler}\";\nfunction requireNoxidLifecycleExport(name, value, feature) {{\n if (typeof value !== \"function\") {{\n throw new Error(`error[SERVER_LIFECYCLE_EXPORT_MISSING]: the Farm-bundled server handler is missing required export \"${{name}}\" for ${{feature}}; rerun \"noxid adapt\" with the same Noxid compiler and do not remove lifecycle exports from server/handler.js`);\n }}\n}}\n{}",
bindings.join("\n")
)
}
fn tracing_lifecycle_bindings(
tracing_export: noxid_codegen_server_js::ServerTracingExport,
include_abandon: bool,
) -> Vec<String> {
let required = tracing_export == noxid_codegen_server_js::ServerTracingExport::Otlp;
let mut bindings = vec![lifecycle_binding(
"flushNoxidTracing",
"flushNoxidTracing",
"noxidServerHandler.flushNoxidTracing",
"tracing shutdown",
(!required).then_some("async () => {}"),
)];
if include_abandon {
bindings.push(lifecycle_binding(
"abandonNoxidTracing",
"abandonNoxidTracing",
"noxidServerHandler.abandonNoxidTracing",
"tracing shutdown",
(!required).then_some("() => 0"),
));
}
bindings
}
fn emit_adapter_files(
target: AdapterTarget,
out_dir: &Path,
base: &str,
build: &project::ProjectBuild,
task_schedules: &[(String, String)],
emission: AdapterEmissionConfig,
) -> Result<(), String> {
let AdapterEmissionConfig {
vercel_max_duration,
queue_drain,
queue_drain_budget_ms,
shutdown_timeout_ms,
tracing_export,
node_database_driver,
} = emission;
validate_build_adapter(target, build)?;
if has_dynamic_rendering(build) && matches!(target, AdapterTarget::Static) {
return Err(format!(
"error[SSR_ADAPTER_PENDING]: adapter `{target}` cannot host SSR; choose node, deno, cloudflare, vercel, or netlify",
));
}
let prefix = if base == "/" { "" } else { base };
match target {
AdapterTarget::Auto => unreachable!("auto is resolved before emission"),
AdapterTarget::Static => {}
AdapterTarget::Cloudflare => {
stage_provider_public_assets(out_dir, base)?;
let published = out_dir.join("public");
write(
&out_dir.join("_redirects"),
&format!("{prefix}/* {prefix}/index.html 200\n"),
)?;
write(
&published.join("_redirects"),
&format!("{prefix}/* {prefix}/index.html 200\n"),
)?;
write(
&out_dir.join("_headers"),
"/*\n X-Content-Type-Options: nosniff\n Referrer-Policy: strict-origin-when-cross-origin\n",
)?;
write(
&published.join("_headers"),
"/*\n X-Content-Type-Options: nosniff\n Referrer-Policy: strict-origin-when-cross-origin\n",
)?;
if has_server_handler(build) {
let root = public_root(out_dir, base);
let template = fs::read_to_string(root.join("index.html"))
.map_err(|error| format!("cannot read Cloudflare HTML template: {error}"))?;
let handler = server_handler_import(base);
let source = provider_runtime(
&template,
base,
"cloudflare",
CLOUDFLARE_PROVIDER_EXPORT,
tracing_export,
false,
0,
build,
)
.replace("./server/handler.js", &handler);
write(&out_dir.join("_worker.js"), &source)?;
}
let worker = if has_server_handler(build) {
"main = \"_worker.js\"\n\n"
} else {
""
};
let asset_behavior = if has_server_handler(build) {
"binding = \"ASSETS\"\nrun_worker_first = true\n"
} else {
"not_found_handling = \"single-page-application\"\n"
};
write(
&out_dir.join("wrangler.toml"),
&format!(
"name = \"noxid-app\"\ncompatibility_date = \"2025-04-01\"\n{worker}[assets]\ndirectory = \"./public\"\n{asset_behavior}"
),
)?;
}
AdapterTarget::Netlify => {
stage_provider_public_assets(out_dir, base)?;
write(
&out_dir.join("_redirects"),
&format!("{prefix}/* /.netlify/functions/noxid 200\n"),
)?;
write(
&out_dir.join("public/_redirects"),
&format!("{prefix}/* /.netlify/functions/noxid 200\n"),
)?;
let mut config =
"[build]\n publish = \"public\"\n functions = \"netlify/functions\"\n"
.to_string();
for (index, (name, schedule)) in task_schedules.iter().enumerate() {
let function_name = format!("noxid-task-{index}");
emit_netlify_task_function(out_dir, base, &function_name, name, tracing_export)?;
config.push_str(&format!(
"\n[functions.\"{function_name}\"]\n schedule = \"{}\"\n",
noxid_source::json_escape(schedule),
));
}
let queue_drain = build.queues > 0 && !build.queue_worker;
if queue_drain {
emit_netlify_queue_drain_function(
out_dir,
base,
queue_drain_budget_ms,
tracing_export,
)?;
config
.push_str("\n[functions.\"noxid-queue-drain\"]\n schedule = \"* * * * *\"\n");
}
write(&out_dir.join("netlify.toml"), &config)?;
if has_server_handler(build) {
emit_netlify_function(
out_dir,
base,
queue_drain,
queue_drain_budget_ms,
build,
tracing_export,
)?;
}
}
AdapterTarget::Vercel => {
emit_vercel_output(
out_dir,
base,
build,
task_schedules,
vercel_max_duration,
build.queues > 0 && !build.queue_worker,
queue_drain_budget_ms,
tracing_export,
)?;
}
AdapterTarget::Node => {
let fallback = if base == "/" {
"index.html".to_string()
} else {
format!("{}/index.html", base.trim_start_matches('/'))
};
let (handler_import, action_branch) = if has_server_handler(build) {
let scheduler_import = if build.tasks > 0 {
Some(lifecycle_binding(
"startTaskScheduler",
"startTaskScheduler",
"noxidServerHandler.startTaskScheduler",
"scheduled tasks",
None,
))
} else {
None
};
let queue_import = if build.queues > 0 && build.queue_worker {
Some(lifecycle_binding(
"startQueueWorker",
"startQueueWorker",
"noxidServerHandler.startQueueWorker",
"the embedded queue worker",
None,
))
} else {
None
};
let mut bindings = vec![
lifecycle_binding(
"closeDatabase",
"closeDatabase",
"noxidServerHandler.closeDatabase",
"server shutdown",
None,
),
lifecycle_binding(
"closeQueueDatabase",
"closeQueueDatabase",
"noxidServerHandler.closeQueueDatabase",
"server shutdown",
None,
),
];
bindings.extend(tracing_lifecycle_bindings(tracing_export, true));
bindings.extend(scheduler_import);
bindings.extend(queue_import);
bindings.push(lifecycle_binding(
"fetch",
"handleNoxid",
"noxidServerHandler.fetch ?? globalThis.__NOXID_FETCH_HANDLER__",
"request handling",
None,
));
(
handler_import_prelude(&server_handler_import(base), &bindings),
NODE_ACTION_BRANCH,
)
} else {
(
"const closeDatabase = async () => {};\nconst closeQueueDatabase = async () => {};\nconst flushNoxidTracing = async () => {};\nconst abandonNoxidTracing = () => 0;"
.into(),
"",
)
};
let ssr_document = if has_dynamic_rendering(build) {
NODE_STREAMING_SSR_DOCUMENT.replace("__NOXID_FALLBACK__", &fallback)
} else {
String::new()
};
let queue_drain_context = if queue_drain {
format!(
"pathname.endsWith(\"/_noxid/queue/drain\") ? Object.assign(Object.create(null), {{ noxidQueueDrain: true, queueDrainBudgetMs: {queue_drain_budget_ms} }}) : Object.create(null)"
)
} else {
"Object.create(null)".into()
};
let endpoint_matcher = endpoint_matcher_javascript(build, base);
let endpoint_condition = endpoint_matcher_condition(build, base);
write(
&out_dir.join("server.mjs"),
&NODE_SERVER
.replace("__NOXID_FALLBACK__", &fallback)
.replace("__NOXID_BASE__", &noxid_source::json_escape(base))
.replace("__NOXID_HANDLER_IMPORT__", &handler_import)
.replace("__NOXID_ENDPOINT_MATCHER__", &endpoint_matcher)
.replace(
"__NOXID_RESERVED_PATH_MATCHER__",
&reserved_deployment_path_javascript(),
)
.replace("__NOXID_ACTION_BRANCH__", action_branch)
.replace("__NOXID_ENDPOINT_ROUTE__", endpoint_condition)
.replace(
"__NOXID_QUEUE_DRAIN_ROUTE__",
if queue_drain {
" || pathname.endsWith(\"/_noxid/queue/drain\")"
} else {
""
},
)
.replace("__NOXID_QUEUE_DRAIN_CONTEXT__", &queue_drain_context)
.replace(
"__NOXID_SHUTDOWN_TIMEOUT_MS__",
&shutdown_timeout_ms.to_string(),
)
.replace(
"__NOXID_TASK_SCHEDULER_START__",
if build.tasks > 0 {
"taskScheduler = startTaskScheduler(process.env, Object.create(null));"
} else {
""
},
)
.replace(
"__NOXID_QUEUE_WORKER_START__",
if build.queues > 0 && build.queue_worker {
"queueWorker = startQueueWorker();"
} else {
""
},
)
.replace("__NOXID_SSR_DOCUMENT__", &ssr_document),
)?;
write(
&out_dir.join("package.json"),
node_package_json(node_database_driver),
)?;
}
AdapterTarget::Deno => {
let fallback = if base == "/" {
"index.html".to_string()
} else {
format!("{}/index.html", base.trim_start_matches('/'))
};
let (handler_import, action_branch) = if has_server_handler(build) {
(
deno_handler_import(base, tracing_export),
DENO_ACTION_BRANCH,
)
} else {
(
"const flushNoxidTracing = async () => {};\nconst abandonNoxidTracing = () => 0;"
.into(),
"",
)
};
let source = if has_dynamic_rendering(build) {
let root = public_root(out_dir, base);
let template = fs::read_to_string(root.join("index.html"))
.map_err(|error| format!("cannot read Deno HTML template: {error}"))?;
provider_runtime(
&template,
base,
"deno",
DENO_PROVIDER_EXPORT,
tracing_export,
false,
0,
build,
)
.replace("./server/handler.js", &server_handler_import(base))
.replace("__NOXID_FALLBACK__", &fallback)
} else {
DENO_SERVER
.replace("__NOXID_FALLBACK__", &fallback)
.replace("__NOXID_BASE__", &noxid_source::json_escape(base))
.replace("__NOXID_HANDLER_IMPORT__", &handler_import)
.replace(
"__NOXID_RESERVED_PATH_MATCHER__",
&reserved_deployment_path_javascript(),
)
.replace(
"__NOXID_ENDPOINT_MATCHER__",
&endpoint_matcher_javascript(build, base),
)
.replace("__NOXID_ACTION_BRANCH__", action_branch)
.replace(
"__NOXID_ENDPOINT_ROUTE__",
endpoint_matcher_condition(build, base),
)
}
.replace("__NOXID_DENO_SHUTDOWN_RUNTIME__", DENO_SHUTDOWN_RUNTIME)
.replace(
"__NOXID_SHUTDOWN_TIMEOUT_MS__",
&shutdown_timeout_ms.to_string(),
);
write(&out_dir.join("server.ts"), &source)?;
write(
&out_dir.join("deno.json"),
"{\n \"tasks\": { \"start\": \"deno run --allow-net --allow-read=. --allow-env=PORT server.ts\" }\n}\n",
)?;
write(
&out_dir.join("package.json"),
"{\n \"private\": true,\n \"type\": \"module\"\n}\n",
)?;
}
}
Ok(())
}
fn public_root(out_dir: &Path, base: &str) -> std::path::PathBuf {
if base == "/" {
out_dir.to_path_buf()
} else {
out_dir.join(base.trim_start_matches('/'))
}
}
fn stage_provider_public_assets(out_dir: &Path, base: &str) -> Result<(), String> {
let source = public_root(out_dir, base);
let destination = if base == "/" {
out_dir.join("public")
} else {
out_dir.join("public").join(base.trim_start_matches('/'))
};
if destination.exists() {
fs::remove_dir_all(&destination).map_err(|error| {
format!(
"cannot refresh provider public directory {}: {error}",
destination.display()
)
})?;
}
copy_tree(&source, &destination, is_provider_public_path)
}
fn is_provider_public_path(path: &Path) -> bool {
let mut components = path.components();
let Some(first) = components.next() else {
return true;
};
let name = first.as_os_str().to_str().unwrap_or_default();
!is_reserved_deployment_path_segment(name)
}
fn emit_netlify_function(
out_dir: &Path,
base: &str,
queue_drain: bool,
queue_drain_budget_ms: u64,
build: &project::ProjectBuild,
tracing_export: noxid_codegen_server_js::ServerTracingExport,
) -> Result<(), String> {
let root = public_root(out_dir, base);
let function = out_dir.join("netlify/functions/noxid");
fs::create_dir_all(&function)
.map_err(|error| format!("cannot create {}: {error}", function.display()))?;
copy_tree(&root.join("server"), &function.join("server"), |_| true)?;
let template = fs::read_to_string(root.join("index.html"))
.map_err(|error| format!("cannot read provider HTML template: {error}"))?;
let source = provider_runtime(
&template,
base,
"netlify",
NETLIFY_EXPORT,
tracing_export,
queue_drain,
queue_drain_budget_ms,
build,
);
write(&function.join("index.mjs"), &source)?;
write(&function.join("package.json"), "{\"type\":\"module\"}\n")
}
fn emit_netlify_queue_drain_function(
out_dir: &Path,
base: &str,
queue_drain_budget_ms: u64,
tracing_export: noxid_codegen_server_js::ServerTracingExport,
) -> Result<(), String> {
let root = public_root(out_dir, base);
let function = out_dir.join("netlify/functions/noxid-queue-drain");
fs::create_dir_all(&function)
.map_err(|error| format!("cannot create {}: {error}", function.display()))?;
copy_tree(&root.join("server"), &function.join("server"), |_| true)?;
let prefix = if base == "/" { "" } else { base };
let source = NETLIFY_QUEUE_DRAIN_EXPORT
.replace(
"__NOXID_HANDLER_PRELUDE__",
&provider_handler_prelude(tracing_export),
)
.replace("__NOXID_BASE__", &noxid_source::json_escape(prefix))
.replace(
"__NOXID_QUEUE_DRAIN_BUDGET__",
&queue_drain_budget_ms.to_string(),
);
write(&function.join("index.mjs"), &source)?;
write(&function.join("package.json"), "{\"type\":\"module\"}\n")
}
fn emit_netlify_task_function(
out_dir: &Path,
base: &str,
function_name: &str,
task_name: &str,
tracing_export: noxid_codegen_server_js::ServerTracingExport,
) -> Result<(), String> {
let root = public_root(out_dir, base);
let function = out_dir.join("netlify/functions").join(function_name);
fs::create_dir_all(&function)
.map_err(|error| format!("cannot create {}: {error}", function.display()))?;
copy_tree(&root.join("server"), &function.join("server"), |_| true)?;
let prefix = if base == "/" { "" } else { base };
let source = NETLIFY_TASK_EXPORT
.replace(
"__NOXID_HANDLER_PRELUDE__",
&provider_handler_prelude(tracing_export),
)
.replace("__NOXID_BASE__", &noxid_source::json_escape(prefix))
.replace("__NOXID_TASK__", &noxid_source::json_escape(task_name));
write(&function.join("index.mjs"), &source)?;
write(&function.join("package.json"), "{\"type\":\"module\"}\n")
}
#[allow(clippy::too_many_arguments)]
fn emit_vercel_output(
out_dir: &Path,
base: &str,
build: &project::ProjectBuild,
task_schedules: &[(String, String)],
max_duration: u64,
queue_drain: bool,
queue_drain_budget_ms: u64,
tracing_export: noxid_codegen_server_js::ServerTracingExport,
) -> Result<(), String> {
let root = public_root(out_dir, base);
let output = out_dir.join(".vercel/output");
let static_dir = output.join("static");
fs::create_dir_all(&static_dir)
.map_err(|error| format!("cannot create {}: {error}", static_dir.display()))?;
let static_public = if base == "/" {
static_dir.clone()
} else {
static_dir.join(base.trim_start_matches('/'))
};
copy_tree(&root, &static_public, is_provider_public_path)?;
let prefix = if base == "/" { "" } else { base };
let mut routes = vec!["{\"handle\":\"filesystem\"}".to_string()];
if has_server_handler(build) {
let function = output.join("functions/noxid.func");
fs::create_dir_all(&function)
.map_err(|error| format!("cannot create {}: {error}", function.display()))?;
copy_tree(&root.join("server"), &function.join("server"), |_| true)?;
let template = fs::read_to_string(root.join("index.html"))
.map_err(|error| format!("cannot read provider HTML template: {error}"))?;
write(
&function.join("index.mjs"),
&provider_runtime(
&template,
base,
"vercel",
VERCEL_EXPORT,
tracing_export,
queue_drain,
queue_drain_budget_ms,
build,
),
)?;
write(
&function.join(".vc-config.json"),
&format!(
"{{\"runtime\":\"nodejs22.x\",\"handler\":\"index.mjs\",\"launcherType\":\"Nodejs\",\"supportsResponseStreaming\":true,\"maxDuration\":{max_duration}}}\n"
),
)?;
write(&function.join("package.json"), "{\"type\":\"module\"}\n")?;
routes.push(format!("{{\"src\":\"{prefix}/(.*)\",\"dest\":\"/noxid\"}}"));
} else {
routes.push(format!(
"{{\"src\":\"{prefix}/(.*)\",\"dest\":\"{prefix}/index.html\"}}"
));
}
let mut crons = task_schedules
.iter()
.map(|(name, schedule)| {
format!(
"{{\"path\":\"{prefix}/_noxid/tasks/{}\",\"schedule\":\"{}\"}}",
noxid_source::json_escape(name),
noxid_source::json_escape(schedule),
)
})
.collect::<Vec<_>>();
if queue_drain {
crons.push(format!(
"{{\"path\":\"{prefix}/_noxid/queue/drain\",\"schedule\":\"* * * * *\"}}"
));
}
let crons = crons.join(",");
let cron_config = if crons.is_empty() {
String::new()
} else {
format!(",\n \"crons\": [{crons}]")
};
write(
&output.join("config.json"),
&format!(
"{{\n \"version\": 3,\n \"routes\": [{}]{}\n}}\n",
routes.join(","),
cron_config,
),
)
}
fn copy_tree<F>(source: &Path, destination: &Path, include: F) -> Result<(), String>
where
F: Fn(&Path) -> bool + Copy,
{
copy_tree_from_root(source, source, destination, include)
}
fn copy_tree_from_root<F>(
root: &Path,
source: &Path,
destination: &Path,
include: F,
) -> Result<(), String>
where
F: Fn(&Path) -> bool + Copy,
{
if !source.exists() {
return Err(format!(
"provider function input {} does not exist",
source.display()
));
}
fs::create_dir_all(destination)
.map_err(|error| format!("cannot create {}: {error}", destination.display()))?;
for entry in fs::read_dir(source)
.map_err(|error| format!("cannot read {}: {error}", source.display()))?
{
let entry = entry.map_err(|error| error.to_string())?;
let path = entry.path();
let relative = path
.strip_prefix(root)
.map_err(|_| "provider copy escaped its source root")?;
if !include(relative) {
continue;
}
let target = destination.join(
path.strip_prefix(source)
.map_err(|_| "provider copy escaped its source directory")?,
);
if entry
.file_type()
.map_err(|error| error.to_string())?
.is_dir()
{
copy_tree_from_root(root, &path, &target, include)?;
} else {
if let Some(parent) = target.parent() {
fs::create_dir_all(parent)
.map_err(|error| format!("cannot create {}: {error}", parent.display()))?;
}
fs::copy(&path, &target).map_err(|error| {
format!(
"cannot copy {} to {}: {error}",
path.display(),
target.display()
)
})?;
}
}
Ok(())
}
#[allow(clippy::too_many_arguments)]
fn provider_runtime(
template: &str,
base: &str,
provider: &str,
export: &str,
tracing_export: noxid_codegen_server_js::ServerTracingExport,
queue_drain: bool,
queue_drain_budget_ms: u64,
build: &project::ProjectBuild,
) -> String {
PROVIDER_RUNTIME
.replace(
"__NOXID_HANDLER_PRELUDE__",
&provider_handler_prelude(tracing_export),
)
.replace("__NOXID_PROVIDER_EXPORT__", export)
.replace(
"__NOXID_TEMPLATE__",
&format!("\"{}\"", noxid_source::json_escape(template)),
)
.replace("__NOXID_BASE__", &noxid_source::json_escape(base))
.replace("__NOXID_PROVIDER__", provider)
.replace(
"__NOXID_ENDPOINT_MATCHER__",
&endpoint_matcher_javascript(build, base),
)
.replace(
"__NOXID_ENDPOINT_ROUTE__",
endpoint_matcher_condition(build, base),
)
.replace(
"__NOXID_QUEUE_DRAIN_ENABLED__",
if queue_drain { "true" } else { "false" },
)
.replace(
"__NOXID_QUEUE_DRAIN_BUDGET__",
&queue_drain_budget_ms.to_string(),
)
.replace(
"__NOXID_RESERVED_PATH_MATCHER__",
&reserved_deployment_path_javascript(),
)
}
fn server_handler_import(base: &str) -> String {
if base == "/" {
"./server/handler.js".into()
} else {
format!("./{}/server/handler.js", base.trim_start_matches('/'))
}
}
fn provider_handler_prelude(
tracing_export: noxid_codegen_server_js::ServerTracingExport,
) -> String {
let mut bindings = vec![lifecycle_binding(
"fetch",
"handleNoxid",
"noxidServerHandler.fetch ?? globalThis.__NOXID_FETCH_HANDLER__",
"request handling",
None,
)];
bindings.extend(tracing_lifecycle_bindings(tracing_export, false));
handler_import_prelude("./server/handler.js", &bindings)
}
fn deno_handler_import(
base: &str,
tracing_export: noxid_codegen_server_js::ServerTracingExport,
) -> String {
let mut bindings = vec![lifecycle_binding(
"fetch",
"handleNoxid",
"noxidServerHandler.fetch ?? globalThis.__NOXID_FETCH_HANDLER__",
"request handling",
None,
)];
bindings.extend(tracing_lifecycle_bindings(tracing_export, true));
handler_import_prelude(&server_handler_import(base), &bindings)
}
fn write(path: &Path, contents: &str) -> Result<(), String> {
fs::write(path, contents).map_err(|error| format!("cannot write {}: {error}", path.display()))
}
const PROVIDER_RUNTIME: &str = r#"__NOXID_HANDLER_PRELUDE__
const template = __NOXID_TEMPLATE__;
const basePath = "__NOXID_BASE__";
const provider = "__NOXID_PROVIDER__";
const queueDrainEnabled = __NOXID_QUEUE_DRAIN_ENABLED__;
const queueDrainBudgetMs = __NOXID_QUEUE_DRAIN_BUDGET__;
__NOXID_ENDPOINT_MATCHER__
const rawRequestPathname = (requestUrl) => {
const target = String(requestUrl).replace(/^[A-Za-z][A-Za-z0-9+.-]*:\/\/[^/]*/, "");
const end = target.search(/[?#]/);
return (end === -1 ? target : target.slice(0, end)) || "/";
};
const normalizePosixPath = (pathname) => {
const segments = [];
for (const segment of pathname.split("/")) {
if (!segment || segment === ".") continue;
if (segment === "..") segments.pop();
else segments.push(segment);
}
const trailingSlash = pathname.endsWith("/") && segments.length > 0 ? "/" : "";
return `/${segments.join("/")}${trailingSlash}`;
};
const normalizedRequestPathname = (requestUrl) => {
let pathname;
try { pathname = decodeURIComponent(rawRequestPathname(requestUrl)); }
catch { return null; }
if (pathname.includes("%") || /[\u0000-\u001f\u007f-\u009f]/.test(pathname)) return null;
return normalizePosixPath(pathname);
};
const isAgentSurfacePath = (pathname) => {
const prefix = basePath === "/" ? "" : basePath.replace(/\/$/, "");
return pathname === `${prefix}/_noxid/openapi.json` || pathname === `${prefix}/_noxid/mcp`;
};
const isAgentSurfaceDoor = (pathname) => {
const candidate = pathname.length > 1 ? pathname.replace(/\/+$/, "") : pathname;
return isAgentSurfacePath(candidate) || candidate.endsWith("/_noxid/openapi.json") || candidate.endsWith("/_noxid/mcp");
};
// The two agent run doors are one family: `runs` starts a run and
// `runs/<id>/resume` continues one. Forwarding only the second left WO-31's
// whole runtime surface unreachable on a deployed build — a run start fell
// through to the SPA document. Matched by suffix for the same reason
// `isAgentSurfaceDoor` is: the front door forwards a superset so a based
// deployment is covered without the base being spelled twice, and
// `handleAgentRunRequest` requires the exact base-prefixed path, so the
// refusal happens inside where it can be structured.
const isAgentRunDoor = (pathname) => {
const candidate = pathname.length > 1 ? pathname.replace(/\/+$/, "") : pathname;
return /\/_noxid\/agents\/[^/]+\/runs(?:\/[^/]+\/resume)?$/.test(candidate);
};
const encoder = new TextEncoder();
const escapeHtml = (value) => String(value).replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll("\"", """);
const injectHead = (document, fragment) => document.includes("</head>")
? document.replace("</head>", `${fragment}\n</head>`)
: document.replace("</title>", `</title>${fragment}`);
function scheduleNoxidTracingFlush(executionContext) {
if (typeof executionContext?.waitUntil !== "function") return;
try { executionContext.waitUntil(flushNoxidTracing()); } catch {}
}
function eventReader(body) {
const reader = body.getReader();
const decoder = new TextDecoder();
let pending = "";
let done = false;
return async () => {
while (true) {
const newline = pending.indexOf("\n");
if (newline !== -1) {
const line = pending.slice(0, newline); pending = pending.slice(newline + 1);
if (line) return JSON.parse(line);
continue;
}
if (done) {
const line = pending.trim(); pending = "";
return line ? JSON.parse(line) : null;
}
const chunk = await reader.read();
done = chunk.done;
if (chunk.value) pending += decoder.decode(chunk.value, { stream: !done });
if (done) pending += decoder.decode();
}
};
}
function cacheHeaders(cache) {
if (!cache) return { "Cache-Control": "no-store" };
const shared = cache.mode === "swr"
? `public, s-maxage=${cache.revalidateSeconds}, stale-while-revalidate=${cache.staleSeconds}`
: `public, s-maxage=${cache.revalidateSeconds}, must-revalidate`;
const headers = {
"Cache-Control": "public, max-age=0, must-revalidate",
"CDN-Cache-Control": shared,
"X-Noxid-Cache-Mode": cache.mode,
};
if (provider === "netlify") headers["Netlify-CDN-Cache-Control"] = `durable, ${shared}`;
const headerVary = Array.isArray(cache.vary) ? cache.vary.filter((value) => value.startsWith("header:")).map((value) => value.slice(7)) : [];
if (headerVary.length) headers["Vary"] = headerVary.join(", ");
if (Array.isArray(cache.tags) && cache.tags.length) {
headers["Cache-Tag"] = cache.tags.join(",");
if (provider === "netlify") headers["Netlify-Cache-Tag"] = cache.tags.join(",");
}
return headers;
}
// Server middleware may attach allowlisted response headers (session
// cookies, x- custom headers); the SSR renderer transports them on
// x-noxid-ssr-headers and the document response replays them.
function middlewarePairs(rendered) {
const raw = rendered.headers.get("x-noxid-ssr-headers");
if (!raw) return [];
try {
const pairs = JSON.parse(raw);
return Array.isArray(pairs) ? pairs.filter((pair) => Array.isArray(pair) && typeof pair[0] === "string" && typeof pair[1] === "string") : [];
} catch { return []; }
}
function withPairs(init, pairs) {
const headers = new Headers(init);
for (const [name, value] of pairs) headers.append(name, value);
return headers;
}
async function renderDocument(request, environment, executionContext) {
const url = new URL(request.url);
const prefix = basePath === "/" ? "" : basePath;
const headers = new Headers(request.headers);
headers.set("content-type", "application/json");
const rendered = await handleNoxid(new Request(`${url.origin}${prefix}/_noxid/ssr`, {
method: "POST",
headers,
body: JSON.stringify({ url: url.href, stream: true }),
signal: request.signal,
}), environment, executionContext);
const middlewareHeaders = middlewarePairs(rendered);
if (!rendered.ok) {
let result = null;
try { result = await rendered.json(); } catch {}
if (result?.error?.code === "SSR_ROUTE_NOT_FOUND") {
return new Response(template, { headers: { "Content-Type": "text/html; charset=utf-8", "Cache-Control": "no-store" } });
}
if (result?.error?.code === "SSR_MIDDLEWARE_RESPONSE" && result.respond && typeof result.respond.body === "string") {
return new Response(result.respond.body, { status: result.respond.status, headers: withPairs({ "Content-Type": `${result.respond.contentType}; charset=utf-8`, "Cache-Control": "no-store", "X-Content-Type-Options": "nosniff" }, middlewareHeaders) });
}
if (result?.error?.code === "SSR_MIDDLEWARE_REDIRECT" && typeof result.redirect === "string") {
return new Response(null, { status: 307, headers: withPairs({ Location: result.redirect, "Cache-Control": "no-store" }, middlewareHeaders) });
}
const status = rendered.status || 500;
const code = result?.error?.code ?? "SSR_RENDER_FAILED";
return new Response(`<!doctype html><html><body><main role="alert" data-noxid-ssr-error="${escapeHtml(code)}"><h1>${status === 403 ? "Access denied" : "Server rendering failed"}</h1></main></body></html>`, {
status,
headers: withPairs({ "Content-Type": "text/html; charset=utf-8", "Cache-Control": "no-store" }, middlewareHeaders),
});
}
if (rendered.headers.get("x-noxid-ssr-stream") !== "1" || !rendered.body) {
return new Response("SSR stream unavailable", { status: 502, headers: { "Cache-Control": "no-store" } });
}
const next = eventReader(rendered.body);
const shell = await next();
if (shell?.schemaVersion !== 1 || shell.type !== "shell") {
return new Response("SSR stream shell missing", { status: 502, headers: { "Cache-Control": "no-store" } });
}
let document = template;
if (typeof shell.head?.title === "string") document = document.replace(/<title>[\s\S]*?<\/title>/, `<title>${escapeHtml(shell.head.title)}</title>`);
if (typeof shell.head?.description === "string") document = injectHead(document, `<meta name="description" data-noxid-route-description content="${escapeHtml(shell.head.description)}">`);
const styles = Array.isArray(shell.head?.styles) ? shell.head.styles : [];
const links = styles.map((relative, index) => `<link rel="stylesheet" href="${escapeHtml(`${prefix}/${String(relative).replace(/^\/+/, "")}`)}" data-noxid-route-style="${escapeHtml(shell.targets?.[index]?.component ?? "")}">`).join("\n");
if (links) document = injectHead(document, links);
const marker = "__NOXID_PROVIDER_APP__";
document = document.replace(/<div\s+id="?app"?\s*>(?:\s*<!--noxid-server-shell-start-->[\s\S]*?<!--noxid-server-shell-end-->\s*)?<\/div>/, marker);
const markerIndex = document.indexOf(marker);
if (markerIndex === -1) return new Response("SSR app root missing", { status: 500 });
const prefixDocument = `${document.slice(0, markerIndex)}<div id="app" data-noxid-ssr="${escapeHtml(shell.routeId ?? "")}">`;
const suffix = document.slice(markerIndex + marker.length);
const body = new ReadableStream({
async start(controller) {
controller.enqueue(encoder.encode(prefixDocument));
let closed = false;
try {
for (let event = await next(); event; event = await next()) {
if (event?.schemaVersion !== 1) throw new Error("SSR_STREAM_EVENT_INVALID");
if (event.type === "html") controller.enqueue(encoder.encode(event.html ?? ""));
else if (event.type === "payload") {
controller.enqueue(encoder.encode(`</div><script type="application/json" id="__NOXID_SSR_PAYLOAD__">${String(event.payload ?? "")}</script>${suffix}`));
closed = true;
} else if (event.type === "error") {
controller.enqueue(encoder.encode(`<main role="alert" data-noxid-ssr-error="${escapeHtml(event.error?.code ?? "SSR_STREAM_FAILED")}"><h1>Server rendering failed</h1></main></div>${suffix}`));
closed = true;
}
}
if (!closed) controller.enqueue(encoder.encode(`<main role="alert" data-noxid-ssr-error="SSR_STREAM_INCOMPLETE"><h1>Server rendering failed</h1></main></div>${suffix}`));
controller.close();
} catch (error) { controller.error(error); }
},
cancel(reason) { request.signal?.throwIfAborted?.(); return reason; },
});
return new Response(body, {
headers: withPairs({
"Content-Type": "text/html; charset=utf-8",
"X-Content-Type-Options": "nosniff",
"X-Noxid-Ssr-Stream": "1",
...cacheHeaders(shell.cache),
}, middlewareHeaders),
});
}
export async function handleProviderRequest(request, environment = Object.create(null), executionContext = Object.create(null)) {
try {
const pathname = normalizedRequestPathname(request.url);
if (pathname === null) return new Response("Not found", { status: 404 });
if (queueDrainEnabled && pathname.endsWith("/_noxid/queue/drain")) {
const drainContext = Object.assign(Object.create(null), executionContext, { noxidQueueDrain: true, queueDrainBudgetMs });
return await handleNoxid(request, environment, drainContext);
}
if (pathname.includes("/_noxid/actions/") || pathname.includes("/_noxid/tasks/") || pathname.endsWith("/_noxid/revalidate") || isAgentRunDoor(pathname) || isAgentSurfaceDoor(pathname)__NOXID_ENDPOINT_ROUTE__) return await handleNoxid(request, environment, executionContext);
if (request.method !== "GET" && request.method !== "HEAD") return new Response("Method not allowed", { status: 405 });
return await renderDocument(request, environment, executionContext);
} finally {
scheduleNoxidTracingFlush(executionContext);
}
}
__NOXID_PROVIDER_EXPORT__
"#;
const NETLIFY_EXPORT: &str = r#"export default (request, context) => handleProviderRequest(request, process.env, context);
export const config = { path: "/*" };"#;
const NETLIFY_TASK_EXPORT: &str = r#"__NOXID_HANDLER_PRELUDE__
export default async (request, context) => {
const origin = new URL(request.url).origin;
const task = new Request(`${origin}__NOXID_BASE__/_noxid/tasks/__NOXID_TASK__`, { method: "POST", headers: request.headers });
try { return await handleNoxid(task, process.env, context); }
finally { try { if (typeof context?.waitUntil === "function") context.waitUntil(flushNoxidTracing()); } catch {} }
};
"#;
const NETLIFY_QUEUE_DRAIN_EXPORT: &str = r#"__NOXID_HANDLER_PRELUDE__
export default async (request, invocationContext) => {
const origin = new URL(request.url).origin;
const drain = new Request(`${origin}__NOXID_BASE__/_noxid/queue/drain`, { method: "POST", headers: request.headers });
const context = Object.assign(Object.create(null), { noxidQueueDrain: true, queueDrainBudgetMs: __NOXID_QUEUE_DRAIN_BUDGET__ });
try { return await handleNoxid(drain, process.env, context); }
finally { try { if (typeof invocationContext?.waitUntil === "function") invocationContext.waitUntil(flushNoxidTracing()); } catch {} }
};
"#;
const VERCEL_EXPORT: &str = r#"import { Readable } from "node:stream";
export default async function noxid(request, response) {
const origin = `https://${request.headers.host ?? "noxid.local"}`;
const url = new URL(request.url, origin);
const cronPath = url.pathname.includes("/_noxid/tasks/") || url.pathname.endsWith("/_noxid/queue/drain");
const cron = request.method === "GET" && cronPath && request.headers["user-agent"] === "vercel-cron/1.0";
const method = cron ? "POST" : request.method;
const init = { method, headers: request.headers };
if (method !== "GET" && method !== "HEAD" && !cron) { init.body = Readable.toWeb(request); init.duplex = "half"; }
const result = await handleProviderRequest(new Request(url, init), process.env, Object.create(null));
const resultHeaders = Object.fromEntries(result.headers);
const setCookies = result.headers.getSetCookie?.() ?? [];
if (setCookies.length) resultHeaders["set-cookie"] = setCookies;
response.writeHead(result.status, resultHeaders);
if (result.body) Readable.fromWeb(result.body).pipe(response); else response.end();
}"#;
const DENO_PROVIDER_EXPORT: &str = r#"const assetRoot = new URL("./", import.meta.url);
const assetTypes = { ".html": "text/html; charset=utf-8", ".js": "text/javascript; charset=utf-8", ".css": "text/css; charset=utf-8", ".json": "application/json; charset=utf-8", ".txt": "text/plain; charset=utf-8", ".svg": "image/svg+xml", ".wasm": "application/wasm", ".png": "image/png", ".jpg": "image/jpeg", ".jpeg": "image/jpeg", ".webp": "image/webp", ".ico": "image/x-icon", ".woff2": "font/woff2" };
__NOXID_RESERVED_PATH_MATCHER__
const isReservedStaticPath = (pathname) => {
const prefix = basePath === "/" ? "" : basePath.replace(/\/$/, "");
const applicationPath = prefix && pathname.startsWith(`${prefix}/`) ? pathname.slice(prefix.length) : pathname;
return [pathname, applicationPath].some((candidate) => isNoxidReservedDeploymentPath(candidate));
};
const noxidDenoServer = Deno.serve({ port: Number(Deno.env.get("PORT") ?? 3000), hostname: "0.0.0.0" }, async (request) => {
const pathname = normalizedRequestPathname(request.url);
if (pathname === null) return new Response("Not found", { status: 404 });
const wantsHtml = request.method === "GET" && (request.headers.get("accept") ?? "").includes("text/html");
if (pathname.includes("/_noxid/actions/") || pathname.includes("/_noxid/tasks/") || pathname.endsWith("/_noxid/revalidate") || isAgentRunDoor(pathname) || isAgentSurfaceDoor(pathname)__NOXID_ENDPOINT_ROUTE__ || wantsHtml) return handleProviderRequest(request, Deno.env.toObject(), Object.create(null));
if (isReservedStaticPath(pathname)) return new Response("Not found", { status: 404 });
const relative = pathname.replace(/^\/+/, "");
if (relative.split("/").includes("..")) return new Response("Bad request", { status: 400 });
if (relative.includes("%") || isNoxidReservedDeploymentPath(relative)) return new Response("Not found", { status: 404 });
try {
const rootPath = (await Deno.realPath(assetRoot)).replace(/[\\/]+$/, "");
const filePath = await Deno.realPath(new URL(relative || "__NOXID_FALLBACK__", assetRoot));
if (filePath !== rootPath && !filePath.startsWith(`${rootPath}/`) && !filePath.startsWith(`${rootPath}\\`)) return new Response("Not found", { status: 404 });
// The same load-bearing re-check as the Node handler's: `Deno.realPath` is
// Rust `std::fs::canonicalize`, i.e. the same `realpath(3)`, so this is
// where a case- or normalization-folded spelling (`SERVER`, U+017F `\u017Ferver`)
// becomes its true on-disk name. The matcher above sees only what the
// caller wrote. Do not remove it.
const served = `/${filePath.slice(rootPath.length).replaceAll("\\", "/").replace(/^\/+/, "")}`;
if (isReservedStaticPath(served)) return new Response("Not found", { status: 404 });
const body = await Deno.readFile(filePath);
const extension = served.includes(".") ? `.${served.split(".").pop()}` : "";
return new Response(body, { headers: { "content-type": assetTypes[extension] ?? "application/octet-stream", "x-content-type-options": "nosniff" } });
} catch { return new Response("Not found", { status: 404 }); }
});
__NOXID_DENO_SHUTDOWN_RUNTIME__"#;
const DENO_SHUTDOWN_RUNTIME: &str = r#"const noxidDenoShutdownTimeoutMs = __NOXID_SHUTDOWN_TIMEOUT_MS__;
const noxidDenoTracingFlushBudgetMs = Math.min(2000, Math.floor(noxidDenoShutdownTimeoutMs / 4));
let noxidDenoShuttingDown = false;
const settleNoxidDenoBeforeDeadline = async (operation, deadline) => {
let timeout;
try {
return await Promise.race([
Promise.resolve().then(operation).then(() => true),
new Promise((resolve) => { timeout = setTimeout(() => resolve(false), Math.max(0, deadline - Date.now())); }),
]);
} finally {
clearTimeout(timeout);
}
};
const shutdownNoxidDeno = async () => {
if (noxidDenoShuttingDown) return;
noxidDenoShuttingDown = true;
const shutdownDeadline = Date.now() + noxidDenoShutdownTimeoutMs;
try {
await settleNoxidDenoBeforeDeadline(() => noxidDenoServer.shutdown(), shutdownDeadline);
} finally {
try {
const tracingFlushDeadline = Math.min(shutdownDeadline, Date.now() + noxidDenoTracingFlushBudgetMs);
const tracingFlushed = await settleNoxidDenoBeforeDeadline(flushNoxidTracing, tracingFlushDeadline);
if (!tracingFlushed) {
const dropped = abandonNoxidTracing();
console.error(JSON.stringify(Object.freeze({ schema: "noxid.tracing.error.v1", event: "tracing.flush.abandoned", code: "TRACING_FLUSH_DEADLINE_EXCEEDED", exporter: "otlp", dropped })));
}
} catch (cause) {
console.error("Noxid tracing flush failed", cause);
}
}
};
for (const signal of ["SIGTERM", "SIGINT"]) {
try { Deno.addSignalListener(signal, () => { void shutdownNoxidDeno(); }); } catch {}
}"#;
const CLOUDFLARE_PROVIDER_EXPORT: &str = r#"export default {
async fetch(request, environment, context) {
const url = new URL(request.url);
const pathname = url.pathname;
const wantsHtml = request.method === "GET" && (request.headers.get("accept") ?? "").includes("text/html");
if (pathname.includes("/_noxid/actions/") || pathname.includes("/_noxid/tasks/") || pathname.endsWith("/_noxid/revalidate") || isAgentRunDoor(pathname) || isAgentSurfaceDoor(pathname)__NOXID_ENDPOINT_ROUTE__ || wantsHtml) return handleProviderRequest(request, environment, context);
return environment.ASSETS.fetch(request);
},
};"#;
const NODE_SERVER: &str = r#"import http from "node:http";
import fs from "node:fs/promises";
import path from "node:path";
import { Readable } from "node:stream";
import { fileURLToPath } from "node:url";
__NOXID_HANDLER_IMPORT__
__NOXID_ENDPOINT_MATCHER__
__NOXID_RESERVED_PATH_MATCHER__
const root = path.dirname(fileURLToPath(import.meta.url));
const realRoot = await fs.realpath(root);
const basePath = "__NOXID_BASE__";
const rawRequestPathname = (requestUrl) => {
const target = String(requestUrl).replace(/^[A-Za-z][A-Za-z0-9+.-]*:\/\/[^/]*/, "");
const end = target.search(/[?#]/);
return (end === -1 ? target : target.slice(0, end)) || "/";
};
const normalizedRequestPathname = (requestUrl) => {
let pathname;
try { pathname = decodeURIComponent(rawRequestPathname(requestUrl)); }
catch { return null; }
if (pathname.includes("%") || /[\u0000-\u001f\u007f-\u009f]/.test(pathname)) return null;
return path.posix.normalize(pathname.startsWith("/") ? pathname : `/${pathname}`);
};
const isAgentSurfacePath = (pathname) => {
const prefix = basePath === "/" ? "" : basePath.replace(/\/$/, "");
return pathname === `${prefix}/_noxid/openapi.json` || pathname === `${prefix}/_noxid/mcp`;
};
const isAgentSurfaceDoor = (pathname) => {
const candidate = pathname.length > 1 ? pathname.replace(/\/+$/, "") : pathname;
return isAgentSurfacePath(candidate) || candidate.endsWith("/_noxid/openapi.json") || candidate.endsWith("/_noxid/mcp");
};
// The two agent run doors are one family: `runs` starts a run and
// `runs/<id>/resume` continues one. Forwarding only the second left WO-31's
// whole runtime surface unreachable on a deployed build — a run start fell
// through to the SPA document. Matched by suffix for the same reason
// `isAgentSurfaceDoor` is: the front door forwards a superset so a based
// deployment is covered without the base being spelled twice, and
// `handleAgentRunRequest` requires the exact base-prefixed path, so the
// refusal happens inside where it can be structured.
const isAgentRunDoor = (pathname) => {
const candidate = pathname.length > 1 ? pathname.replace(/\/+$/, "") : pathname;
return /\/_noxid\/agents\/[^/]+\/runs(?:\/[^/]+\/resume)?$/.test(candidate);
};
const isReservedStaticPath = (pathname) => {
const prefix = basePath === "/" ? "" : basePath.replace(/\/$/, "");
const applicationPath = prefix && pathname.startsWith(`${prefix}/`) ? pathname.slice(prefix.length) : pathname;
return [pathname, applicationPath].some((candidate) => isNoxidReservedDeploymentPath(candidate));
};
const port = Number(process.env.PORT ?? 3000);
const shutdownTimeoutMs = __NOXID_SHUTDOWN_TIMEOUT_MS__;
const tracingFlushBudgetMs = Math.min(2000, Math.floor(shutdownTimeoutMs / 4));
const types = { ".html": "text/html; charset=utf-8", ".js": "text/javascript; charset=utf-8", ".css": "text/css; charset=utf-8", ".json": "application/json; charset=utf-8", ".txt": "text/plain; charset=utf-8", ".svg": "image/svg+xml", ".wasm": "application/wasm", ".png": "image/png", ".jpg": "image/jpeg", ".jpeg": "image/jpeg", ".webp": "image/webp", ".ico": "image/x-icon", ".woff2": "font/woff2" };
const inFlight = new Set();
const sseConnections = new Map();
let taskScheduler = null;
let queueWorker = null;
let shuttingDown = false;
__NOXID_TASK_SCHEDULER_START__
__NOXID_QUEUE_WORKER_START__
const handleRequest = async (request, response) => {
const pathname = normalizedRequestPathname(request.url);
if (pathname === null) {
response.writeHead(404).end("Not found"); return;
}
__NOXID_ACTION_BRANCH__
if (isReservedStaticPath(pathname)) {
response.writeHead(404).end("Not found"); return;
}
const candidate = path.resolve(root, `.${pathname}`);
if (!candidate.startsWith(`${root}${path.sep}`) && candidate !== root) {
response.writeHead(400).end("Bad request"); return;
}
let file = candidate;
try { if ((await fs.stat(file)).isDirectory()) file = path.join(file, "index.html"); }
catch { file = path.join(root, "__NOXID_FALLBACK__"); }
try { file = await fs.realpath(file); }
catch { response.writeHead(404).end("Not found"); return; }
if (!file.startsWith(`${realRoot}${path.sep}`) && file !== realRoot) {
response.writeHead(404).end("Not found"); return;
}
// Load-bearing, and not a duplicate of the check above. The upfront matcher
// sees what the caller *wrote*; this one sees what the filesystem actually
// opened. `fs` here is `node:fs/promises`, whose `realpath` is the native
// one, so on a case- or normalization-insensitive filesystem (APFS, NTFS) it
// folds `SERVER/HANDLER.JS`, `index.HTML`, and `\u017Ferver/handler.js` back
// to their true on-disk names before this line runs. U+017F is the reason
// this is not optional: JS `.toLowerCase()` does not fold it, so the upfront
// matcher cannot see it. The JS `fs.realpathSync` does not canonicalize case
// either — do not swap the import, and do not remove this second check.
const served = `/${path.relative(realRoot, file).split(path.sep).join("/")}`;
if (isReservedStaticPath(served) || isNoxidReservedDeploymentPath(served)) {
response.writeHead(404).end("Not found"); return;
}
try {
let body = await fs.readFile(file);
__NOXID_SSR_DOCUMENT__
response.writeHead(200, { "Content-Type": types[path.extname(file)] ?? "application/octet-stream", "X-Content-Type-Options": "nosniff" }).end(body);
} catch (cause) {
if (!response.headersSent) response.writeHead(404).end("Not found");
else response.destroy(cause instanceof Error ? cause : undefined);
}
};
const server = http.createServer((request, response) => {
let settle;
const completed = new Promise((resolve) => { settle = resolve; });
inFlight.add(completed);
const finish = () => { settle(); inFlight.delete(completed); };
response.once("finish", finish);
response.once("close", finish);
Promise.resolve(handleRequest(request, response)).catch((cause) => {
if (!response.headersSent) response.writeHead(500).end("Internal server error");
else response.destroy(cause instanceof Error ? cause : undefined);
});
});
const serverClosed = () => new Promise((resolve) => server.close(resolve));
const finishSseConnections = () => {
for (const [response, stream] of sseConnections) {
if (!response.writableEnded) {
response.write("retry: 1000\n\n");
response.end();
}
stream.destroy();
}
sseConnections.clear();
};
const settleBeforeShutdownDeadline = async (operation, deadline) => {
let timeout;
try {
return await Promise.race([
Promise.resolve().then(operation).then(() => true),
new Promise((resolve) => { timeout = setTimeout(() => resolve(false), Math.max(0, deadline - Date.now())); }),
]);
} finally {
clearTimeout(timeout);
}
};
const shutdown = async () => {
if (shuttingDown) {
process.exit(1);
return;
}
shuttingDown = true;
const shutdownDeadline = Date.now() + shutdownTimeoutMs;
const closed = serverClosed();
server.closeIdleConnections?.();
finishSseConnections();
const draining = Promise.allSettled([
Promise.resolve().then(() => taskScheduler?.stop?.()),
Promise.resolve().then(() => queueWorker?.stop?.()),
Promise.allSettled([...inFlight]),
closed,
]);
const drained = await settleBeforeShutdownDeadline(() => draining, shutdownDeadline);
let exitCode = drained ? 0 : 1;
try {
const tracingFlushDeadline = Math.min(shutdownDeadline, Date.now() + tracingFlushBudgetMs);
const tracingFlushed = await settleBeforeShutdownDeadline(flushNoxidTracing, tracingFlushDeadline);
if (!tracingFlushed) {
const dropped = abandonNoxidTracing();
console.error(JSON.stringify(Object.freeze({ schema: "noxid.tracing.error.v1", event: "tracing.flush.abandoned", code: "TRACING_FLUSH_DEADLINE_EXCEEDED", exporter: "otlp", dropped })));
}
}
catch (cause) {
console.error("Noxid tracing flush failed", cause);
}
try {
const databaseClosed = await settleBeforeShutdownDeadline(closeDatabase, shutdownDeadline);
if (!databaseClosed) {
exitCode = 1;
console.error("Noxid database shutdown exceeded the shutdown deadline");
}
}
catch (cause) {
exitCode = 1;
console.error("Noxid database shutdown failed", cause);
}
try {
const queueDatabaseClosed = await settleBeforeShutdownDeadline(closeQueueDatabase, shutdownDeadline);
if (!queueDatabaseClosed) {
exitCode = 1;
console.error("Noxid queue database shutdown exceeded the shutdown deadline");
}
}
catch (cause) {
exitCode = 1;
console.error("Noxid queue database shutdown failed", cause);
}
finally {
server.closeAllConnections?.();
process.exit(exitCode);
}
};
process.on("SIGTERM", shutdown);
process.on("SIGINT", shutdown);
server.listen(port, "0.0.0.0", () => console.log(`Noxid Node adapter listening on ${port}`));
"#;
const _NODE_BUFFERED_SSR_DOCUMENT: &str = r#"if (request.method === "GET" && String(request.headers.accept ?? "").includes("text/html")) {
const origin = "http://" + (request.headers.host ?? "localhost");
const baseDirectory = path.posix.dirname("__NOXID_FALLBACK__");
const basePrefix = baseDirectory === "." ? "" : "/" + baseDirectory;
const headers = new Headers(request.headers);
headers.set("content-type", "application/json");
const rendered = await handleNoxid(new Request(origin + basePrefix + "/_noxid/ssr", {
method: "POST",
headers,
body: JSON.stringify({ url: new URL(request.url, origin).href }),
}), process.env, Object.create(null));
const result = await rendered.json();
const escapeHtml = (value) => String(value).replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll("\"", """);
if (!rendered.ok) {
if (result?.error?.code !== "SSR_ROUTE_NOT_FOUND") {
if (result?.error?.code === "SSR_MIDDLEWARE_REDIRECT" && typeof result.redirect === "string") {
response.writeHead(307, { Location: result.redirect, "Cache-Control": "no-store", "X-Content-Type-Options": "nosniff" }).end();
return;
}
const code = typeof result?.error?.code === "string" ? result.error.code : "SSR_RENDER_FAILED";
const message = rendered.status === 403 ? "Access denied" : "Server rendering failed";
const errorDocument = "<!doctype html><html lang=\"en\"><head><meta charset=\"utf-8\"><title>" + escapeHtml(message) + "</title></head><body><main role=\"alert\" data-noxid-ssr-error=\"" + escapeHtml(code) + "\"><h1>" + escapeHtml(message) + "</h1></main></body></html>";
response.writeHead(rendered.status, { "Content-Type": "text/html; charset=utf-8", "Cache-Control": "no-store", "X-Content-Type-Options": "nosniff" }).end(errorDocument);
return;
}
} else if (result?.ok === true && typeof result.html === "string" && typeof result.payload === "string") {
let document = body.toString("utf8");
let payload;
try { payload = JSON.parse(result.payload); } catch { payload = null; }
if (payload) {
document = document.replace(/<div\s+id="?app"?\s*>(?:\s*<!--noxid-server-shell-start-->[\s\S]*?<!--noxid-server-shell-end-->\s*)?<\/div>/, "<div id=\"app\" data-noxid-ssr=\"" + escapeHtml(payload.routeId ?? "") + "\">" + result.html + "</div><script type=\"application/json\" id=\"__NOXID_SSR_PAYLOAD__\">" + result.payload + "</script>");
if (typeof result.head?.title === "string") document = document.replace(/<title>[\s\S]*?<\/title>/, "<title>" + escapeHtml(result.head.title) + "</title>");
const injectHead = (fragment) => { document = document.includes("</head>") ? document.replace("</head>", fragment + "\n</head>") : document.replace("</title>", "</title>" + fragment); };
if (typeof result.head?.description === "string") injectHead("<meta name=\"description\" data-noxid-route-description content=\"" + escapeHtml(result.head.description) + "\">");
const styles = Array.isArray(result.head?.styles) ? result.head.styles : [];
const links = styles.map((relative, index) => "<link rel=\"stylesheet\" href=\"" + escapeHtml(basePrefix + "/" + String(relative).replace(/^\/+/, "")) + "\" data-noxid-route-style=\"" + escapeHtml(payload.targets?.[index]?.component ?? "") + "\">").join("\n");
if (links) injectHead(links);
body = Buffer.from(document);
}
}
}"#;
const NODE_STREAMING_SSR_DOCUMENT: &str = r#"if (request.method === "GET" && String(request.headers.accept ?? "").includes("text/html")) {
const origin = "http://" + (request.headers.host ?? "localhost");
const baseDirectory = path.posix.dirname("__NOXID_FALLBACK__");
const basePrefix = baseDirectory === "." ? "" : "/" + baseDirectory;
const headers = new Headers(request.headers);
headers.set("content-type", "application/json");
const abort = new AbortController();
response.on("close", () => { if (!response.writableEnded) abort.abort(new DOMException("Client disconnected", "AbortError")); });
const rendered = await handleNoxid(new Request(origin + basePrefix + "/_noxid/ssr", {
method: "POST",
headers,
body: JSON.stringify({ url: new URL(request.url, origin).href, stream: true }),
signal: abort.signal,
}), process.env, Object.create(null));
const escapeHtml = (value) => String(value).replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll("\"", """);
const middlewareHeaderObject = {};
try {
const rawPairs = rendered.headers.get("x-noxid-ssr-headers");
const pairs = rawPairs ? JSON.parse(rawPairs) : [];
for (const pair of Array.isArray(pairs) ? pairs : []) {
if (!Array.isArray(pair) || typeof pair[0] !== "string" || typeof pair[1] !== "string") continue;
if (pair[0] === "set-cookie") (middlewareHeaderObject["set-cookie"] ??= []).push(pair[1]);
else middlewareHeaderObject[pair[0]] = middlewareHeaderObject[pair[0]] ? middlewareHeaderObject[pair[0]] + ", " + pair[1] : pair[1];
}
} catch {}
if (!rendered.ok) {
const result = await rendered.json();
if (result?.error?.code !== "SSR_ROUTE_NOT_FOUND") {
if (result?.error?.code === "SSR_MIDDLEWARE_RESPONSE" && result.respond && typeof result.respond.body === "string") {
response.writeHead(result.respond.status, { "Content-Type": result.respond.contentType + "; charset=utf-8", "Cache-Control": "no-store", "X-Content-Type-Options": "nosniff", ...middlewareHeaderObject }).end(result.respond.body);
return;
}
if (result?.error?.code === "SSR_MIDDLEWARE_REDIRECT" && typeof result.redirect === "string") {
response.writeHead(307, { Location: result.redirect, "Cache-Control": "no-store", "X-Content-Type-Options": "nosniff", ...middlewareHeaderObject }).end();
return;
}
const code = typeof result?.error?.code === "string" ? result.error.code : "SSR_RENDER_FAILED";
const message = rendered.status === 403 ? "Access denied" : "Server rendering failed";
const errorDocument = "<!doctype html><html lang=\"en\"><head><meta charset=\"utf-8\"><title>" + escapeHtml(message) + "</title></head><body><main role=\"alert\" data-noxid-ssr-error=\"" + escapeHtml(code) + "\"><h1>" + escapeHtml(message) + "</h1></main></body></html>";
response.writeHead(rendered.status, { "Content-Type": "text/html; charset=utf-8", "Cache-Control": "no-store", "X-Content-Type-Options": "nosniff", ...middlewareHeaderObject }).end(errorDocument);
return;
}
} else if (rendered.headers.get("x-noxid-ssr-stream") === "1" && rendered.body) {
const decoder = new TextDecoder();
let pending = "";
let suffix = "";
let opened = false;
let completed = false;
const consume = (event) => {
if (event?.schemaVersion !== 1 || typeof event.type !== "string") throw new Error("SSR_STREAM_EVENT_INVALID");
if (event.type === "shell") {
let document = body.toString("utf8");
if (typeof event.head?.title === "string") document = document.replace(/<title>[\s\S]*?<\/title>/, "<title>" + escapeHtml(event.head.title) + "</title>");
const injectHead = (fragment) => { document = document.includes("</head>") ? document.replace("</head>", fragment + "\n</head>") : document.replace("</title>", "</title>" + fragment); };
if (typeof event.head?.description === "string") injectHead("<meta name=\"description\" data-noxid-route-description content=\"" + escapeHtml(event.head.description) + "\">");
const styles = Array.isArray(event.head?.styles) ? event.head.styles : [];
const links = styles.map((relative, index) => "<link rel=\"stylesheet\" href=\"" + escapeHtml(basePrefix + "/" + String(relative).replace(/^\/+/, "")) + "\" data-noxid-route-style=\"" + escapeHtml(event.targets?.[index]?.component ?? "") + "\">").join("\n");
if (links) injectHead(links);
const marker = "__NOXID_STREAM_APP__";
document = document.replace(/<div\s+id=\"?app\"?\s*><\/div>/, marker);
const index = document.indexOf(marker);
if (index === -1) throw new Error("SSR_STREAM_APP_ROOT_MISSING");
suffix = document.slice(index + marker.length);
const cache = event.cache;
const cdnCache = !cache ? null : cache.mode === "swr"
? `public, s-maxage=${cache.revalidateSeconds}, stale-while-revalidate=${cache.staleSeconds}`
: `public, s-maxage=${cache.revalidateSeconds}, must-revalidate`;
response.writeHead(200, {
"Content-Type": "text/html; charset=utf-8",
"Cache-Control": cache ? "public, max-age=0, must-revalidate" : "no-store",
...(cdnCache ? { "CDN-Cache-Control": cdnCache, "X-Noxid-Cache-Mode": cache.mode } : {}),
...(cache?.vary?.some((value) => value.startsWith("header:")) ? { "Vary": cache.vary.filter((value) => value.startsWith("header:")).map((value) => value.slice(7)).join(", ") } : {}),
...(cache?.tags?.length ? { "Cache-Tag": cache.tags.join(",") } : {}),
"X-Content-Type-Options": "nosniff",
"X-Noxid-Ssr-Stream": "1",
...middlewareHeaderObject,
});
response.write(document.slice(0, index) + "<div id=\"app\" data-noxid-ssr=\"" + escapeHtml(event.routeId ?? "") + "\">");
opened = true;
} else if (event.type === "html" && opened) response.write(event.html ?? "");
else if (event.type === "payload" && opened) {
response.write("</div><script type=\"application/json\" id=\"__NOXID_SSR_PAYLOAD__\">" + String(event.payload ?? "") + "</script>" + suffix);
completed = true;
} else if (event.type === "error" && opened) {
const code = event.error?.code ?? "SSR_STREAM_FAILED";
response.write("<main role=\"alert\" data-noxid-ssr-error=\"" + escapeHtml(code) + "\"><h1>Server rendering failed</h1></main></div>" + suffix);
completed = true;
}
};
for await (const chunk of rendered.body) {
pending += decoder.decode(chunk, { stream: true });
let newline;
while ((newline = pending.indexOf("\n")) !== -1) {
const line = pending.slice(0, newline); pending = pending.slice(newline + 1);
if (line) consume(JSON.parse(line));
}
}
pending += decoder.decode();
if (pending.trim()) consume(JSON.parse(pending));
if (opened) {
if (!completed) response.write("<main role=\"alert\" data-noxid-ssr-error=\"SSR_STREAM_INCOMPLETE\"><h1>Server rendering failed</h1></main></div>" + suffix);
response.end();
return;
}
}
}"#;
const NODE_ACTION_BRANCH: &str = r#"if (pathname.includes("/_noxid/actions/") || pathname.includes("/_noxid/tasks/") || pathname.endsWith("/_noxid/revalidate") || isAgentRunDoor(pathname) || isAgentSurfaceDoor(pathname)__NOXID_ENDPOINT_ROUTE____NOXID_QUEUE_DRAIN_ROUTE__) {
const origin = `http://${request.headers.host ?? "localhost"}`;
const init = { method: request.method, headers: request.headers };
if (request.method !== "GET" && request.method !== "HEAD") { init.body = Readable.toWeb(request); init.duplex = "half"; }
const result = await handleNoxid(new Request(new URL(request.url, origin), init), process.env, __NOXID_QUEUE_DRAIN_CONTEXT__);
const resultHeaders = Object.fromEntries(result.headers);
const setCookies = result.headers.getSetCookie?.() ?? [];
if (setCookies.length) resultHeaders["set-cookie"] = setCookies;
response.writeHead(result.status, resultHeaders);
if (result.body && (result.headers.get("content-type") ?? "").startsWith("text/event-stream")) {
const stream = Readable.fromWeb(result.body);
sseConnections.set(response, stream);
response.once("close", () => { sseConnections.delete(response); stream.destroy(); });
stream.on("error", (cause) => response.destroy(cause));
stream.pipe(response);
} else {
response.end(Buffer.from(await result.arrayBuffer()));
}
return;
}"#;
const DENO_SERVER: &str = r#"__NOXID_HANDLER_IMPORT__
__NOXID_ENDPOINT_MATCHER__
__NOXID_RESERVED_PATH_MATCHER__
const root = new URL("./", import.meta.url);
const basePath = "__NOXID_BASE__";
const rawRequestPathname = (requestUrl) => {
const target = String(requestUrl).replace(/^[A-Za-z][A-Za-z0-9+.-]*:\/\/[^/]*/, "");
const end = target.search(/[?#]/);
return (end === -1 ? target : target.slice(0, end)) || "/";
};
const normalizePosixPath = (pathname) => {
const segments = [];
for (const segment of pathname.split("/")) {
if (!segment || segment === ".") continue;
if (segment === "..") segments.pop();
else segments.push(segment);
}
const trailingSlash = pathname.endsWith("/") && segments.length > 0 ? "/" : "";
return `/${segments.join("/")}${trailingSlash}`;
};
const normalizedRequestPathname = (requestUrl) => {
let pathname;
try { pathname = decodeURIComponent(rawRequestPathname(requestUrl)); }
catch { return null; }
if (pathname.includes("%") || /[\u0000-\u001f\u007f-\u009f]/.test(pathname)) return null;
return normalizePosixPath(pathname);
};
const isAgentSurfacePath = (pathname) => {
const prefix = basePath === "/" ? "" : basePath.replace(/\/$/, "");
return pathname === `${prefix}/_noxid/openapi.json` || pathname === `${prefix}/_noxid/mcp`;
};
const isAgentSurfaceDoor = (pathname) => {
const candidate = pathname.length > 1 ? pathname.replace(/\/+$/, "") : pathname;
return isAgentSurfacePath(candidate) || candidate.endsWith("/_noxid/openapi.json") || candidate.endsWith("/_noxid/mcp");
};
// The two agent run doors are one family: `runs` starts a run and
// `runs/<id>/resume` continues one. Forwarding only the second left WO-31's
// whole runtime surface unreachable on a deployed build — a run start fell
// through to the SPA document. Matched by suffix for the same reason
// `isAgentSurfaceDoor` is: the front door forwards a superset so a based
// deployment is covered without the base being spelled twice, and
// `handleAgentRunRequest` requires the exact base-prefixed path, so the
// refusal happens inside where it can be structured.
const isAgentRunDoor = (pathname) => {
const candidate = pathname.length > 1 ? pathname.replace(/\/+$/, "") : pathname;
return /\/_noxid\/agents\/[^/]+\/runs(?:\/[^/]+\/resume)?$/.test(candidate);
};
const isReservedStaticPath = (pathname) => {
const prefix = basePath === "/" ? "" : basePath.replace(/\/$/, "");
const applicationPath = prefix && pathname.startsWith(`${prefix}/`) ? pathname.slice(prefix.length) : pathname;
return [pathname, applicationPath].some((candidate) => isNoxidReservedDeploymentPath(candidate));
};
const fallback = "__NOXID_FALLBACK__";
const types = { ".html": "text/html; charset=utf-8", ".js": "text/javascript; charset=utf-8", ".css": "text/css; charset=utf-8", ".json": "application/json; charset=utf-8", ".txt": "text/plain; charset=utf-8", ".svg": "image/svg+xml", ".wasm": "application/wasm", ".png": "image/png", ".jpg": "image/jpeg", ".jpeg": "image/jpeg", ".webp": "image/webp", ".ico": "image/x-icon", ".woff2": "font/woff2" };
const noxidDenoServer = Deno.serve({ port: Number(Deno.env.get("PORT") ?? 3000), hostname: "0.0.0.0" }, async (request) => {
const pathname = normalizedRequestPathname(request.url);
if (pathname === null) return new Response("Not found", { status: 404 });
__NOXID_ACTION_BRANCH__
if (isReservedStaticPath(pathname)) return new Response("Not found", { status: 404 });
const relative = pathname.replace(/^\/+/, "");
if (relative.split("/").includes("..")) return new Response("Bad request", { status: 400 });
if (relative.includes("%") || isNoxidReservedDeploymentPath(relative)) return new Response("Not found", { status: 404 });
let file = relative || "index.html";
try {
const stat = await Deno.stat(new URL(file, root));
if (stat.isDirectory) file = `${file.replace(/\/$/, "")}/index.html`;
} catch { file = fallback; }
try {
const rootPath = (await Deno.realPath(root)).replace(/[\\/]+$/, "");
const filePath = await Deno.realPath(new URL(file, root));
if (filePath !== rootPath && !filePath.startsWith(`${rootPath}/`) && !filePath.startsWith(`${rootPath}\\`)) return new Response("Not found", { status: 404 });
// The same load-bearing re-check as the Node handler's: `Deno.realPath` is
// Rust `std::fs::canonicalize`, i.e. the same `realpath(3)`, so this is
// where a case- or normalization-folded spelling (`SERVER`, U+017F `\u017Ferver`)
// becomes its true on-disk name. The matcher above sees only what the
// caller wrote. Do not remove it.
const served = `/${filePath.slice(rootPath.length).replaceAll("\\", "/").replace(/^\/+/, "")}`;
if (isReservedStaticPath(served)) return new Response("Not found", { status: 404 });
const body = await Deno.readFile(filePath);
const extension = file.includes(".") ? `.${file.split(".").pop()}` : "";
return new Response(body, { headers: { "content-type": types[extension] ?? "application/octet-stream", "x-content-type-options": "nosniff" } });
} catch { return new Response("Not found", { status: 404 }); }
});
__NOXID_DENO_SHUTDOWN_RUNTIME__
"#;
const DENO_ACTION_BRANCH: &str = r#"if (pathname.includes("/_noxid/actions/") || pathname.includes("/_noxid/tasks/") || pathname.endsWith("/_noxid/revalidate") || isAgentRunDoor(pathname) || isAgentSurfaceDoor(pathname)__NOXID_ENDPOINT_ROUTE__) return handleNoxid(request, Deno.env.toObject(), Object.create(null));"#;
#[cfg(test)]
mod tests {
use super::*;
use std::io::Write;
use std::net::{TcpListener, TcpStream};
use std::process::{Command, Stdio};
use std::thread;
use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
fn test_build() -> project::ProjectBuild {
project::ProjectBuild {
routes: 1,
endpoints: 1,
endpoint_paths: vec!["/api/probe".into()],
tasks: 0,
queues: 0,
queue_worker: false,
live_resources: 0,
presences: 0,
api_docs: false,
mcp: false,
components: 1,
middleware: 0,
route_loaders: 0,
ssr_routes: 0,
server_shell_routes: 0,
prerender_routes: 0,
prerender_entries: 0,
isr_routes: 0,
swr_routes: 0,
assets: 3,
compiled_targets: 1,
reused_targets: 0,
server_actions: 0,
edge_actions: 0,
worker_actions: 0,
external_browser_modules: 0,
native_esm_eligible: false,
persistent_cache_hit: false,
}
}
fn selected_driver(database_url: &str) -> Option<NodeDatabaseDriver> {
node_database_driver(
&BTreeMap::from([("DATABASE_URL".into(), database_url.into())]),
&test_build(),
)
}
#[test]
fn deno_templates_gate_the_request_handler_before_startup() {
let static_source = DENO_SERVER.replace(
"__NOXID_HANDLER_IMPORT__",
&deno_handler_import("/", noxid_codegen_server_js::ServerTracingExport::Stdout),
);
let mut dynamic_build = test_build();
dynamic_build.ssr_routes = 1;
let dynamic_source = provider_runtime(
"<!doctype html><main>shell</main>",
"/",
"deno",
DENO_PROVIDER_EXPORT,
noxid_codegen_server_js::ServerTracingExport::Stdout,
false,
0,
&dynamic_build,
);
for (label, source) in [("static", static_source), ("dynamic", dynamic_source)] {
let gate = source
.find("requireNoxidLifecycleExport(\"fetch\", handleNoxid, \"request handling\");")
.unwrap_or_else(|| {
panic!("{label} Deno template omitted the fetch gate:\n{source}")
});
let startup = source
.find("Deno.serve")
.unwrap_or_else(|| panic!("{label} Deno template omitted startup:\n{source}"));
assert!(
source.contains("error[SERVER_LIFECYCLE_EXPORT_MISSING]")
&& source.contains("missing required export \"${name}\"")
&& source.contains("rerun \"noxid adapt\" with the same Noxid compiler"),
"{label} Deno template omitted the teaching lifecycle diagnostic:\n{source}"
);
assert!(
gate < startup,
"{label} Deno template gates fetch only after startup:\n{source}"
);
}
}
#[test]
fn netlify_scheduled_templates_gate_the_request_handler_at_load() {
let prelude =
provider_handler_prelude(noxid_codegen_server_js::ServerTracingExport::Stdout);
for (label, template) in [
("scheduled task", NETLIFY_TASK_EXPORT),
("queue drain", NETLIFY_QUEUE_DRAIN_EXPORT),
] {
let source = template.replace("__NOXID_HANDLER_PRELUDE__", &prelude);
let gate = source
.find("requireNoxidLifecycleExport(\"fetch\", handleNoxid, \"request handling\");")
.unwrap_or_else(|| {
panic!("Netlify {label} template omitted the fetch gate:\n{source}")
});
let export = source.find("export default").unwrap_or_else(|| {
panic!("Netlify {label} template omitted its export:\n{source}")
});
assert!(
source.contains("error[SERVER_LIFECYCLE_EXPORT_MISSING]")
&& source.contains("missing required export \"${name}\"")
&& source.contains("rerun \"noxid adapt\" with the same Noxid compiler"),
"Netlify {label} template omitted the teaching lifecycle diagnostic:\n{source}"
);
assert!(
gate < export,
"Netlify {label} template gates fetch only after exporting its invocation entry point:\n{source}"
);
}
}
#[test]
fn every_front_door_forwards_both_agent_run_doors_through_one_matcher() {
let templates = [
("PROVIDER_RUNTIME", PROVIDER_RUNTIME),
("DENO_PROVIDER_EXPORT", DENO_PROVIDER_EXPORT),
("CLOUDFLARE_PROVIDER_EXPORT", CLOUDFLARE_PROVIDER_EXPORT),
("NODE_ACTION_BRANCH", NODE_ACTION_BRANCH),
("DENO_ACTION_BRANCH", DENO_ACTION_BRANCH),
];
for (label, template) in templates {
assert!(
template.contains("isAgentRunDoor(pathname)"),
"{label} does not forward the agent run doors through the shared matcher"
);
}
for (label, template) in [
("PROVIDER_RUNTIME", PROVIDER_RUNTIME),
("NODE_SERVER", NODE_SERVER),
("DENO_SERVER", DENO_SERVER),
("DENO_PROVIDER_EXPORT", DENO_PROVIDER_EXPORT),
("CLOUDFLARE_PROVIDER_EXPORT", CLOUDFLARE_PROVIDER_EXPORT),
("NODE_ACTION_BRANCH", NODE_ACTION_BRANCH),
("DENO_ACTION_BRANCH", DENO_ACTION_BRANCH),
] {
assert!(
!template.contains("runs\\/[^/]+\\/resume$/"),
"{label} still carries a resume-only agent regex beside the shared matcher"
);
}
let definition = |template: &str| {
let (_, tail) = template
.split_once("const isAgentRunDoor = (pathname) => {")
.expect("template defines the run-door matcher");
let (body, _) = tail.split_once("};").expect("matcher body is closed");
body.to_string()
};
let node = definition(NODE_SERVER);
assert_eq!(node, definition(DENO_SERVER));
assert_eq!(node, definition(PROVIDER_RUNTIME));
}
#[test]
fn compiler_owned_path_set_drives_publish_and_static_refusals() {
for path in [
"server",
"Server",
"SERVER",
"sErVeR",
"public",
"netlify",
".vercel",
"api-contract.json",
"api.openapi.json",
"deployment.plan.json",
"security.manifest.json",
"app.routes.json",
"app.manifest.json",
"server.mjs",
"SERVER.MJS",
"server.ts",
"package.json",
"Package.JSON",
"deno.json",
"pnpm-lock.yaml",
] {
assert!(
is_reserved_deployment_path_segment(path),
"compiler-owned path `{path}` escaped route validation"
);
assert!(
!is_provider_public_path(Path::new(path)),
"compiler-owned path `{path}` escaped the provider-public filter"
);
}
for path in [
"index.html",
"assets",
"docs/server",
"docs/app.routes.json",
"docs/server.mjs",
"docs/package.json",
"server.js",
"packages.json",
] {
assert!(
is_provider_public_path(Path::new(path)),
"public path `{path}` was mistaken for a compiler-owned root"
);
}
let javascript = reserved_deployment_path_javascript();
for path in [
"server",
"public",
"netlify",
".vercel",
"api-contract.json",
"api.openapi.json",
"deployment.plan.json",
"security.manifest.json",
"server.mjs",
"server.ts",
"package.json",
"deno.json",
"pnpm-lock.yaml",
] {
assert!(
javascript.contains(&format!("\"{path}\"")),
"static-handler matcher omitted compiler-owned path `{path}`:\n{javascript}"
);
}
assert!(
javascript.contains("first.startsWith(\"app.\")")
&& javascript.contains("first.endsWith(\".json\")"),
"static-handler matcher omitted compiler-owned app metadata:\n{javascript}"
);
assert!(
javascript.contains(".normalize(\"NFC\")") && javascript.contains(".toLowerCase()"),
"static-handler matcher does not normalize and case-fold paths:\n{javascript}"
);
}
#[test]
fn deno_shutdown_abandons_a_stalled_tracing_flush_within_its_sub_budget() {
let mut source = DENO_SERVER
.replace(
"__NOXID_HANDLER_IMPORT__",
r#"let abandonCalls = 0;
const errors = [];
console.error = (value) => errors.push(String(value));
const handleNoxid = async () => new Response("ok");
const flushNoxidTracing = async () => new Promise(() => {});
const abandonNoxidTracing = () => { abandonCalls += 1; return 7; };
globalThis.Deno = {
env: { get: () => null, toObject: () => Object.create(null) },
serve: () => ({ shutdown: async () => {} }),
addSignalListener: () => {},
stat: async () => ({ isDirectory: false }),
readFile: async () => new Uint8Array(),
};"#,
)
.replace(
"__NOXID_ENDPOINT_MATCHER__",
"function matchesNoxidEndpointPath() { return false; }",
)
.replace(
"__NOXID_RESERVED_PATH_MATCHER__",
&reserved_deployment_path_javascript(),
)
.replace("__NOXID_ACTION_BRANCH__", "")
.replace("__NOXID_FALLBACK__", "index.html")
.replace("__NOXID_DENO_SHUTDOWN_RUNTIME__", DENO_SHUTDOWN_RUNTIME)
.replace("__NOXID_SHUTDOWN_TIMEOUT_MS__", "400");
source.push_str(
r#"
const startedAt = Date.now();
await shutdownNoxidDeno();
const elapsedMs = Date.now() - startedAt;
if (elapsedMs < 80 || elapsedMs > 1000) throw new Error(`Deno tracing deadline was not bounded: ${elapsedMs}`);
if (abandonCalls !== 1) throw new Error(`Deno tracing abandonment count was ${abandonCalls}`);
const abandoned = errors.map((value) => { try { return JSON.parse(value); } catch { return null; } }).find((value) => value?.code === "TRACING_FLUSH_DEADLINE_EXCEEDED");
if (abandoned?.dropped !== 7) throw new Error(`Deno tracing abandonment record was ${JSON.stringify(abandoned)}`);
"#,
);
let output = Command::new("node")
.args(["--input-type=module", "--eval", &source])
.output()
.expect("run the Deno shutdown template under Node's Deno stub");
assert!(
output.status.success(),
"Deno shutdown template failed:\nstdout:\n{}\nstderr:\n{}",
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr),
);
}
#[test]
fn compiler_owned_hidden_http_surfaces_have_exact_deployment_patterns() {
let mut build = test_build();
build.endpoint_paths.clear();
build.endpoints = 0;
build.live_resources = 1;
build.presences = 1;
build.api_docs = true;
build.mcp = true;
let matcher = endpoint_matcher_javascript(&build, "/console");
for expected in [
"Object.freeze([\"console\", \"_noxid\", \"live\"])",
"Object.freeze([\"console\", \"_noxid\", \"presence\"])",
"Object.freeze([\"_noxid\", \"openapi.json\"])",
"Object.freeze([\"_noxid\", \"mcp\"])",
] {
assert!(matcher.contains(expected), "missing {expected}:\n{matcher}");
}
assert!(!matcher.contains("assets"), "{matcher}");
}
#[test]
fn node_package_pins_selected_postgres_driver() {
let package = node_package_json(selected_driver("postgres://database/app"));
assert!(package.contains("\"postgres\": \"3.4.9\""));
assert!(!package.contains("mysql2"));
}
#[test]
fn node_package_pins_selected_mysql_driver() {
let package = node_package_json(selected_driver("mysql://database/app"));
assert!(package.contains("\"mysql2\": \"3.23.4\""));
assert!(!package.contains("\"postgres\""));
}
#[test]
fn node_package_uses_builtin_selected_sqlite_driver() {
let package = node_package_json(selected_driver("sqlite://data/app.db"));
assert!(!package.contains("\"dependencies\""));
assert!(!package.contains("postgres"));
assert!(!package.contains("mysql2"));
}
#[cfg(unix)]
#[test]
fn node_shutdown_deadline_closes_database_and_exits_nonzero() {
let nonce = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_nanos();
let root = env::temp_dir().join(format!(
"noxid-node-shutdown-{}-{nonce}",
std::process::id()
));
fs::create_dir_all(root.join("server")).unwrap();
fs::write(root.join("index.html"), "<!doctype html><p>fallback</p>").unwrap();
let request_marker = root.join("request-started");
let close_marker = root.join("database-closed");
let queue_close_marker = root.join("queue-database-closed");
fs::write(
root.join("server/host.js"),
r#"import { existsSync, writeFileSync } from "node:fs";
export async function closeDatabase() { writeFileSync(process.env.NOXID_CLOSE_MARKER, "closed"); }
export async function closeQueueDatabase() {
if (!process.env.NOXID_CLOSE_MARKER || !process.env.NOXID_QUEUE_CLOSE_MARKER) throw new Error("missing close markers");
writeFileSync(process.env.NOXID_QUEUE_CLOSE_MARKER, String(existsSync(process.env.NOXID_CLOSE_MARKER)));
}
"#,
)
.unwrap();
fs::write(
root.join("server/handler.js"),
r#"import { writeFileSync } from "node:fs";
import { closeDatabase, closeQueueDatabase } from "./host.js";
export { closeDatabase, closeQueueDatabase };
globalThis.__NOXID_FETCH_HANDLER__ = async () => {
writeFileSync(process.env.NOXID_REQUEST_MARKER, "started");
await new Promise(() => {});
};
"#,
)
.unwrap();
emit_adapter_files(
AdapterTarget::Node,
&root,
"/",
&test_build(),
&[],
AdapterEmissionConfig {
vercel_max_duration: 30,
queue_drain: false,
queue_drain_budget_ms: 25_000,
shutdown_timeout_ms: 100,
tracing_export: noxid_codegen_server_js::ServerTracingExport::Stdout,
node_database_driver: Some(NodeDatabaseDriver::Sqlite),
},
)
.unwrap();
let listener = TcpListener::bind("127.0.0.1:0").unwrap();
let port = listener.local_addr().unwrap().port();
drop(listener);
let mut child = Command::new("node")
.arg("server.mjs")
.current_dir(&root)
.env("PORT", port.to_string())
.env("NOXID_REQUEST_MARKER", &request_marker)
.env("NOXID_CLOSE_MARKER", &close_marker)
.env("NOXID_QUEUE_CLOSE_MARKER", &queue_close_marker)
.stdout(Stdio::null())
.stderr(Stdio::piped())
.spawn()
.unwrap();
let deadline = Instant::now() + Duration::from_secs(5);
let mut stream = loop {
match TcpStream::connect(("127.0.0.1", port)) {
Ok(stream) => break stream,
Err(_) if Instant::now() < deadline => thread::sleep(Duration::from_millis(20)),
Err(error) => {
let _ = child.kill();
panic!("emitted Node server did not start: {error}");
}
}
};
stream
.write_all(b"GET /_noxid/actions/Slow HTTP/1.1\r\nHost: localhost\r\nConnection: keep-alive\r\n\r\n")
.unwrap();
while !request_marker.is_file() && Instant::now() < deadline {
thread::sleep(Duration::from_millis(10));
}
if !request_marker.is_file() {
let _ = child.kill();
panic!("slow request never entered the emitted handler");
}
let signal = Command::new("kill")
.args(["-TERM", &child.id().to_string()])
.status()
.unwrap();
assert!(signal.success());
let status = child.wait().unwrap();
assert_eq!(status.code(), Some(1), "shutdown deadline must fail closed");
assert_eq!(fs::read_to_string(&close_marker).unwrap(), "closed");
assert_eq!(
fs::read_to_string(&queue_close_marker).unwrap(),
"true",
"the queue database must close after the application database"
);
let _ = fs::remove_dir_all(root);
}
#[test]
fn auto_detection_is_deterministic_and_rejects_ambiguity() {
assert_eq!(
detect_adapter(&BTreeMap::new()).unwrap().0,
AdapterTarget::Static
);
let cloudflare = BTreeMap::from([("CF_PAGES".into(), "1".into())]);
assert_eq!(
detect_adapter(&cloudflare).unwrap(),
(AdapterTarget::Cloudflare, Some("CF_PAGES".into()))
);
let railway = BTreeMap::from([("RAILWAY_ENVIRONMENT".into(), "production".into())]);
assert_eq!(
detect_adapter(&railway).unwrap(),
(AdapterTarget::Node, Some("RAILWAY_ENVIRONMENT".into()))
);
let railway_with_node = BTreeMap::from([
("NOXID_NODE".into(), "1".into()),
("RAILWAY_ENVIRONMENT".into(), "production".into()),
]);
assert_eq!(
detect_adapter(&railway_with_node).unwrap(),
(AdapterTarget::Node, Some("RAILWAY_ENVIRONMENT".into()))
);
let ambiguous = BTreeMap::from([
("CF_PAGES".into(), "1".into()),
("VERCEL".into(), "1".into()),
]);
assert!(
detect_adapter(&ambiguous)
.unwrap_err()
.contains("AMBIGUOUS")
);
}
#[test]
fn adapters_reject_execution_targets_they_cannot_host() {
assert!(validate_adapter(AdapterTarget::Node, 1, 0, 0, 1).is_ok());
assert!(validate_adapter(AdapterTarget::Deno, 1, 0, 0, 0).is_ok());
assert!(
validate_adapter(AdapterTarget::Static, 1, 0, 0, 0)
.unwrap_err()
.contains("ADAPTER_EXECUTION_UNSUPPORTED")
);
assert!(validate_adapter(AdapterTarget::Cloudflare, 0, 1, 0, 0).is_ok());
assert!(validate_adapter(AdapterTarget::Cloudflare, 0, 0, 1, 0).is_ok());
assert!(validate_adapter(AdapterTarget::Static, 0, 0, 1, 0).is_err());
assert!(validate_adapter(AdapterTarget::Node, 0, 1, 0, 0).is_err());
assert!(validate_adapter(AdapterTarget::Vercel, 1, 0, 0, 1).is_ok());
assert!(validate_adapter(AdapterTarget::Netlify, 1, 0, 0, 1).is_ok());
for target in [
AdapterTarget::Static,
AdapterTarget::Cloudflare,
AdapterTarget::Deno,
] {
assert!(
validate_adapter(target, 1, 0, 0, 1)
.unwrap_err()
.contains("ADAPTER_TASK_SCHEDULING_UNSUPPORTED")
);
}
}
#[test]
fn provider_adapters_emit_streaming_functions_and_cache_primitives() {
let root = std::env::temp_dir().join(format!(
"noxid-provider-adapters-{}-{}",
std::process::id(),
std::thread::current().name().unwrap_or("test")
));
let public = root.join("console");
fs::create_dir_all(public.join("server")).unwrap();
fs::write(
public.join("index.html"),
"<!doctype html><html><head><title>Noxid</title></head><body><div id=\"app\"></div></body></html>",
)
.unwrap();
fs::write(public.join("app.js"), "export {};\n").unwrap();
fs::write(public.join("server/handler.js"), "export {};\n").unwrap();
let build = project::ProjectBuild {
routes: 2,
endpoints: 0,
endpoint_paths: Vec::new(),
tasks: 1,
queues: 1,
queue_worker: true,
live_resources: 0,
presences: 0,
api_docs: false,
mcp: false,
components: 1,
middleware: 0,
route_loaders: 0,
ssr_routes: 2,
server_shell_routes: 0,
prerender_routes: 0,
prerender_entries: 0,
isr_routes: 1,
swr_routes: 1,
assets: 3,
compiled_targets: 1,
reused_targets: 0,
server_actions: 1,
edge_actions: 0,
worker_actions: 0,
external_browser_modules: 0,
native_esm_eligible: false,
persistent_cache_hit: false,
};
let task_schedules = [
("HourlySweep".into(), "0 * * * *".into()),
("NightlyCleanup".into(), "0 3 * * *".into()),
];
let serverless_build = project::ProjectBuild {
queue_worker: false,
..build.clone()
};
for target in [
AdapterTarget::Static,
AdapterTarget::Cloudflare,
AdapterTarget::Deno,
] {
assert!(
validate_build_adapter(target, &serverless_build)
.unwrap_err()
.contains("ADAPTER_QUEUE_UNSUPPORTED")
);
}
for target in [AdapterTarget::Vercel, AdapterTarget::Netlify] {
assert!(
validate_build_adapter(target, &build)
.unwrap_err()
.contains("SERVERLESS_QUEUE_WORKER_UNSUPPORTED")
);
assert!(validate_build_adapter(target, &serverless_build).is_ok());
}
assert!(validate_build_adapter(AdapterTarget::Node, &build).is_ok());
assert_eq!(
provider_function_count(AdapterTarget::Netlify, &serverless_build, 2),
4
);
assert_eq!(
provider_function_count(AdapterTarget::Vercel, &serverless_build, 2),
1
);
let nonqueue_build = project::ProjectBuild {
tasks: 0,
queues: 0,
queue_worker: false,
..build.clone()
};
emit_adapter_files(
AdapterTarget::Vercel,
&root,
"/console",
&serverless_build,
&task_schedules,
AdapterEmissionConfig {
vercel_max_duration: 121,
queue_drain: false,
queue_drain_budget_ms: 17_000,
shutdown_timeout_ms: 20_000,
tracing_export: noxid_codegen_server_js::ServerTracingExport::Stdout,
node_database_driver: Some(NodeDatabaseDriver::Postgres),
},
)
.unwrap();
emit_adapter_files(
AdapterTarget::Netlify,
&root,
"/console",
&serverless_build,
&task_schedules,
AdapterEmissionConfig {
vercel_max_duration: 30,
queue_drain: false,
queue_drain_budget_ms: 17_000,
shutdown_timeout_ms: 20_000,
tracing_export: noxid_codegen_server_js::ServerTracingExport::Stdout,
node_database_driver: Some(NodeDatabaseDriver::Postgres),
},
)
.unwrap();
emit_adapter_files(
AdapterTarget::Deno,
&root,
"/console",
&nonqueue_build,
&[],
AdapterEmissionConfig {
vercel_max_duration: 30,
queue_drain: false,
queue_drain_budget_ms: 25_000,
shutdown_timeout_ms: 20_000,
tracing_export: noxid_codegen_server_js::ServerTracingExport::Stdout,
node_database_driver: Some(NodeDatabaseDriver::Postgres),
},
)
.unwrap();
emit_adapter_files(
AdapterTarget::Node,
&root,
"/console",
&build,
&[],
AdapterEmissionConfig {
vercel_max_duration: 30,
queue_drain: true,
queue_drain_budget_ms: 17_000,
shutdown_timeout_ms: 20_000,
tracing_export: noxid_codegen_server_js::ServerTracingExport::Stdout,
node_database_driver: Some(NodeDatabaseDriver::Postgres),
},
)
.unwrap();
let edge_build = project::ProjectBuild {
server_actions: 0,
edge_actions: 1,
..nonqueue_build
};
emit_adapter_files(
AdapterTarget::Cloudflare,
&root,
"/console",
&edge_build,
&[],
AdapterEmissionConfig {
vercel_max_duration: 30,
queue_drain: false,
queue_drain_budget_ms: 25_000,
shutdown_timeout_ms: 20_000,
tracing_export: noxid_codegen_server_js::ServerTracingExport::Stdout,
node_database_driver: Some(NodeDatabaseDriver::Postgres),
},
)
.unwrap();
let vercel =
fs::read_to_string(root.join(".vercel/output/functions/noxid.func/index.mjs")).unwrap();
let config = fs::read_to_string(root.join(".vercel/output/config.json")).unwrap();
let vercel_function_config =
fs::read_to_string(root.join(".vercel/output/functions/noxid.func/.vc-config.json"))
.unwrap();
let netlify = fs::read_to_string(root.join("netlify/functions/noxid/index.mjs")).unwrap();
let netlify_config = fs::read_to_string(root.join("netlify.toml")).unwrap();
let netlify_task =
fs::read_to_string(root.join("netlify/functions/noxid-task-0/index.mjs")).unwrap();
let netlify_second_task =
fs::read_to_string(root.join("netlify/functions/noxid-task-1/index.mjs")).unwrap();
let netlify_drain =
fs::read_to_string(root.join("netlify/functions/noxid-queue-drain/index.mjs")).unwrap();
let deno = fs::read_to_string(root.join("server.ts")).unwrap();
let cloudflare = fs::read_to_string(root.join("_worker.js")).unwrap();
let node = fs::read_to_string(root.join("server.mjs")).unwrap();
assert!(vercel.contains("Readable.fromWeb(result.body)"));
assert!(config.contains("\"version\": 3"));
assert!(config.contains("\"dest\":\"/noxid\""));
assert!(config.contains(
"{\"path\":\"/console/_noxid/tasks/HourlySweep\",\"schedule\":\"0 * * * *\"}"
));
assert!(config.contains(
"{\"path\":\"/console/_noxid/tasks/NightlyCleanup\",\"schedule\":\"0 3 * * *\"}"
));
assert!(
config
.contains("{\"path\":\"/console/_noxid/queue/drain\",\"schedule\":\"* * * * *\"}")
);
assert!(vercel_function_config.contains("\"maxDuration\":121"));
assert!(vercel.contains("vercel-cron/1.0") && vercel.contains("method = cron ? \"POST\""));
assert!(netlify.contains("Netlify-CDN-Cache-Control"));
assert!(netlify.contains("stale-while-revalidate"));
assert!(netlify.contains("noxidQueueDrain: true"));
assert!(netlify_config.contains("[functions.\"noxid-task-0\"]"));
assert!(netlify_config.contains("[functions.\"noxid-task-1\"]"));
assert!(netlify_config.contains("schedule = \"0 * * * *\""));
assert!(netlify_config.contains("schedule = \"0 3 * * *\""));
assert!(netlify_config.contains("[functions.\"noxid-queue-drain\"]"));
assert!(netlify_task.contains("/console/_noxid/tasks/HourlySweep"));
assert!(netlify_task.contains("method: \"POST\""));
assert!(netlify_second_task.contains("/console/_noxid/tasks/NightlyCleanup"));
assert!(netlify_drain.contains("/console/_noxid/queue/drain"));
assert!(netlify_drain.contains("queueDrainBudgetMs: 17000"));
assert!(deno.contains("Deno.serve"));
assert!(deno.contains("X-Noxid-Ssr-Stream"));
assert!(deno.contains("Deno.addSignalListener"));
assert!(deno.contains("const noxidDenoShutdownTimeoutMs = 20000;"));
assert!(deno.contains(
"const noxidDenoTracingFlushBudgetMs = Math.min(2000, Math.floor(noxidDenoShutdownTimeoutMs / 4));"
));
assert!(
deno.contains("settleNoxidDenoBeforeDeadline(flushNoxidTracing, tracingFlushDeadline)")
);
assert!(deno.contains("const dropped = abandonNoxidTracing();"));
assert!(deno.contains("code: \"TRACING_FLUSH_DEADLINE_EXCEEDED\""));
assert!(!deno.contains("node:stream"));
assert!(cloudflare.contains("environment.ASSETS.fetch"));
assert!(cloudflare.contains("handleProviderRequest"));
assert!(cloudflare.contains("executionContext.waitUntil(flushNoxidTracing())"));
assert!(!cloudflare.contains("node:stream"));
assert!(
node.contains("taskScheduler = startTaskScheduler(process.env, Object.create(null));")
);
assert!(node.contains("queueWorker = startQueueWorker();"));
assert!(node.contains("const shutdownTimeoutMs = 20000;"));
assert!(node.contains(
"const tracingFlushBudgetMs = Math.min(2000, Math.floor(shutdownTimeoutMs / 4));"
));
assert!(node.contains("process.on(\"SIGTERM\", shutdown)"));
let tracing_flush = node
.find("settleBeforeShutdownDeadline(flushNoxidTracing")
.expect("Node shutdown must await tracing");
let database_close = node
.find("settleBeforeShutdownDeadline(closeDatabase")
.expect("Node shutdown must close the database");
assert!(tracing_flush < database_close);
assert!(node.contains(
"const tracingFlushDeadline = Math.min(shutdownDeadline, Date.now() + tracingFlushBudgetMs);"
));
assert!(node.contains("const dropped = abandonNoxidTracing();"));
assert!(node.contains("code: \"TRACING_FLUSH_DEADLINE_EXCEEDED\""));
assert!(node.contains("if (shuttingDown) {\n process.exit(1);"));
assert!(node.contains("response.write(\"retry: 1000\\n\\n\")"));
assert!(node.contains("pathname.endsWith(\"/_noxid/queue/drain\")"));
assert!(node.contains("queueDrainBudgetMs: 17000"));
}
}