diff --git a/src/bun.js/api/JSBundler.zig b/src/bun.js/api/JSBundler.zig index b5d4a6a6775fa7..cf6abc15f8caac 100644 --- a/src/bun.js/api/JSBundler.zig +++ b/src/bun.js/api/JSBundler.zig @@ -58,6 +58,7 @@ pub const JSBundler = struct { rootdir: OwnedString = OwnedString.initEmpty(bun.default_allocator), serve: Serve = .{}, jsx: options.JSX.Pragma = .{}, + force_node_env: options.BundleOptions.ForceNodeEnv = .unspecified, code_splitting: bool = false, minify: Minify = .{}, no_macros: bool = false, diff --git a/src/bun.js/api/server/HTMLBundle.zig b/src/bun.js/api/server/HTMLBundle.zig index a35407720698fa..5af4f1b8d2a10a 100644 --- a/src/bun.js/api/server/HTMLBundle.zig +++ b/src/bun.js/api/server/HTMLBundle.zig @@ -288,6 +288,10 @@ pub const HTMLBundleRoute = struct { if (!server.config().development) { config.define.put("process.env.NODE_ENV", "\"production\"") catch bun.outOfMemory(); + config.jsx.development = false; + } else { + config.force_node_env = .development; + config.jsx.development = true; } config.source_map = .linked; diff --git a/src/bundler/bundle_v2.zig b/src/bundler/bundle_v2.zig index c1003b858741b8..fb9ff6757461f3 100644 --- a/src/bundler/bundle_v2.zig +++ b/src/bundler/bundle_v2.zig @@ -913,7 +913,14 @@ pub const BundleV2 = struct { task.tree_shaking = this.linker.options.tree_shaking; task.is_entry_point = is_entry_point; task.known_target = target; - task.jsx.development = this.bundlerForTarget(target).options.jsx.development; + { + const bundler = this.bundlerForTarget(target); + task.jsx.development = switch (bundler.options.force_node_env) { + .development => true, + .production => false, + .unspecified => bundler.options.jsx.development, + }; + } // Handle onLoad plugins as entry points if (!this.enqueueOnLoadPluginIfNeeded(task)) { @@ -1753,6 +1760,9 @@ pub const BundleV2 = struct { ); transpiler.options.env.behavior = config.env_behavior; transpiler.options.env.prefix = config.env_prefix.slice(); + if (config.force_node_env != .unspecified) { + transpiler.options.force_node_env = config.force_node_env; + } transpiler.options.entry_points = config.entry_points.keys(); transpiler.options.jsx = config.jsx; @@ -2875,7 +2885,11 @@ pub const BundleV2 = struct { resolve_task.secondary_path_for_commonjs_interop = secondary_path_to_copy; resolve_task.known_target = target; resolve_task.jsx = resolve_result.jsx; - resolve_task.jsx.development = this.bundlerForTarget(target).options.jsx.development; + resolve_task.jsx.development = switch (transpiler.options.force_node_env) { + .development => true, + .production => false, + .unspecified => transpiler.options.jsx.development, + }; // Figure out the loader. { @@ -3542,7 +3556,7 @@ pub const ParseTask = struct { }; }; - const debug = Output.scoped(.ParseTask, false); + const debug = Output.scoped(.ParseTask, true); pub fn init(resolve_result: *const _resolver.Result, source_index: Index, ctx: *BundleV2) ParseTask { return .{ @@ -7545,7 +7559,7 @@ pub const LinkerContext = struct { continue; } - _ = this.validateTLA(id, tla_keywords, tla_checks, input_files, import_records, flags); + _ = this.validateTLA(id, tla_keywords, tla_checks, input_files, import_records, flags, import_records_list); for (import_records) |record| { if (!record.source_index.isValid()) { @@ -11027,6 +11041,7 @@ pub const LinkerContext = struct { input_files: []Logger.Source, import_records: []ImportRecord, meta_flags: []JSMeta.Flags, + ast_import_records: []bun.BabyList(ImportRecord), ) js_ast.TlaCheck { var result_tla_check: *js_ast.TlaCheck = &tla_checks[source_index]; @@ -11038,7 +11053,7 @@ pub const LinkerContext = struct { for (import_records, 0..) |record, import_record_index| { if (Index.isValid(record.source_index) and (record.kind == .require or record.kind == .stmt)) { - const parent = c.validateTLA(record.source_index.get(), tla_keywords, tla_checks, input_files, import_records, meta_flags); + const parent = c.validateTLA(record.source_index.get(), tla_keywords, tla_checks, input_files, import_records, meta_flags, ast_import_records); if (Index.isInvalid(Index.init(parent.parent))) { continue; } @@ -11065,9 +11080,11 @@ pub const LinkerContext = struct { const parent_source_index = other_source_index; if (parent_result_tla_keyword.len > 0) { - tla_pretty_path = input_files[other_source_index].path.pretty; + const source = input_files[other_source_index]; + tla_pretty_path = source.path.pretty; notes.append(Logger.Data{ .text = std.fmt.allocPrint(c.allocator, "The top-level await in {s} is here:", .{tla_pretty_path}) catch bun.outOfMemory(), + .location = .initOrNull(&source, parent_result_tla_keyword), }) catch bun.outOfMemory(); break; } @@ -11086,6 +11103,7 @@ pub const LinkerContext = struct { input_files[parent_source_index].path.pretty, input_files[other_source_index].path.pretty, }) catch bun.outOfMemory(), + .location = .initOrNull(&input_files[parent_source_index], ast_import_records[parent_source_index].slice()[tla_checks[parent_source_index].import_record_index].range), }) catch bun.outOfMemory(); } diff --git a/src/crash_handler.zig b/src/crash_handler.zig index 1098ef91077660..982a2970a8abde 100644 --- a/src/crash_handler.zig +++ b/src/crash_handler.zig @@ -301,7 +301,13 @@ pub fn crashHandler( var trace_buf: std.builtin.StackTrace = undefined; // If a trace was not provided, compute one now - const trace = error_return_trace orelse get_backtrace: { + const trace = @as(?*std.builtin.StackTrace, if (error_return_trace) |ert| + if (ert.index > 0) + ert + else + null + else + null) orelse get_backtrace: { trace_buf = std.builtin.StackTrace{ .index = 0, .instruction_addresses = &addr_buf, diff --git a/src/js/internal/html.ts b/src/js/internal/html.ts index 471fd1879be9dd..d1f2f9cf67ef29 100644 --- a/src/js/internal/html.ts +++ b/src/js/internal/html.ts @@ -1,4 +1,5 @@ -// This is the file that loads when you pass a, .html entry point to Bun. +// This is the file that loads when you pass a '.html' entry point to Bun. +// It imports the entry points and initializes a server. import type { HTMLBundle, Server } from "bun"; const initial = performance.now(); const argv = process.argv; @@ -247,15 +248,18 @@ yourself with Bun.serve(). const enableANSIColors = Bun.enableANSIColors; function printInitialMessage(isFirst: boolean) { if (enableANSIColors) { - let topLine = `\n\x1b[1;34m\x1b[5mBun\x1b[0m \x1b[1;34mv${Bun.version}\x1b[0m`; + let topLine = `${server.development ? "\x1b[34;7m DEV \x1b[0m " : ""}\x1b[1;34m\x1b[5mBun\x1b[0m \x1b[1;34mv${Bun.version}\x1b[0m`; if (isFirst) { topLine += ` \x1b[2mready in\x1b[0m \x1b[1m${elapsed}\x1b[0m ms`; } console.log(topLine + "\n"); console.log(`\x1b[1;34m➜\x1b[0m \x1b[36m${server!.url.href}\x1b[0m`); } else { - let topLine = `\n Bun v${Bun.version}`; + let topLine = `Bun v${Bun.version}`; if (isFirst) { + if (server.development) { + topLine += " dev server"; + } topLine += ` ready in ${elapsed} ms`; } console.log(topLine + "\n"); diff --git a/src/js_parser.zig b/src/js_parser.zig index 7d9a1d1389ae21..c99fd26db8bf12 100644 --- a/src/js_parser.zig +++ b/src/js_parser.zig @@ -6812,7 +6812,10 @@ fn NewParser_( if (p.lexer.jsx_pragma.jsxRuntime()) |runtime| { if (options.JSX.RuntimeMap.get(runtime.text)) |jsx_runtime| { - p.options.jsx.runtime = jsx_runtime; + p.options.jsx.runtime = jsx_runtime.runtime; + if (jsx_runtime.development) |dev| { + p.options.jsx.development = dev; + } } else { // make this a warning instead of an error because we don't support "preserve" right now try p.log.addRangeWarningFmt(p.source, runtime.range, p.allocator, "Unsupported JSX runtime: \"{s}\"", .{runtime.text}); diff --git a/src/options.zig b/src/options.zig index 93a309ddc3a4b9..5388884980653d 100644 --- a/src/options.zig +++ b/src/options.zig @@ -991,13 +991,18 @@ pub const ESMConditions = struct { }; pub const JSX = struct { - pub const RuntimeMap = bun.ComptimeStringMap(JSX.Runtime, .{ - .{ "classic", .classic }, - .{ "automatic", .automatic }, - .{ "react", .classic }, - .{ "react-jsx", .automatic }, - .{ "react-jsxdev", .automatic }, - .{ "solid", .solid }, + const RuntimeDevelopmentPair = struct { + runtime: JSX.Runtime, + development: ?bool, + }; + + pub const RuntimeMap = bun.ComptimeStringMap(RuntimeDevelopmentPair, .{ + .{ "classic", RuntimeDevelopmentPair{ .runtime = .classic, .development = null } }, + .{ "automatic", RuntimeDevelopmentPair{ .runtime = .automatic, .development = true } }, + .{ "react", RuntimeDevelopmentPair{ .runtime = .classic, .development = null } }, + .{ "react-jsx", RuntimeDevelopmentPair{ .runtime = .automatic, .development = true } }, + .{ "react-jsxdev", RuntimeDevelopmentPair{ .runtime = .automatic, .development = true } }, + .{ "solid", RuntimeDevelopmentPair{ .runtime = .solid, .development = null } }, }); pub const Pragma = struct { @@ -1013,6 +1018,10 @@ pub const JSX = struct { classic_import_source: string = "react", package_name: []const u8 = "react", + /// Configuration Priority: + /// - `--define=process.env.NODE_ENV=...` + /// - `NODE_ENV=...` + /// - tsconfig.json's `compilerOptions.jsx` (`react-jsx` or `react-jsxdev`) development: bool = true, parse: bool = true, @@ -1575,13 +1584,27 @@ pub const BundleOptions = struct { supports_multiple_outputs: bool = true, + /// This is set by the process environment, which is used to override the + /// JSX configuration. When this is unspecified, the tsconfig.json is used + /// to determine if a development jsx-runtime is used (by going between + /// "react-jsx" or "react-jsx-dev-runtime") + force_node_env: ForceNodeEnv = .unspecified, + + pub const ForceNodeEnv = enum { + unspecified, + development, + production, + }; + pub fn isTest(this: *const BundleOptions) bool { return this.rewrite_jest_for_tests; } pub fn setProduction(this: *BundleOptions, value: bool) void { - this.production = value; - this.jsx.development = !value; + if (this.force_node_env == .unspecified) { + this.production = value; + this.jsx.development = !value; + } } pub const default_unwrap_commonjs_packages = [_]string{ diff --git a/src/resolver/tsconfig_json.zig b/src/resolver/tsconfig_json.zig index 0eb8a1832154e2..2da0564e736a29 100644 --- a/src/resolver/tsconfig_json.zig +++ b/src/resolver/tsconfig_json.zig @@ -216,12 +216,14 @@ pub const TSConfigJSON = struct { defer allocator.free(str_lower); _ = strings.copyLowercase(str, str_lower); // - We don't support "preserve" yet - // - We rely on NODE_ENV for "jsx" or "jsxDEV" - // - We treat "react-jsx" and "react-jsxDEV" identically - // because it is too easy to auto-import the wrong one. if (options.JSX.RuntimeMap.get(str_lower)) |runtime| { - result.jsx.runtime = runtime; + result.jsx.runtime = runtime.runtime; result.jsx_flags.insert(.runtime); + + if (runtime.development) |dev| { + result.jsx.development = dev; + result.jsx_flags.insert(.development); + } } } } diff --git a/src/transpiler.zig b/src/transpiler.zig index 7ac0299e04de43..acaf18b6388248 100644 --- a/src/transpiler.zig +++ b/src/transpiler.zig @@ -553,6 +553,7 @@ pub const Transpiler = struct { const has_production_env = this.env.isProduction(); if (!was_production and has_production_env) { this.options.setProduction(true); + this.resolver.opts.setProduction(true); } if (this.options.isTest() or this.env.isTest()) { @@ -567,6 +568,7 @@ pub const Transpiler = struct { this.env.loadProcess(); if (this.env.isProduction()) { this.options.setProduction(true); + this.resolver.opts.setProduction(true); } }, else => {}, @@ -590,7 +592,7 @@ pub const Transpiler = struct { try this.runEnvLoader(false); - this.options.jsx.setProduction(this.env.isProduction()); + var is_production = this.env.isProduction(); js_ast.Expr.Data.Store.create(); js_ast.Stmt.Data.Store.create(); @@ -600,11 +602,26 @@ pub const Transpiler = struct { try this.options.loadDefines(this.allocator, this.env, &this.options.env); + var is_development = false; if (this.options.define.dots.get("NODE_ENV")) |NODE_ENV| { - if (NODE_ENV.len > 0 and NODE_ENV[0].data.value == .e_string and NODE_ENV[0].data.value.e_string.eqlComptime("production")) { - this.options.production = true; + if (NODE_ENV.len > 0 and NODE_ENV[0].data.value == .e_string) { + if (NODE_ENV[0].data.value.e_string.eqlComptime("production")) { + is_production = true; + } else if (NODE_ENV[0].data.value.e_string.eqlComptime("development")) { + is_development = true; + } } } + + if (is_development) { + this.options.setProduction(false); + this.resolver.opts.setProduction(false); + this.options.force_node_env = .development; + this.resolver.opts.force_node_env = .development; + } else if (is_production) { + this.options.setProduction(true); + this.resolver.opts.setProduction(true); + } } pub fn resetStore(_: *const Transpiler) void { diff --git a/test/bundler/transpiler/jsx-dev/jsx-dev.tsx b/test/bundler/transpiler/jsx-dev/jsx-dev.tsx new file mode 100644 index 00000000000000..ee854a800268c0 --- /dev/null +++ b/test/bundler/transpiler/jsx-dev/jsx-dev.tsx @@ -0,0 +1,37 @@ +import { renderToReadableStream } from "react-dom/server.browser"; + +const HelloWorld = () => { + return
Hello World
; +}; + +const stream = new Response(await renderToReadableStream()); + +console.log(await stream.text()); + +if (!process.env.NO_BUILD) { + const self = await Bun.build({ + entrypoints: [import.meta.path], + define: { + "process.env.NODE_ENV": JSON.stringify(process.env.CHILD_NODE_ENV), + "process.env.NO_BUILD": "1", + }, + }); + const code = await self.outputs[0].text(); + let shouldHaveJSXDev = process.env.CHILD_NODE_ENV === "development"; + let shouldHaveJSX = process.env.CHILD_NODE_ENV === "production"; + + if (shouldHaveJSXDev) { + if (!code.includes("jsx_dev_runtime.jsxDEV")) { + throw new Error("jsxDEV is not included"); + } + } + + if (shouldHaveJSX) { + if (!code.includes("jsx_runtime.jsx")) { + throw new Error("Jsx is not included"); + } + } + + const url = URL.createObjectURL(self.outputs[0]); + await import(url); +} diff --git a/test/bundler/transpiler/jsx-dev/tsconfig.json b/test/bundler/transpiler/jsx-dev/tsconfig.json new file mode 100644 index 00000000000000..12a075f81a9734 --- /dev/null +++ b/test/bundler/transpiler/jsx-dev/tsconfig.json @@ -0,0 +1,6 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "jsx": "react-jsxdev" + } +} diff --git a/test/bundler/transpiler/jsx-production-entry.ts b/test/bundler/transpiler/jsx-production-entry.ts new file mode 100644 index 00000000000000..eec766abee1578 --- /dev/null +++ b/test/bundler/transpiler/jsx-production-entry.ts @@ -0,0 +1 @@ +import "./jsx-production"; diff --git a/test/bundler/transpiler/jsx-production.test.ts b/test/bundler/transpiler/jsx-production.test.ts new file mode 100644 index 00000000000000..7fd7ad30d3062b --- /dev/null +++ b/test/bundler/transpiler/jsx-production.test.ts @@ -0,0 +1,36 @@ +import { describe, expect, test, afterAll } from "bun:test"; +import path from "path"; +import { bunExe, bunEnv } from "harness"; + +const original_node_env = bunEnv.NODE_ENV; + +// https://github.com/oven-sh/bun/issues/3768 +describe("jsx", () => { + for (const node_env of ["production", "development", "test", ""]) { + for (const child_node_env of ["production", "development", "test", ""]) { + test(`react-jsxDEV parent: ${node_env} child: ${child_node_env} should work`, async () => { + bunEnv.NODE_ENV = node_env; + bunEnv.CHILD_NODE_ENV = child_node_env; + bunEnv.TSCONFIG_JSX = "react-jsxdev"; + expect([path.join(import.meta.dirname, "jsx-dev", "jsx-dev.tsx")]).toRun( + "
Hello World
" + "\n" + "
Hello World
" + "\n", + ); + }); + + test(`react-jsx parent: ${node_env} child: ${child_node_env} should work`, async () => { + bunEnv.NODE_ENV = node_env; + bunEnv.CHILD_NODE_ENV = child_node_env; + bunEnv.TSCONFIG_JSX = "react-jsx"; + expect([path.join(import.meta.dirname, "jsx-production-entry.ts")]).toRun( + "
Hello World
" + "\n" + "
Hello World
" + "\n", + ); + }); + } + } + + afterAll(() => { + bunEnv.NODE_ENV = original_node_env; + delete bunEnv.CHILD_NODE_ENV; + delete bunEnv.TSCONFIG_JSX; + }); +}); diff --git a/test/bundler/transpiler/jsx-production.tsx b/test/bundler/transpiler/jsx-production.tsx new file mode 100644 index 00000000000000..ee854a800268c0 --- /dev/null +++ b/test/bundler/transpiler/jsx-production.tsx @@ -0,0 +1,37 @@ +import { renderToReadableStream } from "react-dom/server.browser"; + +const HelloWorld = () => { + return
Hello World
; +}; + +const stream = new Response(await renderToReadableStream()); + +console.log(await stream.text()); + +if (!process.env.NO_BUILD) { + const self = await Bun.build({ + entrypoints: [import.meta.path], + define: { + "process.env.NODE_ENV": JSON.stringify(process.env.CHILD_NODE_ENV), + "process.env.NO_BUILD": "1", + }, + }); + const code = await self.outputs[0].text(); + let shouldHaveJSXDev = process.env.CHILD_NODE_ENV === "development"; + let shouldHaveJSX = process.env.CHILD_NODE_ENV === "production"; + + if (shouldHaveJSXDev) { + if (!code.includes("jsx_dev_runtime.jsxDEV")) { + throw new Error("jsxDEV is not included"); + } + } + + if (shouldHaveJSX) { + if (!code.includes("jsx_runtime.jsx")) { + throw new Error("Jsx is not included"); + } + } + + const url = URL.createObjectURL(self.outputs[0]); + await import(url); +} diff --git a/test/bundler/transpiler/tsconfig.json b/test/bundler/transpiler/tsconfig.json new file mode 100644 index 00000000000000..2431b86fe43ab9 --- /dev/null +++ b/test/bundler/transpiler/tsconfig.json @@ -0,0 +1,6 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "jsx": "react-jsx" + } +} diff --git a/test/js/bun/http/bun-serve-html.test.ts b/test/js/bun/http/bun-serve-html.test.ts index 39295e84b4a394..4c0860fec4cb0d 100644 --- a/test/js/bun/http/bun-serve-html.test.ts +++ b/test/js/bun/http/bun-serve-html.test.ts @@ -716,3 +716,56 @@ test("wildcard static routes", async () => { } } }); + +test("serve html with JSX runtime in development mode", async () => { + const dir = join(import.meta.dir, "jsx-runtime"); + const { default: html } = await import(join(dir, "index.html")); + + using server = Bun.serve({ + port: 0, + development: true, + static: { + "/": html, + }, + fetch(req) { + return new Response("Not found", { status: 404 }); + }, + }); + + const response = await fetch(server.url); + expect(response.status).toBe(200); + const htmlText = await response.text(); + const jsSrc = htmlText.match(/ + + +
+ +