Skip to content
Open
Show file tree
Hide file tree
Changes from 5 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: 1 addition & 3 deletions src/ast/e.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1472,9 +1472,7 @@ impl Object {
}
}

/// Data-file parsers (JSON/JSON5/TOML/YAML) define every key as an own
/// property, so an own `"__proto__"` string key must be marked computed:
/// a plain `"__proto__":` key in a printed object literal sets the prototype.
/// A synthesized `"__proto__"` key is computed: a plain `__proto__:` would set the prototype.
pub fn own_key_property_flags(key: &Expr) -> crate::flags::PropertySet {
match &key.data {
crate::expr::Data::EString(key_str) if key_str.eql_comptime(b"__proto__") => {
Expand Down
8 changes: 4 additions & 4 deletions src/bundler/AstBuilder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -369,11 +369,11 @@ impl<'a, 'bump> AstBuilder<'a, 'bump> {
self.symbols[ref_.inner_index() as usize].original_name;
// ImportScanner.recordExportedBinding → recordExport
self.record_export(binding.loc, original_name.slice(), ref_)?;
let key =
Expr::init(E::String::init(original_name.slice()), binding.loc);
export_props.push(G::Property {
key: Some(Expr::init(
E::String::init(original_name.slice()),
binding.loc,
)),
flags: E::own_key_property_flags(&key),
key: Some(key),
value: Some(Expr::init_identifier(ref_, binding.loc)),
..Default::default()
});
Expand Down
1 change: 1 addition & 0 deletions src/bundler/bundle_v2.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3620,6 +3620,7 @@ pub mod bv2_impl {
..Default::default()
});
*client_item = G::Property {
flags: E::own_key_property_flags(&export_name),
key: Some(export_name),
value: Some(server.new_expr(E::Object {
properties: bun_ast::g::PropertyList::from_owned_slice(Box::new([
Expand Down
7 changes: 5 additions & 2 deletions src/bundler/linker_context/doStep5.rs
Original file line number Diff line number Diff line change
Expand Up @@ -483,7 +483,7 @@ impl LinkerContext<'_> {
stmts: stmts_eat1!(Stmt::allocate(arena, S::Return { value: Some(value) }, loc,)),
loc,
};
properties.push(G::Property {
let mut property = G::Property {
key: Some(Expr::allocate(
arena,
// TODO: test emoji work as expected (relevant for WASM exports)
Expand All @@ -503,7 +503,10 @@ impl LinkerContext<'_> {
loc,
)),
..Default::default()
});
};
property.flags =
E::own_key_property_flags(property.key.as_ref().expect("infallible: prop has key"));
properties.push(property);
ns_export_symbol_uses
.put_assume_capacity(exp_data.import_ref, SymbolUse { count_estimate: 1 });

Expand Down
3 changes: 3 additions & 0 deletions src/bundler/linker_context/generateCodeForFileInChunkJS.rs
Original file line number Diff line number Diff line change
Expand Up @@ -451,6 +451,8 @@ pub fn generate_code_for_file_in_chunk_js<'r, 'src>(
// empty" invariant makes that drop a no-op today, but
// `ptr::write` enforces it structurally.
let key = prop.key;
// keeps a "__proto__" key computed
let flags = prop.flags;
let value_loc =
prop.value.as_ref().expect("infallible: prop has value").loc;
// SAFETY: `prop` is a valid `&mut G::Property` slot;
Expand All @@ -461,6 +463,7 @@ pub fn generate_code_for_file_in_chunk_js<'r, 'src>(
prop,
G::Property {
key,
flags,
value: Some(Expr::init_identifier(export_ref, value_loc)),
..Default::default()
},
Expand Down
54 changes: 31 additions & 23 deletions src/js_parser/lower/lower_esm_exports_hmr.rs
Original file line number Diff line number Diff line change
Expand Up @@ -77,12 +77,14 @@ impl<'a> ConvertESMExportsForHmr<'a> {
// if the symbol is not used, we don't need to preserve
// a binding in this scope. we can move it to the exports object.
if symbol.use_count_estimate == 0 && value.can_be_moved() {
let key = Expr::init(
// SAFETY: arena-owned name slice valid for the parse.
E::EString::init(symbol.original_name.slice()),
binding.loc,
);
self.export_props.push(G::Property {
key: Some(Expr::init(
// SAFETY: arena-owned name slice valid for the parse.
E::EString::init(symbol.original_name.slice()),
binding.loc,
)),
flags: E::own_key_property_flags(&key),
key: Some(key),
value: Some(value),
..Default::default()
});
Expand Down Expand Up @@ -257,16 +259,18 @@ impl<'a> ConvertESMExportsForHmr<'a> {

let class_name_ref = st.class.class_name.unwrap().ref_;
// Export as CommonJS
let key = Expr::init(
// SAFETY: arena-owned name slice valid for the parse.
E::EString::init(
p.symbols[class_name_ref.inner_index() as usize]
.original_name
.slice(),
),
stmt.loc,
);
self.export_props.push(G::Property {
key: Some(Expr::init(
// SAFETY: arena-owned name slice valid for the parse.
E::EString::init(
p.symbols[class_name_ref.inner_index() as usize]
.original_name
.slice(),
),
stmt.loc,
)),
flags: E::own_key_property_flags(&key),
key: Some(key),
value: Some(Expr::init_identifier(class_name_ref, stmt.loc)),
..Default::default()
});
Expand Down Expand Up @@ -350,12 +354,14 @@ impl<'a> ConvertESMExportsForHmr<'a> {

if let Some(alias) = &st.alias {
// 'export * as ns from' creates one named property.
self.export_props.push(G::Property {
let key = Expr::init(
// SAFETY: arena-owned name slice valid for the parse.
key: Some(Expr::init(
E::EString::init(alias.original_name.slice()),
stmt.loc,
)),
E::EString::init(alias.original_name.slice()),
stmt.loc,
);
self.export_props.push(G::Property {
flags: E::own_key_property_flags(&key),
key: Some(key),
value: Some(Expr::init_identifier(deduped.namespace_ref, stmt.loc)),
..Default::default()
});
Expand Down Expand Up @@ -621,11 +627,13 @@ impl<'a> ConvertESMExportsForHmr<'a> {
// no setter is added since live bindings are read-only
} else {
// 'abc,'
let key = Expr::init(
E::EString::init(export_symbol_name.unwrap_or(original_name).slice()),
loc,
);
self.export_props.push(G::Property {
key: Some(Expr::init(
E::EString::init(export_symbol_name.unwrap_or(original_name).slice()),
loc,
)),
flags: E::own_key_property_flags(&key),
key: Some(key),
value: Some(id),
..Default::default()
});
Expand Down
142 changes: 72 additions & 70 deletions src/js_printer/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -950,6 +950,25 @@ where
b'"'
}

/// A `__proto__` key keeps its parsed form: `{ __proto__: x }` sets the prototype, `{ __proto__ }` is an own property.
fn can_use_shorthand_property(
key: &js_ast::e::String,
name: &[u8],
flags: js_ast::flags::PropertySet,
) -> bool {
key.eql_bytes(name)
&& (!key.eql_comptime(b"__proto__")
|| flags.contains(js_ast::flags::Property::WasShorthand))
}

/// `["__proto__"]: x` is the only own-property form left once the shorthand value was renamed.
fn must_print_proto_key_as_computed(
key: &js_ast::e::String,
flags: js_ast::flags::PropertySet,
) -> bool {
flags.contains(js_ast::flags::Property::WasShorthand) && key.eql_comptime(b"__proto__")
}

#[derive(Clone, Copy)]
pub struct Whitespacer {
pub normal: &'static [u8],
Expand Down Expand Up @@ -4703,6 +4722,26 @@ pub(crate) mod __gated_printer {
}
}

/// The identifier a property value prints as, if it prints as a bare identifier.
fn shorthand_value_name(&mut self, item: &G::Property) -> Option<&'a [u8]> {
match &item.value.as_ref()?.data {
ExprData::EIdentifier(e) => Some(self.name_for_symbol(e.ref_)),
ExprData::EImportIdentifier(e) => {
if self.options.input_files_for_dev_server.is_some() {
return None;
}
// Make sure we're not using a property access instead of an identifier
let ref_ = self.symbols().follow(e.ref_);
let symbol = self.symbols().get_const(ref_)?;
if symbol.namespace_alias.is_some() {
return None;
}
Some(self.name_for_symbol(e.ref_))
}
_ => None,
}
}

pub(crate) fn print_property(&mut self, item_in: &G::Property) {
// `G::Property` isn't `Copy`, so take a borrow and shallow-copy the
// mutable bits we may rewrite (key + flags).
Expand Down Expand Up @@ -4871,88 +4910,51 @@ pub(crate) mod __gated_printer {
self.add_source_mapping(key.loc);
if key_str.is_utf8() {
self.print_space_before_identifier();
let mut allow_shorthand = true;
if !IS_JSON && lexer::is_identifier(key_str.slice8()) {
self.print_identifier(key_str.slice8());
} else {
allow_shorthand = false;
self.print_string_literal_e_string(&key_str, false);
}

// Use a shorthand property if the names are the same
if let Some(val) = &item.value {
match &val.data {
ExprData::EIdentifier(e) => {
if key_str.slice8() == self.name_for_symbol(e.ref_) {
if let Some(initial) = &item.initializer {
self.print_initializer(*initial);
}
if allow_shorthand {
return;
}
}
}
ExprData::EImportIdentifier(e) => 'inner: {
let ref_ = self.symbols().follow(e.ref_);
if self.options.input_files_for_dev_server.is_some() {
break 'inner;
}
if let Some(symbol) = self.symbols().get_const(ref_) {
if symbol.namespace_alias.is_none()
&& key_str.slice8() == self.name_for_symbol(e.ref_)
{
if let Some(initial) = &item.initializer {
self.print_initializer(*initial);
}
if allow_shorthand {
return;
}
}
// Use a shorthand property if the names are the same
if let Some(name) = self.shorthand_value_name(&item) {
if can_use_shorthand_property(&key_str, name, item.flags) {
self.print_identifier(key_str.slice8());
if let Some(initial) = &item.initializer {
self.print_initializer(*initial);
}
return;
}
_ => {}
}

if must_print_proto_key_as_computed(&key_str, item.flags) {
self.print(b"[");
self.print_string_literal_e_string(&key_str, false);
self.print(b"]");
} else {
self.print_identifier(key_str.slice8());
}
} else {
self.print_string_literal_e_string(&key_str, false);
}
} else if !IS_JSON && self.can_print_identifier_utf16(key_str.slice16()) {
self.print_space_before_identifier();
self.print_identifier_utf16(key_str.slice16())
.expect("unreachable");

// Use a shorthand property if the names are the same
if let Some(val) = &item.value {
match &val.data {
ExprData::EIdentifier(e) => {
if item.flags.contains(js_ast::flags::Property::WasShorthand)
|| strings::utf16_eql_string(
key_str.slice16(),
self.name_for_symbol(e.ref_),
)
{
if let Some(initial) = &item.initializer {
self.print_initializer(*initial);
}
return;
}
if let Some(name) = self.shorthand_value_name(&item) {
if can_use_shorthand_property(&key_str, name, item.flags) {
self.print_identifier_utf16(key_str.slice16())
.expect("unreachable");
if let Some(initial) = &item.initializer {
self.print_initializer(*initial);
}
ExprData::EImportIdentifier(e) => {
let ref_ = self.symbols().follow(e.ref_);
if let Some(symbol) = self.symbols().get_const(ref_) {
if symbol.namespace_alias.is_none()
&& strings::utf16_eql_string(
key_str.slice16(),
self.name_for_symbol(e.ref_),
)
{
if let Some(initial) = &item.initializer {
self.print_initializer(*initial);
}
return;
}
}
}
_ => {}
return;
}
}

if must_print_proto_key_as_computed(&key_str, item.flags) {
self.print(b"[");
self.print_string_literal_e_string(&key_str, false);
self.print(b"]");
} else {
self.print_identifier_utf16(key_str.slice16())
.expect("unreachable");
}
} else {
let c = best_quote_char_for_string(key_str.slice16(), false);
self.print(c);
Expand Down
45 changes: 45 additions & 0 deletions test/bake/dev/esm.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -232,6 +232,51 @@ devTest("export * as namespace", {
await c.expectMessage("PASS");
},
});
// The exports of a module become keys of an object literal. A key named
// "__proto__" has to be a computed key, or it sets the prototype of that
// object and the export disappears.
devTest("export named __proto__", {
framework: minimalFramework,
files: {
// an unused constant with a literal value is moved into the exports literal
"moved.ts": `
export const __proto__ = "moved";
`,
"local.ts": `
export const __proto__ = { local: true };
`,
"klass.ts": `
export class __proto__ { static tag = "class"; }
`,
"clause.ts": `
function p() { return "clause"; }
export { p as __proto__, p as other };
`,
"star.ts": `
export * as __proto__ from './clause';
`,
"routes/index.ts": `
import * as moved from '../moved';
import * as local from '../local';
import * as klass from '../klass';
import * as clause from '../clause';
import * as star from '../star';
import { __proto__ as p } from '../clause';
export default function(req, meta) {
return new Response(JSON.stringify([
Object.hasOwn(moved, "__proto__") && moved.__proto__ === "moved",
Object.hasOwn(local, "__proto__") && local.__proto__.local,
Object.hasOwn(klass, "__proto__") && klass.__proto__.tag === "class",
Object.hasOwn(clause, "__proto__") && clause.__proto__ === p && p() === "clause",
Object.hasOwn(star, "__proto__") && star.__proto__.other === p,
]));
}
`,
},
async test(dev) {
await dev.fetch("/").equals("[true,true,true,true,true]");
},
});
devTest("ESM <-> CJS sync", {
files: {
"index.html": emptyHtmlFile({
Expand Down
Loading
Loading