diff --git a/.vscode/launch.json b/.vscode/launch.json index 00f72d4ddf..dc019a5445 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -1257,4 +1257,4 @@ "description": "Usage: bun test [...]", }, ], -} +} \ No newline at end of file diff --git a/src/bake/BakeGlobalObject.cpp b/src/bake/BakeGlobalObject.cpp index 44ee0e1854..4bd4a4647e 100644 --- a/src/bake/BakeGlobalObject.cpp +++ b/src/bake/BakeGlobalObject.cpp @@ -1,10 +1,12 @@ #include "BakeGlobalObject.h" +#include "BakeSourceProvider.h" #include "JSNextTickQueue.h" #include "JavaScriptCore/GlobalObjectMethodTable.h" #include "JavaScriptCore/JSInternalPromise.h" #include "headers-handwritten.h" #include "JavaScriptCore/JSModuleLoader.h" #include "JavaScriptCore/Completion.h" +#include "JavaScriptCore/JSSourceCode.h" extern "C" BunString BakeProdResolve(JSC::JSGlobalObject*, BunString a, BunString b); @@ -72,6 +74,58 @@ JSC::Identifier bakeModuleLoaderResolve(JSC::JSGlobalObject* jsGlobal, return Zig::GlobalObject::moduleLoaderResolve(jsGlobal, loader, key, referrer, origin); } +static JSC::JSInternalPromise* rejectedInternalPromise(JSC::JSGlobalObject* globalObject, JSC::JSValue value) +{ + JSC::VM& vm = globalObject->vm(); + JSC::JSInternalPromise* promise = JSC::JSInternalPromise::create(vm, globalObject->internalPromiseStructure()); + promise->internalField(JSC::JSPromise::Field::ReactionsOrResult).set(vm, promise, value); + promise->internalField(JSC::JSPromise::Field::Flags).set(vm, promise, JSC::jsNumber(promise->internalField(JSC::JSPromise::Field::Flags).get().asUInt32AsAnyInt() | JSC::JSPromise::isFirstResolvingFunctionCalledFlag | static_cast(JSC::JSPromise::Status::Rejected))); + return promise; +} + +static JSC::JSInternalPromise* resolvedInternalPromise(JSC::JSGlobalObject* globalObject, JSC::JSValue value) +{ + JSC::VM& vm = globalObject->vm(); + JSC::JSInternalPromise* promise = JSC::JSInternalPromise::create(vm, globalObject->internalPromiseStructure()); + promise->internalField(JSC::JSPromise::Field::ReactionsOrResult).set(vm, promise, value); + promise->internalField(JSC::JSPromise::Field::Flags).set(vm, promise, JSC::jsNumber(promise->internalField(JSC::JSPromise::Field::Flags).get().asUInt32AsAnyInt() | JSC::JSPromise::isFirstResolvingFunctionCalledFlag | static_cast(JSC::JSPromise::Status::Fulfilled))); + return promise; +} + +extern "C" BunString BakeProdLoad(ProductionPerThread* perThreadData, BunString a); + +JSC::JSInternalPromise* bakeModuleLoaderFetch(JSC::JSGlobalObject* globalObject, + JSC::JSModuleLoader* loader, JSC::JSValue key, + JSC::JSValue parameters, JSC::JSValue script) +{ + Bake::GlobalObject* global = jsCast(globalObject); + JSC::VM& vm = globalObject->vm(); + auto scope = DECLARE_THROW_SCOPE(vm); + auto moduleKey = key.toWTFString(globalObject); + if (UNLIKELY(scope.exception())) + return rejectedInternalPromise(globalObject, scope.exception()->value()); + + if (moduleKey.startsWith("bake:/"_s)) { + if (LIKELY(global->m_perThreadData)) { + BunString source = BakeProdLoad(global->m_perThreadData, Bun::toString(moduleKey)); + if (source.tag != BunStringTag::Dead) { + JSC::SourceOrigin origin = JSC::SourceOrigin(WTF::URL(moduleKey)); + JSC::SourceCode sourceCode = JSC::SourceCode(Bake::SourceProvider::create( + source.toWTFString(), + origin, + WTFMove(moduleKey), + WTF::TextPosition(), + JSC::SourceProviderSourceType::Module)); + return resolvedInternalPromise(globalObject, JSC::JSSourceCode::create(vm, WTFMove(sourceCode))); + } + return rejectedInternalPromise(globalObject, createTypeError(globalObject, makeString("Bundle does not have \""_s, moduleKey, "\". This is a bug in Bun's bundler."_s))); + } + return rejectedInternalPromise(globalObject, createTypeError(globalObject, "BakeGlobalObject does not have per-thread data configured"_s)); + } + + return Zig::GlobalObject::moduleLoaderFetch(globalObject, loader, key, parameters, script); +} + #define INHERIT_HOOK_METHOD(name) \ Zig::GlobalObject::s_globalObjectMethodTable.name @@ -83,7 +137,7 @@ const JSC::GlobalObjectMethodTable GlobalObject::s_globalObjectMethodTable = { INHERIT_HOOK_METHOD(shouldInterruptScriptBeforeTimeout), bakeModuleLoaderImportModule, bakeModuleLoaderResolve, - INHERIT_HOOK_METHOD(moduleLoaderFetch), + bakeModuleLoaderFetch, INHERIT_HOOK_METHOD(moduleLoaderCreateImportMetaProperties), INHERIT_HOOK_METHOD(moduleLoaderEvaluate), INHERIT_HOOK_METHOD(promiseRejectionTracker), @@ -155,4 +209,9 @@ extern "C" GlobalObject* BakeCreateProdGlobal(void* console) return global; } +extern "C" void BakeGlobalObject__attachPerThreadData(GlobalObject* global, ProductionPerThread* perThreadData) +{ + global->m_perThreadData = perThreadData; +} + }; // namespace Bake diff --git a/src/bake/BakeGlobalObject.h b/src/bake/BakeGlobalObject.h index af2b3490f9..0ac902422e 100644 --- a/src/bake/BakeGlobalObject.h +++ b/src/bake/BakeGlobalObject.h @@ -4,10 +4,14 @@ namespace Bake { +struct ProductionPerThread; + class GlobalObject : public Zig::GlobalObject { public: using Base = Zig::GlobalObject; + ProductionPerThread* m_perThreadData; + template static JSC::GCClient::IsoSubspace* subspaceFor(JSC::VM& vm) { if constexpr (mode == JSC::SubspaceAccess::Concurrently) diff --git a/src/bake/BakeSourceProvider.cpp b/src/bake/BakeSourceProvider.cpp index cf7ef839ab..2a51ffa0aa 100644 --- a/src/bake/BakeSourceProvider.cpp +++ b/src/bake/BakeSourceProvider.cpp @@ -21,7 +21,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(DevSourceProvider::create( + JSC::SourceCode sourceCode = JSC::SourceCode(SourceProvider::create( source.toWTFString(), origin, WTFMove(string), @@ -54,7 +54,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(DevSourceProvider::create( + JSC::SourceCode sourceCode = JSC::SourceCode(SourceProvider::create( source.toWTFString(), origin, WTFMove(string), @@ -117,7 +117,7 @@ extern "C" JSC::EncodedJSValue BakeRegisterProductionChunk(JSC::JSGlobalObject* String string = virtualPathName.toWTFString(); JSC::JSString* key = JSC::jsString(vm, string); JSC::SourceOrigin origin = JSC::SourceOrigin(WTF::URL(string)); - JSC::SourceCode sourceCode = JSC::SourceCode(DevSourceProvider::create( + JSC::SourceCode sourceCode = JSC::SourceCode(SourceProvider::create( source.toWTFString(), origin, WTFMove(string), diff --git a/src/bake/BakeSourceProvider.h b/src/bake/BakeSourceProvider.h index 2d821fc401..3a3706af85 100644 --- a/src/bake/BakeSourceProvider.h +++ b/src/bake/BakeSourceProvider.h @@ -6,20 +6,20 @@ namespace Bake { -class DevSourceProvider final : public JSC::StringSourceProvider { +class SourceProvider final : public JSC::StringSourceProvider { public: - static Ref create( + static Ref create( const String& source, const JSC::SourceOrigin& sourceOrigin, String&& sourceURL, const TextPosition& startPosition, JSC::SourceProviderSourceType sourceType ) { - return adoptRef(*new DevSourceProvider(source, sourceOrigin, WTFMove(sourceURL), startPosition, sourceType)); + return adoptRef(*new SourceProvider(source, sourceOrigin, WTFMove(sourceURL), startPosition, sourceType)); } private: - DevSourceProvider( + SourceProvider( const String& source, const JSC::SourceOrigin& sourceOrigin, String&& sourceURL, diff --git a/src/bake/DevServer.zig b/src/bake/DevServer.zig index efabe71838..cae853a63d 100644 --- a/src/bake/DevServer.zig +++ b/src/bake/DevServer.zig @@ -20,7 +20,7 @@ pub const Options = struct { // Debugging features dump_sources: ?[]const u8 = if (Environment.isDebug) ".bake-debug" else null, - dump_state_on_crash: ?bool = false, + dump_state_on_crash: ?bool = null, verbose_watcher: bool = false, }; @@ -904,6 +904,7 @@ fn startAsyncBundle( .framework = dev.framework, .client_bundler = &dev.client_bundler, .ssr_bundler = &dev.ssr_bundler, + .plugins = dev.bundler_options.plugin, } else @panic("TODO: support non-server components"), allocator, .{ .js = dev.vm.eventLoop() }, @@ -912,7 +913,6 @@ fn startAsyncBundle( heap, ); bv2.bun_watcher = dev.bun_watcher; - bv2.plugins = dev.bundler_options.plugin; bv2.asynchronous = true; { @@ -4352,7 +4352,7 @@ fn dumpStateDueToCrash(dev: *DevServer) !void { const filepath = std.fmt.bufPrintZ(&filepath_buf, "incremental-graph-crash-dump.{d}.html", .{std.time.timestamp()}) catch "incremental-graph-crash-dump.html"; const file = std.fs.cwd().createFileZ(filepath, .{}) catch |err| { bun.handleErrorReturnTrace(err, @errorReturnTrace()); - Output.warn("Could not open directory for dumping sources: {}", .{err}); + Output.warn("Could not open file for dumping incremental graph: {}", .{err}); return; }; defer file.close(); diff --git a/src/bake/bake.d.ts b/src/bake/bake.d.ts index 6836ec63ad..3e5a6f9ad0 100644 --- a/src/bake/bake.d.ts +++ b/src/bake/bake.d.ts @@ -421,7 +421,7 @@ declare module "bun" { type GetParamIterator = | AsyncIterable, GetParamsFinalOpts> | Iterable, GetParamsFinalOpts> - | ({ pages: Array> } & GetParamsFinalOpts); + | ({ pages: Array> } & GetParamsFinalOpts); type GetParamsFinalOpts = void | null | { /** @@ -516,7 +516,7 @@ declare module "bun" { * Inject a module into the development server's runtime, to be loaded * before all other user code. */ - addPreload(module: string, side: 'client' | 'server'): void; + addPreload(...args: any): void; } declare interface OnLoadArgs { diff --git a/src/bake/bake.zig b/src/bake/bake.zig index 7159b6d3e0..134acce3d9 100644 --- a/src/bake/bake.zig +++ b/src/bake/bake.zig @@ -466,7 +466,7 @@ pub const Framework = struct { } else if (exts_js.isArray()) { var it_2 = exts_js.arrayIterator(global); var i_2: usize = 0; - const extensions = try arena.alloc([]const u8, array.getLength(global)); + const extensions = try arena.alloc([]const u8, exts_js.getLength(global)); while (it_2.next()) |array_item| : (i_2 += 1) { const slice = refs.track(try array_item.toSlice2(global, arena)); if (bun.strings.eqlComptime(slice, "*")) @@ -600,9 +600,13 @@ pub const Framework = struct { out.options.framework = framework; - // In development mode, source maps must always be `linked` - // In production, TODO: follow user configuration - out.options.source_map = .linked; + out.options.source_map = switch (mode) { + // Source maps must always be linked, as DevServer special cases the + // linking and part of the generation of these. + .development => .external, + // TODO: follow user configuration + else => .none, + }; out.configureLinker(); try out.configureDefines(); @@ -615,8 +619,10 @@ pub const Framework = struct { }); if (mode != .development) { - out.options.entry_naming = "[name]-[hash].[ext]"; - out.options.chunk_naming = "chunk-[name]-[hash].[ext]"; + // Hide information about the source repository, at the cost of debugging quality. + out.options.entry_naming = "_bun/[hash].[ext]"; + out.options.chunk_naming = "_bun/[hash].[ext]"; + out.options.asset_naming = "_bun/[hash].[ext]"; } out.resolver.opts = out.options; diff --git a/src/bake/production.zig b/src/bake/production.zig index a8fadb8d59..edeedd1de1 100644 --- a/src/bake/production.zig +++ b/src/bake/production.zig @@ -85,7 +85,8 @@ pub fn buildCommand(ctx: bun.CLI.Command.Context) !void { buildWithVm(ctx, cwd, vm) catch |err| switch (err) { error.JSError => |e| { bun.handleErrorReturnTrace(err, @errorReturnTrace()); - vm.printErrorLikeObjectToConsole(vm.global.takeException(e)); + const err_value = vm.global.takeException(e); + vm.printErrorLikeObjectToConsole(err_value.toError() orelse err_value); if (vm.exit_handler.exit_code == 0) { vm.exit_handler.exit_code = 1; } @@ -103,7 +104,10 @@ pub fn buildWithVm(ctx: bun.CLI.Command.Context, cwd: []const u8, vm: *VirtualMa Output.prettyErrorln("Loading configuration", .{}); Output.flush(); - const unresolved_config_entry_point = if (ctx.args.entry_points.len > 0) ctx.args.entry_points[0] else "./bun.app"; + var unresolved_config_entry_point = if (ctx.args.entry_points.len > 0) ctx.args.entry_points[0] else "./bun.app"; + if (bun.resolver.isPackagePath(unresolved_config_entry_point)) { + unresolved_config_entry_point = try std.fmt.allocPrint(ctx.allocator, "./{s}", .{unresolved_config_entry_point}); + } const config_entry_point = b.resolver.resolve(cwd, unresolved_config_entry_point, .entry_point) catch |err| { if (err == error.ModuleNotFound) { @@ -132,6 +136,7 @@ pub fn buildWithVm(ctx: bun.CLI.Command.Context, cwd: []const u8, vm: *VirtualMa return error.JSError; }; + config_promise.setHandled(vm.jsc); vm.waitForPromise(.{ .internal = config_promise }); var options = switch (config_promise.unwrap(vm.jsc, .mark_handled)) { .pending => unreachable, @@ -150,12 +155,7 @@ pub fn buildWithVm(ctx: bun.CLI.Command.Context, cwd: []const u8, vm: *VirtualMa break :config try bake.UserOptions.fromJS(app, vm.global); }, .rejected => |err| { - // dont run on rejected since we fail the build here - vm.printErrorLikeObjectToConsole(err); - if (vm.exit_handler.exit_code == 0) { - vm.exit_handler.exit_code = 1; - } - vm.globalExit(); + return global.throwValue(err.toError() orelse err); }, }; @@ -180,6 +180,17 @@ pub fn buildWithVm(ctx: bun.CLI.Command.Context, cwd: []const u8, vm: *VirtualMa try framework.initBundler(allocator, vm.log, .production_static, .ssr, &ssr_bundler); } + if (ctx.bundler_options.bake_debug_disable_minify) { + for ([_]*bun.bundler.Bundler{ &client_bundler, &server_bundler, &ssr_bundler }) |bundler| { + bundler.options.minify_syntax = false; + bundler.options.minify_identifiers = false; + bundler.options.minify_whitespace = false; + bundler.resolver.opts.entry_naming = "_bun/[dir]/[name].[hash].[ext]"; + bundler.resolver.opts.chunk_naming = "_bun/[dir]/[name].[hash].chunk.[ext]"; + bundler.resolver.opts.asset_naming = "_bun/[dir]/[name].[hash].asset.[ext]"; + } + } + // these share pointers right now, so setting NODE_ENV == production on one should affect all bun.assert(server_bundler.env == client_bundler.env); @@ -244,6 +255,7 @@ pub fn buildWithVm(ctx: bun.CLI.Command.Context, cwd: []const u8, vm: *VirtualMa .framework = framework.*, .client_bundler = &client_bundler, .ssr_bundler = if (separate_ssr_graph) &ssr_bundler else &server_bundler, + .plugins = options.bundler_options.plugin, }, allocator, .{ .js = vm.event_loop }, @@ -259,15 +271,14 @@ pub fn buildWithVm(ctx: bun.CLI.Command.Context, cwd: []const u8, vm: *VirtualMa var css_chunks_count: usize = 0; var css_chunks_first: usize = 0; - const all_server_files = JSValue.createEmptyArray(global, entry_points.files.count()); - // Index all bundled outputs. // Client files go to disk. // Server files get loaded in memory. // Populate indexes in `entry_points` to be looked up during prerendering - const js_module_keys = try vm.allocator.alloc(?*JSC.JSString, entry_points.files.count()); - @memset(js_module_keys, null); - const paths = entry_points.slice(bundled_outputs, js_module_keys); + const module_keys = try vm.allocator.alloc(bun.String, entry_points.files.count()); + const output_indexes = entry_points.files.values(); + var output_module_map: bun.StringArrayHashMapUnmanaged(OutputFile.Index) = .{}; + @memset(module_keys, bun.String.dead); for (bundled_outputs, 0..) |file, i| { log("{s} - {s} : {s} - {?d}\n", .{ if (file.side) |s| @tagName(s) else "null", @@ -281,20 +292,27 @@ pub fn buildWithVm(ctx: bun.CLI.Command.Context, cwd: []const u8, vm: *VirtualMa } if (file.entry_point_index) |entry_point| { - if (entry_point < paths.output_indexes.len) { - paths.output_indexes[entry_point] = OutputFile.Index.init(@intCast(i)); + if (entry_point < output_indexes.len) { + output_indexes[entry_point] = OutputFile.Index.init(@intCast(i)); } } - switch (file.side orelse .client) { + switch (file.side orelse continue) { .client => { // Client-side resources will be written to disk for usage in on the client side - _ = try file.writeToDisk(root_dir, root_dir_path); + _ = file.writeToDisk(root_dir, ".") catch |err| { + bun.handleErrorReturnTrace(err, @errorReturnTrace()); + Output.err(err, "Failed to write {} to output directory", .{bun.fmt.quote(file.dest_path)}); + }; }, .server => { // For Debugging - if (ctx.bundler_options.bake_debug_dump_server) - _ = try file.writeToDisk(root_dir, root_dir_path); + if (ctx.bundler_options.bake_debug_dump_server) { + _ = file.writeToDisk(root_dir, ".") catch |err| { + bun.handleErrorReturnTrace(err, @errorReturnTrace()); + Output.err(err, "Failed to write {} to output directory", .{bun.fmt.quote(file.dest_path)}); + }; + } switch (file.output_kind) { .@"entry-point", .chunk => { @@ -304,24 +322,19 @@ pub fn buildWithVm(ctx: bun.CLI.Command.Context, cwd: []const u8, vm: *VirtualMa else file.dest_path; - // TODO: later we can lazily register modules - const module_key = BakeRegisterProductionChunk( - global, - try bun.String.createFormat("bake:/{s}", .{without_prefix}), - file.value.toBunString(), - ) catch |err| { - Output.errGeneric("could not load bundled chunk {} for server-side rendering", .{ - bun.fmt.quote(without_prefix), - }); - return err; - }; - bun.assert(module_key.isString()); if (file.entry_point_index) |entry_point_index| { - if (entry_point_index < paths.js_module_keys.len) { - paths.js_module_keys[entry_point_index] = module_key.uncheckedPtrCast(JSC.JSString); - all_server_files.putIndex(global, entry_point_index, module_key); + if (entry_point_index < module_keys.len) { + var str = try bun.String.createFormat("bake:/{s}", .{without_prefix}); + str.toThreadSafe(); + module_keys[entry_point_index] = str; } } + + try output_module_map.put( + allocator, + try std.fmt.allocPrint(allocator, "bake:/{s}", .{without_prefix}), + OutputFile.Index.init(@intCast(i)), + ); }, .asset => {}, .bytecode => {}, @@ -331,6 +344,17 @@ pub fn buildWithVm(ctx: bun.CLI.Command.Context, cwd: []const u8, vm: *VirtualMa } } + const per_thread_options: PerThread.Options = .{ + .input_files = entry_points.files.keys(), + .bundled_outputs = bundled_outputs, + .output_indexes = output_indexes, + .module_keys = module_keys, + .module_map = output_module_map, + }; + + var pt = try PerThread.init(vm, per_thread_options); + pt.attach(); + // Static site generator const server_render_funcs = JSValue.createEmptyArray(global, router.types.len); const server_param_funcs = JSValue.createEmptyArray(global, router.types.len); @@ -340,16 +364,14 @@ pub fn buildWithVm(ctx: bun.CLI.Command.Context, cwd: []const u8, vm: *VirtualMa if (router_type.client_file.unwrap()) |client_file| { const str = (try bun.String.createFormat("{s}{s}", .{ public_path, - paths.outputFile(client_file).dest_path, + pt.outputFile(client_file).dest_path, })).toJS(global); client_entry_urls.putIndex(global, @intCast(i), str); } else { client_entry_urls.putIndex(global, @intCast(i), .null); } - const server_entry_module_key = paths.jsModuleKey(router_type.server_file); - bun.assert(server_entry_module_key != .undefined); - const server_entry_point = try loadModule(vm, global, server_entry_module_key); + const server_entry_point = try pt.loadBundledModule(router_type.server_file); const server_render_func = brk: { const raw = BakeGetOnModuleNamespace(global, server_entry_point, "prerender") orelse break :brk null; @@ -365,20 +387,23 @@ pub fn buildWithVm(ctx: bun.CLI.Command.Context, cwd: []const u8, vm: *VirtualMa bun.Global.crash(); }; - const server_param_func = brk: { - const raw = BakeGetOnModuleNamespace(global, server_entry_point, "getParams") orelse - break :brk null; - if (!raw.isCallable(vm.jsc)) { - break :brk null; + const server_param_func = if (router.dynamic_routes.count() > 0) + brk: { + const raw = BakeGetOnModuleNamespace(global, server_entry_point, "getParams") orelse + break :brk null; + if (!raw.isCallable(vm.jsc)) { + break :brk null; + } + break :brk raw; + } orelse { + Output.errGeneric("Framework does not support static site generation", .{}); + Output.note("The file {s} is missing the \"getParams\" export, which defines how to generate static files.", .{ + bun.fmt.quote(bun.path.relative(cwd, entry_points.files.keys()[router_type.server_file.get()].absPath())), + }); + bun.Global.crash(); } - break :brk raw; - } orelse { - Output.errGeneric("Framework does not support static site generation", .{}); - Output.note("The file {s} is missing the \"getParams\" export, which defines how to generate static files.", .{ - bun.fmt.quote(bun.path.relative(cwd, entry_points.files.keys()[router_type.server_file.get()].absPath())), - }); - bun.Global.crash(); - }; + else + JSValue.null; server_render_funcs.putIndex(global, @intCast(i), server_render_func); server_param_funcs.putIndex(global, @intCast(i), server_param_func); } @@ -411,7 +436,7 @@ pub fn buildWithVm(ctx: bun.CLI.Command.Context, cwd: []const u8, vm: *VirtualMa const route = router.routePtr(route_index); const main_file_route_index = route.file_page.unwrap().?; - const main_file = paths.outputFile(main_file_route_index); + const main_file = pt.outputFile(main_file_route_index); // Count how many JS+CSS files associated with this route and prepare `pattern` pattern.prependPart(route.part); @@ -427,7 +452,7 @@ pub fn buildWithVm(ctx: bun.CLI.Command.Context, cwd: []const u8, vm: *VirtualMa var file_count: u32 = 1; var css_file_count: u32 = @intCast(main_file.referenced_css_files.len); if (route.file_layout.unwrap()) |file| { - css_file_count += @intCast(paths.outputFile(file).referenced_css_files.len); + css_file_count += @intCast(pt.outputFile(file).referenced_css_files.len); file_count += 1; } var next: ?FrameworkRouter.Route.Index = route.parent.unwrap(); @@ -444,7 +469,7 @@ pub fn buildWithVm(ctx: bun.CLI.Command.Context, cwd: []const u8, vm: *VirtualMa else => {}, } if (parent.file_layout.unwrap()) |file| { - css_file_count += @intCast(paths.outputFile(file).referenced_css_files.len); + css_file_count += @intCast(pt.outputFile(file).referenced_css_files.len); file_count += 1; } next = parent.parent.unwrap(); @@ -457,14 +482,14 @@ pub fn buildWithVm(ctx: bun.CLI.Command.Context, cwd: []const u8, vm: *VirtualMa next = route.parent.unwrap(); file_count = 1; css_file_count = 0; - file_list.putIndex(global, 0, JSValue.jsNumberFromInt32(@intCast(main_file_route_index.get()))); + file_list.putIndex(global, 0, pt.preloadBundledModule(main_file_route_index)); for (main_file.referenced_css_files) |ref| { styles.putIndex(global, css_file_count, css_chunk_js_strings[ref.get() - css_chunks_first]); css_file_count += 1; } if (route.file_layout.unwrap()) |file| { - file_list.putIndex(global, file_count, JSValue.jsNumberFromInt32(@intCast(file.get()))); - for (paths.outputFile(file).referenced_css_files) |ref| { + file_list.putIndex(global, file_count, pt.preloadBundledModule(file)); + for (pt.outputFile(file).referenced_css_files) |ref| { styles.putIndex(global, css_file_count, css_chunk_js_strings[ref.get() - css_chunks_first]); css_file_count += 1; } @@ -474,8 +499,8 @@ pub fn buildWithVm(ctx: bun.CLI.Command.Context, cwd: []const u8, vm: *VirtualMa while (next) |parent_index| { const parent = router.routePtr(parent_index); if (parent.file_layout.unwrap()) |file| { - file_list.putIndex(global, file_count, JSValue.jsNumberFromInt32(@intCast(file.get()))); - for (paths.outputFile(file).referenced_css_files) |ref| { + file_list.putIndex(global, file_count, pt.preloadBundledModule(file)); + for (pt.outputFile(file).referenced_css_files) |ref| { styles.putIndex(global, css_file_count, css_chunk_js_strings[ref.get() - css_chunks_first]); css_file_count += 1; } @@ -489,7 +514,7 @@ pub fn buildWithVm(ctx: bun.CLI.Command.Context, cwd: []const u8, vm: *VirtualMa defer pattern_string.deref(); route_patterns.putIndex(global, @intCast(nav_index), pattern_string.toJS(global)); - var src_path = bun.String.createUTF8(bun.path.relative(cwd, paths.inputFile(main_file_route_index).absPath())); + var src_path = bun.String.createUTF8(bun.path.relative(cwd, pt.inputFile(main_file_route_index).absPath())); route_source_files.putIndex(global, @intCast(nav_index), src_path.transferToJS(global)); route_nested_files.putIndex(global, @intCast(nav_index), file_list); @@ -512,7 +537,7 @@ pub fn buildWithVm(ctx: bun.CLI.Command.Context, cwd: []const u8, vm: *VirtualMa const render_promise = BakeRenderRoutesForProdStatic( global, bun.String.init(root_dir_path), - all_server_files, + pt.all_server_files, server_render_funcs, server_param_funcs, client_entry_urls, @@ -524,6 +549,7 @@ pub fn buildWithVm(ctx: bun.CLI.Command.Context, cwd: []const u8, vm: *VirtualMa route_param_info, route_style_references, ); + render_promise.setHandled(vm.jsc); vm.waitForPromise(.{ .normal = render_promise }); switch (render_promise.unwrap(vm.jsc, .mark_handled)) { .pending => unreachable, @@ -541,6 +567,7 @@ pub fn buildWithVm(ctx: bun.CLI.Command.Context, cwd: []const u8, vm: *VirtualMa /// quits the process on exception fn loadModule(vm: *VirtualMachine, global: *JSC.JSGlobalObject, key: JSValue) !JSValue { const promise = BakeLoadModuleByKey(global, key).asAnyPromise().?.internal; + promise.setHandled(vm.jsc); vm.waitForPromise(.{ .internal = promise }); switch (promise.unwrap(vm.jsc, .mark_handled)) { .pending => unreachable, @@ -674,45 +701,6 @@ pub const EntryPointMap = struct { }; }; - /// Convenience structure - pub const Slice = struct { - files: []InputFile, - output_indexes: []OutputFile.Index, - /// From bundle_v2 - bundled_outputs: []OutputFile, - /// Not wrapped with JSC.Strong because JSModuleLoader - /// always contains a reference to this string. - js_module_keys: []?*JSC.JSString, - - pub fn outputIndex(s: Slice, id: OpaqueFileId) OutputFile.Index { - return s.output_indexes[id.get()]; - } - - pub fn inputFile(s: Slice, id: OpaqueFileId) InputFile { - return s.files[id.get()]; - } - - pub fn outputFile(s: Slice, id: OpaqueFileId) *OutputFile { - return &s.bundled_outputs[s.outputIndex(id).get()]; - } - - pub fn jsModuleKey(s: Slice, id: OpaqueFileId) JSValue { - return (s.js_module_keys[id.get()] orelse - Output.panic("Internal Error: {} did not get loaded", .{ - bun.fmt.quote(s.outputFile(id).dest_path), - })).toJS(); - } - }; - - pub fn slice(map: EntryPointMap, bundled_outputs: []OutputFile, js_module_keys: []?*JSC.JSString) Slice { - return .{ - .files = map.files.keys(), - .output_indexes = map.files.values(), - .js_module_keys = js_module_keys, - .bundled_outputs = bundled_outputs, - }; - } - pub fn getOrPutEntryPoint(map: *EntryPointMap, abs_path: []const u8, side: bake.Side) !OpaqueFileId { const k = InputFile.init(abs_path, side); const gop = try map.files.getOrPut(map.allocator, k); @@ -742,6 +730,126 @@ pub const EntryPointMap = struct { } }; +/// Data used on each rendering thread. Contains all information in the bundle needed to render. +/// This is referred to as `pt` in variable/field naming, and Bake::ProductionPerThread in C++ +pub const PerThread = struct { + // Shared Data + input_files: []const EntryPointMap.InputFile, + bundled_outputs: []const OutputFile, + /// Indexed by entry point index (OpaqueFileId) + output_indexes: []const OutputFile.Index, + /// Indexed by entry point index (OpaqueFileId) + module_keys: []const bun.String, + /// Unordered + module_map: bun.StringArrayHashMapUnmanaged(OutputFile.Index), + + // Thread-local + vm: *JSC.VirtualMachine, + /// Indexed by entry point index (OpaqueFileId) + loaded_files: bun.bit_set.AutoBitSet, + /// JSArray of JSString, indexed by entry point index (OpaqueFileId) + all_server_files: JSC.JSValue, + + /// Sent to other threads for rendering + pub const Options = struct { + input_files: []const EntryPointMap.InputFile, + bundled_outputs: []const OutputFile, + /// Indexed by entry point index (OpaqueFileId) + output_indexes: []const OutputFile.Index, + /// Indexed by entry point index (OpaqueFileId) + module_keys: []const bun.String, + /// Unordered + module_map: bun.StringArrayHashMapUnmanaged(OutputFile.Index), + }; + + extern fn BakeGlobalObject__attachPerThreadData(global: *JSC.JSGlobalObject, pt: ?*PerThread) void; + + /// After initializing, call `attach` + pub fn init(vm: *VirtualMachine, opts: Options) !PerThread { + const loaded_files = try bun.bit_set.AutoBitSet.initEmpty(vm.allocator, opts.output_indexes.len); + errdefer loaded_files.deinit(vm.allocator); + + const all_server_files = JSValue.createEmptyArray(vm.global, opts.output_indexes.len); + all_server_files.protect(); + + return .{ + .input_files = opts.input_files, + .bundled_outputs = opts.bundled_outputs, + .output_indexes = opts.output_indexes, + .module_keys = opts.module_keys, + .module_map = opts.module_map, + .vm = vm, + .loaded_files = loaded_files, + .all_server_files = all_server_files, + }; + } + + pub fn attach(pt: *PerThread) void { + BakeGlobalObject__attachPerThreadData(pt.vm.global, pt); + } + + pub fn deinit(pt: *PerThread) void { + BakeGlobalObject__attachPerThreadData(pt.vm.global, null); + pt.all_server_files.unprotect(); + } + + pub fn outputIndex(s: PerThread, id: OpaqueFileId) OutputFile.Index { + return s.output_indexes[id.get()]; + } + + pub fn inputFile(s: PerThread, id: OpaqueFileId) EntryPointMap.InputFile { + return s.input_files[id.get()]; + } + + pub fn outputFile(s: PerThread, id: OpaqueFileId) *const OutputFile { + return &s.bundled_outputs[s.outputIndex(id).get()]; + } + + // Must be run at the top of the event loop + pub fn loadBundledModule(pt: *PerThread, id: OpaqueFileId) bun.JSError!JSValue { + return try loadModule( + pt.vm, + pt.vm.global, + pt.module_keys[id.get()].toJS(pt.vm.global), + ); + } + + /// The JSString entries in `all_server_files` is generated lazily. When + /// multiple rendering threads are used, unreferenced files will contain + /// holes in the array used. Returns a JSValue of the "FileIndex" type + // + // What could be done here is generating a new index type, which is + // specifically for referenced files. This would remove the holes, but make + // it harder to pre-allocate. It's probably worth it. + pub fn preloadBundledModule(pt: *PerThread, id: OpaqueFileId) JSValue { + if (!pt.loaded_files.isSet(id.get())) { + pt.loaded_files.set(id.get()); + pt.all_server_files.putIndex( + pt.vm.global, + @intCast(id.get()), + pt.module_keys[id.get()].toJS(pt.vm.global), + ); + } + + return JSValue.jsNumberFromInt32(@intCast(id.get())); + } +}; + +/// Given a key, returns the source code to load. +export fn BakeProdLoad(pt: *PerThread, key: bun.String) bun.String { + var sfa = std.heap.stackFallback(4096, bun.default_allocator); + const allocator = sfa.get(); + const utf8 = key.toUTF8(allocator); + defer utf8.deinit(); + if (pt.module_map.get(utf8.slice())) |value| { + return pt.bundled_outputs[value.get()].value.toBunString(); + } + for (pt.module_map.keys()) |keys| { + std.debug.print("key that does exist: {s}\n", .{keys}); + } + return bun.String.dead; +} + const TypeAndFlags = packed struct(i32) { type: u8, unused: u24 = 0, diff --git a/src/bun.js/ConsoleObject.zig b/src/bun.js/ConsoleObject.zig index 295b64f3aa..5f08103018 100644 --- a/src/bun.js/ConsoleObject.zig +++ b/src/bun.js/ConsoleObject.zig @@ -2492,8 +2492,15 @@ pub const Formatter = struct { return this.printAs(.Object, Writer, writer_, value, .Event, enable_ansi_colors); }, .NativeCode => { - this.addForNewLine("[native code]".len); - writer.writeAll("[native code]"); + if (value.getClassInfoName()) |class_name| { + this.addForNewLine("[native code: ]".len + class_name.len); + writer.writeAll("[native code: "); + writer.writeAll(class_name); + writer.writeAll("]"); + } else { + this.addForNewLine("[native code]".len); + writer.writeAll("[native code]"); + } }, .Promise => { if (!this.single_line and this.goodTimeForANewLine()) { diff --git a/src/bun.js/api/server.zig b/src/bun.js/api/server.zig index 1cd310d23b..5a37980083 100644 --- a/src/bun.js/api/server.zig +++ b/src/bun.js/api/server.zig @@ -1949,8 +1949,7 @@ fn NewRequestContext(comptime ssl_enabled: bool, comptime debug_mode: bool, comp } fn renderMissingInvalidResponse(ctx: *RequestContext, value: JSC.JSValue) void { - var class_name = value.getClassInfoName() orelse bun.String.empty; - defer class_name.deref(); + const class_name = value.getClassInfoName() orelse ""; if (ctx.server) |server| { const globalThis: *JSC.JSGlobalObject = server.globalThis; @@ -1958,7 +1957,7 @@ fn NewRequestContext(comptime ssl_enabled: bool, comptime debug_mode: bool, comp Output.enableBuffering(); var writer = Output.errorWriter(); - if (class_name.eqlComptime("Response")) { + if (bun.strings.eqlComptime(class_name, "Response")) { Output.errGeneric("Expected a native Response object, but received a polyfilled Response object. Bun.serve() only supports native Response objects.", .{}); } else if (value != .zero and !globalThis.hasException()) { var formatter = JSC.ConsoleObject.Formatter{ diff --git a/src/bun.js/bindings/bindings.cpp b/src/bun.js/bindings/bindings.cpp index 79993209e9..d2b353aa34 100644 --- a/src/bun.js/bindings/bindings.cpp +++ b/src/bun.js/bindings/bindings.cpp @@ -4750,10 +4750,11 @@ void JSC__JSValue__getClassName(JSC__JSValue JSValue0, JSC__JSGlobalObject* arg1 *arg2 = Zig::toZigString(view); } -bool JSC__JSValue__getClassInfoName(JSValue value, BunString* out) +bool JSC__JSValue__getClassInfoName(JSValue value, const uint8_t** outPtr, size_t* outLen) { if (auto info = value.classInfoOrNull()) { - *out = Bun::toString(info->className); + *outPtr = info->className.span8().data(); + *outLen = info->className.span8().size(); return true; } return false; diff --git a/src/bun.js/bindings/bindings.zig b/src/bun.js/bindings/bindings.zig index 6caba4d5d5..fcd4b72254 100644 --- a/src/bun.js/bindings/bindings.zig +++ b/src/bun.js/bindings/bindings.zig @@ -6175,13 +6175,14 @@ pub const JSValue = enum(i64) { return Bun__ProxyObject__getInternalField(this, field); } - extern fn JSC__JSValue__getClassInfoName(value: JSValue, out: *bun.String) bool; + extern fn JSC__JSValue__getClassInfoName(value: JSValue, out: *[*:0]const u8, len: *usize) bool; /// For native C++ classes extending JSCell, this retrieves s_info's name - pub fn getClassInfoName(this: JSValue) ?bun.String { + /// This is a readonly ASCII string. + pub fn getClassInfoName(this: JSValue) ?[:0]const u8 { if (!this.isCell()) return null; - var out: bun.String = bun.String.empty; - if (!JSC__JSValue__getClassInfoName(this, &out)) return null; + var out: [:0]const u8 = ""; + if (!JSC__JSValue__getClassInfoName(this, &out.ptr, &out.len)) return null; return out; } }; diff --git a/src/bundler/bundle_v2.zig b/src/bundler/bundle_v2.zig index 4d7d94e190..555a5157d8 100644 --- a/src/bundler/bundle_v2.zig +++ b/src/bundler/bundle_v2.zig @@ -325,7 +325,11 @@ const Watcher = bun.JSC.NewHotReloader(BundleV2, EventLoop, true); fn genericPathWithPrettyInitialized(path: Fs.Path, target: options.Target, top_level_dir: string, allocator: std.mem.Allocator) !Fs.Path { // TODO: outbase var buf: bun.PathBuffer = undefined; - if (path.isFile()) { + + // "file" namespace should use the relative file path for its display name. + // the "node" namespace is also put through this code path so that the + // "node:" prefix is not emitted. + if (path.isFile() or bun.strings.eqlComptime(path.namespace, "node")) { const rel = bun.path.relativePlatform(top_level_dir, path.text, .loose, false); var path_clone = path; // stack-allocated temporary is not leaked because dupeAlloc on the path will @@ -400,6 +404,7 @@ pub const BundleV2 = struct { framework: bake.Framework, client_bundler: *Bundler, ssr_bundler: *Bundler, + plugins: ?*JSC.API.JSBundler.Plugin, }; const debug = Output.scoped(.Bundle, false); @@ -907,6 +912,7 @@ pub const BundleV2 = struct { this.ssr_bundler = bo.ssr_bundler; this.framework = bo.framework; this.linker.framework = &this.framework.?; + this.plugins = bo.plugins; bun.assert(bundler.options.server_components); bun.assert(this.client_bundler.options.server_components); if (bo.framework.server_components.?.separate_ssr_graph) @@ -2939,7 +2945,9 @@ pub const BundleV2 = struct { graph.ast.set(result.source.index.get(), result.ast); // For files with use directives, index and prepare the other side. - if (result.use_directive != .none and + if (result.use_directive != .none and if (this.framework.?.server_components.?.separate_ssr_graph) + ((result.use_directive == .client) == (result.ast.target == .browser)) + else ((result.use_directive == .client) != (result.ast.target == .browser))) { if (result.use_directive == .server) @@ -11078,7 +11086,7 @@ pub const LinkerContext = struct { const items = try allocator.alloc(Expr, st.items.len); for (st.items, items) |item, *str| { - str.* = Expr.init(E.String, .{ .data = item.original_name }, item.name.loc); + str.* = Expr.init(E.String, .{ .data = item.alias }, item.name.loc); } break :call Expr.init(E.Call, .{ @@ -12352,7 +12360,9 @@ pub const LinkerContext = struct { .is_executable = chunk.is_executable, .source_map_index = source_map_index, .bytecode_index = bytecode_index, - .side = switch (c.graph.ast.items(.target)[chunk.entry_point.source_index]) { + .side = if (chunk.content == .css) + .client + else switch (c.graph.ast.items(.target)[chunk.entry_point.source_index]) { .browser => .client, else => .server, }, @@ -12761,7 +12771,9 @@ pub const LinkerContext = struct { .data = .{ .saved = 0, }, - .side = switch (c.graph.ast.items(.target)[chunk.entry_point.source_index]) { + .side = if (chunk.content == .css) + .client + else switch (c.graph.ast.items(.target)[chunk.entry_point.source_index]) { .browser => .client, else => .server, }, diff --git a/src/cli.zig b/src/cli.zig index 02a8abf23d..8510a5137b 100644 --- a/src/cli.zig +++ b/src/cli.zig @@ -290,6 +290,7 @@ pub const Arguments = struct { clap.parseParam("--server-components (EXPERIMENTAL) Enable server components") catch unreachable, } ++ if (FeatureFlags.bake_debugging_features) [_]ParamType{ clap.parseParam("--debug-dump-server-files When --app is set, dump all server files to disk even when building statically") catch unreachable, + clap.parseParam("--debug-no-minify When --app is set, do not minify anything") catch unreachable, } else .{}; pub const build_params = build_only_params ++ transpiler_params_ ++ base_params_; @@ -797,6 +798,8 @@ pub const Arguments = struct { ctx.bundler_options.bake = true; ctx.bundler_options.bake_debug_dump_server = bun.FeatureFlags.bake_debugging_features and args.flag("--debug-dump-server-files"); + ctx.bundler_options.bake_debug_disable_minify = bun.FeatureFlags.bake_debugging_features and + args.flag("--debug-no-minify"); } // TODO: support --format=esm @@ -1457,6 +1460,7 @@ pub const Command = struct { bake: bool = false, bake_debug_dump_server: bool = false, + bake_debug_disable_minify: bool = false, }; pub fn create(allocator: std.mem.Allocator, log: *logger.Log, comptime command: Command.Tag) anyerror!Context { diff --git a/src/js/builtins/Bake.ts b/src/js/builtins/Bake.ts index ca85b8eadc..9faf064bf8 100644 --- a/src/js/builtins/Bake.ts +++ b/src/js/builtins/Bake.ts @@ -52,8 +52,9 @@ export function renderRoutesForProdStatic( // Call the framework's rendering function const callback = renderStatic[type]; $assert(callback != null && $isCallable(callback)); + let client = clientEntryUrl[type]; const results = await callback({ - modules: [clientEntryUrl[type]], + modules: client ? [client] : [], modulepreload: [], styles: styles[i], layouts, @@ -116,7 +117,7 @@ export function renderRoutesForProdStatic( [pageModule, ...layouts] = anyPromise ? await Promise.all(loaded) : loaded; } else { const id = fileList[0]; - pageModule = loadedModules[id] ?? (loadedModules[id] = await import(allServerFiles[fileList[0]])); + pageModule = loadedModules[id] ?? (loadedModules[id] = await import(allServerFiles[id])); layouts = []; } diff --git a/src/js_ast.zig b/src/js_ast.zig index c7713a3dde..ce24d226f5 100644 --- a/src/js_ast.zig +++ b/src/js_ast.zig @@ -8196,14 +8196,13 @@ pub const Macro = struct { .String => this.coerce(value, .String), .Promise => this.coerce(value, .Promise), else => brk: { - var name = value.getClassInfoName() orelse bun.String.init("unknown"); - defer name.deref(); + const name = value.getClassInfoName() orelse "unknown"; this.log.addErrorFmt( this.source, this.caller.loc, this.allocator, - "cannot coerce {} ({s}) to Bun's AST. Please return a simpler type", + "cannot coerce {s} ({s}) to Bun's AST. Please return a simpler type", .{ name, @tagName(value.jsType()) }, ) catch unreachable; break :brk error.MacroFailed; diff --git a/src/js_parser.zig b/src/js_parser.zig index 812ff3580f..e0fe7510c6 100644 --- a/src/js_parser.zig +++ b/src/js_parser.zig @@ -9062,7 +9062,7 @@ fn NewParser_( if (symbol.namespace_alias == null) { symbol.namespace_alias = .{ .namespace_ref = stmt.namespace_ref, - .alias = name, + .alias = item.alias, .import_record_index = stmt.import_record_index, }; } @@ -23977,7 +23977,7 @@ pub const ConvertESMExportsForHmr = struct { return; // do not emit a statement here }, .s_export_from => |st| stmt: { - for (st.items) |item| { + for (st.items) |*item| { const ref = item.name.ref.?; const symbol = &p.symbols.items[ref.innerIndex()]; if (symbol.namespace_alias == null) { @@ -23988,6 +23988,15 @@ pub const ConvertESMExportsForHmr = struct { }; } try ctx.visitRefToExport(p, ref, item.alias, item.name.loc, true); + + // imports and export statements have their alias + + // original_name swapped. this is likely a design bug in + // the parser but since everything uses these + // assumptions, this hack is simpler than making it + // proper + const alias = item.alias; + item.alias = item.original_name; + item.original_name = alias; } const gop = try ctx.imports_seen.getOrPut(p.allocator, st.import_record_index); diff --git a/src/options.zig b/src/options.zig index d1f669b821..4bccea93e6 100644 --- a/src/options.zig +++ b/src/options.zig @@ -2071,42 +2071,58 @@ pub const OutputFile = struct { } /// Given the `--outdir` as root_dir, this will return the relative path to display in terminal - pub fn writeToDisk(f: OutputFile, root_dir: std.fs.Dir, root_dir_path: []const u8) ![]const u8 { + pub fn writeToDisk(f: OutputFile, root_dir: std.fs.Dir, longest_common_path: []const u8) ![]const u8 { switch (f.value) { .saved => { var rel_path = f.dest_path; - if (f.dest_path.len > root_dir_path.len) { - rel_path = resolve_path.relative(root_dir_path, f.dest_path); + if (f.dest_path.len > longest_common_path.len) { + rel_path = resolve_path.relative(longest_common_path, f.dest_path); } return rel_path; }, .buffer => |value| { var rel_path = f.dest_path; - if (f.dest_path.len > root_dir_path.len) { - rel_path = resolve_path.relative(root_dir_path, f.dest_path); + + if (f.dest_path.len > longest_common_path.len) { + rel_path = resolve_path.relative(longest_common_path, f.dest_path); if (std.fs.path.dirname(rel_path)) |parent| { - if (parent.len > root_dir_path.len) { + if (parent.len > longest_common_path.len) { try root_dir.makePath(parent); } } } - var path_buf: bun.PathBuffer = undefined; - _ = try JSC.Node.NodeFS.writeFileWithPathBuffer(&path_buf, .{ - .data = .{ .buffer = .{ - .buffer = .{ - .ptr = @constCast(value.bytes.ptr), - .len = value.bytes.len, - .byte_len = value.bytes.len, + var handled_file_not_found = false; + while (true) { + var path_buf: bun.PathBuffer = undefined; + JSC.Node.NodeFS.writeFileWithPathBuffer(&path_buf, .{ + .data = .{ .buffer = .{ + .buffer = .{ + .ptr = @constCast(value.bytes.ptr), + .len = value.bytes.len, + .byte_len = value.bytes.len, + }, + } }, + .encoding = .buffer, + .mode = if (f.is_executable) 0o755 else 0o644, + .dirfd = bun.toFD(root_dir.fd), + .file = .{ .path = .{ + .string = JSC.PathString.init(rel_path), + } }, + }).unwrap() catch |err| switch (err) { + error.FileNotFound, error.ENOENT => { + if (handled_file_not_found) return err; + handled_file_not_found = true; + try root_dir.makePath( + std.fs.path.dirname(rel_path) orelse + return err, + ); + continue; }, - } }, - .encoding = .buffer, - .mode = if (f.is_executable) 0o755 else 0o644, - .dirfd = bun.toFD(root_dir.fd), - .file = .{ .path = .{ - .string = JSC.PathString.init(rel_path), - } }, - }).unwrap(); + else => return err, + }; + break; + } return rel_path; }, diff --git a/test/bake/dev-server-harness.ts b/test/bake/dev-server-harness.ts index 947b37d132..49cf8f572e 100644 --- a/test/bake/dev-server-harness.ts +++ b/test/bake/dev-server-harness.ts @@ -27,7 +27,9 @@ export const minimalFramework: Bake.Framework = { }, }; -export interface DevServerTest { +export type DevServerTest = ({ + /** Starting files */ + files: FileObject; /** * Framework to use. Consider `minimalFramework` if possible. * Provide this object or `files['bun.app.ts']` for a dynamic one. @@ -38,8 +40,13 @@ export interface DevServerTest { * combined with the `framework` option. */ pluginFile?: string; - /** Starting files */ - files: FileObject; +} | { + /** + * Copy all files from test/bake/fixtures/ + * This directory must contain `bun.app.ts` to allow hacking on fixtures manually via `bun run .` + */ + fixture: string; +}) & { test: (dev: Dev) => Promise; } @@ -327,31 +334,47 @@ export function devTest(description: string, options: T jest.test(`DevServer > ${basename}.${count}: ${description}`, async () => { const root = path.join(tempDir, basename + count); - writeAll(root, options.files); - if (options.files["bun.app.ts"] == undefined) { - if (!options.framework) { - throw new Error("Must specify a options.framework or provide a bun.app.ts file"); + if ('files' in options) { + writeAll(root, options.files); + if (options.files["bun.app.ts"] == undefined) { + if (!options.framework) { + throw new Error("Must specify a options.framework or provide a bun.app.ts file"); + } + if (options.pluginFile) { + fs.writeFileSync(path.join(root, "pluginFile.ts"), dedent(options.pluginFile)); + } + fs.writeFileSync( + path.join(root, "bun.app.ts"), + dedent` + ${options.pluginFile ? + `import plugins from './pluginFile.ts';` : "let plugins = undefined;" + } + export default { + app: { + framework: ${JSON.stringify(options.framework)}, + plugins, + }, + }; + `, + ); + } else { + if (options.pluginFile) { + throw new Error("Cannot provide both bun.app.ts and pluginFile"); + } } - if (options.pluginFile) { - fs.writeFileSync(path.join(root, "pluginFile.ts"), dedent(options.pluginFile)); - } - fs.writeFileSync( - path.join(root, "bun.app.ts"), - dedent` - ${options.pluginFile ? - `import plugins from './pluginFile.ts';` : "let plugins = undefined;" - } - export default { - app: { - framework: ${JSON.stringify(options.framework)}, - plugins, - }, - }; - `, - ); } else { - if (options.pluginFile) { - throw new Error("Cannot provide both bun.app.ts and pluginFile"); + if (!options.fixture) { + throw new Error("Must provide either `fixture` or `files`"); + } + const fixture = path.join(devTestRoot, "../fixtures", options.fixture); + fs.cpSync(fixture, root, { recursive: true }); + + if(!fs.existsSync(path.join(root, "bun.app.ts"))) { + throw new Error(`Fixture ${fixture} must contain a bun.app.ts file.`); + } + if (!fs.existsSync(path.join(root, "node_modules"))) { + // link the node_modules directory from test/node_modules to the temp directory + fs.symlinkSync(path.join(devTestRoot, "../../node_modules"), path.join(root, "node_modules"), "junction"); } } fs.writeFileSync( @@ -359,8 +382,8 @@ export function devTest(description: string, options: T dedent` import appConfig from "./bun.app.ts"; export default { + ...appConfig, port: 0, - ...appConfig }; `, ); @@ -373,6 +396,7 @@ export function devTest(description: string, options: T { FORCE_COLOR: "1", BUN_DEV_SERVER_TEST_RUNNER: "1", + BUN_DUMP_STATE_ON_CRASH: "1", }, ]), stdio: ["pipe", "pipe", "pipe"], diff --git a/test/bake/dev/ecosystem.test.ts b/test/bake/dev/ecosystem.test.ts index 068795a875..bb0e58d89e 100644 --- a/test/bake/dev/ecosystem.test.ts +++ b/test/bake/dev/ecosystem.test.ts @@ -2,10 +2,22 @@ // should be preferred to write specific tests for the bugs that these libraries // discovered, but it easy and still a reasonable idea to just test the library // entirely. +import { expect } from "bun:test"; import { devTest } from "../dev-server-harness"; -// TODO: svelte server component example project // Bugs discovered thanks to Svelte: -// - Valid circular import use. -// - Re-export `.e_import_identifier`, including live bindings. -// TODO: - something related to the wrong push function being called \ No newline at end of file +// - Circular import situations +// - export { live_binding } +// - export { x as y } +devTest('svelte component islands example', { + fixture: 'svelte-component-islands', + async test(dev) { + const html = await dev.fetch('/').text() + if (html.includes('Bun__renderFallbackError')) throw new Error('failed'); + expect(html).toContain('self.$islands={\"pages/_Counter.svelte\":[[0,\"default\",{initial:5}]]}'); + expect(html).toContain(`

