Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(DevServer): batch bundles & run them asynchronously #15181

Merged
merged 45 commits into from
Nov 26, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
45 commits
Select commit Hold shift + click to select a range
266d3e3
basic async bundling
paperdave Nov 15, 2024
7009557
stuff
paperdave Nov 15, 2024
acdacfc
a
paperdave Nov 15, 2024
eb6f5f8
production revival
paperdave Nov 15, 2024
2dabaf1
Merge remote-tracking branch 'origin/main' into dave/bake-async-bundling
paperdave Nov 15, 2024
fa1d2cf
more baking
paperdave Nov 16, 2024
b2b0fde
half support the clientless route bundles
paperdave Nov 16, 2024
6e8fc52
BakeEntryPoints -> DevServer.EntryPointList
paperdave Nov 16, 2024
4e5efc3
typedef change
paperdave Nov 16, 2024
52481bc
oops
paperdave Nov 16, 2024
56d2fde
ok
paperdave Nov 18, 2024
036e969
Merge remote-tracking branch 'origin/main' into dave/bake-async-bundling
paperdave Nov 18, 2024
0648c3c
a
paperdave Nov 19, 2024
356ca22
Merge remote-tracking branch 'origin/main' into dave/bake-async-bundling
paperdave Nov 19, 2024
d80bbe2
Merge remote-tracking branch 'origin/main' into dave/bake-async-bundling
paperdave Nov 19, 2024
1c7c18f
prelim css fix but this is not the right fix
paperdave Nov 19, 2024
9bde98a
Merge remote-tracking branch 'origin/main' into dave/bake-async-bundling
paperdave Nov 19, 2024
0ea8058
stuff
paperdave Nov 20, 2024
bc05978
put bake behind runtime feature flag, remove bitrotted wipDevServer
paperdave Nov 20, 2024
f82d55c
Merge remote-tracking branch 'origin/main' into dave/bake-async-bundling
paperdave Nov 20, 2024
cc08285
fix compilation
paperdave Nov 20, 2024
9ad7bf8
Merge branch 'main' into dave/bake-async-bundling
Electroid Nov 20, 2024
1c0f582
self review
paperdave Nov 20, 2024
023d63e
wow!
paperdave Nov 20, 2024
448a365
sthfdsaf
paperdave Nov 20, 2024
2dce36d
work on the css
paperdave Nov 21, 2024
587ba23
css test
paperdave Nov 21, 2024
9cdd455
a
paperdave Nov 21, 2024
7832111
Merge remote-tracking branch 'origin/main' into dave/bake-async-bundling
paperdave Nov 22, 2024
60eea0c
hi
paperdave Nov 22, 2024
a075d37
now it worky on safari lol
paperdave Nov 22, 2024
1ed842f
Merge remote-tracking branch 'origin/main' into dave/bake-async-bundling
paperdave Nov 22, 2024
1ab6025
ok
paperdave Nov 22, 2024
0331283
run a formatter on files
paperdave Nov 22, 2024
e0e7617
hi
paperdave Nov 23, 2024
a8574f3
a
paperdave Nov 22, 2024
9222fda
asycn
paperdave Nov 23, 2024
fb780b8
Merge remote-tracking branch 'origin/main' into dave/bake-async-bundling
paperdave Nov 23, 2024
64586b1
a
paperdave Nov 23, 2024
df042e0
a
paperdave Nov 23, 2024
b05fd34
fix assertion failure
paperdave Nov 23, 2024
0db4b28
disable on windows sorry
paperdave Nov 23, 2024
936f72c
Merge remote-tracking branch 'origin/main' into dave/bake-async-bundling
paperdave Nov 25, 2024
bd7eb17
disable css test for now(dev server fault)
paperdave Nov 25, 2024
32dd4c6
Merge remote-tracking branch 'origin/main' into dave/bake-async-bundling
paperdave Nov 26, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions src/analytics/analytics_thread.zig
Original file line number Diff line number Diff line change
Expand Up @@ -99,8 +99,8 @@ pub const Features = struct {
pub var https_server: usize = 0;
/// Set right before JSC::initialize is called
pub var jsc: usize = 0;
/// Set when kit.DevServer is initialized
pub var kit_dev: usize = 0;
/// Set when bake.DevServer is initialized
pub var dev_server: usize = 0;
pub var lifecycle_scripts: usize = 0;
pub var loaders: usize = 0;
pub var lockfile_migration_from_package_lock: usize = 0;
Expand Down
2,112 changes: 1,371 additions & 741 deletions src/bake/DevServer.zig

Large diffs are not rendered by default.

253 changes: 195 additions & 58 deletions src/bake/FrameworkRouter.zig

Large diffs are not rendered by default.

26 changes: 11 additions & 15 deletions src/bake/bake.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -255,6 +255,7 @@ declare module "bun" {
* Do not traverse into directories and files that start with an `_`. Do
* not index pages that start with an `_`. Does not prevent stuff like
* `_layout.tsx` from being recognized.
* @default false
*/
ignoreUnderscores?: boolean;
/**
Expand All @@ -264,8 +265,9 @@ declare module "bun" {
/**
* Extensions to match on.
* '*' - any extension
* @default (set of all valid JavaScript/TypeScript extensions)
*/
extensions: string[] | "*";
extensions?: string[] | "*";
/**
* 'nextjs-app' builds routes out of directories with `page.tsx` and `layout.tsx`
* 'nextjs-pages' builds routes out of any `.tsx` file and layouts with `_layout.tsx`.
Expand Down Expand Up @@ -421,15 +423,7 @@ declare module "bun" {
}

interface ClientEntryPoint {
/**
* Called when server-side code is changed. This can be used to fetch a
* non-html version of the updated page to perform a faster reload. If
* this function does not exist or throws, the client will perform a
* hard reload.
*
* Tree-shaken away in production builds.
*/
onServerSideReload?: () => Promise<void> | void;
// No exports
}

/**
Expand Down Expand Up @@ -459,7 +453,7 @@ declare module "bun" {
/**
* A list of js files that the route will need to be interactive.
*/
readonly scripts: ReadonlyArray<string>;
readonly modules: ReadonlyArray<string>;
/**
* A list of js files that should be preloaded.
*
Expand Down Expand Up @@ -547,9 +541,11 @@ declare module "bun:bake/server" {

declare module "bun:bake/client" {
/**
* Due to the current implementation of the Dev Server, it must be informed of
* client-side routing so it can load client components. This is not necessary
* in production, and calling this in that situation will fail to compile.
* Callback is invoked when server-side code is changed. This can be used to
* fetch a non-html version of the updated page to perform a faster reload. If
* not provided, the client will perform a hard reload.
*
* Only one callback can be set. This function overwrites the previous one.
*/
declare function bundleRouteForDevelopment(href: string, options?: { signal?: AbortSignal }): Promise<void>;
export function onServerSideReload(cb: () => void | Promise<void>): Promise<void>;
}
8 changes: 4 additions & 4 deletions src/bake/bake.private.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,11 +47,11 @@ declare var __bun_f: any;

