diff options
-rw-r--r-- | examples/nexty/client.development.tsx | 5 | ||||
-rw-r--r-- | examples/nexty/index.js | 0 | ||||
-rw-r--r-- | examples/nexty/package.json | 30 | ||||
-rw-r--r-- | examples/nexty/server.development.tsx | 46 | ||||
-rw-r--r-- | examples/nexty/server.production.tsx | 0 | ||||
-rw-r--r-- | src/api/schema.d.ts | 2 | ||||
-rw-r--r-- | src/api/schema.js | 11 | ||||
-rw-r--r-- | src/api/schema.peechy | 2 | ||||
-rw-r--r-- | src/api/schema.zig | 6 | ||||
-rw-r--r-- | src/bundler.zig | 157 | ||||
-rw-r--r-- | src/cli.zig | 7 | ||||
-rw-r--r-- | src/http.zig | 38 | ||||
-rw-r--r-- | src/javascript/jsc/javascript.zig | 89 | ||||
-rw-r--r-- | src/js_ast.zig | 8 | ||||
-rw-r--r-- | src/js_lexer.zig | 1 | ||||
-rw-r--r-- | src/js_parser/js_parser.zig | 180 | ||||
-rw-r--r-- | src/logger.zig | 2 | ||||
-rw-r--r-- | src/options.zig | 23 | ||||
-rw-r--r-- | src/resolver/package_json.zig | 50 | ||||
-rw-r--r-- | src/resolver/resolver.zig | 25 | ||||
-rw-r--r-- | src/runtime.js | 6 | ||||
-rw-r--r-- | src/runtime.version | 2 | ||||
-rw-r--r-- | src/runtime.zig | 8 |
23 files changed, 515 insertions, 183 deletions
diff --git a/examples/nexty/client.development.tsx b/examples/nexty/client.development.tsx deleted file mode 100644 index 7f517ab78..000000000 --- a/examples/nexty/client.development.tsx +++ /dev/null @@ -1,5 +0,0 @@ -import ReactDOM from "react-dom"; - -export function start(EntryPointNamespace) { - ReactDOM.hydrate(<EntryPointNamespace.default />); -} diff --git a/examples/nexty/index.js b/examples/nexty/index.js deleted file mode 100644 index e69de29bb..000000000 --- a/examples/nexty/index.js +++ /dev/null diff --git a/examples/nexty/package.json b/examples/nexty/package.json deleted file mode 100644 index d1e984fdc..000000000 --- a/examples/nexty/package.json +++ /dev/null @@ -1,30 +0,0 @@ -{ - "name": "nexty", - "version": "1.0.0", - "description": "", - "framework": { - "router": { - "dir": "pages", - "extensions": [ - ".js", - ".ts", - ".tsx" - ] - }, - "static": "public", - "development": { - "client": "client.development.tsx", - "server": "server.development.tsx" - }, - "production": { - "client": "client.production.tsx", - "server": "server.production.tsx" - } - }, - "scripts": { - "test": "echo \"Error: no test specified\" && exit 1" - }, - "author": "", - "license": "ISC", - "dependencies": {} -} diff --git a/examples/nexty/server.development.tsx b/examples/nexty/server.development.tsx deleted file mode 100644 index 0e28dac65..000000000 --- a/examples/nexty/server.development.tsx +++ /dev/null @@ -1,46 +0,0 @@ -import ReactDOMServer from "react-dom/server.browser"; - -addEventListener("fetch", async (event: FetchEvent) => { - var route = Wundle.match(event); - - console.log("Main:", Wundle.main); - console.log("cwd:", Wundle.cwd); - console.log("Origin:", Wundle.origin); - - const { default: PageComponent } = await import(route.filePath); - // This returns all .css files that were imported in the line above. - // It's recursive, so any file that imports a CSS file will be included. - const stylesheets = Wundle.getImportedStyles() as string[]; - - const response = new Response(` - <!DOCTYPE html> -<html> - <head> - ${stylesheets - .map((style) => `<link rel="stylesheet" href="${style}">`) - .join("\n")} - - <link - rel="stylesheet" - crossorigin="anonymous" - href="https://fonts.googleapis.com/css2?family=IBM+Plex+Sans:wght@400;700&family=Space+Mono:wght@400;700" - /> - </head> - <body> - - <div id="reactroot">${ReactDOMServer.renderToString( - <PageComponent /> - )}</div> - - <script src="${route.scriptSrc}" async type="module"></script> - </body> -</html> - `); - - event.respondWith(response); -}); - -// typescript isolated modules -export {}; - -declare var Wundle: any; diff --git a/examples/nexty/server.production.tsx b/examples/nexty/server.production.tsx deleted file mode 100644 index e69de29bb..000000000 --- a/examples/nexty/server.production.tsx +++ /dev/null diff --git a/src/api/schema.d.ts b/src/api/schema.d.ts index 0b80700c5..4e652ae81 100644 --- a/src/api/schema.d.ts +++ b/src/api/schema.d.ts @@ -285,7 +285,7 @@ type uint32 = number; } export interface RouteConfig { - dir?: string; + dir?: string[]; extensions?: string[]; static_dir?: string; asset_prefix?: string; diff --git a/src/api/schema.js b/src/api/schema.js index 3a76caedf..5149c1ffd 100644 --- a/src/api/schema.js +++ b/src/api/schema.js @@ -867,7 +867,9 @@ function decodeRouteConfig(bb) { return result; case 1: - result["dir"] = bb.readString(); + var length = bb.readVarUint(); + var values = result["dir"] = Array(length); + for (var i = 0; i < length; i++) values[i] = bb.readString(); break; case 2: @@ -895,7 +897,12 @@ function encodeRouteConfig(message, bb) { var value = message["dir"]; if (value != null) { bb.writeByte(1); - bb.writeString(value); + var values = value, n = values.length; + bb.writeVarUint(n); + for (var i = 0; i < n; i++) { + value = values[i]; + bb.writeString(value); + } } var value = message["extensions"]; diff --git a/src/api/schema.peechy b/src/api/schema.peechy index 431fab3c1..863c69d6a 100644 --- a/src/api/schema.peechy +++ b/src/api/schema.peechy @@ -178,7 +178,7 @@ struct LoadedRouteConfig { } message RouteConfig { - string dir = 1; + string[] dir = 1; string[] extensions = 2; string static_dir = 3; string asset_prefix = 4; diff --git a/src/api/schema.zig b/src/api/schema.zig index ffb949a1f..60eceb1ad 100644 --- a/src/api/schema.zig +++ b/src/api/schema.zig @@ -1027,7 +1027,7 @@ pub fn encode(this: *const @This(), writer: anytype) anyerror!void { pub const RouteConfig = struct { /// dir -dir: ?[]const u8 = null, +dir: []const []const u8, /// extensions extensions: []const []const u8, @@ -1047,7 +1047,7 @@ pub fn decode(reader: anytype) anyerror!RouteConfig { 0 => { return this; }, 1 => { - this.dir = try reader.readValue([]const u8); + this.dir = try reader.readArray([]const u8); }, 2 => { this.extensions = try reader.readArray([]const u8); @@ -1069,7 +1069,7 @@ unreachable; pub fn encode(this: *const @This(), writer: anytype) anyerror!void { if (this.dir) |dir| { try writer.writeFieldID(1); - try writer.writeValue(dir); + try writer.writeArray([]const u8, dir); } if (this.extensions) |extensions| { try writer.writeFieldID(2); diff --git a/src/bundler.zig b/src/bundler.zig index 97e9f692c..0e731eeab 100644 --- a/src/bundler.zig +++ b/src/bundler.zig @@ -130,6 +130,58 @@ pub const ClientEntryPoint = struct { } }; +pub const ServerEntryPoint = struct { + code_buffer: [std.fs.MAX_PATH_BYTES * 2 + 500]u8 = undefined, + output_code_buffer: [std.fs.MAX_PATH_BYTES * 8 + 500]u8 = undefined, + source: logger.Source = undefined, + + pub fn generate( + entry: *ServerEntryPoint, + comptime BundlerType: type, + bundler: *BundlerType, + original_path: Fs.PathName, + name: string, + + ) !void { + + // This is *extremely* naive. + // The basic idea here is this: + // -- + // import * as EntryPoint from 'entry-point'; + // import boot from 'framework'; + // boot(EntryPoint); + // -- + // We go through the steps of printing the code -- only to then parse/transpile it because + // we want it to go through the linker and the rest of the transpilation process + + const dir_to_use: string = original_path.dirWithTrailingSlash(); + + const code = try std.fmt.bufPrint( + &entry.code_buffer, + \\//Auto-generated file + \\import * as start from '{s}{s}'; + \\if ('default' in start && typeof start.default === 'function') {{ + \\ const result = start.default(); + \\ if (result && typeof result === 'object' && result instanceof Promise) {{ + \\ result.then(undefined, undefined); + \\ }} + \\}} + \\export * from '{s}{s}'; + , + .{ + dir_to_use, + original_path.filename, + dir_to_use, + original_path.filename, + }, + ); + + entry.source = logger.Source.initPathString(name, code); + entry.source.path.text = name; + entry.source.path.namespace = "server-entry"; + } +}; + pub const ResolveResults = std.AutoHashMap(u64, void); pub const ResolveQueue = std.fifo.LinearFifo(_resolver.Result, std.fifo.LinearFifoBufferType.Dynamic); @@ -528,6 +580,20 @@ pub fn NewBundler(cache_files: bool) type { pub const current_version: u32 = 1; + pub fn enqueueItem(this: *GenerateNodeModuleBundle, resolve: _resolver.Result) !void { + var this_module_resolved_path = try this.resolved_paths.getOrPut(resolve.path_pair.primary.text); + + if (!this_module_resolved_path.found_existing) { + if (resolve.isLikelyNodeModule()) { + if (BundledModuleData.get(this, &resolve)) |module_data_| { + this_module_resolved_path.value_ptr.* = module_data_.module_id; + } + } + + this.resolve_queue.writeItemAssumeCapacity(resolve); + } + } + // The Speedy Bundle Format // Your entire node_modules folder in a single compact file designed for web browsers. // A binary JavaScript bundle format prioritizing bundle time and serialization/deserialization time @@ -621,75 +687,87 @@ pub fn NewBundler(cache_files: bool) type { if (bundler.log.level == .verbose) { bundler.resolver.debug_logs = try DebugLogs.init(allocator); } + temp_stmts_list = @TypeOf(temp_stmts_list).init(allocator); + const include_refresh_runtime = !this.bundler.options.production and this.bundler.options.jsx.supports_fast_refresh and bundler.options.platform.isWebLike(); + + const resolve_queue_estimate = bundler.options.entry_points.len + + @intCast(usize, @boolToInt(framework_config != null)) + + @intCast(usize, @boolToInt(include_refresh_runtime)) + + @intCast(usize, @boolToInt(bundler.options.jsx.parse)); if (bundler.router) |router| { + defer this.bundler.resetStore(); + const entry_points = try router.getEntryPoints(allocator); - try this.resolve_queue.ensureUnusedCapacity(entry_points.len + bundler.options.entry_points.len + @intCast(usize, @boolToInt(framework_config != null))); + try this.resolve_queue.ensureUnusedCapacity(entry_points.len + resolve_queue_estimate); for (entry_points) |entry_point| { const source_dir = bundler.fs.top_level_dir; const resolved = try bundler.linker.resolver.resolve(source_dir, entry_point, .entry_point); - this.resolve_queue.writeItemAssumeCapacity(resolved); + try this.enqueueItem(resolved); } this.bundler.resetStore(); } else { - try this.resolve_queue.ensureUnusedCapacity(bundler.options.entry_points.len + @intCast(usize, @boolToInt(framework_config != null))); + try this.resolve_queue.ensureUnusedCapacity(resolve_queue_estimate); } for (bundler.options.entry_points) |entry_point| { + defer this.bundler.resetStore(); + const entry_point_path = bundler.normalizeEntryPointPath(entry_point); const source_dir = bundler.fs.top_level_dir; const resolved = try bundler.linker.resolver.resolve(source_dir, entry_point, .entry_point); - this.resolve_queue.writeItemAssumeCapacity(resolved); + try this.enqueueItem(resolved); } if (framework_config) |conf| { + defer this.bundler.resetStore(); + if (conf.client) { if (bundler.configureFrameworkWithResolveResult(true)) |result_| { if (result_) |result| { - this.resolve_queue.writeItemAssumeCapacity(result); + try this.enqueueItem(result); } } else |err| {} } else { if (bundler.configureFrameworkWithResolveResult(false)) |result_| { if (result_) |result| { - this.resolve_queue.writeItemAssumeCapacity(result); + try this.enqueueItem(result); } } else |err| {} } } - this.bundler.resetStore(); - - while (this.resolve_queue.readItem()) |resolved| { - try this.processFile(resolved); - } - // Normally, this is automatic // However, since we only do the parsing pass, it may not get imported automatically. - if (this.has_jsx) { + if (bundler.options.jsx.parse) { + defer this.bundler.resetStore(); if (this.bundler.resolver.resolve( this.bundler.fs.top_level_dir, this.bundler.options.jsx.import_source, - .stmt, + .require, )) |new_jsx_runtime| { - if (!this.resolved_paths.contains(new_jsx_runtime.path_pair.primary.text)) { - try this.processFile(new_jsx_runtime); - } + try this.enqueueItem(new_jsx_runtime); } else |err| {} } - if (this.has_jsx and this.bundler.options.jsx.supports_fast_refresh) { + if (include_refresh_runtime) { + defer this.bundler.resetStore(); + if (this.bundler.resolver.resolve( this.bundler.fs.top_level_dir, - "react-refresh/runtime", + this.bundler.options.jsx.refresh_runtime, .require, )) |refresh_runtime| { - if (!this.resolved_paths.contains(refresh_runtime.path_pair.primary.text)) { - try this.processFile(refresh_runtime); - } + try this.enqueueItem(refresh_runtime); } else |err| {} } + this.bundler.resetStore(); + + while (this.resolve_queue.readItem()) |resolved| { + try this.processFile(resolved); + } + if (this.log.errors > 0) { // We stop here because if there are errors we don't know if the bundle is valid // This manifests as a crash when sorting through the module list because we may have added files to the bundle which were never actually finished being added. @@ -891,6 +969,8 @@ pub fn NewBundler(cache_files: bool) type { fn processImportRecord(this: *GenerateNodeModuleBundle, import_record: ImportRecord) !void {} threadlocal var package_key_buf: [512]u8 = undefined; threadlocal var file_path_buf: [4096]u8 = undefined; + + threadlocal var temp_stmts_list: std.ArrayList(js_ast.Stmt) = undefined; fn processFile(this: *GenerateNodeModuleBundle, _resolve: _resolver.Result) !void { var resolve = _resolve; if (resolve.is_external) return; @@ -929,7 +1009,7 @@ pub fn NewBundler(cache_files: bool) type { var opts = js_parser.Parser.Options.init(jsx, loader); opts.transform_require_to_import = false; opts.enable_bundling = true; - + opts.warn_about_unbundled_modules = false; var ast: js_ast.Ast = (try bundler.resolver.caches.js.parse( bundler.allocator, opts, @@ -1024,12 +1104,26 @@ pub fn NewBundler(cache_files: bool) type { const E = js_ast.E; const Expr = js_ast.Expr; const Stmt = js_ast.Stmt; + + var prepend_part: js_ast.Part = undefined; + var needs_prepend_part = false; + if (ast.parts.len > 1) { + for (ast.parts) |part| { + if (part.tag != .none and part.stmts.len > 0) { + prepend_part = part; + needs_prepend_part = true; + break; + } + } + } + if (ast.parts.len == 0) { if (comptime isDebug) { Output.prettyErrorln("Missing AST for file: {s}", .{file_path.text}); Output.flush(); } } + var part = &ast.parts[ast.parts.len - 1]; var new_stmts: [1]Stmt = undefined; var register_args: [3]Expr = undefined; @@ -1058,6 +1152,7 @@ pub fn NewBundler(cache_files: bool) type { .data = .{ .b_identifier = &exports_binding }, }, }; + var closure = E.Arrow{ .args = &cjs_args, .body = .{ @@ -1101,6 +1196,9 @@ pub fn NewBundler(cache_files: bool) type { var writer = js_printer.NewFileWriter(this.tmpfile); var symbols: [][]js_ast.Symbol = &([_][]js_ast.Symbol{ast.symbols}); + // It should only have one part. + ast.parts = ast.parts[ast.parts.len - 1 ..]; + const code_offset = @truncate(u32, try this.tmpfile.getPos()); const written = @truncate( u32, @@ -1119,6 +1217,8 @@ pub fn NewBundler(cache_files: bool) type { .indent = 0, .module_hash = module_id, .runtime_imports = ast.runtime_imports, + .prepend_part_value = &prepend_part, + .prepend_part_key = if (needs_prepend_part) closure.body.stmts.ptr else null, }, Linker, &bundler.linker, @@ -1219,7 +1319,6 @@ pub fn NewBundler(cache_files: bool) type { try this.resolve_queue.writeItem(_resolved_import.*); if (BundledModuleData.get(this, resolved_import)) |module| { - import_record.path = Fs.Path.init(module.import_path); get_or_put_result.value_ptr.* = module.module_id; } } else |err| { @@ -1301,7 +1400,12 @@ pub fn NewBundler(cache_files: bool) type { errdefer bundler.resetStore(); var file_path = resolve_result.path_pair.primary; - file_path.pretty = allocator.dupe(u8, bundler.fs.relativeTo(file_path.text)) catch unreachable; + + if (strings.indexOf(file_path.text, bundler.fs.top_level_dir)) |i| { + file_path.pretty = file_path.text[i + bundler.fs.top_level_dir.len ..]; + } else { + file_path.pretty = allocator.dupe(u8, bundler.fs.relativeTo(file_path.text)) catch unreachable; + } var old_bundler_allocator = bundler.allocator; bundler.allocator = allocator; @@ -1711,9 +1815,10 @@ pub fn NewBundler(cache_files: bool) type { opts.enable_bundling = false; opts.transform_require_to_import = true; opts.can_import_from_bundle = bundler.options.node_modules_bundle != null; - opts.features.hot_module_reloading = bundler.options.hot_module_reloading and bundler.options.platform != .speedy and client_entry_point_ == null; + opts.features.hot_module_reloading = bundler.options.hot_module_reloading and bundler.options.platform != .speedy; // and client_entry_point_ == null; opts.features.react_fast_refresh = opts.features.hot_module_reloading and jsx.parse and bundler.options.jsx.supports_fast_refresh; opts.filepath_hash_for_hmr = file_hash orelse 0; + opts.warn_about_unbundled_modules = bundler.options.platform != .speedy; const value = (bundler.resolver.caches.js.parse(allocator, opts, bundler.options.define, bundler.log, &source) catch null) orelse return null; return ParseResult{ .ast = value, diff --git a/src/cli.zig b/src/cli.zig index da4186a67..3158f421c 100644 --- a/src/cli.zig +++ b/src/cli.zig @@ -229,7 +229,7 @@ pub const Cli = struct { var route_config: ?Api.RouteConfig = null; if (args.option("--static-dir")) |public_dir| { - route_config = route_config orelse Api.RouteConfig{ .extensions = &.{} }; + route_config = route_config orelse Api.RouteConfig{ .extensions = &.{}, .dir = &.{} }; route_config.?.static_dir = public_dir; } @@ -429,6 +429,7 @@ pub const Cli = struct { var env_loader = this_bundler.env; wait_group = sync.WaitGroup.init(); var server_bundler_generator_thread: ?std.Thread = null; + var generated_server = false; if (this_bundler.options.framework) |*framework| { if (framework.toAPI(allocator, this_bundler.fs.top_level_dir, false)) |_server_conf| { const ServerBundleGeneratorThread = struct { @@ -506,6 +507,7 @@ pub const Cli = struct { this_bundler.router, }, ); + generated_server = true; } else { ServerBundleGeneratorThread.generate( log_, @@ -516,6 +518,7 @@ pub const Cli = struct { loaded_route_config, this_bundler.router, ); + generated_server = true; } } } @@ -551,7 +554,7 @@ pub const Cli = struct { const indent = comptime " "; Output.prettyln(indent ++ "<d>{d:6}ms elapsed", .{@intCast(u32, elapsed)}); - if (server_bundler_generator_thread != null) { + if (generated_server) { Output.prettyln(indent ++ "<r>Saved to ./{s}, ./{s}", .{ filepath, server_bundle_filepath }); } else { Output.prettyln(indent ++ "<r>Saved to ./{s}", .{filepath}); diff --git a/src/http.zig b/src/http.zig index c47c1dbec..4a528626c 100644 --- a/src/http.zig +++ b/src/http.zig @@ -496,7 +496,7 @@ pub const RequestContext = struct { allocator: *std.mem.Allocator, printer: js_printer.BufferPrinter, timer: std.time.Timer, - + count: usize = 0, pub const WatchBuildResult = struct { value: Value, id: u32, @@ -513,6 +513,13 @@ pub const RequestContext = struct { }; }; pub fn build(this: *WatchBuilder, id: u32, from_timestamp: u32) !WatchBuildResult { + if (this.count == 0) { + var writer = try js_printer.BufferWriter.init(this.allocator); + this.printer = js_printer.BufferPrinter.init(writer); + this.printer.ctx.append_null_byte = false; + } + + defer this.count += 1; var log = logger.Log.init(this.allocator); errdefer log.deinit(); @@ -789,7 +796,7 @@ pub const RequestContext = struct { entry_point, ) catch |err| { Output.prettyErrorln( - "<r>JavaScript VM failed to start.\n<red>{s}:<r> while loading <r><b>\"\"", + "<r>JavaScript VM failed to start.\n<red>{s}:<r> while loading <r><b>\"{s}\"", .{ @errorName(err), entry_point }, ); @@ -920,18 +927,20 @@ pub const RequestContext = struct { message_buffer: MutableString, pub var open_websockets: std.ArrayList(*WebsocketHandler) = undefined; var open_websockets_lock = sync.RwLock.init(); - pub fn addWebsocket(ctx: *RequestContext) !*WebsocketHandler { + pub fn addWebsocket(ctx: *RequestContext, server: *Server) !*WebsocketHandler { open_websockets_lock.lock(); defer open_websockets_lock.unlock(); - var clone = try ctx.allocator.create(WebsocketHandler); + + var clone = try server.allocator.create(WebsocketHandler); clone.ctx = ctx.*; clone.conn = ctx.conn.*; - clone.message_buffer = try MutableString.init(ctx.allocator, 0); + clone.message_buffer = try MutableString.init(server.allocator, 0); clone.ctx.conn = &clone.conn; - var printer_writer = try js_printer.BufferWriter.init(ctx.allocator); + + var printer_writer = try js_printer.BufferWriter.init(server.allocator); clone.builder = WatchBuilder{ - .allocator = ctx.allocator, + .allocator = server.allocator, .bundler = ctx.bundler, .printer = js_printer.BufferPrinter.init(printer_writer), .timer = ctx.timer, @@ -940,6 +949,7 @@ pub const RequestContext = struct { clone.websocket = Websocket.Websocket.create(&clone.conn, SOCKET_FLAGS); clone.tombstone = false; + try open_websockets.append(clone); return clone; } @@ -1017,6 +1027,9 @@ pub const RequestContext = struct { Output.enable_ansi_colors = stderr.isTty(); js_ast.Stmt.Data.Store.create(self.ctx.allocator); js_ast.Expr.Data.Store.create(self.ctx.allocator); + self.builder.printer = js_printer.BufferPrinter.init( + js_printer.BufferWriter.init(self.ctx.allocator) catch unreachable, + ); _handle(self) catch {}; } @@ -1032,7 +1045,6 @@ pub const RequestContext = struct { switch (err) { error.BadRequest => { try ctx.sendBadRequest(); - ctx.done(); }, else => { return err; @@ -1277,9 +1289,9 @@ pub const RequestContext = struct { return false; } - pub fn handleWebsocket(ctx: *RequestContext) anyerror!void { + pub fn handleWebsocket(ctx: *RequestContext, server: *Server) anyerror!void { ctx.controlled = true; - var handler = try WebsocketHandler.addWebsocket(ctx); + var handler = try WebsocketHandler.addWebsocket(ctx, server); _ = try std.Thread.spawn(.{}, WebsocketHandler.handle, .{handler}); } @@ -1590,14 +1602,14 @@ pub const RequestContext = struct { } } - pub fn handleReservedRoutes(ctx: *RequestContext) !bool { + pub fn handleReservedRoutes(ctx: *RequestContext, server: *Server) !bool { if (strings.eqlComptime(ctx.url.extname, "jsb") and ctx.bundler.options.node_modules_bundle != null) { try ctx.sendJSB(); return true; } if (strings.eqlComptime(ctx.url.path, "_api.hmr")) { - try ctx.handleWebsocket(); + try ctx.handleWebsocket(server); return true; } @@ -1879,7 +1891,7 @@ pub const Server = struct { req_ctx.keep_alive = false; } - var finished = req_ctx.handleReservedRoutes() catch |err| { + var finished = req_ctx.handleReservedRoutes(server) catch |err| { Output.printErrorln("FAIL [{s}] - {s}: {s}", .{ @errorName(err), req.method, req.path }); return; }; diff --git a/src/javascript/jsc/javascript.zig b/src/javascript/jsc/javascript.zig index 21756bf9f..29ede6112 100644 --- a/src/javascript/jsc/javascript.zig +++ b/src/javascript/jsc/javascript.zig @@ -8,7 +8,9 @@ const logger = @import("../../logger.zig"); const Api = @import("../../api/schema.zig").Api; const options = @import("../../options.zig"); const Bundler = @import("../../bundler.zig").ServeBundler; +const ServerEntryPoint = @import("../../bundler.zig").ServerEntryPoint; const js_printer = @import("../../js_printer.zig"); +const js_parser = @import("../../js_parser.zig"); const hash_map = @import("../../hash_map.zig"); const http = @import("../../http.zig"); const ImportKind = ast.ImportKind; @@ -22,7 +24,7 @@ const Runtime = @import("../../runtime.zig"); const Router = @import("./api/router.zig"); const ImportRecord = ast.ImportRecord; const DotEnv = @import("../../env_loader.zig"); - +const ParseResult = @import("../../bundler.zig").ParseResult; pub const GlobalClasses = [_]type{ Request.Class, Response.Class, @@ -275,6 +277,7 @@ pub const VirtualMachine = struct { process: js.JSObjectRef = null, flush_list: std.ArrayList(string), + entry_point: ServerEntryPoint = undefined, pub var vm_loaded = false; pub var vm: *VirtualMachine = undefined; @@ -390,9 +393,77 @@ pub const VirtualMachine = struct { Runtime.Runtime.byteCodeCacheFile(&vm.bundler.fs.fs) orelse 0, ), }; + // This is all complicated because the imports have to be linked and we want to run the printer on it + // so it consistently handles bundled imports + // we can't take the shortcut of just directly importing the file, sadly. + } else if (strings.eqlComptime(_specifier, main_file_name)) { + var bundler = &vm.bundler; + var old = vm.bundler.log; + vm.bundler.log = log; + vm.bundler.linker.log = log; + vm.bundler.resolver.log = log; + defer { + vm.bundler.log = old; + vm.bundler.linker.log = old; + vm.bundler.resolver.log = old; + } + + var jsx = bundler.options.jsx; + jsx.parse = false; + var opts = js_parser.Parser.Options.init(jsx, .js); + opts.enable_bundling = false; + opts.transform_require_to_import = true; + opts.can_import_from_bundle = bundler.options.node_modules_bundle != null; + opts.features.hot_module_reloading = false; + opts.features.react_fast_refresh = false; + opts.filepath_hash_for_hmr = 0; + opts.warn_about_unbundled_modules = false; + const main_ast = (bundler.resolver.caches.js.parse(vm.allocator, opts, bundler.options.define, bundler.log, &vm.entry_point.source) catch null) orelse { + return error.ParseError; + }; + var parse_result = ParseResult{ .source = vm.entry_point.source, .ast = main_ast, .loader = .js, .input_fd = null }; + var file_path = Fs.Path.init(bundler.fs.top_level_dir); + file_path.name.dir = bundler.fs.top_level_dir; + file_path.name.base = "bun:main"; + try bundler.linker.link( + file_path, + &parse_result, + .absolute_path, + true, + ); + + if (!source_code_printer_loaded) { + var writer = try js_printer.BufferWriter.init(vm.allocator); + source_code_printer = js_printer.BufferPrinter.init(writer); + source_code_printer.ctx.append_null_byte = false; + + source_code_printer_loaded = true; + } + + source_code_printer.ctx.reset(); + + var written = try vm.bundler.print( + parse_result, + @TypeOf(&source_code_printer), + &source_code_printer, + .esm, + ); + + if (written == 0) { + return error.PrintingErrorWriteFailed; + } + + return ResolvedSource{ + .source_code = ZigString.init(vm.allocator.dupe(u8, source_code_printer.ctx.written) catch unreachable), + .specifier = ZigString.init(std.mem.span(main_file_name)), + .source_url = ZigString.init(std.mem.span(main_file_name)), + .hash = 0, + .bytecodecache_fd = 0, + }; } const specifier = normalizeSpecifier(_specifier); + std.debug.assert(std.fs.path.isAbsolute(specifier)); // if this crashes, it means the resolver was skipped. const path = Fs.Path.init(specifier); @@ -496,10 +567,14 @@ pub const VirtualMachine = struct { } else if (vm.node_modules != null and strings.eql(specifier, vm.bundler.linker.nodeModuleBundleImportPath())) { ret.path = vm.bundler.linker.nodeModuleBundleImportPath(); return; + } else if (strings.eqlComptime(specifier, main_file_name)) { + ret.result = null; + ret.path = vm.entry_point.source.path.text; + return; } const result = try vm.bundler.resolver.resolve( - Fs.PathName.init(source).dirWithTrailingSlash(), + if (!strings.eqlComptime(source, main_file_name)) Fs.PathName.init(source).dirWithTrailingSlash() else VirtualMachine.vm.bundler.fs.top_level_dir, specifier, .stmt, ); @@ -605,6 +680,7 @@ pub const VirtualMachine = struct { return slice; } + const main_file_name: string = "bun:main"; threadlocal var errors_stack: [256]*c_void = undefined; pub fn fetch(ret: *ErrorableResolvedSource, global: *JSGlobalObject, specifier: ZigString, source: ZigString) callconv(.C) void { var log = logger.Log.init(vm.bundler.allocator); @@ -705,11 +781,10 @@ pub const VirtualMachine = struct { } } - pub fn loadEntryPoint(this: *VirtualMachine, entry_point: string) !*JSInternalPromise { - var path = this.bundler.normalizeEntryPointPath(entry_point); - - this.main = entry_point; - var promise = JSModuleLoader.loadAndEvaluateModule(this.global, ZigString.init(path)); + pub fn loadEntryPoint(this: *VirtualMachine, entry_path: string) !*JSInternalPromise { + try this.entry_point.generate(@TypeOf(this.bundler), &this.bundler, Fs.PathName.init(entry_path), main_file_name); + this.main = entry_path; + var promise = JSModuleLoader.loadAndEvaluateModule(this.global, ZigString.init(std.mem.span(main_file_name))); this.global.vm().drainMicrotasks(); diff --git a/src/js_ast.zig b/src/js_ast.zig index 8e388a103..f6bd9e24e 100644 --- a/src/js_ast.zig +++ b/src/js_ast.zig @@ -3757,6 +3757,14 @@ pub const Part = struct { // This is true if this file has been marked as live by the tree shaking // algorithm. is_live: bool = false, + + tag: Tag = Tag.none, + + pub const Tag = enum { + none, + jsx_import, + }; + pub const SymbolUseMap = AutoHashMap(Ref, Symbol.Use); pub fn jsonStringify(self: *const Part, options: std.json.StringifyOptions, writer: anytype) !void { return std.json.stringify(self.stmts, options, writer); diff --git a/src/js_lexer.zig b/src/js_lexer.zig index cf1470cab..2ddd25f61 100644 --- a/src/js_lexer.zig +++ b/src/js_lexer.zig @@ -689,6 +689,7 @@ pub const Lexer = struct { // what caused us to get on this slow path in the first place. if (lexer.code_point == '\\') { try lexer.step(); + if (lexer.code_point != 'u') { try lexer.syntaxError(); } diff --git a/src/js_parser/js_parser.zig b/src/js_parser/js_parser.zig index 4bc70eb01..cf36c89e6 100644 --- a/src/js_parser/js_parser.zig +++ b/src/js_parser/js_parser.zig @@ -1680,6 +1680,8 @@ pub const Parser = struct { filepath_hash_for_hmr: u32 = 0, features: RuntimeFeatures = RuntimeFeatures{}, + warn_about_unbundled_modules: bool = true, + // Used when bundling node_modules enable_bundling: bool = false, transform_require_to_import: bool = true, @@ -1822,6 +1824,8 @@ pub const Parser = struct { var parts = List(js_ast.Part).init(p.allocator); try p.appendPart(&parts, stmts); + var did_import_fast_refresh = false; + // Auto-import JSX if (p.options.jsx.parse) { const jsx_symbol: Symbol = p.symbols.items[p.jsx_runtime_ref.inner_index]; @@ -1869,7 +1873,7 @@ pub const Parser = struct { @intCast(u32, @boolToInt(jsx_filename_symbol.use_count_estimate > 0)); const imports_count = - @intCast(u32, @boolToInt(jsx_symbol.use_count_estimate > 0)) + @intCast(u32, std.math.max(jsx_factory_symbol.use_count_estimate, jsx_fragment_symbol.use_count_estimate)); + @intCast(u32, @boolToInt(jsx_symbol.use_count_estimate > 0)) + @intCast(u32, std.math.max(jsx_factory_symbol.use_count_estimate, jsx_fragment_symbol.use_count_estimate)) + @intCast(u32, @boolToInt(p.options.features.react_fast_refresh)); const stmts_count = imports_count + 1; const symbols_count: u32 = imports_count + decls_count; const loc = logger.Loc{ .start = 0 }; @@ -1896,7 +1900,7 @@ pub const Parser = struct { const automatic_identifier = p.e(E.Identifier{ .ref = automatic_namespace_ref }, loc); const dot_call_target = brk: { - if (p.options.can_import_from_bundle) { + if (p.options.can_import_from_bundle or p.options.enable_bundling) { break :brk automatic_identifier; } else { require_call_args_base[require_call_args_i] = automatic_identifier; @@ -1973,7 +1977,7 @@ pub const Parser = struct { const dot_call_target = brk: { // var react = $aopaSD123(); - if (p.options.can_import_from_bundle) { + if (p.options.can_import_from_bundle or p.options.enable_bundling) { break :brk classic_identifier; } else { require_call_args_base[require_call_args_i] = classic_identifier; @@ -2051,6 +2055,36 @@ pub const Parser = struct { declared_symbols_i += 1; } + if (p.options.features.react_fast_refresh) { + defer did_import_fast_refresh = true; + const refresh_runtime_symbol: Symbol = p.symbols.items[p.jsx_refresh_runtime_ref.inner_index]; + + declared_symbols[declared_symbols_i] = .{ .ref = p.jsx_refresh_runtime_ref, .is_top_level = true }; + declared_symbols_i += 1; + + const import_record_id = p.addImportRecord(.require, loc, p.options.jsx.refresh_runtime); + jsx_part_stmts[stmt_i] = p.s(S.Import{ + .namespace_ref = p.jsx_refresh_runtime_ref, + .star_name_loc = loc, + .is_single_line = true, + .import_record_index = import_record_id, + }, loc); + + stmt_i += 1; + p.named_imports.put( + p.jsx_refresh_runtime_ref, + js_ast.NamedImport{ + .alias = refresh_runtime_symbol.original_name, + .alias_is_star = true, + .alias_loc = loc, + .namespace_ref = p.jsx_refresh_runtime_ref, + .import_record_index = import_record_id, + }, + ) catch unreachable; + p.is_import_item.put(p.jsx_refresh_runtime_ref, true) catch unreachable; + import_records[import_record_i] = import_record_id; + } + jsx_part_stmts[stmt_i] = p.s(S.Local{ .kind = .k_var, .decls = decls[0..decl_i] }, loc); stmt_i += 1; @@ -2059,10 +2093,50 @@ pub const Parser = struct { .declared_symbols = declared_symbols, .import_record_indices = import_records, .symbol_uses = SymbolUseMap.init(p.allocator), + .tag = .jsx_import, }) catch unreachable; } } + if (!did_import_fast_refresh and p.options.features.react_fast_refresh) { + std.debug.assert(!p.options.enable_bundling); + var declared_symbols = try p.allocator.alloc(js_ast.DeclaredSymbol, 1); + const loc = logger.Loc.Empty; + const import_record_id = p.addImportRecord(.require, loc, p.options.jsx.refresh_runtime); + var import_stmt = p.s(S.Import{ + .namespace_ref = p.jsx_refresh_runtime_ref, + .star_name_loc = loc, + .is_single_line = true, + .import_record_index = import_record_id, + }, loc); + + p.recordUsage(p.jsx_refresh_runtime_ref); + const refresh_runtime_symbol: Symbol = p.symbols.items[p.jsx_refresh_runtime_ref.inner_index]; + p.named_imports.put( + p.jsx_refresh_runtime_ref, + js_ast.NamedImport{ + .alias = refresh_runtime_symbol.original_name, + .alias_is_star = true, + .alias_loc = loc, + .namespace_ref = p.jsx_refresh_runtime_ref, + .import_record_index = import_record_id, + }, + ) catch unreachable; + p.is_import_item.put(p.jsx_refresh_runtime_ref, true) catch unreachable; + var import_records = try p.allocator.alloc(@TypeOf(import_record_id), 1); + import_records[0] = import_record_id; + declared_symbols[0] = .{ .ref = p.jsx_refresh_runtime_ref, .is_top_level = true }; + var part_stmts = try p.allocator.alloc(Stmt, 1); + part_stmts[0] = import_stmt; + + before.append(js_ast.Part{ + .stmts = part_stmts, + .declared_symbols = declared_symbols, + .import_record_indices = import_records, + .symbol_uses = SymbolUseMap.init(p.allocator), + }) catch unreachable; + } + // Analyze cross-part dependencies for tree shaking and code splitting var exports_kind = js_ast.ExportsKind.none; const uses_exports_ref = p.symbols.items[p.exports_ref.inner_index].use_count_estimate > 0; @@ -2102,6 +2176,7 @@ pub const Parser = struct { ) catch unreachable; } } + if (has_cjs_imports) { var import_records = try p.allocator.alloc(u32, p.cjs_import_stmts.items.len); var declared_symbols = try p.allocator.alloc(js_ast.DeclaredSymbol, p.cjs_import_stmts.items.len); @@ -2467,6 +2542,9 @@ pub fn NewParser( jsx_automatic_ref: js_ast.Ref = Ref.None, jsx_classic_ref: js_ast.Ref = Ref.None, + // only applicable when is_react_fast_refresh_enabled + jsx_refresh_runtime_ref: js_ast.Ref = Ref.None, + jsx_source_list_ref: js_ast.Ref = Ref.None, // Imports (both ES6 and CommonJS) are tracked at the top level @@ -2651,9 +2729,11 @@ pub fn NewParser( }, state.loc); } - // Use a debug log so people can see this if they want to - const r = js_lexer.rangeOfIdentifier(p.source, state.loc); - p.log.addRangeDebug(p.source, r, "This \"import\" expression will not be bundled because the argument is not a string literal") catch unreachable; + if (p.options.warn_about_unbundled_modules) { + // Use a debug log so people can see this if they want to + const r = js_lexer.rangeOfIdentifier(p.source, state.loc); + p.log.addRangeDebug(p.source, r, "This \"import\" expression cannot be bundled because the argument is not a string literal") catch unreachable; + } return p.e(E.Import{ .expr = arg, @@ -3242,10 +3322,18 @@ pub fn NewParser( if (p.options.features.hot_module_reloading) { p.hmr_module_ref = try p.declareSymbol(.hoisted, logger.Loc.Empty, "__hmrModule"); - p.runtime_imports.__HMRModule = try p.declareSymbol(.hoisted, logger.Loc.Empty, "__HMRModule"); + if (is_react_fast_refresh_enabled) { + p.jsx_refresh_runtime_ref = try p.declareSymbol(.hoisted, logger.Loc.Empty, "__RefreshRuntime"); + + p.runtime_imports.__FastRefreshModule = try p.declareSymbol(.hoisted, logger.Loc.Empty, "__FastRefreshModule"); + p.recordUsage(p.runtime_imports.__FastRefreshModule.?); + } else { + p.runtime_imports.__HMRModule = try p.declareSymbol(.hoisted, logger.Loc.Empty, "__HMRModule"); + p.recordUsage(p.runtime_imports.__HMRModule.?); + } + p.runtime_imports.__HMRClient = try p.declareSymbol(.hoisted, logger.Loc.Empty, "__HMRClient"); p.recordUsage(p.hmr_module_ref); - p.recordUsage(p.runtime_imports.__HMRModule.?); p.recordUsage(p.runtime_imports.__HMRClient.?); } else { p.runtime_imports.__export = p.exports_ref; @@ -11254,7 +11342,7 @@ pub fn NewParser( // error from the unbundled require() call failing. if (e_.args.len == 1) { return p.require_transposer.maybeTransposeIf(e_.args[0], null); - } else { + } else if (p.options.warn_about_unbundled_modules) { const r = js_lexer.rangeOfIdentifier(p.source, e_.target.loc); p.log.addRangeDebug(p.source, r, "This call to \"require\" will not be bundled because it has multiple arguments") catch unreachable; } @@ -13697,9 +13785,17 @@ pub fn NewParser( // 6. __hmrModule.onSetExports = (newExports) => { // $named_exports_count __hmrExport_exportName = newExports.exportName; <-- ${named_exports_count} // } + + // if there are no exports: + // - there shouldn't be an export statement + // - we don't need the S.Local for wrapping the exports + // We still call exportAll just with an empty object. + const has_any_exports = named_exports_count > 0; + + const toplevel_stmts_count = 4 + (@intCast(usize, @boolToInt(has_any_exports)) * 2); var _stmts = p.allocator.alloc( Stmt, - end_iife_stmts_count + 6 + (named_exports_count * 2) + imports_count + exports_from_count, + end_iife_stmts_count + toplevel_stmts_count + (named_exports_count * 2) + imports_count + exports_from_count, ) catch unreachable; // Normally, we'd have to grow that inner function's stmts list by one // But we can avoid that by just making them all use this same array. @@ -13715,7 +13811,7 @@ pub fn NewParser( // Second pass: move any imports from the part's stmts array to the new stmts var imports_list = curr_stmts[0..imports_count]; curr_stmts = curr_stmts[imports_list.len..]; - var toplevel_stmts = curr_stmts[0..6]; + var toplevel_stmts = curr_stmts[0..toplevel_stmts_count]; curr_stmts = curr_stmts[toplevel_stmts.len..]; var exports_from = curr_stmts[0..exports_from_count]; curr_stmts = curr_stmts[exports_from.len..]; @@ -13752,21 +13848,27 @@ pub fn NewParser( } var args_list: []Expr = if (isDebug) &Prefill.HotModuleReloading.DebugEnabledArgs else &Prefill.HotModuleReloading.DebugDisabled; - var call_args = try p.allocator.alloc(Expr, 3); - var new_call_args = call_args[0..2]; + + const new_call_args_count: usize = if (is_react_fast_refresh_enabled) 3 else 2; + var call_args = try p.allocator.alloc(Expr, new_call_args_count + 1); + var new_call_args = call_args[0..new_call_args_count]; var hmr_module_ident = p.e(E.Identifier{ .ref = p.hmr_module_ref }, logger.Loc.Empty); new_call_args[0] = p.e(E.Number{ .value = @intToFloat(f64, p.options.filepath_hash_for_hmr) }, logger.Loc.Empty); // This helps us provide better error messages new_call_args[1] = p.e(E.String{ .utf8 = p.source.path.pretty }, logger.Loc.Empty); + if (is_react_fast_refresh_enabled) { + new_call_args[2] = p.e(E.Identifier{ .ref = p.jsx_refresh_runtime_ref }, logger.Loc.Empty); + } var exports_dot = p.e(E.Dot{ .target = hmr_module_ident, .name = ExportsStringName, .name_loc = logger.Loc.Empty, }, logger.Loc.Empty); var hmr_module_class_ident = p.e(E.Identifier{ .ref = p.runtime_imports.__HMRClient.? }, logger.Loc.Empty); + var toplevel_stmts_i: u8 = 0; // HMRClient.activate(true) - toplevel_stmts[0] = p.s( + toplevel_stmts[toplevel_stmts_i] = p.s( S.SExpr{ .value = p.e(E.Call{ .target = p.e(E.Dot{ @@ -13780,16 +13882,23 @@ pub fn NewParser( }, logger.Loc.Empty, ); + toplevel_stmts_i += 1; var decls = try p.allocator.alloc(G.Decl, 2 + named_exports_count); var first_decl = decls[0..2]; // We cannot rely on import.meta.url because if we import it within a blob: url, it will be nonsensical // var __hmrModule = new HMRModule(123123124, "/index.js"), __exports = __hmrModule.exports; + const hmr_import_ref = if (is_react_fast_refresh_enabled) p.runtime_imports.__FastRefreshModule else p.runtime_imports.__HMRModule; first_decl[0] = G.Decl{ .binding = p.b(B.Identifier{ .ref = p.hmr_module_ref }, logger.Loc.Empty), .value = p.e(E.New{ .args = new_call_args, - .target = p.e(E.Identifier{ .ref = p.runtime_imports.__HMRModule.? }, logger.Loc.Empty), + .target = p.e( + E.Identifier{ + .ref = hmr_import_ref.?, + }, + logger.Loc.Empty, + ), }, logger.Loc.Empty), }; first_decl[1] = G.Decl{ @@ -13914,13 +14023,15 @@ pub fn NewParser( logger.Loc.Empty, ); - toplevel_stmts[1] = p.s( + toplevel_stmts[toplevel_stmts_i] = p.s( S.Local{ .decls = first_decl, }, logger.Loc.Empty, ); + toplevel_stmts_i += 1; + var func = p.e( E.Function{ .func = .{ @@ -13936,7 +14047,7 @@ pub fn NewParser( ); // (__hmrModule._load = function())() - toplevel_stmts[2] = p.s( + toplevel_stmts[toplevel_stmts_i] = p.s( S.SExpr{ .value = p.e( E.Call{ @@ -13959,13 +14070,19 @@ pub fn NewParser( logger.Loc.Empty, ); - toplevel_stmts[3] = p.s( - S.Local{ - .decls = exports_decls, - }, - logger.Loc.Empty, - ); - toplevel_stmts[4] = p.s( + toplevel_stmts_i += 1; + + if (has_any_exports) { + toplevel_stmts[toplevel_stmts_i] = p.s( + S.Local{ + .decls = exports_decls, + }, + logger.Loc.Empty, + ); + toplevel_stmts_i += 1; + } + + toplevel_stmts[toplevel_stmts_i] = p.s( S.SExpr{ .value = Expr.assign( p.e( @@ -13992,12 +14109,15 @@ pub fn NewParser( }, logger.Loc.Empty, ); - toplevel_stmts[5] = p.s( - S.ExportClause{ - .items = export_clauses, - }, - logger.Loc.Empty, - ); + toplevel_stmts_i += 1; + if (export_clauses.len > 0) { + toplevel_stmts[toplevel_stmts_i] = p.s( + S.ExportClause{ + .items = export_clauses, + }, + logger.Loc.Empty, + ); + } part.stmts = stmts_for_top_part; } diff --git a/src/logger.zig b/src/logger.zig index 9d1db56e6..a5f4eeae2 100644 --- a/src/logger.zig +++ b/src/logger.zig @@ -596,6 +596,8 @@ pub const Source = struct { } pub fn rangeOfString(self: *const Source, loc: Loc) Range { + if (loc.start < 0) return Range.None; + const text = self.contents[loc.i()..]; if (text.len == 0) { diff --git a/src/options.zig b/src/options.zig index 44a4f06b3..583ecb5dd 100644 --- a/src/options.zig +++ b/src/options.zig @@ -429,6 +429,7 @@ pub const JSX = struct { import_source: string = "react/jsx-dev-runtime", classic_import_source: string = "react", package_name: []const u8 = "react", + refresh_runtime: string = "react-refresh/runtime", supports_fast_refresh: bool = false, jsx: string = "jsxDEV", @@ -1465,6 +1466,7 @@ pub const Framework = struct { pub const RouteConfig = struct { dir: string = "", + possible_dirs: []const string = &[_]string{}, // Frameworks like Next.js (and others) use a special prefix for bundled/transpiled assets // This is combined with "origin" when printing import paths @@ -1517,13 +1519,26 @@ pub const RouteConfig = struct { pub fn fromApi(router_: Api.RouteConfig, allocator: *std.mem.Allocator) !RouteConfig { var router = zero(); - var router_dir: string = std.mem.trimRight(u8, router_.dir orelse "", "/\\"); var static_dir: string = std.mem.trimRight(u8, router_.static_dir orelse "", "/\\"); var asset_prefix: string = std.mem.trimRight(u8, router_.asset_prefix orelse "", "/\\"); - if (router_dir.len != 0) { - router.dir = router_dir; - router.routes_enabled = true; + switch (router_.dir.len) { + 0 => {}, + 1 => { + router.dir = std.mem.trimRight(u8, router_.dir[0], "/\\"); + router.routes_enabled = router.dir.len > 0; + }, + else => { + router.possible_dirs = router_.dir; + for (router_.dir) |dir| { + const trimmed = std.mem.trimRight(u8, dir, "/\\"); + if (trimmed.len > 0) { + router.dir = trimmed; + } + } + + router.routes_enabled = router.dir.len > 0; + }, } if (static_dir.len > 0) { diff --git a/src/resolver/package_json.zig b/src/resolver/package_json.zig index 3191f0a59..5a6c2f5b0 100644 --- a/src/resolver/package_json.zig +++ b/src/resolver/package_json.zig @@ -201,11 +201,51 @@ pub const PackageJSON = struct { if (!pair.router.routes_enabled) { if (framework_object.expr.asProperty("router")) |router| { if (router.expr.asProperty("dir")) |route_dir| { - if (route_dir.expr.asString(allocator)) |str| { - if (str.len > 0) { - pair.router.dir = str; - pair.loaded_routes = true; - } + switch (route_dir.expr.data) { + .e_string => |estr| { + const str = estr.string(allocator) catch unreachable; + if (str.len > 0) { + pair.router.dir = str; + pair.router.possible_dirs = &[_]string{}; + + pair.loaded_routes = true; + } + }, + .e_array => |array| { + var count: usize = 0; + for (array.items) |item| { + count += @boolToInt(item.data == .e_string and item.data.e_string.utf8.len > 0); + } + switch (count) { + 0 => {}, + 1 => { + const str = array.items[0].data.e_string.string(allocator) catch unreachable; + if (str.len > 0) { + pair.router.dir = str; + pair.router.possible_dirs = &[_]string{}; + + pair.loaded_routes = true; + } + }, + else => { + const list = allocator.alloc(string, count) catch unreachable; + + var list_i: usize = 0; + for (array.items) |item| { + if (item.data == .e_string and item.data.e_string.utf8.len > 0) { + list[list_i] = item.data.e_string.string(allocator) catch unreachable; + list_i += 1; + } + } + + pair.router.dir = list[0]; + pair.router.possible_dirs = list; + + pair.loaded_routes = true; + }, + } + }, + else => {}, } } diff --git a/src/resolver/resolver.zig b/src/resolver/resolver.zig index 9ff5a4eab..db8ee6fd3 100644 --- a/src/resolver/resolver.zig +++ b/src/resolver/resolver.zig @@ -403,12 +403,25 @@ pub fn NewResolver(cache_files: bool) type { } if (pair.loaded_routes) { - var parts = [_]string{ r.fs.top_level_dir, std.fs.path.sep_str, pair.router.dir }; - const abs = r.fs.join(&parts); - // must end in trailing slash - var realpath = std.os.realpath(abs, &buf) catch return error.RoutesDirNotFound; - var out = try r.allocator.alloc(u8, realpath.len + 1); - std.mem.copy(u8, out, realpath); + const chosen_dir: string = brk: { + if (pair.router.possible_dirs.len > 0) { + for (pair.router.possible_dirs) |route_dir| { + var parts = [_]string{ r.fs.top_level_dir, std.fs.path.sep_str, route_dir }; + const abs = r.fs.join(&parts); + // must end in trailing slash + break :brk (std.os.realpath(abs, &buf) catch continue); + } + return error.MissingRouteDir; + } else { + var parts = [_]string{ r.fs.top_level_dir, std.fs.path.sep_str, pair.router.dir }; + const abs = r.fs.join(&parts); + // must end in trailing slash + break :brk std.os.realpath(abs, &buf) catch return error.MissingRouteDir; + } + }; + + var out = try r.allocator.alloc(u8, chosen_dir.len + 1); + std.mem.copy(u8, out, chosen_dir); out[out.len - 1] = '/'; pair.router.dir = out; pair.router.routes_enabled = true; diff --git a/src/runtime.js b/src/runtime.js index affd3bc4d..3afbf80f7 100644 --- a/src/runtime.js +++ b/src/runtime.js @@ -70,9 +70,13 @@ export var __commonJS = (cb, name) => { typeof mod.exports === "object" && !("default" in mod.exports) ) { + var defaultValue = mod.exports; Object.defineProperty(mod.exports, "default", { get() { - return mod.exports; + return defaultValue; + }, + set(value) { + defaultValue = value; }, enumerable: true, }); diff --git a/src/runtime.version b/src/runtime.version index 4cda1e7bd..2de34e9ad 100644 --- a/src/runtime.version +++ b/src/runtime.version @@ -1 +1 @@ -7d529e0a92952d7a
\ No newline at end of file +bf074417343b306f
\ No newline at end of file diff --git a/src/runtime.zig b/src/runtime.zig index 33a8fbd0c..04161a524 100644 --- a/src/runtime.zig +++ b/src/runtime.zig @@ -62,6 +62,7 @@ pub const Runtime = struct { lazy_export: ?Ref = null, __HMRModule: ?Ref = null, __HMRClient: ?Ref = null, + __FastRefreshModule: ?Ref = null, pub const all = [_][]const u8{ "__name", @@ -78,6 +79,7 @@ pub const Runtime = struct { "lazy_export", "__HMRModule", "__HMRClient", + "__FastRefreshModule", }; pub const Name = "<RUNTIME"; @@ -156,6 +158,11 @@ pub const Runtime = struct { return Entry{ .key = 11, .value = val }; } }, + 12 => { + if (@field(this.runtime_imports, all[12])) |val| { + return Entry{ .key = 12, .value = val }; + } + }, else => { return null; @@ -213,6 +220,7 @@ pub const Runtime = struct { 9 => @field(imports, all[9]), 10 => @field(imports, all[10]), 11 => @field(imports, all[11]), + 12 => @field(imports, all[12]), else => null, }; } |