diff options
author | 2021-08-26 19:56:25 -0700 | |
---|---|---|
committer | 2021-08-26 19:56:25 -0700 | |
commit | 3ae0accbe3b34617be328ac46a3d8c7cbdbae6f6 (patch) | |
tree | 22cf61beab784cf3c24c87860cd08fc6a614d050 | |
parent | db740a4eb45aecddb1c4bddbf13e7254065ef6a6 (diff) | |
download | bun-3ae0accbe3b34617be328ac46a3d8c7cbdbae6f6.tar.gz bun-3ae0accbe3b34617be328ac46a3d8c7cbdbae6f6.tar.zst bun-3ae0accbe3b34617be328ac46a3d8c7cbdbae6f6.zip |
Fix file loader, automatically support CSS imports when a framework isn't set
Former-commit-id: 94750e5987ea8f6e4c946bfc06715e09a48c0eec
-rw-r--r-- | src/api/schema.d.ts | 7 | ||||
-rw-r--r-- | src/api/schema.js | 8 | ||||
-rw-r--r-- | src/api/schema.peechy | 1 | ||||
-rw-r--r-- | src/api/schema.zig | 3 | ||||
-rw-r--r-- | src/bundler.zig | 148 | ||||
-rw-r--r-- | src/cli.zig | 4 | ||||
-rw-r--r-- | src/fs.zig | 13 | ||||
-rw-r--r-- | src/http.zig | 39 | ||||
-rw-r--r-- | src/http/mime_type.zig | 3 | ||||
-rw-r--r-- | src/http/url_path.zig | 9 | ||||
-rw-r--r-- | src/import_record.zig | 8 | ||||
-rw-r--r-- | src/js_printer.zig | 84 | ||||
-rw-r--r-- | src/linker.zig | 18 | ||||
-rw-r--r-- | src/main_javascript.zig | 2 | ||||
-rw-r--r-- | src/options.zig | 22 | ||||
-rw-r--r-- | src/query_string_map.zig | 33 | ||||
-rw-r--r-- | src/runtime.version | 2 | ||||
-rw-r--r-- | src/runtime/hmr.ts | 136 |
18 files changed, 410 insertions, 130 deletions
diff --git a/src/api/schema.d.ts b/src/api/schema.d.ts index 07f865983..304ff65d5 100644 --- a/src/api/schema.d.ts +++ b/src/api/schema.d.ts @@ -68,13 +68,16 @@ type uint32 = number; } export enum CSSInJSBehavior { facade = 1, - facade_onimportcss = 2 + facade_onimportcss = 2, + auto_onimportcss = 3 } export const CSSInJSBehaviorKeys = { 1: "facade", facade: "facade", 2: "facade_onimportcss", - facade_onimportcss: "facade_onimportcss" + facade_onimportcss: "facade_onimportcss", + 3: "auto_onimportcss", + auto_onimportcss: "auto_onimportcss" } export enum JSXRuntime { automatic = 1, diff --git a/src/api/schema.js b/src/api/schema.js index bc4430489..6f7f60a73 100644 --- a/src/api/schema.js +++ b/src/api/schema.js @@ -69,14 +69,18 @@ const PlatformKeys = { const CSSInJSBehavior = { "1": 1, "2": 2, + "3": 3, "facade": 1, - "facade_onimportcss": 2 + "facade_onimportcss": 2, + "auto_onimportcss": 3 }; const CSSInJSBehaviorKeys = { "1": "facade", "2": "facade_onimportcss", + "3": "auto_onimportcss", "facade": "facade", - "facade_onimportcss": "facade_onimportcss" + "facade_onimportcss": "facade_onimportcss", + "auto_onimportcss": "auto_onimportcss" }; const JSXRuntime = { "1": 1, diff --git a/src/api/schema.peechy b/src/api/schema.peechy index 0e0680b8d..552160be9 100644 --- a/src/api/schema.peechy +++ b/src/api/schema.peechy @@ -26,6 +26,7 @@ smol Platform { smol CSSInJSBehavior { facade = 1; facade_onimportcss = 2; + auto_onimportcss = 3; } smol JSXRuntime { diff --git a/src/api/schema.zig b/src/api/schema.zig index a01ba641a..6afbc02de 100644 --- a/src/api/schema.zig +++ b/src/api/schema.zig @@ -374,6 +374,9 @@ _none, /// facade_onimportcss facade_onimportcss, + /// auto_onimportcss + auto_onimportcss, + _, pub fn jsonStringify(self: *const @This(), opts: anytype, o: anytype) !void { diff --git a/src/bundler.zig b/src/bundler.zig index 4942c3989..1ffd25cef 100644 --- a/src/bundler.zig +++ b/src/bundler.zig @@ -314,7 +314,7 @@ pub fn NewBundler(cache_files: bool) type { // for now: // - "." is not supported // - multiple pages directories is not supported - if (!this.options.routes.routes_enabled and this.options.entry_points.len == 1) { + if (!this.options.routes.routes_enabled and this.options.entry_points.len == 1 and !this.options.serve) { // When inferring: // - pages directory with a file extension is not supported. e.g. "pages.app/" won't work. @@ -1804,7 +1804,7 @@ pub fn NewBundler(cache_files: bool) type { const resolved = if (comptime !client_entry_point_enabled) (try bundler.resolver.resolve(bundler.fs.top_level_dir, absolute_path, .stmt)) else brk: { const absolute_pathname = Fs.PathName.init(absolute_path); - const loader_for_ext = bundler.options.loaders.get(absolute_pathname.ext) orelse .file; + const loader_for_ext = bundler.options.loader(absolute_pathname.ext); // The expected pathname looks like: // /pages/index.entry.tsx @@ -1832,25 +1832,35 @@ pub fn NewBundler(cache_files: bool) type { break :brk (try bundler.resolver.resolve(bundler.fs.top_level_dir, absolute_path, .stmt)); }; - const loader = bundler.options.loaders.get(resolved.path_pair.primary.name.ext) orelse .file; + const loader = bundler.options.loader(resolved.path_pair.primary.name.ext); + const mime_type_ext = bundler.options.out_extensions.get(resolved.path_pair.primary.name.ext) orelse resolved.path_pair.primary.name.ext; switch (loader) { - .js, .jsx, .ts, .tsx, .json, .css => { + .js, .jsx, .ts, .tsx, .css => { return ServeResult{ .file = options.OutputFile.initPending(loader, resolved), .mime_type = MimeType.byLoader( loader, - bundler.options.out_extensions.get(resolved.path_pair.primary.name.ext) orelse resolved.path_pair.primary.name.ext, + mime_type_ext[1..], ), }; }, + .json => { + return ServeResult{ + .file = options.OutputFile.initPending(loader, resolved), + .mime_type = MimeType.transpiled_json, + }; + }, else => { var abs_path = resolved.path_pair.primary.text; const file = try std.fs.openFileAbsolute(abs_path, .{ .read = true }); var stat = try file.stat(); return ServeResult{ .file = options.OutputFile.initFile(file, abs_path, stat.size), - .mime_type = MimeType.byLoader(loader, abs_path), + .mime_type = MimeType.byLoader( + loader, + mime_type_ext[1..], + ), }; }, } @@ -2440,46 +2450,92 @@ pub const ClientEntryPoint = struct { // 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, - \\var lastErrorHandler = globalThis.onerror; - \\var loaded = {{boot: false, entry: false, onError: null}}; - \\if (!lastErrorHandler || !lastErrorHandler.__onceTag) {{ - \\ globalThis.onerror = function (evt) {{ - \\ if (this.onError && typeof this.onError == 'function') {{ - \\ this.onError(evt, loaded); - \\ }} - \\ console.error(evt.error); - \\ debugger; - \\ }}; - \\ globalThis.onerror.__onceTag = true; - \\ globalThis.onerror.loaded = loaded; - \\}} - \\ - \\import boot from '{s}'; - \\loaded.boot = true; - \\if ('setLoaded' in boot) boot.setLoaded(loaded); - \\import * as EntryPoint from '{s}{s}'; - \\loaded.entry = true; - \\ - \\if (!boot) {{ - \\ const now = Date.now(); - \\ debugger; - \\ const elapsed = Date.now() - now; - \\ if (elapsed < 1000) {{ - \\ throw new Error('Expected framework to export default a function. Instead, framework exported:', Object.keys(boot)); - \\ }} - \\}} - \\ - \\boot(EntryPoint, loaded); - , - .{ - client, - dir_to_use, - original_path.filename, - }, - ); + const disable_css_imports = bundler.options.framework.?.client_css_in_js != .auto_onimportcss; + + var code: string = undefined; + + if (disable_css_imports) { + code = try std.fmt.bufPrint( + &entry.code_buffer, + \\globalThis.Bun_disableCSSImports = true; + \\var lastErrorHandler = globalThis.onerror; + \\var loaded = {{boot: false, entry: false, onError: null}}; + \\if (!lastErrorHandler || !lastErrorHandler.__onceTag) {{ + \\ globalThis.onerror = function (evt) {{ + \\ if (this.onError && typeof this.onError == 'function') {{ + \\ this.onError(evt, loaded); + \\ }} + \\ console.error(evt.error); + \\ debugger; + \\ }}; + \\ globalThis.onerror.__onceTag = true; + \\ globalThis.onerror.loaded = loaded; + \\}} + \\ + \\import boot from '{s}'; + \\loaded.boot = true; + \\if ('setLoaded' in boot) boot.setLoaded(loaded); + \\import * as EntryPoint from '{s}{s}'; + \\loaded.entry = true; + \\ + \\if (!boot) {{ + \\ const now = Date.now(); + \\ debugger; + \\ const elapsed = Date.now() - now; + \\ if (elapsed < 1000) {{ + \\ throw new Error('Expected framework to export default a function. Instead, framework exported:', Object.keys(boot)); + \\ }} + \\}} + \\ + \\boot(EntryPoint, loaded); + , + .{ + client, + dir_to_use, + original_path.filename, + }, + ); + } else { + code = try std.fmt.bufPrint( + &entry.code_buffer, + \\var lastErrorHandler = globalThis.onerror; + \\var loaded = {{boot: false, entry: false, onError: null}}; + \\if (!lastErrorHandler || !lastErrorHandler.__onceTag) {{ + \\ globalThis.onerror = function (evt) {{ + \\ if (this.onError && typeof this.onError == 'function') {{ + \\ this.onError(evt, loaded); + \\ }} + \\ console.error(evt.error); + \\ debugger; + \\ }}; + \\ globalThis.onerror.__onceTag = true; + \\ globalThis.onerror.loaded = loaded; + \\}} + \\ + \\import boot from '{s}'; + \\loaded.boot = true; + \\if ('setLoaded' in boot) boot.setLoaded(loaded); + \\import * as EntryPoint from '{s}{s}'; + \\loaded.entry = true; + \\ + \\if (!boot) {{ + \\ const now = Date.now(); + \\ debugger; + \\ const elapsed = Date.now() - now; + \\ if (elapsed < 1000) {{ + \\ throw new Error('Expected framework to export default a function. Instead, framework exported:', Object.keys(boot)); + \\ }} + \\}} + \\ + \\boot(EntryPoint, loaded); + , + .{ + client, + dir_to_use, + original_path.filename, + }, + ); + } entry.source = logger.Source.initPathString(generateEntryPointPath(&entry.path_buffer, original_path), code); entry.source.path.namespace = "client-entry"; diff --git a/src/cli.zig b/src/cli.zig index 2589fbcfc..e77e76154 100644 --- a/src/cli.zig +++ b/src/cli.zig @@ -158,7 +158,7 @@ pub const Arguments = struct { clap.parseParam("--origin <STR> Rewrite import paths to start with --origin. Default: \"/\"") catch unreachable, clap.parseParam("--platform <STR> \"browser\" or \"node\". Defaults to \"browser\"") catch unreachable, // clap.parseParam("--production [not implemented] generate production code") catch unreachable, - clap.parseParam("--static-dir <STR> Top-level directory for .html files, fonts or anything external. Defaults to \"<cwd>/public\", to match create-react-app and Next.js") catch unreachable, + clap.parseParam("--public-dir <STR> Top-level directory for .html files, fonts or anything external. Defaults to \"<cwd>/public\", to match create-react-app and Next.js") catch unreachable, clap.parseParam("--tsconfig-override <STR> Load tsconfig from path instead of cwd/tsconfig.json") catch unreachable, clap.parseParam("-d, --define <STR>... Substitute K:V while parsing, e.g. --define process.env.NODE_ENV:development") catch unreachable, clap.parseParam("-e, --external <STR>... Exclude module from transpilation (can use * wildcards). ex: -e react") catch unreachable, @@ -310,7 +310,7 @@ pub const Arguments = struct { switch (comptime cmd) { .AutoCommand, .DevCommand, .BuildCommand => { - if (args.option("--static-dir")) |public_dir| { + if (args.option("--public-dir")) |public_dir| { opts.router = Api.RouteConfig{ .extensions = &.{}, .dir = &.{}, .static_dir = public_dir }; } }, diff --git a/src/fs.zig b/src/fs.zig index 93eb16acc..d5bec356e 100644 --- a/src/fs.zig +++ b/src/fs.zig @@ -476,19 +476,6 @@ pub const FileSystem = struct { return try allocator.dupe(u8, joined); } - threadlocal var realpath_buffer: [std.fs.MAX_PATH_BYTES]u8 = undefined; - pub fn resolveAlloc(f: *@This(), allocator: *std.mem.Allocator, parts: anytype) !string { - const joined = f.abs(parts); - - const realpath = f.resolvePath(joined); - - return try allocator.dupe(u8, realpath); - } - - pub fn resolvePath(f: *@This(), part: string) ![]u8 { - return try std.fs.realpath(part, (&realpath_buffer).ptr); - } - pub const RealFS = struct { entries_mutex: Mutex = Mutex.init(), entries: *EntriesOption.Map, diff --git a/src/http.zig b/src/http.zig index fd23b349d..aca0edc88 100644 --- a/src/http.zig +++ b/src/http.zig @@ -239,9 +239,13 @@ pub const RequestContext = struct { var absolute_path = resolve_path.joinAbs(this.bundler.options.routes.static_dir, .auto, relative_unrooted_path); if (stat.kind == .SymLink) { - absolute_path = std.fs.realpath(absolute_path, &Bundler.tmp_buildfile_buf) catch return null; - file.close(); file.* = std.fs.openFileAbsolute(absolute_path, .{ .read = true }) catch return null; + + absolute_path = std.os.getFdPath( + file.handle, + &Bundler.tmp_buildfile_buf, + ) catch return null; + stat = file.stat() catch return null; } @@ -265,9 +269,10 @@ pub const RequestContext = struct { pub fn printStatusLine(comptime code: HTTPStatusCode) []const u8 { const status_text = switch (code) { 101 => "ACTIVATING WEBSOCKET", - 200...299 => "OK", - 300...399 => "=>", - 400...499 => "DID YOU KNOW YOU CAN MAKE THIS SAY WHATEVER YOU WANT", + 200...299 => "YAY", + 304 => "NOT MODIFIED", + 300...303, 305...399 => "REDIRECT", + 400...499 => "bad request :'(", 500...599 => "ERR", else => @compileError("Invalid code passed to printStatusLine"), }; @@ -377,7 +382,7 @@ pub const RequestContext = struct { .arena = arena, .bundler = bundler_, .log = undefined, - .url = URLPath.parse(req.path), + .url = try URLPath.parse(req.path), .conn = conn, .allocator = undefined, .method = Method.which(req.method) orelse return error.InvalidMethod, @@ -700,6 +705,7 @@ pub const RequestContext = struct { pub var javascript_disabled = false; pub fn spawnThread(handler: HandlerThread) !void { var thread = try std.Thread.spawn(.{}, spawn, .{handler}); + thread.setName("WebSocket") catch {}; thread.detach(); } @@ -1936,11 +1942,20 @@ pub const Server = struct { server.watcher, server.timer, ) catch |err| { - Output.printErrorln("<r>[<red>{s}<r>] - <b>{s}<r>: {s}", .{ @errorName(err), req.method, req.path }); + Output.prettyErrorln("<r>[<red>{s}<r>] - <b>{s}<r>: {s}", .{ @errorName(err), req.method, req.path }); conn.client.deinit(); return; }; + if (req_ctx.url.needs_redirect) { + req_ctx.handleRedirect(req_ctx.url.path) catch |err| { + Output.prettyErrorln("<r>[<red>{s}<r>] - <b>{s}<r>: {s}", .{ @errorName(err), req.method, req.path }); + conn.client.deinit(); + return; + }; + return; + } + defer { if (!req_ctx.controlled) { req_ctx.arena.deinit(); @@ -2054,6 +2069,7 @@ pub const Server = struct { }, } }; + finished = true; } } else { if (!finished) { @@ -2069,6 +2085,7 @@ pub const Server = struct { }, } }; + finished = true; } } @@ -2139,13 +2156,13 @@ pub const Server = struct { ); if (server.bundler.router != null and server.bundler.options.routes.static_dir_enabled) { - if (public_folder_is_top_level) { + if (!public_folder_is_top_level) { try server.run( - ConnectionFeatures{ .public_folder = .last, .filesystem_router = true }, + ConnectionFeatures{ .public_folder = .first, .filesystem_router = true }, ); } else { try server.run( - ConnectionFeatures{ .public_folder = .first, .filesystem_router = true }, + ConnectionFeatures{ .public_folder = .last, .filesystem_router = true }, ); } } else if (server.bundler.router != null) { @@ -2153,7 +2170,7 @@ pub const Server = struct { ConnectionFeatures{ .filesystem_router = true }, ); } else if (server.bundler.options.routes.static_dir_enabled) { - if (public_folder_is_top_level) { + if (!public_folder_is_top_level) { try server.run( ConnectionFeatures{ .public_folder = .first, diff --git a/src/http/mime_type.zig b/src/http/mime_type.zig index 55d7ec72b..3d6509183 100644 --- a/src/http/mime_type.zig +++ b/src/http/mime_type.zig @@ -31,7 +31,8 @@ pub const javascript = MimeType.init("text/javascript;charset=utf-8", .javascrip pub const ico = MimeType.init("image/vnd.microsoft.icon", .image); pub const html = MimeType.init("text/html;charset=utf-8", .html); // we transpile json to javascript so that it is importable without import assertions. -pub const json = MimeType.init(javascript.value, .json); +pub const json = MimeType.init("application/json", .json); +pub const transpiled_json = javascript; fn init(comptime str: string, t: Category) MimeType { return MimeType{ diff --git a/src/http/url_path.zig b/src/http/url_path.zig index 0a4d35069..e36ea9d93 100644 --- a/src/http/url_path.zig +++ b/src/http/url_path.zig @@ -10,6 +10,7 @@ path: string = "", pathname: string = "", first_segment: string = "", query_string: string = "", +needs_redirect: bool = false, const toMutable = allocators.constStrToU8; // TODO: use a real URL parser @@ -30,8 +31,10 @@ pub fn pathWithoutAssetPrefix(this: *const URLPath, asset_prefix: string) string threadlocal var temp_path_buf: [1024]u8 = undefined; threadlocal var big_temp_path_buf: [16384]u8 = undefined; -pub fn parse(possibly_encoded_pathname_: string) URLPath { +pub fn parse(possibly_encoded_pathname_: string) !URLPath { var decoded_pathname = possibly_encoded_pathname_; + var needs_redirect = false; + var invalid_uri = false; if (strings.indexOfChar(decoded_pathname, '%') != null) { var possibly_encoded_pathname = switch (decoded_pathname.len) { @@ -55,7 +58,8 @@ pub fn parse(possibly_encoded_pathname_: string) URLPath { ), ); var writer = fbs.writer(); - decoded_pathname = possibly_encoded_pathname[0 .. PercentEncoding.decode(@TypeOf(writer), writer, clone) catch unreachable]; + + decoded_pathname = possibly_encoded_pathname[0..try PercentEncoding.decodeFaultTolerant(@TypeOf(writer), writer, clone, &needs_redirect, true)]; } var question_mark_i: i16 = -1; @@ -119,5 +123,6 @@ pub fn parse(possibly_encoded_pathname_: string) URLPath { .first_segment = first_segment, .path = if (decoded_pathname.len == 1) "." else path, .query_string = if (question_mark_i > -1) decoded_pathname[@intCast(usize, question_mark_i)..@intCast(usize, decoded_pathname.len)] else "", + .needs_redirect = needs_redirect, }; } diff --git a/src/import_record.zig b/src/import_record.zig index 0d3cff529..ae09f8d2f 100644 --- a/src/import_record.zig +++ b/src/import_record.zig @@ -49,6 +49,8 @@ pub const ImportRecord = struct { source_index: Ref.Int = std.math.maxInt(Ref.Int), + print_mode: PrintMode = .normal, + // True for the following cases: // // try { require('x') } catch { handle } @@ -94,4 +96,10 @@ pub const ImportRecord = struct { was_originally_bare_import: bool = false, kind: ImportKind, + + pub const PrintMode = enum { + normal, + import_path, + css, + }; }; diff --git a/src/js_printer.zig b/src/js_printer.zig index 9f039eb5e..4e8c8fa28 100644 --- a/src/js_printer.zig +++ b/src/js_printer.zig @@ -2874,44 +2874,60 @@ pub fn NewPrinter( .s_import => |s| { // TODO: check loader instead - if (strings.eqlComptime(p.import_records[s.import_record_index].path.name.ext, ".css")) { - switch (p.options.css_import_behavior) { - .facade => { - - // This comment exists to let tooling authors know which files CSS originated from - // To parse this, you just look for a line that starts with //@import url(" - p.print("//@import url(\""); - // We do not URL escape here. - p.print(p.import_records[s.import_record_index].path.text); - - // If they actually use the code, then we emit a facade that just echos whatever they write - if (s.default_name) |name| { - p.print("\"); css-module-facade\nvar "); - p.printSymbol(name.ref.?); - p.print(" = new Proxy({}, {get(_,className,__){return className;}});\n"); - } else { - p.print("\"); css-import-facade\n"); - } + switch (p.import_records[s.import_record_index].print_mode) { + .css => { + switch (p.options.css_import_behavior) { + .facade => { + + // This comment exists to let tooling authors know which files CSS originated from + // To parse this, you just look for a line that starts with //@import url(" + p.print("//@import url(\""); + // We do not URL escape here. + p.print(p.import_records[s.import_record_index].path.text); + + // If they actually use the code, then we emit a facade that just echos whatever they write + if (s.default_name) |name| { + p.print("\"); css-module-facade\nvar "); + p.printSymbol(name.ref.?); + p.print(" = new Proxy({}, {get(_,className,__){return className;}});\n"); + } else { + p.print("\"); css-import-facade\n"); + } - return; - }, + return; + }, - .facade_onimportcss => { - p.print("globalThis.document?.dispatchEvent(new CustomEvent(\"onimportcss\", {detail: \""); - p.print(p.import_records[s.import_record_index].path.text); - p.print("\"}));\n"); + .auto_onimportcss, .facade_onimportcss => { + p.print("globalThis.document?.dispatchEvent(new CustomEvent(\"onimportcss\", {detail: \""); + p.print(p.import_records[s.import_record_index].path.text); + p.print("\"}));\n"); - // If they actually use the code, then we emit a facade that just echos whatever they write - if (s.default_name) |name| { - p.print("var "); - p.printSymbol(name.ref.?); - p.print(" = new Proxy({}, {get(_,className,__){return className;}});\n"); - } - }, - else => {}, - } + // If they actually use the code, then we emit a facade that just echos whatever they write + if (s.default_name) |name| { + p.print("var "); + p.printSymbol(name.ref.?); + p.print(" = new Proxy({}, {get(_,className,__){return className;}});\n"); + } + }, + else => {}, + } + return; + }, + .import_path => { + if (s.default_name) |name| { + const quotes = p.bestQuoteCharForString(p.import_records[s.import_record_index].path.text, true); - return; + p.print("var "); + p.printSymbol(name.ref.?); + p.print(" = "); + p.print(quotes); + p.printUTF8StringEscapedQuotes(p.import_records[s.import_record_index].path.text, quotes); + p.print(quotes); + p.printSemicolonAfterStatement(); + } + return; + }, + else => {}, } const record = p.import_records[s.import_record_index]; diff --git a/src/linker.zig b/src/linker.zig index 9b0918d42..c4c43a003 100644 --- a/src/linker.zig +++ b/src/linker.zig @@ -301,7 +301,7 @@ pub fn NewLinker(comptime BundlerType: type) type { } linker.processImportRecord( - linker.options.loaders.get(resolved_import.path_pair.primary.name.ext) orelse .file, + linker.options.loader(resolved_import.path_pair.primary.name.ext), // Include trailing slash process_import_record_path, @@ -568,10 +568,18 @@ pub fn NewLinker(comptime BundlerType: type) type { import_path_format, ); - if (loader == .css) { - if (linker.onImportCSS) |callback| { - callback(resolve_result, import_record, source_dir); - } + switch (loader) { + .css => { + if (linker.onImportCSS) |callback| { + callback(resolve_result, import_record, source_dir); + } + // This saves us a less reliable string check + import_record.print_mode = .css; + }, + .file => { + import_record.print_mode = .import_path; + }, + else => {}, } } diff --git a/src/main_javascript.zig b/src/main_javascript.zig index 3258c4c6c..0ecff30d5 100644 --- a/src/main_javascript.zig +++ b/src/main_javascript.zig @@ -144,7 +144,7 @@ pub const Cli = struct { clap.parseParam("-e, --external <STR>... Exclude module from transpilation (can use * wildcards). ex: -e react") catch unreachable, clap.parseParam("-i, --inject <STR>... Inject module at the top of every file") catch unreachable, clap.parseParam("--cwd <STR> Absolute path to resolve entry points from. Defaults to cwd") catch unreachable, - clap.parseParam("--origin <STR> Rewrite import paths to start with --origin. Useful for web browsers.") catch unreachable, + clap.parseParam("--origin <STR> Rewrite import paths to start with --origin. Useful for web browsers.") catch unreachable, clap.parseParam("--serve Start a local dev server. This also sets resolve to \"lazy\".") catch unreachable, clap.parseParam("--public-dir <STR> Top-level directory for .html files, fonts, images, or anything external. Only relevant with --serve. Defaults to \"<cwd>/public\", to match create-react-app and Next.js") catch unreachable, clap.parseParam("--jsx-factory <STR> Changes the function called when compiling JSX elements using the classic JSX runtime") catch unreachable, diff --git a/src/options.zig b/src/options.zig index 1c22153bd..1302ae288 100644 --- a/src/options.zig +++ b/src/options.zig @@ -704,6 +704,7 @@ pub const BundleOptions = struct { timings: Timings = Timings{}, node_modules_bundle: ?*NodeModuleBundle = null, production: bool = false, + serve: bool = false, append_package_version_in_query_string: bool = false, @@ -730,7 +731,7 @@ pub const BundleOptions = struct { return framework.client_css_in_js; } - return .facade; + return .auto_onimportcss; }, else => return .facade, } @@ -740,7 +741,7 @@ pub const BundleOptions = struct { return !this.defines_loaded; } - pub fn loadDefines(this: *BundleOptions, allocator: *std.mem.Allocator, loader: ?*DotEnv.Loader, env: ?*const Env) !void { + pub fn loadDefines(this: *BundleOptions, allocator: *std.mem.Allocator, loader_: ?*DotEnv.Loader, env: ?*const Env) !void { if (this.defines_loaded) { return; } @@ -750,12 +751,16 @@ pub const BundleOptions = struct { this.transform_options.define, this.transform_options.serve orelse false, this.platform, - loader, + loader_, env, ); this.defines_loaded = true; } + pub fn loader(this: *const BundleOptions, ext: string) Loader { + return this.loaders.get(ext) orelse .file; + } + pub fn asJavascriptBundleConfig(this: *const BundleOptions) Api.JavascriptBundleConfig {} pub fn isFrontendFrameworkEnabled(this: *const BundleOptions) bool { @@ -960,7 +965,7 @@ pub const BundleOptions = struct { opts.resolve_mode = .lazy; var dir_to_use: string = opts.routes.static_dir; - const static_dir_set = !opts.routes.static_dir_enabled; + const static_dir_set = !opts.routes.static_dir_enabled or dir_to_use.len > 0; var disabled_static = false; var chosen_dir = dir_to_use; @@ -1027,6 +1032,7 @@ pub const BundleOptions = struct { break :brk null; }; + opts.routes.static_dir_enabled = opts.routes.static_dir_handle != null; } // Windows has weird locking rules for file access. // so it's a bad idea to keep a file handle open for a long time on Windows. @@ -1034,6 +1040,7 @@ pub const BundleOptions = struct { opts.routes.static_dir_handle.?.close(); } opts.hot_module_reloading = opts.platform.isWebLike(); + opts.serve = true; } if (opts.origin.isAbsolute()) { @@ -1424,7 +1431,7 @@ pub const Framework = struct { client_env: Env = Env{}, server_env: Env = Env{}, - client_css_in_js: Api.CssInJsBehavior = .facade, + client_css_in_js: Api.CssInJsBehavior = .auto_onimportcss, fn normalizedPath(allocator: *std.mem.Allocator, toplevel_path: string, path: string) !string { std.debug.assert(std.fs.path.isAbsolute(path)); @@ -1509,9 +1516,10 @@ pub const Framework = struct { .package = transform.package orelse "", .development = transform.development orelse true, .resolved = false, - .client_css_in_js = switch (transform.client_css_in_js orelse .facade) { + .client_css_in_js = switch (transform.client_css_in_js orelse .auto_onimportcss) { .facade_onimportcss => .facade_onimportcss, - else => .facade, + .facade => .facade, + else => .auto_onimportcss, }, }; } diff --git a/src/query_string_map.zig b/src/query_string_map.zig index c5acd5777..e9e1f2e6d 100644 --- a/src/query_string_map.zig +++ b/src/query_string_map.zig @@ -638,6 +638,16 @@ pub const QueryStringMap = struct { pub const PercentEncoding = struct { pub fn decode(comptime Writer: type, writer: Writer, input: string) !u32 { + return @call(.{ .modifier = .always_inline }, decodeFaultTolerant, .{ Writer, writer, input, null, false }); + } + + pub fn decodeFaultTolerant( + comptime Writer: type, + writer: Writer, + input: string, + needs_redirect: ?*bool, + comptime fault_tolerant: bool, + ) !u32 { var i: usize = 0; var written: u32 = 0; // unlike JavaScript's decodeURIComponent, we are not handling invalid surrogate pairs @@ -645,7 +655,28 @@ pub const PercentEncoding = struct { while (i < input.len) { switch (input[i]) { '%' => { - if (!(i + 3 <= input.len and strings.isASCIIHexDigit(input[i + 1]) and strings.isASCIIHexDigit(input[i + 2]))) return error.DecodingError; + if (comptime fault_tolerant) { + if (!(i + 3 <= input.len and strings.isASCIIHexDigit(input[i + 1]) and strings.isASCIIHexDigit(input[i + 2]))) { + // i do not feel good about this + // create-react-app's public/index.html uses %PUBLIC_URL% in various tags + // This is an invalid %-encoded string, intended to be swapped out at build time by webpack-html-plugin + // We don't process HTML, so rewriting this URL path won't happen + // But we want to be a little more fault tolerant here than just throwing up an error for something that works in other tools + // So we just skip over it and issue a redirect + // We issue a redirect because various other tooling client-side may validate URLs + // We can't expect other tools to be as fault tolerant + if (i + "PUBLIC_URL%".len < input.len and strings.eqlComptime(input[i + 1 ..][0.."PUBLIC_URL%".len], "PUBLIC_URL%")) { + i += "PUBLIC_URL%".len + 1; + needs_redirect.?.* = true; + continue; + } + return error.DecodingError; + } + } else { + if (!(i + 3 <= input.len and strings.isASCIIHexDigit(input[i + 1]) and strings.isASCIIHexDigit(input[i + 2]))) + return error.DecodingError; + } + try writer.writeByte((strings.toASCIIHexValue(input[i + 1]) << 4) | strings.toASCIIHexValue(input[i + 2])); i += 3; written += 1; diff --git a/src/runtime.version b/src/runtime.version index dd9a0085a..e76ed155e 100644 --- a/src/runtime.version +++ b/src/runtime.version @@ -1 +1 @@ -ef1f71fecb089b94
\ No newline at end of file +3b71ea301d8e2123
\ No newline at end of file diff --git a/src/runtime/hmr.ts b/src/runtime/hmr.ts index 10f7e4a5d..ecd3aadb2 100644 --- a/src/runtime/hmr.ts +++ b/src/runtime/hmr.ts @@ -11,6 +11,12 @@ if (typeof window !== "undefined") { return Math.round(duration * 1000) / 1000; } + enum CSSImportState { + Pending, + Loading, + Loaded, + } + type HTMLStylableElement = HTMLLinkElement | HTMLStyleElement; type CSSHMRInsertionPoint = { id: number; @@ -447,13 +453,133 @@ if (typeof window !== "undefined") { return HMRModule.dependencies.graph.indexOf(id); } + static cssQueue = []; + static cssState = CSSImportState.Pending; + static cssAutoFOUC = false; + + static processPendingCSSImports() { + const pending = HMRClient.cssQueue.slice(); + HMRClient.cssQueue.length = 0; + return Promise.all(pending).then(() => { + if (HMRClient.cssQueue.length > 0) { + const _pending = HMRClient.cssQueue.slice(); + HMRClient.cssQueue.length = 0; + return Promise.all(_pending).then(HMRClient.processPendingCSSImports); + } else { + return true; + } + }); + } + + static importCSS(promise: Promise<unknown>) { + switch (HMRClient.cssState) { + case CSSImportState.Pending: { + this.cssState = CSSImportState.Loading; + // This means we can import without risk of FOUC + if ( + document.documentElement.innerText === "" && + !HMRClient.cssAutoFOUC + ) { + if (document.body) document.body.style.visibility = "hidden"; + HMRClient.cssAutoFOUC = true; + } + + promise.then(this.processPendingCSSImports).finally(() => { + if (HMRClient.cssAutoFOUC) { + // "delete" doesn't work here. Not sure why. + if (document.body) { + // Force layout + window.getComputedStyle(document.body); + + document.body.style.visibility = "visible"; + } + HMRClient.cssAutoFOUC = false; + } + this.cssState = CSSImportState.Loaded; + }); + break; + } + case CSSImportState.Loaded: { + promise.then( + () => {}, + () => {} + ); + break; + } + case CSSImportState.Loading: { + this.cssQueue.push(promise); + break; + } + } + } + + static onCSSImport(event) { + if (globalThis["Bun_disableCSSImports"]) { + document.removeEventListener("onimportcss", HMRClient.onCSSImport); + return; + } + + const url = event.detail; + + if (typeof url !== "string" || url.length === 0) { + console.warn("[CSS Importer] Received invalid CSS import", url); + return; + } + + const thisURL = new URL(url, location.origin); + + for (let i = 0; i < document.styleSheets.length; i++) { + const sheet = document.styleSheets[i]; + if (!sheet.href) continue; + + if (sheet.href === url) { + // Already imported + return; + } + + try { + const _url1 = new URL(sheet.href, location.origin); + + if (_url1.pathname === thisURL.pathname) { + // Already imported + return; + } + } catch (e) {} + } + + const urlString = thisURL.toString(); + HMRClient.importCSS( + new Promise((resolve, reject) => { + if (globalThis["Bun_disableCSSImports"]) { + document.removeEventListener("onimportcss", HMRClient.onCSSImport); + return; + } + + var link = document.createElement("link"); + link.rel = "stylesheet"; + link.href = urlString; + link.onload = () => { + resolve(); + }; + + link.onerror = (evt) => { + console.error( + `[CSS Importer] Error loading CSS file: ${urlString}\n`, + evt.toString() + ); + reject(); + }; + document.head.appendChild(link); + }).then(() => Promise.resolve()) + ); + } static activate(verbose: boolean = false) { // Support browser-like envirnments where location and WebSocket exist // Maybe it'll work in Deno! Who knows. if ( this.client || - typeof location === "undefined" || - typeof WebSocket === "undefined" + !("location" in globalThis) || + !("WebSocket" in globalThis) ) { return; } @@ -1173,6 +1299,12 @@ if (typeof window !== "undefined") { __HMRModule = HMRModule; __FastRefreshModule = FastRefreshModule; __HMRClient = HMRClient; + + if (!globalThis["Bun_disableCSSImports"] && "document" in globalThis) { + document.addEventListener("onimportcss", HMRClient.onCSSImport, { + passive: true, + }); + } } export { __HMRModule, __FastRefreshModule, __HMRClient }; |