pub const css = @import("./css_parser.zig"); pub const css_values = @import("./values/values.zig"); pub const Error = css.Error; const PrintErr = css.PrintErr; pub const CssModule = struct { config: *const Config, sources: *const ArrayList([]const u8), hashes: ArrayList([]const u8), exports_by_source_index: ArrayList(CssModuleExports), references: *CssModuleReferences, pub fn new( allocator: Allocator, config: *const Config, sources: *const ArrayList([]const u8), project_root: ?[]const u8, references: *CssModuleReferences, ) CssModule { // TODO: this is BAAAAAAAAAAD we are going to remove it const hashes = hashes: { var hashes = bun.handleOom(ArrayList([]const u8).initCapacity(allocator, sources.items.len)); for (sources.items) |path| { var alloced = false; const source = source: { // Make paths relative to project root so hashes are stable if (project_root) |root| { if (bun.path.Platform.auto.isAbsolute(root)) { alloced = true; break :source bun.handleOom(allocator.dupe(u8, bun.path.relative(root, path))); } } break :source path; }; defer if (alloced) allocator.free(source); hashes.appendAssumeCapacity(hash( allocator, "{s}", .{source}, config.pattern.segments.at(0).* == .hash, )); } break :hashes hashes; }; const exports_by_source_index = exports_by_source_index: { var exports_by_source_index = bun.handleOom(ArrayList(CssModuleExports).initCapacity(allocator, sources.items.len)); exports_by_source_index.appendNTimesAssumeCapacity(CssModuleExports{}, sources.items.len); break :exports_by_source_index exports_by_source_index; }; return CssModule{ .config = config, .sources = sources, .references = references, .hashes = hashes, .exports_by_source_index = exports_by_source_index, }; } pub fn deinit(this: *CssModule) void { _ = this; // autofix // TODO: deinit } pub fn getReference(this: *CssModule, allocator: Allocator, name: []const u8, source_index: u32) void { const gop = bun.handleOom(this.exports_by_source_index.items[source_index].getOrPut(allocator, name)); if (gop.found_existing) { gop.value_ptr.is_referenced = true; } else { gop.value_ptr.* = CssModuleExport{ .name = this.config.pattern.writeToString(allocator, .{}, this.hashes.items[source_index], this.sources.items[source_index], name), .composes = .{}, .is_referenced = true, }; } } pub fn referenceDashed( this: *CssModule, dest: *css.Printer, name: []const u8, from: *const ?css.css_properties.css_modules.Specifier, source_index: u32, ) PrintErr!?[]const u8 { const allocator = dest.allocator; const reference, const key = if (from.*) |specifier| switch (specifier) { .global => return name[2..], .import_record_index => |import_record_index| init: { const import_record = try dest.importRecord(import_record_index); break :init .{ CssModuleReference{ .dependency = .{ .name = name[2..], .specifier = import_record.path.text, }, }, import_record.path.text, }; }, } else { // Local export. Mark as used. const gop = bun.handleOom(this.exports_by_source_index.items[source_index].getOrPut(allocator, name)); if (gop.found_existing) { gop.value_ptr.is_referenced = true; } else { var res = ArrayList(u8){}; bun.handleOom(res.appendSlice(allocator, "--")); gop.value_ptr.* = CssModuleExport{ .name = this.config.pattern.writeToString( allocator, res, this.hashes.items[source_index], this.sources.items[source_index], name[2..], ), .composes = .{}, .is_referenced = true, }; } return null; }; const the_hash = hash(allocator, "{s}_{s}_{s}", .{ this.hashes.items[source_index], name, key }, false); this.references.put( allocator, bun.handleOom(std.fmt.allocPrint(allocator, "--{s}", .{the_hash})), reference, ) catch |err| bun.handleOom(err); return the_hash; } pub fn handleComposes( _: *CssModule, _: *css.Printer, selectors: *const css.selector.parser.SelectorList, _: *const css.css_properties.css_modules.Composes, _: u32, ) css.Maybe(void, css.PrinterErrorKind) { // const allocator = dest.allocator; for (selectors.v.slice()) |*sel| { if (sel.len() == 1 and sel.components.items[0] == .class) { continue; } // The composes property can only be used within a simple class selector. return .{ .err = css.PrinterErrorKind.invalid_composes_selector }; } return .success; } pub fn addDashed(this: *CssModule, allocator: Allocator, local: []const u8, source_index: u32) void { const gop = bun.handleOom(this.exports_by_source_index.items[source_index].getOrPut(allocator, local)); if (!gop.found_existing) { gop.value_ptr.* = CssModuleExport{ // todo_stuff.depth .name = this.config.pattern.writeToStringWithPrefix( allocator, "--", this.hashes.items[source_index], this.sources.items[source_index], local[2..], ), .composes = .{}, .is_referenced = false, }; } } pub fn addLocal(this: *CssModule, allocator: Allocator, exported: []const u8, local: []const u8, source_index: u32) void { const gop = bun.handleOom(this.exports_by_source_index.items[source_index].getOrPut(allocator, exported)); if (!gop.found_existing) { gop.value_ptr.* = CssModuleExport{ // todo_stuff.depth .name = this.config.pattern.writeToString( allocator, .{}, this.hashes.items[source_index], this.sources.items[source_index], local, ), .composes = .{}, .is_referenced = false, }; } } }; /// Configuration for CSS modules. pub const Config = struct { /// The name pattern to use when renaming class names and other identifiers. /// Default is `[hash]_[local]`. pattern: Pattern = .{}, /// Whether to rename dashed identifiers, e.g. custom properties. dashed_idents: bool = false, /// Whether to scope animation names. /// Default is `true`. animation: bool = true, /// Whether to scope grid names. /// Default is `true`. grid: bool = true, /// Whether to scope custom identifiers /// Default is `true`. custom_idents: bool = true, }; /// A CSS modules class name pattern. pub const Pattern = struct { /// The list of segments in the pattern. segments: css.SmallList(Segment, 3) = css.SmallList(Segment, 3).initInlined(&[_]Segment{ .local, .{ .literal = "_" }, .hash }), /// Write the substituted pattern to a destination. pub fn write( this: *const Pattern, hash_: []const u8, path: []const u8, local: []const u8, closure: anytype, comptime writefn: *const fn (@TypeOf(closure), []const u8, replace_dots: bool) void, ) void { for (this.segments.slice()) |*segment| { switch (segment.*) { .literal => |s| { writefn(closure, s, false); }, .name => { const stem = std.fs.path.stem(path); if (std.mem.indexOf(u8, stem, ".")) |_| { writefn(closure, stem, true); } else { writefn(closure, stem, false); } }, .local => { writefn(closure, local, false); }, .hash => { writefn(closure, hash_, false); }, } } } pub fn writeToStringWithPrefix( this: *const Pattern, allocator: Allocator, comptime prefix: []const u8, hash_: []const u8, path: []const u8, local: []const u8, ) []const u8 { const Closure = struct { res: ArrayList(u8), allocator: Allocator }; var closure = Closure{ .res = .{}, .allocator = allocator }; this.write( hash_, path, local, &closure, struct { pub fn writefn(self: *Closure, slice: []const u8, replace_dots: bool) void { bun.handleOom(self.res.appendSlice(self.allocator, prefix)); if (replace_dots) { const start = self.res.items.len; bun.handleOom(self.res.appendSlice(self.allocator, slice)); const end = self.res.items.len; for (self.res.items[start..end]) |*c| { if (c.* == '.') { c.* = '-'; } } return; } bun.handleOom(self.res.appendSlice(self.allocator, slice)); } }.writefn, ); return closure.res.items; } pub fn writeToString( this: *const Pattern, allocator: Allocator, res_: ArrayList(u8), hash_: []const u8, path: []const u8, local: []const u8, ) []const u8 { var res = res_; const Closure = struct { res: *ArrayList(u8), allocator: Allocator }; var closure = Closure{ .res = &res, .allocator = allocator }; this.write( hash_, path, local, &closure, struct { pub fn writefn(self: *Closure, slice: []const u8, replace_dots: bool) void { if (replace_dots) { const start = self.res.items.len; bun.handleOom(self.res.appendSlice(self.allocator, slice)); const end = self.res.items.len; for (self.res.items[start..end]) |*c| { if (c.* == '.') { c.* = '-'; } } return; } bun.handleOom(self.res.appendSlice(self.allocator, slice)); return; } }.writefn, ); return res.items; } }; /// A segment in a CSS modules class name pattern. /// /// See [Pattern](Pattern). pub const Segment = union(enum) { /// A literal string segment. literal: []const u8, /// The base file name. name, /// The original class name. local, /// A hash of the file name. hash, }; /// A map of exported names to values. pub const CssModuleExports = std.StringArrayHashMapUnmanaged(CssModuleExport); /// A map of placeholders to references. pub const CssModuleReferences = std.StringArrayHashMapUnmanaged(CssModuleReference); /// An exported value from a CSS module. pub const CssModuleExport = struct { /// The local (compiled) name for this export. name: []const u8, /// Other names that are composed by this export. composes: ArrayList(CssModuleReference), /// Whether the export is referenced in this file. is_referenced: bool, }; /// A referenced name within a CSS module, e.g. via the `composes` property. /// /// See [CssModuleExport](CssModuleExport). pub const CssModuleReference = union(enum) { /// A local reference. local: struct { /// The local (compiled) name for the reference. name: []const u8, }, /// A global reference. global: struct { /// The referenced global name. name: []const u8, }, /// A reference to an export in a different file. dependency: struct { /// The name to reference within the dependency. name: []const u8, /// The dependency specifier for the referenced file. /// /// import record idx specifier: []const u8, }, pub fn eql(this: *const @This(), other: *const @This()) bool { if (@intFromEnum(this.*) != @intFromEnum(other.*)) return false; return switch (this.*) { .local => |v| bun.strings.eql(v.name, other.local.name), .global => |v| bun.strings.eql(v.name, other.global.name), // .dependency => |v| bun.strings.eql(v.name, other.dependency.name) and bun.strings.eql(v.specifier, other.dependency.specifier), .dependency => |v| bun.strings.eql(v.name, other.dependency.name) and bun.strings.eql(v.specifier, other.dependency.specifier), }; } }; // TODO: replace with bun's hash pub fn hash(allocator: Allocator, comptime fmt: []const u8, args: anytype, at_start: bool) []const u8 { const count = std.fmt.count(fmt, args); var stack_fallback = std.heap.stackFallback(128, allocator); const fmt_alloc = if (count <= 128) stack_fallback.get() else allocator; var hasher = bun.Wyhash11.init(0); var fmt_str = bun.handleOom(std.fmt.allocPrint(fmt_alloc, fmt, args)); hasher.update(fmt_str); const h: u32 = @truncate(hasher.final()); var h_bytes: [4]u8 = undefined; std.mem.writeInt(u32, &h_bytes, h, .little); const encode_len = bun.base64.simdutfEncodeLenUrlSafe(h_bytes[0..].len); var slice_to_write = if (encode_len <= 128 - @as(usize, @intFromBool(at_start))) bun.handleOom(allocator.alloc(u8, encode_len + @as(usize, @intFromBool(at_start)))) else fmt_str[0..]; const base64_encoded_hash_len = bun.base64.simdutfEncodeUrlSafe(slice_to_write, &h_bytes); const base64_encoded_hash = slice_to_write[0..base64_encoded_hash_len]; if (at_start and base64_encoded_hash.len > 0 and base64_encoded_hash[0] >= '0' and base64_encoded_hash[0] <= '9') { std.mem.copyBackwards(u8, slice_to_write[1..][0..base64_encoded_hash_len], base64_encoded_hash); slice_to_write[0] = '_'; return slice_to_write[0 .. base64_encoded_hash_len + 1]; } return base64_encoded_hash; } const bun = @import("bun"); const std = @import("std"); const ArrayList = std.ArrayListUnmanaged; const Allocator = std.mem.Allocator;