diff --git a/src/ast/ts.rs b/src/ast/ts.rs index 2d769002e203..3f316e1318e3 100644 --- a/src/ast/ts.rs +++ b/src/ast/ts.rs @@ -150,6 +150,40 @@ impl Data { // Data-only; the parser-state predicates that depend on `P` stay in // `bun_js_parser::typescript`. +/// tsconfig's `emitDecoratorMetadata` widened with the effective +/// `strictNullChecks`, because tsc's type serializer reads both. +#[repr(u8)] +#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Debug, Default)] +pub enum DecoratorMetadata { + /// `emitDecoratorMetadata` is off. + #[default] + Off = 0, + /// `emitDecoratorMetadata` is on and `strictNullChecks` is off. + Loose = 1, + /// `emitDecoratorMetadata` is on and `strictNullChecks` is on. + Strict = 2, +} + +impl DecoratorMetadata { + pub const fn new(emit_decorator_metadata: bool, strict_null_checks: bool) -> Self { + match (emit_decorator_metadata, strict_null_checks) { + (false, _) => Self::Off, + (true, false) => Self::Loose, + (true, true) => Self::Strict, + } + } + + #[inline] + pub const fn is_on(self) -> bool { + !matches!(self, Self::Off) + } + + #[inline] + pub const fn strict_null_checks(self) -> bool { + matches!(self, Self::Strict) + } +} + #[derive(Clone, Default)] pub enum Metadata { #[default] @@ -182,13 +216,28 @@ impl Metadata { // the logic in finish_union, merge_union, finish_intersection and merge_intersection is // translated from: // https://github.com/microsoft/TypeScript/blob/e0a324b0503be479f2b33fd2e17c6e86c94d1297/src/compiler/transformers/typeSerializer.ts#L402 + // + // With `strict_null_checks` off, `null` and `undefined` are elided. With it + // on they serialize to `void 0` like `void`, so `string | undefined` is `Object`. + + /// Serializes to `void 0`. `MNever` is handled before any comparison. + fn is_void_like(&self) -> bool { + matches!( + self, + Metadata::MNull | Metadata::MUndefined | Metadata::MVoid + ) + } /// Return the final union type if possible, or return None to continue merging. /// - /// If the current type is MNever, MNull, or MUndefined assign the current type - /// to MNone and return None to ensure it's always replaced by the next type. + /// MNever (and MNull / MUndefined without strict null checks) becomes MNone + /// so the next type replaces it. /// `load_name`: closure form of `p.load_name_from_ref` to avoid coupling Metadata to P. - pub fn finish_union<'b, F: Fn(Ref) -> &'b [u8]>(&mut self, load_name: F) -> Option { + pub fn finish_union<'b, F: Fn(Ref) -> &'b [u8]>( + &mut self, + strict_null_checks: bool, + load_name: F, + ) -> Option { let current = self; match current { Metadata::MIdentifier(r) => { @@ -200,7 +249,12 @@ impl Metadata { Metadata::MUnknown | Metadata::MAny | Metadata::MObject => Some(Metadata::MObject), - Metadata::MNever | Metadata::MNull | Metadata::MUndefined => { + Metadata::MNever => { + *current = Metadata::MNone; + None + } + + Metadata::MNull | Metadata::MUndefined if !strict_null_checks => { *current = Metadata::MNone; None } @@ -209,12 +263,21 @@ impl Metadata { } } - pub fn merge_union(&mut self, left: Self) { + pub fn merge_union(&mut self, strict_null_checks: bool, left: Self) { let result = self; if !matches!(left, Metadata::MNone) { if core::mem::discriminant(result) != core::mem::discriminant(&left) { *result = match result { - Metadata::MNever | Metadata::MUndefined | Metadata::MNull => left, + Metadata::MNever => left, + + // both sides are `void 0` + Metadata::MNull | Metadata::MUndefined | Metadata::MVoid + if left.is_void_like() => + { + left + } + + Metadata::MNull | Metadata::MUndefined if !strict_null_checks => left, _ => Metadata::MObject, }; @@ -236,10 +299,11 @@ impl Metadata { /// Return the final intersection type if possible, or return None to continue merging. /// - /// If the current type is MUnknown, MNull, or MUndefined assign the current type - /// to MNone and return None to ensure it's always replaced by the next type. + /// MUnknown (and MNull / MUndefined without strict null checks) becomes + /// MNone so the next type replaces it. pub fn finish_intersection<'b, F: Fn(Ref) -> &'b [u8]>( &mut self, + strict_null_checks: bool, load_name: F, ) -> Option { let current = self; @@ -256,7 +320,12 @@ impl Metadata { Metadata::MAny | Metadata::MObject => Some(Metadata::MObject), - Metadata::MUnknown | Metadata::MNull | Metadata::MUndefined => { + Metadata::MUnknown => { + *current = Metadata::MNone; + None + } + + Metadata::MNull | Metadata::MUndefined if !strict_null_checks => { *current = Metadata::MNone; None } @@ -265,16 +334,25 @@ impl Metadata { } } - pub fn merge_intersection(&mut self, left: Self) { + pub fn merge_intersection(&mut self, strict_null_checks: bool, left: Self) { let result = self; if !matches!(left, Metadata::MNone) { if core::mem::discriminant(result) != core::mem::discriminant(&left) { *result = match result { - Metadata::MUnknown | Metadata::MUndefined | Metadata::MNull => left, + Metadata::MUnknown => left, // ensure MNever is the final type Metadata::MNever => Metadata::MNever, + // both sides are `void 0` + Metadata::MNull | Metadata::MUndefined | Metadata::MVoid + if left.is_void_like() => + { + left + } + + Metadata::MNull | Metadata::MUndefined if !strict_null_checks => left, + _ => Metadata::MObject, }; } else { diff --git a/src/bundler/ParseTask.rs b/src/bundler/ParseTask.rs index 4df8c65c2197..5bbadfad49d8 100644 --- a/src/bundler/ParseTask.rs +++ b/src/bundler/ParseTask.rs @@ -10,6 +10,7 @@ use core::sync::atomic::{AtomicU32, Ordering}; use crate::Error as AnyError; use bun_alloc::Arena as Bump; // bumpalo::Bump re-export use bun_ast::ImportRecord; +use bun_ast::ts::DecoratorMetadata; use bun_ast::{Loc, Location, Log, Msg, Source}; use bun_collections::VecExt; use bun_core::strings; @@ -107,7 +108,7 @@ pub struct ParseTask { pub(crate) known_target: options::Target, pub(crate) module_type: options::ModuleType, - pub(crate) emit_decorator_metadata: bool, + pub(crate) decorator_metadata: DecoratorMetadata, pub(crate) experimental_decorators: bool, pub(crate) use_define_for_class_fields: bool, // BACKREF; `None` only before enqueue (`Default`, runtime source). @@ -287,7 +288,7 @@ impl ParseTask { jsx: resolve_result.jsx.clone(), source_index, module_type: resolve_result.module_type, - emit_decorator_metadata: resolve_result.flags.emit_decorator_metadata(), + decorator_metadata: resolve_result.flags.decorator_metadata(), experimental_decorators: resolve_result.flags.experimental_decorators(), use_define_for_class_fields: resolve_result.flags.use_define_for_class_fields(), package_version, @@ -341,7 +342,7 @@ impl Default for ParseTask { stage: ParseTaskStage::NeedsSourceCode, known_target: options::Target::default(), module_type: options::ModuleType::Unknown, - emit_decorator_metadata: false, + decorator_metadata: DecoratorMetadata::Off, experimental_decorators: false, use_define_for_class_fields: true, package_version: ast::StoreStr::EMPTY, @@ -620,7 +621,7 @@ pub mod parse_worker { }, stage: ParseTaskStage::NeedsSourceCode, module_type: options::ModuleType::Unknown, - emit_decorator_metadata: false, + decorator_metadata: DecoratorMetadata::Off, experimental_decorators: false, use_define_for_class_fields: true, package_version: ast::StoreStr::EMPTY, @@ -2539,12 +2540,12 @@ pub mod parse_worker { opts.features.minify_keep_names = topts.keep_names; opts.features.minify_whitespace = topts.minify_whitespace; opts.use_define_for_class_fields = task.use_define_for_class_fields; - opts.features.emit_decorator_metadata = task.emit_decorator_metadata; + opts.features.decorator_metadata = task.decorator_metadata; // emitDecoratorMetadata implies legacy/experimental decorators, as it only // makes sense with TypeScript's legacy decorator system (reflect-metadata). // TC39 standard decorators have their own metadata mechanism. opts.features.standard_decorators = !loader.is_typescript() - || !(task.experimental_decorators || task.emit_decorator_metadata); + || !(task.experimental_decorators || task.decorator_metadata.is_on()); opts.features.unwrap_commonjs_packages = topts.unwrap_commonjs_packages; opts.features.no_macros = topts.no_macros; // Modeled as diff --git a/src/bundler/bundle_v2.rs b/src/bundler/bundle_v2.rs index 149d48977849..b81e5b3800db 100644 --- a/src/bundler/bundle_v2.rs +++ b/src/bundler/bundle_v2.rs @@ -4128,7 +4128,6 @@ pub mod bv2_impl { jsx, source_index: bun_ast::Index::init(source_index.get()), module_type, - emit_decorator_metadata: false, // TODO package_version: bun_ast::StoreStr::EMPTY, loader: Some(loader), known_target, diff --git a/src/bundler/options.rs b/src/bundler/options.rs index 501821ffe0ac..d9c4f10ca30c 100644 --- a/src/bundler/options.rs +++ b/src/bundler/options.rs @@ -2,6 +2,7 @@ //! into the concrete `BundleOptions` the bundler and runtime consume. use bun_analytics as analytics; +use bun_ast::ts::DecoratorMetadata; use bun_collections::{StringArrayHashMap, StringHashMap}; use bun_core::strings; use bun_core::{Global, Output}; @@ -1207,7 +1208,7 @@ pub struct BundleOptions<'a> { pub bundler_feature_flags: Option>, pub loaders: LoaderHashTable, pub jsx: jsx::Pragma, - pub emit_decorator_metadata: bool, + pub decorator_metadata: DecoratorMetadata, pub experimental_decorators: bool, pub use_define_for_class_fields: bool, pub auto_import_jsx: bool, @@ -1459,7 +1460,7 @@ impl<'a> BundleOptions<'a> { .map(|s| Box::new(bun_core::handle_oom(s.clone()))), loaders: bun_core::handle_oom(self.loaders.clone()), jsx: self.jsx.clone(), - emit_decorator_metadata: self.emit_decorator_metadata, + decorator_metadata: self.decorator_metadata, experimental_decorators: self.experimental_decorators, use_define_for_class_fields: self.use_define_for_class_fields, auto_import_jsx: self.auto_import_jsx, @@ -1724,7 +1725,7 @@ impl<'a> BundleOptions<'a> { bundler_feature_flags, jsx: jsx::Pragma::default(), - emit_decorator_metadata: false, + decorator_metadata: DecoratorMetadata::Off, experimental_decorators: false, use_define_for_class_fields: true, auto_import_jsx: true, diff --git a/src/bundler/transpiler.rs b/src/bundler/transpiler.rs index cf0c9a3bcd57..d51ffb6d74e7 100644 --- a/src/bundler/transpiler.rs +++ b/src/bundler/transpiler.rs @@ -3,6 +3,7 @@ // ══════════════════════════════════════════════════════════════════════════ use bun_alloc::Arena; +use bun_ast::ts::DecoratorMetadata; use bun_collections::HashMap; use bun_collections::VecExt; use bun_dotenv as dot_env; @@ -734,7 +735,7 @@ impl<'a> Transpiler<'a> { if self.options.transform_options.jsx.is_none() { self.options.jsx = jsx_pragma_from_resolver(&tsconfig.jsx); } - self.options.emit_decorator_metadata = tsconfig.emit_decorator_metadata; + self.options.decorator_metadata = tsconfig.decorator_metadata(); self.options.experimental_decorators = tsconfig.experimental_decorators; if let Some(v) = tsconfig.use_define_for_class_fields { self.options.use_define_for_class_fields = v; @@ -995,7 +996,7 @@ pub struct ParseOptions<'a, 'b> { pub replace_exports: bun_collections::StringArrayHashMap, pub inject_jest_globals: bool, pub set_breakpoint_on_first_line: bool, - pub emit_decorator_metadata: bool, + pub decorator_metadata: DecoratorMetadata, pub experimental_decorators: bool, pub use_define_for_class_fields: bool, pub remove_cjs_module_wrapper: bool, @@ -1602,12 +1603,13 @@ impl<'a> Transpiler<'a> { is_entry_point: false, }; - opts.features.emit_decorator_metadata = this_parse.emit_decorator_metadata; + opts.features.decorator_metadata = this_parse.decorator_metadata; // emitDecoratorMetadata implies legacy/experimental decorators, as it only // makes sense with TypeScript's legacy decorator system (reflect-metadata). // TC39 standard decorators have their own metadata mechanism. opts.features.standard_decorators = !loader.is_typescript() - || !(this_parse.experimental_decorators || this_parse.emit_decorator_metadata); + || !(this_parse.experimental_decorators + || this_parse.decorator_metadata.is_on()); opts.features.allow_runtime = self.options.allow_runtime; opts.features.set_breakpoint_on_first_line = this_parse.set_breakpoint_on_first_line; @@ -2938,7 +2940,7 @@ impl<'a> Transpiler<'a> { // the option fields needed for `ParseOptions` first. let jsx = jsx_pragma_from_resolver(&resolve_result.jsx); let dirname_fd = resolve_result.dirname_fd; - let emit_decorator_metadata = resolve_result.flags.emit_decorator_metadata(); + let decorator_metadata = resolve_result.flags.decorator_metadata(); let experimental_decorators = resolve_result.flags.experimental_decorators(); let use_define_for_class_fields = resolve_result.flags.use_define_for_class_fields(); @@ -2968,7 +2970,7 @@ impl<'a> Transpiler<'a> { macro_remappings, macro_js_ctx: default_macro_js_value(), jsx, - emit_decorator_metadata, + decorator_metadata, experimental_decorators, use_define_for_class_fields, virtual_source: None, diff --git a/src/js_parser/p.rs b/src/js_parser/p.rs index 2383804eb65e..cc06e69b9b1c 100644 --- a/src/js_parser/p.rs +++ b/src/js_parser/p.rs @@ -7440,7 +7440,7 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O let mut array = BumpVec::::new_in(self.arena); - if self.options.features.emit_decorator_metadata { + if self.options.features.decorator_metadata.is_on() { self.emit_decorator_metadata_for_prop(prop, &mut array, loc); } @@ -7677,7 +7677,7 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O if s_class.class.ts_decorators.len_u32() > 0 { let mut array: Vec = s_class.class.ts_decorators.move_to_list_managed(); - if self.options.features.emit_decorator_metadata { + if self.options.features.decorator_metadata.is_on() { if let Some(cf) = constructor_function { // design:paramtypes let constructor_args: &[G::Arg] = cf.func.args.slice(); @@ -9705,7 +9705,7 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O if !TYPESCRIPT { // This is so it doesn't impact runtime transpiler caching when not in use - opts.features.emit_decorator_metadata = false; + opts.features.decorator_metadata = bun_ast::ts::DecoratorMetadata::Off; } let unwrap_all_requires = 'brk: { diff --git a/src/js_parser/parse/parse_entry.rs b/src/js_parser/parse/parse_entry.rs index e1c242d36853..a87f914bc879 100644 --- a/src/js_parser/parse/parse_entry.rs +++ b/src/js_parser/parse/parse_entry.rs @@ -251,7 +251,7 @@ impl<'a> Options<'a> { unwrap_commonjs_packages: f.unwrap_commonjs_packages, commonjs_at_runtime: f.commonjs_at_runtime, unwrap_commonjs_to_esm: f.unwrap_commonjs_to_esm, - emit_decorator_metadata: f.emit_decorator_metadata, + decorator_metadata: f.decorator_metadata, standard_decorators: f.standard_decorators, remove_cjs_module_wrapper: f.remove_cjs_module_wrapper, runtime_transpiler_cache: None, diff --git a/src/js_parser/parse/parse_fn.rs b/src/js_parser/parse/parse_fn.rs index 6555dfa0f358..b97cf4f12101 100644 --- a/src/js_parser/parse/parse_fn.rs +++ b/src/js_parser/parse/parse_fn.rs @@ -286,7 +286,7 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O if p.lexer.token == T::TColon { p.lexer.next()?; if !rest_arg { - if p.options.features.emit_decorator_metadata + if p.options.features.decorator_metadata.is_on() && opts.allow_ts_decorators && (opts.has_argument_decorators || opts.has_decorators @@ -372,7 +372,7 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O if p.lexer.token == T::TColon { p.lexer.next()?; - if p.options.features.emit_decorator_metadata + if p.options.features.decorator_metadata.is_on() && opts.allow_ts_decorators && (opts.has_argument_decorators || opts.has_decorators) { @@ -380,7 +380,7 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O } else { p.skip_typescript_return_type()?; } - } else if p.options.features.emit_decorator_metadata + } else if p.options.features.decorator_metadata.is_on() && opts.allow_ts_decorators && (opts.has_argument_decorators || opts.has_decorators) { diff --git a/src/js_parser/parse/parse_property.rs b/src/js_parser/parse/parse_property.rs index a0a597a0bab2..d40cb8ab5a1b 100644 --- a/src/js_parser/parse/parse_property.rs +++ b/src/js_parser/parse/parse_property.rs @@ -664,7 +664,7 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O // Skip over types if p.lexer.token == T::TColon { p.lexer.next()?; - if p.options.features.emit_decorator_metadata + if p.options.features.decorator_metadata.is_on() && opts.is_class && opts.ts_decorators.len() > 0 { diff --git a/src/js_parser/parse/parse_skip_typescript.rs b/src/js_parser/parse/parse_skip_typescript.rs index 5fb94136a3bc..44878474817d 100644 --- a/src/js_parser/parse/parse_skip_typescript.rs +++ b/src/js_parser/parse/parse_skip_typescript.rs @@ -16,6 +16,15 @@ use bun_ast::ts::Metadata; pub(crate) type SkipTypeOptionsBitset = typescript::SkipTypeOptionsBitset; impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_ONLY> { + /// tsc's `strictNullChecks`, as the decorator metadata serializer reads it. + #[inline] + fn strict_null_checks(&self) -> bool { + self.options + .features + .decorator_metadata + .strict_null_checks() + } + #[inline] pub(crate) fn skip_typescript_return_type(&mut self) -> Result<(), Error> { self.skip_type_script_type_with_opts::( @@ -802,7 +811,9 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O .expect("infallible: GET_METADATA implies Some")) .clone(); if let Some(final_) = - Metadata::finish_union(&mut left, |r| self.load_name_from_ref(r)) + Metadata::finish_union(&mut left, self.strict_null_checks(), |r| { + self.load_name_from_ref(r) + }) { // finish skipping the rest of the type without collecting type metadata. **result @@ -823,6 +834,7 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O result .as_deref_mut() .expect("infallible: GET_METADATA implies Some"), + self.strict_null_checks(), left, ); } @@ -846,9 +858,11 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O .as_mut() .expect("infallible: GET_METADATA implies Some")) .clone(); - if let Some(final_) = - Metadata::finish_intersection(&mut left, |r| self.load_name_from_ref(r)) - { + if let Some(final_) = Metadata::finish_intersection( + &mut left, + self.strict_null_checks(), + |r| self.load_name_from_ref(r), + ) { // finish skipping the rest of the type without collecting type metadata. **result .as_mut() @@ -868,6 +882,7 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O result .as_deref_mut() .expect("infallible: GET_METADATA implies Some"), + self.strict_null_checks(), left, ); } @@ -987,9 +1002,11 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O self.lexer.expect(T::TQuestion)?; let mut left = self.skip_type_script_type_with_metadata(Level::Lowest)?; self.lexer.expect(T::TColon)?; - if let Some(final_) = - Metadata::finish_intersection(&mut left, |r| self.load_name_from_ref(r)) - { + if let Some(final_) = Metadata::finish_intersection( + &mut left, + self.strict_null_checks(), + |r| self.load_name_from_ref(r), + ) { **result .as_mut() .expect("infallible: GET_METADATA implies Some") = final_; @@ -1004,6 +1021,7 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O result .as_deref_mut() .expect("infallible: GET_METADATA implies Some"), + self.strict_null_checks(), left, ); } diff --git a/src/js_parser/parser.rs b/src/js_parser/parser.rs index 455c70b23813..25e9c287aa61 100644 --- a/src/js_parser/parser.rs +++ b/src/js_parser/parser.rs @@ -232,7 +232,7 @@ pub mod Runtime { pub commonjs_at_runtime: bool, pub unwrap_commonjs_to_esm: bool, - pub emit_decorator_metadata: bool, + pub decorator_metadata: bun_ast::ts::DecoratorMetadata, pub standard_decorators: bool, /// If true and if the source is transpiled as cjs, don't wrap the module. @@ -301,7 +301,7 @@ pub mod Runtime { unwrap_commonjs_packages: &[], commonjs_at_runtime: false, unwrap_commonjs_to_esm: false, - emit_decorator_metadata: false, + decorator_metadata: bun_ast::ts::DecoratorMetadata::Off, standard_decorators: false, remove_cjs_module_wrapper: false, runtime_transpiler_cache: None, @@ -359,30 +359,30 @@ pub mod Runtime { pub(crate) fn hash_for_runtime_transpiler(&self, hasher: &mut Wyhash) { debug_assert!(self.runtime_transpiler_cache.is_some()); - let bools: [bool; 17] = [ - self.top_level_await, - self.auto_import_jsx, - self.allow_runtime, - self.inlining, - self.commonjs_named_exports, - self.minify_syntax, - self.minify_identifiers, - self.minify_keep_names, - self.dead_code_elimination, - self.set_breakpoint_on_first_line, - self.trim_unused_imports, - self.dont_bundle_twice, - self.commonjs_at_runtime, - self.emit_decorator_metadata, - self.standard_decorators, - self.lower_using, - self.repl_mode, + // `decorator_metadata as u8` keeps the bytes the old bool produced + // for `Off`/`Loose`, so only `Strict` entries get a new key. + let bytes: [u8; 17] = [ + self.top_level_await as u8, + self.auto_import_jsx as u8, + self.allow_runtime as u8, + self.inlining as u8, + self.commonjs_named_exports as u8, + self.minify_syntax as u8, + self.minify_identifiers as u8, + self.minify_keep_names as u8, + self.dead_code_elimination as u8, + self.set_breakpoint_on_first_line as u8, + self.trim_unused_imports as u8, + self.dont_bundle_twice as u8, + self.commonjs_at_runtime as u8, + self.decorator_metadata as u8, + self.standard_decorators as u8, + self.lower_using as u8, + self.repl_mode as u8, // note that we do not include .inject_jest_globals, as we bail out of the cache entirely if this is true ]; - // `[bool; N]` is N bytes of 0x00/0x01. - // `bool: NoUninit`, `u8: AnyBitPattern` → `cast_slice` is statically sound. - hasher.update(bytemuck::cast_slice::(&bools)); + hasher.update(&bytes); hasher.update(&[self.react_compiler as u8]); // Hash --feature flags. These directly affect transpiled output via diff --git a/src/jsc/RuntimeTranspilerStore.rs b/src/jsc/RuntimeTranspilerStore.rs index 5efa3a7879bb..9f1e70be5eb1 100644 --- a/src/jsc/RuntimeTranspilerStore.rs +++ b/src/jsc/RuntimeTranspilerStore.rs @@ -815,7 +815,7 @@ impl TranspilerJob { macro_remappings, macro_js_ctx: transpiler::default_macro_js_value(), jsx: transpiler.options.jsx.clone(), - emit_decorator_metadata: transpiler.options.emit_decorator_metadata, + decorator_metadata: transpiler.options.decorator_metadata, experimental_decorators: transpiler.options.experimental_decorators, use_define_for_class_fields: transpiler.options.use_define_for_class_fields, virtual_source: None, diff --git a/src/resolver/resolver.rs b/src/resolver/resolver.rs index 3615a6efe7fe..58ed8ed05df9 100644 --- a/src/resolver/resolver.rs +++ b/src/resolver/resolver.rs @@ -1586,9 +1586,12 @@ impl<'a> Resolver<'a> { if let Some(tsconfig) = dir.enclosing_tsconfig_json { result.jsx = tsconfig.merge_jsx(core::mem::take(&mut result.jsx)); - result.flags.set_emit_decorator_metadata( - result.flags.emit_decorator_metadata() || tsconfig.emit_decorator_metadata, - ); + // Only the primary path is parsed. + if primary { + result + .flags + .set_decorator_metadata(tsconfig.decorator_metadata()); + } result.flags.set_experimental_decorators( result.flags.experimental_decorators() || tsconfig.experimental_decorators, ); @@ -6558,6 +6561,12 @@ impl<'a> Resolver<'a> { if let Some(v) = parent_config.use_define_for_class_fields { mc.use_define_for_class_fields = Some(v); } + if let Some(v) = parent_config.strict { + mc.strict = Some(v); + } + if let Some(v) = parent_config.strict_null_checks { + mc.strict_null_checks = Some(v); + } if !parent_config.base_url.is_empty() { mc.base_url = core::mem::take(&mut parent_config.base_url); } diff --git a/src/resolver/result.rs b/src/resolver/result.rs index 15acdfd35711..5b22dfa78be7 100644 --- a/src/resolver/result.rs +++ b/src/resolver/result.rs @@ -6,6 +6,7 @@ use std::io::Write as _; use ::bun_ast::import_record as ast; +use ::bun_ast::ts::DecoratorMetadata; use ::bun_install_types::resolver_hooks as Install; use bun_alloc as allocators; use bun_core::MutableString; @@ -133,6 +134,8 @@ bitflags::bitflags! { const IS_STANDALONE_MODULE = 1 << 2; // This is true when the package was loaded from within the node_modules directory. const IS_FROM_NODE_MODULES = 1 << 3; + // Bits 4..=5 encode [`DecoratorMetadata`]; write via `set_decorator_metadata`. + const DECORATOR_METADATA_STRICT = 1 << 4; const EMIT_DECORATOR_METADATA = 1 << 5; const EXPERIMENTAL_DECORATORS = 1 << 6; /// tsconfig `"useDefineForClassFields": false` was set explicitly. @@ -191,12 +194,16 @@ impl ResultFlags { self.set(Self::IS_FROM_NODE_MODULES, v) } #[inline] - pub fn emit_decorator_metadata(self) -> bool { - self.contains(Self::EMIT_DECORATOR_METADATA) + pub fn decorator_metadata(self) -> DecoratorMetadata { + DecoratorMetadata::new( + self.contains(Self::EMIT_DECORATOR_METADATA), + self.contains(Self::DECORATOR_METADATA_STRICT), + ) } #[inline] - pub(crate) fn set_emit_decorator_metadata(&mut self, v: bool) { - self.set(Self::EMIT_DECORATOR_METADATA, v) + pub(crate) fn set_decorator_metadata(&mut self, mode: DecoratorMetadata) { + self.set(Self::EMIT_DECORATOR_METADATA, mode.is_on()); + self.set(Self::DECORATOR_METADATA_STRICT, mode.strict_null_checks()); } #[inline] pub fn experimental_decorators(self) -> bool { diff --git a/src/resolver/tsconfig_json.rs b/src/resolver/tsconfig_json.rs index 753c29d75067..fbe37da3c2c9 100644 --- a/src/resolver/tsconfig_json.rs +++ b/src/resolver/tsconfig_json.rs @@ -132,6 +132,10 @@ pub(crate) enum JsxField { pub(crate) type JsxFieldSet = EnumSet; +/// `strictNullChecks` when a tsconfig sets neither it nor `strict`. This is +/// the TypeScript 5 default. TypeScript 6 turned `strict` on by default. +pub const STRICT_NULL_CHECKS_DEFAULT: bool = false; + pub struct TSConfigJSON { pub(crate) abs_path: Box<[u8]>, @@ -163,6 +167,10 @@ pub struct TSConfigJSON { pub experimental_decorators: bool, /// `None` = unset (keeps native [[Define]] class-field semantics). pub use_define_for_class_fields: Option, + /// `None` = unset. Only read as the fallback for `strictNullChecks`. + pub strict: Option, + /// `None` = unset, then `strict` decides. + pub strict_null_checks: Option, } impl Default for TSConfigJSON { @@ -179,6 +187,8 @@ impl Default for TSConfigJSON { emit_decorator_metadata: false, experimental_decorators: false, use_define_for_class_fields: None, + strict: None, + strict_null_checks: None, } } } @@ -226,6 +236,18 @@ impl TSConfigJSON { !self.base_url.is_empty() } + /// tsc's `getStrictOptionValue`: explicit `strictNullChecks`, else `strict`, else off. + pub fn strict_null_checks(&self) -> bool { + self.strict_null_checks + .or(self.strict) + .unwrap_or(STRICT_NULL_CHECKS_DEFAULT) + } + + /// The `design:*` metadata mode this tsconfig asks for. + pub fn decorator_metadata(&self) -> bun_ast::ts::DecoratorMetadata { + bun_ast::ts::DecoratorMetadata::new(self.emit_decorator_metadata, self.strict_null_checks()) + } + pub fn merge_jsx(&self, current: options::jsx::Pragma) -> options::jsx::Pragma { let mut out = current; @@ -375,6 +397,8 @@ impl TSConfigJSON { let mut emit_decorator_metadata_v: Option<&bun_ast::E::JsonValue> = None; let mut experimental_decorators_v: Option<&bun_ast::E::JsonValue> = None; let mut use_define_for_class_fields_v: Option<&bun_ast::E::JsonValue> = None; + let mut strict_v: Option<&bun_ast::E::JsonValue> = None; + let mut strict_null_checks_v: Option<&bun_ast::E::JsonValue> = None; let mut jsx_factory_v: Option<(&bun_ast::E::JsonValue, bun_ast::Loc)> = None; let mut jsx_fragment_factory_v: Option<(&bun_ast::E::JsonValue, bun_ast::Loc)> = None; let mut jsx_v: Option<&bun_ast::E::JsonValue> = None; @@ -398,6 +422,10 @@ impl TSConfigJSON { b"useDefineForClassFields" if use_define_for_class_fields_v.is_none() => { use_define_for_class_fields_v = Some(value) } + b"strict" if strict_v.is_none() => strict_v = Some(value), + b"strictNullChecks" if strict_null_checks_v.is_none() => { + strict_null_checks_v = Some(value) + } b"jsxFactory" if jsx_factory_v.is_none() => { jsx_factory_v = Some((value, loc)) } @@ -447,6 +475,16 @@ impl TSConfigJSON { result.use_define_for_class_fields = Some(val); } + // Parse "strict" + if let Some(&bun_ast::E::JsonValue::Boolean(val)) = strict_v { + result.strict = Some(val); + } + + // Parse "strictNullChecks" + if let Some(&bun_ast::E::JsonValue::Boolean(val)) = strict_null_checks_v { + result.strict_null_checks = Some(val); + } + // Parse "jsxFactory" if let Some((jsx_prop, loc)) = jsx_factory_v { if let Some(factory) = jsx_prop diff --git a/src/runtime/api/JSTranspiler.rs b/src/runtime/api/JSTranspiler.rs index df5d4bc5f529..10788a2e09e1 100644 --- a/src/runtime/api/JSTranspiler.rs +++ b/src/runtime/api/JSTranspiler.rs @@ -9,6 +9,7 @@ use crate::node::{StringOrBuffer, ThreadIsolated}; use bun_alloc::{Arena, ArenaVec}; // bumpalo::Bump / bumpalo::collections::Vec re-exports use bun_ast::Expr; use bun_ast::Loader; +use bun_ast::ts::DecoratorMetadata; use bun_ast::{ImportRecord, ImportRecordFlags}; use bun_bundler::options::{self, PackagesOption, SourceMapOption}; use bun_bundler::transpiler::{MacroJSCtx, ParseOptions, ParseResult}; @@ -776,7 +777,8 @@ impl TransformTask { virtual_source: Some(source), replace_exports: self.replace_exports.entries.clone().expect("OOM"), experimental_decorators: tsconfig.is_some_and(|ts| ts.experimental_decorators), - emit_decorator_metadata: tsconfig.is_some_and(|ts| ts.emit_decorator_metadata), + decorator_metadata: tsconfig + .map_or(DecoratorMetadata::Off, |ts| ts.decorator_metadata()), use_define_for_class_fields: tsconfig .and_then(|ts| ts.use_define_for_class_fields) .unwrap_or(true), @@ -1235,10 +1237,10 @@ impl JSTranspiler { .tsconfig .as_deref() .is_some_and(|ts| ts.experimental_decorators), - emit_decorator_metadata: config + decorator_metadata: config .tsconfig .as_deref() - .is_some_and(|ts| ts.emit_decorator_metadata), + .map_or(DecoratorMetadata::Off, |ts| ts.decorator_metadata()), use_define_for_class_fields: config .tsconfig .as_deref() diff --git a/src/runtime/jsc_hooks.rs b/src/runtime/jsc_hooks.rs index 8d9df13ac8a5..bfca3857d853 100644 --- a/src/runtime/jsc_hooks.rs +++ b/src/runtime/jsc_hooks.rs @@ -2520,8 +2520,8 @@ fn transpile_source_code_inner( // SAFETY: per fn contract — `jsc_vm` is the live per-thread VM. jsx: unsafe { &*jsc_vm }.transpiler.options.jsx.clone(), // SAFETY: per fn contract — `jsc_vm` is the live per-thread VM. - emit_decorator_metadata: unsafe { - (*jsc_vm).transpiler.options.emit_decorator_metadata + decorator_metadata: unsafe { + (*jsc_vm).transpiler.options.decorator_metadata }, // SAFETY: per fn contract — `jsc_vm` is the live per-thread VM. experimental_decorators: unsafe { diff --git a/test/bundler/transpiler/decorator-metadata.test.ts b/test/bundler/transpiler/decorator-metadata.test.ts index 41d1b95fc8a1..d3a52ca21804 100644 --- a/test/bundler/transpiler/decorator-metadata.test.ts +++ b/test/bundler/transpiler/decorator-metadata.test.ts @@ -1,3 +1,5 @@ +import { bunEnv, bunExe, tempDir } from "harness"; +import { join } from "path"; import "reflect-metadata"; describe("decorator metadata", () => { @@ -7,6 +9,21 @@ describe("decorator metadata", () => { class Swag {} class A_1 {} + // `null` and `undefined` in a union or intersection are elided unless the + // tsconfig this file is transpiled under turns `strictNullChecks` on + // (directly or through `strict`). Then they serialize as `void 0`, so + // `string | undefined` becomes `Object`, like tsc. The cwd's tsconfig + // decides at runtime, so read the mode off a sentinel instead of pinning + // one. The "strictNullChecks" block below pins each tsconfig combination. + // @ts-ignore + @d1 + class Sentinel { + constructor(p: string | undefined) {} + } + const strictNullChecks = Reflect.getMetadata("design:paramtypes", Sentinel)[0] === Object; + const nullableString = strictNullChecks ? Object : String; + const nullableSwag = strictNullChecks ? Object : Swag; + // @ts-ignore @d1 class A { @@ -226,10 +243,10 @@ describe("decorator metadata", () => { expect(received[24]).toBe(Object); expect(received[25]).toBe(void 0); expect(received[26]).toBe(void 0); - expect(received[27]).toBe(String); - expect(received[28]).toBe(String); - expect(received[29]).toBe(String); - expect(received[30]).toBe(String); + expect(received[27]).toBe(nullableString); + expect(received[28]).toBe(nullableString); + expect(received[29]).toBe(nullableString); + expect(received[30]).toBe(nullableString); expect(received[31]).toBe(Object); expect(received[32]).toBe(Object); expect(received[33]).toBe(String); @@ -238,10 +255,10 @@ describe("decorator metadata", () => { expect(received[36]).toBe(Object); expect(received[37]).toBe(String); expect(received[38]).toBe(String); - expect(received[39]).toBe(String); - expect(received[40]).toBe(String); - expect(received[41]).toBe(String); - expect(received[42]).toBe(String); + expect(received[39]).toBe(nullableString); + expect(received[40]).toBe(nullableString); + expect(received[41]).toBe(nullableString); + expect(received[42]).toBe(nullableString); expect(received[43]).toBe(Object); expect(received[44]).toBe(Object); expect(received[45]).toBe(Object); @@ -256,10 +273,10 @@ describe("decorator metadata", () => { expect(received[54]).toBe(Object); expect(received[55]).toBe(Swag); expect(received[56]).toBe(Swag); - expect(received[57]).toBe(Swag); - expect(received[58]).toBe(Swag); - expect(received[59]).toBe(Swag); - expect(received[60]).toBe(Swag); + expect(received[57]).toBe(nullableSwag); + expect(received[58]).toBe(nullableSwag); + expect(received[59]).toBe(nullableSwag); + expect(received[60]).toBe(nullableSwag); expect(received[61]).toBe(Object); expect(received[62]).toBe(Object); expect(received[63]).toBe(Object); @@ -268,10 +285,10 @@ describe("decorator metadata", () => { expect(received[66]).toBe(Object); expect(received[67]).toBe(void 0); expect(received[68]).toBe(void 0); - expect(received[69]).toBe(Swag); - expect(received[70]).toBe(Swag); - expect(received[71]).toBe(Swag); - expect(received[72]).toBe(Swag); + expect(received[69]).toBe(nullableSwag); + expect(received[70]).toBe(nullableSwag); + expect(received[71]).toBe(nullableSwag); + expect(received[72]).toBe(nullableSwag); expect(received[73]).toBe(Object); expect(received[74]).toBe(Object); expect(received[75]).toBe(Swag); @@ -543,4 +560,357 @@ describe("decorator metadata", () => { expect(Reflect.getMetadata("design:type", A.prototype, "method4")).toBe(Function); expect(Reflect.getMetadata("design:returntype", A.prototype, "method4")).toBeUndefined(); }); + describe("strictNullChecks", () => { + // A tiny stand-in for reflect-metadata: it records what the transpiler + // passes as design:paramtypes for the decorated method. + const probe = /* ts */ ` + const seen: unknown[][] = []; + (Reflect as any).metadata = (key: string, value: unknown) => () => { + if (key === "design:paramtypes") seen.push(value as unknown[]); + }; + function d(): any { + return () => {}; + } + class Dto {} + class Probe { + @d() + method( + @d() a: string | undefined, + @d() b: string | null, + @d() c: number | undefined, + @d() e: Dto | undefined, + @d() f: string, + @d() g: null | undefined, + @d() h: string & null, + ) {} + } + console.log(seen[0].map(t => (t === undefined ? "undefined" : (t as any).name)).join(" ")); + `; + const loose = "String String Number Dto String undefined String"; + const strict = "Object Object Object Object String undefined Object"; + + const tsconfig = (compilerOptions: Record) => + JSON.stringify({ + compilerOptions: { experimentalDecorators: true, emitDecoratorMetadata: true, ...compilerOptions }, + }); + + test.concurrent.each([ + ["neither key", {}, loose], + ["strict: true", { strict: true }, strict], + ["strict: false", { strict: false }, loose], + ["strictNullChecks: true", { strictNullChecks: true }, strict], + ["strictNullChecks: false", { strictNullChecks: false }, loose], + ["strict: true, strictNullChecks: false", { strict: true, strictNullChecks: false }, loose], + ["strict: false, strictNullChecks: true", { strict: false, strictNullChecks: true }, strict], + ])("runtime transpiler honours %s", async (_, compilerOptions, expected) => { + using dir = tempDir("decorator-metadata-strict", { + "tsconfig.json": tsconfig(compilerOptions), + "index.ts": probe, + }); + await using proc = Bun.spawn({ + cmd: [bunExe(), "index.ts"], + env: bunEnv, + cwd: String(dir), + stderr: "pipe", + }); + const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); + expect(stderr).toBe(""); + expect(stdout).toBe(expected + "\n"); + expect(exitCode).toBe(0); + }); + + test.concurrent.each([ + ["inherits strict from the base", { strict: true }, {}, strict], + [ + "child strictNullChecks: false wins over base strict: true", + { strict: true }, + { strictNullChecks: false }, + loose, + ], + ["child strict: true wins over base strict: false", { strict: false }, { strict: true }, strict], + ])("tsconfig extends: %s", async (_, base, child, expected) => { + using dir = tempDir("decorator-metadata-strict-extends", { + "tsconfig.base.json": JSON.stringify({ compilerOptions: base }), + "tsconfig.json": JSON.stringify({ + extends: "./tsconfig.base.json", + compilerOptions: { experimentalDecorators: true, emitDecoratorMetadata: true, ...child }, + }), + "index.ts": probe, + }); + await using proc = Bun.spawn({ + cmd: [bunExe(), "index.ts"], + env: bunEnv, + cwd: String(dir), + stderr: "pipe", + }); + const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); + expect(stderr).toBe(""); + expect(stdout).toBe(expected + "\n"); + expect(exitCode).toBe(0); + }); + + test.concurrent.each([ + ["neither key", {}, loose], + ["strict: true", { strict: true }, strict], + ["strictNullChecks: true", { strictNullChecks: true }, strict], + ["strict: true, strictNullChecks: false", { strict: true, strictNullChecks: false }, loose], + ])("Bun.build honours %s", async (_, compilerOptions, expected) => { + using dir = tempDir("decorator-metadata-strict-build", { + "tsconfig.json": tsconfig(compilerOptions), + "index.ts": probe, + }); + const result = await Bun.build({ + entrypoints: [join(String(dir), "index.ts")], + target: "bun", + }); + expect(result.success).toBe(true); + await using proc = Bun.spawn({ + cmd: [bunExe(), "-e", await result.outputs[0].text()], + env: bunEnv, + cwd: String(dir), + stderr: "pipe", + }); + const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); + expect(stderr).toBe(""); + expect(stdout).toBe(expected + "\n"); + expect(exitCode).toBe(0); + }); + + // The 174 constructor parameter types of the "type serialization" test + // above, run under an explicit tsconfig in each mode. The expected strings + // are what tsc 6.0.2 emits for the same class (`tsc -p .` then run the + // output): they differ at indices 27 to 30, 39 to 42, 57 to 60 and 69 to 72. + const types = [ + "any", + "unknown", + "never", + "void", + "null", + "undefined", + "number", + "string", + "boolean", + "symbol", + "bigint", + "object", + "() => {}", + "[]", + "{}", + "123", + "123n", + '"123"', + "`123`", + "true", + "false", + "Map", + "Set", + "Known", + "Unknown", + "never & string", + "string & never", + "null & string", + "string & null", + "undefined & string", + "string & undefined", + "void & string", + "string & void", + "unknown & string", + "string & unknown", + "any & string", + "string & any", + "never | string", + "string | never", + "null | string", + "string | null", + "undefined | string", + "string | undefined", + "void | string", + "string | void", + "unknown | string", + "string | unknown", + "any | string", + "string | any", + "string | string", + "string & string", + "Known | Swag", + "Swag | Known", + "Known & Swag", + "Swag & Known", + "never | Swag", + "Swag | never", + "null | Swag", + "Swag | null", + "undefined | Swag", + "Swag | undefined", + "void | Swag", + "Swag | void", + "unknown | Swag", + "Swag | unknown", + "any | Swag", + "Swag | any", + "never & Swag", + "Swag & never", + "null & Swag", + "Swag & null", + "undefined & Swag", + "Swag & undefined", + "void & Swag", + "Swag & void", + "unknown & Swag", + "Swag & unknown", + "any & Swag", + "Swag & any", + "Swag | Swag", + "Swag & Swag", + "Unknown | Known", + "Known | Unknown", + "Unknown & Known", + "Known & Unknown", + "Unknown | Unknown", + "Unknown & Unknown", + "never | never", + "never & never", + "null | null", + "null & null", + "undefined | undefined", + "undefined & undefined", + "void | void", + "void & void", + "unknown | unknown", + "unknown & unknown", + "any | any", + "any & any", + "never | void", + "void | never", + "null | void", + "void | null", + "undefined | void", + "void | undefined", + "void | void", + "void & void", + "unknown | void", + "void | unknown", + "any | void", + "void | any", + "never | unknown", + "unknown | never", + "null | unknown", + "unknown | null", + "undefined | unknown", + "unknown | undefined", + "void | unknown", + "unknown | void", + "unknown | unknown", + "unknown & unknown", + "any | unknown", + "unknown | any", + "never | any", + "any | never", + "null | any", + "any | null", + "undefined | any", + "any | undefined", + "void | any", + "any | void", + "unknown | any", + "any | unknown", + "any | any", + "never & void", + "void & never", + "null & void", + "void & null", + "undefined & void", + "void & undefined", + "void & void", + "void | void", + "unknown & void", + "void & unknown", + "any & void", + "void & any", + "never & unknown", + "unknown & never", + "null & unknown", + "unknown & null", + "undefined & unknown", + "unknown & undefined", + "void & unknown", + "unknown & void", + "unknown & unknown", + "unknown | unknown", + "any & unknown", + "unknown & any", + "never & any", + "any & never", + "null & any", + "any & null", + "undefined & any", + "any & undefined", + "void & any", + "any & void", + "unknown & any", + "any & unknown", + "any & any", + "string & number & boolean & never & symbol", + '"foo" | A_1', + "true | boolean", + '"foo" | boolean', + 'A_1 | "foo"', + ]; + + const tscLoose = + "Object Object undefined undefined undefined undefined Number String Boolean Symbol BigInt Object Function Array Object Number BigInt String String Boolean Boolean Map Set Known Object undefined undefined String String String String Object Object String String Object Object String String String String String String Object Object Object Object Object Object String String Object Object Object Object Swag Swag Swag Swag Swag Swag Object Object Object Object Object Object undefined undefined Swag Swag Swag Swag Object Object Swag Swag Object Object Swag Swag Object Object Object Object Object Object undefined undefined undefined undefined undefined undefined undefined undefined Object undefined Object Object undefined undefined undefined undefined undefined undefined undefined undefined Object Object Object Object Object Object Object Object Object Object Object Object Object undefined Object Object Object Object Object Object Object Object Object Object Object Object Object undefined undefined undefined undefined undefined undefined undefined undefined undefined undefined Object Object undefined undefined undefined undefined undefined undefined undefined undefined undefined Object Object Object undefined Object Object Object Object Object Object Object Object Object Object Object Object Boolean Object Object"; + const tscStrict = + "Object Object undefined undefined undefined undefined Number String Boolean Symbol BigInt Object Function Array Object Number BigInt String String Boolean Boolean Map Set Known Object undefined undefined Object Object Object Object Object Object String String Object Object String String Object Object Object Object Object Object Object Object Object Object String String Object Object Object Object Swag Swag Object Object Object Object Object Object Object Object Object Object undefined undefined Object Object Object Object Object Object Swag Swag Object Object Swag Swag Object Object Object Object Object Object undefined undefined undefined undefined undefined undefined undefined undefined Object undefined Object Object undefined undefined undefined undefined undefined undefined undefined undefined Object Object Object Object Object Object Object Object Object Object Object Object Object undefined Object Object Object Object Object Object Object Object Object Object Object Object Object undefined undefined undefined undefined undefined undefined undefined undefined undefined undefined Object Object undefined undefined undefined undefined undefined undefined undefined undefined undefined Object Object Object undefined Object Object Object Object Object Object Object Object Object Object Object Object Boolean Object Object"; + + test.concurrent.each([ + ["strict: false", { strict: false }, tscLoose], + ["strict: true", { strict: true }, tscStrict], + ])("all 174 type serialization cases match tsc with %s", async (_, compilerOptions, expected) => { + using dir = tempDir("decorator-metadata-strict-table", { + "tsconfig.json": tsconfig(compilerOptions), + "index.ts": ` + const seen: unknown[][] = []; + (Reflect as any).metadata = (key: string, value: unknown) => () => { + if (key === "design:paramtypes") seen.push(value as unknown[]); + }; + function d1() {} + class Known {} + class Swag {} + class A_1 {} + // @ts-ignore + @d1 + class A { + constructor(${types.map((t, i) => `p${i}: ${t}`).join(", ")}) {} + } + console.log(seen[0].map(t => (t === undefined ? "undefined" : (t as any).name)).join(" ")); + `, + }); + await using proc = Bun.spawn({ + cmd: [bunExe(), "index.ts"], + env: bunEnv, + cwd: String(dir), + stderr: "pipe", + }); + const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); + expect(stderr).toBe(""); + expect(stdout.trim().split(" ")).toEqual(expected.split(" ")); + expect(exitCode).toBe(0); + }); + + test.each([ + ["neither key", {}, "String"], + ["strict: true", { strict: true }, "Object"], + ["strictNullChecks: true", { strictNullChecks: true }, "Object"], + ["strict: true, strictNullChecks: false", { strict: true, strictNullChecks: false }, "String"], + ])("Bun.Transpiler honours %s", (_, compilerOptions, expected) => { + const transpiler = new Bun.Transpiler({ loader: "ts", tsconfig: tsconfig(compilerOptions) }); + const out = transpiler.transformSync(` + function d(): any { return () => {}; } + class Probe { + @d() method(@d() a: string | undefined) {} + } + `); + expect(out).toMatch(new RegExp(`"design:paramtypes", \\[\\s*${expected}\\s*\\]`)); + }); + }); });