diff options
author | 2021-06-17 11:14:20 -0700 | |
---|---|---|
committer | 2021-06-17 11:14:20 -0700 | |
commit | 9ca283bb43ebee74bf36af50807474b962ac44a1 (patch) | |
tree | a7040b8b07a38f48b1993d4515d876ed075c1833 /src | |
parent | e1677bb77414710c1114c3b52b3fa954d9276c45 (diff) | |
download | bun-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.zig | 76 | ||||
-rw-r--r-- | src/cli.zig | 10 | ||||
-rw-r--r-- | src/css_scanner.zig | 986 | ||||
-rw-r--r-- | src/darwin_c.zig | 4 | ||||
-rw-r--r-- | src/env.zig | 21 | ||||
-rw-r--r-- | src/feature_flags.zig | 36 | ||||
-rw-r--r-- | src/fs.zig | 28 | ||||
-rw-r--r-- | src/global.zig | 58 | ||||
-rw-r--r-- | src/http.zig | 8 | ||||
-rw-r--r-- | src/js_printer.zig | 6 | ||||
-rw-r--r-- | src/linker.zig | 171 | ||||
-rw-r--r-- | src/options.zig | 15 | ||||
-rw-r--r-- | src/resolver/resolve_path.zig | 4 | ||||
-rw-r--r-- | src/test/fixtures/me@2x.jpeg | bin | 0 -> 6829 bytes | |||
-rw-r--r-- | src/test/fixtures/simple.css | 1205 | ||||
-rw-r--r-- | src/test/fixtures/test-import.css | 3 |
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 Binary files differnew file mode 100644 index 000000000..02f8a3b19 --- /dev/null +++ b/src/test/fixtures/me@2x.jpeg 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; +} |