// The following interfaces have been transcribed manually.

declare module "react-server-dom-webpack/client.browser" {
declare module "react-server-dom-bun/client.browser" {
export function createFromReadableStream<T = any>(readable: ReadableStream<Uint8Array>): Promise<T>;
}

declare module "react-server-dom-webpack/client.node.unbundled.js" {
declare module "react-server-dom-bun/client.node.unbundled.js" {
import type { ReactClientManifest } from "bun:bake/server";
import type { Readable } from "node:stream";
export interface Manifest {
Expand All @@ -70,7 +70,7 @@ declare module "react-server-dom-webpack/client.node.unbundled.js" {
export function createFromNodeStream<T = any>(readable: Readable, manifest?: Manifest): Promise<T>;
}

declare module "react-server-dom-webpack/server.node.unbundled.js" {
declare module "react-server-dom-bun/server.node.unbundled.js" {
import type { ReactServerManifest } from "bun:bake/server";
import type { ReactElement, ReactElement } from "react";
import type { Writable } from "node:stream";
Expand Down Expand Up @@ -98,7 +98,7 @@ declare module "react-server-dom-webpack/server.node.unbundled.js" {
}

declare module "react-dom/server.node" {
import type { PipeableStream } from "react-server-dom-webpack/server.node.unbundled.js";
import type { PipeableStream } from "react-server-dom-bun/server.node.unbundled.js";
import type { ReactElement } from "react";

export type RenderToPipeableStreamOptions = any;
Expand Down
131 changes: 79 additions & 52 deletions src/bake/bake.zig
Original file line number Diff line number Diff line change
Expand Up @@ -12,15 +12,16 @@ pub const api_name = "app";

/// Zig version of the TS definition 'Bake.Options' in 'bake.d.ts'
pub const UserOptions = struct {
arena: std.heap.ArenaAllocator.State,
arena: std.heap.ArenaAllocator,
allocations: StringRefList,

root: []const u8,
framework: Framework,
bundler_options: SplitBundlerOptions,
// bundler_plugin: ?*Plugin,

pub fn deinit(options: *UserOptions) void {
options.arena.promote(bun.default_allocator).deinit();
options.arena.deinit();
options.allocations.free();
}

Expand Down Expand Up @@ -60,7 +61,7 @@ pub const UserOptions = struct {
};

return .{
.arena = arena.state,
.arena = arena,
.allocations = allocations,
.root = root,
.framework = framework,
Expand Down Expand Up @@ -99,31 +100,13 @@ const BuildConfigSubset = struct {
conditions: bun.StringArrayHashMapUnmanaged(void) = .{},
drop: bun.StringArrayHashMapUnmanaged(void) = .{},
// TODO: plugins
};

/// Temporary function to invoke dev server via JavaScript. Will be
/// replaced with a user-facing API. Refs the event loop forever.
pub fn jsWipDevServer(global: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) bun.JSError!JSValue {
_ = global;
_ = callframe;

if (!bun.FeatureFlags.bake) return .undefined;

bun.Output.errGeneric(
\\This api has moved to the `app` property of the default export.
\\
\\ export default {{
\\ port: 3000,
\\ app: {{
\\ framework: 'react'
\\ }},
\\ }};
\\
,
.{},
);
return .undefined;
}
pub fn loadFromJs(config: *BuildConfigSubset, value: JSValue, arena: Allocator) !void {
_ = config; // autofix
_ = value; // autofix
_ = arena; // autofix
}
};

/// A "Framework" in our eyes is simply set of bundler options that a framework
/// author would set in order to integrate the framework with the application.
Expand All @@ -132,6 +115,7 @@ pub fn jsWipDevServer(global: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) bu
///
/// Full documentation on these fields is located in the TypeScript definitions.
pub const Framework = struct {
is_built_in_react: bool,
file_system_router_types: []FileSystemRouterType,
// static_routers: [][]const u8,
server_components: ?ServerComponents = null,
Expand All @@ -141,9 +125,10 @@ pub const Framework = struct {
/// Bun provides built-in support for using React as a framework.
/// Depends on externally provided React
///
/// $ bun i react@experimental react-dom@experimental react-server-dom-webpack@experimental react-refresh@experimental
/// $ bun i react@experimental react-dom@experimental react-refresh@experimental react-server-dom-bun
pub fn react(arena: std.mem.Allocator) !Framework {
return .{
.is_built_in_react = true,
.server_components = .{
.separate_ssr_graph = true,
.server_runtime_import = "react-server-dom-bun/server",
Expand All @@ -158,7 +143,8 @@ pub const Framework = struct {
.ignore_underscores = true,
.ignore_dirs = &.{ "node_modules", ".git" },
.extensions = &.{ ".tsx", ".jsx" },
.style = .@"nextjs-pages-ui",
.style = .nextjs_pages,
.allow_layouts = true,
},
}),
// .static_routers = try arena.dupe([]const u8, &.{"public"}),
Expand Down Expand Up @@ -188,6 +174,7 @@ pub const Framework = struct {
ignore_dirs: []const []const u8,
extensions: []const []const u8,
style: FrameworkRouter.Style,
allow_layouts: bool,
};

const BuiltInModule = union(enum) {
Expand All @@ -208,11 +195,22 @@ pub const Framework = struct {
import_source: []const u8 = "react-refresh/runtime",
};

/// Given a Framework configuration, this returns another one with all modules resolved.
pub const react_install_command = "bun i react@experimental react-dom@experimental react-refresh@experimental react-server-dom-bun";

pub fn addReactInstallCommandNote(log: *bun.logger.Log) !void {
try log.addMsg(.{
.kind = .note,
.data = try bun.logger.rangeData(null, bun.logger.Range.none, "Install the built in react integration with \"" ++ react_install_command ++ "\"")
.cloneLineText(log.clone_line_text, log.msgs.allocator),
});
}

/// Given a Framework configuration, this returns another one with all paths resolved.
/// New memory allocated into provided arena.
///
/// All resolution errors will happen before returning error.ModuleNotFound
/// Details written into `r.log`
pub fn resolve(f: Framework, server: *bun.resolver.Resolver, client: *bun.resolver.Resolver) !Framework {
/// Errors written into `r.log`
pub fn resolve(f: Framework, server: *bun.resolver.Resolver, client: *bun.resolver.Resolver, arena: Allocator) !Framework {
var clone = f;
var had_errors: bool = false;

Expand All @@ -226,8 +224,7 @@ pub const Framework = struct {
}

for (clone.file_system_router_types) |*fsr| {
// TODO: unonwned memory
fsr.root = bun.path.joinAbs(server.fs.top_level_dir, .auto, fsr.root);
fsr.root = try arena.dupe(u8, bun.path.joinAbs(server.fs.top_level_dir, .auto, fsr.root));
if (fsr.entry_client) |*entry_client| f.resolveHelper(client, entry_client, &had_errors, "client side entrypoint");
f.resolveHelper(client, &fsr.entry_server, &had_errors, "server side entrypoint");
}
Expand Down Expand Up @@ -384,6 +381,7 @@ pub const Framework = struct {

var it = array.arrayIterator(global);
var i: usize = 0;
errdefer for (file_system_router_types[0..i]) |*fsr| fsr.style.deinit();
while (it.next()) |fsr_opts| : (i += 1) {
const root = try getOptionalString(fsr_opts, global, "root", refs, arena) orelse {
return global.throwInvalidArguments("'fileSystemRouterTypes[{d}]' is missing 'root'", .{i});
Expand All @@ -394,14 +392,12 @@ pub const Framework = struct {
const client_entry_point = try getOptionalString(fsr_opts, global, "clientEntryPoint", refs, arena);
const prefix = try getOptionalString(fsr_opts, global, "prefix", refs, arena) orelse "/";
const ignore_underscores = try fsr_opts.getBooleanStrict(global, "ignoreUnderscores") orelse false;
const layouts = try fsr_opts.getBooleanStrict(global, "layouts") orelse false;

const style = try validators.validateStringEnum(
FrameworkRouter.Style,
global,
try opts.getOptional(global, "style", JSValue) orelse .undefined,
"style",
.{},
);
var style = try FrameworkRouter.Style.fromJS(try fsr_opts.get(global, "style") orelse {
return global.throwInvalidArguments("'fileSystemRouterTypes[{d}]' is missing 'style'", .{i});
}, global);
errdefer style.deinit();

const extensions: []const []const u8 = if (try fsr_opts.get(global, "extensions")) |exts_js| exts: {
if (exts_js.isString()) {
Expand All @@ -415,8 +411,18 @@ pub const Framework = struct {
var i_2: usize = 0;
const extensions = try arena.alloc([]const u8, len);
while (it_2.next()) |array_item| : (i_2 += 1) {
// TODO: remove/add the prefix `.`, throw error if specifying '*' as an array item instead of as root
extensions[i_2] = refs.track(try array_item.toSlice2(global, arena));
const slice = refs.track(try array_item.toSlice2(global, arena));
if (bun.strings.eqlComptime(slice, "*"))
return global.throwInvalidArguments("'extensions' cannot include \"*\" as an extension. Pass \"*\" instead of the array.", .{});

if (slice.len == 0) {
return global.throwInvalidArguments("'extensions' cannot include \"\" as an extension.", .{});
}

extensions[i_2] = if (slice[0] == '.')
slice
else
try std.mem.concat(arena, u8, &.{ ".", slice });
}
break :exts extensions;
}
Expand Down Expand Up @@ -447,13 +453,16 @@ pub const Framework = struct {
.ignore_underscores = ignore_underscores,
.extensions = extensions,
.ignore_dirs = ignore_dirs,
.allow_layouts = layouts,
};
}

break :brk file_system_router_types;
};
errdefer for (file_system_router_types) |*fsr| fsr.style.deinit();

const framework: Framework = .{
.is_built_in_react = false,
.file_system_router_types = file_system_router_types,
.react_fast_refresh = react_fast_refresh,
.server_components = server_components,
Expand Down Expand Up @@ -517,9 +526,9 @@ pub const Framework = struct {
out.options.production = mode != .development;

out.options.tree_shaking = mode != .development;
out.options.minify_syntax = true; // required for DCE
// out.options.minify_identifiers = mode != .development;
// out.options.minify_whitespace = mode != .development;
out.options.minify_syntax = mode != .development;
out.options.minify_identifiers = mode != .development;
out.options.minify_whitespace = mode != .development;

out.options.experimental_css = true;
out.options.css_chunking = true;
Expand Down Expand Up @@ -560,11 +569,6 @@ fn getOptionalString(
return allocations.track(str.toUTF8(arena));
}

export fn Bun__getTemporaryDevServer(global: *JSC.JSGlobalObject) JSValue {
if (!bun.FeatureFlags.bake) return .undefined;
return JSC.JSFunction.create(global, "wipDevServer", jsWipDevServer, 0, .{});
}

pub inline fn getHmrRuntime(side: Side) [:0]const u8 {
return if (Environment.codegen_embed)
switch (side) {
Expand All @@ -586,6 +590,13 @@ pub const Mode = enum {
pub const Side = enum(u1) {
client,
server,

pub fn graph(s: Side) Graph {
return switch (s) {
.client => .client,
.server => .server,
};
}
};
pub const Graph = enum(u2) {
client,
Expand Down Expand Up @@ -670,6 +681,7 @@ pub const PatternBuffer = struct {
pub fn prependPart(pb: *PatternBuffer, part: FrameworkRouter.Part) void {
switch (part) {
.text => |text| {
bun.assert(text.len == 0 or text[0] != '/');
pb.prepend(text);
pb.prepend("/");
},
Expand All @@ -686,13 +698,28 @@ pub const PatternBuffer = struct {
}
};

pub fn printWarning() void {
// Silence this for the test suite
if (bun.getenvZ("BUN_DEV_SERVER_TEST_RUNNER") == null) {
bun.Output.warn(
\\Be advised that Bun Bake is highly experimental, and its API
\\will have breaking changes. Join the <magenta>#bake<r> Discord
\\channel to help us find bugs: <blue>https://bun.sh/discord<r>
\\
\\
, .{});
bun.Output.flush();
}
}

const std = @import("std");
const Allocator = std.mem.Allocator;

const bun = @import("root").bun;
const Environment = bun.Environment;
const ZigString = bun.JSC.ZigString;

const JSC = bun.JSC;
const JSValue = JSC.JSValue;
const validators = bun.JSC.Node.validators;
const ZigString = JSC.ZigString;
const Plugin = JSC.API.JSBundler.Plugin;
Loading