Skip to content

Ensure namespace reset with escaped * works #15603

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 5 commits into from
Jan 13, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Fixed

- Add missing `main` and `browser` fields for `@tailwindcss/browser` ([#15594](https://github.com/tailwindlabs/tailwindcss/pull/15594))
- Ensure namespace reset with escaped `*` (e.g.: `--color-\*: initial;`) ([#15603](https://github.com/tailwindlabs/tailwindcss/pull/15603))
- _Upgrade (experimental)_: Pretty print `--spacing(…)` to prevent ambiguity ([#15596](https://github.com/tailwindlabs/tailwindcss/pull/15596))

## [4.0.0-beta.9] - 2025-01-09
Expand Down
47 changes: 47 additions & 0 deletions packages/tailwindcss/src/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1208,6 +1208,53 @@ describe('Parsing themes values from CSS', () => {
`)
})

test('`@theme` values can be unset (using the escaped syntax)', async () => {
expect(
await compileCss(
css`
@theme {
--color-red: #f00;
--color-blue: #00f;
--text-sm: 13px;
--text-md: 16px;

--animate-spin: spin 1s infinite linear;

@keyframes spin {
to {
transform: rotate(360deg);
}
}
}
@theme {
--color-\*: initial;
--text-md: initial;
--animate-\*: initial;
--keyframes-\*: initial;
}
@theme {
--color-green: #0f0;
}
@tailwind utilities;
`,
['accent-red', 'accent-blue', 'accent-green', 'text-sm', 'text-md'],
),
).toMatchInlineSnapshot(`
":root {
--text-sm: 13px;
--color-green: #0f0;
}

.text-sm {
font-size: var(--text-sm);
}

.accent-green {
accent-color: var(--color-green);
}"
`)
})

test('all `@theme` values can be unset at once', async () => {
expect(
await compileCss(
Expand Down
4 changes: 4 additions & 0 deletions packages/tailwindcss/src/theme.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,10 @@ export class Theme {
) {}

add(key: string, value: string, options = ThemeOptions.NONE): void {
if (key.endsWith('\\*')) {
key = key.slice(0, -2) + '*'
}
Copy link
Contributor

@thecrypticace thecrypticace Jan 11, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is there a significant penalty to just always running the regex replace? Since it's anchored I'd expect it to not be too bad (maybe hoist the regex itself tho?)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep it's faster with the regex if you hoist it up!

Benchmark 1: regex-always
  Time (mean ± σ):     322.9 ms ±   8.8 ms    [User: 1180.2 ms, System: 115.0 ms]
  Range (min … max):   312.5 ms … 341.1 ms    10 runs

Benchmark 2: regex-conditional
  Time (mean ± σ):     329.7 ms ±  10.7 ms    [User: 1235.7 ms, System: 139.1 ms]
  Range (min … max):   317.5 ms … 349.8 ms    10 runs

Summary
  regex-always ran
    1.02 ± 0.04 times faster than regex-conditional

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What about if you don't use a regex at all and just use key.slice(0, -2) + '*' to build a new string, since you know the number of characters you are removing from the end?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good question. I did a microbenchmark of the slicing:

import { run, bench } from "mitata";

function slice1(key) {
  let escapedStarIdx = key.lastIndexOf("-\\*");
  if (escapedStarIdx !== -1) {
    key = key.slice(0, escapedStarIdx) + "-*";
  }

  return key;
}

function slice2(key) {
  if (key.endsWith("-\\*")) {
    key = key.slice(0, -3) + "-*";
  }

  return key;
}

let regex = /-\\\*$/;
function slice3(key) {
  return key.replace(regex, "-*");
}

let keys = ["--color-\\*", "--font-\\*", "--border-\\*", "--background-\\*", "--padding-\\*"];

let n = 0;

bench("slice1(…)", () => slice1(keys[(n = (n + 1) % 5)]));
bench("slice2(…)", () => slice2(keys[(n = (n + 1) % 5)]));
bench("slice3(…)", () => slice3(keys[(n = (n + 1) % 5)]));

await run();
clk: ~3.77 GHz
cpu: Apple M3 Max
runtime: node 23.6.0 (arm64-darwin)

benchmark                   avg (min … max) p75 / p99    (min … top 1%)
------------------------------------------- -------------------------------
slice1(…)                     15.06 ns/iter  14.94 ns   █                  
                      (13.75 ns … 42.47 ns)  24.37 ns   █                  
                    ( 27.20 kb …  62.53 kb)  27.20 kb ▁▁█▃▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁
slice2(…)                      8.07 ns/iter   7.95 ns █                    
                       (7.79 ns … 50.43 ns)  18.65 ns █                    
                    ( 27.21 kb …  62.18 kb)  27.18 kb █▃▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁
slice3(…)                     34.29 ns/iter  34.11 ns   █                  
                      (32.49 ns … 67.69 ns)  47.01 ns   █                  
                    ( 57.61 kb …  61.75 kb)  56.79 kb ▁▁█▄▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁
clk: ~3.95 GHz
cpu: Apple M3 Max
runtime: bun 1.1.43 (arm64-darwin)

benchmark                   avg (min … max) p75 / p99    (min … top 1%)
------------------------------------------- -------------------------------
slice1(…)                     17.00 ns/iter  16.95 ns      █               
                      (15.36 ns … 67.54 ns)  20.26 ns      █▅              
                    (  0.00  b …  84.00 kb) 142.42  b ▂▂▂▁▁██▅▃▂▂▂▂▂▂▁▁▁▁▁▁
slice2(…)                     16.66 ns/iter  16.74 ns      █               
                      (14.72 ns … 59.11 ns)  20.84 ns     ▂██              
                    (  0.00  b …  16.00 kb)  16.16  b ▁▄▆▄████▅▃▂▂▂▂▂▁▁▁▁▁▁
slice3(…)                     41.57 ns/iter  42.57 ns       ▃▄█▂           
                      (37.54 ns … 96.04 ns)  48.33 ns  ▅▂   ████▄          
                    (  0.00  b …  80.00 kb)  13.81 kb ▂██▅▄▃██████▅▄▃▂▂▂▁▁▁

Some additional testing (not shown here) shows that with a constant value Bun hyper-optimizes the regex into picoseconds but when you start passing various things through that optimization disappears. Node doesn't appear to have a similar thing here afaict.

I think this implementation is the best to use based on these numbers:

if (key.endsWith("-\\*")) {
  key = key.slice(0, -3) + "-*";
}

Copy link
Contributor

@thecrypticace thecrypticace Jan 12, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

err, this actually like Adam mentioned above. Don't need to slice out the - and then copy it back in lol.

if (key.endsWith("\\*")) {
  key = key.slice(0, -2) + "*";
}


if (key.endsWith('-*')) {
if (value !== 'initial') {
throw new Error(`Invalid theme value \`${value}\` for namespace \`${key}\``)
Expand Down