From 7debe3ceee0cc316cc79656fae36fdf039c84ca9 Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Mon, 25 Nov 2024 12:34:30 +0100 Subject: [PATCH 1/8] format parser --- crates/oxide/src/parser.rs | 19 +++---------------- 1 file changed, 3 insertions(+), 16 deletions(-) diff --git a/crates/oxide/src/parser.rs b/crates/oxide/src/parser.rs index e49b2d21c891..e1bc6510eda1 100644 --- a/crates/oxide/src/parser.rs +++ b/crates/oxide/src/parser.rs @@ -253,9 +253,7 @@ impl<'a> Extractor<'a> { // Reject candidates that are single camelCase words, e.g.: `useEffect` if candidate.iter().all(|c| c.is_ascii_alphanumeric()) - && candidate - .iter() - .any(|c| c.is_ascii_uppercase()) + && candidate.iter().any(|c| c.is_ascii_uppercase()) { return ValidationResult::Invalid; } @@ -1542,18 +1540,7 @@ mod test { #[test] fn simple_utility_names_with_numbers_work() { - let candidates = run( - r#"
"#, - false, - ); - assert_eq!( - candidates, - vec![ - "div", - "class", - "h2", - "hz", - ] - ); + let candidates = run(r#"
"#, false); + assert_eq!(candidates, vec!["div", "class", "h2", "hz",]); } } From 580977bc39a439778a5a40949f986b4edb1dc4e2 Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Mon, 25 Nov 2024 13:23:29 +0100 Subject: [PATCH 2/8] add failing integration test --- integrations/vite/svelte.test.ts | 125 ++++++++++++++++++++++++++++++- 1 file changed, 124 insertions(+), 1 deletion(-) diff --git a/integrations/vite/svelte.test.ts b/integrations/vite/svelte.test.ts index ed0a6f9cb368..42e46fa37646 100644 --- a/integrations/vite/svelte.test.ts +++ b/integrations/vite/svelte.test.ts @@ -1,5 +1,5 @@ import { expect } from 'vitest' -import { candidate, css, html, json, retryAssertion, test, ts } from '../utils' +import { candidate, css, html, js, json, retryAssertion, test, ts } from '../utils' test( 'production build', @@ -252,3 +252,126 @@ test( }) }, ) + +test( + 'https://github.com/tailwindlabs/tailwindcss/issues/15148', + { + fs: { + 'package.json': json` + { + "type": "module", + "dependencies": { + "svelte": "^4.2.18", + "tailwindcss": "workspace:^" + }, + "devDependencies": { + "@sveltejs/vite-plugin-svelte": "^3.1.1", + "@tailwindcss/vite": "workspace:^", + "vite": "^5.3.5", + "svelte": "^5.1.3" + } + } + `, + 'vite.config.ts': ts` + import { defineConfig } from 'vite' + import tailwindcss from '@tailwindcss/vite' + import { svelte } from '@sveltejs/vite-plugin-svelte' + + // https://vite.dev/config/ + export default defineConfig({ + plugins: [svelte(), tailwindcss()], + }) + `, + 'svelte.config.js': js` + import { vitePreprocess } from '@sveltejs/vite-plugin-svelte' + + export default { + // Consult https://svelte.dev/docs#compile-time-svelte-preprocess + // for more information about preprocessors + preprocess: vitePreprocess(), + } + `, + 'index.html': html` + + + +
+ + + + `, + 'src/main.ts': ts` + import App from './App.svelte' + import './app.css' + const app = new App({ target: document.body }) + `, + 'src/app.css': css` + @import 'tailwindcss'; + + @utility test-red { + width: 10rem; + height: 10rem; + background-color: red; + } + + @utility test-green { + width: 10rem; + height: 10rem; + background-color: green; + } + + @utility test-blue { + width: 10rem; + height: 10rem; + background-color: blue; + } + + @utility test-tomato { + width: 10rem; + height: 10rem; + background-color: tomato; + } + `, + 'src/App.svelte': html` + + + {#each classes as cls} +
+ {/each} + ` + + // Replace all spaces with tabs + .replace(/\[[\s\S]*\]/gm, (m) => m.replace(/[ ]+/g, '\t')), + }, + }, + async ({ fs, exec }) => { + await exec('pnpm vite build') + + let files = await fs.glob('dist/**/*.css') + expect(files).toHaveLength(1) + + await fs.expectFileToContain(files[0][0], [ + candidate`test-red`, + candidate`test-green`, + candidate`test-blue`, + candidate`test-tomato`, + ]) + }, +) From 150bde0e7e05a220cd42155da78885318ce19a23 Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Mon, 25 Nov 2024 12:34:53 +0100 Subject: [PATCH 3/8] add failing test --- crates/oxide/src/parser.rs | 39 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/crates/oxide/src/parser.rs b/crates/oxide/src/parser.rs index e1bc6510eda1..c3ea114ce679 100644 --- a/crates/oxide/src/parser.rs +++ b/crates/oxide/src/parser.rs @@ -1543,4 +1543,43 @@ mod test { let candidates = run(r#"
"#, false); assert_eq!(candidates, vec!["div", "class", "h2", "hz",]); } + + #[test] + fn classes_in_js_arrays_multi_line() { + let candidates = run( + "let classes = [\n\t'bg-black',\n\t'hover:px-0.5',\n\t'text-[13px]',\n\t'[--my-var:1_/_2]',\n\t'[.foo_&]:px-[0]',\n\t'[.foo_&]:[color:red]'\n]", + false, + ); + + assert_eq!( + candidates, + vec![ + "let", + "classes", + "bg-black", + "hover:px-0.5", + "text-[13px]", + "[--my-var:1_/_2]", + "--my-var:1_/_2", + "[.foo_&]:px-[0]", + "[.foo_&]:[color:red]", + ] + ); + + let candidates = run( + "\n \n ", + false, + ); + assert_eq!( + candidates, + vec![ + "script", + "const", + "classes", + "text-red-500", + "text-green-500", + "text-blue-500" + ] + ); + } } From 74dcd3b74a2b10978d28cbe75ad26dc5eb32243f Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Mon, 25 Nov 2024 13:10:37 +0100 Subject: [PATCH 4/8] skip all whitespace We were only looking at spaces, but we should look at newlines, tabs, ... as well. --- crates/oxide/src/parser.rs | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/crates/oxide/src/parser.rs b/crates/oxide/src/parser.rs index c3ea114ce679..8c542a524ff5 100644 --- a/crates/oxide/src/parser.rs +++ b/crates/oxide/src/parser.rs @@ -568,7 +568,7 @@ impl<'a> Extractor<'a> { } }, - b' ' if !self.opts.preserve_spaces_in_arbitrary => { + c if c.is_ascii_whitespace() && !self.opts.preserve_spaces_in_arbitrary => { trace!("Arbitrary::SkipAndEndEarly\t"); if let Arbitrary::Brackets { start_idx } | Arbitrary::Parens { start_idx } = @@ -631,10 +631,8 @@ impl<'a> Extractor<'a> { match self.cursor.curr { // Enter arbitrary value mode. E.g.: `bg-[rgba(0, 0, 0)]` // ^ - b'[' if matches!( - self.cursor.prev, - b'@' | b'-' | b' ' | b':' | b'/' | b'!' | b'\0' - ) => + b'[' if matches!(self.cursor.prev, b'@' | b'-' | b':' | b'/' | b'!' | b'\0') + || self.cursor.prev.is_ascii_whitespace() => { trace!("Arbitrary::Start\t"); self.arbitrary = Arbitrary::Brackets { @@ -666,7 +664,8 @@ impl<'a> Extractor<'a> { (true, _) => ParseAction::Consume, // Looks like the end of a candidate == okay - (_, b' ' | b'\'' | b'"' | b'`') => ParseAction::Consume, + (_, b'\'' | b'"' | b'`') => ParseAction::Consume, + (_, c) if c.is_ascii_whitespace() => ParseAction::Consume, // Otherwise, not a valid character in a candidate _ => ParseAction::Skip, From c3e044e2a99b954d7680532f4e2cae17019cc11d Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Mon, 25 Nov 2024 16:04:59 +0100 Subject: [PATCH 5/8] update changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index bcb1289fe94f..76dc831422f6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Ensure `.group` and `.peer` are prefixed when using the `prefix(…)` option ([#15174](https://github.com/tailwindlabs/tailwindcss/pull/15174)) - Ensure 3D transforms render correctly in Safari ([#15179](https://github.com/tailwindlabs/tailwindcss/pull/15179)) - Ensure `--spacing-*` variables take precedence over `--container-*` variables ([#15180](https://github.com/tailwindlabs/tailwindcss/pull/15180)) +- Fix scanning classes delimited by tab characters ([#15169](https://github.com/tailwindlabs/tailwindcss/pull/15169)) ## [4.0.0-beta.2] - 2024-11-22 From 9329d5715baeb7e9a961ba043e1e7ea0ef9ba7f3 Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Mon, 25 Nov 2024 22:20:04 +0100 Subject: [PATCH 6/8] add dedicated tests with different whitespace (none, spaces, tabs and newline) --- crates/oxide/src/parser.rs | 69 +++++++++++++++++++++++++++++++++----- 1 file changed, 61 insertions(+), 8 deletions(-) diff --git a/crates/oxide/src/parser.rs b/crates/oxide/src/parser.rs index 8c542a524ff5..281774261ad6 100644 --- a/crates/oxide/src/parser.rs +++ b/crates/oxide/src/parser.rs @@ -1544,9 +1544,9 @@ mod test { } #[test] - fn classes_in_js_arrays_multi_line() { + fn classes_in_an_array_without_whitespace() { let candidates = run( - "let classes = [\n\t'bg-black',\n\t'hover:px-0.5',\n\t'text-[13px]',\n\t'[--my-var:1_/_2]',\n\t'[.foo_&]:px-[0]',\n\t'[.foo_&]:[color:red]'\n]", + "let classes = ['bg-black','hover:px-0.5','text-[13px]','[--my-var:1_/_2]','[.foo_&]:px-[0]','[.foo_&]:[color:red]']", false, ); @@ -1564,20 +1564,73 @@ mod test { "[.foo_&]:[color:red]", ] ); + } + #[test] + fn classes_in_an_array_with_spaces() { let candidates = run( - "\n \n ", + "let classes = ['bg-black', 'hover:px-0.5', 'text-[13px]', '[--my-var:1_/_2]', '[.foo_&]:px-[0]', '[.foo_&]:[color:red]']", false, ); + assert_eq!( candidates, vec![ - "script", - "const", + "let", "classes", - "text-red-500", - "text-green-500", - "text-blue-500" + "bg-black", + "hover:px-0.5", + "text-[13px]", + "[--my-var:1_/_2]", + "--my-var:1_/_2", + "[.foo_&]:px-[0]", + "[.foo_&]:[color:red]", + ] + ); + } + + #[test] + fn classes_in_an_array_with_tabs() { + let candidates = run( + "let classes = ['bg-black',\t'hover:px-0.5',\t'text-[13px]',\t'[--my-var:1_/_2]',\t'[.foo_&]:px-[0]',\t'[.foo_&]:[color:red]']", + false, + ); + + assert_eq!( + candidates, + vec![ + "let", + "classes", + "bg-black", + "hover:px-0.5", + "text-[13px]", + "[--my-var:1_/_2]", + "--my-var:1_/_2", + "[.foo_&]:px-[0]", + "[.foo_&]:[color:red]", + ] + ); + } + + #[test] + fn classes_in_an_array_with_newlines() { + let candidates = run( + "let classes = [\n'bg-black',\n'hover:px-0.5',\n'text-[13px]',\n'[--my-var:1_/_2]',\n'[.foo_&]:px-[0]',\n'[.foo_&]:[color:red]'\n]", + false, + ); + + assert_eq!( + candidates, + vec![ + "let", + "classes", + "bg-black", + "hover:px-0.5", + "text-[13px]", + "[--my-var:1_/_2]", + "--my-var:1_/_2", + "[.foo_&]:px-[0]", + "[.foo_&]:[color:red]", ] ); } From 31772318700f080a6567b2dc7959875b83cadaec Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Mon, 25 Nov 2024 22:34:35 +0100 Subject: [PATCH 7/8] improve reproduction integration tests --- integrations/cli/index.test.ts | 75 ++++++++++++++++++++++++++++++++ integrations/vite/svelte.test.ts | 23 +++------- 2 files changed, 82 insertions(+), 16 deletions(-) diff --git a/integrations/cli/index.test.ts b/integrations/cli/index.test.ts index c6eac6fa782f..e125ff5058ba 100644 --- a/integrations/cli/index.test.ts +++ b/integrations/cli/index.test.ts @@ -992,3 +992,78 @@ test( `) }, ) + +test( + 'https://github.com/tailwindlabs/tailwindcss/issues/15148', + { + fs: { + 'package.json': json` + { + "type": "module", + "dependencies": { + "tailwindcss": "workspace:^", + "@tailwindcss/cli": "workspace:^" + } + } + `, + 'index.css': css` + @import 'tailwindcss'; + @utility test-a { + color: red; + } + @utility test-b { + color: green; + } + @utility test-c { + color: blue; + } + @utility test-d { + color: tomato; + } + `, + 'app.js': js` + const classes = [ + 'test-a', + 'test-b', + 'test-c', + 'test-d', + 'multiple-entries-to-keep-newlines', + 'multiple-entries-to-keep-newlines', + 'multiple-entries-to-keep-newlines', + 'multiple-entries-to-keep-newlines', + 'multiple-entries-to-keep-newlines', + 'multiple-entries-to-keep-newlines', + 'multiple-entries-to-keep-newlines', + 'multiple-entries-to-keep-newlines', + 'multiple-entries-to-keep-newlines', + 'multiple-entries-to-keep-newlines', + 'multiple-entries-to-keep-newlines', + 'multiple-entries-to-keep-newlines', + 'multiple-entries-to-keep-newlines', + 'multiple-entries-to-keep-newlines', + 'multiple-entries-to-keep-newlines', + 'multiple-entries-to-keep-newlines', + ] + ` + // The original test case used Svelte, Vite, etc… The issue is caused by + // the fact that output from the compiler is what is/was scanned by Oxide. + // The Svelte compiler uses tabs for indents instead of spaces which was + // the actual cause of the bug. It could be reproduced in the CLI just by + // using tabs + .replace(/\[[\s\S]*\]/gm, (m) => m.replace(/\s+/g, '\t')), + }, + }, + async ({ fs, exec }) => { + await exec('pnpm tailwindcss --input index.css --output dist/out.css') + + let files = await fs.glob('dist/**/*.css') + expect(files).toHaveLength(1) + + await fs.expectFileToContain(files[0][0], [ + candidate`test-a`, + candidate`test-b`, + candidate`test-c`, + candidate`test-d`, + ]) + }, +) diff --git a/integrations/vite/svelte.test.ts b/integrations/vite/svelte.test.ts index 42e46fa37646..b6daa606ce3d 100644 --- a/integrations/vite/svelte.test.ts +++ b/integrations/vite/svelte.test.ts @@ -1,5 +1,5 @@ import { expect } from 'vitest' -import { candidate, css, html, js, json, retryAssertion, test, ts } from '../utils' +import { candidate, css, html, json, retryAssertion, test, ts } from '../utils' test( 'production build', @@ -253,6 +253,9 @@ test( }, ) +// Context: when using `svelte()` before `tailwindcss()` it means that +// `tailwindcss()` sees the contents after the svelte compiler ran. The svelte +// compiler outputs tabs instead of spaces which we didn't handle correctly. test( 'https://github.com/tailwindlabs/tailwindcss/issues/15148', { @@ -275,22 +278,13 @@ test( 'vite.config.ts': ts` import { defineConfig } from 'vite' import tailwindcss from '@tailwindcss/vite' - import { svelte } from '@sveltejs/vite-plugin-svelte' + import { svelte, vitePreprocess } from '@sveltejs/vite-plugin-svelte' // https://vite.dev/config/ export default defineConfig({ - plugins: [svelte(), tailwindcss()], + plugins: [svelte({ preprocess: [vitePreprocess()] }), tailwindcss()], }) `, - 'svelte.config.js': js` - import { vitePreprocess } from '@sveltejs/vite-plugin-svelte' - - export default { - // Consult https://svelte.dev/docs#compile-time-svelte-preprocess - // for more information about preprocessors - preprocess: vitePreprocess(), - } - `, 'index.html': html` @@ -355,10 +349,7 @@ test( {#each classes as cls}
{/each} - ` - - // Replace all spaces with tabs - .replace(/\[[\s\S]*\]/gm, (m) => m.replace(/[ ]+/g, '\t')), + `, }, }, async ({ fs, exec }) => { From 7ec2bf8ae0d20675a2a25222bce81d211754ce66 Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Tue, 26 Nov 2024 19:14:15 +0100 Subject: [PATCH 8/8] drop integration tests --- integrations/cli/index.test.ts | 75 -------------------- integrations/vite/svelte.test.ts | 114 ------------------------------- 2 files changed, 189 deletions(-) diff --git a/integrations/cli/index.test.ts b/integrations/cli/index.test.ts index e125ff5058ba..c6eac6fa782f 100644 --- a/integrations/cli/index.test.ts +++ b/integrations/cli/index.test.ts @@ -992,78 +992,3 @@ test( `) }, ) - -test( - 'https://github.com/tailwindlabs/tailwindcss/issues/15148', - { - fs: { - 'package.json': json` - { - "type": "module", - "dependencies": { - "tailwindcss": "workspace:^", - "@tailwindcss/cli": "workspace:^" - } - } - `, - 'index.css': css` - @import 'tailwindcss'; - @utility test-a { - color: red; - } - @utility test-b { - color: green; - } - @utility test-c { - color: blue; - } - @utility test-d { - color: tomato; - } - `, - 'app.js': js` - const classes = [ - 'test-a', - 'test-b', - 'test-c', - 'test-d', - 'multiple-entries-to-keep-newlines', - 'multiple-entries-to-keep-newlines', - 'multiple-entries-to-keep-newlines', - 'multiple-entries-to-keep-newlines', - 'multiple-entries-to-keep-newlines', - 'multiple-entries-to-keep-newlines', - 'multiple-entries-to-keep-newlines', - 'multiple-entries-to-keep-newlines', - 'multiple-entries-to-keep-newlines', - 'multiple-entries-to-keep-newlines', - 'multiple-entries-to-keep-newlines', - 'multiple-entries-to-keep-newlines', - 'multiple-entries-to-keep-newlines', - 'multiple-entries-to-keep-newlines', - 'multiple-entries-to-keep-newlines', - 'multiple-entries-to-keep-newlines', - ] - ` - // The original test case used Svelte, Vite, etc… The issue is caused by - // the fact that output from the compiler is what is/was scanned by Oxide. - // The Svelte compiler uses tabs for indents instead of spaces which was - // the actual cause of the bug. It could be reproduced in the CLI just by - // using tabs - .replace(/\[[\s\S]*\]/gm, (m) => m.replace(/\s+/g, '\t')), - }, - }, - async ({ fs, exec }) => { - await exec('pnpm tailwindcss --input index.css --output dist/out.css') - - let files = await fs.glob('dist/**/*.css') - expect(files).toHaveLength(1) - - await fs.expectFileToContain(files[0][0], [ - candidate`test-a`, - candidate`test-b`, - candidate`test-c`, - candidate`test-d`, - ]) - }, -) diff --git a/integrations/vite/svelte.test.ts b/integrations/vite/svelte.test.ts index b6daa606ce3d..ed0a6f9cb368 100644 --- a/integrations/vite/svelte.test.ts +++ b/integrations/vite/svelte.test.ts @@ -252,117 +252,3 @@ test( }) }, ) - -// Context: when using `svelte()` before `tailwindcss()` it means that -// `tailwindcss()` sees the contents after the svelte compiler ran. The svelte -// compiler outputs tabs instead of spaces which we didn't handle correctly. -test( - 'https://github.com/tailwindlabs/tailwindcss/issues/15148', - { - fs: { - 'package.json': json` - { - "type": "module", - "dependencies": { - "svelte": "^4.2.18", - "tailwindcss": "workspace:^" - }, - "devDependencies": { - "@sveltejs/vite-plugin-svelte": "^3.1.1", - "@tailwindcss/vite": "workspace:^", - "vite": "^5.3.5", - "svelte": "^5.1.3" - } - } - `, - 'vite.config.ts': ts` - import { defineConfig } from 'vite' - import tailwindcss from '@tailwindcss/vite' - import { svelte, vitePreprocess } from '@sveltejs/vite-plugin-svelte' - - // https://vite.dev/config/ - export default defineConfig({ - plugins: [svelte({ preprocess: [vitePreprocess()] }), tailwindcss()], - }) - `, - 'index.html': html` - - - -
- - - - `, - 'src/main.ts': ts` - import App from './App.svelte' - import './app.css' - const app = new App({ target: document.body }) - `, - 'src/app.css': css` - @import 'tailwindcss'; - - @utility test-red { - width: 10rem; - height: 10rem; - background-color: red; - } - - @utility test-green { - width: 10rem; - height: 10rem; - background-color: green; - } - - @utility test-blue { - width: 10rem; - height: 10rem; - background-color: blue; - } - - @utility test-tomato { - width: 10rem; - height: 10rem; - background-color: tomato; - } - `, - 'src/App.svelte': html` - - - {#each classes as cls} -
- {/each} - `, - }, - }, - async ({ fs, exec }) => { - await exec('pnpm vite build') - - let files = await fs.glob('dist/**/*.css') - expect(files).toHaveLength(1) - - await fs.expectFileToContain(files[0][0], [ - candidate`test-red`, - candidate`test-green`, - candidate`test-blue`, - candidate`test-tomato`, - ]) - }, -)