diff options
author | 2021-12-16 01:29:45 -0800 | |
---|---|---|
committer | 2021-12-16 01:29:45 -0800 | |
commit | 92f3efeac244716d8ad45000166626e06589d290 (patch) | |
tree | 69bdb00bcc971e2bc63205b23e0545ba050d472a | |
parent | a772b56cb6b9edba0d0ef7abc2b73a595d7368b2 (diff) | |
download | bun-92f3efeac244716d8ad45000166626e06589d290.tar.gz bun-92f3efeac244716d8ad45000166626e06589d290.tar.zst bun-92f3efeac244716d8ad45000166626e06589d290.zip |
[bundler][JS transpiler] Improve reliability of ESM <> CommonJS interop
This fixes a number of issues caused by not using live bindings when referencing bundled code. This also fixes an issue with libraries looping over `Object.keys(moduleNamespace)`
-rw-r--r-- | src/import_record.zig | 8 | ||||
-rw-r--r-- | src/js_ast.zig | 19 | ||||
-rw-r--r-- | src/js_parser/js_parser.zig | 162 | ||||
-rw-r--r-- | src/js_printer.zig | 117 | ||||
-rw-r--r-- | src/runtime.js | 99 | ||||
-rw-r--r-- | src/runtime.version | 2 |
6 files changed, 165 insertions, 242 deletions
diff --git a/src/import_record.zig b/src/import_record.zig index 4d4199004..b7146215a 100644 --- a/src/import_record.zig +++ b/src/import_record.zig @@ -17,16 +17,16 @@ pub const ImportKind = enum(u8) { // An "import()" expression with a string argument dynamic, - // A call to "require.resolve()" + /// A call to "require.resolve()" require_resolve, - // A CSS "@import" rule + /// A CSS "@import" rule at, - // A CSS "@import" rule with import conditions + /// A CSS "@import" rule with import conditions at_conditional, - // A CSS "url(...)" token + /// A CSS "url(...)" token url, internal, diff --git a/src/js_ast.zig b/src/js_ast.zig index 73ddac0f4..132ff1e05 100644 --- a/src/js_ast.zig +++ b/src/js_ast.zig @@ -406,6 +406,8 @@ pub const ClauseItem = struct { // We need to preserve both aliases in case the symbol is renamed. In this // example, "foo" is "OriginalName" and "bar" is "Alias". original_name: string, + + pub const default_alias: string = "default"; }; pub const G = struct { @@ -417,6 +419,9 @@ pub const G = struct { pub const NamespaceAlias = struct { namespace_ref: Ref, alias: string, + + was_originally_property_access: bool = false, + import_record_index: u32 = std.math.maxInt(u32), }; @@ -725,7 +730,7 @@ pub const Symbol = struct { symbols_for_source: [][]Symbol, pub fn get(self: *Map, ref: Ref) ?*Symbol { - if (Ref.isSourceIndexNull(ref.source_index)) { + if (Ref.isSourceIndexNull(ref.source_index) or ref.is_source_contents_slice) { return null; } @@ -741,6 +746,14 @@ pub const Symbol = struct { return Map{ .symbols_for_source = list }; } + pub fn getWithLink(symbols: *Map, ref: Ref) ?*Symbol { + var symbol: *Symbol = symbols.get(ref) orelse return null; + if (symbol.link) |link| { + return symbols.get(link) orelse symbol; + } + return symbol; + } + pub fn follow(symbols: *Map, ref: Ref) Ref { if (symbols.get(ref)) |symbol| { const link = symbol.link orelse return ref; @@ -975,8 +988,6 @@ pub const E = struct { // false, this could potentially have been a member access expression such // as "ns.foo" off of an imported namespace object. was_originally_identifier: bool = false, - - was_from_macro: bool = false, }; // This is similar to EIdentifier but it represents class-private fields and @@ -6315,7 +6326,7 @@ pub const Macro = struct { if (has_default) { import.import.items[import_item_i] = ClauseItem{ - .alias = "default", + .alias = ClauseItem.default_alias, .name = .{ .loc = writer.loc, .ref = Ref.None }, .original_name = import_default_name, .alias_loc = writer.loc, diff --git a/src/js_parser/js_parser.zig b/src/js_parser/js_parser.zig index 6bb003e89..33b999e4f 100644 --- a/src/js_parser/js_parser.zig +++ b/src/js_parser/js_parser.zig @@ -439,78 +439,47 @@ pub const ImportScanner = struct { } } - if (st.star_name_loc != null or did_remove_star_loc) { - // -- Original Comment -- - // If we're bundling a star import and the namespace is only ever - // used for property accesses, then convert each unique property to - // a clause item in the import statement and remove the star import. - // That will cause the bundler to bundle them more efficiently when - // both this module and the imported module are in the same group. - // - // Before: - // - // import * as ns from 'foo' - // console.log(ns.a, ns.b) - // - // After: - // - // import {a, b} from 'foo' - // console.log(a, b) - // - // This is not done if the namespace itself is used, because in that - // case the code for the namespace will have to be generated. This is - // determined by the symbol count because the parser only counts the - // star import as used if it was used for something other than a - // property access: - // - // import * as ns from 'foo' - // console.log(ns, ns.a, ns.b) - // - // -- Original Comment -- + const namespace_ref = st.namespace_ref; + const convert_star_to_clause = !p.options.enable_bundling and !p.options.can_import_from_bundle and p.symbols.items[namespace_ref.inner_index].use_count_estimate == 0; - // jarred: we don't use the same grouping mechanism as esbuild - // but, we do this anyway. - // The reasons why are: - // * It makes static analysis for other tools simpler. - // * I imagine browsers may someday do some optimizations - // when it's "easier" to know only certain modules are used - // For example, if you're importing a component from a design system - // it's really stupid to import all 1,000 components from that design system - // when you just want <Button /> - const namespace_ref = st.namespace_ref; - const convert_star_to_clause = !p.options.enable_bundling and !p.options.can_import_from_bundle and p.symbols.items[namespace_ref.inner_index].use_count_estimate == 0; + if (convert_star_to_clause and !keep_unused_imports) { + st.star_name_loc = null; + } - if (convert_star_to_clause and !keep_unused_imports) { - st.star_name_loc = null; - } + record.contains_default_alias = record.contains_default_alias or st.default_name != null; - const default_name: string = if (st.default_name != null) - p.loadNameFromRef(st.default_name.?.ref.?) - else - ""; + const existing_items: ImportItemForNamespaceMap = p.import_items_for_namespace.get(namespace_ref) orelse + ImportItemForNamespaceMap.init(p.allocator); - for (st.items) |item| { - const is_default = strings.eqlComptime(item.alias, "default"); - record.contains_default_alias = record.contains_default_alias or is_default; + // ESM requires live bindings + // CommonJS does not require live bindings + // We load ESM in browsers & in Bun.js + // We have to simulate live bindings for cases where the code is bundled + // We do not know at this stage whether or not the import statement is bundled + // This keeps track of the `namespace_alias` incase, at printing time, we determine that we should print it with the namespace + for (st.items) |item| { + const is_default = strings.eqlComptime(item.alias, "default"); + record.contains_default_alias = record.contains_default_alias or is_default; - const name: LocRef = item.name; - const name_ref = name.ref.?; + const name: LocRef = item.name; + const name_ref = name.ref.?; - try p.named_imports.put(name_ref, js_ast.NamedImport{ - .alias = item.alias, - .alias_loc = name.loc, - .namespace_ref = st.namespace_ref, - .import_record_index = st.import_record_index, - }); + try p.named_imports.put(name_ref, js_ast.NamedImport{ + .alias = item.alias, + .alias_loc = name.loc, + .namespace_ref = st.namespace_ref, + .import_record_index = st.import_record_index, + }); - // Make sure the printer prints this as a property access - var symbol: *Symbol = &p.symbols.items[name_ref.inner_index]; - symbol.namespace_alias = G.NamespaceAlias{ - .namespace_ref = st.namespace_ref, - .alias = item.original_name, - .import_record_index = st.import_record_index, - }; - } + // Make sure the printer prints this as a property access + var symbol: *Symbol = &p.symbols.items[name_ref.inner_index]; + + symbol.namespace_alias = G.NamespaceAlias{ + .namespace_ref = st.namespace_ref, + .alias = item.alias, + .import_record_index = st.import_record_index, + .was_originally_property_access = st.star_name_loc != null and existing_items.contains(symbol.original_name), + }; } try p.import_records_for_current_part.append(st.import_record_index); @@ -518,18 +487,8 @@ pub const ImportScanner = struct { if (st.star_name_loc != null) { record.contains_import_star = true; } - - if (st.default_name != null) { - record.contains_default_alias = true; - } else { - for (st.items) |item| { - if (strings.eqlComptime(item.alias, "default")) { - record.contains_default_alias = true; - break; - } - } - } }, + .s_function => |st| { if (st.func.flags.is_export) { if (st.func.name) |name| { @@ -3626,7 +3585,6 @@ pub fn NewParser( const namespace_ref = try p.newSymbol(.other, namespace_identifier); try p.module_scope.generated.append(namespace_ref); - for (imports) |alias, i| { const ref = symbols.get(alias) orelse unreachable; const alias_name = if (@TypeOf(symbols) == RuntimeImports) RuntimeImports.all[alias] else alias; @@ -6182,6 +6140,7 @@ pub fn NewParser( var remap_count: u16 = 0; if (stmt.star_name_loc) |star| { const name = p.loadNameFromRef(stmt.namespace_ref); + stmt.namespace_ref = try p.declareSymbol(.import, star, name); if (comptime ParsePassSymbolUsageType != void) { if (!is_macro) { @@ -6257,9 +6216,11 @@ pub fn NewParser( if (stmt.items.len > 0) { for (stmt.items) |*item| { const name = p.loadNameFromRef(item.name.ref orelse unreachable); + const ref = try p.declareSymbol(.import, item.name.loc, name); p.checkForNonBMPCodePoint(item.alias_loc, item.alias); try p.is_import_item.put(ref, true); + item.name.ref = ref; item_refs.putAssumeCapacity(item.alias, LocRef{ .loc = item.name.loc, .ref = ref }); @@ -7692,10 +7653,9 @@ pub fn NewParser( var entry = try scope.members.getOrPut(name); if (entry.found_existing) { const existing = entry.entry.value; + var symbol: *Symbol = &p.symbols.items[@intCast(usize, existing.ref.inner_index)]; if (comptime !is_generated) { - var symbol: *Symbol = &p.symbols.items[@intCast(usize, existing.ref.inner_index)]; - switch (p.canMergeSymbols(scope, symbol.kind, kind)) { .forbidden => { const r = js_lexer.rangeOfIdentifier(p.source, loc); @@ -7723,6 +7683,11 @@ pub fn NewParser( // else => unreachable, } } else { + // Ensure that EImportIdentifier is created for the symbol in handleIdentifier + if (symbol.kind == .import and kind != .import) { + try p.is_import_item.put(ref, true); + } + p.symbols.items[ref.inner_index].link = existing.ref; } } @@ -9635,9 +9600,10 @@ pub fn NewParser( for (import.items) |*clause| { const import_hash_name = clause.original_name; - if (strings.eqlComptime(clause.alias, "default") and strings.eqlComptime(clause.original_name, "default")) { + if (strings.eqlComptime(clause.alias, "default")) { var non_unique_name = record.path.name.nonUniqueNameString(p.allocator) catch unreachable; clause.original_name = std.fmt.allocPrint(p.allocator, "{s}_default", .{non_unique_name}) catch unreachable; + record.contains_default_alias = true; } const name_ref = p.declareSymbol(.import, this.loc, clause.original_name) catch unreachable; clause.name = LocRef{ .loc = this.loc, .ref = name_ref }; @@ -9649,7 +9615,7 @@ pub fn NewParser( // Ensure we don't accidentally think this is an export from } - p.macro.prepend_stmts.append(p.visitSingleStmt(p.s(import, this.loc), StmtsKind.none)) catch unreachable; + p.macro.prepend_stmts.append(p.s(import, this.loc)) catch unreachable; } }; @@ -11325,7 +11291,6 @@ pub fn NewParser( return p.e( E.ImportIdentifier{ .was_originally_identifier = false, - .was_from_macro = true, .ref = ref, }, expr.loc, @@ -12571,7 +12536,10 @@ pub fn NewParser( } fn jsxStringsToMemberExpressionAutomatic(p: *P, loc: logger.Loc, is_static: bool) Expr { - return p.jsxStringsToMemberExpression(loc, if (is_static) p.jsxs_runtime.ref else p.jsx_runtime.ref); + return p.jsxStringsToMemberExpression(loc, if (is_static and !p.options.jsx.development) + p.jsxs_runtime.ref + else + p.jsx_runtime.ref); } fn maybeRelocateVarsToTopLevel(p: *P, decls: []G.Decl, mode: RelocateVars.Mode) RelocateVars { @@ -12790,17 +12758,19 @@ pub fn NewParser( data.value.expr = p.visitExpr(expr); // // Optionally preserve the name - data.value.expr = p.maybeKeepExprSymbolName(data.value.expr, "default", was_anonymous_named_expr); + data.value.expr = p.maybeKeepExprSymbolName(data.value.expr, js_ast.ClauseItem.default_alias, was_anonymous_named_expr); // Discard type-only export default statements if (is_typescript_enabled) { switch (expr.data) { .e_identifier => |ident| { - const symbol = p.symbols.items[ident.ref.inner_index]; - if (symbol.kind == .unbound) { - if (p.local_type_names.get(symbol.original_name)) |local_type| { - if (local_type) { - return; + if (!ident.ref.is_source_contents_slice) { + const symbol = p.symbols.items[ident.ref.inner_index]; + if (symbol.kind == .unbound) { + if (p.local_type_names.get(symbol.original_name)) |local_type| { + if (local_type) { + return; + } } } } @@ -12828,7 +12798,7 @@ pub fn NewParser( name = p.loadNameFromRef(func_loc.ref.?); } else { func.func.name = data.default_name; - name = "default"; + name = js_ast.ClauseItem.default_alias; } func.func = p.visitFunc(func.func, func.func.open_parens_loc); @@ -12906,17 +12876,7 @@ pub fn NewParser( // "module.exports = value" stmts.append( Expr.assignStmt( - p.e( - E.Dot{ - .target = p.e( - E.Identifier{ - .ref = p.module_ref, - }, - stmt.loc, - ), - .name = "exports", - .name_loc = stmt.loc, - }, + p.@"module.exports"( stmt.loc, ), p.visitExpr(data.value), diff --git a/src/js_printer.zig b/src/js_printer.zig index 259f661a4..30c51a962 100644 --- a/src/js_printer.zig +++ b/src/js_printer.zig @@ -46,6 +46,7 @@ const assert = std.debug.assert; threadlocal var imported_module_ids_list: std.ArrayList(u32) = undefined; threadlocal var imported_module_ids_list_unset: bool = true; +const ImportRecord = importRecord.ImportRecord; fn notimpl() void { Global.panic("Not implemented yet!", .{}); @@ -1561,34 +1562,33 @@ pub fn NewPrinter( } }, .e_import_identifier => |e| { - if (e.ref.is_source_contents_slice or e.was_from_macro) { - p.printSymbol(e.ref); - return; - } // Potentially use a property access instead of an identifier var didPrint = false; - if (p.symbols.get(e.ref)) |symbol| { + if (p.symbols.getWithLink(e.ref)) |symbol| { if (symbol.import_item_status == .missing) { p.printUndefined(level); didPrint = true; } else if (symbol.namespace_alias) |namespace| { - var wrap = false; - didPrint = true; - - if (p.call_target) |target| { - wrap = e.was_originally_identifier and (target == .e_identifier and - target.e_identifier.ref.eql(expr.data.e_import_identifier.ref)); - } + const import_record = p.import_records[namespace.import_record_index]; + if ((comptime is_inside_bundle) or import_record.is_bundled or namespace.was_originally_property_access) { + var wrap = false; + didPrint = true; + + if (p.call_target) |target| { + wrap = e.was_originally_identifier and (target == .e_identifier and + target.e_identifier.ref.eql(expr.data.e_import_identifier.ref)); + } - if (wrap) { - p.print("(0, "); - } + if (wrap) { + p.print("(0, "); + } - p.printNamespaceAlias(namespace); + p.printNamespaceAlias(import_record, namespace); - if (wrap) { - p.print(")"); + if (wrap) { + p.print(")"); + } } } } @@ -1863,9 +1863,7 @@ pub fn NewPrinter( } } - pub fn printNamespaceAlias(p: *Printer, namespace: G.NamespaceAlias) void { - const import_record = &p.import_records[namespace.import_record_index]; - + pub fn printNamespaceAlias(p: *Printer, import_record: ImportRecord, namespace: G.NamespaceAlias) void { if (import_record.module_id > 0 and !import_record.contains_import_star) { p.print("$"); p.printModuleId(import_record.module_id); @@ -2410,7 +2408,7 @@ pub fn NewPrinter( // this is still necessary for JSON if (is_inside_bundle) { p.printModuleExportSymbol(); - p.print(".default = "); + p.print(" = "); } // Functions and classes must be wrapped to avoid confusion with their statement forms @@ -2431,7 +2429,7 @@ pub fn NewPrinter( // p.print(" = "); } else { p.printModuleExportSymbol(); - p.print(".default = "); + p.print(" = "); } } @@ -2475,7 +2473,7 @@ pub fn NewPrinter( // p.print(" = "); } else { p.printModuleExportSymbol(); - p.print(".default = "); + p.print(" = "); } } @@ -2589,17 +2587,33 @@ pub fn NewPrinter( p.print(", {"); const last = s.items.len - 1; for (s.items) |item, i| { - const name = p.renamer.nameForSymbol(item.name.ref.?); - p.printClauseAlias(item.alias); - if (comptime is_inside_bundle) { - p.print(":"); - p.printSpace(); - p.print("() => "); - p.printIdentifier(name); - } else if (!strings.eql(name, item.alias)) { - p.print(":"); - p.printSpace(); - p.printIdentifier(name); + const symbol = p.symbols.getWithLink(item.name.ref.?).?; + const name = symbol.original_name; + var did_print = false; + + if (symbol.namespace_alias) |namespace| { + const import_record = p.import_records[namespace.import_record_index]; + if (import_record.is_bundled or (comptime is_inside_bundle) or namespace.was_originally_property_access) { + p.printIdentifier(name); + p.print(": () => "); + p.printNamespaceAlias(import_record, namespace); + did_print = true; + } + } + + if (!did_print) { + p.printClauseAlias(item.alias); + if (comptime is_inside_bundle) { + p.print(":"); + p.printSpace(); + p.print("() => "); + + p.printIdentifier(name); + } else if (!strings.eql(name, item.alias)) { + p.print(":"); + p.printSpace(); + p.printIdentifier(name); + } } if (i < last) { @@ -2644,11 +2658,11 @@ pub fn NewPrinter( if (p.symbols.get(item.name.ref.?)) |symbol| { if (symbol.namespace_alias) |namespace| { const import_record = p.import_records[namespace.import_record_index]; - if (import_record.is_bundled) { + if (import_record.is_bundled or (comptime is_inside_bundle) or namespace.was_originally_property_access) { p.print("var "); p.printSymbol(item.name.ref.?); - p.print("= "); - p.printNamespaceAlias(namespace); + p.print(" = "); + p.printNamespaceAlias(import_record, namespace); p.printSemicolonAfterStatement(); _ = array.swapRemove(i); @@ -3457,29 +3471,6 @@ pub fn NewPrinter( p.printSemicolonAfterStatement(); }, } - - const items = s.items; - if (items.len > 0 and !is_disabled) { - for (items) |item| { - const ref = item.name.ref.?; - const symbol = p.symbols.get(ref) orelse continue; - - p.printIndent(); - p.print("var "); - p.printSymbol(ref); - p.print(" = "); - if (!import_record.contains_import_star) { - p.print("$"); - p.printModuleId(module_id); - } else { - p.printSymbol(s.namespace_ref); - } - p.print("."); - p.printSymbol(ref); - - p.printSemicolonAfterStatement(); - } - } } pub fn printLoadFromBundle(p: *Printer, import_record_index: u32) void { if (bun) { @@ -3527,7 +3518,11 @@ pub fn NewPrinter( return; } + p.printSymbol(p.options.runtime_imports.__require.?); + // d is for default + p.print(".d("); p.printLoadFromBundle(require.import_record_index); + p.print(")"); } pub fn printBundledRexport(p: *Printer, name: string, import_record_index: u32) void { diff --git a/src/runtime.js b/src/runtime.js index 200f178ca..23cfc6062 100644 --- a/src/runtime.js +++ b/src/runtime.js @@ -66,66 +66,6 @@ export var __commonJS = (cb, name) => { cb(((mod = { exports: {} }), mod), mod.exports); const kind = typeof mod.exports; - // if ( - // (kind === "function" || kind === "object") && - // "__esModule" in mod.exports && - // !mod.exports[tagSymbol] - // ) { - // if (!("default" in mod.exports)) { - // Object.defineProperty(mod.exports, "default", { - // get() { - // return mod.exports; - // }, - // set(v) { - // mod.exports = v; - // }, - // enumerable: true, - // configurable: true, - // }); - // } - // } else if ( - // kind === "object" && - // "default" in mod.exports && - // !mod.exports[tagSymbol] && - // Object.keys(mod.exports).length === 1 - // ) { - // // if mod.exports.default === true this won't work because we can't define a property on a boolean - // if ( - // typeof mod.exports.default === "object" || - // typeof mod.exports.default === "function" - // ) { - // mod.exports = mod.exports.default; - - // Object.defineProperty(mod.exports, "default", { - // get() { - // return mod.exports; - // }, - // set(v) { - // mod.exports = v; - // }, - // enumerable: true, - // configurable: true, - // }); - // } - - // // If it's a namespace export without .default, pretend .default is the same as mod.exports - // } else if ( - // (kind === "function" || kind === "object") && - // !("default" in mod.exports) && - // Object.isExtensible(mod.exports) - // ) { - // var defaultValue = mod.exports; - // Object.defineProperty(mod.exports, "default", { - // get() { - // return defaultValue; - // }, - // set(value) { - // defaultValue = value; - // }, - // enumerable: true, - // configurable: true, - // }); - // } if ( (kind === "object" || kind === "function") && @@ -137,6 +77,22 @@ export var __commonJS = (cb, name) => { enumerable: false, configurable: false, }); + + if (!("default" in mod.exports)) { + Object.defineProperty(mod.exports, "default", { + get() { + return mod.exports; + }, + set(v) { + if (v === mod.exports) return; + mod.exports = v; + return true; + }, + // enumerable: false is important here + enumerable: false, + configurable: true, + }); + } } return mod.exports; @@ -150,21 +106,22 @@ export var __cJS2eSM = __commonJS; export var __internalIsCommonJSNamespace = (namespace) => typeof namespace === "object" && - "default" in namespace && - namespace.default[cjsRequireSymbol]; + (("default" in namespace && namespace.default[cjsRequireSymbol]) || + namespace[cjsRequireSymbol]); +// require() export var __require = (namespace) => { - const namespaceType = typeof namespace; - if (namespaceType === "function" && namespace[cjsRequireSymbol]) - return namespace(); - - if ( - namespaceType === "object" && - "default" in namespace && - namespace.default[cjsRequireSymbol] - ) + if (__internalIsCommonJSNamespace(namespace)) { return namespace.default(); + } + + return namespace; +}; +// require().default +// this currently does nothing +// get rid of this wrapper once we're more confident we do not need special handling for default +__require.d = (namespace) => { return namespace; }; diff --git a/src/runtime.version b/src/runtime.version index a2d50cf8a..b5f16523a 100644 --- a/src/runtime.version +++ b/src/runtime.version @@ -1 +1 @@ -b63df695723aa470
\ No newline at end of file +3f5763ef0f32f299
\ No newline at end of file |