Skip to content

Commit e6cb0de

Browse files
authored
CSS modules (#17958)
1 parent 924e50b commit e6cb0de

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

51 files changed

+3922
-603
lines changed

docs/bundler/css.md

Lines changed: 1030 additions & 0 deletions
Large diffs are not rendered by default.

docs/bundler/css_modules.md

Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
# CSS Modules
2+
3+
Bun's bundler also supports bundling [CSS modules](https://css-tricks.com/css-modules-part-1-need/) in addition to [regular CSS](/docs/bundler/css) with support for the following features:
4+
5+
- Automatically detecting CSS module files (`.module.css`) with zero configuration
6+
- Composition (`composes` property)
7+
- Importing CSS modules into JSX/TSX
8+
- Warnings/errors for invalid usages of CSS modules
9+
10+
A CSS module is a CSS file (with the `.module.css` extension) where are all class names and animations are scoped to the file. This helps you avoid class name collisions as CSS declarations are globally scoped by default.
11+
12+
Under the hood, Bun's bundler transforms locally scoped class names into unique identifiers.
13+
14+
## Getting started
15+
16+
Create a CSS file with the `.module.css` extension:
17+
18+
```css
19+
/* styles.module.css */
20+
.button {
21+
color: red;
22+
}
23+
24+
/* other-styles.module.css */
25+
.button {
26+
color: blue;
27+
}
28+
```
29+
30+
You can then import this file, for example into a TSX file:
31+
32+
```tsx
33+
import styles from "./styles.module.css";
34+
import otherStyles from "./other-styles.module.css";
35+
36+
export default function App() {
37+
return (
38+
<>
39+
<button className={styles.button}>Red button!</button>
40+
<button className={otherStyles.button}>Blue button!</button>
41+
</>
42+
);
43+
}
44+
```
45+
46+
The `styles` object from importing the CSS module file will be an object with all class names as keys and
47+
their unique identifiers as values:
48+
49+
```tsx
50+
import styles from "./styles.module.css";
51+
import otherStyles from "./other-styles.module.css";
52+
53+
console.log(styles);
54+
console.log(otherStyles);
55+
```
56+
57+
This will output:
58+
59+
```ts
60+
{
61+
button: "button_123";
62+
}
63+
64+
{
65+
button: "button_456";
66+
}
67+
```
68+
69+
As you can see, the class names are unique to each file, avoiding any collisions!
70+
71+
### Composition
72+
73+
CSS modules allow you to _compose_ class selectors together. This lets you reuse style rules across multiple classes.
74+
75+
For example:
76+
77+
```css
78+
/* styles.module.css */
79+
.button {
80+
composes: background;
81+
color: red;
82+
}
83+
84+
.background {
85+
background-color: blue;
86+
}
87+
```
88+
89+
Would be the same as writing:
90+
91+
```css
92+
.button {
93+
background-color: blue;
94+
color: red;
95+
}
96+
97+
.background {
98+
background-color: blue;
99+
}
100+
```
101+
102+
{% callout %}
103+
There are a couple rules to keep in mind when using `composes`:
104+
105+
- A `composes` property must come before any regular CSS properties or declarations
106+
- You can only use `composes` on a **simple selector with a single class name**:
107+
108+
```css
109+
#button {
110+
/* Invalid! `#button` is not a class selector */
111+
composes: background;
112+
}
113+
114+
.button,
115+
.button-secondary {
116+
/* Invalid! `.button, .button-secondary` is not a simple selector */
117+
composes: background;
118+
}
119+
```
120+
121+
{% /callout %}
122+
123+
### Composing from a separate CSS module file
124+
125+
You can also compose from a separate CSS module file:
126+
127+
```css
128+
/* background.module.css */
129+
.background {
130+
background-color: blue;
131+
}
132+
133+
/* styles.module.css */
134+
.button {
135+
composes: background from "./background.module.css";
136+
color: red;
137+
}
138+
```
139+
140+
{% callout %}
141+
When composing classes from separate files, be sure that they do not contain the same properties.
142+
143+
The CSS module spec says that composing classes from separate files with conflicting properties is
144+
undefined behavior, meaning that the output may differ and be unreliable.
145+
{% /callout %}

docs/nav.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -218,6 +218,9 @@ export default {
218218
page("bundler/html", "Bundle frontend & static sites", {
219219
description: `Zero-config HTML bundler for single-page apps and multi-page apps. Automatic bundling, TailwindCSS plugins, TypeScript, JSX, React support, and incredibly fast builds`,
220220
}),
221+
page("bundler/css", "Bundle, transpile, and minify CSS", {
222+
description: `Production ready CSS bundler with support for modern CSS features, CSS modules, and more.`,
223+
}),
221224
page("bundler/fullstack", "Fullstack Dev Server", {
222225
description: "Serve your frontend and backend from the same app with Bun's dev server.",
223226
}),

src/baby_list.zig

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -316,6 +316,12 @@ pub fn BabyList(comptime Type: type) type {
316316
this.update(list__);
317317
}
318318

319+
pub fn insertSlice(this: *@This(), allocator: std.mem.Allocator, index: usize, vals: []const Type) !void {
320+
var list__ = this.listManaged(allocator);
321+
try list__.insertSlice(index, vals);
322+
this.update(list__);
323+
}
324+
319325
pub fn append(this: *@This(), allocator: std.mem.Allocator, value: []const Type) !void {
320326
var list__ = this.listManaged(allocator);
321327
try list__.appendSlice(value);

src/bake/DevServer.zig

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1321,6 +1321,15 @@ fn appendRouteEntryPointsIfNotStale(dev: *DevServer, entry_points: *EntryPointLi
13211321
try entry_points.append(alloc, html.html_bundle.html_bundle.path, .{ .client = true });
13221322
},
13231323
}
1324+
1325+
if (dev.has_tailwind_plugin_hack) |*map| {
1326+
for (map.keys()) |abs_path| {
1327+
const file = dev.client_graph.bundled_files.get(abs_path) orelse
1328+
continue;
1329+
if (file.flags.kind == .css)
1330+
entry_points.appendCss(alloc, abs_path) catch bun.outOfMemory();
1331+
}
1332+
}
13241333
}
13251334

13261335
fn onFrameworkRequestWithBundle(
@@ -3664,7 +3673,7 @@ pub fn IncrementalGraph(side: bake.Side) type {
36643673
bundler_index: bun.JSAst.Index,
36653674
) !void {
36663675
bun.assert(bundler_index.isValid());
3667-
bun.assert(ctx.loaders[bundler_index.get()] == .css);
3676+
bun.assert(ctx.loaders[bundler_index.get()].isCSS());
36683677

36693678
var sfb = std.heap.stackFallback(@sizeOf(bun.JSAst.Index) * 64, temp_alloc);
36703679
const queue_alloc = sfb.get();

src/bake/production.zig

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -286,7 +286,7 @@ pub fn buildWithVm(ctx: bun.CLI.Command.Context, cwd: []const u8, vm: *VirtualMa
286286
file.dest_path,
287287
file.entry_point_index,
288288
});
289-
if (file.loader == .css) {
289+
if (file.loader.isCSS()) {
290290
if (css_chunks_count == 0) css_chunks_first = i;
291291
css_chunks_count += 1;
292292
}
@@ -417,7 +417,7 @@ pub fn buildWithVm(ctx: bun.CLI.Command.Context, cwd: []const u8, vm: *VirtualMa
417417
const css_chunk_js_strings = try allocator.alloc(JSValue, css_chunks_count);
418418
for (bundled_outputs[css_chunks_first..][0..css_chunks_count], css_chunk_js_strings) |output_file, *str| {
419419
bun.assert(output_file.dest_path[0] != '.');
420-
bun.assert(output_file.loader == .css);
420+
bun.assert(output_file.loader.isCSS());
421421
str.* = (try bun.String.createFormat("{s}{s}", .{ public_path, output_file.dest_path })).toJS(global);
422422
}
423423

src/base64/base64.zig

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,20 @@ pub fn encode(destination: []u8, source: []const u8) usize {
4848
return bun.simdutf.base64.encode(source, destination, false);
4949
}
5050

51+
pub fn simdutfEncodeLenUrlSafe(source_len: usize) usize {
52+
return bun.simdutf.base64.encode_len(source_len, true);
53+
}
54+
55+
/// Encode with the following differences from regular `encode` function:
56+
///
57+
/// * No padding is added (the extra `=` characters at the end)
58+
/// * `-` and `_` are used instead of `+` and `/`
59+
///
60+
/// See the documentation for simdutf's `binary_to_base64` function for more details (simdutf_impl.h).
61+
pub fn simdutfEncodeUrlSafe(destination: []u8, source: []const u8) usize {
62+
return bun.simdutf.base64.encode(source, destination, true);
63+
}
64+
5165
pub fn decodeLenUpperBound(len: usize) usize {
5266
return zig_base64.standard.Decoder.calcSizeUpperBound(len) catch {
5367
//fallback

src/bun.js/api/BunObject.zig

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3625,7 +3625,15 @@ const TOMLObject = struct {
36253625
return globalThis.throwValue(log.toJS(globalThis, default_allocator, "Failed to print toml"));
36263626
};
36273627
var writer = js_printer.BufferPrinter.init(buffer_writer);
3628-
_ = js_printer.printJSON(*js_printer.BufferPrinter, &writer, parse_result, &source, .{}) catch {
3628+
_ = js_printer.printJSON(
3629+
*js_printer.BufferPrinter,
3630+
&writer,
3631+
parse_result,
3632+
&source,
3633+
.{
3634+
.mangled_props = null,
3635+
},
3636+
) catch {
36293637
return globalThis.throwValue(log.toJS(globalThis, default_allocator, "Failed to print toml"));
36303638
};
36313639

src/bun.js/api/server.zig

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7518,7 +7518,7 @@ pub fn NewServer(comptime NamespaceType: type, comptime ssl_enabled_: bool, comp
75187518
&writer,
75197519
bun.Global.BunInfo.generate(*Transpiler, &JSC.VirtualMachine.get().transpiler, allocator) catch unreachable,
75207520
&source,
7521-
.{},
7521+
.{ .mangled_props = null },
75227522
) catch unreachable;
75237523

75247524
resp.writeStatus("200 OK");

src/bun.js/bindings/bun-simdutf.cpp

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -339,6 +339,11 @@ size_t simdutf__base64_encode(const char* input, size_t length, char* output, in
339339
return simdutf::binary_to_base64(input, length, output, is_urlsafe ? simdutf::base64_url : simdutf::base64_default);
340340
}
341341

342+
size_t simdutf__base64_length_from_binary(size_t length, int is_urlsafe)
343+
{
344+
return simdutf::base64_length_from_binary(length, is_urlsafe ? simdutf::base64_url : simdutf::base64_default);
345+
}
346+
342347
SIMDUTFResult simdutf__base64_decode_from_binary(const char* input, size_t length, char* output, size_t outlen_, int is_urlsafe)
343348
{
344349
size_t outlen = outlen_;

src/bun.js/bindings/bun-simdutf.zig

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -387,11 +387,16 @@ pub const base64 = struct {
387387
extern fn simdutf__base64_encode(input: [*]const u8, length: usize, output: [*]u8, is_urlsafe: c_int) usize;
388388
extern fn simdutf__base64_decode_from_binary(input: [*]const u8, length: usize, output: [*]u8, outlen: usize, is_urlsafe: c_int) SIMDUTFResult;
389389
extern fn simdutf__base64_decode_from_binary16(input: [*]const u16, length: usize, output: [*]u8, outlen: usize, is_urlsafe: c_int) SIMDUTFResult;
390+
extern fn simdutf__base64_length_from_binary(length: usize, options: c_int) usize;
390391

391392
pub fn encode(input: []const u8, output: []u8, is_urlsafe: bool) usize {
392393
return simdutf__base64_encode(input.ptr, input.len, output.ptr, @intFromBool(is_urlsafe));
393394
}
394395

396+
pub fn encode_len(input: usize, is_urlsafe: bool) usize {
397+
return simdutf__base64_length_from_binary(input, @intFromBool(is_urlsafe));
398+
}
399+
395400
pub fn decode(input: []const u8, output: []u8, is_urlsafe: bool) SIMDUTFResult {
396401
return simdutf__base64_decode_from_binary(input.ptr, input.len, output.ptr, output.len, @intFromBool(is_urlsafe));
397402
}

0 commit comments

Comments
 (0)