Skip to content
Closed

ai slop #36754

Show file tree
Hide file tree
Changes from 14 commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
a284f83
runtime: load TypeScript declaration files as type shims so their exp…
robobun Aug 2, 2026
1d09776
Address review: dedup condition selection, stricter tests
robobun Aug 2, 2026
50ecbf6
Tighten comments
robobun Aug 2, 2026
fc5e62f
Gate declaration-file routes to module scope, synthesize type-only de…
robobun Aug 2, 2026
1872e72
Drop stale commented-out args in moved export-star body
robobun Aug 2, 2026
01c44fd
Keep for_kind behavior-preserving for CSS import kinds
robobun Aug 2, 2026
ecf4a52
Suppress type-specifier retention in ambient bodies, reuse path buffe…
robobun Aug 2, 2026
d0c8d9e
Propagate is_export through the type-alias fallthrough arm
robobun Aug 2, 2026
b9a299f
Synthesize only one default export for overloaded signatures
robobun Aug 2, 2026
ca39f1c
Let export clauses win over synthesized exports for the same name
robobun Aug 2, 2026
881610d
Cover the .jsx declaration-sibling mapping in the extension matrix test
robobun Aug 2, 2026
aa21572
Defer the type-only default-export stub to the end of the parse pass
robobun Aug 2, 2026
b73485c
[autofix.ci] apply automated fixes
autofix-ci[bot] Aug 2, 2026
f3ff602
Record star-export aliases and import-equals names in declaration files
robobun Aug 2, 2026
cbf17f5
Drop unused parameter from the default-export stub helper
robobun Aug 2, 2026
a026d5b
Let a default clause alias win over the default-export stub
robobun Aug 2, 2026
1a73691
Route declare var/let/const through the unified declaration synthesis
robobun Aug 2, 2026
d331322
Guard the namespace local_type_names entry on the identifier form
robobun Aug 2, 2026
5573aa7
Defer clause exports to real exported declarations in declaration files
robobun Aug 2, 2026
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
7 changes: 7 additions & 0 deletions src/ast/loader.rs
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,13 @@ bun_core::comptime_string_map! {
};
}

/// tsc's `isDeclarationFileName`, minus the rare `.d.*.ts` form.
pub fn is_type_script_declaration_file(path: &[u8]) -> bool {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there some more canonical function that offers this behaviour? Module resolution is a complicated topic. The code that implements it must be extremely clear and easy to understand and read, and should be colocated. Maybe make a pull request before this one that improves and refactors and restructures many parts of module resolution; stacking this PR afterwards on top.

bun_core::strings::has_suffix_comptime(path, b".d.ts")
|| bun_core::strings::has_suffix_comptime(path, b".d.mts")
|| bun_core::strings::has_suffix_comptime(path, b".d.cts")
}