This is my svelte server component (non-interactive)

Bun v${Bun.version}

`); + expect(html).toContain(`>This is a client component (interactive island)

`); + // TODO: puppeteer test for client-side interactivity, hmr. + // care must be taken to implement this in a way that is not flaky. + }, +}); diff --git a/test/bake/dev/esm.test.ts b/test/bake/dev/esm.test.ts index 1864c5e387..b08fcf1418 100644 --- a/test/bake/dev/esm.test.ts +++ b/test/bake/dev/esm.test.ts @@ -135,4 +135,73 @@ devTest("export { x as y }", { }); await dev.fetch("/").expect("Value: 3"); } -}); \ No newline at end of file +}); +devTest("import { x as y }", { + framework: minimalFramework, + files: { + "module.ts": ` + export const x = 1; + `, + "routes/index.ts": ` + import { x as y } from '../module'; + export default function(req, meta) { + return new Response('Value: ' + y); + } + `, + }, + async test(dev) { + await dev.fetch("/").expect("Value: 1"); + await dev.patch("module.ts", { + find: "1", + replace: "2", + }); + await dev.fetch("/").expect("Value: 2"); + } +}); +devTest("import { default as y }", { + framework: minimalFramework, + files: { + "module.ts": ` + export default 1; + `, + "routes/index.ts": ` + import { default as y } from '../module'; + export default function(req, meta) { + return new Response('Value: ' + y); + } + `, + }, + async test(dev) { + await dev.fetch("/").expect("Value: 1"); + await dev.patch("module.ts", { + find: "1", + replace: "2", + }); + await dev.fetch("/").expect("Value: 2"); + } +}); +devTest("export { default as y }", { + framework: minimalFramework, + files: { + "module.ts": ` + export default 1; + `, + "middle.ts": ` + export { default as y } from './module'; + `, + "routes/index.ts": ` + import { y } from '../middle'; + export default function(req, meta) { + return new Response('Value: ' + y); + } + `, + }, + async test(dev) { + await dev.fetch("/").expect("Value: 1"); + await dev.patch("module.ts", { + find: "1", + replace: "2", + }); + await dev.fetch("/").expect("Value: 2"); + } +}); diff --git a/test/bake/fixtures/svelte-component-islands/bun.app.ts b/test/bake/fixtures/svelte-component-islands/bun.app.ts new file mode 100644 index 0000000000..9813768a22 --- /dev/null +++ b/test/bake/fixtures/svelte-component-islands/bun.app.ts @@ -0,0 +1,8 @@ +import svelte from "./framework"; + +export default { + port: 3000, + app: { + framework: svelte(), + }, +}; diff --git a/test/bake/fixtures/svelte-component-islands/framework/client.ts b/test/bake/fixtures/svelte-component-islands/framework/client.ts new file mode 100644 index 0000000000..aca37275e0 --- /dev/null +++ b/test/bake/fixtures/svelte-component-islands/framework/client.ts @@ -0,0 +1,14 @@ +import type { IslandMap } from "./server"; +import { hydrate } from 'svelte'; + +declare var $islands: IslandMap; +Object.entries($islands).forEach(async([moduleId, islands]) => { + const mod = await import(moduleId); + for(const [islandId, exportId, props] of islands) { + const elem = document.getElementById(`I:${islandId}`)!; + hydrate(mod[exportId], { + target: elem, + props, + }); + } +}); diff --git a/test/bake/fixtures/svelte-component-islands/framework/index.ts b/test/bake/fixtures/svelte-component-islands/framework/index.ts new file mode 100644 index 0000000000..7cecf75358 --- /dev/null +++ b/test/bake/fixtures/svelte-component-islands/framework/index.ts @@ -0,0 +1,68 @@ +import type { Bake } from "bun"; +import * as path from "node:path"; +import * as fs from "node:fs/promises"; +import * as svelte from "svelte/compiler"; + +export default function (): Bake.Framework { + return { + serverComponents: { + separateSSRGraph: false, + serverRuntimeImportSource: "./framework/server.ts", + }, + fileSystemRouterTypes: [ + { + root: "pages", + serverEntryPoint: "./framework/server.ts", + clientEntryPoint: "./framework/client.ts", + style: "nextjs-pages", // later, this will be fully programmable + extensions: [".svelte"], + }, + ], + plugins: [ + { + // This is missing a lot of code that a plugin like `esbuild-svelte` + // handles, but this is only an examplea of how such a plugin could + // have server-components at a minimal level. + name: "svelte-server-components", + setup(b) { + const cssMap = new Map(); + b.onLoad({ filter: /.svelte$/ }, async (args) => { + const contents = await fs.readFile(args.path, "utf-8"); + const result = svelte.compile(contents, { + filename: args.path, + css: "external", + cssOutputFilename: path.basename(args.path, ".svelte") + ".css", + hmr: true, + dev: true, + generate: args.side, + }); + // If CSS is specified, add a CSS import + let jsCode = result.js.code; + if (result.css) { + cssMap.set(args.path, result.css.code); + jsCode = `import ${JSON.stringify("svelte-css:" + args.path)};` + jsCode; + } + // Extract a "use client" directive from the file. + const header = contents.match(/^\s*\s*("[^"\n]*"|'[^'\n]*')/)?.[1]; + if (header) { + jsCode = header + ';' + jsCode; + } + return { + contents: jsCode, + loader: "js", + watchFiles: [args.path], + }; + }); + + // Resolve CSS files + b.onResolve({ filter: /^svelte-css:/ }, async (args) => { + return { path: args.path.replace(/^svelte-css:/, ""), namespace: "svelte-css" }; + }); + b.onLoad({ filter: /./, namespace: "svelte-css" }, async (args) => { + return { contents: cssMap.get(args.path) ?? "", loader: "css" }; + }); + }, + }, + ], + }; +} diff --git a/test/bake/fixtures/svelte-component-islands/framework/server.ts b/test/bake/fixtures/svelte-component-islands/framework/server.ts new file mode 100644 index 0000000000..29253245f8 --- /dev/null +++ b/test/bake/fixtures/svelte-component-islands/framework/server.ts @@ -0,0 +1,71 @@ +/// +import type { Bake } from "bun"; +import * as svelte from "svelte/server"; +import { uneval } from "devalue"; + +export function render(req: Request, meta: Bake.RouteMetadata) { + isInsideIsland = false; + islands = {}; + const { body, head } = svelte.render(meta.pageModule.default, { + props: { + params: meta.params, + }, + }); + + // Add stylesheets and preloaded modules to the head + const extraHead = meta.styles.map((style) => ``).join("") + + meta.modulepreload.map((style) => ``).join(""); + // Script tags + const scripts = nextIslandId > 0 + ? `` + + meta.modules.map((module) => ``).join("") + : ""; // If no islands, no JavaScript + + return new Response( + "" + head + extraHead + "" + + body + "" + scripts + "", + { headers: { "content-type": "text/html" } }, + ); +} + +// To allow static site generation, frameworks can specify a prerender function +export function prerender(meta: Bake.RouteMetadata) { + return { + files: { + '/index.html': render(null!, meta), + }, + }; +} + +let isInsideIsland = false; +let nextIslandId = 0; +let islands: IslandMap; +export type IslandMap = Record; +export type Island = [islandId: number, exportId: string, props: any]; + +/** + * @param component The original export value, as is. + * @param clientModuleId A string that the browser will pass to `import()`. + * @param clientExportId The export ID from the imported module. + * @returns A wrapped value for the export. + */ +export function registerClientReference( + component: Function, + clientModuleId: string, + clientExportId: string, +) { + return function Island(...args: any[]) { + if (isInsideIsland) { + return component(...args); + } + isInsideIsland = true; + const [payload, props] = args; + const islandId = nextIslandId++; + payload.out += ``; + const file = (islands[clientModuleId] ??= []); + file.push([islandId, clientExportId, props]); + component(...args); + payload.out += ``; + isInsideIsland = false; + }; +} diff --git a/test/bake/fixtures/svelte-component-islands/pages/_Counter.svelte b/test/bake/fixtures/svelte-component-islands/pages/_Counter.svelte new file mode 100644 index 0000000000..7c4a618edb --- /dev/null +++ b/test/bake/fixtures/svelte-component-islands/pages/_Counter.svelte @@ -0,0 +1,24 @@ + + +
+

This is a client component (interactive island)

+ +
+ diff --git a/test/bake/fixtures/svelte-component-islands/pages/index.svelte b/test/bake/fixtures/svelte-component-islands/pages/index.svelte new file mode 100644 index 0000000000..f5b8c0728c --- /dev/null +++ b/test/bake/fixtures/svelte-component-islands/pages/index.svelte @@ -0,0 +1,18 @@ + +
+

hello

+

This is my svelte server component (non-interactive)

+

Bun v{Bun.version}

+ +
+ \ No newline at end of file diff --git a/test/bun.lockb b/test/bun.lockb index 6a1061e911..699279fcb2 100755 Binary files a/test/bun.lockb and b/test/bun.lockb differ diff --git a/test/js/bun/plugin/plugins.test.ts b/test/js/bun/plugin/plugins.test.ts index 5222bc29a6..d544fb953b 100644 --- a/test/js/bun/plugin/plugins.test.ts +++ b/test/js/bun/plugin/plugins.test.ts @@ -187,15 +187,14 @@ plugin({ // This is to test that it works when imported from a separate file import "../../third_party/svelte"; import "./module-plugins"; -import { bunEnv, bunExe, tempDirWithFiles } from "harness"; -import { filter } from "js/node/test/fixtures/aead-vectors"; +import { render as svelteRender } from 'svelte/server'; describe("require", () => { it("SSRs `

