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/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 { 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) {