aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorGravatar Jarred Sumner <jarred@jarredsumner.com> 2021-06-17 11:14:20 -0700
committerGravatar Jarred Sumner <jarred@jarredsumner.com> 2021-06-17 11:14:20 -0700
commit9ca283bb43ebee74bf36af50807474b962ac44a1 (patch)
treea7040b8b07a38f48b1993d4515d876ed075c1833 /src
parente1677bb77414710c1114c3b52b3fa954d9276c45 (diff)
downloadbun-9ca283bb43ebee74bf36af50807474b962ac44a1.tar.gz
bun-9ca283bb43ebee74bf36af50807474b962ac44a1.tar.zst
bun-9ca283bb43ebee74bf36af50807474b962ac44a1.zip
CSS scanner works
Former-commit-id: 4ca1e17778dc4a331da5a9a21f56e0e590c799ce
Diffstat (limited to 'src')
-rw-r--r--src/bundler.zig76
-rw-r--r--src/cli.zig10
-rw-r--r--src/css_scanner.zig986
-rw-r--r--src/darwin_c.zig4
-rw-r--r--src/env.zig21
-rw-r--r--src/feature_flags.zig36
-rw-r--r--src/fs.zig28
-rw-r--r--src/global.zig58
-rw-r--r--src/http.zig8
-rw-r--r--src/js_printer.zig6
-rw-r--r--src/linker.zig171
-rw-r--r--src/options.zig15
-rw-r--r--src/resolver/resolve_path.zig4
-rw-r--r--src/test/fixtures/me@2x.jpegbin0 -> 6829 bytes
-rw-r--r--src/test/fixtures/simple.css1205
-rw-r--r--src/test/fixtures/test-import.css3
16 files changed, 2058 insertions, 573 deletions
diff --git a/src/bundler.zig b/src/bundler.zig
index fe2592c00..c1ab43638 100644
--- a/src/bundler.zig
+++ b/src/bundler.zig
@@ -151,7 +151,7 @@ pub fn NewBundler(cache_files: bool) type {
linker: Linker,
timer: Timer = Timer{},
- pub const RuntimeCode = @embedFile("./runtime.js");
+ pub const isCacheEnabled = cache_files;
// to_bundle:
@@ -949,9 +949,30 @@ pub fn NewBundler(cache_files: bool) type {
js_printer.FileWriter,
js_printer.NewFileWriter(file),
);
+
+ var file_op = options.OutputFile.FileOperation.fromFile(file.handle, file_path.pretty);
+
+ file_op.fd = file.handle;
+
+ file_op.is_tmpdir = false;
+
+ if (Outstream == std.fs.Dir) {
+ file_op.dir = outstream.fd;
+
+ if (bundler.fs.fs.needToCloseFiles()) {
+ file.close();
+ file_op.fd = 0;
+ }
+ }
+
+ output_file.value = .{ .move = file_op };
},
.css => {
- const CSSWriter = Css.NewWriter(@TypeOf(file), @TypeOf(bundler.linker), import_path_format);
+ const CSSWriter = Css.NewWriter(
+ std.fs.File,
+ @TypeOf(&bundler.linker),
+ import_path_format,
+ );
const entry = bundler.resolver.caches.fs.readFile(
bundler.fs,
file_path.text,
@@ -963,27 +984,48 @@ pub fn NewBundler(cache_files: bool) type {
const _file = Fs.File{ .path = file_path, .contents = entry.contents };
const source = try logger.Source.initFile(_file, bundler.allocator);
- var css_writer = CSSWriter.init(&source, file, bundler.linker);
+ var css_writer = CSSWriter.init(
+ &source,
+ file,
+ &bundler.linker,
+ );
try css_writer.run(bundler.log, bundler.allocator);
output_file.size = css_writer.written;
- },
- // TODO:
- else => {},
- }
+ var file_op = options.OutputFile.FileOperation.fromFile(file.handle, file_path.pretty);
- var file_op = options.OutputFile.FileOperation.fromFile(file.handle, file_path.pretty);
+ file_op.fd = file.handle;
- file_op.fd = file.handle;
+ file_op.is_tmpdir = false;
- file_op.is_tmpdir = false;
- output_file.value = .{ .move = file_op };
- if (Outstream == std.fs.Dir) {
- file_op.dir = outstream.fd;
+ if (Outstream == std.fs.Dir) {
+ file_op.dir = outstream.fd;
- if (bundler.fs.fs.needToCloseFiles()) {
- file.close();
- file_op.fd = 0;
- }
+ if (bundler.fs.fs.needToCloseFiles()) {
+ file.close();
+ file_op.fd = 0;
+ }
+ }
+
+ output_file.value = .{ .move = file_op };
+ },
+ .file => {
+ var hashed_name = try bundler.linker.getHashedFilename(file_path, null);
+ var pathname = try bundler.allocator.alloc(u8, hashed_name.len + file_path.name.ext.len);
+ std.mem.copy(u8, pathname, hashed_name);
+ std.mem.copy(u8, pathname[hashed_name.len..], file_path.name.ext);
+ const dir = if (bundler.options.output_dir_handle) |output_handle| output_handle.fd else 0;
+
+ output_file.value = .{
+ .copy = options.OutputFile.FileOperation{
+ .pathname = pathname,
+ .dir = dir,
+ .is_outdir = true,
+ },
+ };
+ },
+
+ // // TODO:
+ // else => {},
}
return output_file;
diff --git a/src/cli.zig b/src/cli.zig
index 81f7b1c87..744369472 100644
--- a/src/cli.zig
+++ b/src/cli.zig
@@ -457,15 +457,7 @@ pub const Cli = struct {
// try f.moveTo(result.outbase, constStrToU8(rel_path), root_dir.fd);
},
.copy => |value| {
- const rel_path_base = resolve_path.relativeToCommonPath(
- from_path,
- from_path,
- f.input.text,
- filepath_buf[2..],
- comptime resolve_path.Platform.auto.separator(),
- false,
- );
- rel_path = filepath_buf[0 .. rel_path_base.len + 2];
+ rel_path = value.pathname;
try f.copyTo(result.outbase, constStrToU8(rel_path), root_dir.fd);
},
diff --git a/src/css_scanner.zig b/src/css_scanner.zig
index 11f0c69a2..42c7a5778 100644
--- a/src/css_scanner.zig
+++ b/src/css_scanner.zig
@@ -6,7 +6,9 @@ const import_record = @import("import_record.zig");
const logger = @import("./logger.zig");
const Options = options;
-const replacementCharacter = 0xFFFD;
+const _linker = @import("./linker.zig");
+
+const replacementCharacter: CodePoint = 0xFFFD;
pub const Chunk = struct {
// Entire chunk
@@ -20,7 +22,7 @@ pub const Chunk = struct {
};
pub fn raw(chunk: *const Chunk, source: *const logger.Source) string {
- return source.contents[chunk.range.loc.start..][0..chunk.range.len];
+ return source.contents[@intCast(usize, chunk.range.loc.start)..][0..@intCast(usize, chunk.range.len)];
}
// pub fn string(chunk: *const Chunk, source: *const logger.Source) string {
@@ -98,503 +100,626 @@ const escLineFeed = 0x0C;
// Once found, it resolves & rewrites them
// Eventually, there will be a real CSS parser in here.
// But, no time yet.
-pub fn NewScanner(
- comptime WriterType: type,
-) type {
- return struct {
- const Scanner = @This();
- current: usize = 0,
- start: usize = 0,
- end: usize = 0,
- log: *logger.Log,
+pub const Scanner = struct {
+ current: usize = 0,
+ start: usize = 0,
+ end: usize = 0,
+ log: *logger.Log,
+
+ has_newline_before: bool = false,
+ has_delimiter_before: bool = false,
+ allocator: *std.mem.Allocator,
+
+ source: *const logger.Source,
+ codepoint: CodePoint = -1,
+ approximate_newline_count: usize = 0,
+
+ pub fn init(log: *logger.Log, allocator: *std.mem.Allocator, source: *const logger.Source) Scanner {
+ return Scanner{ .log = log, .source = source, .allocator = allocator };
+ }
- has_newline_before: bool = false,
- has_delimiter_before: bool = false,
- allocator: *std.mem.Allocator,
+ pub fn range(scanner: *Scanner) logger.Range {
+ return logger.Range{
+ .loc = .{ .start = @intCast(i32, scanner.start) },
+ .len = @intCast(i32, scanner.end - scanner.start),
+ };
+ }
- source: *const logger.Source,
- writer: WriterType,
- codepoint: CodePoint = -1,
- approximate_newline_count: usize = 0,
+ pub fn step(scanner: *Scanner) void {
+ scanner.codepoint = scanner.nextCodepoint();
+ scanner.approximate_newline_count += @boolToInt(scanner.codepoint == '\n');
+ }
+ pub fn raw(scanner: *Scanner) string {}
+
+ pub fn isValidEscape(scanner: *Scanner) bool {
+ if (scanner.codepoint != '\\') return false;
+ const slice = scanner.nextCodepointSlice(false);
+ return switch (slice.len) {
+ 0 => false,
+ 1 => true,
+ 2 => (std.unicode.utf8Decode2(slice) catch 0) > 0,
+ 3 => (std.unicode.utf8Decode3(slice) catch 0) > 0,
+ 4 => (std.unicode.utf8Decode4(slice) catch 0) > 0,
+ else => false,
+ };
+ }
- pub fn init(log: *logger.Log, allocator: *std.mem.Allocator, writer: WriterType, source: *const logger.Source) Scanner {
- return Scanner{ .writer = writer, .log = log, .source = source, .allocator = allocator };
- }
+ pub fn consumeString(
+ scanner: *Scanner,
+ comptime quote: CodePoint,
+ ) ?string {
+ const start = scanner.current;
+ scanner.step();
- pub fn range(scanner: *Scanner) logger.Range {
- return logger.Range{
- .loc = .{ .start = @intCast(i32, scanner.start) },
- .len = @intCast(i32, scanner.end - scanner.start),
- };
+ while (true) {
+ switch (scanner.codepoint) {
+ '\\' => {
+ scanner.step();
+ // Handle Windows CRLF
+ if (scanner.codepoint == '\r') {
+ scanner.step();
+ if (scanner.codepoint == '\n') {
+ scanner.step();
+ }
+ continue;
+ }
+
+ // Otherwise, fall through to ignore the character after the backslash
+ },
+ -1 => {
+ scanner.end = scanner.current;
+ scanner.log.addRangeError(
+ scanner.source,
+ scanner.range(),
+ "Unterminated string token",
+ ) catch unreachable;
+ return null;
+ },
+ '\n', '\r', escLineFeed => {
+ scanner.end = scanner.current;
+ scanner.log.addRangeError(
+ scanner.source,
+ scanner.range(),
+ "Unterminated string token",
+ ) catch unreachable;
+ return null;
+ },
+ quote => {
+ const result = scanner.source.contents[start..scanner.end];
+ scanner.step();
+ return result;
+ },
+ else => {},
+ }
+ scanner.step();
}
+ unreachable;
+ }
- pub fn step(scanner: *Scanner) void {
- scanner.codepoint = scanner.nextCodepoint();
- scanner.approximate_newline_count += @boolToInt(scanner.codepoint == '\n');
+ pub fn consumeToEndOfMultiLineComment(scanner: *Scanner, start_range: logger.Range) void {
+ while (true) {
+ switch (scanner.codepoint) {
+ '*' => {
+ scanner.step();
+ if (scanner.codepoint == '/') {
+ scanner.step();
+ return;
+ }
+ },
+ -1 => {
+ scanner.log.addRangeError(scanner.source, start_range, "Expected \"*/\" to terminate multi-line comment") catch {};
+ return;
+ },
+ else => {
+ scanner.step();
+ },
+ }
}
- pub fn raw(scanner: *Scanner) string {}
-
- pub fn isValidEscape(scanner: *Scanner) bool {
- if (scanner.codepoint != '\\') return false;
- const slice = scanner.nextCodepointSlice(false);
- return switch (slice.len) {
- 0 => false,
- 1 => true,
- 2 => (std.unicode.utf8Decode2(slice) catch 0) > 0,
- 3 => (std.unicode.utf8Decode3(slice) catch 0) > 0,
- 4 => (std.unicode.utf8Decode4(slice) catch 0) > 0,
- else => false,
- };
+ }
+ pub fn consumeToEndOfSingleLineComment(scanner: *Scanner) void {
+ while (!isNewline(scanner.codepoint) and scanner.codepoint != -1) {
+ scanner.step();
}
- pub fn consumeString(scanner: *Scanner, comptime quote: CodePoint) ?string {
- const start = scanner.current;
- scanner.step();
+ scanner.log.addRangeWarning(
+ scanner.source,
+ scanner.range(),
+ "Comments in CSS use \"/* ... */\" instead of \"//\"",
+ ) catch {};
+ }
- while (true) {
- switch (scanner.codepoint) {
- '\\' => {
+ pub fn consumeURL(scanner: *Scanner) Chunk.TextContent {
+ var text = Chunk.TextContent{ .utf8 = "" };
+ const start = scanner.end;
+ validURL: while (true) {
+ switch (scanner.codepoint) {
+ ')' => {
+ text.utf8 = scanner.source.contents[start..scanner.end];
+ scanner.step();
+ return text;
+ },
+ -1 => {
+ const loc = logger.Loc{ .start = @intCast(i32, scanner.end) };
+ scanner.log.addError(scanner.source, loc, "Expected \")\" to end URL token") catch {};
+ return text;
+ },
+ '\t', '\n', '\r', escLineFeed => {
+ scanner.step();
+ while (isWhitespace(scanner.codepoint)) {
scanner.step();
- // Handle Windows CRLF
- if (scanner.codepoint == '\r') {
- scanner.step();
- if (scanner.codepoint == '\n') {
- scanner.step();
- }
- continue;
- }
+ }
- // Otherwise, fall through to ignore the character after the backslash
- },
- -1 => {
- scanner.end = scanner.current;
- scanner.log.addRangeError(
- scanner.source,
- scanner.range(),
- "Unterminated string token",
- ) catch unreachable;
- return null;
- },
- '\n', '\r', escLineFeed => {
- scanner.end = scanner.current;
- scanner.log.addRangeError(
- scanner.source,
- scanner.range(),
- "Unterminated string token",
- ) catch unreachable;
- return null;
- },
- quote => {
- scanner.step();
- return scanner.source.contents[start..scanner.current];
- },
- else => {},
- }
- scanner.step();
+ text.utf8 = scanner.source.contents[start..scanner.end];
+
+ if (scanner.codepoint != ')') {
+ const loc = logger.Loc{ .start = @intCast(i32, scanner.end) };
+ scanner.log.addError(scanner.source, loc, "Expected \")\" to end URL token") catch {};
+ break :validURL;
+ }
+ scanner.step();
+
+ return text;
+ },
+ '"', '\'', '(' => {
+ const r = logger.Range{ .loc = logger.Loc{ .start = @intCast(i32, start) }, .len = @intCast(i32, scanner.end - start) };
+
+ scanner.log.addRangeError(scanner.source, r, "Expected \")\" to end URL token") catch {};
+ break :validURL;
+ },
+ '\\' => {
+ text.needs_decode_escape = true;
+ if (!scanner.isValidEscape()) {
+ var loc = logger.Loc{
+ .start = @intCast(i32, scanner.end),
+ };
+ scanner.log.addError(scanner.source, loc, "Expected \")\" to end URL token") catch {};
+ break :validURL;
+ }
+ _ = scanner.consumeEscape();
+ },
+ else => {
+ if (isNonPrintable(scanner.codepoint)) {
+ const r = logger.Range{
+ .loc = logger.Loc{
+ .start = @intCast(i32, start),
+ },
+ .len = 1,
+ };
+ scanner.log.addRangeError(scanner.source, r, "Invalid escape") catch {};
+ break :validURL;
+ }
+ scanner.step();
+ },
+ }
+ }
+ text.valid = false;
+ // Consume the remnants of a bad url
+ while (true) {
+ switch (scanner.codepoint) {
+ ')', -1 => {
+ scanner.step();
+ text.utf8 = scanner.source.contents[start..scanner.end];
+ return text;
+ },
+ '\\' => {
+ text.needs_decode_escape = true;
+ if (scanner.isValidEscape()) {
+ _ = scanner.consumeEscape();
+ }
+ },
+ else => {},
}
- unreachable;
+
+ scanner.step();
}
- pub fn consumeURL(scanner: *Scanner) Chunk.TextContent {
- var text = Chunk.TextContent{ .utf8 = "" };
- const start = scanner.end;
- validURL: while (true) {
+ return text;
+ }
+
+ pub fn next(scanner: *Scanner, comptime WriterType: type, writer: WriterType, writeChunk: (fn (ctx: WriterType, Chunk) anyerror!void)) !void {
+ scanner.has_newline_before = scanner.end == 0;
+ scanner.has_delimiter_before = false;
+ scanner.step();
+
+ restart: while (true) {
+ var chunk = Chunk{
+ .range = logger.Range{
+ .loc = .{ .start = @intCast(i32, scanner.end) },
+ .len = 0,
+ },
+ .content = .{
+ .t_verbatim = .{},
+ },
+ };
+ scanner.start = scanner.end;
+
+ toplevel: while (true) {
+
+ // We only care about two things.
+ // 1. url()
+ // 2. @import
+ // To correctly parse, url(), we need to verify that the character preceding it is either whitespace, a colon, or a comma
+ // We also need to parse strings and comments, or else we risk resolving comments like this /* url(hi.jpg) */
switch (scanner.codepoint) {
- ')' => {
- scanner.step();
- text.utf8 = scanner.source.contents[start..scanner.current];
- return text;
- },
-1 => {
- const loc = logger.Loc{ .start = @intCast(i32, scanner.end) };
- scanner.log.addError(scanner.source, loc, "Expected \")\" to end URL token") catch {};
- return text;
+ chunk.range.len = @intCast(i32, scanner.end) - chunk.range.loc.start;
+ chunk.content.t_verbatim = .{};
+ try writeChunk(writer, chunk);
+ return;
},
- '\t', '\n', '\r', escLineFeed => {
- scanner.step();
- while (isWhitespace(scanner.codepoint)) {
- scanner.step();
- }
- if (scanner.codepoint != ')') {
- const loc = logger.Loc{ .start = @intCast(i32, scanner.end) };
- scanner.log.addError(scanner.source, loc, "Expected \")\" to end URL token") catch {};
- break :validURL;
- }
+ '\t', '\n', '\r', escLineFeed => {
+ scanner.has_newline_before = true;
scanner.step();
- text.utf8 = scanner.source.contents[start..scanner.current];
- return text;
+ continue;
},
- '"', '\'', '(' => {
- const r = logger.Range{ .loc = logger.Loc{ .start = @intCast(i32, start) }, .len = @intCast(i32, scanner.end - start) };
+ // Ensure whitespace doesn't affect scanner.has_delimiter_before
+ ' ' => {},
- scanner.log.addRangeError(scanner.source, r, "Expected \")\" to end URL token") catch {};
- break :validURL;
+ ':', ',' => {
+ scanner.has_delimiter_before = true;
},
- '\\' => {
- text.needs_decode_escape = true;
- if (!scanner.isValidEscape()) {
- var loc = logger.Loc{
- .start = @intCast(i32, scanner.end),
- };
- scanner.log.addError(scanner.source, loc, "Expected \")\" to end URL token") catch {};
- break :validURL;
- }
- _ = scanner.consumeEscape();
+ // this is a little hacky, but it should work since we're not parsing scopes
+ '{', '}', ';' => {
+ scanner.has_delimiter_before = false;
},
- else => {
- if (isNonPrintable(scanner.codepoint)) {
- const r = logger.Range{
- .loc = logger.Loc{
- .start = @intCast(i32, start),
- },
- .len = 1,
- };
- scanner.log.addRangeError(scanner.source, r, "Invalid escape") catch {};
- break :validURL;
+ 'u', 'U' => {
+ // url() always appears on the property value side
+ // so we should ignore it if it's part of a different token
+ if (!scanner.has_delimiter_before) {
+ scanner.step();
+ continue :toplevel;
}
+
+ var url_start = scanner.end;
scanner.step();
- },
- }
- }
- text.valid = false;
- // Consume the remnants of a bad url
- while (true) {
- switch (scanner.codepoint) {
- ')', -1 => {
+ switch (scanner.codepoint) {
+ 'r', 'R' => {},
+ else => {
+ continue;
+ },
+ }
scanner.step();
- text.utf8 = scanner.source.contents[start..scanner.end];
- return text;
- },
- '\\' => {
- text.needs_decode_escape = true;
- if (scanner.isValidEscape()) {
- _ = scanner.consumeEscape();
+ switch (scanner.codepoint) {
+ 'l', 'L' => {},
+ else => {
+ continue;
+ },
+ }
+ scanner.step();
+ if (scanner.codepoint != '(') {
+ continue;
}
- },
- else => {},
- }
- scanner.step();
- }
+ scanner.step();
- return text;
- }
+ var url_text: Chunk.TextContent = undefined;
- pub fn next(scanner: *Scanner) !void {
- scanner.has_newline_before = scanner.end == 0;
- scanner.has_delimiter_before = false;
- scanner.step();
+ switch (scanner.codepoint) {
+ '\'' => {
+ const str = scanner.consumeString('\'') orelse return error.SyntaxError;
+ if (scanner.codepoint != ')') {
+ continue;
+ }
+ scanner.step();
+ url_text = .{ .utf8 = str, .quote = .double };
+ },
+ '"' => {
+ const str = scanner.consumeString('"') orelse return error.SyntaxError;
+ if (scanner.codepoint != ')') {
+ continue;
+ }
+ scanner.step();
+ url_text = .{ .utf8 = str, .quote = .single };
+ },
+ else => {
+ url_text = scanner.consumeURL();
+ },
+ }
- restart: while (true) {
- var chunk = Chunk{
- .range = logger.Range{
- .loc = .{ .start = @intCast(i32, scanner.end) },
- .len = 0,
+ chunk.range.len = @intCast(i32, url_start) - chunk.range.loc.start;
+ chunk.content = .{ .t_verbatim = .{} };
+ // flush the pending chunk
+ try writeChunk(writer, chunk);
+ chunk.range.loc.start = @intCast(i32, url_start);
+ chunk.range.len = @intCast(i32, scanner.end) - chunk.range.loc.start;
+ chunk.content = .{ .t_url = url_text };
+ try writeChunk(writer, chunk);
+ scanner.has_delimiter_before = false;
+ continue :restart;
},
- .content = .{
- .t_verbatim = .{},
- },
- };
- scanner.start = scanner.end;
-
- toplevel: while (true) {
-
- // We only care about two things.
- // 1. url()
- // 2. @import
- // To correctly parse, url(), we need to verify that the character preceding it is either whitespace, a colon, or a comma
- // We also need to parse strings and comments, or else we risk resolving comments like this /* url(hi.jpg) */
- switch (scanner.codepoint) {
- -1 => {
- chunk.range.len = @intCast(i32, scanner.end) - chunk.range.loc.start;
- chunk.content.t_verbatim = .{};
- try scanner.writer.writeChunk(chunk);
- return;
- },
-
- '\t', '\n', '\r', escLineFeed => {
- scanner.has_newline_before = true;
- continue;
- },
- // Ensure whitespace doesn't affect scanner.has_delimiter_before
- ' ' => {},
-
- ':', ',' => {
- scanner.has_delimiter_before = true;
- },
- // this is a little hacky, but it should work since we're not parsing scopes
- '{', '}', ';' => {
- scanner.has_delimiter_before = false;
- },
- 'u', 'U' => {
- // url() always appears on the property value side
- // so we should ignore it if it's part of a different token
- if (!scanner.has_delimiter_before) {
- scanner.step();
- continue :toplevel;
- }
- var url_start = scanner.current;
- scanner.step();
- switch (scanner.codepoint) {
- 'r', 'R' => {},
- else => {
- continue;
- },
- }
- scanner.step();
- switch (scanner.codepoint) {
- 'l', 'L' => {},
- else => {
- continue;
- },
- }
- scanner.step();
- if (scanner.codepoint != '(') {
- continue;
- }
- const url_text = scanner.consumeURL();
- chunk.range.len = @intCast(i32, url_start) - chunk.range.loc.start;
- chunk.content = .{ .t_verbatim = .{} };
- // flush the pending chunk
- try scanner.writer.writeChunk(chunk);
- chunk.range.loc.start = @intCast(i32, url_start);
- chunk.range.len = @intCast(i32, scanner.end) - chunk.range.loc.start;
- chunk.content.t_url = url_text;
- try scanner.writer.writeChunk(chunk);
- scanner.has_delimiter_before = false;
- continue :restart;
- },
-
- '@' => {
- const start = scanner.end;
+ '@' => {
+ const start = scanner.end;
+ scanner.step();
+ if (scanner.codepoint != 'i') continue :toplevel;
+ scanner.step();
+ if (scanner.codepoint != 'm') continue :toplevel;
+ scanner.step();
+ if (scanner.codepoint != 'p') continue :toplevel;
+ scanner.step();
+ if (scanner.codepoint != 'o') continue :toplevel;
+ scanner.step();
+ if (scanner.codepoint != 'r') continue :toplevel;
+ scanner.step();
+ if (scanner.codepoint != 't') continue :toplevel;
+ scanner.step();
+ if (scanner.codepoint != ' ') continue :toplevel;
+
+ // Now that we know to expect an import url, we flush the chunk
+ chunk.range.len = @intCast(i32, start) - chunk.range.loc.start;
+ chunk.content = .{ .t_verbatim = .{} };
+ // flush the pending chunk
+ try writeChunk(writer, chunk);
+
+ // Don't write the .start until we know it's an @import rule
+ // We want to avoid messing with other rules
+ scanner.start = start;
+
+ var url_token_start = scanner.current;
+ var url_token_end = scanner.current;
+ // "Imported rules must precede all other types of rule"
+ // https://developer.mozilla.org/en-US/docs/Web/CSS/@import
+ // @import url;
+ // @import url list-of-media-queries;
+ // @import url supports( supports-query );
+ // @import url supports( supports-query ) list-of-media-queries;
+
+ var is_url_token = false;
+ var quote: CodePoint = -1;
+ while (isWhitespace(scanner.codepoint)) {
scanner.step();
- if (scanner.codepoint != 'i') continue :toplevel;
- scanner.step();
- if (scanner.codepoint != 'm') continue :toplevel;
- scanner.step();
- if (scanner.codepoint != 'p') continue :toplevel;
- scanner.step();
- if (scanner.codepoint != 'o') continue :toplevel;
- scanner.step();
- if (scanner.codepoint != 'r') continue :toplevel;
- scanner.step();
- if (scanner.codepoint != 't') continue :toplevel;
- scanner.step();
- if (scanner.codepoint != 't') continue :toplevel;
- scanner.step();
- if (scanner.codepoint != ' ') continue :toplevel;
-
- // Now that we know to expect an import url, we flush the chunk
- chunk.range.len = @intCast(i32, start) - chunk.range.loc.start;
- chunk.content = .{ .t_verbatim = .{} };
- // flush the pending chunk
- try scanner.writer.writeChunk(chunk);
-
- // Don't write the .start until we know it's an @import rule
- // We want to avoid messing with other rules
- scanner.start = start;
-
- var url_token_start = scanner.current;
- var url_token_end = scanner.current;
- // "Imported rules must precede all other types of rule"
- // https://developer.mozilla.org/en-US/docs/Web/CSS/@import
- // @import url;
- // @import url list-of-media-queries;
- // @import url supports( supports-query );
- // @import url supports( supports-query ) list-of-media-queries;
-
- var is_url_token = false;
- var quote: CodePoint = -1;
- while (isWhitespace(scanner.codepoint)) {
- scanner.step();
- }
-
- var import = Chunk.Import{
- .text = .{
- .utf8 = "",
- },
- };
+ }
- switch (scanner.codepoint) {
- // spongebob-case url() are supported, I guess.
- // uRL()
- // uRL()
- // URl()
- 'u', 'U' => {
- scanner.step();
- switch (scanner.codepoint) {
- 'r', 'R' => {},
- else => {
- scanner.log.addError(
- scanner.source,
- logger.Loc{ .start = @intCast(i32, scanner.end) },
- "Expected @import to start with a string or url()",
- ) catch {};
- return error.SyntaxError;
- },
- }
- scanner.step();
- switch (scanner.codepoint) {
- 'l', 'L' => {},
- else => {
- scanner.log.addError(
- scanner.source,
- logger.Loc{ .start = @intCast(i32, scanner.end) },
- "Expected @import to start with a \", ' or url()",
- ) catch {};
- return error.SyntaxError;
- },
- }
- scanner.step();
- if (scanner.codepoint != '(') {
+ var import = Chunk.Import{
+ .text = .{
+ .utf8 = "",
+ },
+ };
+
+ switch (scanner.codepoint) {
+ // spongebob-case url() are supported, I guess.
+ // uRL()
+ // uRL()
+ // URl()
+ 'u', 'U' => {
+ scanner.step();
+ switch (scanner.codepoint) {
+ 'r', 'R' => {},
+ else => {
scanner.log.addError(
scanner.source,
logger.Loc{ .start = @intCast(i32, scanner.end) },
- "Expected \"(\" in @import url",
+ "Expected @import to start with a string or url()",
) catch {};
return error.SyntaxError;
- }
- import.text = scanner.consumeURL();
- },
- '"' => {
- import.text.quote = .double;
- if (scanner.consumeString('"')) |str| {
- import.text.utf8 = str;
- } else {
- return error.SyntaxError;
- }
- },
- '\'' => {
- import.text.quote = .single;
- if (scanner.consumeString('\'')) |str| {
- import.text.utf8 = str;
- } else {
+ },
+ }
+ scanner.step();
+ switch (scanner.codepoint) {
+ 'l', 'L' => {},
+ else => {
+ scanner.log.addError(
+ scanner.source,
+ logger.Loc{ .start = @intCast(i32, scanner.end) },
+ "Expected @import to start with a \", ' or url()",
+ ) catch {};
return error.SyntaxError;
- }
- },
- else => {
+ },
+ }
+ scanner.step();
+ if (scanner.codepoint != '(') {
+ scanner.log.addError(
+ scanner.source,
+ logger.Loc{ .start = @intCast(i32, scanner.end) },
+ "Expected \"(\" in @import url",
+ ) catch {};
return error.SyntaxError;
- },
- }
+ }
+
+ scanner.step();
- var suffix_start = scanner.end;
+ var url_text: Chunk.TextContent = undefined;
- get_suffix: while (true) {
switch (scanner.codepoint) {
- ';' => {
+ '\'' => {
+ const str = scanner.consumeString('\'') orelse return error.SyntaxError;
+ if (scanner.codepoint != ')') {
+ continue;
+ }
scanner.step();
- import.suffix = scanner.source.contents[suffix_start..scanner.end];
- scanner.has_delimiter_before = false;
- break :get_suffix;
+
+ url_text = .{ .utf8 = str, .quote = .single };
},
- -1 => {
- scanner.log.addError(
- scanner.source,
- logger.Loc{ .start = @intCast(i32, scanner.end) },
- "Expected \";\" at end of @import",
- ) catch {};
+ '"' => {
+ const str = scanner.consumeString('"') orelse return error.SyntaxError;
+ if (scanner.codepoint != ')') {
+ continue;
+ }
+ scanner.step();
+ url_text = .{ .utf8 = str, .quote = .double };
+ },
+ else => {
+ url_text = scanner.consumeURL();
},
- else => {},
}
- scanner.step();
- }
- chunk.range.len = @intCast(i32, scanner.end) - std.math.max(chunk.range.loc.start, 0);
- chunk.content = .{ .t_import = import };
- try scanner.writer.writeChunk(chunk);
- continue :restart;
- },
-
- // We don't actually care what the values are here, we just want to avoid confusing strings for URLs.
- '\'' => {
- scanner.has_delimiter_before = false;
- if (scanner.consumeString('\'') == null) {
- return error.SyntaxError;
- }
- },
- '"' => {
- scanner.has_delimiter_before = false;
- if (scanner.consumeString('"') == null) {
+
+ import.text = url_text;
+ },
+ '"' => {
+ import.text.quote = .double;
+ if (scanner.consumeString('"')) |str| {
+ import.text.utf8 = str;
+ } else {
+ return error.SyntaxError;
+ }
+ },
+ '\'' => {
+ import.text.quote = .single;
+ if (scanner.consumeString('\'')) |str| {
+ import.text.utf8 = str;
+ } else {
+ return error.SyntaxError;
+ }
+ },
+ else => {
return error.SyntaxError;
+ },
+ }
+
+ var suffix_start = scanner.end;
+
+ get_suffix: while (true) {
+ switch (scanner.codepoint) {
+ ';' => {
+ scanner.step();
+ import.suffix = scanner.source.contents[suffix_start..scanner.end];
+ scanner.has_delimiter_before = false;
+ break :get_suffix;
+ },
+ -1 => {
+ scanner.log.addError(
+ scanner.source,
+ logger.Loc{ .start = @intCast(i32, scanner.end) },
+ "Expected \";\" at end of @import",
+ ) catch {};
+ return;
+ },
+ else => {},
}
- },
- // Skip comments
- '/' => {},
- else => {
- scanner.has_delimiter_before = false;
- },
- }
+ scanner.step();
+ }
+ chunk.range.len = @intCast(i32, scanner.end) - std.math.max(chunk.range.loc.start, 0);
+ chunk.content = .{ .t_import = import };
+ try writeChunk(writer, chunk);
+ scanner.step();
+ continue :restart;
+ },
- scanner.step();
+ // We don't actually care what the values are here, we just want to avoid confusing strings for URLs.
+ '\'' => {
+ scanner.has_delimiter_before = false;
+ if (scanner.consumeString('\'') == null) {
+ return error.SyntaxError;
+ }
+ },
+ '"' => {
+ scanner.has_delimiter_before = false;
+ if (scanner.consumeString('"') == null) {
+ return error.SyntaxError;
+ }
+ },
+ // Skip comments
+ '/' => {
+ scanner.step();
+ switch (scanner.codepoint) {
+ '*' => {
+ scanner.step();
+ chunk.range.len = @intCast(i32, scanner.end);
+ scanner.consumeToEndOfMultiLineComment(chunk.range);
+ },
+ '/' => {
+ scanner.step();
+ scanner.consumeToEndOfSingleLineComment();
+ continue;
+ },
+ else => {
+ continue;
+ },
+ }
+ },
+ else => {
+ scanner.has_delimiter_before = false;
+ },
}
+
+ scanner.step();
}
}
+ }
+
+ pub fn consumeEscape(scanner: *Scanner) CodePoint {
+ scanner.step();
+
+ var c = scanner.codepoint;
- pub fn consumeEscape(scanner: *Scanner) CodePoint {
+ if (isHex(c)) |__hex| {
+ var hex = __hex;
scanner.step();
+ value: {
+ if (isHex(scanner.codepoint)) |_hex| {
+ scanner.step();
+ hex = hex * 16 + _hex;
+ } else {
+ break :value;
+ }
- var c = scanner.codepoint;
+ if (isHex(scanner.codepoint)) |_hex| {
+ scanner.step();
+ hex = hex * 16 + _hex;
+ } else {
+ break :value;
+ }
- if (isHex(c)) |__hex| {
- var hex = __hex;
- scanner.step();
- value: {
- comptime var i: usize = 0;
- inline while (i < 5) : (i += 1) {
- if (isHex(scanner.codepoint)) |_hex| {
- scanner.step();
- hex = hex * 16 + _hex;
- } else {
- break :value;
- }
- }
+ if (isHex(scanner.codepoint)) |_hex| {
+ scanner.step();
+ hex = hex * 16 + _hex;
+ } else {
break :value;
}
- if (isWhitespace(scanner.codepoint)) {
+ if (isHex(scanner.codepoint)) |_hex| {
scanner.step();
+ hex = hex * 16 + _hex;
+ } else {
+ break :value;
}
- return switch (hex) {
- 0, 0xD800...0xDFFF, 0x10FFFF...std.math.maxInt(CodePoint) => replacementCharacter,
- else => hex,
- };
- }
- if (c == -1) return replacementCharacter;
+ break :value;
+ }
- scanner.step();
- return c;
+ if (isWhitespace(scanner.codepoint)) {
+ scanner.step();
+ }
+ return switch (hex) {
+ 0, 0xD800...0xDFFF, 0x10FFFF...std.math.maxInt(CodePoint) => replacementCharacter,
+ else => hex,
+ };
}
- inline fn nextCodepointSlice(it: *Scanner, comptime advance: bool) []const u8 {
- @setRuntimeSafety(false);
+ if (c == -1) return replacementCharacter;
- const cp_len = strings.utf8ByteSequenceLength(it.source.contents[it.current]);
- if (advance) {
- it.end = it.current;
- it.current += cp_len;
- }
+ scanner.step();
+ return c;
+ }
- return if (!(it.current > it.source.contents.len)) it.source.contents[it.current - cp_len .. it.current] else "";
- }
+ inline fn nextCodepointSlice(it: *Scanner, comptime advance: bool) []const u8 {
+ @setRuntimeSafety(false);
- pub inline fn nextCodepoint(it: *Scanner) CodePoint {
- const slice = it.nextCodepointSlice(true);
- @setRuntimeSafety(false);
-
- return switch (slice.len) {
- 0 => -1,
- 1 => @intCast(CodePoint, slice[0]),
- 2 => @intCast(CodePoint, std.unicode.utf8Decode2(slice) catch unreachable),
- 3 => @intCast(CodePoint, std.unicode.utf8Decode3(slice) catch unreachable),
- 4 => @intCast(CodePoint, std.unicode.utf8Decode4(slice) catch unreachable),
- else => unreachable,
- };
+ const cp_len = strings.utf8ByteSequenceLength(it.source.contents[it.current]);
+ if (advance) {
+ it.end = it.current;
+ it.current += cp_len;
}
- };
-}
+
+ return if (!(it.current > it.source.contents.len)) it.source.contents[it.current - cp_len .. it.current] else "";
+ }
+
+ pub inline fn nextCodepoint(it: *Scanner) CodePoint {
+ const slice = it.nextCodepointSlice(true);
+ @setRuntimeSafety(false);
+
+ return switch (slice.len) {
+ 0 => -1,
+ 1 => @intCast(CodePoint, slice[0]),
+ 2 => @intCast(CodePoint, std.unicode.utf8Decode2(slice) catch unreachable),
+ 3 => @intCast(CodePoint, std.unicode.utf8Decode3(slice) catch unreachable),
+ 4 => @intCast(CodePoint, std.unicode.utf8Decode4(slice) catch unreachable),
+ else => unreachable,
+ };
+ }
+};
fn isWhitespace(c: CodePoint) bool {
return switch (c) {
@@ -603,6 +728,13 @@ fn isWhitespace(c: CodePoint) bool {
};
}
+fn isNewline(c: CodePoint) bool {
+ return switch (c) {
+ '\t', '\n', '\r', escLineFeed => true,
+ else => false,
+ };
+}
+
fn isNonPrintable(c: CodePoint) bool {
return switch (c) {
0...0x08, 0x0B, 0x0E...0x1F, 0x7F => true,
@@ -626,7 +758,6 @@ pub fn NewWriter(
) type {
return struct {
const Writer = @This();
- const Scanner = NewScanner(*Writer);
ctx: WriterType,
linker: LinkerType,
@@ -651,11 +782,10 @@ pub fn NewWriter(
log,
allocator,
- writer,
writer.source,
);
- try scanner.next();
+ try scanner.next(@TypeOf(writer), writer, writeChunk);
}
fn writeString(writer: *Writer, str: string, quote: Chunk.TextContent.Quote) !void {
diff --git a/src/darwin_c.zig b/src/darwin_c.zig
index 12f61d8ba..faeeea3c0 100644
--- a/src/darwin_c.zig
+++ b/src/darwin_c.zig
@@ -1,4 +1,4 @@
-pub usingnamespace @import("std").c.builtins;
+usingnamespace @import("std").c;
// int clonefileat(int src_dirfd, const char * src, int dst_dirfd, const char * dst, int flags);
pub extern "c" fn clonefileat(c_int, [*c]const u8, c_int, [*c]const u8, uint32_t: c_int) c_int;
@@ -11,5 +11,3 @@ pub extern "c" fn chmod([*c]const u8, mode_t) c_int;
pub extern "c" fn fchmod(c_int, mode_t) c_int;
pub extern "c" fn umask(mode_t) mode_t;
pub extern "c" fn fchmodat(c_int, [*c]const u8, mode_t, c_int) c_int;
-
-const mode_t = u16;
diff --git a/src/env.zig b/src/env.zig
new file mode 100644
index 000000000..2c680867c
--- /dev/null
+++ b/src/env.zig
@@ -0,0 +1,21 @@
+const std = @import("std");
+
+pub const BuildTarget = enum { native, wasm, wasi };
+pub const build_target: BuildTarget = {
+ if (std.Target.current.isWasm() and std.Target.current.getOsTag() == .wasi) {
+ return BuildTarget.wasi;
+ } else if (std.Target.current.isWasm()) {
+ return BuildTarget.wasm;
+ } else {
+ return BuildTarget.native;
+ }
+};
+
+pub const isWasm = build_target == .wasm;
+pub const isNative = build_target == .native;
+pub const isWasi = build_target == .wasi;
+pub const isMac = build_target == .native and std.Target.current.os.tag == .macos;
+pub const isBrowser = !isWasi and isWasm;
+pub const isWindows = std.Target.current.os.tag == .windows;
+pub const isDebug = std.builtin.Mode.Debug == std.builtin.mode;
+pub const isTest = std.builtin.is_test;
diff --git a/src/feature_flags.zig b/src/feature_flags.zig
new file mode 100644
index 000000000..0078d4cb9
--- /dev/null
+++ b/src/feature_flags.zig
@@ -0,0 +1,36 @@
+const env = @import("env.zig");
+
+pub const strong_etags_for_built_files = true;
+pub const keep_alive = true;
+
+// it just doesn't work well.
+pub const use_std_path_relative = false;
+pub const use_std_path_join = false;
+
+// Debug helpers
+pub const print_ast = false;
+pub const disable_printing_null = false;
+
+// This was a ~5% performance improvement
+pub const store_file_descriptors = !env.isWindows and !env.isBrowser;
+
+// This doesn't really seem to do anything for us
+pub const disable_filesystem_cache = false and std.Target.current.os.tag == .macos;
+
+pub const css_in_js_import_behavior = CSSModulePolyfill.facade;
+
+pub const only_output_esm = true;
+
+pub const jsx_runtime_is_cjs = true;
+
+pub const bundle_node_modules = true;
+
+pub const tracing = true;
+
+pub const verbose_watcher = true;
+
+pub const CSSModulePolyfill = enum {
+ // When you import a .css file and you reference the import in JavaScript
+ // Just return whatever the property key they referenced was
+ facade,
+};
diff --git a/src/fs.zig b/src/fs.zig
index 043a7bd19..dd5029b40 100644
--- a/src/fs.zig
+++ b/src/fs.zig
@@ -462,6 +462,34 @@ pub const FileSystem = struct {
mtime: i128 = 0,
mode: std.fs.File.Mode = 0,
+ threadlocal var hash_bytes: [32]u8 = undefined;
+ threadlocal var hash_name_buf: [1024]u8 = undefined;
+
+ pub fn hashName(
+ this: *const ModKey,
+ basename: string,
+ ) !string {
+
+ // We shouldn't just read the contents of the ModKey into memory
+ // The hash should be deterministic across computers and operating systems.
+ // inode is non-deterministic across volumes within the same compuiter
+ // so if we're not going to do a full content hash, we should use mtime and size.
+ // even mtime is debatable.
+ var hash_bytes_remain: []u8 = hash_bytes[0..];
+ std.mem.writeIntNative(@TypeOf(this.size), hash_bytes_remain[0..@sizeOf(@TypeOf(this.size))], this.size);
+ hash_bytes_remain = hash_bytes_remain[@sizeOf(@TypeOf(this.size))..];
+ std.mem.writeIntNative(@TypeOf(this.mtime), hash_bytes_remain[0..@sizeOf(@TypeOf(this.mtime))], this.mtime);
+
+ return try std.fmt.bufPrint(
+ &hash_name_buf,
+ "{s}-{x}",
+ .{
+ basename,
+ @truncate(u32, std.hash.Wyhash.hash(1, &hash_bytes)),
+ },
+ );
+ }
+
pub fn generate(fs: *RealFS, path: string, file: std.fs.File) anyerror!ModKey {
const stat = try file.stat();
diff --git a/src/global.zig b/src/global.zig
index 97727458f..3d985a930 100644
--- a/src/global.zig
+++ b/src/global.zig
@@ -2,63 +2,9 @@ const std = @import("std");
pub usingnamespace @import("strings.zig");
pub const C = @import("c.zig");
-pub const BuildTarget = enum { native, wasm, wasi };
-pub const build_target: BuildTarget = comptime {
- if (std.Target.current.isWasm() and std.Target.current.getOsTag() == .wasi) {
- return BuildTarget.wasi;
- } else if (std.Target.current.isWasm()) {
- return BuildTarget.wasm;
- } else {
- return BuildTarget.native;
- }
-};
-
-pub const isWasm = build_target == .wasm;
-pub const isNative = build_target == .native;
-pub const isWasi = build_target == .wasi;
-pub const isMac = build_target == .native and std.Target.current.os.tag == .macos;
-pub const isBrowser = !isWasi and isWasm;
-pub const isWindows = std.Target.current.os.tag == .windows;
-
-pub const FeatureFlags = struct {
- pub const strong_etags_for_built_files = true;
- pub const keep_alive = true;
-
- // it just doesn't work well.
- pub const use_std_path_relative = false;
- pub const use_std_path_join = false;
-
- // Debug helpers
- pub const print_ast = false;
- pub const disable_printing_null = false;
-
- // This was a ~5% performance improvement
- pub const store_file_descriptors = !isWindows and !isBrowser;
-
- // This doesn't really seem to do anything for us
- pub const disable_filesystem_cache = false and std.Target.current.os.tag == .macos;
-
- pub const css_in_js_import_behavior = CSSModulePolyfill.facade;
-
- pub const only_output_esm = true;
-
- pub const jsx_runtime_is_cjs = true;
-
- pub const bundle_node_modules = true;
-
- pub const tracing = true;
-
- pub const verbose_watcher = true;
-
- pub const CSSModulePolyfill = enum {
- // When you import a .css file and you reference the import in JavaScript
- // Just return whatever the property key they referenced was
- facade,
- };
-};
+pub usingnamespace @import("env.zig");
-pub const isDebug = std.builtin.Mode.Debug == std.builtin.mode;
-pub const isTest = std.builtin.is_test;
+pub const FeatureFlags = @import("feature_flags.zig");
pub const Output = struct {
threadlocal var source: *Source = undefined;
diff --git a/src/http.zig b/src/http.zig
index e818efd89..e6184915e 100644
--- a/src/http.zig
+++ b/src/http.zig
@@ -237,7 +237,7 @@ pub const RequestContext = struct {
ctx.appendHeader("Transfer-Encoding", "Chunked");
} else {
const length_str = try ctx.allocator.alloc(u8, 64);
- ctx.appendHeader("Content-Length", length_str[0..std.fmt.formatIntBuf(length_str, length, 10, true, .{})]);
+ ctx.appendHeader("Content-Length", length_str[0..std.fmt.formatIntBuf(length_str, length, 10, .upper, .{})]);
}
try ctx.flushHeaders();
@@ -952,7 +952,7 @@ pub const RequestContext = struct {
if (FeatureFlags.strong_etags_for_built_files) {
if (buf.len < 16 * 16 * 16 * 16) {
const strong_etag = std.hash.Wyhash.hash(1, buf);
- const etag_content_slice = std.fmt.bufPrintIntToSlice(strong_etag_buffer[0..49], strong_etag, 16, true, .{});
+ const etag_content_slice = std.fmt.bufPrintIntToSlice(strong_etag_buffer[0..49], strong_etag, 16, .upper, .{});
chunky.rctx.appendHeader("ETag", etag_content_slice);
@@ -1047,7 +1047,7 @@ pub const RequestContext = struct {
weak_etag.update(weak_etag_tmp_buffer[0..16]);
}
- const etag_content_slice = std.fmt.bufPrintIntToSlice(weak_etag_buffer[2..], weak_etag.final(), 16, true, .{});
+ const etag_content_slice = std.fmt.bufPrintIntToSlice(weak_etag_buffer[2..], weak_etag.final(), 16, .upper, .{});
const complete_weak_etag = weak_etag_buffer[0 .. etag_content_slice.len + 2];
ctx.appendHeader("ETag", complete_weak_etag);
@@ -1101,7 +1101,7 @@ pub const RequestContext = struct {
if (FeatureFlags.strong_etags_for_built_files) {
// TODO: don't hash runtime.js
const strong_etag = std.hash.Wyhash.hash(1, buffer);
- const etag_content_slice = std.fmt.bufPrintIntToSlice(strong_etag_buffer[0..49], strong_etag, 16, true, .{});
+ const etag_content_slice = std.fmt.bufPrintIntToSlice(strong_etag_buffer[0..49], strong_etag, 16, .upper, .{});
ctx.appendHeader("ETag", etag_content_slice);
diff --git a/src/js_printer.zig b/src/js_printer.zig
index a15b9f3c7..d55473481 100644
--- a/src/js_printer.zig
+++ b/src/js_printer.zig
@@ -482,7 +482,7 @@ pub fn NewPrinter(
// In JavaScript, numbers are represented as 64 bit floats
// However, they could also be signed or unsigned int 32 (when doing bit shifts)
// In this case, it's always going to unsigned since that conversion has already happened.
- std.fmt.formatInt(@floatToInt(u32, float), 10, true, .{}, p) catch unreachable;
+ std.fmt.formatInt(@floatToInt(u32, float), 10, .upper, .{}, p) catch unreachable;
return;
}
@@ -3274,7 +3274,7 @@ pub fn NewPrinter(
pub fn printLoadFromBundleWithoutCall(p: *Printer, import_record_index: u32) void {
const record = p.import_records[import_record_index];
p.print("$");
- std.fmt.formatInt(record.module_id, 16, false, .{}, p) catch unreachable;
+ std.fmt.formatInt(record.module_id, 16, .lower, .{}, p) catch unreachable;
}
pub fn printBundledRequire(p: *Printer, require: E.Require) void {
if (p.import_records[require.import_record_index].is_internal) {
@@ -3426,7 +3426,7 @@ pub fn NewPrinter(
p.print("// ");
p.print(p.options.source_path.?.pretty);
p.print("\nexport var $");
- std.fmt.formatInt(p.options.module_hash, 16, false, .{}, p) catch unreachable;
+ std.fmt.formatInt(p.options.module_hash, 16, .lower, .{}, p) catch unreachable;
p.print(" = ");
p.printExpr(decls[0].value.?, .comma, ExprFlag.None());
p.printSemicolonAfterStatement();
diff --git a/src/linker.zig b/src/linker.zig
index 550b48590..9c20718e0 100644
--- a/src/linker.zig
+++ b/src/linker.zig
@@ -28,8 +28,11 @@ const Bundler = _bundler.Bundler;
const ResolveQueue = _bundler.ResolveQueue;
const Runtime = @import("./runtime.zig").Runtime;
+pub const CSSResolveError = error{ResolveError};
+
pub fn NewLinker(comptime BundlerType: type) type {
return struct {
+ const HashedFileNameMap = std.AutoHashMap(u64, string);
const ThisLinker = @This();
allocator: *std.mem.Allocator,
options: *Options.BundleOptions,
@@ -41,6 +44,7 @@ pub fn NewLinker(comptime BundlerType: type) type {
any_needs_runtime: bool = false,
runtime_import_record: ?ImportRecord = null,
runtime_source_path: string,
+ hashed_filenames: HashedFileNameMap,
pub fn init(
allocator: *std.mem.Allocator,
@@ -62,6 +66,7 @@ pub fn NewLinker(comptime BundlerType: type) type {
.resolver = resolver,
.resolve_results = resolve_results,
.runtime_source_path = fs.absAlloc(allocator, &([_]string{"__runtime.js"})) catch unreachable,
+ .hashed_filenames = HashedFileNameMap.init(allocator),
};
}
@@ -71,35 +76,87 @@ pub fn NewLinker(comptime BundlerType: type) type {
return RequireOrImportMeta{};
}
- pub fn resolveCSS(
+ pub fn getHashedFilename(
this: *ThisLinker,
+ file_path: Fs.Path,
+ fd: ?FileDescriptorType,
+ ) !string {
+ if (BundlerType.isCacheEnabled) {
+ var hashed = std.hash.Wyhash.hash(0, file_path.text);
+ var hashed_result = try this.hashed_filenames.getOrPut(hashed);
+ if (hashed_result.found_existing) {
+ return hashed_result.value_ptr.*;
+ }
+ }
+
+ var file: std.fs.File = if (fd) |_fd| std.fs.File{ .handle = _fd } else try std.fs.openFileAbsolute(file_path.text, .{ .read = true });
+ Fs.FileSystem.setMaxFd(file.handle);
+ var modkey = try Fs.FileSystem.RealFS.ModKey.generate(&this.fs.fs, file_path.text, file);
+ const hash_name = try modkey.hashName(file_path.name.base);
+
+ if (BundlerType.isCacheEnabled) {
+ var hashed = std.hash.Wyhash.hash(0, file_path.text);
+ try this.hashed_filenames.put(hashed, try this.allocator.dupe(u8, hash_name));
+ }
+
+ if (this.fs.fs.needToCloseFiles() and fd == null) {
+ file.close();
+ }
+
+ return hash_name;
+ }
+
+ pub fn resolveCSS(
+ this: anytype,
path: Fs.Path,
url: string,
range: logger.Range,
- comptime kind: ImportKind,
+ kind: ImportKind,
comptime import_path_format: Options.BundleOptions.ImportPathFormat,
) !string {
+ const dir = path.name.dirWithTrailingSlash();
+
switch (kind) {
.at => {
- var resolve_result = try this.resolver.resolve(path.name.dir, url, .at);
+ var resolve_result = try this.resolver.resolve(dir, url, .at);
+ if (resolve_result.is_external) {
+ return resolve_result.path_pair.primary.text;
+ }
+
var import_record = ImportRecord{ .range = range, .path = resolve_result.path_pair.primary, .kind = kind };
- try this.processImportRecord(path.name.dir, &resolve_result, &import_record, import_path_format);
+
+ const loader = this.options.loaders.get(resolve_result.path_pair.primary.name.ext) orelse .file;
+
+ this.processImportRecord(loader, dir, &resolve_result, &import_record, import_path_format) catch unreachable;
return import_record.path.text;
},
.at_conditional => {
- var resolve_result = try this.resolver.resolve(path.name.dir, url, .at_conditional);
+ var resolve_result = try this.resolver.resolve(dir, url, .at_conditional);
+ if (resolve_result.is_external) {
+ return resolve_result.path_pair.primary.text;
+ }
+
var import_record = ImportRecord{ .range = range, .path = resolve_result.path_pair.primary, .kind = kind };
- try this.processImportRecord(path.name.dir, &resolve_result, &import_record, import_path_format);
+ const loader = this.options.loaders.get(resolve_result.path_pair.primary.name.ext) orelse .file;
+
+ this.processImportRecord(loader, dir, &resolve_result, &import_record, import_path_format) catch unreachable;
return import_record.path.text;
},
.url => {
- var resolve_result = try this.resolver.resolve(path.name.dir, url, .url);
+ var resolve_result = try this.resolver.resolve(dir, url, .url);
+ if (resolve_result.is_external) {
+ return resolve_result.path_pair.primary.text;
+ }
+
var import_record = ImportRecord{ .range = range, .path = resolve_result.path_pair.primary, .kind = kind };
- try this.processImportRecord(path.name.dir, &resolve_result, &import_record, import_path_format);
+ const loader = this.options.loaders.get(resolve_result.path_pair.primary.name.ext) orelse .file;
+
+ this.processImportRecord(loader, dir, &resolve_result, &import_record, import_path_format) catch unreachable;
return import_record.path.text;
},
else => unreachable,
}
+ unreachable;
}
// pub const Scratch = struct {
@@ -137,6 +194,7 @@ pub fn NewLinker(comptime BundlerType: type) type {
source_dir,
linker.runtime_source_path,
Runtime.version(),
+ false,
import_path_format,
);
result.ast.runtime_import_record_id = record_index;
@@ -214,6 +272,8 @@ pub fn NewLinker(comptime BundlerType: type) type {
}
linker.processImportRecord(
+ linker.options.loaders.get(resolved_import.path_pair.primary.name.ext) orelse .file,
+
// Include trailing slash
file_path.text[0 .. source_dir.len + 1],
resolved_import,
@@ -290,6 +350,7 @@ pub fn NewLinker(comptime BundlerType: type) type {
source_dir,
linker.runtime_source_path,
Runtime.version(),
+ false,
import_path_format,
),
.range = logger.Range{ .loc = logger.Loc{ .start = 0 }, .len = 0 },
@@ -355,18 +416,54 @@ pub fn NewLinker(comptime BundlerType: type) type {
source_dir: string,
source_path: string,
package_version: ?string,
+ use_hashed_name: bool,
comptime import_path_format: Options.BundleOptions.ImportPathFormat,
) !Fs.Path {
switch (import_path_format) {
.relative => {
- var pretty = try linker.allocator.dupe(u8, linker.fs.relative(source_dir, source_path));
- var pathname = Fs.PathName.init(pretty);
- return Fs.Path.initWithPretty(pretty, pretty);
+ var relative_name = linker.fs.relative(source_dir, source_path);
+ var pretty: string = undefined;
+ if (use_hashed_name) {
+ var basepath = Fs.Path.init(source_path);
+ const basename = try linker.getHashedFilename(basepath, null);
+ var dir = basepath.name.dirWithTrailingSlash();
+ var _pretty = try linker.allocator.alloc(u8, dir.len + basename.len + basepath.name.ext.len);
+ std.mem.copy(u8, _pretty, dir);
+ var remaining_pretty = _pretty[dir.len..];
+ std.mem.copy(u8, remaining_pretty, basename);
+ remaining_pretty = remaining_pretty[basename.len..];
+ std.mem.copy(u8, remaining_pretty, basepath.name.ext);
+ pretty = _pretty;
+ relative_name = try linker.allocator.dupe(u8, relative_name);
+ } else {
+ pretty = try linker.allocator.dupe(u8, relative_name);
+ relative_name = pretty;
+ }
+
+ return Fs.Path.initWithPretty(pretty, relative_name);
},
.relative_nodejs => {
- var pretty = try linker.allocator.dupe(u8, linker.fs.relative(source_dir, source_path));
+ var relative_name = linker.fs.relative(source_dir, source_path);
+ var pretty: string = undefined;
+ if (use_hashed_name) {
+ var basepath = Fs.Path.init(source_path);
+ const basename = try linker.getHashedFilename(basepath, null);
+ var dir = basepath.name.dirWithTrailingSlash();
+ var _pretty = try linker.allocator.alloc(u8, dir.len + basename.len + basepath.name.ext.len);
+ std.mem.copy(u8, _pretty, dir);
+ var remaining_pretty = _pretty[dir.len..];
+ std.mem.copy(u8, remaining_pretty, basename);
+ remaining_pretty = remaining_pretty[basename.len..];
+ std.mem.copy(u8, remaining_pretty, basepath.name.ext);
+ pretty = _pretty;
+ relative_name = try linker.allocator.dupe(u8, relative_name);
+ } else {
+ pretty = try linker.allocator.dupe(u8, relative_name);
+ relative_name = pretty;
+ }
+
var pathname = Fs.PathName.init(pretty);
- var path = Fs.Path.initWithPretty(pretty, pretty);
+ var path = Fs.Path.initWithPretty(pretty, relative_name);
path.text = path.text[0 .. path.text.len - path.name.ext.len];
return path;
},
@@ -385,33 +482,27 @@ pub fn NewLinker(comptime BundlerType: type) type {
base = base[0..dot];
}
- if (linker.options.append_package_version_in_query_string and package_version != null) {
- const absolute_url =
- try std.fmt.allocPrint(
- linker.allocator,
- "{s}{s}{s}?v={s}",
- .{
- linker.options.public_url,
- base,
- absolute_pathname.ext,
- package_version.?,
- },
- );
-
- return Fs.Path.initWithPretty(absolute_url, absolute_url);
- } else {
- const absolute_url = try std.fmt.allocPrint(
- linker.allocator,
- "{s}{s}{s}",
- .{
- linker.options.public_url,
- base,
- absolute_pathname.ext,
- },
- );
-
- return Fs.Path.initWithPretty(absolute_url, absolute_url);
+ var dirname = std.fs.path.dirname(base) orelse "";
+
+ var basename = std.fs.path.basename(base);
+
+ if (use_hashed_name) {
+ var basepath = Fs.Path.init(source_path);
+ basename = try linker.getHashedFilename(basepath, null);
}
+
+ const absolute_url = try std.fmt.allocPrint(
+ linker.allocator,
+ "{s}{s}{s}{s}",
+ .{
+ linker.options.public_url,
+ dirname,
+ basename,
+ absolute_pathname.ext,
+ },
+ );
+
+ return Fs.Path.initWithPretty(absolute_url, absolute_url);
},
else => unreachable,
@@ -420,6 +511,7 @@ pub fn NewLinker(comptime BundlerType: type) type {
pub fn processImportRecord(
linker: *ThisLinker,
+ loader: Options.Loader,
source_dir: string,
resolve_result: *Resolver.Result,
import_record: *ImportRecord,
@@ -440,6 +532,7 @@ pub fn NewLinker(comptime BundlerType: type) type {
source_dir,
resolve_result.path_pair.primary.text,
if (resolve_result.package_json) |package_json| package_json.version else "",
+ BundlerType.isCacheEnabled and loader == .file,
import_path_format,
);
}
diff --git a/src/options.zig b/src/options.zig
index 204f457e1..e9f48ee17 100644
--- a/src/options.zig
+++ b/src/options.zig
@@ -607,7 +607,7 @@ pub const BundleOptions = struct {
};
pub const Defaults = struct {
- pub var ExtensionOrder = [_]string{ ".tsx", ".ts", ".jsx", ".js", ".json" };
+ pub var ExtensionOrder = [_]string{ ".tsx", ".ts", ".jsx", ".js", ".json", ".css" };
};
pub fn fromApi(
@@ -860,6 +860,7 @@ pub const OutputFile = struct {
fd: FileDescriptorType = 0,
dir: FileDescriptorType = 0,
is_tmpdir: bool = false,
+ is_outdir: bool = false,
pub fn fromFile(fd: FileDescriptorType, pathname: string) FileOperation {
return .{
@@ -939,16 +940,6 @@ pub const OutputFile = struct {
pub fn copyTo(file: *const OutputFile, base_path: string, rel_path: []u8, dir: FileDescriptorType) !void {
var copy = file.value.copy;
- if (isMac and copy.fd > 0) {
- // First try using a copy-on-write clonefile()
- // this will fail if the destination already exists
- rel_path.ptr[rel_path.len + 1] = 0;
- var rel_c_path = rel_path.ptr[0..rel_path.len :0];
- const success = C.fclonefileat(copy.fd, dir, rel_c_path, 0) == 0;
- if (success) {
- return;
- }
- }
var dir_obj = std.fs.Dir{ .fd = dir };
const file_out = (try dir_obj.createFile(rel_path, .{}));
@@ -956,7 +947,7 @@ pub const OutputFile = struct {
const fd_out = file_out.handle;
var do_close = false;
// TODO: close file_out on error
- const fd_in = if (copy.fd > 0) copy.fd else (try std.fs.openFileAbsolute(copy.getPathname(), .{ .read = true })).handle;
+ const fd_in = (try std.fs.openFileAbsolute(file.input.text, .{ .read = true })).handle;
if (isNative) {
Fs.FileSystem.setMaxFd(fd_out);
diff --git a/src/resolver/resolve_path.zig b/src/resolver/resolve_path.zig
index 2bbd83c55..1bba7d12c 100644
--- a/src/resolver/resolve_path.zig
+++ b/src/resolver/resolve_path.zig
@@ -1,6 +1,6 @@
const tester = @import("../test/tester.zig");
-const FeatureFlags = @import("../global.zig").FeatureFlags;
+const FeatureFlags = @import("../feature_flags.zig");
const std = @import("std");
threadlocal var parser_join_input_buffer: [1024]u8 = undefined;
@@ -259,7 +259,7 @@ pub fn relativeToCommonPath(
var out_slice: []u8 = buf[0..0];
if (normalized_from.len > 0) {
- var i: usize = @boolToInt(normalized_from[0] == separator) + 1 + last_common_separator;
+ var i: usize = @intCast(usize, @boolToInt(normalized_from[0] == separator)) + 1 + last_common_separator;
while (i <= normalized_from.len) : (i += 1) {
if (i == normalized_from.len or (normalized_from[i] == separator and i + 1 < normalized_from.len)) {
diff --git a/src/test/fixtures/me@2x.jpeg b/src/test/fixtures/me@2x.jpeg
new file mode 100644
index 000000000..02f8a3b19
--- /dev/null
+++ b/src/test/fixtures/me@2x.jpeg
Binary files differ
diff --git a/src/test/fixtures/simple.css b/src/test/fixtures/simple.css
new file mode 100644
index 000000000..63b3aa46e
--- /dev/null
+++ b/src/test/fixtures/simple.css
@@ -0,0 +1,1205 @@
+@import url("./test-import.css");
+
+* {
+ box-sizing: border-box;
+
+ background-image: url("./me@2x.jpeg");
+}
+
+:root {
+ --heading-font: "Space Mono", system-ui;
+ --body-font: "IBM Plex Sans", system-ui;
+
+ --color-brand: #02ff00;
+ --color-brand-muted: rgb(2, 150, 0);
+
+ --padding-horizontal: 90px;
+
+ --page-background: black;
+ --page-background-alpha: rgba(0, 0, 0, 0.8);
+
+ --result__background-color: black;
+ --result__primary-color: var(--color-brand);
+ --result__foreground-color: white;
+ --result__muted-color: rgb(165, 165, 165);
+
+ --card-width: 352px;
+
+ --page-width: 1152px;
+
+ --snippets_container-background-unfocused: #171717;
+ --snippets_container-background-focused: #0017e9;
+ --snippets_container-background: var(
+ --snippets_container-background-unfocused
+ );
+ --snippets_container-muted-color: rgb(153, 153, 153);
+}
+
+html,
+body {
+ padding: 0;
+ margin: 0;
+}
+
+body {
+ font-family: var(--body-font);
+ background-color: var(--page-background);
+ color: white;
+}
+
+a {
+ color: inherit;
+ text-decoration: none;
+}
+
+* {
+ box-sizing: border-box;
+ font-variant-ligatures: no-common-ligatures;
+}
+
+h1,
+h2,
+h3 {
+ font-family: var(--heading-font);
+ font-weight: normal;
+}
+
+.NavigationContainer {
+ display: grid;
+ grid-template-columns: min-content min-content min-content;
+ align-items: center;
+}
+
+.NavigationLink,
+.NavigationLink:visited {
+ font-weight: 500;
+ font-family: var(--heading-font);
+ font-size: 0.9rem;
+ letter-spacing: 0.05rem;
+ text-transform: uppercase;
+ display: block;
+ text-decoration: none;
+}
+
+.SnippetList {
+ scroll-margin-top: 200px;
+}
+
+.SnippetList,
+.NewBenchmarkPageContent {
+ display: grid;
+ grid-row-gap: 16px;
+}
+
+.NewBenchmarkPageContent {
+ margin-top: 24px;
+ margin-bottom: 42px;
+}
+
+.NavigationLink:hover {
+ border-bottom-color: var(--color-brand);
+}
+
+.NavigationLink--inactive {
+ color: rgb(165, 165, 165);
+}
+
+.NavigationLink--active {
+ color: var(--color-brand);
+}
+
+.NavigationLink--inactive:hover {
+ cursor: pointer;
+ color: var(--color-brand);
+}
+
+.LandingHeader-Container {
+ padding: 0 var(--padding-horizontal);
+ backdrop-filter: blur(5px);
+ background-color: var(--page-background-alpha);
+ position: sticky;
+ top: 0;
+ z-index: 999;
+ padding-bottom: 1.2em;
+ mask-image: linear-gradient(to bottom, black 85%, transparent);
+}
+
+.LandingHeader {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ padding: 34px var(--padding-horizontal);
+}
+
+.HeroContainer {
+ padding: 34px var(--padding-horizontal);
+ width: 100%;
+ display: flex;
+}
+
+.Gallery,
+.HeroContainer,
+.LandingHeader {
+ max-width: var(--page-width);
+ margin: 0 auto;
+}
+
+.GalleryList {
+ padding: 34px var(--padding-horizontal);
+}
+
+.GalleryList {
+ display: grid;
+ grid-gap: 16px;
+ grid-template-columns: repeat(auto-fit, var(--card-width));
+}
+
+.HeroContainer {
+ align-items: center;
+}
+
+.Hero {
+ flex: 1;
+}
+
+.Description {
+ margin-bottom: 24px;
+}
+
+.Tagline,
+.Hero-demo {
+ margin-block-start: 0.67em;
+ margin-block-end: 0.67em;
+}
+
+.Hero-demo {
+ margin-left: 48px;
+}
+
+.Tagline {
+ font-size: 2.2rem;
+}
+
+.RunTestButtonContainer {
+ position: sticky;
+}
+
+.LinkButton {
+ color: black;
+ background: none;
+ outline: none;
+ background-color: var(--color-brand);
+ font-weight: bold;
+ font-family: var(--heading-font);
+ font-size: 1.2rem;
+ padding: 10px 17px;
+ text-transform: uppercase;
+ width: min-content;
+ text-decoration: none;
+
+ white-space: nowrap;
+ cursor: pointer;
+ display: flex;
+ justify-content: space-between;
+ transition: transform 0.1s linear;
+}
+
+.LinkButton:hover {
+ transform: scale(1.03, 1.03);
+}
+
+.LinkButton-arrow {
+ animation: arrow-move-animation 1s ease-in-out;
+ animation-iteration-count: infinite;
+ animation-direction: alternate;
+ animation-delay: 0.1s;
+ transform: translateX(0px) scale(1, 1);
+ animation-play-state: paused;
+}
+
+.LinkButton:hover .LinkButton-arrow {
+ animation-play-state: running;
+}
+
+@keyframes arrow-move-animation {
+ 0% {
+ transform: translateX(0px) scale(1, 1);
+ }
+ 100% {
+ transform: translateX(4px) scale(1.1, 1.1);
+ }
+}
+
+.LinkButton-arrow {
+ margin-left: 12px;
+ align-self: center;
+ margin-top: auto;
+ margin-bottom: auto;
+}
+
+.ResultCard--blue,
+.ResultCard--blue * {
+ --result__background-color: #0017e9;
+ --result__primary-color: var(--color-brand);
+ --result__foreground-color: white;
+ --result__muted-color: rgb(165, 165, 165);
+}
+
+.ResultCard {
+ padding: 16px;
+ max-width: var(--card-width);
+
+ display: grid;
+ grid-template-rows: 1fr auto 1fr;
+ grid-template-columns: auto;
+ grid-row-gap: 16px;
+
+ background-color: var(--result__background-color);
+ color: var(--result__foreground-color);
+}
+
+.ResultCard-title {
+ font-size: 1rem;
+ font-weight: bold;
+ font-family: var(--body-font);
+ white-space: nowrap;
+ text-overflow: ellipsis;
+ max-width: 100%;
+}
+
+.ResultList {
+ display: grid;
+ grid-row-gap: 8px;
+}
+
+.ResultListItem-name {
+ font-weight: bold;
+ white-space: nowrap;
+}
+
+.ResultListItem-progressContainer {
+ display: block;
+ width: auto;
+ margin-top: auto;
+ margin-bottom: auto;
+ height: 2px;
+ content: "";
+}
+
+.ResultListItem-progressValue {
+ background-color: var(--result__foreground-color);
+ height: 2px;
+ content: "";
+ display: block;
+}
+
+.ResultListItem--fastest .ResultListItem-name {
+ color: var(--result__primary-color);
+}
+
+.ResultListItem--fastest .ResultListItem-progressValue {
+ background-color: var(--result__primary-color);
+}
+
+.ResultListItem--slowest .ResultListItem-progressValue {
+ background-color: var(--result__muted-color);
+}
+
+.ResultListItem--slowest .ResultListItem-name {
+ color: var(--result__muted-color);
+}
+
+.ResultListItem {
+ grid-template-columns: 100px 100px;
+ display: grid;
+ grid-column-gap: 6px;
+}
+
+.ListContainer {
+ display: flex;
+}
+
+.ScoreContainer {
+ margin-right: 26px;
+}
+
+.Score {
+ color: var(--result__primary-color);
+ font-size: 2rem;
+ font-weight: bold;
+ font-family: var(--heading-font);
+}
+
+.Operations {
+ color: var(--result__muted-color);
+ font-size: 1rem;
+}
+
+.ResultCard-link {
+ display: flex;
+ align-items: center;
+ font-weight: bold;
+ text-transform: uppercase;
+}
+.ResultCard-linkArrow {
+ margin-left: 6px;
+}
+
+.Confetti {
+ position: fixed;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ transform: translate(25vh, 10vw);
+ overflow: hidden;
+ pointer-events: none;
+ user-select: none;
+ webkit-user-select: none;
+}
+
+.ShareSheet,
+.ResultListSection,
+.BenchmarkHeader,
+.SnippetList,
+.CodeEditor {
+ width: var(--page-width);
+ padding: 0px var(--padding-horizontal);
+ margin: 0 auto;
+}
+
+.BenchmarkHeader {
+ display: flex;
+ align-items: center;
+}
+
+.TitleInput-container {
+ display: flex;
+ flex: 1;
+ width: 100%;
+}
+
+.TitleInput {
+ background-color: transparent;
+ appearance: none;
+ -webkit-appearance: none;
+ font-size: 2.5rem;
+ box-shadow: none;
+ outline: 0;
+
+ border: 0;
+ width: 100%;
+ margin-right: 32px;
+ font-family: var(--heading-font);
+ border-bottom: 1px solid transparent;
+ color: white;
+ transition: all 0.2s ease;
+}
+
+.TitleInput-container--readOnly .TitleInput,
+.TitleInput-container--readOnly .TitleInput:hover {
+ cursor: pointer;
+}
+.TitleInput-container--readWrite .TitleInput:hover {
+ background: rgba(255, 255, 255, 0.1);
+}
+
+.TitleInput-container--readWrite .TitleInput:focus {
+ background: rgba(255, 255, 255, 0.1);
+}
+
+.TitleInput,
+.SnippetTitle {
+ text-transform: none;
+}
+
+.SnippetTitle[disabled] {
+ background-color: transparent;
+}
+
+.SnippetTitle,
+.NewSnippetContainer-label {
+ background: transparent;
+ display: block;
+ flex: 1;
+ appearance: none;
+ -webkit-appearance: none;
+
+ color: white;
+ font-family: var(--heading-font);
+ font-weight: 400;
+ height: 100%;
+ padding: 16px 12px;
+ margin: 0;
+ border: 0;
+ box-shadow: none;
+}
+
+.NewSnippetContainer,
+.SnippetContainer *,
+.SnippetContainer {
+ --snippets_container-background: var(
+ --snippets_container-background-unfocused
+ );
+}
+
+.SnippetContainer--isRunning {
+ overflow: hidden;
+}
+
+.SnippetContainer {
+ position: relative;
+}
+
+.SnippetContainer-ErrorTitle {
+ position: absolute;
+ bottom: 0;
+ left: 0;
+ right: 0;
+ background-color: #fe0100;
+ padding: 6px 14px;
+
+ font-size: 0.8rem;
+ color: #ffffff;
+ font-weight: 500;
+ font-family: var(--heading-font);
+}
+
+.SnippetContainer .SnippetOverlay,
+.SnippetContainer .SnippetBackground {
+ display: none;
+}
+
+.SnippetBackground {
+ position: absolute;
+ top: 0;
+ left: 0;
+ bottom: 0;
+ right: 0;
+ width: 100%;
+ height: 100%;
+ z-index: 999;
+ mix-blend-mode: difference;
+ -webkit-user-select: none;
+ user-select: none;
+ pointer-events: none;
+ height: 100%;
+ transform-origin: left;
+ transform: scaleX(0);
+ content: "";
+ background-color: var(--color-brand);
+ transition: transform 0.12s linear;
+ transition-property: opacity, transform;
+}
+
+.SnippetOverlay {
+ position: absolute;
+
+ left: 24px;
+ top: 50%;
+ transform: translateY(-50%);
+
+ z-index: 1;
+ -webkit-user-select: none;
+ user-select: none;
+ flex-direction: column;
+ justify-content: flex-start;
+ font-family: var(--heading-font);
+
+ pointer-events: none;
+ color: white;
+}
+
+/* .SnippetContainer--isRunning .SnippetOverlay {
+ transform: translateX(16px) translateY(100%) translateY(-2rem)
+ translateY(-32px);
+} */
+
+.SnippetOverlayLabel {
+ font-size: 2rem;
+
+ transition: filter 0.12s linear;
+ transition-property: filter, color;
+}
+
+.CodeContainer {
+ filter: blur(0px);
+ transition: filter 0.12s linear;
+}
+
+.SnippetContainer--ran .CodeContainer {
+ filter: none;
+}
+
+.SnippetContainer--ran .SnippetOverlayLabel {
+ filter: none;
+ color: var(--color-brand);
+}
+
+.SnippetOverlayLabel-ops {
+}
+
+.SnippetContainer--ran .SnippetOverlay,
+.SnippetContainer--isRunning .SnippetOverlay {
+ display: flex;
+}
+
+.SnippetContainer--ran .SnippetBackground {
+ opacity: 0;
+}
+
+.SnippetContainer--ran .SnippetBackground,
+.SnippetContainer--isRunning .SnippetBackground {
+ display: block;
+}
+
+.NewSnippetContainer,
+.SnippetContainer {
+ transition: background-color 0.1s linear;
+}
+
+.SnippetIcon {
+ display: flex;
+ user-select: none;
+ -webkit-user-select: none;
+ margin-right: 6px;
+ margin-left: auto;
+}
+
+.SnippetIndexIcon,
+.SnippetRank {
+ text-transform: uppercase;
+ font-family: var(--heading-font);
+ font-size: 1.1rem;
+ text-align: left;
+ margin-left: 21px;
+ margin-right: 1ch;
+ opacity: 0.5;
+}
+
+.SnippetRank--first {
+ color: var(--color-brand);
+}
+
+.NewSnippetContainer:hover,
+.SnippetContainer:hover *,
+.SnippetContainer:hover {
+ --snippets_container-background: var(--snippets_container-background-focused);
+}
+
+.NewSnippetContainer,
+.SnippetTitleContainer {
+ display: grid;
+ flex: 1;
+ grid-template-columns: 42px auto;
+
+ align-items: center;
+ grid-column-gap: 0;
+ position: relative;
+}
+
+.SnippetContainer-ErrorClose,
+.SnippetTitle-deleteButton {
+ --color: rgb(153, 153, 153);
+ --active-color: rgb(189, 58, 58);
+}
+
+.SnippetTitle-deleteButton {
+ position: absolute;
+ right: 0;
+}
+
+.SnippetTitle-importButtonContainer {
+ position: absolute;
+ right: 0;
+ display: grid;
+ grid-auto-flow: column;
+ grid-column-gap: 16px;
+ align-items: center;
+ z-index: 10;
+}
+
+.SnippetTitle-transformField {
+ display: flex;
+ cursor: pointer;
+ align-items: center;
+ transition: transform 0.1s linear;
+}
+
+.SnippetTitle-transformField:hover {
+ transform: scale(1.05, 1.05);
+}
+
+.SnippetTitle-transformField:active {
+ transform: scale(1.2, 1.2);
+}
+
+.Toggler {
+ width: 16px;
+ height: 16px;
+ border-radius: 0;
+ border: 2px solid rgba(255, 255, 255, 0.3);
+ margin-right: 6px;
+}
+
+.SnippetTitle-transformField--checked .Toggler {
+ background-color: var(--color-brand);
+ border-color: rgb(50, 50, 50);
+}
+.SnippetTitle-transformField-label {
+ text-transform: uppercase;
+ margin-left: 4px;
+ user-select: none;
+ -webkit-user-select: none;
+ color: white;
+ font-family: var(--heading-font);
+}
+
+.SnippetTitle-transformField--checked .SnippetTitle-transformField-label {
+ color: var(--color-brand);
+}
+
+.SnippetContainer-ErrorClose,
+.SnippetTitle-transformField,
+.SnippetTitle-deleteButton,
+.SnippetTitle-importButton {
+ font-weight: 500;
+ font-size: 0.8rem;
+ font-family: var(--heading-font);
+
+ color: var(--color);
+ padding-right: 16px;
+ cursor: pointer;
+ pointer-events: all;
+ transition: transform 0.1s linear;
+ transition-property: transform, color;
+}
+
+.SnippetContainer-ErrorClose {
+ --translate-offset: -50%;
+ color: white;
+ position: absolute;
+ top: 50%;
+ transform: translateY(var(--translate-offset));
+ right: 0;
+}
+
+.SnippetTitle-importButton {
+ --color: white;
+ --active-color: var(--color-brand);
+}
+
+.SnippetContainer-ErrorClose:hover,
+.SnippetTitle-importButton:hover,
+.SnippetTitle-deleteButton:hover {
+ transform: translateY(var(--translate-offset, 0%)) scale(1.1, 1.1);
+ color: var(--active-color);
+}
+
+.SnippetContainer-ErrorClose:active,
+.SnippetTitle-importButton:active,
+.SnippetTitle-deleteButton:active {
+ transform: translateY(var(--translate-offset, 0%)) scale(1.2, 1.2);
+ color: var(--active-color);
+}
+
+.SnippetContainer-ErrorClose:hover,
+.SnippetContainer-ErrorClose:active {
+ color: white;
+}
+.SnippetTitleContainer {
+ position: relative;
+ z-index: 10;
+}
+
+.ShareHeader {
+ font-family: var(--heading-font);
+ font-size: 1.5rem;
+ color: white;
+ user-select: none;
+ -webkit-user-select: none;
+ -moz-user-select: none;
+}
+
+.ShareSheet {
+ display: grid;
+
+ grid-template-columns: max-content;
+ margin-bottom: 24px;
+ user-select: none;
+ -webkit-user-select: none;
+ -moz-user-select: none;
+}
+
+.ShareHeader-copyButton {
+ margin-top: auto;
+ margin-bottom: auto;
+
+ font-family: var(--heading-font);
+ color: white;
+ transform: scale(1, 1);
+ transition: transform 0.1s linear;
+ transition-property: color, transform;
+
+ cursor: pointer;
+ user-select: none;
+ -webkit-user-select: none;
+ -moz-user-select: none;
+ padding: 0 16px;
+ display: block;
+}
+
+.ShareHeader-copyButton:hover {
+ transform: scale(1.2, 1.2);
+ color: var(--color-brand);
+}
+
+.ShareHeader-copyButton:active {
+ transform: scale(1.5, 1.5);
+ color: var(--color-brand);
+}
+
+.ShareHeader-urlBox {
+ padding: 16px 0;
+ margin: 0;
+ width: 100%;
+ overflow-x: hidden;
+ white-space: nowrap;
+}
+.ShareHeader-url {
+ font-size: 1rem;
+ user-select: auto;
+ --webkit-user-select: auto;
+ margin: 0;
+ min-width: 480px;
+ max-width: var(--page-width);
+ border-radius: 0;
+ border: 1px solid rgb(32, 32, 32);
+ flex: 1;
+ color: rgb(220, 220, 220);
+ appearance: none;
+ background-color: rgba(255, 255, 255, 0.1);
+ box-shadow: none;
+ font-family: var(--heading-font);
+ font-variant-ligatures: none;
+ padding: 8px 16px;
+ width: 100%;
+ outline: 0;
+ transition: all 0.2s ease;
+}
+
+.ShareHeader-urlBox {
+ display: flex;
+}
+
+.ShareHeader-url:hover {
+ background: rgba(255, 255, 255, 0.15);
+}
+
+@media (max-width: 1152px) {
+ :root {
+ --page-width: 800px;
+ --padding-horizontal: 24px;
+ }
+}
+
+@media (max-width: 800px) {
+ :root {
+ --page-width: 100%;
+ --padding-horizontal: 24px;
+ }
+
+ .Hero-demo {
+ display: none;
+ }
+}
+
+@media (max-width: 600px) {
+ :root {
+ --card-width: 100%;
+ }
+}
+
+.NewSnippetContainer,
+.SnippetContainer {
+ background-color: var(--snippets_container-background);
+}
+
+.SnippetContainer--isRunning {
+ background-color: var(--snippets_container-background-unfocused);
+}
+
+.SnippetContainer--isRunning .CodeContainer {
+ opacity: 0.5;
+}
+
+.NewSnippetContainer {
+ cursor: pointer;
+ transition: all 0.2s ease;
+ opacity: 0.7;
+}
+
+.NewSnippetContainer:hover {
+ opacity: 1;
+}
+
+.SnippetHeading-subheader {
+ display: grid;
+ grid-template-columns: auto auto auto auto auto;
+ grid-column-gap: 6px;
+ text-transform: uppercase;
+ letter-spacing: 1.35px;
+ padding-left: 54px;
+ margin-top: -12px;
+ color: inherit;
+ padding-bottom: 16px;
+}
+
+.SnipptHeading-Dot {
+}
+
+.xIcon {
+ text-transform: lowercase;
+}
+
+.SnippetHeading--first {
+ color: var(--color-brand);
+}
+
+.SnippetHeading--notFirst {
+ color: rgb(153, 153, 153);
+}
+
+.ResultListSection {
+}
+
+.ResultListSection {
+ margin-bottom: 1rem;
+}
+.ResultListSection--heading {
+ font-size: 1.5rem;
+ color: white;
+ font-family: var(--heading-font);
+}
+
+a.ResultListSection--subHeading-section {
+ cursor: pointer;
+}
+
+a.ResultListSection--subHeading-section:hover {
+ text-decoration: underline;
+}
+
+.ResultListSection--disabled {
+ opacity: 0.5;
+}
+.ResultListSection--subHeading {
+ color: rgb(153, 153, 153);
+ display: grid;
+ width: 100%;
+ grid-auto-flow: column;
+ grid-auto-columns: min-content;
+ white-space: nowrap;
+
+ grid-column-gap: 8px;
+ margin-bottom: 16px;
+}
+.ResultListSection--subHeading--section {
+ white-space: nowrap;
+ display: block;
+}
+.ResultListSection--subHeading--separator {
+}
+.ResultListSection--results {
+ display: grid;
+ grid-row-gap: 20px;
+}
+
+.ResultLongListItem--notFirst {
+ color: white;
+}
+
+.ResultLongListItem--first {
+ color: var(--color-brand);
+}
+
+.ResultLongListItem--first {
+}
+.ResultLongListItem--notFirst {
+}
+.ResultLongListItem-line {
+ display: flex;
+}
+
+.ResultLongListItem {
+ margin-bottom: 24px;
+}
+.ResultLongListItem-name {
+ color: rgb(153, 153, 153);
+}
+
+.ResultLongListItem--first .ResultLongListItem-name,
+.ResultLongListItem--first .ResultLongListItem-progressBar {
+ color: var(--color-brand);
+}
+
+.ResultLongListItem-multiplier {
+}
+.ResultLongListItem-progressBarContainer {
+ margin-top: auto;
+ margin-bottom: auto;
+}
+.ResultLongListItem-progressBar {
+}
+.ResultLongListItem-statGroup {
+ display: grid;
+ width: 400px;
+ max-width: var(--page-width);
+ grid-template-columns: 300px 100px;
+ grid-column-gap: 0;
+ align-items: center;
+
+ font-size: 2rem;
+ white-space: nowrap;
+ padding-right: 28px;
+}
+
+.ResultLongListItem-progressBar,
+.ResultLongListItem-progressBarContainer {
+ height: 3px;
+ content: "";
+}
+
+.ResultLongListItem-progressBar {
+ background-color: currentColor;
+}
+
+.ResultLongListItem-progressBarContainer {
+ width: 100%;
+}
+
+@media (max-width: 600px) {
+ .ResultLongListItem-statGroup {
+ grid-template-columns: 200px 100px;
+ }
+
+ .ResultLonglistItem {
+ margin-top: 12px;
+ }
+
+ .ResultLongListItem-line {
+ flex-direction: column;
+ }
+
+ .ResultLongListItem-statGroup {
+ margin-bottom: 12px;
+ }
+
+ .ShareHeader-urlBox {
+ display: none;
+ }
+}
+
+.GithubLink {
+ font-size: 0.8rem;
+ padding: 4px;
+ display: block;
+ color: rgb(153, 153, 153);
+}
+
+.ModuleListContainer {
+ max-height: 400px;
+ overflow-y: scroll;
+ max-height: 400px;
+ min-height: 200px;
+}
+
+.ModulePickerModal {
+ position: absolute;
+
+ background: linear-gradient(
+ 136.61deg,
+ rgb(39, 40, 43) 13.72%,
+ rgb(45, 46, 49) 74.3%
+ );
+ backdrop-filter: blur(12px);
+ color: white;
+ z-index: 9999999;
+ max-height: 400px;
+ min-height: 200px;
+ overflow: hidden;
+ pointer-events: all;
+
+ border-radius: 8px;
+ box-shadow: rgba(0, 0, 0, 0.2) 0px 4px 24px;
+}
+
+.SnippetContainer--unposition .SnippetTitle-deleteButton {
+ display: none;
+}
+.SnippetContainer--unposition .SnippetTitleContainer {
+ position: static;
+}
+
+.ModuleListContainer-empty {
+ display: flex;
+ flex-direction: column;
+ width: 100%;
+ min-height: 100%;
+ height: 100%;
+ flex: 1;
+ padding-left: 12px;
+ margin-top: 12px;
+}
+
+.ModuleListContainer-emptyText {
+}
+
+.ModuleListContainer-emptyText-muted {
+ color: var(--result__muted-color);
+ opacity: 0.5;
+ margin-top: 4px;
+}
+
+.ModulePicker-search {
+ width: 100%;
+ display: flex;
+ flex: 0 0 48px;
+ background-color: rgb(32, 32, 32);
+ color: white;
+ font-family: var(--heading-font);
+ font-size: 1rem;
+ outline: none;
+ box-shadow: none;
+ -webkit-appearance: none;
+ appearance: none;
+ border: 0;
+ padding-left: 12px;
+ padding-right: 12px;
+}
+
+.ModulePicker {
+ display: flex;
+ flex-direction: column;
+ width: 400px;
+}
+
+.ModuleListItem {
+ display: flex;
+ justify-content: space-between;
+ padding: 8px 12px;
+ cursor: pointer;
+ background-color: transparent;
+ transform: background-color 0.2s ease;
+}
+
+.ModuleListItem:hover {
+ background-color: rgba(255, 255, 255, 0.05);
+}
+
+.ModuleListItem-info {
+ display: grid;
+ grid-template-rows: min-content min-content;
+ font-variant-ligatures: none;
+ grid-row-gap: 2px;
+}
+
+.ModuleListItem-name {
+ color: rgba(255, 255, 255, 0.8);
+ font-family: var(--heading-font);
+ font-size: 0.9rem;
+}
+
+.ModuleListItem-description {
+ color: rgba(255, 255, 255, 0.5);
+ font-size: 0.9rem;
+}
+
+.ModuleListItem-description,
+.ModuleListItem-name {
+ white-space: nowrap;
+ text-overflow: ellipsis;
+ overflow: hidden;
+ width: 280px;
+}
+
+.ModuleListItem:hover .ModuleListItem-importButton {
+ opacity: 1;
+}
+.ModuleListItem-importButton {
+ text-transform: uppercase;
+ margin-top: auto;
+ margin-bottom: auto;
+ font-family: var(--heading-font);
+ font-weight: bold;
+ color: var(--color-brand);
+ padding: 4px 6px;
+ margin-left: 8px;
+ letter-spacing: 0.05rem;
+ font-size: 0.9rem;
+ transition: all 0.2s ease;
+ opacity: 0;
+}
+
+.BenchmarkHeader--mobile {
+ display: none;
+}
+@media (max-width: 600px) {
+ :root {
+ --padding-horizontal: 16px;
+ }
+
+ .BenchmarkHeader--mobile {
+ display: flex;
+ }
+ .BenchmarkHeader--desktop {
+ display: none;
+ }
+
+ .ResultListSection--subHeading-separator {
+ display: none;
+ }
+
+ .ResultListSection--subHeading {
+ grid-auto-flow: row;
+ white-space: normal;
+ grid-auto-columns: auto;
+ --page-width: auto;
+ }
+
+ .ResultLongListItem-statGroup {
+ display: flex;
+ max-width: 100%;
+ width: auto;
+ justify-content: space-between;
+ padding-right: var(--padding-horizontal);
+ }
+
+ .CodeContainer {
+ zoom: 0.8;
+ }
+
+ .BenchmarkHeader {
+ flex-direction: column;
+ }
+}
+
+.ShareSheet-image-loading {
+ animation: fade-in-out 1.5s ease;
+ transform: scale(2);
+ transform-origin: center center;
+ animation-iteration-count: infinite;
+ animation-direction: alternate-reverse;
+}
+
+@keyframes fade-in-out {
+ 0% {
+ opacity: 1;
+ }
+ 50% {
+ opacity: 0.66;
+ }
+ 100% {
+ opacity: 1;
+ }
+}
diff --git a/src/test/fixtures/test-import.css b/src/test/fixtures/test-import.css
new file mode 100644
index 000000000..4fecc3135
--- /dev/null
+++ b/src/test/fixtures/test-import.css
@@ -0,0 +1,3 @@
+.bacon {
+ display: block;
+}