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)
+
+ Clicked {count} {count === 1 ? 'time' : 'times'}
+
+
+
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",