diff --git a/bun.lock b/bun.lock index f4e82bb81b..345e3a7b9b 100644 --- a/bun.lock +++ b/bun.lock @@ -4,7 +4,6 @@ "": { "name": "bun", "devDependencies": { - "@types/react": "^18.3.3", "esbuild": "^0.21.4", "mitata": "^0.1.11", "peechy": "0.4.34", @@ -29,13 +28,17 @@ "@types/node": "*", }, "devDependencies": { + "@types/react": "^19", "typescript": "^5.0.2", }, + "peerDependencies": { + "@types/react": "^19", + }, }, }, "overrides": { - "bun-types": "workspace:packages/bun-types", "@types/bun": "workspace:packages/@types/bun", + "bun-types": "workspace:packages/bun-types", }, "packages": { "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.21.5", "", { "os": "aix", "cpu": "ppc64" }, "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ=="], @@ -88,9 +91,7 @@ "@types/node": ["@types/node@22.15.18", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-v1DKRfUdyW+jJhZNEI1PYy29S2YRxMV5AOO/x/SjKmW0acCIOqmbj6Haf9eHAhsPmrhlHSxEhv/1WszcLWV4cg=="], - "@types/prop-types": ["@types/prop-types@15.7.14", "", {}, "sha512-gNMvNH49DJ7OJYv+KAKn0Xp45p8PLl6zo2YnvDIbTd4J6MER2BmWN49TG7n9LvkyihINxeKW8+3bfS2yDC9dzQ=="], - - "@types/react": ["@types/react@18.3.21", "", { "dependencies": { "@types/prop-types": "*", "csstype": "^3.0.2" } }, "sha512-gXLBtmlcRJeT09/sI4PxVwyrku6SaNUj/6cMubjE6T6XdY1fDmBL7r0nX0jbSZPU/Xr0KuwLLZh6aOYY5d91Xw=="], + "@types/react": ["@types/react@19.1.8", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-AwAfQ2Wa5bCx9WP8nZL2uMZWod7J7/JSplxbTmBQ5ms6QpqNYm672H0Vu9ZVKVngQ+ii4R/byguVEUZQyeg44g=="], "bun-types": ["bun-types@workspace:packages/bun-types"], diff --git a/package.json b/package.json index ec82d74cf0..7fb823515f 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,6 @@ "./packages/@types/bun" ], "devDependencies": { - "@types/react": "^18.3.3", "esbuild": "^0.21.4", "mitata": "^0.1.11", "peechy": "0.4.34", diff --git a/packages/bun-types/bun.d.ts b/packages/bun-types/bun.d.ts index 62c2948436..869cc59e50 100644 --- a/packages/bun-types/bun.d.ts +++ b/packages/bun-types/bun.d.ts @@ -45,6 +45,7 @@ declare module "bun" { type DOMHighResTimeStamp = number; type EventListenerOrEventListenerObject = EventListener | EventListenerObject; type BlobOrStringOrBuffer = string | NodeJS.TypedArray | ArrayBufferLike | Blob; + type MaybePromise = T | Promise; namespace __internal { type LibDomIsLoaded = typeof globalThis extends { onabort: any } ? true : false; diff --git a/packages/bun-types/experimental.d.ts b/packages/bun-types/experimental.d.ts new file mode 100644 index 0000000000..093f00e622 --- /dev/null +++ b/packages/bun-types/experimental.d.ts @@ -0,0 +1,276 @@ +declare module "bun" { + export namespace __experimental { + /** + * Base interface for static site generation route parameters. + * + * Supports both single string values and arrays of strings for dynamic route segments. + * This is typically used for route parameters like `[slug]`, `[...rest]`, or `[id]`. + * + * @warning These APIs are experimental and might be moved/changed in future releases. + * + * @example + * ```tsx + * // Simple slug parameter + * type BlogParams = { slug: string }; + * + * // Multiple parameters + * type ProductParams = { + * category: string; + * id: string; + * }; + * + * // Catch-all routes with string arrays + * type DocsParams = { + * path: string[]; + * }; + * ``` + */ + export interface SSGParamsLike { + [key: string]: string | string[]; + } + + /** + * Configuration object for a single static route to be generated. + * + * Each path object contains the parameters needed to render a specific + * instance of a dynamic route at build time. + * + * @warning These APIs are experimental and might be moved/changed in future releases. + * + * @template Params - The shape of route parameters for this path + * + * @example + * ```tsx + * // Single blog post path + * const blogPath: SSGPath<{ slug: string }> = { + * params: { slug: "my-first-post" } + * }; + * + * // Product page with multiple params + * const productPath: SSGPath<{ category: string; id: string }> = { + * params: { + * category: "electronics", + * id: "laptop-123" + * } + * }; + * + * // Documentation with catch-all route + * const docsPath: SSGPath<{ path: string[] }> = { + * params: { path: ["getting-started", "installation"] } + * }; + * ``` + */ + export interface SSGPath { + params: Params; + } + + /** + * Array of static paths to be generated at build time. + * + * This type represents the collection of all route configurations + * that should be pre-rendered for a dynamic route. + * + * @warning These APIs are experimental and might be moved/changed in future releases. + * + * @template Params - The shape of route parameters for these paths + * + * @example + * ```tsx + * // Array of blog post paths + * const blogPaths: SSGPaths<{ slug: string }> = [ + * { params: { slug: "introduction-to-bun" } }, + * { params: { slug: "performance-benchmarks" } }, + * { params: { slug: "getting-started-guide" } } + * ]; + * + * // Mixed parameter types + * const productPaths: SSGPaths<{ category: string; id: string }> = [ + * { params: { category: "books", id: "javascript-guide" } }, + * { params: { category: "electronics", id: "smartphone-x" } } + * ]; + * ``` + */ + export type SSGPaths = SSGPath[]; + + /** + * Props interface for SSG page components. + * + * This interface defines the shape of props that will be passed to your + * static page components during the build process. The `params` object + * contains the route parameters extracted from the URL pattern. + * + * @warning These APIs are experimental and might be moved/changed in future releases. + * + * @template Params - The shape of route parameters for this page + * + * @example + * ```tsx + * // Blog post component props + * interface BlogPageProps extends SSGPageProps<{ slug: string }> { + * // params: { slug: string } is automatically included + * } + * + * // Product page component props + * interface ProductPageProps extends SSGPageProps<{ + * category: string; + * id: string; + * }> { + * // params: { category: string; id: string } is automatically included + * } + * + * // Usage in component + * function BlogPost({ params }: BlogPageProps) { + * const { slug } = params; // TypeScript knows slug is a string + * return

Blog post: {slug}

; + * } + * ``` + */ + export interface SSGPageProps { + params: Params; + } + + /** + * React component type for SSG pages that can be statically generated. + * + * This type represents a React component that receives SSG page props + * and can be rendered at build time. The component can be either a regular + * React component or an async React Server Component for advanced use cases + * like data fetching during static generation. + * + * @warning These APIs are experimental and might be moved/changed in future releases. + * + * @template Params - The shape of route parameters for this page component + * + * @example + * ```tsx + * // Regular synchronous SSG page component + * const BlogPost: SSGPage<{ slug: string }> = ({ params }) => { + * return ( + *
+ *

Blog Post: {params.slug}

+ *

This content was generated at build time!

+ *
+ * ); + * }; + * + * // Async React Server Component for data fetching + * const AsyncBlogPost: SSGPage<{ slug: string }> = async ({ params }) => { + * // Fetch data during static generation + * const post = await fetchBlogPost(params.slug); + * const author = await fetchAuthor(post.authorId); + * + * return ( + *
+ *

{post.title}

+ *

By {author.name}

+ *
+ *
+ * ); + * }; + * + * // Product page with multiple params and async data fetching + * const ProductPage: SSGPage<{ category: string; id: string }> = async ({ params }) => { + * const [product, reviews] = await Promise.all([ + * fetchProduct(params.category, params.id), + * fetchProductReviews(params.id) + * ]); + * + * return ( + *
+ *

{product.name}

+ *

Category: {params.category}

+ *

Price: ${product.price}

+ *
+ *

Reviews ({reviews.length})

+ * {reviews.map(review => ( + *
{review.comment}
+ * ))} + *
+ *
+ * ); + * }; + * ``` + */ + export type SSGPage = React.ComponentType>; + + /** + * getStaticPaths is Bun's implementation of SSG (Static Site Generation) path determination. + * + * This function is called at your app's build time to determine which + * dynamic routes should be pre-rendered as static pages. It returns an + * array of path parameters that will be used to generate static pages for + * dynamic routes (e.g., [slug].tsx, [category]/[id].tsx). + * + * The function can be either synchronous or asynchronous, allowing you to + * fetch data from APIs, databases, or file systems to determine which paths + * should be statically generated. + * + * @warning These APIs are experimental and might be moved/changed in future releases. + * + * @template Params - The shape of route parameters for the dynamic route + * + * @returns An object containing an array of paths to be statically generated + * + * @example + * ```tsx + * // In pages/blog/[slug].tsx ———————————————————╮ + * export const getStaticPaths: GetStaticPaths<{ slug: string }> = async () => { + * // Fetch all blog posts from your CMS or API at build time + * const posts = await fetchBlogPosts(); + * + * return { + * paths: posts.map((post) => ({ + * params: { slug: post.slug } + * })) + * }; + * }; + * + * // In pages/products/[category]/[id].tsx + * export const getStaticPaths: GetStaticPaths<{ + * category: string; + * id: string; + * }> = async () => { + * // Fetch products from database + * const products = await db.products.findMany({ + * select: { id: true, category: { slug: true } } + * }); + * + * return { + * paths: products.map(product => ({ + * params: { + * category: product.category.slug, + * id: product.id + * } + * })) + * }; + * }; + * + * // In pages/docs/[...path].tsx (catch-all route) + * export const getStaticPaths: GetStaticPaths<{ path: string[] }> = async () => { + * // Read documentation structure from file system + * const docPaths = await getDocumentationPaths('./content/docs'); + * + * return { + * paths: docPaths.map(docPath => ({ + * params: { path: docPath.split('/') } + * })) + * }; + * }; + * + * // Synchronous example with static data + * export const getStaticPaths: GetStaticPaths<{ id: string }> = () => { + * const staticIds = ['1', '2', '3', '4', '5']; + * + * return { + * paths: staticIds.map(id => ({ + * params: { id } + * })) + * }; + * }; + * ``` + */ + export type GetStaticPaths = () => MaybePromise<{ + paths: SSGPaths; + }>; + } +} diff --git a/packages/bun-types/index.d.ts b/packages/bun-types/index.d.ts index 3eed5f1b6c..c5b488ba22 100644 --- a/packages/bun-types/index.d.ts +++ b/packages/bun-types/index.d.ts @@ -20,6 +20,7 @@ /// /// /// +/// /// diff --git a/packages/bun-types/package.json b/packages/bun-types/package.json index d9bc95f0a6..24ce468bca 100644 --- a/packages/bun-types/package.json +++ b/packages/bun-types/package.json @@ -19,7 +19,11 @@ "dependencies": { "@types/node": "*" }, + "peerDependencies": { + "@types/react": "^19" + }, "devDependencies": { + "@types/react": "^19", "typescript": "^5.0.2" }, "scripts": { diff --git a/src/StandaloneModuleGraph.zig b/src/StandaloneModuleGraph.zig index eee598ea68..478a6b563e 100644 --- a/src/StandaloneModuleGraph.zig +++ b/src/StandaloneModuleGraph.zig @@ -262,7 +262,7 @@ pub const StandaloneModuleGraph = struct { }); stored.external_source_names = file_names; - stored.underlying_provider = .{ .data = @truncate(@intFromPtr(data)), .load_hint = .none }; + stored.underlying_provider = .{ .data = @truncate(@intFromPtr(data)), .load_hint = .none, .kind = .zig }; stored.is_standalone_module_graph = true; const parsed = bun.new(SourceMap.ParsedSourceMap, stored); diff --git a/src/allocators/AllocationScope.zig b/src/allocators/AllocationScope.zig index f3ab31f5b9..bce56a36bf 100644 --- a/src/allocators/AllocationScope.zig +++ b/src/allocators/AllocationScope.zig @@ -36,7 +36,7 @@ pub const Extra = union(enum) { }; pub fn init(parent: Allocator) AllocationScope { - return if (enabled) + return if (comptime enabled) .{ .parent = parent, .state = .{ @@ -52,7 +52,7 @@ pub fn init(parent: Allocator) AllocationScope { } pub fn deinit(scope: *AllocationScope) void { - if (enabled) { + if (comptime enabled) { scope.state.mutex.lock(); defer scope.state.allocations.deinit(scope.parent); const count = scope.state.allocations.count(); @@ -83,7 +83,7 @@ pub fn deinit(scope: *AllocationScope) void { } pub fn allocator(scope: *AllocationScope) Allocator { - return if (enabled) .{ .ptr = scope, .vtable = &vtable } else scope.parent; + return if (comptime enabled) .{ .ptr = scope, .vtable = &vtable } else scope.parent; } const vtable: Allocator.VTable = .{ @@ -176,7 +176,7 @@ fn trackFreeAssumeLocked(scope: *AllocationScope, buf: []const u8, ret_addr: usi } pub fn assertOwned(scope: *AllocationScope, ptr: anytype) void { - if (!enabled) return; + if (comptime !enabled) return; const cast_ptr: [*]const u8 = @ptrCast(switch (@typeInfo(@TypeOf(ptr)).pointer.size) { .c, .one, .many => ptr, .slice => if (ptr.len > 0) ptr.ptr else return, @@ -188,7 +188,7 @@ pub fn assertOwned(scope: *AllocationScope, ptr: anytype) void { } pub fn assertUnowned(scope: *AllocationScope, ptr: anytype) void { - if (!enabled) return; + if (comptime !enabled) return; const cast_ptr: [*]const u8 = @ptrCast(switch (@typeInfo(@TypeOf(ptr)).pointer.size) { .c, .one, .many => ptr, .slice => if (ptr.len > 0) ptr.ptr else return, @@ -196,7 +196,7 @@ pub fn assertUnowned(scope: *AllocationScope, ptr: anytype) void { scope.state.mutex.lock(); defer scope.state.mutex.unlock(); if (scope.state.allocations.getPtr(cast_ptr)) |owned| { - Output.warn("Pointer allocated here:"); + Output.warn("Owned pointer allocated here:"); bun.crash_handler.dumpStackTrace(owned.allocated_at.trace(), trace_limits, trace_limits); } @panic("this pointer was owned by the allocation scope when it was not supposed to be"); @@ -205,7 +205,7 @@ pub fn assertUnowned(scope: *AllocationScope, ptr: anytype) void { /// Track an arbitrary pointer. Extra data can be stored in the allocation, /// which will be printed when a leak is detected. pub fn trackExternalAllocation(scope: *AllocationScope, ptr: []const u8, ret_addr: ?usize, extra: Extra) void { - if (!enabled) return; + if (comptime !enabled) return; scope.state.mutex.lock(); defer scope.state.mutex.unlock(); scope.state.allocations.ensureUnusedCapacity(scope.parent, 1) catch bun.outOfMemory(); @@ -215,7 +215,7 @@ pub fn trackExternalAllocation(scope: *AllocationScope, ptr: []const u8, ret_add /// Call when the pointer from `trackExternalAllocation` is freed. /// Returns true if the free was invalid. pub fn trackExternalFree(scope: *AllocationScope, slice: anytype, ret_addr: ?usize) bool { - if (!enabled) return; + if (comptime !enabled) return; const ptr: []const u8 = switch (@typeInfo(@TypeOf(slice))) { .pointer => |p| switch (p.size) { .slice => brk: { @@ -236,7 +236,7 @@ pub fn trackExternalFree(scope: *AllocationScope, slice: anytype, ret_addr: ?usi } pub fn setPointerExtra(scope: *AllocationScope, ptr: *anyopaque, extra: Extra) void { - if (!enabled) return; + if (comptime !enabled) return; scope.state.mutex.lock(); defer scope.state.mutex.unlock(); const allocation = scope.state.allocations.getPtr(ptr) orelse diff --git a/src/bake/BakeGlobalObject.cpp b/src/bake/BakeGlobalObject.cpp index 33f44c737b..f7ce8b326e 100644 --- a/src/bake/BakeGlobalObject.cpp +++ b/src/bake/BakeGlobalObject.cpp @@ -46,6 +46,7 @@ bakeModuleLoaderImportModule(JSC::JSGlobalObject* global, JSC::jsUndefined(), parameters, JSC::jsUndefined()); } + // TODO: make static cast instead of jscast // Use Zig::GlobalObject's function return jsCast(global)->moduleLoaderImportModule(global, moduleLoader, moduleNameValue, parameters, sourceOrigin); } @@ -72,6 +73,18 @@ JSC::Identifier bakeModuleLoaderResolve(JSC::JSGlobalObject* jsGlobal, } } + if (auto string = jsDynamicCast(key)) { + auto keyView = string->getString(global); + RETURN_IF_EXCEPTION(scope, vm.propertyNames->emptyIdentifier); + + if (keyView.startsWith("bake:/"_s)) { + BunString result = BakeProdResolve(global, Bun::toString("bake:/"_s), Bun::toString(keyView.substringSharingImpl("bake:"_s.length()))); + RETURN_IF_EXCEPTION(scope, vm.propertyNames->emptyIdentifier); + + return JSC::Identifier::fromString(vm, result.transferToWTFString()); + } + } + // Use Zig::GlobalObject's function return Zig::GlobalObject::moduleLoaderResolve(jsGlobal, loader, key, referrer, origin); } @@ -113,6 +126,7 @@ JSC::JSInternalPromise* bakeModuleLoaderFetch(JSC::JSGlobalObject* globalObject, if (source.tag != BunStringTag::Dead) { JSC::SourceOrigin origin = JSC::SourceOrigin(WTF::URL(moduleKey)); JSC::SourceCode sourceCode = JSC::SourceCode(Bake::SourceProvider::create( + globalObject, source.toWTFString(), origin, WTFMove(moduleKey), diff --git a/src/bake/BakeSourceProvider.cpp b/src/bake/BakeSourceProvider.cpp index 2722c22452..15d17d28ba 100644 --- a/src/bake/BakeSourceProvider.cpp +++ b/src/bake/BakeSourceProvider.cpp @@ -16,6 +16,12 @@ namespace Bake { + +extern "C" BunString BakeSourceProvider__getSourceSlice(SourceProvider* provider) +{ + return Bun::toStringView(provider->source()); +} + extern "C" JSC::EncodedJSValue BakeLoadInitialServerCode(GlobalObject* global, BunString source, bool separateSSRGraph) { auto& vm = JSC::getVM(global); auto scope = DECLARE_THROW_SCOPE(vm); @@ -23,6 +29,7 @@ extern "C" JSC::EncodedJSValue BakeLoadInitialServerCode(GlobalObject* global, B String string = "bake://server-runtime.js"_s; JSC::SourceOrigin origin = JSC::SourceOrigin(WTF::URL(string)); JSC::SourceCode sourceCode = JSC::SourceCode(SourceProvider::create( + global, source.toWTFString(), origin, WTFMove(string), @@ -56,6 +63,7 @@ extern "C" JSC::EncodedJSValue BakeLoadServerHmrPatch(GlobalObject* global, BunS String string = "bake://server.patch.js"_s; JSC::SourceOrigin origin = JSC::SourceOrigin(WTF::URL(string)); JSC::SourceCode sourceCode = JSC::SourceCode(SourceProvider::create( + global, source.toWTFString(), origin, WTFMove(string), @@ -119,6 +127,7 @@ extern "C" JSC::EncodedJSValue BakeRegisterProductionChunk(JSC::JSGlobalObject* JSC::JSString* key = JSC::jsString(vm, string); JSC::SourceOrigin origin = JSC::SourceOrigin(WTF::URL(string)); JSC::SourceCode sourceCode = JSC::SourceCode(SourceProvider::create( + global, source.toWTFString(), origin, WTFMove(string), diff --git a/src/bake/BakeSourceProvider.h b/src/bake/BakeSourceProvider.h index 3a3706af85..7055fcedbe 100644 --- a/src/bake/BakeSourceProvider.h +++ b/src/bake/BakeSourceProvider.h @@ -6,33 +6,43 @@ namespace Bake { +class SourceProvider; + +extern "C" void Bun__addBakeSourceProviderSourceMap(void* bun_vm, SourceProvider* opaque_source_provider, BunString* specifier); + class SourceProvider final : public JSC::StringSourceProvider { public: static Ref create( - const String& source, - const JSC::SourceOrigin& sourceOrigin, - String&& sourceURL, - const TextPosition& startPosition, - JSC::SourceProviderSourceType sourceType - ) { - return adoptRef(*new SourceProvider(source, sourceOrigin, WTFMove(sourceURL), startPosition, sourceType)); + JSC::JSGlobalObject* globalObject, + const String& source, + const JSC::SourceOrigin& sourceOrigin, + String&& sourceURL, + const TextPosition& startPosition, + JSC::SourceProviderSourceType sourceType) + { + auto provider = adoptRef(*new SourceProvider(source, sourceOrigin, WTFMove(sourceURL), startPosition, sourceType)); + auto* zigGlobalObject = jsCast(globalObject); + auto specifier = Bun::toString(provider->sourceURL()); + Bun__addBakeSourceProviderSourceMap(zigGlobalObject->bunVM(), provider.ptr(), &specifier); + return provider; } private: - SourceProvider( - const String& source, - const JSC::SourceOrigin& sourceOrigin, - String&& sourceURL, - const TextPosition& startPosition, - JSC::SourceProviderSourceType sourceType - ) : StringSourceProvider( - source, - sourceOrigin, - JSC::SourceTaintedOrigin::Untainted, - WTFMove(sourceURL), - startPosition, - sourceType - ) {} + SourceProvider( + const String& source, + const JSC::SourceOrigin& sourceOrigin, + String&& sourceURL, + const TextPosition& startPosition, + JSC::SourceProviderSourceType sourceType) + : StringSourceProvider( + source, + sourceOrigin, + JSC::SourceTaintedOrigin::Untainted, + WTFMove(sourceURL), + startPosition, + sourceType) + { + } }; } // namespace Bake diff --git a/src/bake/DevServer.zig b/src/bake/DevServer.zig index 78bedb78ba..2c855dc6ea 100644 --- a/src/bake/DevServer.zig +++ b/src/bake/DevServer.zig @@ -1531,8 +1531,61 @@ fn onFrameworkRequestWithBundle( ) bun.JSError!void { const route_bundle = dev.routeBundlePtr(route_bundle_index); assert(route_bundle.data == .framework); + const bundle = &route_bundle.data.framework; + // Extract route params by re-matching the URL + var params: FrameworkRouter.MatchedParams = undefined; + const url_bunstr = switch (req) { + .stack => |r| bun.String{ + .tag = .ZigString, + .value = .{ .ZigString = bun.ZigString.fromUTF8(r.url()) }, + }, + .saved => |data| brk: { + const url = data.request.url; + url.ref(); + break :brk url; + }, + }; + defer url_bunstr.deref(); + const url = url_bunstr.toUTF8(bun.default_allocator); + defer url.deinit(); + + // Extract pathname from URL (remove protocol, host, query, hash) + const pathname = if (std.mem.indexOf(u8, url.byteSlice(), "://")) |proto_end| blk: { + const after_proto = url.byteSlice()[proto_end + 3 ..]; + if (std.mem.indexOfScalar(u8, after_proto, '/')) |path_start| { + const path_with_query = after_proto[path_start..]; + // Remove query string and hash + const end = bun.strings.indexOfAny(path_with_query, "?#") orelse path_with_query.len; + break :blk path_with_query[0..end]; + } + break :blk "/"; + } else url.byteSlice(); + + // Create params JSValue + // TODO: lazy structure caching since we are making these objects a lot + const params_js_value = if (dev.router.matchSlow(pathname, ¶ms)) |_| blk: { + const global = dev.vm.global; + const params_array = params.params.slice(); + + if (params_array.len == 0) { + break :blk JSValue.null; + } + + // Create a JavaScript object with params + const obj = JSValue.createEmptyObject(global, params_array.len); + for (params_array) |param| { + const key_str = bun.String.createUTF8(param.key); + defer key_str.deref(); + const value_str = bun.String.createUTF8(param.value); + defer value_str.deref(); + + obj.put(global, key_str, value_str.toJS(global)); + } + break :blk obj; + } else JSValue.null; + const server_request_callback = dev.server_fetch_function_callback.get() orelse unreachable; // did not initialize server code @@ -1542,7 +1595,7 @@ fn onFrameworkRequestWithBundle( req, resp, server_request_callback, - 4, + 5, .{ // routerTypeMain router_type.server_file_string.get() orelse str: { @@ -1599,6 +1652,8 @@ fn onFrameworkRequestWithBundle( bundle.cached_css_file_array = .create(js, dev.vm.global); break :arr js; }, + // params + params_js_value, }, ); } @@ -3083,12 +3138,8 @@ fn onRequest(dev: *DevServer, req: *Request, resp: anytype) void { return; } - if (DevServer.AnyResponse != @typeInfo(@TypeOf(resp)).pointer.child) { - unreachable; // mismatch between `is_ssl` with server and response types. optimize these checks out. - } - - if (dev.server.?.config.onRequest != .zero) { - dev.server.?.onRequest(req, resp); + if (dev.server.?.config().onRequest != .zero) { + dev.server.?.onRequest(req, AnyResponse.init(resp)); return; } diff --git a/src/bake/FrameworkRouter.zig b/src/bake/FrameworkRouter.zig index 4ff15e5f33..e175381ecd 100644 --- a/src/bake/FrameworkRouter.zig +++ b/src/bake/FrameworkRouter.zig @@ -31,6 +31,25 @@ static_routes: StaticRouteMap, // TODO: no code to sort this data structure dynamic_routes: DynamicRouteMap, +/// Arena allocator for pattern strings. +/// +/// This should be passed into `EncodedPattern.initFromParts` or should be the +/// allocator used to allocate `StaticRoute.route_path`. +/// +/// Q: Why use this and not just free the strings for `EncodedPattern` and +/// `StaticRoute` manually? +/// +/// A: Inside `fr.insert(...)` we iterate over `EncodedPattern/StaticRoute`, +/// turning them into a bunch of `Route.Part`s, and we discard the original +/// `EncodePattern/StaticRoute` structure. +/// +/// In this process it's too easy to lose the original base pointer and +/// length of the entire allocation. So we'll just allocate everything in +/// this arena to ensure that everything gets freed. +/// +/// Thank you to `AllocationScope` for catching this! Hell yeah! +pattern_string_arena: bun.ArenaAllocator, + /// The above structure is optimized for incremental updates, but /// production has a different set of requirements: /// - Trivially serializable to a binary file (no pointers) @@ -128,6 +147,7 @@ pub fn initEmpty(root: []const u8, types: []Type, allocator: Allocator) !Framewo .routes = routes, .dynamic_routes = .{}, .static_routes = .{}, + .pattern_string_arena = bun.ArenaAllocator.init(allocator), }; } @@ -136,6 +156,7 @@ pub fn deinit(fr: *FrameworkRouter, allocator: Allocator) void { fr.static_routes.deinit(allocator); fr.dynamic_routes.deinit(allocator); allocator.free(fr.types); + fr.pattern_string_arena.deinit(); } pub fn memoryCost(fr: *FrameworkRouter) usize { @@ -1029,9 +1050,9 @@ fn scanInner( const result = switch (param_count > 0) { inline else => |has_dynamic_comptime| result: { const pattern = if (has_dynamic_comptime) - try EncodedPattern.initFromParts(parsed.parts, alloc) + try EncodedPattern.initFromParts(parsed.parts, fr.pattern_string_arena.allocator()) else static_route: { - const allocation = try bun.default_allocator.alloc(u8, static_total_len); + const allocation = try fr.pattern_string_arena.allocator().alloc(u8, static_total_len); var s = std.io.fixedBufferStream(allocation); for (parsed.parts) |part| switch (part) { diff --git a/src/bake/bake.zig b/src/bake/bake.zig index a03eca25f0..40c96f361a 100644 --- a/src/bake/bake.zig +++ b/src/bake/bake.zig @@ -290,7 +290,7 @@ pub const Framework = struct { import_source: []const u8 = "react-refresh/runtime", }; - pub const react_install_command = "bun i react@experimental react-dom@experimental react-server-dom-bun"; + pub const react_install_command = "bun i react@experimental react-dom@experimental react-server-dom-bun react-refresh@experimental"; pub fn addReactInstallCommandNote(log: *bun.logger.Log) !void { try log.addMsg(.{ @@ -593,6 +593,37 @@ pub const Framework = struct { renderer: Graph, out: *bun.transpiler.Transpiler, bundler_options: *const BuildConfigSubset, + ) !void { + const source_map: bun.options.SourceMapOption = switch (mode) { + // Source maps must always be external, as DevServer special cases + // the linking and part of the generation of these. It also relies + // on source maps always being enabled. + .development => .external, + // TODO: follow user configuration + else => .none, + }; + + return initTranspilerWithSourceMap( + framework, + arena, + log, + mode, + renderer, + out, + bundler_options, + source_map, + ); + } + + pub fn initTranspilerWithSourceMap( + framework: *Framework, + arena: std.mem.Allocator, + log: *bun.logger.Log, + mode: Mode, + renderer: Graph, + out: *bun.transpiler.Transpiler, + bundler_options: *const BuildConfigSubset, + source_map: bun.options.SourceMapOption, ) !void { const JSAst = bun.JSAst; @@ -646,6 +677,11 @@ pub const Framework = struct { // Support `esm-env` package using this condition. try out.options.conditions.appendSlice(&.{"development"}); } + // Ensure "node" condition is included for server-side rendering + // This helps with package.json imports field resolution + if (renderer == .server or renderer == .ssr) { + try out.options.conditions.appendSlice(&.{"node"}); + } if (bundler_options.conditions.count() > 0) { try out.options.conditions.appendSlice(bundler_options.conditions.keys()); } @@ -661,14 +697,7 @@ pub const Framework = struct { if (bundler_options.ignoreDCEAnnotations) |ignore| out.options.ignore_dce_annotations = ignore; - out.options.source_map = switch (mode) { - // Source maps must always be external, as DevServer special cases - // the linking and part of the generation of these. It also relies - // on source maps always being enabled. - .development => .external, - // TODO: follow user configuration - else => .none, - }; + out.options.source_map = source_map; if (bundler_options.env != ._none) { out.options.env.behavior = bundler_options.env; out.options.env.prefix = bundler_options.env_prefix orelse ""; diff --git a/src/bake/hmr-module.ts b/src/bake/hmr-module.ts index 308e9dca75..8c8b666eb3 100644 --- a/src/bake/hmr-module.ts +++ b/src/bake/hmr-module.ts @@ -249,7 +249,9 @@ export class HMRModule { declare builtin: (id: string) => any; } if (side === "server") { - HMRModule.prototype.builtin = import.meta.require; + HMRModule.prototype.builtin = (id: string) => + // @ts-expect-error + import.meta.bakeBuiltin(import.meta.resolve(id)); } // prettier-ignore HMRModule.prototype.indirectHot = new Proxy({}, { diff --git a/src/bake/production.zig b/src/bake/production.zig index b4c1ed10f7..7864940b66 100644 --- a/src/bake/production.zig +++ b/src/bake/production.zig @@ -174,10 +174,10 @@ pub fn buildWithVm(ctx: bun.CLI.Command.Context, cwd: []const u8, vm: *VirtualMa var client_transpiler: bun.transpiler.Transpiler = undefined; var server_transpiler: bun.transpiler.Transpiler = undefined; var ssr_transpiler: bun.transpiler.Transpiler = undefined; - try framework.initTranspiler(allocator, vm.log, .production_static, .server, &server_transpiler, &options.bundler_options.server); - try framework.initTranspiler(allocator, vm.log, .production_static, .client, &client_transpiler, &options.bundler_options.client); + try framework.initTranspilerWithSourceMap(allocator, vm.log, .production_static, .server, &server_transpiler, &options.bundler_options.server, .@"inline"); + try framework.initTranspilerWithSourceMap(allocator, vm.log, .production_static, .client, &client_transpiler, &options.bundler_options.client, .@"inline"); if (separate_ssr_graph) { - try framework.initTranspiler(allocator, vm.log, .production_static, .ssr, &ssr_transpiler, &options.bundler_options.ssr); + try framework.initTranspilerWithSourceMap(allocator, vm.log, .production_static, .ssr, &ssr_transpiler, &options.bundler_options.ssr, .@"inline"); } if (ctx.bundler_options.bake_debug_disable_minify) { @@ -561,6 +561,7 @@ pub fn buildWithVm(ctx: bun.CLI.Command.Context, cwd: []const u8, vm: *VirtualMa return vm.global.throwValue(err); }, } + vm.waitForTasks(); } /// unsafe function, must be run outside of the event loop diff --git a/src/bun.js/SavedSourceMap.zig b/src/bun.js/SavedSourceMap.zig index d909f854f1..2784c08369 100644 --- a/src/bun.js/SavedSourceMap.zig +++ b/src/bun.js/SavedSourceMap.zig @@ -78,6 +78,8 @@ pub const SavedMappings = struct { } }; +const BakeSourceProvider = bun.sourcemap.BakeSourceProvider; + /// ParsedSourceMap is the canonical form for sourcemaps, /// /// but `SavedMappings` and `SourceProviderMap` are much cheaper to construct. @@ -86,6 +88,7 @@ pub const Value = bun.TaggedPointerUnion(.{ ParsedSourceMap, SavedMappings, SourceProviderMap, + BakeSourceProvider, }); pub const MissingSourceMapNoteInfo = struct { @@ -102,6 +105,10 @@ pub const MissingSourceMapNoteInfo = struct { } }; +pub fn putBakeSourceProvider(this: *SavedSourceMap, opaque_source_provider: *BakeSourceProvider, path: []const u8) void { + this.putValue(path, Value.init(opaque_source_provider)) catch bun.outOfMemory(); +} + pub fn putZigSourceProvider(this: *SavedSourceMap, opaque_source_provider: *anyopaque, path: []const u8) void { const source_provider: *SourceProviderMap = @ptrCast(opaque_source_provider); this.putValue(path, Value.init(source_provider)) catch bun.outOfMemory(); @@ -120,7 +127,7 @@ pub fn removeZigSourceProvider(this: *SavedSourceMap, opaque_source_provider: *a } } else if (old_value.get(ParsedSourceMap)) |map| { if (map.underlying_provider.provider()) |prov| { - if (@intFromPtr(prov) == @intFromPtr(opaque_source_provider)) { + if (@intFromPtr(prov.ptr()) == @intFromPtr(opaque_source_provider)) { this.map.removeByPtr(entry.key_ptr); map.deref(); } @@ -246,11 +253,39 @@ fn getWithContent( MissingSourceMapNoteInfo.path = storage; return .{}; }, + @field(Value.Tag, @typeName(BakeSourceProvider)) => { + // TODO: This is a copy-paste of above branch + const ptr: *BakeSourceProvider = Value.from(mapping.value_ptr.*).as(BakeSourceProvider); + this.unlock(); + + // Do not lock the mutex while we're parsing JSON! + if (ptr.getSourceMap(path, .none, hint)) |parse| { + if (parse.map) |map| { + map.ref(); + // The mutex is not locked. We have to check the hash table again. + this.putValue(path, Value.init(map)) catch bun.outOfMemory(); + + return parse; + } + } + + this.lock(); + defer this.unlock(); + // does not have a valid source map. let's not try again + _ = this.map.remove(hash); + + // Store path for a user note. + const storage = MissingSourceMapNoteInfo.storage[0..path.len]; + @memcpy(storage, path); + MissingSourceMapNoteInfo.path = storage; + return .{}; + }, else => { if (Environment.allow_assert) { @panic("Corrupt pointer tag"); } this.unlock(); + return .{}; }, } diff --git a/src/bun.js/VirtualMachine.zig b/src/bun.js/VirtualMachine.zig index daf49f889a..a28633e557 100644 --- a/src/bun.js/VirtualMachine.zig +++ b/src/bun.js/VirtualMachine.zig @@ -869,7 +869,13 @@ pub fn waitForPromise(this: *VirtualMachine, promise: JSC.AnyPromise) void { } pub fn waitForTasks(this: *VirtualMachine) void { - this.eventLoop().waitForTasks(); + while (this.isEventLoopAlive()) { + this.eventLoop().tick(); + + if (this.isEventLoopAlive()) { + this.eventLoop().autoTick(); + } + } } pub const MacroMap = std.AutoArrayHashMap(i32, JSC.C.JSObjectRef); diff --git a/src/bun.js/api/server.zig b/src/bun.js/api/server.zig index cc140d473a..60f6644d6f 100644 --- a/src/bun.js/api/server.zig +++ b/src/bun.js/api/server.zig @@ -3156,13 +3156,13 @@ pub const AnyServer = struct { pub fn onRequest( this: AnyServer, req: *uws.Request, - resp: *uws.NewApp(false).Response, + resp: bun.uws.AnyResponse, ) void { return switch (this.ptr.tag()) { - Ptr.case(HTTPServer) => this.ptr.as(HTTPServer).onRequest(req, resp), - Ptr.case(HTTPSServer) => @panic("TODO: https"), - Ptr.case(DebugHTTPServer) => this.ptr.as(DebugHTTPServer).onRequest(req, resp), - Ptr.case(DebugHTTPSServer) => @panic("TODO: https"), + Ptr.case(HTTPServer) => this.ptr.as(HTTPServer).onRequest(req, resp.assertNoSSL()), + Ptr.case(HTTPSServer) => this.ptr.as(HTTPSServer).onRequest(req, resp.assertSSL()), + Ptr.case(DebugHTTPServer) => this.ptr.as(DebugHTTPServer).onRequest(req, resp.assertNoSSL()), + Ptr.case(DebugHTTPSServer) => this.ptr.as(DebugHTTPSServer).onRequest(req, resp.assertSSL()), else => bun.unreachablePanic("Invalid pointer tag", .{}), }; } diff --git a/src/bun.js/bindings/ImportMetaObject.cpp b/src/bun.js/bindings/ImportMetaObject.cpp index c7b76015a1..b7df44de7e 100644 --- a/src/bun.js/bindings/ImportMetaObject.cpp +++ b/src/bun.js/bindings/ImportMetaObject.cpp @@ -51,6 +51,7 @@ #include "wtf/text/StringView.h" #include "isBuiltinModule.h" +#include "WebCoreJSBuiltins.h" namespace Zig { using namespace JSC; @@ -143,7 +144,13 @@ ImportMetaObject* ImportMetaObject::create(JSC::JSGlobalObject* globalObject, co { VM& vm = globalObject->vm(); Zig::GlobalObject* zigGlobalObject = jsCast(globalObject); - auto structure = zigGlobalObject->ImportMetaObjectStructure(); + bool isBake = url.startsWith("bake:"_s); + + // Get the appropriate structure + Structure* structure = isBake + ? zigGlobalObject->ImportMetaBakeObjectStructure() + : zigGlobalObject->ImportMetaObjectStructure(); + return create(vm, globalObject, structure, url); } @@ -592,7 +599,21 @@ static const HashTableValue ImportMetaObjectPrototypeValues[] = { { "url"_s, static_cast(JSC::PropertyAttribute::ReadOnly | JSC::PropertyAttribute::CustomAccessor | PropertyAttribute::DontDelete), NoIntrinsic, { HashTableValue::GetterSetterType, jsImportMetaObjectGetter_url, 0 } }, }; -class ImportMetaObjectPrototype final : public JSC::JSNonFinalObject { +static const HashTableValue ImportMetaObjectBakePrototypeValues[] = { + { "bakeBuiltin"_s, static_cast(JSC::PropertyAttribute::Builtin | PropertyAttribute::DontDelete | PropertyAttribute::ReadOnly), NoIntrinsic, { HashTableValue::BuiltinGeneratorType, commonJSRequireESMCodeGenerator, 0 } }, + { "dir"_s, static_cast(JSC::PropertyAttribute::ReadOnly | JSC::PropertyAttribute::CustomAccessor | PropertyAttribute::DontDelete), NoIntrinsic, { HashTableValue::GetterSetterType, jsImportMetaObjectGetter_dir, 0 } }, + { "dirname"_s, static_cast(JSC::PropertyAttribute::ReadOnly | JSC::PropertyAttribute::CustomAccessor | PropertyAttribute::DontDelete), NoIntrinsic, { HashTableValue::GetterSetterType, jsImportMetaObjectGetter_dir, 0 } }, + { "env"_s, static_cast(JSC::PropertyAttribute::ReadOnly | JSC::PropertyAttribute::CustomAccessor | PropertyAttribute::DontDelete), NoIntrinsic, { HashTableValue::GetterSetterType, jsImportMetaObjectGetter_env, 0 } }, + { "file"_s, static_cast(JSC::PropertyAttribute::ReadOnly | JSC::PropertyAttribute::CustomAccessor | PropertyAttribute::DontDelete), NoIntrinsic, { HashTableValue::GetterSetterType, jsImportMetaObjectGetter_file, 0 } }, + { "filename"_s, static_cast(JSC::PropertyAttribute::ReadOnly | JSC::PropertyAttribute::CustomAccessor | PropertyAttribute::DontDelete), NoIntrinsic, { HashTableValue::GetterSetterType, jsImportMetaObjectGetter_path, 0 } }, + { "path"_s, static_cast(JSC::PropertyAttribute::ReadOnly | JSC::PropertyAttribute::CustomAccessor | PropertyAttribute::DontDelete), NoIntrinsic, { HashTableValue::GetterSetterType, jsImportMetaObjectGetter_path, 0 } }, + { "require"_s, static_cast(JSC::PropertyAttribute::CustomAccessor | PropertyAttribute::DontDelete), NoIntrinsic, { HashTableValue::GetterSetterType, jsImportMetaObjectGetter_require, jsImportMetaObjectSetter_require } }, + { "resolve"_s, static_cast(JSC::PropertyAttribute::Function | PropertyAttribute::DontDelete), NoIntrinsic, { HashTableValue::NativeFunctionType, functionImportMeta__resolve, 0 } }, + { "resolveSync"_s, static_cast(JSC::PropertyAttribute::Function | PropertyAttribute::DontDelete), NoIntrinsic, { HashTableValue::NativeFunctionType, functionImportMeta__resolveSync, 0 } }, + { "url"_s, static_cast(JSC::PropertyAttribute::ReadOnly | JSC::PropertyAttribute::CustomAccessor | PropertyAttribute::DontDelete), NoIntrinsic, { HashTableValue::GetterSetterType, jsImportMetaObjectGetter_url, 0 } }, +}; + +class ImportMetaObjectPrototype : public JSC::JSNonFinalObject { public: DECLARE_INFO; using Base = JSC::JSNonFinalObject; @@ -602,10 +623,10 @@ public: return Structure::create(vm, globalObject, globalObject->objectPrototype(), TypeInfo(ObjectType, StructureFlags), info()); } - static ImportMetaObjectPrototype* create(JSC::VM& vm, JSC::JSGlobalObject* globalObject, JSC::Structure* structure) + static ImportMetaObjectPrototype* create(JSC::VM& vm, JSC::JSGlobalObject* globalObject, JSC::Structure* structure, bool isBake = false) { ImportMetaObjectPrototype* prototype = new (NotNull, JSC::allocateCell(vm)) ImportMetaObjectPrototype(vm, structure); - prototype->finishCreation(vm, globalObject); + prototype->finishCreation(vm, globalObject, isBake); return prototype; } @@ -616,14 +637,19 @@ public: return &vm.plainObjectSpace(); } - void finishCreation(JSC::VM& vm, JSC::JSGlobalObject* globalObject) + void finishCreation(JSC::VM& vm, JSC::JSGlobalObject* globalObject, bool isBake) { Base::finishCreation(vm); auto* clientData = WebCore::clientData(vm); auto& builtinNames = clientData->builtinNames(); - reifyStaticProperties(vm, ImportMetaObject::info(), ImportMetaObjectPrototypeValues, *this); + // Use the appropriate prototype values based on whether this is a bake import meta object + if (isBake) { + reifyStaticProperties(vm, ImportMetaObject::info(), ImportMetaObjectBakePrototypeValues, *this); + } else { + reifyStaticProperties(vm, ImportMetaObject::info(), ImportMetaObjectPrototypeValues, *this); + } JSC_TO_STRING_TAG_WITHOUT_TRANSITION(); auto mainGetter = JSFunction::create(vm, globalObject, importMetaObjectMainCodeGenerator(vm), globalObject); @@ -647,11 +673,12 @@ const ClassInfo ImportMetaObjectPrototype::s_info = { &Base::s_info, nullptr, nullptr, CREATE_METHOD_TABLE(ImportMetaObjectPrototype) }; -JSC::Structure* ImportMetaObject::createStructure(JSC::VM& vm, JSC::JSGlobalObject* globalObject) +JSC::Structure* ImportMetaObject::createStructure(JSC::VM& vm, JSC::JSGlobalObject* globalObject, bool isBake) { ImportMetaObjectPrototype* prototype = ImportMetaObjectPrototype::create(vm, globalObject, - ImportMetaObjectPrototype::createStructure(vm, globalObject)); + ImportMetaObjectPrototype::createStructure(vm, globalObject), + isBake); return Structure::create(vm, globalObject, prototype, TypeInfo(ObjectType, StructureFlags), ImportMetaObject::info()); } diff --git a/src/bun.js/bindings/ImportMetaObject.h b/src/bun.js/bindings/ImportMetaObject.h index 64bae5cc84..4ec49bcda7 100644 --- a/src/bun.js/bindings/ImportMetaObject.h +++ b/src/bun.js/bindings/ImportMetaObject.h @@ -70,7 +70,7 @@ public: [](auto& spaces, auto&& space) { spaces.m_subspaceForImportMeta = std::forward(space); }); } - static JSC::Structure* createStructure(JSC::VM& vm, JSC::JSGlobalObject* globalObject); + static JSC::Structure* createStructure(JSC::VM& vm, JSC::JSGlobalObject* globalObject, bool isBake = false); static void analyzeHeap(JSCell*, JSC::HeapAnalyzer&); static JSValue getPrototype(JSObject*, JSC::JSGlobalObject* globalObject); diff --git a/src/bun.js/bindings/ZigGlobalObject.cpp b/src/bun.js/bindings/ZigGlobalObject.cpp index 9dc722cb57..d69a6da4f6 100644 --- a/src/bun.js/bindings/ZigGlobalObject.cpp +++ b/src/bun.js/bindings/ZigGlobalObject.cpp @@ -3297,6 +3297,11 @@ void GlobalObject::finishCreation(VM& vm) init.set(Zig::ImportMetaObject::createStructure(init.vm, init.owner)); }); + m_importMetaBakeObjectStructure.initLater( + [](const JSC::LazyProperty::Initializer& init) { + init.set(Zig::ImportMetaObject::createStructure(init.vm, init.owner, true)); + }); + m_asyncBoundFunctionStructure.initLater( [](const JSC::LazyProperty::Initializer& init) { init.set(AsyncContextFrame::createStructure(init.vm, init.owner)); diff --git a/src/bun.js/bindings/ZigGlobalObject.h b/src/bun.js/bindings/ZigGlobalObject.h index 41de94aa16..3bf94799d4 100644 --- a/src/bun.js/bindings/ZigGlobalObject.h +++ b/src/bun.js/bindings/ZigGlobalObject.h @@ -295,6 +295,7 @@ public: Structure* CommonJSModuleObjectStructure() const { return m_commonJSModuleObjectStructure.getInitializedOnMainThread(this); } Structure* JSSocketAddressDTOStructure() const { return m_JSSocketAddressDTOStructure.getInitializedOnMainThread(this); } Structure* ImportMetaObjectStructure() const { return m_importMetaObjectStructure.getInitializedOnMainThread(this); } + Structure* ImportMetaBakeObjectStructure() const { return m_importMetaBakeObjectStructure.getInitializedOnMainThread(this); } Structure* AsyncContextFrameStructure() const { return m_asyncBoundFunctionStructure.getInitializedOnMainThread(this); } JSWeakMap* vmModuleContextMap() const { return m_vmModuleContextMap.getInitializedOnMainThread(this); } @@ -587,6 +588,7 @@ public: V(private, LazyPropertyOfGlobalObject, m_processBindingFs) \ V(private, LazyPropertyOfGlobalObject, m_processBindingHTTPParser) \ V(private, LazyPropertyOfGlobalObject, m_importMetaObjectStructure) \ + V(private, LazyPropertyOfGlobalObject, m_importMetaBakeObjectStructure) \ V(private, LazyPropertyOfGlobalObject, m_asyncBoundFunctionStructure) \ V(public, LazyPropertyOfGlobalObject, m_JSDOMFileConstructor) \ V(public, LazyPropertyOfGlobalObject, m_JSMIMEParamsConstructor) \ diff --git a/src/bun.js/virtual_machine_exports.zig b/src/bun.js/virtual_machine_exports.zig index e81efc744e..1a1448012b 100644 --- a/src/bun.js/virtual_machine_exports.zig +++ b/src/bun.js/virtual_machine_exports.zig @@ -172,6 +172,14 @@ export fn Bun__getVerboseFetchValue() i32 { }; } +const BakeSourceProvider = bun.sourcemap.BakeSourceProvider; +export fn Bun__addBakeSourceProviderSourceMap(vm: *VirtualMachine, opaque_source_provider: *anyopaque, specifier: *bun.String) void { + var sfb = std.heap.stackFallback(4096, bun.default_allocator); + const slice = specifier.toUTF8(sfb.get()); + defer slice.deinit(); + vm.source_mappings.putBakeSourceProvider(@as(*BakeSourceProvider, @ptrCast(opaque_source_provider)), slice.slice()); +} + export fn Bun__addSourceProviderSourceMap(vm: *VirtualMachine, opaque_source_provider: *anyopaque, specifier: *bun.String) void { var sfb = std.heap.stackFallback(4096, bun.default_allocator); const slice = specifier.toUTF8(sfb.get()); diff --git a/src/bun.zig b/src/bun.zig index dc357b262a..b875783824 100644 --- a/src/bun.zig +++ b/src/bun.zig @@ -1922,6 +1922,7 @@ pub const StandaloneModuleGraph = @import("./StandaloneModuleGraph.zig").Standal const _string = @import("./string.zig"); pub const strings = @import("string_immutable.zig"); pub const String = _string.String; +pub const ZigString = JSC.ZigString; pub const StringJoiner = _string.StringJoiner; pub const SliceWithUnderlyingString = _string.SliceWithUnderlyingString; pub const PathString = _string.PathString; diff --git a/src/bundler/bundle_v2.zig b/src/bundler/bundle_v2.zig index 6766ad5552..8f3e95ec38 100644 --- a/src/bundler/bundle_v2.zig +++ b/src/bundler/bundle_v2.zig @@ -136,6 +136,9 @@ pub const BundleV2 = struct { /// Set true by DevServer. Currently every usage of the transpiler (Bun.build /// and `bun build` cli) runs at the top of an event loop. When this is /// true, a callback is executed after all work is complete. + /// + /// You can find which callbacks are run by looking at the + /// `finishFromBakeDevServer(...)` function here asynchronous: bool = false, thread_lock: bun.DebugThreadLock, diff --git a/src/deps/uws/Response.zig b/src/deps/uws/Response.zig index db9a64add3..01ad887ddc 100644 --- a/src/deps/uws/Response.zig +++ b/src/deps/uws/Response.zig @@ -316,6 +316,20 @@ pub const AnyResponse = union(enum) { SSL: *uws.NewApp(true).Response, TCP: *uws.NewApp(false).Response, + pub fn assertSSL(this: AnyResponse) *uws.NewApp(true).Response { + return switch (this) { + .SSL => |resp| resp, + .TCP => bun.Output.panic("Expected SSL response, got TCP response", .{}), + }; + } + + pub fn assertNoSSL(this: AnyResponse) *uws.NewApp(false).Response { + return switch (this) { + .SSL => bun.Output.panic("Expected TCP response, got SSL response", .{}), + .TCP => |resp| resp, + }; + } + pub fn markNeedsMore(this: AnyResponse) void { return switch (this) { inline else => |resp| resp.markNeedsMore(), diff --git a/src/js/builtins/Bake.ts b/src/js/builtins/Bake.ts index 9faf064bf8..bfe3cdfcb4 100644 --- a/src/js/builtins/Bake.ts +++ b/src/js/builtins/Bake.ts @@ -128,13 +128,20 @@ export function renderRoutesForProdStatic( pageModule, layouts, }); + let result; if (paramGetter[Symbol.asyncIterator] != undefined) { for await (const params of paramGetter) { - callRouteGenerator(type, i, layouts, pageModule, params); + result = callRouteGenerator(type, i, layouts, pageModule, params); + if ($isPromise(result) && $isPromisePending(result)) { + await result; + } } } else if (paramGetter[Symbol.iterator] != undefined) { for (const params of paramGetter) { - callRouteGenerator(type, i, layouts, pageModule, params); + result = callRouteGenerator(type, i, layouts, pageModule, params); + if ($isPromise(result) && $isPromisePending(result)) { + await result; + } } } else { await Promise.all( diff --git a/src/sourcemap/sourcemap.zig b/src/sourcemap/sourcemap.zig index b15d5fe3d7..bb4d969fe0 100644 --- a/src/sourcemap/sourcemap.zig +++ b/src/sourcemap/sourcemap.zig @@ -639,18 +639,51 @@ pub const ParsedSourceMap = struct { is_standalone_module_graph: bool = false, - const SourceContentPtr = packed struct(u64) { - load_hint: SourceMapLoadHint, - data: u62, + const SourceProviderKind = enum(u1) { zig, bake }; + const AnySourceProvider = union(enum) { + zig: *SourceProviderMap, + bake: *BakeSourceProvider, - pub const none: SourceContentPtr = .{ .load_hint = .none, .data = 0 }; - - fn fromProvider(p: *SourceProviderMap) SourceContentPtr { - return .{ .load_hint = .none, .data = @intCast(@intFromPtr(p)) }; + pub fn ptr(this: AnySourceProvider) *anyopaque { + return switch (this) { + .zig => @ptrCast(this.zig), + .bake => @ptrCast(this.bake), + }; } - pub fn provider(sc: SourceContentPtr) ?*SourceProviderMap { - return @ptrFromInt(sc.data); + pub fn getSourceMap( + this: AnySourceProvider, + source_filename: []const u8, + load_hint: SourceMapLoadHint, + result: ParseUrlResultHint, + ) ?SourceMap.ParseUrl { + return switch (this) { + .zig => this.zig.getSourceMap(source_filename, load_hint, result), + .bake => this.bake.getSourceMap(source_filename, load_hint, result), + }; + } + }; + + const SourceContentPtr = packed struct(u64) { + load_hint: SourceMapLoadHint, + kind: SourceProviderKind, + data: u61, + + pub const none: SourceContentPtr = .{ .load_hint = .none, .kind = .zig, .data = 0 }; + + fn fromProvider(p: *SourceProviderMap) SourceContentPtr { + return .{ .load_hint = .none, .data = @intCast(@intFromPtr(p)), .kind = .zig }; + } + + fn fromBakeProvider(p: *BakeSourceProvider) SourceContentPtr { + return .{ .load_hint = .none, .data = @intCast(@intFromPtr(p)), .kind = .bake }; + } + + pub fn provider(sc: SourceContentPtr) ?AnySourceProvider { + switch (sc.kind) { + .zig => return .{ .zig = @ptrFromInt(sc.data) }, + .bake => return .{ .bake = @ptrFromInt(sc.data) }, + } } }; @@ -735,26 +768,137 @@ pub const SourceMapLoadHint = enum(u2) { is_external_map, }; +fn findSourceMappingURL(comptime T: type, source: []const T, alloc: std.mem.Allocator) ?bun.JSC.ZigString.Slice { + const needle = comptime bun.strings.literal(T, "\n//# sourceMappingURL="); + const found = bun.strings.indexOfT(T, source, needle) orelse return null; + const end = std.mem.indexOfScalarPos(T, source, found + needle.len, '\n') orelse source.len; + const url = std.mem.trimRight(T, source[found + needle.len .. end], &.{ ' ', '\r' }); + return switch (T) { + u8 => bun.JSC.ZigString.Slice.fromUTF8NeverFree(url), + u16 => bun.JSC.ZigString.Slice.init( + alloc, + bun.strings.toUTF8Alloc(alloc, url) catch bun.outOfMemory(), + ), + else => @compileError("Not Supported"), + }; +} + +/// The last two arguments to this specify loading hints +pub fn getSourceMapImpl( + comptime SourceProviderKind: type, + provider: *SourceProviderKind, + source_filename: []const u8, + load_hint: SourceMapLoadHint, + result: ParseUrlResultHint, +) ?SourceMap.ParseUrl { + // This was previously 65535 but that is a size that can risk stack overflow + // and due to the many layers of indirections and wrappers this function is called in, it + // is difficult to reason about how deeply nested of a callstack this + // function is called in. 1024 is a safer number. + // + // TODO: Experiment in debug builds calculating how much stack space we have left and using that to + // adjust the size + const STACK_SPACE_TO_USE = 1024; + var sfb = std.heap.stackFallback(STACK_SPACE_TO_USE, bun.default_allocator); + var arena = bun.ArenaAllocator.init(sfb.get()); + defer arena.deinit(); + const allocator = arena.allocator(); + + const new_load_hint: SourceMapLoadHint, const parsed = parsed: { + var inline_err: ?anyerror = null; + + // try to get an inline source map + if (load_hint != .is_external_map) try_inline: { + const source = SourceProviderKind.getSourceSlice(provider); + defer source.deref(); + bun.assert(source.tag == .ZigString); + + const found_url = (if (source.is8Bit()) + findSourceMappingURL(u8, source.latin1(), allocator) + else + findSourceMappingURL(u16, source.utf16(), allocator)) orelse + break :try_inline; + defer found_url.deinit(); + + break :parsed .{ + .is_inline_map, + parseUrl( + bun.default_allocator, + allocator, + found_url.slice(), + result, + ) catch |err| { + inline_err = err; + break :try_inline; + }, + }; + } + + // try to load a .map file + if (load_hint != .is_inline_map) try_external: { + var load_path_buf: *bun.PathBuffer = bun.PathBufferPool.get(); + defer bun.PathBufferPool.put(load_path_buf); + if (source_filename.len + 4 > load_path_buf.len) + break :try_external; + @memcpy(load_path_buf[0..source_filename.len], source_filename); + @memcpy(load_path_buf[source_filename.len..][0..4], ".map"); + + const load_path = load_path_buf[0 .. source_filename.len + 4]; + const data = switch (bun.sys.File.readFrom(std.fs.cwd(), load_path, allocator)) { + .err => break :try_external, + .result => |data| data, + }; + + break :parsed .{ + .is_external_map, + parseJSON( + bun.default_allocator, + allocator, + data, + result, + ) catch |err| { + // Print warning even if this came from non-visible code like + // calling `error.stack`. This message is only printed if + // the sourcemap has been found but is invalid, such as being + // invalid JSON text or corrupt mappings. + bun.Output.warn("Could not decode sourcemap in '{s}': {s}", .{ + source_filename, + @errorName(err), + }); // Disable the "try using --sourcemap=external" hint + bun.JSC.SavedSourceMap.MissingSourceMapNoteInfo.seen_invalid = true; + return null; + }, + }; + } + + if (inline_err) |err| { + bun.Output.warn("Could not decode sourcemap in '{s}': {s}", .{ + source_filename, + @errorName(err), + }); + // Disable the "try using --sourcemap=external" hint + bun.JSC.SavedSourceMap.MissingSourceMapNoteInfo.seen_invalid = true; + return null; + } + + return null; + }; + if (parsed.map) |ptr| { + ptr.underlying_provider = SourceProviderKind.toSourceContentPtr(provider); + ptr.underlying_provider.load_hint = new_load_hint; + } + return parsed; +} + /// This is a pointer to a ZigSourceProvider that may or may not have a `//# sourceMappingURL` comment /// when we want to lookup this data, we will then resolve it to a ParsedSourceMap if it does. /// /// This is used for files that were pre-bundled with `bun build --target=bun --sourcemap` pub const SourceProviderMap = opaque { extern fn ZigSourceProvider__getSourceSlice(*SourceProviderMap) bun.String; - - fn findSourceMappingURL(comptime T: type, source: []const T, alloc: std.mem.Allocator) ?bun.JSC.ZigString.Slice { - const needle = comptime bun.strings.literal(T, "\n//# sourceMappingURL="); - const found = bun.strings.indexOfT(T, source, needle) orelse return null; - const end = std.mem.indexOfScalarPos(T, source, found + needle.len, '\n') orelse source.len; - const url = std.mem.trimRight(T, source[found + needle.len .. end], &.{ ' ', '\r' }); - return switch (T) { - u8 => bun.JSC.ZigString.Slice.fromUTF8NeverFree(url), - u16 => bun.JSC.ZigString.Slice.init( - alloc, - bun.strings.toUTF8Alloc(alloc, url) catch bun.outOfMemory(), - ), - else => @compileError("Not Supported"), - }; + pub const getSourceSlice = ZigSourceProvider__getSourceSlice; + pub fn toSourceContentPtr(this: *SourceProviderMap) ParsedSourceMap.SourceContentPtr { + return ParsedSourceMap.SourceContentPtr.fromProvider(this); } /// The last two arguments to this specify loading hints @@ -764,94 +908,37 @@ pub const SourceProviderMap = opaque { load_hint: SourceMapLoadHint, result: ParseUrlResultHint, ) ?SourceMap.ParseUrl { - var sfb = std.heap.stackFallback(65536, bun.default_allocator); - var arena = bun.ArenaAllocator.init(sfb.get()); - defer arena.deinit(); - const allocator = arena.allocator(); + return getSourceMapImpl( + SourceProviderMap, + provider, + source_filename, + load_hint, + result, + ); + } +}; - const new_load_hint: SourceMapLoadHint, const parsed = parsed: { - var inline_err: ?anyerror = null; +pub const BakeSourceProvider = opaque { + extern fn BakeSourceProvider__getSourceSlice(*BakeSourceProvider) bun.String; + pub const getSourceSlice = BakeSourceProvider__getSourceSlice; + pub fn toSourceContentPtr(this: *BakeSourceProvider) ParsedSourceMap.SourceContentPtr { + return ParsedSourceMap.SourceContentPtr.fromBakeProvider(this); + } - // try to get an inline source map - if (load_hint != .is_external_map) try_inline: { - const source = ZigSourceProvider__getSourceSlice(provider); - defer source.deref(); - bun.assert(source.tag == .ZigString); - - const found_url = (if (source.is8Bit()) - findSourceMappingURL(u8, source.latin1(), allocator) - else - findSourceMappingURL(u16, source.utf16(), allocator)) orelse - break :try_inline; - defer found_url.deinit(); - - break :parsed .{ - .is_inline_map, - parseUrl( - bun.default_allocator, - allocator, - found_url.slice(), - result, - ) catch |err| { - inline_err = err; - break :try_inline; - }, - }; - } - - // try to load a .map file - if (load_hint != .is_inline_map) try_external: { - var load_path_buf: bun.PathBuffer = undefined; - if (source_filename.len + 4 > load_path_buf.len) - break :try_external; - @memcpy(load_path_buf[0..source_filename.len], source_filename); - @memcpy(load_path_buf[source_filename.len..][0..4], ".map"); - - const load_path = load_path_buf[0 .. source_filename.len + 4]; - const data = switch (bun.sys.File.readFrom(std.fs.cwd(), load_path, allocator)) { - .err => break :try_external, - .result => |data| data, - }; - - break :parsed .{ - .is_external_map, - parseJSON( - bun.default_allocator, - allocator, - data, - result, - ) catch |err| { - // Print warning even if this came from non-visible code like - // calling `error.stack`. This message is only printed if - // the sourcemap has been found but is invalid, such as being - // invalid JSON text or corrupt mappings. - bun.Output.warn("Could not decode sourcemap in '{s}': {s}", .{ - source_filename, - @errorName(err), - }); // Disable the "try using --sourcemap=external" hint - bun.JSC.SavedSourceMap.MissingSourceMapNoteInfo.seen_invalid = true; - return null; - }, - }; - } - - if (inline_err) |err| { - bun.Output.warn("Could not decode sourcemap in '{s}': {s}", .{ - source_filename, - @errorName(err), - }); - // Disable the "try using --sourcemap=external" hint - bun.JSC.SavedSourceMap.MissingSourceMapNoteInfo.seen_invalid = true; - return null; - } - - return null; - }; - if (parsed.map) |ptr| { - ptr.underlying_provider = ParsedSourceMap.SourceContentPtr.fromProvider(provider); - ptr.underlying_provider.load_hint = new_load_hint; - } - return parsed; + /// The last two arguments to this specify loading hints + pub fn getSourceMap( + provider: *BakeSourceProvider, + source_filename: []const u8, + load_hint: SourceMap.SourceMapLoadHint, + result: SourceMap.ParseUrlResultHint, + ) ?SourceMap.ParseUrl { + return getSourceMapImpl( + BakeSourceProvider, + provider, + source_filename, + load_hint, + result, + ); } }; diff --git a/src/watcher/INotifyWatcher.zig b/src/watcher/INotifyWatcher.zig index 9a180c3dd8..3a75557215 100644 --- a/src/watcher/INotifyWatcher.zig +++ b/src/watcher/INotifyWatcher.zig @@ -51,7 +51,7 @@ pub const Event = extern struct { const largest_size = std.mem.alignForward(usize, @sizeOf(Event) + bun.MAX_PATH_BYTES, @alignOf(Event)); pub fn name(event: *align(1) Event) [:0]u8 { - if (comptime Environment.allow_assert) bun.assert(event.name_len > 0); + if (comptime Environment.allow_assert) bun.assertf(event.name_len > 0, "INotifyWatcher.Event.name() called with name_len == 0, you should check it before calling this function.", .{}); const name_first_char_ptr = std.mem.asBytes(&event.name_len).ptr + @sizeOf(u32); return bun.sliceTo(@as([*:0]u8, @ptrCast(name_first_char_ptr)), 0); } diff --git a/test/bake/bake-harness.ts b/test/bake/bake-harness.ts index de46d788bc..9d71c8c01a 100644 --- a/test/bake/bake-harness.ts +++ b/test/bake/bake-harness.ts @@ -18,7 +18,7 @@ import { Matchers } from "bun:test"; import { EventEmitter } from "node:events"; // @ts-ignore import { dedent } from "../bundler/expectBundled.ts"; -import { bunEnv, isCI, isWindows, mergeWindowEnvs } from "harness"; +import { bunEnv, bunExe, isCI, isWindows, mergeWindowEnvs } from "harness"; import { expect } from "bun:test"; import { exitCodeMapStrings } from "./exit-code-map.mjs"; @@ -34,7 +34,7 @@ const verboseSynchronization = process.env.BUN_DEV_SERVER_VERBOSE_SYNC * Can be set in fast development environments to improve iteration time. * In CI/Windows it appears that sometimes these tests dont wait enough * for things to happen, so the extra delay reduces flakiness. - * + * * Needs much more investigation. */ const fastBatches = !!process.env.BUN_DEV_SERVER_FAST_BATCHES; @@ -128,7 +128,7 @@ export interface DevServerTest { */ mainDir?: string; - skip?: ('win32'|'darwin'|'linux'|'ci')[], + skip?: ("win32" | "darwin" | "linux" | "ci")[]; } let interactive = false; @@ -310,14 +310,11 @@ export class Dev extends EventEmitter { const wait = this.waitForHotReload(wantsHmrEvent); const b = { write: resetSeenFilesWithResolvers, - [Symbol.asyncDispose]: async() => { + [Symbol.asyncDispose]: async () => { if (wantsHmrEvent && interactive) { await seenFiles.promise; } else if (wantsHmrEvent) { - await Promise.race([ - seenFiles.promise, - Bun.sleep(1000), - ]); + await Promise.race([seenFiles.promise, Bun.sleep(1000)]); } if (!fastBatches) { // Wait an extra delay to avoid double-triggering events. @@ -348,10 +345,12 @@ export class Dev extends EventEmitter { return withAnnotatedStack(snapshot, async () => { await maybeWaitInteractive("write " + file); const isDev = this.nodeEnv === "development"; - await using _wait = isDev ? await this.batchChanges({ - errors: options.errors, - snapshot: snapshot, - }) : null; + await using _wait = isDev + ? await this.batchChanges({ + errors: options.errors, + snapshot: snapshot, + }) + : null; await Bun.write( this.join(file), @@ -384,10 +383,12 @@ export class Dev extends EventEmitter { return withAnnotatedStack(snapshot, async () => { await maybeWaitInteractive("delete " + file); const isDev = this.nodeEnv === "development"; - await using _wait = isDev ? await this.batchChanges({ - errors: options.errors, - snapshot: snapshot, - }) : null; + await using _wait = isDev + ? await this.batchChanges({ + errors: options.errors, + snapshot: snapshot, + }) + : null; const filePath = this.join(file); if (!fs.existsSync(filePath)) { @@ -411,10 +412,12 @@ export class Dev extends EventEmitter { return withAnnotatedStack(snapshot, async () => { await maybeWaitInteractive("patch " + file); const isDev = this.nodeEnv === "development"; - await using _wait = isDev ? await this.batchChanges({ - errors: errors, - snapshot: snapshot, - }) : null; + await using _wait = isDev + ? await this.batchChanges({ + errors: errors, + snapshot: snapshot, + }) + : null; const filename = this.join(file); const source = fs.readFileSync(filename, "utf8"); @@ -477,11 +480,13 @@ export class Dev extends EventEmitter { if (wantsHmrEvent) { await Bun.sleep(500); if (seenMainEvent) return; - console.warn("\x1b[33mWARN: Dev Server did not pick up any changed files. Consider wrapping this call in expectNoWebSocketActivity\x1b[35m"); + console.warn( + "\x1b[33mWARN: Dev Server did not pick up any changed files. Consider wrapping this call in expectNoWebSocketActivity\x1b[35m", + ); } cleanupAndResolve(); } - }; + } dev.on("watch_synchronization", onEvent); }); } @@ -552,10 +557,10 @@ export class Dev extends EventEmitter { * Run a stress test. The function should perform I/O in a loop, for about a * couple of seconds. In CI, this round is run once. In development, this can * be run forever using `DEV_SERVER_STRESS=FILTER`. - * + * * Tests using this should go in `stress.test.ts` */ - async stressTest(round: () => (Promise | void)) { + async stressTest(round: () => Promise | void) { if (!this.stressTestEndurance) { await round(); await Bun.sleep(250); @@ -567,16 +572,18 @@ export class Dev extends EventEmitter { const endTime = Date.now() + 10 * 60 * 1000; let iteration = 0; - - using log = new TrailingLog; + + using log = new TrailingLog(); while (Date.now() < endTime) { const timeRemaining = endTime - Date.now(); const minutes = Math.floor(timeRemaining / 60000); const seconds = Math.floor((timeRemaining % 60000) / 1000); - log.setMessage(`[STRESS] Time remaining: ${minutes}:${seconds.toString().padStart(2, '0')}. Iteration ${++iteration}`); + log.setMessage( + `[STRESS] Time remaining: ${minutes}:${seconds.toString().padStart(2, "0")}. Iteration ${++iteration}`, + ); await round(); - + if (this.output.panicked) { throw new Error("DevServer panicked in stress test"); } @@ -749,7 +756,10 @@ export class Client extends EventEmitter { hmr = false; webSocketMessagesAllowed = true; - constructor(url: string, options: { storeHotChunks?: boolean; hmr: boolean; expectErrors?: boolean; allowUnlimitedReloads?: boolean, }) { + constructor( + url: string, + options: { storeHotChunks?: boolean; hmr: boolean; expectErrors?: boolean; allowUnlimitedReloads?: boolean }, + ) { super(); activeClient = this; const proc = Bun.spawn({ @@ -822,6 +832,15 @@ export class Client extends EventEmitter { }); } + elemsText(selector: string): Promise { + return withAnnotatedStack(snapshotCallerLocation(), async () => { + const elems = await this.js< + string[] + >`Array.from(document.querySelectorAll(${selector})).map(elem => elem.innerHTML)`; + return elems; + }); + } + async [Symbol.asyncDispose]() { if (activeClient === this) { activeClient = null; @@ -946,7 +965,14 @@ export class Client extends EventEmitter { expectErrorOverlay(errors: ErrorSpec[], caller: string | null = null) { return withAnnotatedStack(caller ?? snapshotCallerLocationMayFail(), async () => { this.suppressInteractivePrompt = true; - const hasVisibleModal = await this.js`document.querySelector("bun-hmr")?.style.display === "block"`; + let retries = 0; + let hasVisibleModal = false; + while (retries < 5) { + hasVisibleModal = await this.js`document.querySelector("bun-hmr")?.style.display === "block"`; + if (hasVisibleModal) break; + await Bun.sleep(200); + retries++; + } this.suppressInteractivePrompt = false; if (errors && errors.length > 0) { if (!hasVisibleModal) { @@ -1333,6 +1359,12 @@ if (!fs.existsSync(tempDir)) { fs.mkdirSync(tempDir, { recursive: true }); } +// Create a cache directory for React dependencies +const reactCacheDir = path.join(tempDir, ".react-cache"); +if (!fs.existsSync(reactCacheDir)) { + fs.mkdirSync(reactCacheDir, { recursive: true }); +} + function cleanTestDir(dir: string) { if (!fs.existsSync(dir)) return; const files = fs.readdirSync(dir); @@ -1342,6 +1374,43 @@ function cleanTestDir(dir: string) { } } +async function installReactWithCache(root: string) { + const cacheFiles = ["node_modules", "package.json", "bun.lock"]; + const cacheValid = cacheFiles.every(file => fs.existsSync(path.join(reactCacheDir, file))); + + if (cacheValid) { + // Copy from cache + for (const file of cacheFiles) { + const src = path.join(reactCacheDir, file); + const dest = path.join(root, file); + if (fs.statSync(src).isDirectory()) { + fs.cpSync(src, dest, { recursive: true }); + } else { + fs.copyFileSync(src, dest); + } + } + } else { + // Install fresh and populate cache + await Bun.$`${bunExe()} i react@experimental react-dom@experimental react-server-dom-bun react-refresh@experimental && ${bunExe()} install` + .cwd(root) + .env({ ...bunEnv }) + .throws(true); + + // Copy to cache for future use + for (const file of cacheFiles) { + const src = path.join(root, file); + const dest = path.join(reactCacheDir, file); + if (fs.existsSync(src)) { + if (fs.statSync(src).isDirectory()) { + fs.cpSync(src, dest, { recursive: true, force: true }); + } else { + fs.copyFileSync(src, dest); + } + } + } + } +} + const devTestRoot = path.join(import.meta.dir, "dev").replaceAll("\\", "/"); const prodTestRoot = path.join(import.meta.dir, "dev").replaceAll("\\", "/"); const counts: Record = {}; @@ -1495,7 +1564,7 @@ class OutputLineStream extends EventEmitter { export function indexHtmlScript(htmlFiles: string[]) { return [ - ...htmlFiles.map((file, i) => `import html${i} from ${JSON.stringify("./" + file.replaceAll(path.sep, '/'))};`), + ...htmlFiles.map((file, i) => `import html${i} from ${JSON.stringify("./" + file.replaceAll(path.sep, "/"))};`), "export default {", " static: {", ...(htmlFiles.length === 1 @@ -1518,10 +1587,7 @@ export function indexHtmlScript(htmlFiles: string[]) { ].join("\n"); } -const skipTargets = [ - process.platform, - isCI ? 'ci' : null, -].filter(Boolean); +const skipTargets = [process.platform, isCI ? "ci" : null].filter(Boolean); function testImpl( description: string, @@ -1560,6 +1626,10 @@ function testImpl( x => path.join(root, x), ); await writeAll(root, options.files); + const runInstall = options.framework === "react"; + if (runInstall) { + await installReactWithCache(root); + } if (options.files["bun.app.ts"] == undefined && htmlFiles.length === 0) { if (!options.framework) { throw new Error("Must specify one of: `options.framework`, `*.html`, or `bun.app.ts`"); @@ -1749,8 +1819,8 @@ function testImpl( isStressTest ? 11 * 60 * 1000 : interactive - ? interactive_timeout - : (options.timeoutMultiplier ?? 1) * (isWindows ? 15_000 : 10_000) * (Bun.version.includes("debug") ? 2 : 1), + ? interactive_timeout + : (options.timeoutMultiplier ?? 1) * (isWindows ? 15_000 : 10_000) * (Bun.version.includes("debug") ? 2 : 1), ); return options; } catch { @@ -1806,20 +1876,20 @@ class TrailingLog { } #wrapLog(method: keyof Console) { - const m: Function = this.realConsole[method] = console[method]; + const m: Function = (this.realConsole[method] = console[method]); return (...args: any[]) => { if (this.lines > 0) { - process.stderr.write('\u001B[?2026h' + this.#clear()); + process.stderr.write("\u001B[?2026h" + this.#clear()); this.realConsole[method](...args); - process.stderr.write(this.message + '\u001B[?2026l'); + process.stderr.write(this.message + "\u001B[?2026l"); } else { m.apply(console, args); } - } + }; } #clear() { - return '\x1b[2K' + ("\x1b[1A\x1b[2K").repeat(this.lines) + '\r'; + return "\x1b[2K" + "\x1b[1A\x1b[2K".repeat(this.lines) + "\r"; } [Symbol.dispose] = () => { @@ -1832,12 +1902,12 @@ class TrailingLog { console.warn = this.realConsole.warn; console.info = this.realConsole.info; console.debug = this.realConsole.debug; - } + }; setMessage(message: string) { this.message = message.trim() + "\n"; this.lines = this.message.split("\n").length - 1; - process.stderr.write('\u001B[?2026h' + this.#clear() + this.message + '\u001B[?2026l'); + process.stderr.write("\u001B[?2026h" + this.#clear() + this.message + "\u001B[?2026l"); } } diff --git a/test/bake/client-fixture.mjs b/test/bake/client-fixture.mjs index d5bb42beb0..39f3968670 100644 --- a/test/bake/client-fixture.mjs +++ b/test/bake/client-fixture.mjs @@ -69,11 +69,12 @@ function createWindow(windowUrl) { window.internal = internal; }; + const original_window_fetch = window.fetch; window.fetch = async function (url, options) { if (typeof url === "string") { url = new URL(url, windowUrl).href; } - return fetch(url, options); + return await original_window_fetch(url, options); }; // Provide WebSocket diff --git a/test/bake/dev/bundle.test.ts b/test/bake/dev/bundle.test.ts index 2e1a4f59c4..075ade9c32 100644 --- a/test/bake/dev/bundle.test.ts +++ b/test/bake/dev/bundle.test.ts @@ -352,6 +352,7 @@ devTest("import.meta.main", { }, }); devTest("commonjs forms", { + timeoutMultiplier: 2, files: { "index.html": emptyHtmlFile({ styles: [], @@ -366,32 +367,50 @@ devTest("commonjs forms", { `, }, async test(dev) { + console.log("Initial"); await using c = await dev.client("/"); + console.log(" expecting message"); await c.expectMessage({ field: {} }); + console.log(" expecting reload"); await c.expectReload(async () => { + console.log(" writing"); await dev.write("cjs.js", `exports.field = "1";`); + console.log(" now reloading"); }); + console.log(" expecting message"); await c.expectMessage({ field: "1" }); + console.log("Second"); + console.log(" expecting reload"); await c.expectReload(async () => { + console.log(" writing"); await dev.write("cjs.js", `let theExports = exports; theExports.field = "2";`); }); + console.log(" expecting message"); await c.expectMessage({ field: "2" }); + console.log("Third"); + console.log(" expecting reload"); await c.expectReload(async () => { + console.log(" writing"); await dev.write("cjs.js", `let theModule = module; theModule.exports.field = "3";`); }); + console.log(" expecting message"); await c.expectMessage({ field: "3" }); + console.log("Fourth"); await c.expectReload(async () => { await dev.write("cjs.js", `let { exports } = module; exports.field = "4";`); }); await c.expectMessage({ field: "4" }); + console.log("Fifth"); await c.expectReload(async () => { await dev.write("cjs.js", `var { exports } = module; exports.field = "4.5";`); }); await c.expectMessage({ field: "4.5" }); + console.log("Sixth"); await c.expectReload(async () => { await dev.write("cjs.js", `let theExports = module.exports; theExports.field = "5";`); }); await c.expectMessage({ field: "5" }); + console.log("Seventh"); await c.expectReload(async () => { await dev.write("cjs.js", `require; eval("module.exports.field = '6'");`); }); diff --git a/test/bake/dev/ssg-pages-router.test.ts b/test/bake/dev/ssg-pages-router.test.ts new file mode 100644 index 0000000000..af60b9e88b --- /dev/null +++ b/test/bake/dev/ssg-pages-router.test.ts @@ -0,0 +1,295 @@ +// Test SSG pages router functionality +import { expect } from "bun:test"; +import { devTest } from "../bake-harness"; + +devTest("SSG pages router - multiple static pages", { + framework: "react", + files: { + "pages/about.tsx": ` + export default function AboutPage() { + return

About Page

; + } + `, + "pages/contact.tsx": ` + export default function ContactPage() { + return

Contact Page

; + } + `, + }, + async test(dev) { + // Test about page + await using c2 = await dev.client("/about"); + expect(await c2.elemText("h1")).toBe("About Page"); + + // Test contact page + await using c3 = await dev.client("/contact"); + expect(await c3.elemText("h1")).toBe("Contact Page"); + }, +}); + +devTest("SSG pages router - dynamic routes with [slug]", { + framework: "react", + files: { + "pages/[slug].tsx": ` + type Props = Bun.SSGProps; + + const Page: Bun.SSGPage = async ({ params }) => { + return ( +
+

Dynamic Page: {params.slug}

+

Slug value: {params.slug}

+
+ ); + }; + + export default Page; + + export const getStaticPaths: Bun.GetStaticPaths = async () => { + return { + paths: [ + { params: { slug: "first-post" } }, + { params: { slug: "second-post" } }, + { params: { slug: "third-post" } }, + ], + }; + }; + `, + }, + async test(dev) { + // Test dynamic routes + await using c1 = await dev.client("/first-post"); + expect(await c1.elemText("h1")).toBe("Dynamic Page: first-post"); + expect(await c1.elemText("p")).toBe("Slug value: first-post"); + + await using c2 = await dev.client("/second-post"); + expect(await c2.elemText("h1")).toBe("Dynamic Page: second-post"); + + await using c3 = await dev.client("/third-post"); + expect(await c3.elemText("h1")).toBe("Dynamic Page: third-post"); + }, +}); + +devTest("SSG pages router - nested routes", { + framework: "react", + files: { + "pages/blog/index.tsx": ` + export default function BlogIndex() { + return

Blog Index

; + } + `, + "pages/blog/[id].tsx": ` + const BlogPost: Bun.SSGPage = ({ params }) => { + return

Blog Post {params.id}

; + }; + + export default BlogPost; + + export const getStaticPaths: Bun.GetStaticPaths = async () => { + return { + paths: [ + { params: { id: "1" } }, + { params: { id: "2" } }, + ], + }; + }; + `, + "pages/blog/categories/[category].tsx": ` + const CategoryPage: Bun.SSGPage = ({ params }) => { + return

Category: {params.category}

; + }; + + export default CategoryPage; + + export const getStaticPaths: Bun.GetStaticPaths = async () => { + return { + paths: [ + { params: { category: "tech" } }, + { params: { category: "lifestyle" } }, + ], + }; + }; + `, + }, + async test(dev) { + // Test blog index + await using c1 = await dev.client("/blog"); + expect(await c1.elemText("h1")).toBe("Blog Index"); + + // Test blog posts + await using c2 = await dev.client("/blog/1"); + expect(await c2.elemText("h1")).toBe("Blog Post 1"); + + await using c3 = await dev.client("/blog/2"); + expect(await c3.elemText("h1")).toBe("Blog Post 2"); + + // Test categories + await using c4 = await dev.client("/blog/categories/tech"); + expect(await c4.elemText("h1")).toBe("Category: tech"); + + await using c5 = await dev.client("/blog/categories/lifestyle"); + expect(await c5.elemText("h1")).toBe("Category: lifestyle"); + }, +}); + +devTest("SSG pages router - hot reload on page changes", { + framework: "react", + files: { + "pages/index.tsx": ` + export default function IndexPage() { + return

Welcome to SSG

; + } + `, + }, + async test(dev) { + await using c = await dev.client("/"); + expect(await c.elemText("h1")).toBe("Welcome to SSG"); + + // Update the page + await dev.write( + "pages/index.tsx", + ` + export default function IndexPage() { + console.log("updated load"); + return

Updated Content

; + } + `, + ); + + // this %c%s%c is a react devtools thing and I don't know how to turn it off + await c.expectMessage("%c%s%c updated load"); + expect(await c.elemText("h1")).toBe("Updated Content"); + }, +}); + +devTest("SSG pages router - data fetching with async components", { + framework: "react", + files: { + "pages/data.tsx": ` + async function fetchData() { + // Simulate API call + return new Promise(resolve => { + setTimeout(() => { + resolve({ message: "Data from API", items: ["Item 1", "Item 2", "Item 3"] }); + }, 10); + }); + } + + export default async function DataPage() { + const data = await fetchData(); + + return ( +
+

{data.message}

+
    + {data.items.map((item, index) => ( +
  • {item}
  • + ))} +
+
+ ); + } + `, + }, + async test(dev) { + await using c = await dev.client("/data"); + expect(await c.elemText("h1")).toBe("Data from API"); + + const items = await c.elemsText("li"); + expect(items).toEqual(["Item 1", "Item 2", "Item 3"]); + }, +}); + +devTest("SSG pages router - multiple dynamic segments", { + framework: "react", + files: { + "pages/[category]/[year]/[slug].tsx": ` + const ArticlePage: Bun.SSGPage = ({ params }) => { + return ( +
+

{params.slug}

+

Category: {params.category}

+

Year: {params.year}

+
+ ); + }; + + export default ArticlePage; + + export const getStaticPaths: Bun.GetStaticPaths = async () => { + return { + paths: [ + { params: { category: "tech", year: "2024", slug: "bun-release" } }, + { params: { category: "news", year: "2024", slug: "breaking-story" } }, + { params: { category: "tech", year: "2023", slug: "year-review" } }, + ], + }; + }; + `, + }, + async test(dev) { + // Test first path + await using c1 = await dev.client("/tech/2024/bun-release"); + expect(await c1.elemText("h1")).toBe("bun-release"); + expect(await c1.elemsText("p")).toEqual(["Category: tech", "Year: 2024"]); + + // Test second path + await using c2 = await dev.client("/news/2024/breaking-story"); + expect(await c2.elemText("h1")).toBe("breaking-story"); + expect(await c2.elemsText("p")).toEqual(["Category: news", "Year: 2024"]); + + // Test third path + await using c3 = await dev.client("/tech/2023/year-review"); + expect(await c3.elemText("h1")).toBe("year-review"); + expect(await c3.elemsText("p")).toEqual(["Category: tech", "Year: 2023"]); + }, +}); + +devTest("SSG pages router - file loading with Bun.file", { + framework: "react", + fixture: "ssg-pages-router", + files: { + "pages/[slug].tsx": ` + import { join } from "path"; + + const PostPage: Bun.SSGPage = async ({ params }) => { + const content = await Bun.file( + join(process.cwd(), "posts", params.slug + ".txt") + ).text(); + + return ( +
+

{params.slug}

+
{content}
+
+ ); + }; + + export default PostPage; + + export const getStaticPaths: Bun.GetStaticPaths = async () => { + const glob = new Bun.Glob("**/*.txt"); + const paths = []; + + for (const file of Array.from(glob.scanSync({ cwd: join(process.cwd(), "posts") }))) { + const slug = file.replace(/\\.txt$/, ""); + paths.push({ params: { slug } }); + } + + return { paths }; + }; + `, + "posts/hello-world.txt": "This is the content of hello world post", + "posts/second-post.txt": "This is the second post content", + }, + async test(dev) { + // Test first post + await using c1 = await dev.client("/hello-world"); + expect(await c1.elemText("h1")).toBe("hello-world"); + expect(await c1.elemText("div div")).toBe("This is the content of hello world post"); + + // Test second post + await using c2 = await dev.client("/second-post"); + expect(await c2.elemText("h1")).toBe("second-post"); + expect(await c2.elemText("div div")).toBe("This is the second post content"); + }, +}); diff --git a/test/bake/dev/vfile.test.ts b/test/bake/dev/vfile.test.ts new file mode 100644 index 0000000000..beed2583d8 --- /dev/null +++ b/test/bake/dev/vfile.test.ts @@ -0,0 +1,65 @@ +import { describe, expect } from "bun:test"; +import { devTest, minimalFramework } from "../bake-harness"; + +/** + * Enure that node builtins imported on the server behave properly + */ +describe("node builtin test", () => { + /** + * + * This creates a minimal reproduction of an issue when VFile was imported on the dev server. + * + * The issue was that it was importing node:process and this was not correctly handled + */ + devTest("vfile import in server component", { + framework: minimalFramework, + files: { + "node_modules/vfile/package.json": JSON.stringify({ + name: "vfile", + version: "6.0.3", + type: "module", + exports: { + ".": "./lib/index.js", + }, + }), + "node_modules/vfile/lib/process.js": ` + export { default as minproc } from 'process'; + `, + "node_modules/vfile/lib/index.js": ` + // Minimal VFile implementation for testing + import { minproc } from './process.js'; + + export class VFile { + constructor(value) { + this.value = value; + this.data = {}; + this.messages = []; + this.history = []; + this.cwd = minproc.cwd(); + } + } + `, + "routes/test.ts": ` + import { VFile } from "vfile"; + + export default function (req, meta) { + const foo = new VFile("hello world"); + console.log(foo.value); + + return new Response(\`VFile content: \${foo.value}\`, { + headers: { "Content-Type": "text/plain" } + }); + } + `, + }, + async test(dev) { + // Test that the dev server can bundle the page without errors + const response = await dev.fetch("/test"); + expect(response.status).toBe(200); + + // Check that VFile is properly bundled and works + const text = await response.text(); + expect(text).toBe("VFile content: hello world"); + }, + }); +}); diff --git a/test/integration/bun-types/fixture/[slug].tsx b/test/integration/bun-types/fixture/[slug].tsx new file mode 100644 index 0000000000..02ebd5fd7f --- /dev/null +++ b/test/integration/bun-types/fixture/[slug].tsx @@ -0,0 +1,38 @@ +import { join } from "path"; +import { expectType } from "./utilities"; + +// we're just checking types here really +declare function markdownToJSX(markdown: string): React.ReactNode; + +type Params = { + slug: string; +}; + +const Index: Bun.__experimental.SSGPage = async ({ params }) => { + expectType(params.slug).is(); + + const content = await Bun.file(join(process.cwd(), "posts", params.slug + ".md")).text(); + const node = markdownToJSX(content); + + return
{node}
; +}; + +expectType(Index.displayName).is(); + +export default Index; + +export const getStaticPaths: Bun.__experimental.GetStaticPaths = async () => { + const glob = new Bun.Glob("**/*.md"); + const postsDir = join(process.cwd(), "posts"); + const paths: Bun.__experimental.SSGPaths = []; + + for (const file of glob.scanSync({ cwd: postsDir })) { + const slug = file.replace(/\.md$/, ""); + + paths.push({ + params: { slug }, + }); + } + + return { paths }; +}; diff --git a/test/integration/bun-types/fixture/utilities.ts b/test/integration/bun-types/fixture/utilities.ts index 4a81e3acdf..ba282c6c60 100644 --- a/test/integration/bun-types/fixture/utilities.ts +++ b/test/integration/bun-types/fixture/utilities.ts @@ -26,7 +26,7 @@ export function expectType(arg: T): { * expectType(my_Uint8Array).is(); // pass * ``` */ - is(...args: IfEquals extends true ? [] : [expected: X, butGot: T]): void; + is(...args: IfEquals extends true ? [] : [expected: X, but_got: T]): void; }; export function expectType(arg?: T) { return { is() {} }; diff --git a/test/no-validate-exceptions.txt b/test/no-validate-exceptions.txt index f12b2b3940..b89e668567 100644 --- a/test/no-validate-exceptions.txt +++ b/test/no-validate-exceptions.txt @@ -1,6 +1,8 @@ # List of tests for which we do NOT set validateExceptionChecks=1 when running in ASan CI test/bake/dev-and-prod.test.ts test/bake/dev/plugins.test.ts +test/bake/dev/vfile.test.ts +test/bake/dev/ssg-pages-router.test.ts test/bake/framework-router.test.ts test/bundler/bun-build-api.test.ts test/bundler/bundler_banner.test.ts