From 589fa6274d41ad3325ead5133dd7d871bf870826 Mon Sep 17 00:00:00 2001 From: chloe caruso Date: Fri, 7 Mar 2025 17:53:07 -0800 Subject: [PATCH] dev server: forgotten changes (#17985) Co-authored-by: Jarred Sumner --- docs/bundler/hmr.md | 29 ++++++- src/bake/DevServer.zig | 7 +- src/bake/bake.zig | 1 + src/bake/client/websocket.ts | 15 ++-- src/bake/hmr-module.ts | 111 +++++++++++++++------------ src/bake/hmr-runtime-client.ts | 26 +++++-- src/bundler/bundle_v2.zig | 95 ++++++++++++++++++----- src/js_ast.zig | 9 +-- src/js_parser.zig | 68 ++++++++-------- src/js_printer.zig | 42 +++++----- src/transpiler.zig | 1 + test/bake/dev/bundle.test.ts | 59 ++++++++++++++ test/bundler/bundler_browser.test.ts | 2 +- 13 files changed, 319 insertions(+), 146 deletions(-) diff --git a/docs/bundler/hmr.md b/docs/bundler/hmr.md index bb681676cc..6213561ea4 100644 --- a/docs/bundler/hmr.md +++ b/docs/bundler/hmr.md @@ -58,7 +58,7 @@ doSomething(import.meta.hot.data); | ✅ | `hot.data` | Persist data between module evaluations. | | ✅ | `hot.dispose()` | Add a callback function to run when a module is about to be replaced. | | ❌ | `hot.invalidate()` | | -| 🚧 | `hot.on()` | **NOTE**: Only a subset of events are implemented. | +| ✅ | `hot.on()` | Attach an event listener | | ✅ | `hot.off()` | Remove an event listener from `on`. | | ❌ | `hot.send()` | | | 🚧 | `hot.prune()` | **NOTE**: Callback is currently never called. | @@ -205,3 +205,30 @@ import.meta.hot.prune(() => { If `dispose` was used instead, the WebSocket would close and re-open on every hot update. Both versions of the code will prevent page reloads when imported files are updated. + +### `import.meta.hot.on()` and `off()` + +`on()` and `off()` are used to listen for events from the HMR runtime. Event names are prefixed with a prefix so that plugins do not conflict with each other. + +```ts +import.meta.hot.on("bun:beforeUpdate", () => { + console.log("before a hot update"); +}); +``` + +When a file is replaced, all of its event listeners are automatically removed. + +A list of all built-in events: + +| Event | Emitted when | +| ---------------------- | ----------------------------------------------------------------------------------------------- | +| `bun:beforeUpdate` | before a hot update is applied. | +| `bun:afterUpdate` | after a hot update is applied. | +| `bun:beforeFullReload` | before a full page reload happens. | +| `bun:beforePrune` | before prune callbacks are called. | +| `bun:invalidate` | when a module is invalidated with `import.meta.hot.invalidate()` | +| `bun:error` | when a build or runtime error occurs | +| `bun:ws:disconnect` | when the HMR WebSocket connection is lost. This can indicate the development server is offline. | +| `bun:ws:connect` | when the HMR WebSocket connects or re-connects. | + +For compatibility with Vite, the above events are also available via `vite:*` prefix instead of `bun:*`. diff --git a/src/bake/DevServer.zig b/src/bake/DevServer.zig index 8b675ed256..db63dc3903 100644 --- a/src/bake/DevServer.zig +++ b/src/bake/DevServer.zig @@ -2987,6 +2987,9 @@ pub fn IncrementalGraph(side: bake.Side) type { /// exact size, instead of the log approach that dynamic arrays use. stale_files: DynamicBitSetUnmanaged, + // TODO: rename `dependencies` to something that clearly indicates direction. + // such as "parent" or "consumer" + /// Start of a file's 'dependencies' linked list. These are the other /// files that have imports to this file. Walk this list to discover /// what files are to be reloaded when something changes. @@ -3276,9 +3279,9 @@ pub fn IncrementalGraph(side: bake.Side) type { // for a simpler example. It is more complicated here because this // structure is two-way. pub const Edge = struct { - /// The file with the `import` statement + /// The file with the import statement dependency: FileIndex, - /// The file that `dependency` is importing + /// The file the import statement references. imported: FileIndex, next_import: EdgeIndex.Optional, diff --git a/src/bake/bake.zig b/src/bake/bake.zig index 95931f0a2c..eec9f07262 100644 --- a/src/bake/bake.zig +++ b/src/bake/bake.zig @@ -646,6 +646,7 @@ pub const Framework = struct { out.options.minify_whitespace = mode != .development; out.options.css_chunking = true; out.options.framework = framework; + out.options.inline_entrypoint_import_meta_main = true; if (bundler_options.ignoreDCEAnnotations) |ignore| out.options.ignore_dce_annotations = ignore; diff --git a/src/bake/client/websocket.ts b/src/bake/client/websocket.ts index 470ff595e7..c3f2d39c54 100644 --- a/src/bake/client/websocket.ts +++ b/src/bake/client/websocket.ts @@ -52,7 +52,7 @@ export function getMainWebSocket(): WebSocketWrapper | null { export function initWebSocket( handlers: Record, ws: WebSocket) => void>, - { url = "/_bun/hmr" }: { url?: string } = {}, + { url = "/_bun/hmr", onStatusChange }: { url?: string; onStatusChange?: (connected: boolean) => void } = {}, ): WebSocketWrapper { let firstConnection = true; let closed = false; @@ -81,11 +81,9 @@ export function initWebSocket( mainWebSocket = wsProxy; } - function onOpen() { - if (firstConnection) { - firstConnection = false; - console.info("[Bun] Hot-module-reloading socket connected, waiting for changes..."); - } + function onFirstOpen() { + console.info("[Bun] Hot-module-reloading socket connected, waiting for changes..."); + onStatusChange?.(true); } function onMessage(ev: MessageEvent) { @@ -107,6 +105,7 @@ export function initWebSocket( } async function onClose() { + onStatusChange?.(false); console.warn("[Bun] Hot-module-reloading socket disconnected, reconnecting..."); await new Promise(done => setTimeout(done, 1000)); @@ -123,7 +122,7 @@ export function initWebSocket( ws.onopen = () => { console.info("[Bun] Reconnected"); done(true); - onOpen(); + onStatusChange?.(true); ws.onerror = onError; }; ws.onmessage = onMessage; @@ -141,7 +140,7 @@ export function initWebSocket( let ws = (wsProxy.wrapped = new WebSocket(url)); ws.binaryType = "arraybuffer"; - ws.onopen = onOpen; + ws.onopen = onFirstOpen; ws.onmessage = onMessage; ws.onclose = onClose; ws.onerror = onError; diff --git a/src/bake/hmr-module.ts b/src/bake/hmr-module.ts index fc0900b30c..f0ecb339ff 100644 --- a/src/bake/hmr-module.ts +++ b/src/bake/hmr-module.ts @@ -23,10 +23,7 @@ export const serverManifest = {}; export const ssrManifest = {}; /** Client */ export let onServerSideReload: (() => Promise) | null = null; -const eventHandlers: Record = { - "bun:afterUpdate": [], - "bun:beforeUpdate": [], -}; +const eventHandlers: Record = {}; let refreshRuntime: any; /** The expression `import(a,b)` is not supported in all browsers, most notably * in Mozilla Firefox in 2025. Bun lazily evaluates it, so a SyntaxError gets @@ -39,6 +36,13 @@ const enum State { Loaded, Error, } +const enum ESMProps { + imports, + exports, + stars, + load, + isAsync, +} /** Given an Id, return the module namespace object. * For use in other functions in the HMR runtime. @@ -222,17 +226,14 @@ export class HMRModule { } invalidate() { + emitEvent("bun:invalidate", null); // by throwing an error right now, this will cause a page refresh throw new Error("TODO: implement ImportMetaHot.invalidate"); } on(event: string, cb: HotEventHandler) { - if (isUnsupportedViteEventName(event)) { - throw new Error(`Unsupported event name: ${event}`); - } - // Vite compatibility, but favor using Bun's event names. - if (event === "vite:beforeUpdate" || event === "vite:afterUpdate") { + if (event.startsWith("vite:")) { event = "bun:" + event.slice(4); } @@ -272,6 +273,7 @@ HMRModule.prototype.indirectHot = new Proxy({}, { }, }); +// TODO: This function is currently recursive. export function loadModuleSync(id: Id, isUserDynamic: boolean, importer: HMRModule | null): HMRModule { // First, try and re-use an existing module. let mod = registry.get(id); @@ -311,17 +313,17 @@ export function loadModuleSync(id: Id, isUserDynamic: boolean, importer: HMRModu // ESM if (IS_BUN_DEVELOPMENT) { try { - assert(Array.isArray(loadOrEsmModule[0])); - assert(Array.isArray(loadOrEsmModule[1])); - assert(Array.isArray(loadOrEsmModule[2])); - assert(typeof loadOrEsmModule[3] === "function"); - assert(typeof loadOrEsmModule[4] === "boolean"); + assert(Array.isArray(loadOrEsmModule[ESMProps.imports])); + assert(Array.isArray(loadOrEsmModule[ESMProps.exports])); + assert(Array.isArray(loadOrEsmModule[ESMProps.stars])); + assert(typeof loadOrEsmModule[ESMProps.load] === "function"); + assert(typeof loadOrEsmModule[ESMProps.isAsync] === "boolean"); } catch (e) { console.warn(id, loadOrEsmModule); throw e; } } - const [deps /* exports */ /* stars */, , , load, isAsync] = loadOrEsmModule; + const { [ESMProps.imports]: deps, [ESMProps.load]: load, [ESMProps.isAsync]: isAsync } = loadOrEsmModule; if (isAsync) { throw new AsyncImportError(id); } @@ -350,6 +352,7 @@ export function loadModuleSync(id: Id, isUserDynamic: boolean, importer: HMRModu // `HMRModule`s can be created synchronously, even if evaluation is not. // Returns `null` if the module is not found in dynamic mode, so that the caller // can use the `import` keyword instead. +// TODO: This function is currently recursive. export function loadModuleAsync( id: Id, isUserDynamic: IsUserDynamic, @@ -358,8 +361,9 @@ export function loadModuleAsync( // First, try and re-use an existing module. let mod = registry.get(id)!; if (mod) { - if (mod.state === State.Error) throw mod.failure; - if (mod.state === State.Stale) { + const { state } = mod; + if (state === State.Error) throw mod.failure; + if (state === State.Stale) { mod.state = State.Pending; isUserDynamic = false as IsUserDynamic; } else { @@ -475,6 +479,7 @@ function finishLoadModuleAsync(mod: HMRModule, load: UnloadedESM[3], modules: HM } type GenericModuleLoader = (id: Id, isUserDynamic: false, importer: HMRModule) => R; +// TODO: This function is currently recursive. function parseEsmDependencies>( mod: HMRModule, deps: (string | number)[], @@ -482,9 +487,9 @@ function parseEsmDependencies>( ) { let i = 0; let list: ReturnType[] = []; - let dedupeSet: Set | null = null; let isAsync = false; - while (i < deps.length) { + const { length } = deps; + while (i < length) { const dep = deps[i] as string; if (IS_BUN_DEVELOPMENT) assert(typeof dep === "string"); let expectedExportKeyEnd = i + 2 + (deps[i + 1] as number); @@ -496,21 +501,19 @@ function parseEsmDependencies>( throwNotFound(dep, false); } if (typeof unloadedModule !== "function") { - const availableExportKeys = unloadedModule[1]; + const availableExportKeys = unloadedModule[ESMProps.exports]; i += 2; while (i < expectedExportKeyEnd) { const key = deps[i] as string; if (IS_BUN_DEVELOPMENT) assert(typeof key === "string"); if (!availableExportKeys.includes(key)) { - dedupeSet ??= new Set(); - if (!findExportStar(unloadedModule[2], key, dedupeSet)) { + if (!hasExportStar(unloadedModule[ESMProps.stars], key)) { throw new SyntaxError(`Module "${dep}" does not export key "${key}"`); } - dedupeSet.clear(); } i++; } - isAsync ||= unloadedModule[4]; + isAsync ||= unloadedModule[ESMProps.isAsync]; } else { if (IS_BUN_DEVELOPMENT) assert(!registry.get(dep)?.esm); i = expectedExportKeyEnd; @@ -519,24 +522,31 @@ function parseEsmDependencies>( return { list, isAsync }; } -function findExportStar(starImports: Id[], key: string, dedupeSet: Set) { - for (const starImport of starImports) { - if (dedupeSet.has(starImport)) continue; - dedupeSet.add(starImport); +function hasExportStar(starImports: Id[], key: string) { + if (starImports.length === 0) return false; + const queue: Id[] = [...starImports]; + const visited = new Set(); + while (queue.length > 0) { + const starImport = queue.shift()!; + if (visited.has(starImport)) continue; + visited.add(starImport); const mod = unloadedModuleRegistry[starImport]; + if (IS_BUN_DEVELOPMENT) assert(mod, `Module "${starImport}" not found`); if (typeof mod === "function") { - // CommonJS has dynamic keys (can export anything, even a Proxy) return true; } - const availableExportKeys = mod[1]; + const availableExportKeys = mod[ESMProps.exports]; if (availableExportKeys.includes(key)) { return true; // Found } - // Recurse to further star imports. - if (findExportStar(mod[2], key, dedupeSet)) { - return true; + const nestedStarImports = mod[ESMProps.stars]; + for (const nestedImport of nestedStarImports) { + if (!visited.has(nestedImport)) { + queue.push(nestedImport); + } } } + return false; } @@ -548,7 +558,15 @@ type HotAcceptFunction = (esmExports?: any | void) => void; type HotArrayAcceptFunction = (esmExports: (any | void)[]) => void; type HotDisposeFunction = (data: any) => void | Promise; type HotEventHandler = (data: any) => void; -type HMREvent = ("bun:beforeUpdate" | "bun:afterUpdate") & string; +type HMREvent = + | "bun:beforeUpdate" + | "bun:afterUpdate" + | "bun:beforeFullReload" + | "bun:beforePrune" + | "bun:invalidate" + | "bun:error" + | "bun:ws:disconnect" + | "bun:ws:connect"; /** Called when modules are replaced. */ export async function replaceModules(modules: Record) { @@ -566,8 +584,7 @@ export async function replaceModules(modules: Record) { const toDispose: HMRModule[] = []; // Discover all HMR boundaries - outer: for (const key in modules) { - if (!modules.hasOwnProperty(key)) continue; + outer: for (const key of Object.keys(modules)) { const existing = registry.get(key); if (!existing) continue; @@ -670,7 +687,7 @@ export async function replaceModules(modules: Record) { kind: "warn", }), ); - location.reload(); + fullReload(); } else { console.warn(message); } @@ -761,18 +778,7 @@ function createAcceptArray(modules: string[], key: Id) { return arr; } -function isUnsupportedViteEventName(str: string) { - return ( - str === "vite:beforeFullReload" || - str === "vite:beforePrune" || - str === "vite:invalidate" || - str === "vite:error" || - str === "vite:ws:disconnect" || - str === "vite:ws:connect" - ); -} - -function emitEvent(event: string, data: any) { +export function emitEvent(event: HMREvent, data: any) { const handlers = eventHandlers[event]; if (!handlers) return; for (const handler of handlers) { @@ -794,6 +800,13 @@ function throwNotFound(id: Id, isUserDynamic: boolean) { ); } +export function fullReload() { + try { + emitEvent("bun:beforeFullReload", null); + } catch {} + location.reload(); +} + class AsyncImportError extends Error { asyncId: string; constructor(asyncId: string) { diff --git a/src/bake/hmr-runtime-client.ts b/src/bake/hmr-runtime-client.ts index 1e5b8d7b40..d5ce1b4113 100644 --- a/src/bake/hmr-runtime-client.ts +++ b/src/bake/hmr-runtime-client.ts @@ -1,6 +1,13 @@ // This file is the entrypoint to the hot-module-reloading runtime // In the browser, this uses a WebSocket to communicate with the bundler. -import { loadModuleAsync, replaceModules, onServerSideReload, loadExports, setRefreshRuntime } from "./hmr-module"; +import { + loadModuleAsync, + replaceModules, + onServerSideReload, + setRefreshRuntime, + emitEvent, + fullReload, +} from "./hmr-module"; import { hasFatalError, onServerErrorPayload, onRuntimeError } from "./client/overlay"; import { DataViewReader } from "./client/data-view"; import { initWebSocket } from "./client/websocket"; @@ -41,15 +48,15 @@ async function performRouteReload() { // Fallback for when reloading fails or is not implemented by the framework is // to hard-reload. - location.reload(); + fullReload(); } let isFirstRun = true; -const ws = initWebSocket({ +const handlers = { [MessageId.version](view) { if (td.decode(view.buffer.slice(1)) !== config.version) { console.error("Version mismatch, hard-reloading"); - location.reload(); + fullReload(); return; } @@ -60,7 +67,7 @@ const ws = initWebSocket({ // but the issue lies in possibly outdated client files. For correctness, // all client files have to be HMR reloaded or proven unchanged. // Configuration changes are already handled by the `config.version` data. - location.reload(); + fullReload(); return; } @@ -120,7 +127,7 @@ const ws = initWebSocket({ } } if (hasFatalError && (isServerSideRouteUpdate || reader.hasMoreData())) { - location.reload(); + fullReload(); return; } if (isServerSideRouteUpdate) { @@ -138,7 +145,7 @@ const ws = initWebSocket({ const modules = (0, eval)(code); replaceModules(modules).catch(e => { console.error(e); - location.reload(); + fullReload(); }); } catch (e) { if (IS_BUN_DEVELOPMENT) { @@ -155,6 +162,11 @@ const ws = initWebSocket({ currentRouteIndex = reader.u32(); }, [MessageId.errors]: onServerErrorPayload, +}; +const ws = initWebSocket(handlers, { + onStatusChange(connected) { + emitEvent(connected ? "bun:ws:connect" : "bun:ws:disconnect", null); + }, }); // Before loading user code, instrument some globals. diff --git a/src/bundler/bundle_v2.zig b/src/bundler/bundle_v2.zig index 00670c657f..5c81129c12 100644 --- a/src/bundler/bundle_v2.zig +++ b/src/bundler/bundle_v2.zig @@ -3062,8 +3062,6 @@ pub const BundleV2 = struct { const transpiler, const bake_graph: bake.Graph, const target = if (import_record.tag == .bake_resolve_to_ssr_graph) brk: { - // TODO: consider moving this error into js_parser so it is caught more reliably - // Then we can assert(this.framework != null) if (this.framework == null) { this.logForResolutionFailures(source.path.text, .ssr).addErrorFmt( source, @@ -3152,8 +3150,49 @@ pub const BundleV2 = struct { source, import_record.range, this.graph.allocator, - "Browser build cannot {s} Node.js builtin: \"{s}\". To use Node.js builtins, set target to 'node' or 'bun'", - .{ import_record.kind.errorLabel(), import_record.path.text }, + "Browser build cannot {s} Node.js builtin: \"{s}\"{s}", + .{ + import_record.kind.errorLabel(), + import_record.path.text, + if (this.transpiler.options.dev_server == null) + ". To use Node.js builtins, set target to 'node' or 'bun'" + else + "", + }, + import_record.kind, + ) catch bun.outOfMemory(); + } else if (!ast.target.isBun() and strings.eqlComptime(import_record.path.text, "bun")) { + addError( + log, + source, + import_record.range, + this.graph.allocator, + "Browser build cannot {s} Bun builtin: \"{s}\"{s}", + .{ + import_record.kind.errorLabel(), + import_record.path.text, + if (this.transpiler.options.dev_server == null) + ". When bundling for Bun, set target to 'bun'" + else + "", + }, + import_record.kind, + ) catch bun.outOfMemory(); + } else if (!ast.target.isBun() and strings.hasPrefixComptime(import_record.path.text, "bun:")) { + addError( + log, + source, + import_record.range, + this.graph.allocator, + "Browser build cannot {s} Bun builtin: \"{s}\"{s}", + .{ + import_record.kind.errorLabel(), + import_record.path.text, + if (this.transpiler.options.dev_server == null) + ". When bundling for Bun, set target to 'bun'" + else + "", + }, import_record.kind, ) catch bun.outOfMemory(); } else { @@ -3209,7 +3248,23 @@ pub const BundleV2 = struct { } if (this.transpiler.options.dev_server) |dev_server| brk: { - if (loader.isCSS()) { + if (path.loader(&this.transpiler.options.loaders) == .html) { + // This use case is currently not supported. This error + // blocks an assertion failure because the DevServer + // reserves the HTML file's spot in IncrementalGraph for the + // route definition. + const log = this.logForResolutionFailures(source.path.text, bake_graph); + log.addRangeErrorFmt( + source, + import_record.range, + this.graph.allocator, + "Browser builds cannot import HTML files.", + .{}, + ) catch bun.outOfMemory(); + continue; + } + + if (loader == .css) { // Do not use cached files for CSS. break :brk; } @@ -5091,7 +5146,7 @@ pub const ParseTask = struct { // Entrypoints will have `import.meta.main` set as "unknown", unless we use `--compile`, // in which we inline `true`. if (transpiler.options.inline_entrypoint_import_meta_main or !task.is_entry_point) { - opts.import_meta_main_value = task.is_entry_point; + opts.import_meta_main_value = task.is_entry_point and transpiler.options.dev_server == null; } else if (target == .node) { opts.lower_import_meta_main_for_node_js = true; } @@ -10288,7 +10343,6 @@ pub const LinkerContext = struct { } }, else => {}, - // else => bun.unreachablePanic("Unexpected output format", .{}), } } } @@ -12592,6 +12646,7 @@ pub const LinkerContext = struct { S.ExportClause, .{ .items = items.items, + .is_single_line = false, }, Logger.Loc.Empty, ), @@ -13561,18 +13616,25 @@ pub const LinkerContext = struct { var esm_decls: std.ArrayListUnmanaged(B.Array.Item) = .empty; var esm_callbacks: std.ArrayListUnmanaged(Expr) = .empty; + for (ast.import_records.slice()) |*record| { + if (record.path.is_disabled) continue; + if (record.source_index.isValid() and c.parse_graph.input_files.items(.loader)[record.source_index.get()] == .css) { + record.path.is_disabled = true; + continue; + } + // Make sure the printer gets the resolved path + if (record.source_index.isValid()) { + record.path = c.parse_graph.input_files.items(.source)[record.source_index.get()].path; + } + } + // Modules which do not have side effects for (part_stmts) |stmt| switch (stmt.data) { else => try stmts.inside_wrapper_suffix.append(stmt), .s_import => |st| { const record = ast.import_records.mut(st.import_record_index); - - const is_enabled = !record.path.is_disabled and if (record.source_index.isValid()) - c.parse_graph.input_files.items(.loader)[record.source_index.get()] != .css - else - true; - if (!is_enabled) continue; + if (record.path.is_disabled) continue; const is_builtin = record.tag == .builtin or record.tag == .bun_test or record.tag == .bun or record.tag == .runtime; const is_bare_import = st.star_name_loc == null and st.items.len == 0 and st.default_name == null; @@ -13627,13 +13689,6 @@ pub const LinkerContext = struct { }, .Empty)); } - // Make sure the printer gets the resolved path - const path = if (record.source_index.isValid()) - c.parse_graph.input_files.items(.source)[record.source_index.get()].path - else - record.path; - record.path = path; - try stmts.outside_wrapper_prefix.append(stmt); } }, diff --git a/src/js_ast.zig b/src/js_ast.zig index bec14f46d5..c5a5a6d735 100644 --- a/src/js_ast.zig +++ b/src/js_ast.zig @@ -1608,10 +1608,6 @@ pub const E = struct { hot_data, /// `import.meta.hot.accept` when HMR is enabled. Truthy. hot_accept, - /// `import.meta.hot.accept` when HMR is disabled. Falsy and DCE's when called. - hot_accept_disabled, - /// `import.meta.hot.*` when HMR is disabled. Falsy and DCE's when called. - hot_function_disabled, /// Converted from `hot_accept` to this in js_parser.zig when it is /// passed strings. Printed as `hmr.hot.acceptSpecifiers` hot_accept_visited, @@ -6396,7 +6392,10 @@ pub const S = struct { value: []const u8, }; - pub const ExportClause = struct { items: []ClauseItem, is_single_line: bool = false }; + pub const ExportClause = struct { + items: []ClauseItem, + is_single_line: bool, + }; pub const Empty = struct {}; diff --git a/src/js_parser.zig b/src/js_parser.zig index de2998cfdd..4e6f6a420a 100644 --- a/src/js_parser.zig +++ b/src/js_parser.zig @@ -2403,8 +2403,6 @@ pub const SideEffects = enum(u1) { .hot_enabled, => return .{ .ok = true, .value = true, .side_effects = .no_side_effects }, .hot_disabled, - .hot_accept_disabled, - .hot_function_disabled, => return .{ .ok = true, .value = false, .side_effects = .no_side_effects }, }, else => {}, @@ -9821,7 +9819,10 @@ fn NewParser_( } } p.has_es_module_syntax = true; - return p.s(S.ExportClause{ .items = export_clause.clauses, .is_single_line = export_clause.is_single_line }, loc); + return p.s(S.ExportClause{ + .items = export_clause.clauses, + .is_single_line = export_clause.is_single_line, + }, loc); }, T.t_equals => { // "export = value;" @@ -17310,8 +17311,9 @@ fn NewParser_( p.should_fold_typescript_constant_expressions = true; } + // When a value is targeted by `--drop`, it will be removed. + // The HMR APIs in `import.meta.hot` are implicitly dropped when HMR is disabled. var method_call_should_be_replaced_with_undefined = p.method_call_must_be_replaced_with_undefined; - if (method_call_should_be_replaced_with_undefined) { p.method_call_must_be_replaced_with_undefined = false; switch (e_.target.data) { @@ -17319,22 +17321,16 @@ fn NewParser_( .e_index, .e_dot => { p.is_control_flow_dead = true; }, + // Special case from `import.meta.hot.*` functions. + .e_undefined => { + p.is_control_flow_dead = true; + }, else => { method_call_should_be_replaced_with_undefined = false; }, } } - if (e_.target.data.as(.e_special)) |special| { - switch (special) { - .hot_accept_disabled, .hot_function_disabled => { - method_call_should_be_replaced_with_undefined = true; - p.is_control_flow_dead = true; - }, - else => {}, - } - } - for (e_.args.slice()) |*arg| { arg.* = p.visitExpr(arg.*); } @@ -17426,11 +17422,6 @@ fn NewParser_( if (!p.options.features.hot_module_reloading) return .{ .data = .e_undefined, .loc = expr.loc }; }, - // These APIs do not have any special parser transforms. - .hot_function_disabled, .hot_accept_disabled => { - bun.debugAssert(false); - return .{ .data = .e_undefined, .loc = expr.loc }; - }, else => {}, }; @@ -18879,8 +18870,12 @@ fn NewParser_( Expr.init(E.Object, .{}, loc); } if (bun.strings.eqlComptime(name, "accept")) { + if (!enabled) { + p.method_call_must_be_replaced_with_undefined = true; + return .{ .data = .e_undefined, .loc = loc }; + } return .{ .data = .{ - .e_special = if (enabled) .hot_accept else .hot_accept_disabled, + .e_special = .hot_accept, }, .loc = loc }; } const lookup_table = comptime bun.ComptimeStringMap(void, [_]struct { [:0]const u8, void }{ @@ -18900,7 +18895,8 @@ fn NewParser_( .name_loc = name_loc, }, loc); } else { - return .{ .data = .{ .e_special = .hot_function_disabled }, .loc = loc }; + p.method_call_must_be_replaced_with_undefined = true; + return .{ .data = .e_undefined, .loc = loc }; } } else { // This error is a bit out of place since the HMR @@ -19321,6 +19317,7 @@ fn NewParser_( }; stmts.appendAssumeCapacity(p.s(S.ExportClause{ .items = items, + .is_single_line = false, }, stmt.loc)); } @@ -23316,6 +23313,7 @@ fn NewParser_( if (exports.items.len > 0) { result.appendAssumeCapacity(p.s(S.ExportClause{ .items = exports.items, + .is_single_line = false, }, loc)); } @@ -24351,10 +24349,9 @@ pub const ConvertESMExportsForHmr = struct { try ctx.visitBindingToExport(p, decl.binding, true); } } else { - // TODO: remove this dupe - var dupe_decls = try std.ArrayListUnmanaged(G.Decl).initCapacity(p.allocator, st.decls.len); - - for (st.decls.slice()) |decl| { + var new_len: usize = 0; + for (st.decls.slice()) |*decl_ptr| { + const decl = decl_ptr.*; // explicit copy to avoid aliasinng bun.assert(decl.value != null); // const must be initialized switch (decl.binding.data) { @@ -24376,24 +24373,24 @@ pub const ConvertESMExportsForHmr = struct { .value = decl.value, }); } else { - dupe_decls.appendAssumeCapacity(decl); + st.decls.mut(new_len).* = decl; + new_len += 1; try ctx.visitBindingToExport(p, decl.binding, false); } }, else => { ctx.can_implicitly_accept = false; - dupe_decls.appendAssumeCapacity(decl); + st.decls.mut(new_len).* = decl; + new_len += 1; try ctx.visitBindingToExport(p, decl.binding, false); }, } } - - if (dupe_decls.items.len == 0) { + if (new_len == 0) { return; } - - st.decls = G.Decl.List.fromList(dupe_decls); + st.decls.len = @intCast(new_len); } break :stmt stmt; @@ -24526,6 +24523,14 @@ pub const ConvertESMExportsForHmr = struct { for (st.items) |item| { const ref = item.name.ref.?; try ctx.visitRefToExport(p, ref, item.alias, item.name.loc, false); + + if (ctx.can_implicitly_accept) { + const symbol: *Symbol = &p.symbols.items[ref.inner_index]; + switch (symbol.kind) { + .hoisted_function, .generator_or_async_function => {}, + else => ctx.can_implicitly_accept = false, + } + } } return; // do not emit a statement here @@ -24592,7 +24597,6 @@ pub const ConvertESMExportsForHmr = struct { }); } else { // 'export * from' creates a spread, hoisted at the top. - // TODO: for perfect HMR, this likely needs more instrumentation try ctx.export_star_props.append(p.allocator, .{ .kind = .spread, .value = Expr.initIdentifier(namespace_ref, stmt.loc), diff --git a/src/js_printer.zig b/src/js_printer.zig index 39a0002b08..4e5fcbd960 100644 --- a/src/js_printer.zig +++ b/src/js_printer.zig @@ -2108,7 +2108,7 @@ fn NewPrinter( } p.print("import.meta.main"); } else { - bun.assert(p.options.module_type != .internal_bake_dev); + bun.debugAssert(p.options.module_type != .internal_bake_dev); p.printSpaceBeforeIdentifier(); p.addSourceMapping(expr.loc); @@ -2155,34 +2155,31 @@ fn NewPrinter( } }, .hot_enabled => { - bun.assert(p.options.module_type == .internal_bake_dev); + bun.debugAssert(p.options.module_type == .internal_bake_dev); p.printSymbol(p.options.hmr_ref); p.print(".indirectHot"); }, .hot_data => { - bun.assert(p.options.module_type == .internal_bake_dev); + bun.debugAssert(p.options.module_type == .internal_bake_dev); p.printSymbol(p.options.hmr_ref); p.print(".data"); }, .hot_accept => { - bun.assert(p.options.module_type == .internal_bake_dev); + bun.debugAssert(p.options.module_type == .internal_bake_dev); p.printSymbol(p.options.hmr_ref); p.print(".accept"); }, .hot_accept_visited => { - bun.assert(p.options.module_type == .internal_bake_dev); + bun.debugAssert(p.options.module_type == .internal_bake_dev); p.printSymbol(p.options.hmr_ref); p.print(".acceptSpecifiers"); }, - .hot_function_disabled, - .hot_accept_disabled, - .hot_disabled, - => { - bun.assert(p.options.module_type != .internal_bake_dev); + .hot_disabled => { + bun.debugAssert(p.options.module_type != .internal_bake_dev); p.printExpr(.{ .data = .e_undefined, .loc = expr.loc }, level, in_flags); }, .resolved_specifier_string => |index| { - bun.assert(p.options.module_type == .internal_bake_dev); + bun.debugAssert(p.options.module_type == .internal_bake_dev); p.printStringLiteralUTF8(p.importRecord(index.get()).path.pretty, true); }, }, @@ -5368,16 +5365,19 @@ fn NewPrinter( p.print("], ["); // Print export stars - if (ast.export_star_import_records.len > 0) { - p.indent(); - for (ast.export_star_import_records) |star| { - p.printNewline(); - p.printIndent(); - const record = p.importRecord(star); - p.printStringLiteralUTF8(record.path.pretty, false); - p.print(","); - } - p.unindent(); + p.indent(); + var had_any_stars = false; + for (ast.export_star_import_records) |star| { + const record = p.importRecord(star); + if (record.path.is_disabled) continue; + had_any_stars = true; + p.printNewline(); + p.printIndent(); + p.printStringLiteralUTF8(record.path.pretty, false); + p.print(","); + } + p.unindent(); + if (had_any_stars) { p.printNewline(); p.printIndent(); } diff --git a/src/transpiler.zig b/src/transpiler.zig index e09efe61c1..506b87aa3d 100644 --- a/src/transpiler.zig +++ b/src/transpiler.zig @@ -1294,6 +1294,7 @@ pub const Transpiler = struct { js_ast.S.ExportClause, js_ast.S.ExportClause{ .items = export_clauses[0..count], + .is_single_line = false, }, logger.Loc{ .start = 0, diff --git a/test/bake/dev/bundle.test.ts b/test/bake/dev/bundle.test.ts index 8cef49b809..d7ac4e0d87 100644 --- a/test/bake/dev/bundle.test.ts +++ b/test/bake/dev/bundle.test.ts @@ -267,3 +267,62 @@ devTest("deleting imported file shows error then recovers", { }); }, }); +devTest("importing html file", { + files: { + "index.html": emptyHtmlFile({ + styles: [], + scripts: ["index.ts"], + }), + "index.ts": ` + import html from "./index.html"; + console.log(html); + `, + }, + async test(dev) { + await using c = await dev.client("/", { + errors: ["index.ts:1:18: error: Browser builds cannot import HTML files."], + }); + }, +}); +devTest("importing bun on the client", { + files: { + "index.html": emptyHtmlFile({ + styles: [], + scripts: ["index.ts"], + }), + "index.ts": ` + import bun from "bun"; + console.log(bun); + `, + }, + async test(dev) { + await using c = await dev.client("/", { + errors: ['index.ts:1:17: error: Browser build cannot import Bun builtin: "bun"'], + }); + }, +}); +devTest("import.meta.main", { + files: { + "index.html": emptyHtmlFile({ + styles: [], + scripts: ["index.ts"], + }), + "index.ts": ` + console.log(import.meta.main); + import.meta.hot.accept(); + `, + }, + async test(dev) { + await using c = await dev.client("/"); + await c.expectMessage(false); // import.meta.main is always false because there is no single entry point + + await dev.write( + "index.ts", + ` + require; + console.log(import.meta.main); + `, + ); + await c.expectMessage(false); + }, +}); diff --git a/test/bundler/bundler_browser.test.ts b/test/bundler/bundler_browser.test.ts index d437447d7f..634db16923 100644 --- a/test/bundler/bundler_browser.test.ts +++ b/test/bundler/bundler_browser.test.ts @@ -306,7 +306,7 @@ describe("bundler", () => { bundleErrors: { "/entry.js": Object.keys(bunModules) .filter(x => bunModules[x] === "error") - .map(x => `Could not resolve: "${x}". Maybe you need to "bun install"?`), + .map(x => `Browser build cannot import Bun builtin: "${x}". When bundling for Bun, set target to 'bun'`), }, });