From 622432e8433de27ea8a433fcaaa6d22897cbbe9b Mon Sep 17 00:00:00 2001 From: dave caruso Date: Thu, 1 Aug 2024 17:25:38 -0700 Subject: [PATCH] feat(bundler): inlining/dead-code-elimination for `import.meta.main` (and --compile) (#12867) Co-authored-by: Meghan Denny Co-authored-by: Dylan Conway <35280289+dylan-conway@users.noreply.github.com> Co-authored-by: dylan-conway Co-authored-by: Jarred Sumner Co-authored-by: paperdave Co-authored-by: Jarred-Sumner Co-authored-by: Andrew Johnston Co-authored-by: Ashcon Partovi --- src/bun.js/RuntimeTranspilerCache.zig | 3 +- src/bundler.zig | 1 + src/bundler/bundle_v2.zig | 20 +++- src/cli.zig | 4 +- src/cli/build_command.zig | 3 + src/crash_handler.zig | 2 +- src/js_ast.zig | 61 ++++++++---- src/js_parser.zig | 130 ++++++++++++++++++-------- src/js_printer.zig | 66 ++++++++++++- src/options.zig | 1 + test/bundler/bundler_compile.test.ts | 16 ++++ test/bundler/bundler_edgecase.test.ts | 39 ++++++++ test/bundler/bundler_minify.test.ts | 70 ++++++++++++++ test/cli/run/run-importmetamain.ts | 44 +++++++++ 14 files changed, 397 insertions(+), 63 deletions(-) create mode 100644 test/cli/run/run-importmetamain.ts diff --git a/src/bun.js/RuntimeTranspilerCache.zig b/src/bun.js/RuntimeTranspilerCache.zig index 551659c6e0..380679cc04 100644 --- a/src/bun.js/RuntimeTranspilerCache.zig +++ b/src/bun.js/RuntimeTranspilerCache.zig @@ -1,7 +1,8 @@ /// ** Update the version number when any breaking changes are made to the cache format or to the JS parser ** /// Version 3: "Infinity" becomes "1/0". /// Version 4: TypeScript enums are properly handled + more constant folding -const expected_version = 4; +/// Version 5: `require.main === module` no longer marks a module as CJS +const expected_version = 5; const bun = @import("root").bun; const std = @import("std"); diff --git a/src/bundler.zig b/src/bundler.zig index cd88138e94..d1d561d902 100644 --- a/src/bundler.zig +++ b/src/bundler.zig @@ -1201,6 +1201,7 @@ pub const Bundler = struct { .inline_require_and_import_errors = false, .import_meta_ref = ast.import_meta_ref, .runtime_transpiler_cache = runtime_transpiler_cache, + .target = bundler.options.target, .print_dce_annotations = bundler.options.emit_dce_annotations, }, enable_source_map, diff --git a/src/bundler/bundle_v2.zig b/src/bundler/bundle_v2.zig index ec46e197b3..9d25f6311b 100644 --- a/src/bundler/bundle_v2.zig +++ b/src/bundler/bundle_v2.zig @@ -655,6 +655,7 @@ pub const BundleV2 = struct { hash: ?u64, batch: *ThreadPoolLib.Batch, resolve: _resolver.Result, + is_entry_point: bool, ) !?Index.Int { var result = resolve; var path = result.path() orelse return null; @@ -692,6 +693,7 @@ pub const BundleV2 = struct { task.loader = loader; task.task.node.next = null; task.tree_shaking = this.linker.options.tree_shaking; + task.is_entry_point = is_entry_point; // Handle onLoad plugins as entry points if (!this.enqueueOnLoadPluginIfNeeded(task)) { @@ -766,6 +768,7 @@ pub const BundleV2 = struct { generator.linker.options.source_maps = bundler.options.source_map; generator.linker.options.tree_shaking = bundler.options.tree_shaking; generator.linker.options.public_path = bundler.options.public_path; + generator.linker.options.target = bundler.options.target; var pool = try generator.graph.allocator.create(ThreadPool); if (enable_reloading) { @@ -822,7 +825,7 @@ pub const BundleV2 = struct { for (entry_points) |entry_point| { const resolved = this.bundler.resolveEntryPoint(entry_point) catch continue; - if (try this.enqueueItem(null, &batch, resolved)) |source_index| { + if (try this.enqueueItem(null, &batch, resolved, true)) |source_index| { this.graph.entry_points.append(this.graph.allocator, Index.source(source_index)) catch unreachable; } else {} } @@ -836,7 +839,7 @@ pub const BundleV2 = struct { for (user_entry_points) |entry_point| { const resolved = this.bundler.resolveEntryPoint(entry_point) catch continue; - if (try this.enqueueItem(null, &batch, resolved)) |source_index| { + if (try this.enqueueItem(null, &batch, resolved, true)) |source_index| { this.graph.entry_points.append(this.graph.allocator, Index.source(source_index)) catch unreachable; } else {} } @@ -2323,6 +2326,7 @@ pub const ParseTask = struct { emit_decorator_metadata: bool = false, ctx: *BundleV2, package_version: string = "", + is_entry_point: bool = false, /// Used by generated client components presolved_source_indices: []const Index.Int = &.{}, @@ -2883,6 +2887,15 @@ pub const ParseTask = struct { opts.features.emit_decorator_metadata = bundler.options.emit_decorator_metadata; opts.ignore_dce_annotations = bundler.options.ignore_dce_annotations and !source.index.isRuntime(); + // For files that are not user-specified entrypoints, set `import.meta.main` to `false`. + // Entrypoints will have `import.meta.main` set as "unknown", unless we use `--compile`, + // in which we inline `true`. + if (bundler.options.inline_entrypoint_import_meta_main or !task.is_entry_point) { + opts.import_meta_main_value = task.is_entry_point; + } else if (bundler.options.target == .node) { + opts.lower_import_meta_main_for_node_js = true; + } + opts.tree_shaking = if (source.index.isRuntime()) true else bundler.options.tree_shaking; opts.module_type = task.module_type; opts.features.unwrap_commonjs_packages = bundler.options.unwrap_commonjs_packages; @@ -3865,6 +3878,7 @@ pub const LinkerContext = struct { minify_syntax: bool = false, minify_identifiers: bool = false, source_maps: options.SourceMapOption = .none, + target: options.Target = .browser, mode: Mode = Mode.bundle, @@ -6813,6 +6827,7 @@ pub const LinkerContext = struct { .minify_whitespace = c.options.minify_whitespace, .minify_identifiers = c.options.minify_identifiers, .minify_syntax = c.options.minify_syntax, + .target = c.options.target, .print_dce_annotations = c.options.emit_dce_annotations, // .const_values = c.graph.const_values, }; @@ -9039,6 +9054,7 @@ pub const LinkerContext = struct { c, ), .line_offset_tables = c.graph.files.items(.line_offset_table)[part_range.source_index.get()], + .target = c.options.target, }; writer.buffer.reset(); diff --git a/src/cli.zig b/src/cli.zig index 5de5c1e2d1..31946329c6 100644 --- a/src/cli.zig +++ b/src/cli.zig @@ -81,7 +81,7 @@ pub const debug_flags = if (Environment.isDebug) struct { } return false; } -} else @compileError("Do not access this namespace []const u8; in a release build"); +} else @compileError("Do not access this namespace in a release build"); const LoaderMatcher = strings.ExactSizeMatcher(4); const ColonListType = @import("./cli/colon_list_type.zig").ColonListType; @@ -774,6 +774,7 @@ pub const Arguments = struct { if (args.flag("--compile")) { ctx.bundler_options.compile = true; + ctx.bundler_options.inline_entrypoint_import_meta_main = true; } if (args.option("--outdir")) |outdir| { @@ -1279,6 +1280,7 @@ pub const Command = struct { react_server_components: bool = false, code_splitting: bool = false, transform_only: bool = false, + inline_entrypoint_import_meta_main: bool = false, minify_syntax: bool = false, minify_whitespace: bool = false, minify_identifiers: bool = false, diff --git a/src/cli/build_command.zig b/src/cli/build_command.zig index 5fee90e015..f3b51fb118 100644 --- a/src/cli/build_command.zig +++ b/src/cli/build_command.zig @@ -102,6 +102,9 @@ pub const BuildCommand = struct { this_bundler.options.react_server_components = ctx.bundler_options.react_server_components; this_bundler.resolver.opts.react_server_components = ctx.bundler_options.react_server_components; + this_bundler.options.inline_entrypoint_import_meta_main = ctx.bundler_options.inline_entrypoint_import_meta_main; + this_bundler.resolver.opts.inline_entrypoint_import_meta_main = ctx.bundler_options.inline_entrypoint_import_meta_main; + this_bundler.options.code_splitting = ctx.bundler_options.code_splitting; this_bundler.resolver.opts.code_splitting = ctx.bundler_options.code_splitting; diff --git a/src/crash_handler.zig b/src/crash_handler.zig index 937df618de..39a2d5ebf5 100644 --- a/src/crash_handler.zig +++ b/src/crash_handler.zig @@ -1519,7 +1519,7 @@ pub fn dumpStackTrace(trace: std.builtin.StackTrace) void { }, .linux => { // Linux doesnt seem to be able to decode it's own debug info. - // TODO(@paperdave): see if zig 0.12 fixes this + // TODO(@paperdave): see if zig 0.14 fixes this }, else => { stdDumpStackTrace(trace); diff --git a/src/js_ast.zig b/src/js_ast.zig index f0f3f93ae9..9f0166a39f 100644 --- a/src/js_ast.zig +++ b/src/js_ast.zig @@ -1550,6 +1550,12 @@ pub const E = struct { range: logger.Range, }; pub const ImportMeta = struct {}; + pub const ImportMetaMain = struct { + /// If we want to print `!import.meta.main`, set this flag to true + /// instead of wrapping in a unary not. This way, the printer can easily + /// print `require.main != module` instead of `!(require.main == module)` + inverted: bool = false, + }; pub const Call = struct { // Node: @@ -4468,14 +4474,13 @@ pub const Expr = struct { e_undefined, e_new_target, e_import_meta, + e_import_meta_main, + e_require_main, e_inlined_enum, /// A string that is UTF-8 encoded without escaping for use in JavaScript. e_utf8_string, - // This should never make it to the printer - inline_identifier, - // object, regex and array may have had side effects pub fn isPrimitiveLiteral(tag: Tag) bool { return switch (tag) { @@ -5156,8 +5161,8 @@ pub const Expr = struct { e_require_string: E.RequireString, e_require_resolve_string: E.RequireResolveString, - e_require_call_target: void, - e_require_resolve_call_target: void, + e_require_call_target, + e_require_resolve_call_target, e_missing: E.Missing, e_this: E.This, @@ -5166,14 +5171,12 @@ pub const Expr = struct { e_undefined: E.Undefined, e_new_target: E.NewTarget, e_import_meta: E.ImportMeta, + e_import_meta_main: E.ImportMetaMain, + e_require_main, + e_inlined_enum: *E.InlinedEnum, - e_utf8_string: *E.UTF8String, - // This type should not exist outside of MacroContext - // If it ends up in JSParser or JSPrinter, it is a bug. - inline_identifier: i32, - pub fn as(data: Data, comptime tag: Tag) ?std.meta.FieldType(Data, tag) { return if (data == tag) @field(data, @tagName(tag)) else null; } @@ -5754,6 +5757,14 @@ pub const Expr = struct { equal: bool = false, ok: bool = false, + /// This extra flag is unfortunately required for the case of visiting the expression + /// `require.main === module` (and any combination of !==, ==, !=, either ordering) + /// + /// We want to replace this with the dedicated import_meta_main node, which: + /// - Stops this module from having p.require_ref, allowing conversion to ESM + /// - Allows us to inline `import.meta.main`'s value, if it is known (bun build --compile) + is_require_main_and_module: bool = false, + pub const @"true" = Equality{ .ok = true, .equal = true }; pub const @"false" = Equality{ .ok = true, .equal = false }; pub const unknown = Equality{ .ok = false }; @@ -5765,12 +5776,14 @@ pub const Expr = struct { pub fn eql( left: Expr.Data, right: Expr.Data, - allocator: std.mem.Allocator, + p: anytype, comptime kind: enum { loose, strict }, ) Equality { + comptime bun.assert(@typeInfo(@TypeOf(p)).Pointer.size == .One); // pass *Parser + // https://dorey.github.io/JavaScript-Equality-Table/ switch (left) { - .e_inlined_enum => |inlined| return inlined.value.data.eql(right, allocator, kind), + .e_inlined_enum => |inlined| return inlined.value.data.eql(right, p, kind), .e_null, .e_undefined => { const ok = switch (@as(Expr.Tag, right)) { @@ -5881,8 +5894,8 @@ pub const Expr = struct { .e_string => |l| { switch (right) { .e_string => |r| { - r.resolveRopeIfNeeded(allocator); - l.resolveRopeIfNeeded(allocator); + r.resolveRopeIfNeeded(p.allocator); + l.resolveRopeIfNeeded(p.allocator); return .{ .ok = true, .equal = r.eql(E.String, l), @@ -5892,8 +5905,8 @@ pub const Expr = struct { if (inlined.value.data == .e_string) { const r = inlined.value.data.e_string; - r.resolveRopeIfNeeded(allocator); - l.resolveRopeIfNeeded(allocator); + r.resolveRopeIfNeeded(p.allocator); + l.resolveRopeIfNeeded(p.allocator); return .{ .ok = true, @@ -5924,7 +5937,20 @@ pub const Expr = struct { else => {}, } }, - else => {}, + + else => { + // Do not need to check left because e_require_main is + // always re-ordered to the right side. + if (right == .e_require_main) { + if (left.as(.e_identifier)) |id| { + if (id.ref.eql(p.module_ref)) return .{ + .ok = true, + .equal = true, + .is_require_main_and_module = true, + }; + } + } + }, } return Equality.unknown; @@ -5949,7 +5975,6 @@ pub const Expr = struct { .e_identifier, .e_import_identifier, - .inline_identifier, .e_private_identifier, .e_commonjs_export_identifier, => error.@"Cannot convert identifier to JS. Try a statically-known value", diff --git a/src/js_parser.zig b/src/js_parser.zig index 370d6bb579..a50470e86d 100644 --- a/src/js_parser.zig +++ b/src/js_parser.zig @@ -511,13 +511,6 @@ const TransposeState = struct { import_options: Expr = Expr.empty, }; -var true_args = &[_]Expr{ - .{ - .data = .{ .e_boolean = .{ .value = true } }, - .loc = logger.Loc.Empty, - }, -}; - const JSXTag = struct { pub const TagType = enum { fragment, tag }; pub const Data = union(TagType) { @@ -1845,6 +1838,7 @@ pub const SideEffects = enum(u1) { .e_number, .e_big_int, .e_inlined_enum, + .e_require_main, => true, else => false, }; @@ -3084,6 +3078,10 @@ pub const Parser = struct { transform_only: bool = false, + /// Used for inlining the state of import.meta.main during visiting + import_meta_main_value: ?bool = null, + lower_import_meta_main_for_node_js: bool = false, + pub fn hashForRuntimeTranspiler(this: *const Options, hasher: *std.hash.Wyhash, did_use_jsx: bool) void { bun.assert(!this.bundle); @@ -7519,8 +7517,14 @@ fn NewParser_( } }, .bin_loose_eq => { - const equality = e_.left.data.eql(e_.right.data, p.allocator, .loose); + const equality = e_.left.data.eql(e_.right.data, p, .loose); if (equality.ok) { + if (equality.is_require_main_and_module) { + p.ignoreUsageOfRuntimeRequire(); + p.ignoreUsage(p.module_ref); + return p.valueForImportMetaMain(false, v.loc); + } + return p.newExpr( E.Boolean{ .value = equality.equal }, v.loc, @@ -7542,8 +7546,14 @@ fn NewParser_( }, .bin_strict_eq => { - const equality = e_.left.data.eql(e_.right.data, p.allocator, .strict); + const equality = e_.left.data.eql(e_.right.data, p, .strict); if (equality.ok) { + if (equality.is_require_main_and_module) { + p.ignoreUsage(p.module_ref); + p.ignoreUsageOfRuntimeRequire(); + return p.valueForImportMetaMain(false, v.loc); + } + return p.newExpr(E.Boolean{ .value = equality.equal }, v.loc); } @@ -7552,8 +7562,14 @@ fn NewParser_( // TODO: warn about typeof string }, .bin_loose_ne => { - const equality = e_.left.data.eql(e_.right.data, p.allocator, .loose); + const equality = e_.left.data.eql(e_.right.data, p, .loose); if (equality.ok) { + if (equality.is_require_main_and_module) { + p.ignoreUsage(p.module_ref); + p.ignoreUsageOfRuntimeRequire(); + return p.valueForImportMetaMain(true, v.loc); + } + return p.newExpr(E.Boolean{ .value = !equality.equal }, v.loc); } // const after_op_loc = locAfterOp(e_.); @@ -7566,8 +7582,14 @@ fn NewParser_( } }, .bin_strict_ne => { - const equality = e_.left.data.eql(e_.right.data, p.allocator, .strict); + const equality = e_.left.data.eql(e_.right.data, p, .strict); if (equality.ok) { + if (equality.is_require_main_and_module) { + p.ignoreUsage(p.module_ref); + p.ignoreUsageOfRuntimeRequire(); + return p.valueForImportMetaMain(true, v.loc); + } + return p.newExpr(E.Boolean{ .value = !equality.equal }, v.loc); } }, @@ -16880,24 +16902,6 @@ fn NewParser_( } }, - .inline_identifier => |id| { - const ref = p.macro.imports.get(id) orelse { - p.panic("Internal error: missing identifier from macro: {d}", .{id}); - }; - - if (!p.is_control_flow_dead) { - p.recordUsage(ref); - } - - return p.newExpr( - E.ImportIdentifier{ - .was_originally_identifier = false, - .ref = ref, - }, - expr.loc, - ); - }, - .e_binary => |e_| { // The handling of binary expressions is convoluted because we're using @@ -17154,9 +17158,9 @@ fn NewParser_( .e_unary => |e_| { switch (e_.op) { .un_typeof => { - const id_before = std.meta.activeTag(e_.value.data) == Expr.Tag.e_identifier; + const id_before = e_.value.data == .e_identifier; e_.value = p.visitExprInOut(e_.value, ExprIn{ .assign_target = e_.op.unaryAssignTarget() }); - const id_after = std.meta.activeTag(e_.value.data) == Expr.Tag.e_identifier; + const id_after = e_.value.data == .e_identifier; // The expression "typeof (0, x)" must not become "typeof x" if "x" // is unbound because that could suppress a ReferenceError from "x" @@ -17168,6 +17172,11 @@ fn NewParser_( ); } + if (e_.value.data == .e_require_call_target) { + p.ignoreUsageOfRuntimeRequire(); + return p.newExpr(E.String{ .data = "function" }, expr.loc); + } + if (SideEffects.typeof(e_.value.data)) |typeof| { return p.newExpr(E.String{ .data = typeof }, expr.loc); } @@ -17193,6 +17202,10 @@ fn NewParser_( if (e_.value.maybeSimplifyNot(p.allocator)) |exp| { return exp; } + if (e_.value.data == .e_import_meta_main) { + e_.value.data.e_import_meta_main.inverted = !e_.value.data.e_import_meta_main.inverted; + return e_.value; + } } }, .un_cpl => { @@ -17868,6 +17881,16 @@ fn NewParser_( } } + fn ignoreUsageOfRuntimeRequire(p: *P) void { + if (!p.options.features.use_import_meta_require and + p.options.features.allow_runtime) + { + bun.assert(p.runtime_imports.__require != null); + p.ignoreUsage(p.runtimeIdentifierRef(logger.Loc.Empty, "__require")); + p.symbols.items[p.require_ref.innerIndex()].use_count_estimate -|= 1; + } + } + inline fn valueForRequire(p: *P, loc: logger.Loc) Expr { bun.assert(!p.isSourceRuntime()); return Expr{ @@ -17878,6 +17901,32 @@ fn NewParser_( }; } + inline fn valueForImportMetaMain(p: *P, inverted: bool, loc: logger.Loc) Expr { + if (p.options.import_meta_main_value) |known| { + return .{ .loc = loc, .data = .{ .e_boolean = .{ .value = if (inverted) !known else known } } }; + } else { + // Node.js does not have import.meta.main, so we end up lowering + // this to `require.main === module`, but with the ESM format, + // both `require` and `module` are not present, so the code + // generation we need is: + // + // import { createRequire } from "node:module"; + // var __require = createRequire(import.meta.url); + // var import_meta_main = __require.main === __require.module; + // + // The printer can handle this for us, but we need to reference + // a handle to the `__require` function. + if (p.options.lower_import_meta_main_for_node_js) { + p.recordUsageOfRuntimeRequire(); + } + + return .{ + .loc = loc, + .data = .{ .e_import_meta_main = .{ .inverted = inverted } }, + }; + } + } + fn visitArgs(p: *P, args: []G.Arg, opts: VisitArgsOpts) void { const strict_loc = fnBodyContainsUseStrict(opts.body); const has_simple_args = isSimpleParameterList(args, opts.has_rest_arg); @@ -18946,6 +18995,15 @@ fn NewParser_( target.loc, ); } + + if (strings.eqlComptime(name, "main")) { + return p.valueForImportMetaMain(false, target.loc); + } + }, + .e_require_call_target => { + if (strings.eqlComptime(name, "main")) { + return .{ .loc = loc, .data = .e_require_main }; + } }, .e_import_identifier => |id| { // Symbol uses due to a property access off of an imported symbol are tracked @@ -23889,12 +23947,10 @@ fn NewParser_( p.require_ref, .force_cjs_to_esm = p.unwrap_all_requires or exports_kind == .esm_with_dynamic_fallback_from_cjs, - .uses_module_ref = (p.symbols.items[p.module_ref.innerIndex()].use_count_estimate > 0), - .uses_exports_ref = (p.symbols.items[p.exports_ref.innerIndex()].use_count_estimate > 0), - .uses_require_ref = if (p.runtime_imports.__require != null) - (p.symbols.items[p.runtime_imports.__require.?.ref.innerIndex()].use_count_estimate > 0) - else - false, + .uses_module_ref = p.symbols.items[p.module_ref.inner_index].use_count_estimate > 0, + .uses_exports_ref = p.symbols.items[p.exports_ref.inner_index].use_count_estimate > 0, + .uses_require_ref = p.runtime_imports.__require != null and + p.symbols.items[p.runtime_imports.__require.?.ref.inner_index].use_count_estimate > 0, // .top_Level_await_keyword = p.top_level_await_keyword, .commonjs_named_exports = p.commonjs_named_exports, .has_commonjs_export_names = p.has_commonjs_export_names, diff --git a/src/js_printer.zig b/src/js_printer.zig index 6a636edffc..27182343fb 100644 --- a/src/js_printer.zig +++ b/src/js_printer.zig @@ -526,12 +526,14 @@ pub const Options = struct { source_map_handler: ?SourceMapHandler = null, source_map_builder: ?*bun.sourcemap.Chunk.Builder = null, css_import_behavior: Api.CssInJsBehavior = Api.CssInJsBehavior.facade, + target: options.Target = .browser, runtime_transpiler_cache: ?*bun.JSC.RuntimeTranspilerCache = null, commonjs_named_exports: js_ast.Ast.CommonJSNamedExports = .{}, commonjs_named_exports_deoptimized: bool = false, commonjs_named_exports_ref: Ref = Ref.None, + commonjs_module_ref: Ref = Ref.None, minify_whitespace: bool = false, minify_identifiers: bool = false, @@ -2282,8 +2284,8 @@ fn NewPrinter( } } - pub fn printExpr(p: *Printer, expr: Expr, level: Level, _flags: ExprFlag.Set) void { - var flags = _flags; + pub fn printExpr(p: *Printer, expr: Expr, level: Level, in_flags: ExprFlag.Set) void { + var flags = in_flags; switch (expr.data) { .e_missing => {}, @@ -2333,6 +2335,47 @@ fn NewPrinter( p.printSymbol(p.options.import_meta_ref); } }, + .e_import_meta_main => |data| { + if (p.options.module_type == .esm and p.options.target != .node) { + // Node.js doesn't support import.meta.main + // Most of the time, leave it in there + if (data.inverted) { + p.addSourceMapping(expr.loc); + p.print("!"); + } else { + p.printSpaceBeforeIdentifier(); + p.addSourceMapping(expr.loc); + } + p.print("import.meta.main"); + } else { + p.printSpaceBeforeIdentifier(); + p.addSourceMapping(expr.loc); + + if (p.options.require_ref) |require| + p.printSymbol(require) + else + p.print("require"); + + if (data.inverted) + p.printWhitespacer(ws(".main != ")) + else + p.printWhitespacer(ws(".main == ")); + + if (p.options.target == .node) { + // "__require.module" + if (p.options.require_ref) |require| + p.printSymbol(require) + else + p.print("require"); + + p.print(".module"); + } else if (p.options.commonjs_module_ref.isValid()) { + p.printSymbol(p.options.commonjs_module_ref); + } else { + p.print("module"); + } + } + }, .e_commonjs_export_identifier => |id| { p.printSpaceBeforeIdentifier(); p.addSourceMapping(expr.loc); @@ -2461,6 +2504,19 @@ fn NewPrinter( p.print(")"); } }, + .e_require_main => { + p.printSpaceBeforeIdentifier(); + p.addSourceMapping(expr.loc); + + if (p.options.module_type == .esm and is_bun_platform) { + p.print("import.meta.require.main"); + } else if (p.options.require_ref) |require_ref| { + p.printSymbol(require_ref); + p.print(".main"); + } else { + p.print("require.main"); + } + }, .e_require_call_target => { p.printSpaceBeforeIdentifier(); p.addSourceMapping(expr.loc); @@ -3225,7 +3281,11 @@ fn NewPrinter( p.print(" */"); } }, - else => { + + .e_jsx_element, + .e_private_identifier, + .e_template_part, + => { if (Environment.isDebug) Output.panic("Unexpected expression of type .{s}", .{@tagName(expr.data)}); }, diff --git a/src/options.zig b/src/options.zig index 9a7abd5763..61e4fc3539 100644 --- a/src/options.zig +++ b/src/options.zig @@ -1510,6 +1510,7 @@ pub const BundleOptions = struct { install: ?*Api.BunInstall = null, inlining: bool = false, + inline_entrypoint_import_meta_main: bool = false, minify_whitespace: bool = false, minify_syntax: bool = false, minify_identifiers: bool = false, diff --git a/test/bundler/bundler_compile.test.ts b/test/bundler/bundler_compile.test.ts index 934b8e9571..b07c72552c 100644 --- a/test/bundler/bundler_compile.test.ts +++ b/test/bundler/bundler_compile.test.ts @@ -296,4 +296,20 @@ describe("bundler", () => { }, run: { stdout: '{"\u{6211}":"\u{6211}"}' }, }); + itBundled("compile/ImportMetaMain", { + compile: true, + files: { + "/entry.ts": /* js */ ` + // test toString on function to observe what the inlined value was + console.log((() => import.meta.main).toString().includes('true')); + console.log((() => !import.meta.main).toString().includes('false')); + console.log((() => !!import.meta.main).toString().includes('true')); + console.log((() => require.main == module).toString().includes('true')); + console.log((() => require.main === module).toString().includes('true')); + console.log((() => require.main !== module).toString().includes('false')); + console.log((() => require.main !== module).toString().includes('false')); + `, + }, + run: { stdout: new Array(7).fill("true").join("\n") }, + }); }); diff --git a/test/bundler/bundler_edgecase.test.ts b/test/bundler/bundler_edgecase.test.ts index a5948940b3..abf6b0a5f4 100644 --- a/test/bundler/bundler_edgecase.test.ts +++ b/test/bundler/bundler_edgecase.test.ts @@ -1767,6 +1767,45 @@ describe("bundler", () => { `, }, }); + itBundled("edgecase/ImportMetaMain", { + files: { + "/entry.ts": /* js */ ` + import {other} from './other'; + console.log(capture(import.meta.main), capture(require.main === module), ...other); + `, + "/other.ts": ` + globalThis['ca' + 'pture'] = x => x; + + export const other = [capture(require.main === module), capture(import.meta.main)]; + `, + }, + capture: ["false", "false", "import.meta.main", "import.meta.main"], + onAfterBundle(api) { + // This should not be marked as a CommonJS module + api.expectFile("/out.js").not.toContain("require"); + api.expectFile("/out.js").not.toContain("module"); + }, + }); + itBundled("edgecase/ImportMetaMainTargetNode", { + files: { + "/entry.ts": /* js */ ` + import {other} from './other'; + console.log(capture(import.meta.main), capture(require.main === module), ...other); + `, + "/other.ts": ` + globalThis['ca' + 'pture'] = x => x; + + export const other = [capture(require.main === module), capture(import.meta.main)]; + `, + }, + target: 'node', + capture: ["false", "false", "__require.main == __require.module", "__require.main == __require.module"], + onAfterBundle(api) { + // This should not be marked as a CommonJS module + api.expectFile("/out.js").not.toMatch(/\brequire\b/); // __require is ok + api.expectFile("/out.js").not.toMatch(/[^\.:]module/); // `.module` and `node:module` are ok. + }, + }); // TODO(@paperdave): test every case of this. I had already tested it manually, but it may break later const requireTranspilationListESM = [ diff --git a/test/bundler/bundler_minify.test.ts b/test/bundler/bundler_minify.test.ts index da39021791..54ad7ab614 100644 --- a/test/bundler/bundler_minify.test.ts +++ b/test/bundler/bundler_minify.test.ts @@ -397,4 +397,74 @@ describe("bundler", () => { stdout: "PASS", }, }); + itBundled("minify/RequireInDeadBranch", { + files: { + "/entry.ts": /* js */ ` + if (0 !== 0) { + require; + } + `, + }, + outfile: "/out.js", + minifySyntax: true, + onAfterBundle(api) { + // This should not be marked as a CommonJS module + api.expectFile("/out.js").not.toContain("require"); + api.expectFile("/out.js").not.toContain("module"); + }, + }); + itBundled("minify/TypeOfRequire", { + files: { + "/entry.ts": /* js */ ` + capture(typeof require); + `, + }, + outfile: "/out.js", + capture: ['"function"'], + minifySyntax: true, + onAfterBundle(api) { + // This should not be marked as a CommonJS module + api.expectFile("/out.js").not.toContain("require"); + api.expectFile("/out.js").not.toContain("module"); + }, + }); + itBundled("minify/RequireMainToImportMetaMain", { + files: { + "/entry.ts": /* js */ ` + capture(require.main === module); + capture(require.main !== module); + capture(require.main == module); + capture(require.main != module); + capture(!(require.main === module)); + capture(!(require.main !== module)); + capture(!(require.main == module)); + capture(!(require.main != module)); + capture(!!(require.main === module)); + capture(!!(require.main !== module)); + capture(!!(require.main == module)); + capture(!!(require.main != module)); + `, + }, + outfile: "/out.js", + capture: [ + "import.meta.main", + "!import.meta.main", + "import.meta.main", + "!import.meta.main", + "!import.meta.main", + "import.meta.main", + "!import.meta.main", + "import.meta.main", + "import.meta.main", + "!import.meta.main", + "import.meta.main", + "!import.meta.main", + ], + minifySyntax: true, + onAfterBundle(api) { + // This should not be marked as a CommonJS module + api.expectFile("/out.js").not.toContain("require"); + api.expectFile("/out.js").not.toContain("module"); + }, + }); }); diff --git a/test/cli/run/run-importmetamain.ts b/test/cli/run/run-importmetamain.ts new file mode 100644 index 0000000000..e56414b366 --- /dev/null +++ b/test/cli/run/run-importmetamain.ts @@ -0,0 +1,44 @@ +import { expect, test } from "bun:test"; +import { mkdirSync } from "fs"; +import { bunEnv, bunExe, tmpdirSync } from "harness"; +import { join } from "path"; + +test("import.meta.main", async () => { + const dir = tmpdirSync(); + mkdirSync(dir, { recursive: true }); + await Bun.write(join(dir, "index1.js"), `import "fs"; console.log(JSON.stringify([typeof require, import.meta.main, !import.meta.main, require.main === module, require.main !== module]));`); + const { stdout } = Bun.spawnSync({ + cmd: [bunExe(), join(dir, "index1.js")], + cwd: dir, + env: bunEnv, + stderr: 'inherit', + stdout: 'pipe', + }); + expect(stdout.toString("utf8").trim()).toEqual(JSON.stringify([ + "function", + true, + false, + true, + false, + ])); +}); + +test("import.meta.main in a common.js file", async () => { + const dir = tmpdirSync(); + mkdirSync(dir, { recursive: true }); + await Bun.write(join(dir, "index1.js"), `module.exports = {}; console.log(JSON.stringify([typeof require, import.meta.main, !import.meta.main, require.main === module, require.main !== module]));`); + const { stdout } = Bun.spawnSync({ + cmd: [bunExe(), join(dir, "index1.js")], + cwd: dir, + env: bunEnv, + stderr: 'inherit', + stdout: 'pipe', + }); + expect(stdout.toString("utf8").trim()).toEqual(JSON.stringify([ + "function", + true, + false, + true, + false, + ])); +});