aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGravatar Jarred Sumner <jarred@jarredsumner.com> 2021-08-26 19:56:25 -0700
committerGravatar Jarred Sumner <jarred@jarredsumner.com> 2021-08-26 19:56:25 -0700
commit3ae0accbe3b34617be328ac46a3d8c7cbdbae6f6 (patch)
tree22cf61beab784cf3c24c87860cd08fc6a614d050
parentdb740a4eb45aecddb1c4bddbf13e7254065ef6a6 (diff)
downloadbun-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.ts7
-rw-r--r--src/api/schema.js8
-rw-r--r--src/api/schema.peechy1
-rw-r--r--src/api/schema.zig3
-rw-r--r--src/bundler.zig148
-rw-r--r--src/cli.zig4
-rw-r--r--src/fs.zig13
-rw-r--r--src/http.zig39
-rw-r--r--src/http/mime_type.zig3
-rw-r--r--src/http/url_path.zig9
-rw-r--r--src/import_record.zig8
-rw-r--r--src/js_printer.zig84
-rw-r--r--src/linker.zig18
-rw-r--r--src/main_javascript.zig2
-rw-r--r--src/options.zig22
-rw-r--r--src/query_string_map.zig33
-rw-r--r--src/runtime.version2
-rw-r--r--src/runtime/hmr.ts136
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 };