From 1e3c98c199ac61f02d2fb1bd4d2bc864eb6c3999 Mon Sep 17 00:00:00 2001 From: Lee Yunjin Date: Thu, 10 Sep 2026 18:21:31 +0900 Subject: [PATCH 1/2] Plumb tsconfig importHelpers through to the emitter The emitter already implements importHelpers end to end (EmitOptions.import_helpers, HelperStyle selection in transform_view, tslib import/require emission in emitter/helpers.rs), but tsconfig had no CompilerOptions field for it, so the key was silently dropped on both the CLI and worker paths and helpers were always inlined. Replicate the landed useDefineForClassFields plumbing shape: - project.rs: tri-state Option field, parse arm, accessor, tri-state parse test - program.rs: ResolvedProgram carry and accessor - emitter.rs: apply_emit_fields takes import_helpers set-when-Some - pipeline.rs, project/effective.rs: forward on both emit paths - check_cells.rs: worker build_tsconfig bool list + camelCase remap so @importHelpers serializes as a typed boolean "importHelpers" key Verified: fmt --check, check, bamts-compiler lib 2007/0, bamts-verification lib 696/0, clippy --all-targets -D warnings, diagnostics regenerate --check PASS. CLI smoke: es2015 commonjs async fn emits require("tslib").__awaiter with importHelpers:true and the inline helper body with importHelpers:false. --- crates/bamts-compiler/src/emitter.rs | 4 +++ crates/bamts-compiler/src/pipeline.rs | 1 + crates/bamts-compiler/src/program.rs | 7 ++++ crates/bamts-compiler/src/project.rs | 32 +++++++++++++++++++ .../bamts-compiler/src/project/effective.rs | 1 + crates/bamts-verification/src/check_cells.rs | 5 ++- 6 files changed, 49 insertions(+), 1 deletion(-) diff --git a/crates/bamts-compiler/src/emitter.rs b/crates/bamts-compiler/src/emitter.rs index 84c68431..23a0640d 100644 --- a/crates/bamts-compiler/src/emitter.rs +++ b/crates/bamts-compiler/src/emitter.rs @@ -210,6 +210,7 @@ impl EmitOptions { always_strict: bool, module: Option, use_define_for_class_fields: Option, + import_helpers: Option, ) { self.target = target; self.always_strict = always_strict; @@ -219,6 +220,9 @@ impl EmitOptions { if let Some(use_define) = use_define_for_class_fields { self.use_define_for_class_fields = Some(use_define); } + if let Some(import_helpers) = import_helpers { + self.import_helpers = import_helpers; + } } /// Applies one compiler directive, returning a typed diagnostic on failure. diff --git a/crates/bamts-compiler/src/pipeline.rs b/crates/bamts-compiler/src/pipeline.rs index 282d3c70..c7cee279 100644 --- a/crates/bamts-compiler/src/pipeline.rs +++ b/crates/bamts-compiler/src/pipeline.rs @@ -216,6 +216,7 @@ fn program_emit_options(program: &ResolvedProgram, mode: FrontendMode) -> Option always_strict, module, program.use_define_for_class_fields(), + program.import_helpers(), ); options.no_emit_helpers = check.no_emit_helpers(); diff --git a/crates/bamts-compiler/src/program.rs b/crates/bamts-compiler/src/program.rs index 12a7630b..cb04202b 100644 --- a/crates/bamts-compiler/src/program.rs +++ b/crates/bamts-compiler/src/program.rs @@ -222,6 +222,7 @@ pub struct ResolvedProgram { always_strict: bool, no_emit_helpers: bool, use_define_for_class_fields: Option, + import_helpers: Option, target: crate::emitter::ScriptTarget, libs: crate::checker::intrinsic_environment::LibSet, check_js: bool, @@ -242,6 +243,11 @@ impl ResolvedProgram { self.use_define_for_class_fields } + #[must_use] + pub const fn import_helpers(&self) -> Option { + self.import_helpers + } + #[must_use] pub fn roots(&self) -> &[SourceId] { &self.roots @@ -716,6 +722,7 @@ impl ProgramLoader { always_strict: self.options.always_strict(), no_emit_helpers: self.options.no_emit_helpers(), use_define_for_class_fields: self.options.use_define_for_class_fields(), + import_helpers: self.options.import_helpers(), target, libs, check_js: self.options.check_js(), diff --git a/crates/bamts-compiler/src/project.rs b/crates/bamts-compiler/src/project.rs index 83cdf9c6..f4552ab0 100644 --- a/crates/bamts-compiler/src/project.rs +++ b/crates/bamts-compiler/src/project.rs @@ -957,6 +957,7 @@ pub struct CompilerOptions { strict_property_initialization: bool, always_strict: bool, use_define_for_class_fields: Option, + import_helpers: Option, allow_js: bool, check_js: bool, resolve_json_module: bool, @@ -1058,6 +1059,11 @@ impl CompilerOptions { self.use_define_for_class_fields } + #[must_use] + pub const fn import_helpers(&self) -> Option { + self.import_helpers + } + #[must_use] pub const fn allow_js(&self) -> bool { self.allow_js @@ -1249,6 +1255,7 @@ impl ProjectConfig { .unwrap_or(strict), always_strict: optional_bool(compiler, "alwaysStrict")?.unwrap_or(strict), use_define_for_class_fields: optional_bool(compiler, "useDefineForClassFields")?, + import_helpers: optional_bool(compiler, "importHelpers")?, allow_js: optional_bool(compiler, "allowJs")?.unwrap_or(false), check_js: optional_bool(compiler, "checkJs")?.unwrap_or(false), resolve_json_module: optional_bool(compiler, "resolveJsonModule")?.unwrap_or(false), @@ -2590,6 +2597,31 @@ mod tests { assert_eq!(silent.options().use_define_for_class_fields(), None); } + #[test] + fn project_config_parses_import_helpers_as_tri_state() { + let pinned_true = ProjectConfig::parse( + &root(), + "/workspace/corpus/tsconfig.json", + r#"{"compilerOptions":{"importHelpers":true}}"#, + ) + .expect("pinned true"); + assert_eq!(pinned_true.options().import_helpers(), Some(true)); + let pinned_false = ProjectConfig::parse( + &root(), + "/workspace/corpus/tsconfig.json", + r#"{"compilerOptions":{"importHelpers":false}}"#, + ) + .expect("pinned false"); + assert_eq!(pinned_false.options().import_helpers(), Some(false)); + let silent = ProjectConfig::parse( + &root(), + "/workspace/corpus/tsconfig.json", + r#"{"compilerOptions":{"strict":true}}"#, + ) + .expect("silent key"); + assert_eq!(silent.options().import_helpers(), None); + } + #[test] fn project_config_rejects_wrong_types_and_every_root_escape() { let wrong = ProjectConfig::parse( diff --git a/crates/bamts-compiler/src/project/effective.rs b/crates/bamts-compiler/src/project/effective.rs index b4c6f969..ba36b16b 100644 --- a/crates/bamts-compiler/src/project/effective.rs +++ b/crates/bamts-compiler/src/project/effective.rs @@ -723,6 +723,7 @@ fn emit_options(options: &CompilerOptions, source_id: SourceId) -> (EmitOptions, always_strict, module, options.use_define_for_class_fields(), + options.import_helpers(), ); (emit_options, diagnostics) diff --git a/crates/bamts-verification/src/check_cells.rs b/crates/bamts-verification/src/check_cells.rs index 41386a0c..2a5edbaf 100644 --- a/crates/bamts-verification/src/check_cells.rs +++ b/crates/bamts-verification/src/check_cells.rs @@ -997,7 +997,8 @@ fn build_tsconfig(pragmas: &CasePragmas) -> String { | "emitdeclarationonly" | "sourcemap" | "declarationmap" - | "usedefineforclassfields" => value.eq_ignore_ascii_case("true").to_string(), + | "usedefineforclassfields" + | "importhelpers" => value.eq_ignore_ascii_case("true").to_string(), "lib" => { let items: Vec = values .iter() @@ -1021,6 +1022,7 @@ fn build_tsconfig(pragmas: &CasePragmas) -> String { "noemit" => "noEmit", "alwaysstrict" => "alwaysStrict", "usedefineforclassfields" => "useDefineForClassFields", + "importhelpers" => "importHelpers", "exactoptionalpropertytypes" => "exactOptionalPropertyTypes", "nouncheckedindexedaccess" => "noUncheckedIndexedAccess", "strictpropertyinitialization" => "strictPropertyInitialization", @@ -2492,6 +2494,7 @@ pub fn emit_source_map_baseline( check.always_strict(), prog.is_commonjs().then_some(ModuleKind::CommonJs), prog.use_define_for_class_fields(), + prog.import_helpers(), ); options.no_emit_helpers = check.no_emit_helpers(); match prog.jsx_routing_decision(ProgramOutputKind::JavaScript) { From f797348f6c816902bf79b0c665c3933262724190 Mon Sep 17 00:00:00 2001 From: metaphorics <152830360+metaphorics@users.noreply.github.com> Date: Sun, 20 Sep 2026 16:00:08 +0900 Subject: [PATCH 2/2] Honor importHelpers helper precedence Apply imported helpers only to effective modules and preserve tslib imports when noEmitHelpers suppresses inline definitions. --- crates/bamts-compiler/src/emitter/helpers.rs | 109 +++++++++++++----- .../bamts-compiler/src/emitter/transforms.rs | 3 +- .../bamts-compiler/src/emitter/transpile.rs | 74 +++++++++++- 3 files changed, 153 insertions(+), 33 deletions(-) diff --git a/crates/bamts-compiler/src/emitter/helpers.rs b/crates/bamts-compiler/src/emitter/helpers.rs index c7b7b76d..b9285358 100644 --- a/crates/bamts-compiler/src/emitter/helpers.rs +++ b/crates/bamts-compiler/src/emitter/helpers.rs @@ -51,10 +51,9 @@ pub struct HelperOptions { /// When true, bind helpers from [`HelperOptions::module_specifier`] instead /// of inlining their bodies. pub import_helpers: bool, - /// Assume helpers exist globally; emit no prelude (`noEmitHelpers`). Takes - /// precedence over `import_helpers` when both are set: the more specific - /// "assume global" instruction wins, and the combination is contradictory - /// configuration no baseline exercises. + /// Assume inline helpers exist globally; emit no inline helper definitions + /// (`noEmitHelpers`). External imports still emit when `import_helpers` + /// is enabled. pub no_emit_helpers: bool, pub style: HelperStyle, /// The module specifier used for imported helpers. Defaults to `tslib`. @@ -309,7 +308,36 @@ pub fn emit_helpers( options: &HelperOptions, file: Option<&SourceFile>, ) -> HelperEmit { - emit_closed(close_helpers(requested), options, file, Vec::new()) + let source_is_module = file.is_some_and(crate::checker::source_is_module); + emit_closed( + close_helpers(requested), + options, + file, + Vec::new(), + options.import_helpers, + source_is_module, + ) +} + +/// Emits helpers using the caller's effective module classification. +/// +/// Project emission uses this when synthesized imports make a source a module +/// even though its original syntax has no import or export. +#[must_use] +pub(super) fn emit_helpers_for_source( + requested: &[HelperKind], + options: &HelperOptions, + file: &SourceFile, + source_is_module: bool, +) -> HelperEmit { + emit_closed( + close_helpers(requested), + options, + Some(file), + Vec::new(), + options.import_helpers && source_is_module, + source_is_module, + ) } /// Resolves helper identifiers, recording [`codes::UNKNOWN_HELPER`] for names @@ -335,7 +363,15 @@ pub fn emit_helpers_named( )), } } - emit_closed(close_helpers(&requested), options, file, diagnostics) + let source_is_module = file.is_some_and(crate::checker::source_is_module); + emit_closed( + close_helpers(&requested), + options, + file, + diagnostics, + options.import_helpers, + source_is_module, + ) } fn close_helpers(requested: &[HelperKind]) -> Vec { @@ -354,6 +390,8 @@ fn emit_closed( options: &HelperOptions, file: Option<&SourceFile>, mut diagnostics: Vec, + import_helpers: bool, + source_is_module: bool, ) -> HelperEmit { if helpers.is_empty() { diagnostics.sort(); @@ -363,19 +401,7 @@ fn emit_closed( diagnostics, }; } - if options.no_emit_helpers { - // `noEmitHelpers`: callers provide the helpers; the closed set is - // still recorded (and name-resolution diagnostics kept) but no - // definition text is emitted. One gate serves both entry points. - diagnostics.sort(); - return HelperEmit { - prelude: String::new(), - helpers, - diagnostics, - }; - } - - let external_imports = options.import_helpers && options.style != HelperStyle::Inline; + let external_imports = import_helpers && options.style != HelperStyle::Inline; let (imported, inline_only): (Vec<_>, Vec<_>) = if external_imports { helpers .iter() @@ -384,10 +410,7 @@ fn emit_closed( } else { (Vec::new(), helpers.clone()) }; - if !imported.is_empty() - && options.style == HelperStyle::EsModule - && !file.is_some_and(crate::checker::source_is_module) - { + if !imported.is_empty() && options.style == HelperStyle::EsModule && !source_is_module { let (source_id, range) = file.map_or((SourceId::new(0), empty_range()), |file| { (file.source_id(), file.range()) }); @@ -405,23 +428,28 @@ fn emit_closed( }; } + let inline_prelude = if options.no_emit_helpers { + String::new() + } else { + inline_prelude(&inline_only) + }; let prelude = match options.style { - HelperStyle::Inline => inline_prelude(&inline_only), + HelperStyle::Inline => inline_prelude, HelperStyle::EsModule if external_imports => { let mut prelude = if imported.is_empty() { String::new() } else { es_import_prelude(&imported, &options.module_specifier) }; - prelude.push_str(&inline_prelude(&inline_only)); + prelude.push_str(&inline_prelude); prelude } HelperStyle::CommonJs if external_imports => { let mut prelude = cjs_prelude(&imported, &options.module_specifier); - prelude.push_str(&inline_prelude(&inline_only)); + prelude.push_str(&inline_prelude); prelude } - HelperStyle::EsModule | HelperStyle::CommonJs => inline_prelude(&inline_only), + HelperStyle::EsModule | HelperStyle::CommonJs => inline_prelude, }; diagnostics.sort(); @@ -629,6 +657,33 @@ mod tests { ); } + #[test] + fn imported_helpers_still_emit_with_no_emit_helpers() { + let file = parse("export const x = 1;\n"); + let mut esm = HelperOptions::es_module(); + esm.no_emit_helpers = true; + let esm = emit_helpers( + &[HelperKind::Awaiter, HelperKind::PropKey], + &esm, + Some(&file), + ); + assert!(!esm.has_errors()); + assert_eq!(esm.prelude, "import { __awaiter } from \"tslib\";\n"); + + let mut common_js = HelperOptions::common_js(); + common_js.no_emit_helpers = true; + let common_js = emit_helpers( + &[HelperKind::Awaiter, HelperKind::PropKey], + &common_js, + None, + ); + assert!(!common_js.has_errors()); + assert_eq!( + common_js.prelude, + "var __awaiter = require(\"tslib\").__awaiter;\n" + ); + } + #[test] fn unknown_helper_name_is_diagnosed_and_known_names_still_emit() { let emitted = emit_helpers_named( diff --git a/crates/bamts-compiler/src/emitter/transforms.rs b/crates/bamts-compiler/src/emitter/transforms.rs index 5abf133f..c3eec17d 100644 --- a/crates/bamts-compiler/src/emitter/transforms.rs +++ b/crates/bamts-compiler/src/emitter/transforms.rs @@ -275,11 +275,12 @@ pub fn emit_transformed( eof, file.diagnostics().to_vec(), ); - let helper_emit = helpers::emit_helpers(&used_helpers, &options.helpers, Some(&rewritten)); // Under `moduleDetection: auto` a file whose JSX draws the automatic // runtime import is a module (commentsOnJSXExpressionsArePreserved), // so the synthesized import counts like a source-level one. let is_module = !runtime_prelude.is_empty() || crate::checker::source_is_module(file); + let helper_emit = + helpers::emit_helpers_for_source(&used_helpers, &options.helpers, &rewritten, is_module); let cjs_marker = rewriter.cjs_marker_prelude(!runtime_prelude.is_empty()); let cjs_requires = rewriter.cjs_require_prelude(); let prelude = join_preludes( diff --git a/crates/bamts-compiler/src/emitter/transpile.rs b/crates/bamts-compiler/src/emitter/transpile.rs index 4c1b706e..07afaa25 100644 --- a/crates/bamts-compiler/src/emitter/transpile.rs +++ b/crates/bamts-compiler/src/emitter/transpile.rs @@ -213,20 +213,58 @@ mod tests { } #[test] - fn import_helpers_require_module_for_esm() { + fn import_helpers_fall_back_to_inline_helpers_for_scripts() { + let esm_options = EmitOptions { + target: ScriptTarget::Es5, + import_helpers: true, + module: Some(ModuleKind::Es2015), + ..EmitOptions::default() + }; + let esm = one("async function f() { return 1; }", &esm_options); + assert!( + !esm.has_errors(), + "unexpected diagnostics: {:?}", + esm.diagnostics + ); + let esm_js = esm.javascript.expect("javascript output").code; + assert!(esm_js.contains("var __awaiter =")); + assert!(!esm_js.contains("from \"tslib\"")); + + let common_js_options = EmitOptions { + module: Some(ModuleKind::CommonJs), + ..esm_options + }; + let common_js = one("async function f() { return 1; }", &common_js_options); + assert!( + !common_js.has_errors(), + "unexpected diagnostics: {:?}", + common_js.diagnostics + ); + let common_js = common_js.javascript.expect("javascript output").code; + assert!(common_js.contains("var __awaiter =")); + assert!(!common_js.contains("require(\"tslib\")")); + } + + #[test] + fn import_helpers_take_precedence_over_no_emit_helpers_for_modules() { let options = EmitOptions { target: ScriptTarget::Es5, import_helpers: true, + no_emit_helpers: true, module: Some(ModuleKind::Es2015), ..EmitOptions::default() }; - let out = one("async function f() { return 1; }", &options); + let out = one("export async function f() { return 1; }", &options); assert!( - out.diagnostics.iter().any(|diagnostic| diagnostic.code() - == super::super::helpers::codes::IMPORT_HELPERS_REQUIRES_MODULE), - "non-module ESM helpers should be rejected: {:?}", + !out.has_errors(), + "unexpected diagnostics: {:?}", out.diagnostics ); + let js = out.javascript.expect("javascript output").code; + assert!(js.contains("from \"tslib\";"), "got:\n{js}"); + assert!(js.contains("__awaiter"), "got:\n{js}"); + assert!(js.contains("__generator"), "got:\n{js}"); + assert!(!js.contains("var __awaiter =")); } #[test] @@ -318,6 +356,32 @@ mod tests { assert!(code.contains("_jsx_1(\"div\""), "{code}"); } + #[test] + fn automatic_jsx_module_can_import_helpers() { + let options = EmitOptions { + target: ScriptTarget::Es5, + import_helpers: true, + jsx: Some(JsxEmit::ReactJsx), + ..EmitOptions::default() + }; + let output = one_jsx( + "const view =
; async function f() { return 1; }", + &options, + ); + assert!( + !output.has_errors(), + "unexpected diagnostics: {:?}", + output.diagnostics + ); + let code = output.javascript.expect("JavaScript output").code; + assert!(code.contains("from \"tslib\";"), "{code}"); + assert!(code.contains("from \"react/jsx-runtime\";"), "{code}"); + assert!( + !code.contains("var __awaiter = (this && this.__awaiter)"), + "{code}" + ); + } + #[test] fn automatic_jsx_emits_commonjs_runtime_bindings() { let options = EmitOptions {