oj_js 0.2.16

Embedded JavaScript engine: Deno runtime with Node compatibility over the app's node_modules
// SPDX-License-Identifier: MIT
// Copyright (c) 2026 Raphael Amorim

//! Assembles a deno_runtime `MainWorker` with Node compatibility over the
//! app's existing node_modules (byonm), booted from the prebuilt
//! `deno_snapshots::CLI_SNAPSHOT` so startup never transpiles internal TS.

use std::path::Path;
use std::path::PathBuf;
use std::rc::Rc;
use std::sync::Arc;

use deno_config::deno_json::NodeModulesDirMode;
use deno_core::url::Url;
use deno_core::v8;
use deno_core::CompiledWasmModuleStore;
use deno_core::SharedArrayBufferStore;
use deno_resolver::cjs::IsCjsResolutionMode;
use deno_resolver::factory::ResolverFactory;
use deno_resolver::factory::ResolverFactoryOptions;
use deno_resolver::factory::WorkspaceFactory;
use deno_resolver::factory::WorkspaceFactoryOptions;
use deno_resolver::npm::DenoInNpmPackageChecker;
use deno_resolver::npm::NpmResolver;
use deno_runtime::deno_fs::RealFs;
use deno_runtime::deno_node::NodeExtInitServices;
use deno_runtime::deno_permissions::Permissions;
use deno_runtime::deno_permissions::PermissionsContainer;
use deno_runtime::deno_web::BlobStore;
use deno_runtime::deno_web::InMemoryBroadcastChannel;
use deno_runtime::permissions::RuntimePermissionDescriptorParser;
use deno_runtime::worker::MainWorker;
use deno_runtime::worker::WorkerOptions;
use deno_runtime::worker::WorkerServiceOptions;
use deno_runtime::BootstrapOptions;
use deno_runtime::WorkerExecutionMode;

use crate::code_cache::FsCodeCache;
use crate::host::ModuleHost;
use crate::loader::EngineModuleLoader;
use crate::loader::EngineRequireLoader;
use crate::loader::Sys;
use crate::EngineConfig;
use crate::EngineError;

fn boot(e: impl std::fmt::Display) -> EngineError {
    EngineError::Boot(e.to_string())
}

/// The byonm resolution stack every isolate gets. Built once for the main
/// worker and rebuilt per worker thread from this ONE recipe, so main-thread
/// and worker resolution semantics can never drift apart.
struct EngineLoaders {
    module_loader: Rc<EngineModuleLoader>,
    node_services: NodeExtInitServices<DenoInNpmPackageChecker, NpmResolver<Sys>, Sys>,
    code_cache: Option<Arc<FsCodeCache>>,
    sys: Sys,
}

fn engine_loaders(
    root: &Path,
    code_cache_dir: Option<PathBuf>,
    host: Option<ModuleHost>,
) -> Result<EngineLoaders, EngineError> {
    let sys = Sys::default();

    let code_cache = code_cache_dir.map(|dir| Arc::new(FsCodeCache::new(dir)));

    let workspace_factory = Arc::new(WorkspaceFactory::new(
        sys.clone(),
        root.to_path_buf(),
        WorkspaceFactoryOptions {
            // byonm: resolve npm packages from the node_modules directory the
            // app's own package manager installed, never manage one ourselves.
            node_modules_dir: Some(NodeModulesDirMode::Manual),
            ..Default::default()
        },
    ));
    let resolver_factory = ResolverFactory::new(
        workspace_factory,
        ResolverFactoryOptions {
            // Node semantics: extension-ambiguous files without a package.json
            // "type" are CommonJS.
            is_cjs_resolution_mode: IsCjsResolutionMode::ImplicitTypeCommonJs,
            // Persist CJS export analysis next to the V8 code cache: the
            // swc parse it costs is a per-spawn repeat otherwise.
            node_analysis_cache: code_cache
                .clone()
                .map(|cache| cache as deno_resolver::cjs::analyzer::NodeAnalysisCacheRc),
            ..Default::default()
        },
    );

    let node_resolver = resolver_factory.node_resolver().map_err(boot)?.clone();
    let npm_module_loader = resolver_factory.npm_module_loader().map_err(boot)?.clone();
    let cjs_tracker = resolver_factory.cjs_tracker().map_err(boot)?.clone();
    let pkg_json_resolver = resolver_factory.pkg_json_resolver().clone();

    let root_url = deno_path_util::url_from_directory_path(root).map_err(boot)?;
    let module_loader = Rc::new(EngineModuleLoader {
        node_resolver: node_resolver.clone(),
        npm_module_loader,
        root: root_url,
        host,
        code_cache: code_cache.clone(),
    });
    let require_loader = Rc::new(EngineRequireLoader {
        cjs_tracker,
        sys: sys.clone(),
    });

    Ok(EngineLoaders {
        module_loader,
        node_services: NodeExtInitServices {
            node_require_loader: require_loader,
            node_resolver,
            pkg_json_resolver,
            sys: sys.clone(),
        },
        code_cache,
        sys,
    })
}