impl Loader {
#[inline]
pub fn is_css(self) -> bool {
Expand Down
2 changes: 2 additions & 0 deletions src/bundler/ParseTask.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2395,6 +2395,8 @@ pub mod parse_worker {
loader,
);
opts.bundle = true;
opts.typescript_declaration_file = loader.is_typescript()
&& bun_ast::loader::is_type_script_declaration_file(source.path.text);
opts.warn_about_unbundled_modules = false;
// `AllowUnresolved` is the same nominal type on
// both sides (re-export in options.rs). `'static` erasure: `topts` borrows
Expand Down
48 changes: 37 additions & 11 deletions src/bundler/bundle_v2.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2194,12 +2194,29 @@ pub mod bv2_impl {

let mut had_busted_dir_cache = false;
let resolve_result: _resolver::Result = loop {
// SAFETY: see `transpiler` note above.
match unsafe { &mut *transpiler }.resolver.resolve(
source_dir,
&import_record.specifier,
import_record.kind,
) {
// Per-resolve flag; see
// `Resolver::importer_is_type_script_declaration_file`.
Comment thread
robobun marked this conversation as resolved.
let resolved = {
// SAFETY: see `transpiler` note above.
unsafe { &mut *transpiler }
.resolver
.importer_is_type_script_declaration_file =
bun_ast::loader::is_type_script_declaration_file(
&import_record.source_file,
);
// SAFETY: see `transpiler` note above.
let r = unsafe { &mut *transpiler }.resolver.resolve(
source_dir,
&import_record.specifier,
import_record.kind,
);
// SAFETY: see `transpiler` note above.
unsafe { &mut *transpiler }
.resolver
.importer_is_type_script_declaration_file = false;
r
};
match resolved {
Ok(r) => break r,
Err(err) => {
// Only perform directory busting when hot-reloading is enabled
Expand Down Expand Up @@ -6063,11 +6080,20 @@ pub mod bv2_impl {

let mut had_busted_dir_cache = false;
let resolve_result: _resolver::Result = 'inner: loop {
match transpiler.resolver.resolve_with_framework(
source_dir,
import_record.path.text,
import_record.kind,
) {
// Per-resolve flag; see
// `Resolver::importer_is_type_script_declaration_file`.
Comment thread
robobun marked this conversation as resolved.
let resolved = {
transpiler.resolver.importer_is_type_script_declaration_file =
bun_ast::loader::is_type_script_declaration_file(source.path.text);
let r = transpiler.resolver.resolve_with_framework(
source_dir,
import_record.path.text,
import_record.kind,
);
transpiler.resolver.importer_is_type_script_declaration_file = false;
r
};
match resolved {
Ok(r) => break r,
Err(err) => {
// borrowck — `log_for_resolution_failures` returns
Expand Down
23 changes: 23 additions & 0 deletions src/bundler/options.rs
Original file line number Diff line number Diff line change
Expand Up @@ -713,6 +713,10 @@ pub struct ESMConditions {
pub(crate) import: ConditionsMap,
pub(crate) require: ConditionsMap,
pub(crate) style: ConditionsMap,
/// `import` / `require` plus the "types" condition, for declaration-file
/// importers.
Comment thread
robobun marked this conversation as resolved.
pub(crate) import_types: ConditionsMap,
pub(crate) require_types: ConditionsMap,
}

impl ESMConditions {
Expand Down Expand Up @@ -763,11 +767,18 @@ impl ESMConditions {
require_condition_map.insert(b"default".as_slice(), ());
style_condition_map.insert(b"default".as_slice(), ());

let mut import_types_condition_map = import_condition_map.clone()?;
import_types_condition_map.insert(b"types".as_slice(), ());
let mut require_types_condition_map = require_condition_map.clone()?;
require_types_condition_map.insert(b"types".as_slice(), ());

Ok(ESMConditions {
default: default_condition_amp,
import: import_condition_map,
require: require_condition_map,
style: style_condition_map,
import_types: import_types_condition_map,
require_types: require_types_condition_map,
})
}

Expand All @@ -776,12 +787,16 @@ impl ESMConditions {
let import = self.import.clone()?;
let require = self.require.clone()?;
let style = self.style.clone()?;
let import_types = self.import_types.clone()?;
let require_types = self.require_types.clone()?;

Ok(ESMConditions {
default,
import,
require,
style,
import_types,
require_types,
})
}

Expand All @@ -790,12 +805,16 @@ impl ESMConditions {
self.import.reserve(conditions.len());
self.require.reserve(conditions.len());
self.style.reserve(conditions.len());
self.import_types.reserve(conditions.len());
self.require_types.reserve(conditions.len());

for condition in conditions {
self.default.insert(*condition, ());
self.import.insert(*condition, ());
self.require.insert(*condition, ());
self.style.insert(*condition, ());
self.import_types.insert(*condition, ());
self.require_types.insert(*condition, ());
}
Ok(())
}
Expand Down Expand Up @@ -1438,6 +1457,8 @@ impl<'a> BundleOptions<'a> {
import: bun_core::handle_oom(self.conditions.import.clone()),
require: bun_core::handle_oom(self.conditions.require.clone()),
style: bun_core::handle_oom(self.conditions.style.clone()),
import_types: bun_core::handle_oom(self.conditions.import_types.clone()),
require_types: bun_core::handle_oom(self.conditions.require_types.clone()),
},
tree_shaking: self.tree_shaking,
tree_shaking_override: self.tree_shaking_override,
Expand Down Expand Up @@ -1692,6 +1713,8 @@ impl<'a> BundleOptions<'a> {
import: Default::default(),
require: Default::default(),
style: Default::default(),
import_types: Default::default(),
require_types: Default::default(),
}, // filled below
tree_shaking: false,
tree_shaking_override: None,
Expand Down
4 changes: 4 additions & 0 deletions src/bundler/transpiler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1075,6 +1075,8 @@ fn resolver_bundle_options_subset(
import: src.conditions.import.clone().expect("oom"),
require: src.conditions.require.clone().expect("oom"),
style: src.conditions.style.clone().expect("oom"),
import_types: src.conditions.import_types.clone().expect("oom"),
require_types: src.conditions.require_types.clone().expect("oom"),
},
external: src.external.clone(),
extra_cjs_extensions: src.extra_cjs_extensions.clone(),
Expand Down Expand Up @@ -1536,6 +1538,8 @@ impl<'a> Transpiler<'a> {
use js_ast::parser::options as p_opts;
let mut opts = js_ast::ParserOptions::<'_> {
ts: loader.is_typescript(),
typescript_declaration_file: loader.is_typescript()
&& bun_ast::loader::is_type_script_declaration_file(source.path.text),
jsx: to_parser_jsx_pragma(jsx),
keep_names: true,
ignore_dce_annotations: self.options.ignore_dce_annotations,
Expand Down
131 changes: 130 additions & 1 deletion src/js_parser/p.rs
Original file line number Diff line number Diff line change
Expand Up @@ -345,6 +345,21 @@
pub(crate) is_exported_inside_namespace: RefRefMap,
pub(crate) local_type_names: StringBoolMap,

/// Declaration-file mode: module-scope type-only declaration names, in
/// source order, to synthesize `var` bindings for at the end of the parse
/// pass. The map value is whether the declaration was exported.
Comment thread
robobun marked this conversation as resolved.
pub(crate) dts_type_names: StringBoolMap,
pub(crate) dts_type_name_order: Vec<&'a [u8]>,
/// True inside a `declare global { ... }` body, which shares the
/// module-scope parse options but must not synthesize bindings.
Comment thread
robobun marked this conversation as resolved.
pub(crate) dts_suppress_type_name_recording: bool,
/// A type-only `export default` was elided; the end of the parse pass
/// synthesizes one default export unless a real one exists.
Comment thread
robobun marked this conversation as resolved.
pub(crate) dts_needs_default_export_stub: bool,
/// Aliases exported by module-scope export clauses; synthesized bindings
/// for these names stay non-exported so the clause is the sole export.
Comment thread
robobun marked this conversation as resolved.
pub(crate) dts_export_clause_aliases: StringBoolMap,

// This is the reference to the generated function argument for the namespace,
// which is different than the reference to the namespace itself:
//
Expand Down Expand Up @@ -4529,6 +4544,107 @@
ref_
}

/// Declaration-file mode: queue a type-only declaration's name for
/// `synthesize_declaration_file_bindings`.
Comment thread
robobun marked this conversation as resolved.
pub(crate) fn record_declaration_file_type_name(
&mut self,
name: &'a [u8],
is_export: bool,
) -> Result<(), crate::Error> {
if !Self::IS_TYPESCRIPT_ENABLED
|| !self.options.typescript_declaration_file
|| self.dts_suppress_type_name_recording
|| !js_lexer::is_identifier(name)
{
return Ok(());
}
match self.dts_type_names.get(name) {
Some(&prev_is_export) => {
if is_export && !prev_is_export {
self.dts_type_names.put(name, true)?;
}
}
None => {
self.dts_type_names.put(name, is_export)?;
self.dts_type_name_order.push(name);
}
}
Ok(())
}

/// Declaration-file mode: synthesize `var` bindings (undefined) for the
/// recorded type-only declarations so re-export chains link. Names with a
/// real binding (declaration merging, imports) are skipped.
Comment thread
robobun marked this conversation as resolved.
pub(crate) fn synthesize_declaration_file_bindings(
&mut self,
stmts: &mut BumpVec<'a, Stmt>,
) -> Result<(), crate::Error> {
// A type-only default export produces a stub only when no real
// default export exists (class/interface declaration merging on the
// default export keeps the real one).
Comment thread
robobun marked this conversation as resolved.
Outdated
if self.dts_needs_default_export_stub
&& !stmts
.iter()
.any(|s| matches!(s.data, js_ast::StmtData::SExportDefault(_)))
{
self.has_es_module_syntax = true;
let default_name = self.create_default_name(bun_ast::Loc::EMPTY);
let value = js_ast::StmtOrExpr::Expr(
self.new_expr(js_ast::E::Undefined {}, bun_ast::Loc::EMPTY),
);
let stmt = self.s(
S::ExportDefault {
default_name,
value,
},
bun_ast::Loc::EMPTY,

Check failure on line 4600 in src/js_parser/p.rs

View check run for this annotation

Claude / Claude Code Review

Deferred default-export stub misses clause-form default exports

The default-export stub guard here only scans for `SExportDefault(_)`, so a .d.ts with `export { default } from "./impl"` (or `export { X as default }`) alongside `export default interface Props {}` emits both the clause export and `export default undefined;`, and JSC rejects the module with a duplicate-`default` SyntaxError — a regression (pre-PR the interface elided and only the clause/re-export survived). This is the fifth sibling of the class already fixed four times in this PR (b9a299fc, aa
Comment thread
robobun marked this conversation as resolved.
Outdated
);
stmts.push(stmt);
}
if self.dts_type_name_order.is_empty() {
return Ok(());
}
debug_assert!(self.current_scope == self.module_scope);
let names = core::mem::take(&mut self.dts_type_name_order);
let mut exported_decls: Vec<G::Decl> = Vec::new();
let mut local_decls: Vec<G::Decl> = Vec::new();
for name in names {
if self.current_scope().members.contains_key(name) {
continue;
}
let is_export = self.dts_type_names.get(name).copied().unwrap_or(false)
&& !self.dts_export_clause_aliases.contains_key(name);
let ref_ =
self.declare_symbol(js_ast::symbol::Kind::Hoisted, bun_ast::Loc::EMPTY, name)?;
let decl = G::Decl {
binding: self.b(B::Identifier { r#ref: ref_ }, bun_ast::Loc::EMPTY),
value: None,
};
if is_export {
exported_decls.push(decl);
} else {
local_decls.push(decl);
}
}
for (decls, is_export) in [(exported_decls, true), (local_decls, false)] {
Comment thread
robobun marked this conversation as resolved.
if decls.is_empty() {
continue;
}
let decls = js_ast::g::DeclList::from_slice(&decls);
let stmt = self.s(
S::Local {
kind: js_ast::s::Kind::KVar,
decls,
is_export,
..Default::default()
},
bun_ast::Loc::EMPTY,
);
stmts.push(stmt);
}
Ok(())
}

pub(crate) fn declare_symbol(
&mut self,
kind: js_ast::symbol::Kind,
Expand Down Expand Up @@ -4657,7 +4773,15 @@
match &mut binding.data {
js_ast::b::B::BMissing(_) => {}
js_ast::b::B::BIdentifier(bind) => {
if !opts.is_typescript_declare || (opts.is_namespace_scope && opts.is_export) {
// Declaration files keep module-scope `declare` bindings as
// real symbols; the statement becomes a `var` in parse_stmt.
Comment thread
robobun marked this conversation as resolved.
Outdated
if !opts.is_typescript_declare
|| (opts.is_namespace_scope && opts.is_export)
|| (Self::IS_TYPESCRIPT_ENABLED
&& self.options.typescript_declaration_file
&& opts.is_module_scope
&& !self.dts_suppress_type_name_recording)
{
bind.r#ref = self.declare_symbol(
kind,
binding.loc,
Expand Down Expand Up @@ -8789,6 +8913,11 @@
emitted_namespace_vars: RefMap::default(),
is_exported_inside_namespace: Default::default(),
local_type_names: StringBoolMap::default(),
dts_type_names: StringBoolMap::default(),
dts_type_name_order: Vec::new(),
dts_suppress_type_name_recording: false,
dts_needs_default_export_stub: false,
dts_export_clause_aliases: StringBoolMap::default(),
enclosing_namespace_arg_ref: None,
jsx_imports: crate::JSXImportSymbols::default(),
react_refresh: ReactRefresh::default(),
Expand Down
7 changes: 6 additions & 1 deletion src/js_parser/parse/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -652,6 +652,7 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O
) -> Result<Stmt, Error> {
let p = self;
let mut name: Option<js_ast::LocRef> = None;
let mut name_text: &'a [u8] = b"";
let class_keyword = p.lexer.range();
if p.lexer.token == T::TClass {
//marksyntaxfeature
Expand All @@ -667,7 +668,7 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O
&& (!Self::IS_TYPESCRIPT_ENABLED || p.lexer.identifier != b"implements"))
{
let name_loc = p.lexer.loc();
let name_text = p.lexer.identifier;
name_text = p.lexer.identifier;
p.lexer.expect(T::TIdentifier)?;

// We must return here
Expand Down Expand Up @@ -727,6 +728,10 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O
p.has_non_local_export_declare_inside_namespace = true;
}

if opts.is_module_scope && !name_text.is_empty() {
p.record_declaration_file_type_name(name_text, opts.is_export)?;
}

return Ok(p.s(S::TypeScript {}, loc));
}
}
Expand Down
Loading
Loading