From 064b973ae107eed4e2753c7ece6705537fc7f2b9 Mon Sep 17 00:00:00 2001 From: "MK (fengmk2)" Date: Fri, 30 Oct 2026 14:39:52 +0900 Subject: [PATCH] fix(core): keep the loader initialization APIs for embedders An embedder that links oxc-node with `default-features = false` cannot call `initialize_loader_js` or `freeze_tsconfig_js`, because `#[cfg(feature = "napi")]` removes them. The plain `initialize_loader` does not keep the diagnostic source, and the plain `freeze_tsconfig` does not read the calling JavaScript thread's environment. The helpers that do both are private, so an embedder cannot rebuild the functions. Use `cfg_attr` for the two `#[napi]` registrations, as the other exports do. The functions compile either way, and only the registration depends on the feature. Co-Authored-By: Claude Opus 5.5 --- Cargo.toml | 2 +- src/lib.rs | 6 ++---- tests/loader_context.rs | 25 ++++++++++++++++++++++++- 3 files changed, 27 insertions(+), 6 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 0abd306d..bbef822d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -22,7 +22,7 @@ default = ["napi"] # # The embedder writes its own thin `#[napi]` forwarders — for `create_resolve`, # `load`, `transform`, `OxcTransformer`, `set_tsconfig_path`, -# `get_tsconfig_path`, `initialize_loader`, `freeze_tsconfig`, +# `get_tsconfig_path`, `initialize_loader_js`, `freeze_tsconfig_js`, # `set_transform_deps`, `get_transform_deps`, # `transforms_dep`, `freeze_transform_deps`, `transform_module_runner`, and for # watch mode `enable_watch_report` and `take_watch_dependencies` — compiling this crate against diff --git a/src/lib.rs b/src/lib.rs index 7bfca46d..0438aaf1 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -862,8 +862,7 @@ pub fn initialize_loader(path: Option) -> Result<()> { set_tsconfig_override(path, false, "setTsconfigPath()") } -#[cfg(feature = "napi")] -#[napi(js_name = "initializeLoader")] +#[cfg_attr(feature = "napi", napi(js_name = "initializeLoader"))] /// Initialize this loader from a frozen tsconfig selection. A missing or empty path /// selects discovery without reading environment variables. The optional source /// preserves the original diagnostic label: `setTsconfigPath()`, `TS_NODE_PROJECT`, @@ -946,8 +945,7 @@ pub fn freeze_tsconfig(cwd: String) -> FrozenTsconfig { } } -#[cfg(feature = "napi")] -#[napi(js_name = "freezeTsconfig")] +#[cfg_attr(feature = "napi", napi(js_name = "freezeTsconfig"))] /// Freeze this thread's effective tsconfig path and diagnostic source for an /// asynchronous loader. An absent path means discovery. This does not parse the /// config or emit diagnostics; it prevents later changes to the selected config. diff --git a/tests/loader_context.rs b/tests/loader_context.rs index b3b88fe2..d3cdff5b 100644 --- a/tests/loader_context.rs +++ b/tests/loader_context.rs @@ -13,7 +13,8 @@ use std::{ use napi::Either; use oxc_node::{ - CapturedLoaderContext, OxcTransformer, get_tsconfig_path, set_transform_deps, set_tsconfig_path, + CapturedLoaderContext, OxcTransformer, get_tsconfig_path, initialize_loader_js, + set_transform_deps, set_tsconfig_path, }; const SOURCE: &str = r#" @@ -175,3 +176,25 @@ fn nested_contexts_restore_after_errors_and_panics() { }); assert_eq!(get_tsconfig_path(), None); } + +#[test] +fn embedders_initialize_loaders_from_a_frozen_selection() { + let fixture = Fixture::new(); + thread::scope(|scope| { + scope + .spawn(|| { + assert!( + error_reason(initialize_loader_js(None, Some("unknown".into()))) + .contains("Invalid tsconfig source") + ); + unwrap_oxc(initialize_loader_js( + Some(fixture.config("assign")), + Some("TS_NODE_PROJECT".into()), + )); + assert_eq!(get_tsconfig_path(), Some(fixture.config("assign"))); + assert_eq!(setter_calls(&transform(&fixture.transformer())), 1); + }) + .join() + .unwrap(); + }); +}