/// Services one engine shares with every worker it spawns (Deno CLI shares
/// the same set), so SharedArrayBuffers, compiled wasm, BroadcastChannel and
/// blob URLs cross threads the way Node's process-wide registries do. The
/// heap cap travels too: a worker spawned without `resourceLimits` inherits
/// the engine's own limit rather than escaping it.
#[derive(Clone)]
struct WorkerShared {
    root: PathBuf,
    code_cache_dir: Option<PathBuf>,
    memory_limit_bytes: Option<usize>,
    blob_store: Arc<BlobStore>,
    broadcast_channel: InMemoryBroadcastChannel,
    shared_array_buffer_store: SharedArrayBufferStore,
    compiled_wasm_module_store: CompiledWasmModuleStore,
}

pub(crate) fn build_worker(
    config: &EngineConfig,
    main_module: &Url,
    host: Option<ModuleHost>,
) -> Result<MainWorker, EngineError> {
    let loaders = engine_loaders(&config.root, config.code_cache_dir.clone(), host)?;

    // Trusted oj-owned scripts run against the user's project: allow-all.
    let permissions = PermissionsContainer::new(
        Arc::new(RuntimePermissionDescriptorParser::new(loaders.sys.clone())),
        Permissions::allow_all(),
    );

    let shared = WorkerShared {
        root: config.root.clone(),
        code_cache_dir: config.code_cache_dir.clone(),
        memory_limit_bytes: config.memory_limit_bytes,
        blob_store: Arc::new(BlobStore::default()),
        broadcast_channel: InMemoryBroadcastChannel::default(),
        shared_array_buffer_store: SharedArrayBufferStore::default(),
        compiled_wasm_module_store: CompiledWasmModuleStore::default(),
    };

    let services = WorkerServiceOptions::<DenoInNpmPackageChecker, NpmResolver<Sys>, Sys> {
        blob_store: shared.blob_store.clone(),
        broadcast_channel: shared.broadcast_channel.clone(),
        deno_rt_native_addon_loader: None,
        feature_checker: Default::default(),
        fs: Arc::new(RealFs),
        module_loader: loaders.module_loader,
        node_services: Some(loaders.node_services),
        npm_process_state_provider: None,
        permissions,
        root_cert_store_provider: None,
        fetch_dns_resolver: Default::default(),
        shared_array_buffer_store: Some(shared.shared_array_buffer_store.clone()),
        compiled_wasm_module_store: Some(shared.compiled_wasm_module_store.clone()),
        // Covers the CJS path: deno_runtime wires this into the eval-context
        // compile callbacks `require` goes through. The ESM and ext-script
        // paths ride the module loader (see loader.rs).
        v8_code_cache: loaders
            .code_cache
            .map(|cache| cache as Arc<dyn deno_runtime::code_cache::CodeCache>),
        bundle_provider: None,
    };

    let create_params = config
        .memory_limit_bytes
        .map(|limit| v8::CreateParams::default().heap_limits(0, limit));

    let options = WorkerOptions {
        bootstrap: BootstrapOptions {
            mode: WorkerExecutionMode::Run,
            has_node_modules_dir: true,
            ..Default::default()
        },
        startup_snapshot: deno_snapshots::CLI_SNAPSHOT,
        residual_lazy_js_sources: deno_snapshots::RESIDUAL_LAZY_JS,
        residual_lazy_esm_sources: deno_snapshots::RESIDUAL_LAZY_ESM,
        create_params,
        create_web_worker_cb: create_web_worker_cb(shared),
        ..Default::default()
    };

    Ok(MainWorker::bootstrap_from_options(
        main_module,
        services,
        options,
    ))
}