Hello world!

` with Svelte", () => { const { default: App } = require("./hello.svelte"); - const { html } = App.render(); + const { body } = svelteRender(App); - expect(html).toBe("

Hello world!

"); + expect(body).toBe("

Hello world!

"); }); it("beep:boop returns 42", () => { @@ -295,9 +294,8 @@ describe("dynamic import", () => { it("SSRs `

Hello world!

` with Svelte", async () => { const { default: App }: any = await import("./hello.svelte"); - const { html } = App.render(); - - expect(html).toBe("

Hello world!

"); + const { body } = svelteRender(App); + expect(body).toBe("

Hello world!

"); }); it("beep:boop returns 42", async () => { @@ -326,9 +324,9 @@ import Hello from ${JSON.stringify(resolve(import.meta.dir, "hello2.svelte"))}; export default Hello; `; const { default: SvelteApp } = await import("delay:hello2.svelte"); - const { html } = SvelteApp.render(); + const { body } = svelteRender(SvelteApp); - expect(html).toBe("

Hello world!

"); + expect(body).toBe("

Hello world!

"); }); }); diff --git a/test/js/third_party/svelte/bun-loader-svelte.ts b/test/js/third_party/svelte/bun-loader-svelte.ts index c30a7bd11e..e90562b38f 100644 --- a/test/js/third_party/svelte/bun-loader-svelte.ts +++ b/test/js/third_party/svelte/bun-loader-svelte.ts @@ -11,7 +11,8 @@ await plugin({ readFileSync(path.substring(0, path.includes("?") ? path.indexOf("?") : path.length), "utf-8"), { filename: path, - generate: "ssr", + generate: "server", + dev: false, }, ).js.code, loader: "js", diff --git a/test/js/third_party/svelte/svelte.test.ts b/test/js/third_party/svelte/svelte.test.ts index 67167ecbe0..05a56e4c42 100644 --- a/test/js/third_party/svelte/svelte.test.ts +++ b/test/js/third_party/svelte/svelte.test.ts @@ -1,12 +1,13 @@ import { describe, expect, it } from "bun:test"; +import { render as svelteRender } from "svelte/server"; import "./bun-loader-svelte"; describe("require", () => { it("SSRs `

Hello world!

` with Svelte", () => { const { default: App } = require("./hello.svelte"); - const { html } = App.render(); + const { body } = svelteRender(App); - expect(html).toBe("

Hello world!

"); + expect(body).toBe("

Hello world!

"); }); it("works if you require it 1,000 times", () => { @@ -14,7 +15,7 @@ describe("require", () => { Bun.unsafe.gcAggressionLevel(0); for (let i = 0; i < 1000; i++) { const { default: App } = require("./hello.svelte?r" + i); - expect(App.render).toBeFunction(); + expect(App).toBeFunction(); } Bun.gc(true); Bun.unsafe.gcAggressionLevel(prev); @@ -27,7 +28,7 @@ describe("dynamic import", () => { Bun.unsafe.gcAggressionLevel(0); for (let i = 0; i < 1000; i++) { const { default: App } = await import("./hello.svelte?i" + i); - expect(App.render).toBeFunction(); + expect(App).toBeFunction(); } Bun.gc(true); Bun.unsafe.gcAggressionLevel(prev); @@ -35,8 +36,7 @@ describe("dynamic import", () => { it("SSRs `

Hello world!

` with Svelte", async () => { const { default: App }: any = await import("./hello.svelte"); - const { html } = App.render(); - - expect(html).toBe("

Hello world!

"); + const { body } = svelteRender(App); + expect(body).toBe("

Hello world!

"); }); }); diff --git a/test/package.json b/test/package.json index 03a8717ce3..f643ef682d 100644 --- a/test/package.json +++ b/test/package.json @@ -21,6 +21,7 @@ "axios": "1.6.8", "body-parser": "1.20.2", "comlink": "4.4.1", + "devalue": "5.1.1", "es-module-lexer": "1.3.0", "esbuild": "0.18.6", "express": "4.18.2", @@ -59,7 +60,7 @@ "string-width": "7.0.0", "stripe": "15.4.0", "supertest": "6.3.3", - "svelte": "3.55.1", + "svelte": "5.4.0", "typescript": "5.0.2", "undici": "5.20.0", "verdaccio": "6.0.0",