From baff3c900e3c908f62b321eb84b726876aded0b3 Mon Sep 17 00:00:00 2001 From: dave caruso Date: Tue, 3 Dec 2024 22:15:59 -0800 Subject: [PATCH] bake: fix the big regressions (#15544) Co-authored-by: paperdave --- .vscode/launch.json | 2 +- src/bake/BakeGlobalObject.cpp | 61 +++- src/bake/BakeGlobalObject.h | 4 + src/bake/BakeSourceProvider.cpp | 6 +- src/bake/BakeSourceProvider.h | 8 +- src/bake/DevServer.zig | 6 +- src/bake/bake.d.ts | 4 +- src/bake/bake.zig | 18 +- src/bake/production.zig | 308 ++++++++++++------ src/bun.js/ConsoleObject.zig | 11 +- src/bun.js/api/server.zig | 5 +- src/bun.js/bindings/bindings.cpp | 5 +- src/bun.js/bindings/bindings.zig | 9 +- src/bundler/bundle_v2.zig | 22 +- src/cli.zig | 4 + src/js/builtins/Bake.ts | 5 +- src/js_ast.zig | 5 +- src/js_parser.zig | 13 +- src/options.zig | 58 ++-- test/bake/dev-server-harness.ts | 78 +++-- test/bake/dev/ecosystem.test.ts | 20 +- test/bake/dev/esm.test.ts | 71 +++- .../svelte-component-islands/bun.app.ts | 8 + .../framework/client.ts | 14 + .../framework/index.ts | 68 ++++ .../framework/server.ts | 71 ++++ .../pages/_Counter.svelte | 24 ++ .../pages/index.svelte | 18 + test/bun.lockb | Bin 375426 -> 380850 bytes test/js/bun/plugin/plugins.test.ts | 16 +- .../third_party/svelte/bun-loader-svelte.ts | 3 +- test/js/third_party/svelte/svelte.test.ts | 14 +- test/package.json | 3 +- 33 files changed, 748 insertions(+), 214 deletions(-) create mode 100644 test/bake/fixtures/svelte-component-islands/bun.app.ts create mode 100644 test/bake/fixtures/svelte-component-islands/framework/client.ts create mode 100644 test/bake/fixtures/svelte-component-islands/framework/index.ts create mode 100644 test/bake/fixtures/svelte-component-islands/framework/server.ts create mode 100644 test/bake/fixtures/svelte-component-islands/pages/_Counter.svelte create mode 100644 test/bake/fixtures/svelte-component-islands/pages/index.svelte 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 6a1061e911b76ab6b2492a1669ec0bb3ba2fb9ed..699279fcb23528b9d24e1f7fbf0fc08fe1dd6b90 100755 GIT binary patch delta 47020 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 Khf!~*vh_dxwmxG3 delta 43704 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�RY7%Yq3 zbURW3=oaKYa6hOBboUVf^yTEcfWEAJ1Kb3+fDYDP-~(a64|I2O5$KNO63|_U?mEVs z;IXh`o%F9_Mu3rE6i5Z5!5Hu`cm#|E7%Ag9U3XakLx=T3$J_Dz~X>dl<`7D8R;0y32I1es>i{KLY4qOJ`gDc<%a25Or zeggZ!Ti|VQ0K5bA^A~%;KJW(66nhok0y;5KI)g5ttIte39+ofdC3OAs!6jfRSO(^T zd0;*;U;$VN76IK3^#lFE05A{?0?FVZFa!(*DPR~F4n}~HpeyJGx`Q4d(a*&1Mc^`( z{2u5Q>Ib0PrXRsiK$lNlE>D5eK-YU+-_HSE({&BkDdht*<3w1Av||L110C~ptkW^> z4X_971v;(01$6S#n$XEg$3z_|b%fNxSqEhujCBy!!S@xQv+ib~^R3RaI=||?s`Kd@ zpo2L#Oli&y(qs(X>^udY22;T_FdaMtW`JkGOfU<~2HfCyLSQbK2j&9~W5PoNE`1L}eVP!H4xaiA8c4Rog#4JNa>Oa;@y4Dc+N31)*i zKzC~MO|H+vsz>X#Mz=Amfo@&&Gq!%9Tb67f+$4S$Ryr4ttvvk9xrg5@_Y^c!KMN~X zVE{96Akf)TXUQR8C`bXi2jR7|T&Y7$s z+z2!VO@WS3`sRKwu%EDgtMFx@n~<(RHy--!gX|y&$O&?R+~97I2jm6$z-CtNtAHPI z@LUDD8Tm>FK>d2eQE(ib04Kp`pdkIY8eR*u$M=DgKu^#Pv(3_+C*RjSOr#tl0c`PnP4J#42%Vjf<7Pt=<3)2 z=<27dT^R6#yFd_R2RT4akPGAnK5&f+{to^GH^6Tu!+nP!UuHWk6YQgwcK+C^#(;;xBS6Py9h3E&v75-_ zRj>v01v(Ty2J3dLp8h0&`d}9kQ-JPLbS~Qho&Yz%HE<8|X{D^*{qq8^nV;;1hgygO(tzHGkTI+CX0wSxu|cKn-vS=kLH}pr1HB4bFgb zK&RC&!BKDwtN;cq01JVBYgVUMokY`V+@J78@GUq44pY`1ZHarSslHv@6f^@Jh|mGQ z4$z&$6|`r8o*r!?p-!f6l4d`63%m_J0tdjm;4<<0m7l{vzcq6Oo&aitugGr?=ty3J z;r$x_7inoX&>B2RO{ajTz%-yIkF&sCKsSC(fSy!!0Choa&=lwiR1El?LVg1q!H?iP zm;v&TcLA`9@QXlac0G*hn8u$7YEv2fL5=cb7XW1lccxJNc=0U4y+M7D4g5hy>%eI6 z2p9|GtLHWSK{99udIRYlU_E+i4DJD6qv?K4_h)IB`STOdec7MjCh$;b7$}1C7uaF2 zeqQ@ia2)(X+;wmme1xsXA^JIPJp$1q5B)N7f6xK!L(|iZx4>GUCm0)mewCdk7imrn zAH=8*DuC^je+#q*Z9sF-5~wA*p)XGllmq&~B*nF|al6oFgQ;K|m<~FFrl2vX1gd}t zPz>Y&ck7bqA#jrxIt8c@iF`&ZuBU#m zU>vlAeg$+C(2ltde|F1W&bi`cgw&=QJoHCrXv8ffea8Tw7=SS1> z){j8u0oi~b=#UkFHG6Y{93Tkp0^uM#kdHXGweN;C#0B8{!F~D(cy;Z)7!`o_k9$Bl zPzIC&B|&iz3Ce=fAR3eaQJ@&m(kToIfg+$NkiXI?z4B3h%2)ZToVzKvqOO{G2vmY~ zjj0B7)36_{I=m0A59^$e0O|tmN%25uhFTyF#DW-56Ep^mKvOW4UThDy1Dab8z-_^M zgmn{Gunk6Q@EArbxFu*|I{nJgvopa?Kv!d3XmztV1IXn;AbSc>(*D@}fYwJ}kOX>w z?w}jc^pI~K!o5K+kO+FpM%UU}1zrI20dq9XlS*JD7y&d&$?zaB7z_s&(1yapKni#W z3<1(IcogAzU>X<)rh=z|7Q#eW8{i}0VK4@a21+|bQ$q=q@mR2%1d5md#)GH8lVA#% z3?8$#7K}!95=aM6fX9J!`DbM{NUc=`X9Ja|d}jgYzYd^UJp-l#)lLpGz_T{2UYSF9 zE(lred3X_6Xzc~C0m@kEgEV9bycjG4?BdRL!S&j?%;kbtprW%WaSt`u5q<){ zfS*5`9NNvqxsz+H^>E?Y$_+|psWjJIap7^^u$3=y=s9tP!s4LsXEZzqIzGe zhZ4{25lbg>EvlQ%p5j*UeZs#zM9RS!efVpebkq zbdGNX8iEF(KB#97{uy>plhy=V0i9iSPumsf7U5y=7R7XfyMX)2R$RKDR(}$I z9{Z$u`XKZJ1Hd3K5a>iP0@edfJ=h!!h5|iJc?j0ilw>#t9s=tDsC=Z!N323snDW&# z08QoJ!J~vVg~!2Tb%VH%!ZgRnBS=(O^H*~=4f_E67#s=r15*jBrB4C1R6U_ycpRPt zrhq5G6JW9pD~)`g2J%yQ8fc=0Kaap%5CU_+Y%mMV1kZvQ;CY};)4yp6HMur5mwwP@B+xlFD)ZOIqe{#D9q{3xvAfdy%}r(Tfio;9;^ee zfS18bU@h1PUIa=n{|vv3>zjPm*m(H}*lA8c8UAbJzh;z?feKNpUbUI2vPszKa7JY_ z3Rj-GU}pF#tbDhkZ*%ftI`1N|+cDVw;XU9@un+7NuJVV^`)1xDzxE5y)+_!`z2=P1 zAIe5%e6~Ll=I<1it$L-JH7ix!{&6<{fmUXDM}JY@m@}RIt$p=oZoj{azn$0DXO?-a ztG}8rakeSi&0o0${}yB1Z0FdQ{%axc^kT^+_V~Py#Z;nbCg<$=-TV>OcVOgWyh-MK zm|k&Lt!2*@{rnJ)H7iw<j^ z+s&Wudo^T+cE`3wydE;kyZbNr+Rru9dQiQdbIlt)h<{C4{u)%ybYYI#$|N9SB4_BB#S5&DKt15MwZ}KG46}b&x2_hxDA(0gKEO1V{>C4Nk zE0g=+jUJ@1Rm9^g9=_BC=69vMwb1nK<*yc*eGwN0%2>Or&$lZdANa7}TcuKUr@NyU znOAy|B4&{pmPFp2@QlLq0e`WmlpJp+`MtDVt@n&tWNymy*+r(LJU8H39M5h0p5Gjk z@BFGv&qIq$>)v>LjYlaw`gH6N@m7n^iEV#@Q1yZ|2GKmGKn@Ma?)&#YG+ zV$>_+zw7%$&L`GBpBd9(v3Xx<2ivrhKJeZ1;1j-ynI02}DN5QaT~E);-EwQ;%$WI$ zO=KTx_7Wa>@i@D$;LRtG5B@6CW6xsKP9C4w+*>{3ZQ1^(Q+YBy-d$pz?89i-HvB~^ zGy37*_mu5RpY7Yf=zVv5s`=tpn&*=k(I;0r*VXj93iK{8{$j(Iomo?@5^Le;O7jCA zzIHE|GJXB``ntVf+V%C<_BB~$p6N@=`mAy%_|{wB{g}Pzp|XB&jY>5%-8>UknZ0<# zEXG44n{D*R{fEB)lP}X_Cov_6d9|!RvC4(X`7>jVuQIv&k@hz{H0IN1-dM7|blz_> zJ&LS$bN#wO+3;T)HGV5ICU&*yN!rMEcxdiLJ^Fz6{De=_Gd)sPn^}1Hrmi;Y`q8#Q z>&+S2(HqP)+2=Qy`}<>G*<87{ z6;a;CwD>SG*mL~R$B40OC1$gEV=$#P+H8)KM`Y2h+$K=UqIMts*!}(a*%>T`N~@~$ zR#QKj`owJA{%o>8#_K!3b^C#b{64R*+75GQ2u+CJVQvngW1H`AYx-f`ZpVi796jI3 zql#+Ui5QMp>GNLO-FjPOu&uKlIBT}w4wHKX^%%Ls^b|k7!=w*o=i0H;_)`2^d=++? z9VwJiYnS;h#Xm6ez%EW_ZeVZ-q3`ug_ghf&X%bS@oo zeZ6(B37*ZJ4?2Z9+vIr~n}e83m(AQEzYX>Mm+Hh!BQ1C9>508s=BW72#A%rx3yIP8 z{B`NrGb>;JU?nl?Y#Qk{p_(stkC~;Wr0+57Mld`nd)>SO$69$uKVLIFGp{#^Q9Z(^ zHoEKcmCx6;dC}=d_L}=g(wM{hOrMeDb!wk^bR@%cexJMNtjl@y=B-Ab)*yu)&_DK> zFGl(s`&AC*l;K!5kjH8~cz8d>Y|55&G1&-mNmR$RN{NOjc z&Rym6?%i+Jk+#5}#AvwsciaEwvb&cDeBN*Nn=^_j{+63-)$CU<-P^PPvmpChCSR(* zqi@w)W^k(i{m88c+==wcuitE6)AC?7%4MK6KyMr{9Y(V+yn{zxa-UWrFm4H8!^|zxB7^wG}eD_ znH_ir@!SWiCkIZM{cW*dc8?(iwNw2*I$|b_rSDH4F|Ujz?~67?p}F01ram?LFrIAQ z^8E40_OHfj=U`wjj3ddhk4@yGB>C-Q=Po5Zy6UW{2YekL^Lv#yj^3o_8pFX3Uy@m}R8(9sP&d@hGLOJ!Za>z5AHS zHlFEq@|YPmp1i;2EvuUL$tNYccV8RKRlwb0sj%L?79nQ-_X9_~^@abb0^VQo)uvM9 zxOsg%c`i6^eh`0m+?1I>o}EsbXC_d_x|3%01plJQw?1>buXM`w-cuU1?4?1|hE>IL zqd1S^lhlOJwBF?qP6kt$8Cl6#4Nv9T%%tsvgG!ho8-enbf8!9vYJq-#%?h&slRM z(<70X2x69PJTvt7KAoJysGD}o&*lxKoq>mDTU62KF0S8IvUq0N)i$P6&Wn==bbGW} zX3UX`gw1YweMnCKt{0$amd6ZdK?z=b?M{g*DBLse0X< zQq9`oQJUQE{Vusc{srM9Gd)tTn>@40eI_0{RNdR6MUnhhHmu3?*g#B4Vg?sD_-cVO zt50Ue9KLRPk=FO!bu)Ihe^Jz~o9;#S?Bs%tFQ@NF^U=Dh&R8G1=`oe(_^XEDvc2CN z|Mal1*|$9A>O6m|u%!g*&G#1$+e9GOQUZqvd~W=O&9wRcw!UXQ=8Ywkl5v??&cH_r4idkUhXvTG=BCr=#1CQ zBB$7^ctqgwL$$AVjEQZO<&YBec|8S*>3FQyxN*Ddyg*tl5@xNkHbpc6Lv=P&XX46v|r1x%HNERWIwvtS{)zY;Kq7c$gsvU#0} zv}^3Q?TrmCj|39r6?M@co$Glv?6XkKB^O?aH2^>LwPCV}O##E|S zQ{!W2P2^rY@{m{jw#gO0{9y!pSB8h3M;X(us_(-brv6e!vOrF+Q^5{jt{yVC@5`Mj zp6g(BcC)CQ=2>|pTaOECrrmw?XtohPZ;s0DX774L{6)nmw%0Q9_>hXJ8^%Y(|DOC* z$ANjg33aQ+X!^Pyt$mkrnGMV6jzDg&bLOb=!j>sbI}MA`J!aKPHJBCAxy@NTViNGs z;(g}p5{YYTmRy+W(Sw-c#01MHxA3pnu|G3rTy9h8Ino+Dbg$N_+1j>^l0Rsb>9LI% zE&Q(!Y?-;e=-!mfn4`JP1k(Dp<}=GyFtl&yGl!mobL2NSm1jwwKj$N5*$U0S7(47! z&aTd~Vb)e7MvJl7yVJ&3`MuO`XYi|3V(vG|Z(1*>wTXCWS%39&t*56va(S#x%a9Js zZ_@GbrR6v6R^T}YPYvyh2iwLj^~`@Yvy|uaoA>3pIlsBEoGF(BPpzwnmtV`%rVc+% zk zl#{Kab>wGg+_5ubHG&zHcgv43=`Z>p z_tlLw{xwXEBT?>j+jF+p7j1^u@9Xp4%i&0$=;CJR8v3GYakCtbj4$DO?tbRyrUkBy zJCNzwx`g>2kL(c(^eTfB#zJ%NT7P{rVy%C9+Odz_%jfU&o5$yUYxVxV$-^o=l*${C zO(#Dqcg#C))luGW)1r61FE5Ik^Zfin*lOW79S*(o&}YF%j$v28F7s=Fg8gE?`d+4Pz} udi$DMfjUXsi!Kg4RXzK1n`X^Uv;O@+#T>`2{o~HSy!ZTNP51|ab^i}LXETNX 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",