/// Web workers for the engine, which Node code reaches through the
/// `node:worker_threads` compat layer (terser, workbox, jest-worker all spawn
/// them; deno_runtime's default panics the process instead).
///
/// worker_host's op runs this callback ON the spawned worker thread, so the
/// closure captures only Send data and rebuilds the byonm stack there through
/// the same `engine_loaders` recipe. Workers get no host bridge: their
/// payloads are dependency code, which resolves with plain Node semantics.
fn create_web_worker_cb(
    shared: WorkerShared,
) -> Arc<deno_runtime::ops::worker_host::CreateWebWorkerCb> {
    use deno_runtime::deno_node::ops::worker_threads::ResolvedResourceLimits;
    use deno_runtime::deno_node::ops::worker_threads::DEFAULT_STACK_SIZE_MB;
    use deno_runtime::web_worker::WebWorker;
    use deno_runtime::web_worker::WebWorkerOptions;
    use deno_runtime::web_worker::WebWorkerServiceOptions;

    Arc::new(move |args| {
        // The callback type is infallible, so a failure here PANICS BY
        // DESIGN: the panic unwinds only this worker thread and worker_host's
        // recv error surfaces it to JS as the worker's boot error.
        let loaders = engine_loaders(&shared.root, shared.code_cache_dir.clone(), None)
            .expect("worker loaders (parent booted from this workspace)");

        let services = WebWorkerServiceOptions::<DenoInNpmPackageChecker, NpmResolver<Sys>, Sys> {
            blob_store: shared.blob_store.clone(),
            broadcast_channel: shared.broadcast_channel.clone(),
            deno_rt_native_addon_loader: None,
            compiled_wasm_module_store: Some(shared.compiled_wasm_module_store.clone()),
            feature_checker: Default::default(),
            fs: Arc::new(RealFs),
            main_inspector_session_tx: Default::default(),
            module_loader: loaders.module_loader,
            node_services: Some(loaders.node_services),
            npm_process_state_provider: None,
            permissions: args.permissions,
            root_cert_store_provider: None,
            shared_array_buffer_store: Some(shared.shared_array_buffer_store.clone()),
            bundle_provider: None,
        };

        // Node `resourceLimits` -> V8 CreateParams, Deno CLI's recipe
        // (cli/lib/worker.rs): set each constraint, then read back the
        // resolved values so the worker_threads polyfill reports what V8
        // actually applied. A worker spawned WITHOUT limits inherits the
        // engine's own heap cap instead of escaping it.
        let mb = 1024 * 1024;
        let (create_params, resolved_limits) = if let Some(ref limits) = args.resource_limits {
            let mut params = v8::CreateParams::default();
            if let Some(v) = limits.max_old_generation_size_mb.filter(|&v| v > 0) {
                params = params.set_max_old_generation_size_in_bytes(v * mb);
            }
            if let Some(v) = limits.max_young_generation_size_mb.filter(|&v| v > 0) {
                params = params.set_max_young_generation_size_in_bytes(v * mb);
            }
            if let Some(v) = limits.code_range_size_mb.filter(|&v| v > 0) {
                params = params.set_code_range_size_in_bytes(v * mb);
            }
            let resolved = ResolvedResourceLimits {
                max_young_generation_size_mb: params.max_young_generation_size_in_bytes() / mb,
                max_old_generation_size_mb: params.max_old_generation_size_in_bytes() / mb,
                code_range_size_mb: params.code_range_size_in_bytes() / mb,
                stack_size_mb: limits.stack_size_mb.unwrap_or(DEFAULT_STACK_SIZE_MB),
            };
            (Some(params), Some(resolved))
        } else {
            (
                shared
                    .memory_limit_bytes
                    .map(|limit| v8::CreateParams::default().heap_limits(0, limit)),
                None,
            )
        };
        let has_heap_limit = create_params.is_some();

        let options = WebWorkerOptions {
            name: args.name,
            main_module: args.main_module.clone(),
            worker_id: args.worker_id,
            bootstrap: BootstrapOptions {
                mode: WorkerExecutionMode::Worker,
                has_node_modules_dir: true,
                // Deno CLI gives every worker its main module as `location`;
                // without one, bootstrap's setLocationHref(null) throws
                // `Invalid URL: 'null'` before any user code runs.
                location: Some(args.main_module.clone()),
                ..Default::default()
            },
            extensions: vec![],
            startup_snapshot: deno_snapshots::CLI_SNAPSHOT,
            residual_lazy_js_sources: deno_snapshots::RESIDUAL_LAZY_JS,
            residual_lazy_esm_sources: deno_snapshots::RESIDUAL_LAZY_ESM,
            unsafely_ignore_certificate_errors: None,
            create_params,
            seed: None,
            create_web_worker_cb: create_web_worker_cb(shared.clone()),
            format_js_error_fn: None,
            worker_type: args.worker_type,
            cache_storage_dir: None,
            stdio: Default::default(),
            trace_ops: None,
            close_on_idle: args.close_on_idle,
            maybe_worker_metadata: args.maybe_worker_metadata,
            maybe_main_module_blob: args.maybe_main_module_blob,
            maybe_coverage_dir: None,
            maybe_cpu_prof_config: None,
            enable_raw_imports: false,
            enable_stack_trace_arg_in_ops: false,
            wait_for_debugger_on_start: args.wait_for_debugger_on_start,
            wait_for_page_wait_for_debugger: args.wait_for_page_wait_for_debugger,
        };

        // from_options + explicit bootstrap, not bootstrap_from_options: the
        // resolved limits must sit in op_state BEFORE bootstrap so the
        // worker_threads polyfill can read them during init (Deno CLI's
        // ordering).
        let (mut worker, handle, bootstrap_options) = WebWorker::from_options(services, options);
        if let Some(resolved) = resolved_limits {
            worker.js_runtime.op_state().borrow_mut().put(resolved);
        }
        worker.bootstrap(&bootstrap_options);

        // Graceful OOM for a capped worker: flag oom_triggered (surfaced as
        // ERR_WORKER_OUT_OF_MEMORY) and terminate only this isolate, instead
        // of V8's fatal OOM taking the whole process.
        if has_heap_limit {
            let ts_handle = worker.js_runtime.v8_isolate().thread_safe_handle();
            let oom_flag = worker.oom_triggered.clone();
            worker
                .js_runtime
                .add_near_heap_limit_callback(move |current_limit, _initial_limit| {
                    oom_flag.store(true, std::sync::atomic::Ordering::SeqCst);
                    ts_handle.terminate_execution();
                    current_limit * 2
                });
        }

        (worker, handle)
    })
}