Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions crates/bamts-compiler/src/emitter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,7 @@ impl EmitOptions {
always_strict: bool,
module: Option<ModuleKind>,
use_define_for_class_fields: Option<bool>,
import_helpers: Option<bool>,
) {
self.target = target;
self.always_strict = always_strict;
Expand All @@ -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;
Comment thread
metaphorics marked this conversation as resolved.
}
}

/// Applies one compiler directive, returning a typed diagnostic on failure.
Expand Down
109 changes: 82 additions & 27 deletions crates/bamts-compiler/src/emitter/helpers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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`.
Expand Down Expand Up @@ -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
Expand All @@ -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<HelperKind> {
Expand All @@ -354,6 +390,8 @@ fn emit_closed(
options: &HelperOptions,
file: Option<&SourceFile>,
mut diagnostics: Vec<Diagnostic>,
import_helpers: bool,
source_is_module: bool,
) -> HelperEmit {
if helpers.is_empty() {
diagnostics.sort();
Expand All @@ -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()
Expand All @@ -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())
});
Expand All @@ -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();
Expand Down Expand Up @@ -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(
Expand Down
3 changes: 2 additions & 1 deletion crates/bamts-compiler/src/emitter/transforms.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
74 changes: 69 additions & 5 deletions crates/bamts-compiler/src/emitter/transpile.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down Expand Up @@ -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 = <div />; 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 {
Expand Down
1 change: 1 addition & 0 deletions crates/bamts-compiler/src/pipeline.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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();

Expand Down
7 changes: 7 additions & 0 deletions crates/bamts-compiler/src/program.rs
Original file line number Diff line number Diff line change
Expand Up @@ -222,6 +222,7 @@ pub struct ResolvedProgram {
always_strict: bool,
no_emit_helpers: bool,
use_define_for_class_fields: Option<bool>,
import_helpers: Option<bool>,
target: crate::emitter::ScriptTarget,
libs: crate::checker::intrinsic_environment::LibSet,
check_js: bool,
Expand All @@ -242,6 +243,11 @@ impl ResolvedProgram {
self.use_define_for_class_fields
}

#[must_use]
pub const fn import_helpers(&self) -> Option<bool> {
self.import_helpers
}

#[must_use]
pub fn roots(&self) -> &[SourceId] {
&self.roots
Expand Down Expand Up @@ -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(),
Expand Down
Loading
Loading