From 4a0701a1cd99799658441a354dac41d5d140ef04 Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Thu, 31 Oct 2024 14:33:22 -0400 Subject: [PATCH 1/6] Add helper for writing binary files in integration tests --- integrations/utils.ts | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/integrations/utils.ts b/integrations/utils.ts index 70b97c111d06..cee50c6593df 100644 --- a/integrations/utils.ts +++ b/integrations/utils.ts @@ -29,7 +29,7 @@ interface ExecOptions { interface TestConfig { fs: { - [filePath: string]: string + [filePath: string]: string | Uint8Array } } interface TestContext { @@ -279,8 +279,14 @@ export function test( }) }, fs: { - async write(filename: string, content: string): Promise { + async write(filename: string, content: string | Uint8Array): Promise { let full = path.join(root, filename) + let dir = path.dirname(full) + await fs.mkdir(dir, { recursive: true }) + + if (typeof content !== 'string') { + return await fs.writeFile(full, content) + } if (filename.endsWith('package.json')) { content = await overwriteVersionsInPackageJson(content) @@ -291,8 +297,6 @@ export function test( content = content.replace(/\n/g, '\r\n') } - let dir = path.dirname(full) - await fs.mkdir(dir, { recursive: true }) await fs.writeFile(full, content) }, @@ -494,6 +498,12 @@ export let json = dedent export let yaml = dedent export let txt = dedent +export function binary(str: string | TemplateStringsArray, ...values: unknown[]): Uint8Array { + let base64 = typeof str === 'string' ? str : String.raw(str, ...values) + + return Uint8Array.from(atob(base64), (c) => c.charCodeAt(0)) +} + export function candidate(strings: TemplateStringsArray, ...values: any[]) { let output: string[] = [] for (let i = 0; i < strings.length; i++) { From 20685a3fd24ca80b0c0f427fb1db1fdde6286e6b Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Thu, 31 Oct 2024 15:30:17 -0400 Subject: [PATCH 2/6] Add failing test for Vite URL rewriting --- integrations/vite/url-rewriting.test.ts | 68 +++++++++++++++++++++++++ 1 file changed, 68 insertions(+) create mode 100644 integrations/vite/url-rewriting.test.ts diff --git a/integrations/vite/url-rewriting.test.ts b/integrations/vite/url-rewriting.test.ts new file mode 100644 index 000000000000..36439551506f --- /dev/null +++ b/integrations/vite/url-rewriting.test.ts @@ -0,0 +1,68 @@ +import { expect } from 'vitest' +import { binary, css, html, json, test, ts } from '../utils' + +const SIMPLE_IMAGE = `iVBORw0KGgoAAAANSUhEUgAAADAAAAAlAQAAAAAsYlcCAAAACklEQVR4AWMYBQABAwABRUEDtQAAAABJRU5ErkJggg==` + +test( + 'can rewrite urls in production builds', + { + fs: { + 'package.json': json` + { + "type": "module", + "dependencies": { + "tailwindcss": "workspace:^" + }, + "devDependencies": { + "@tailwindcss/vite": "workspace:^", + "vite": "^5.3.5" + } + } + `, + 'vite.config.ts': ts` + import { defineConfig } from 'vite' + import tailwindcss from '@tailwindcss/vite' + + export default defineConfig({ + plugins: [tailwindcss()], + }) + `, + 'index.html': html` + + + + + + +
+ + + + `, + 'src/main.ts': ts``, + 'src/app.css': css` + @import './dir-1/bar.css'; + @import './dir-1/dir-2/baz.css'; + `, + 'src/dir-1/bar.css': css` + .bar { + background-image: url('../../resources/image.png'); + } + `, + 'src/dir-1/dir-2/baz.css': css` + .baz { + background-image: url('../../../resources/image.png'); + } + `, + 'resources/image.png': binary(SIMPLE_IMAGE), + }, + }, + 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], [SIMPLE_IMAGE]) + }, +) From 35eb86dbf0bf14d8e37f4dc8c618ae71868ea187 Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Thu, 31 Oct 2024 15:37:39 -0400 Subject: [PATCH 3/6] =?UTF-8?q?Add=20test=20for=20`:deep(=E2=80=A6)`=20in?= =?UTF-8?q?=20Vue=20scoped=20style=20blocks?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- integrations/vite/vue.test.ts | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/integrations/vite/vue.test.ts b/integrations/vite/vue.test.ts index 4f582a99fcdc..35ed5de77ba6 100644 --- a/integrations/vite/vue.test.ts +++ b/integrations/vite/vue.test.ts @@ -51,9 +51,15 @@ test( @apply text-red-500; } - + `, }, @@ -65,5 +71,7 @@ test( expect(files).toHaveLength(1) await fs.expectFileToContain(files[0][0], [candidate`underline`, candidate`foo`]) + + await fs.expectFileNotToContain(files[0][0], [':deep(.bar)']) }, ) From 53049e989105389ad7b3878383a13e6543b34c2a Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Thu, 31 Oct 2024 15:39:25 -0400 Subject: [PATCH 4/6] Generate CSS after Vite handles imports during prod builds --- packages/@tailwindcss-vite/src/index.ts | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/packages/@tailwindcss-vite/src/index.ts b/packages/@tailwindcss-vite/src/index.ts index 57eaf42affb5..bcb829fbc559 100644 --- a/packages/@tailwindcss-vite/src/index.ts +++ b/packages/@tailwindcss-vite/src/index.ts @@ -209,7 +209,22 @@ export default function tailwindcss(): Plugin[] { // Step 2 (full build): Generate CSS name: '@tailwindcss/vite:generate:build', apply: 'build', - enforce: 'pre', + + // NOTE: + // We used to use `enforce: 'pre'` here because Tailwind CSS can handle + // imports itself. However, this caused two problems: + // + // - Relative asset URL rewriting for was not happening so things like + // `background-image: url(../image.png)` could break if done in an + // imported CSS file. + // + // - Processing of Vue scoped style blocks didn't happen at the right time + // which caused `:deep(…)` to end up in the generated CSS rather than + // appropriately handled by Vue. + // + // This does mean that Tailwind is no longer handling the imports itself + // which is not ideal but it's a reasonable tradeoff until we can resolve + // both of these issues with Tailwind's own import handling. async transform(src, id) { if (!isPotentialCssRootFile(id)) return From eb8ae5279b446c2a654a242ffde05317c4fc17c7 Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Thu, 31 Oct 2024 16:15:11 -0400 Subject: [PATCH 5/6] Disable fix when using Lightning CSS --- packages/@tailwindcss-vite/src/index.ts | 63 +++++++++++++++++++++++++ 1 file changed, 63 insertions(+) diff --git a/packages/@tailwindcss-vite/src/index.ts b/packages/@tailwindcss-vite/src/index.ts index bcb829fbc559..014755d60a4d 100644 --- a/packages/@tailwindcss-vite/src/index.ts +++ b/packages/@tailwindcss-vite/src/index.ts @@ -133,6 +133,10 @@ export default function tailwindcss(): Plugin[] { return css } + function isUsingLightningCSS() { + return config?.css.transformer === 'lightningcss' + } + return [ { // Step 1: Scan source files for candidates @@ -227,6 +231,7 @@ export default function tailwindcss(): Plugin[] { // both of these issues with Tailwind's own import handling. async transform(src, id) { + if (isUsingLightningCSS()) return if (!isPotentialCssRootFile(id)) return let root = roots.get(id) @@ -246,6 +251,64 @@ export default function tailwindcss(): Plugin[] { // We must run before `enforce: post` so the updated chunks are picked up // by vite:css-post. async renderStart() { + if (isUsingLightningCSS()) return + + for (let [id, root] of roots.entries()) { + let generated = await regenerateOptimizedCss( + root, + // During the renderStart phase, we can not add watch files since + // those would not be causing a refresh of the right CSS file. This + // should not be an issue since we did already process the CSS file + // before and the dependencies should not be changed (only the + // candidate list might have) + () => {}, + ) + if (!generated) { + roots.delete(id) + continue + } + + // These plugins have side effects which, during build, results in CSS + // being written to the output dir. We need to run them here to ensure + // the CSS is written before the bundle is generated. + await transformWithPlugins(this, id, generated) + } + }, + }, + + { + // NOTE: This is an exact copy of the above plugin but with `enforce: pre` + // when using Lightning CSS. + + // Step 2 (full build): Generate CSS + name: '@tailwindcss/vite:generate:build', + apply: 'build', + enforce: 'pre', + + async transform(src, id) { + if (!isUsingLightningCSS()) return + + if (!isPotentialCssRootFile(id)) return + + let root = roots.get(id) + + // We do a first pass to generate valid CSS for the downstream plugins. + // However, since not all candidates are guaranteed to be extracted by + // this time, we have to re-run a transform for the root later. + let generated = await root.generate(src, (file) => this.addWatchFile(file)) + if (!generated) { + roots.delete(id) + return src + } + return { code: generated } + }, + + // `renderStart` runs in the bundle generation stage after all transforms. + // We must run before `enforce: post` so the updated chunks are picked up + // by vite:css-post. + async renderStart() { + if (!isUsingLightningCSS()) return + for (let [id, root] of roots.entries()) { let generated = await regenerateOptimizedCss( root, From 45da06c8f143e789156f662c1c4c74302a0d97da Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Fri, 1 Nov 2024 10:27:38 -0400 Subject: [PATCH 6/6] Add stub test for lightningcss This fails so its marked as a `todo` test. --- integrations/utils.ts | 5 + integrations/vite/url-rewriting.test.ts | 132 +++++++++++++----------- 2 files changed, 75 insertions(+), 62 deletions(-) diff --git a/integrations/utils.ts b/integrations/utils.ts index cee50c6593df..c52271bdb83b 100644 --- a/integrations/utils.ts +++ b/integrations/utils.ts @@ -28,6 +28,7 @@ interface ExecOptions { } interface TestConfig { + todo?: boolean fs: { [filePath: string]: string | Uint8Array } @@ -74,6 +75,10 @@ export function test( testCallback: TestCallback, { only = false, debug = false }: TestFlags = {}, ) { + if (config.todo) { + return defaultTest.todo(name) + } + return (only || (!process.env.CI && debug) ? defaultTest.only : defaultTest)( name, { timeout: TEST_TIMEOUT, retry: process.env.CI ? 2 : 0 }, diff --git a/integrations/vite/url-rewriting.test.ts b/integrations/vite/url-rewriting.test.ts index 36439551506f..77a131f80287 100644 --- a/integrations/vite/url-rewriting.test.ts +++ b/integrations/vite/url-rewriting.test.ts @@ -1,68 +1,76 @@ -import { expect } from 'vitest' -import { binary, css, html, json, test, ts } from '../utils' +import { describe, expect } from 'vitest' +import { binary, css, html, test, ts, txt } from '../utils' const SIMPLE_IMAGE = `iVBORw0KGgoAAAANSUhEUgAAADAAAAAlAQAAAAAsYlcCAAAACklEQVR4AWMYBQABAwABRUEDtQAAAABJRU5ErkJggg==` -test( - 'can rewrite urls in production builds', - { - fs: { - 'package.json': json` - { - "type": "module", - "dependencies": { - "tailwindcss": "workspace:^" - }, - "devDependencies": { - "@tailwindcss/vite": "workspace:^", - "vite": "^5.3.5" - } - } - `, - 'vite.config.ts': ts` - import { defineConfig } from 'vite' - import tailwindcss from '@tailwindcss/vite' +for (let transformer of ['postcss', 'lightningcss']) { + describe(transformer, () => { + test( + 'can rewrite urls in production builds', + { + todo: transformer === 'lightningcss', + fs: { + 'package.json': txt` + { + "type": "module", + "dependencies": { + "tailwindcss": "workspace:^" + }, + "devDependencies": { + ${transformer === 'lightningcss' ? `"lightningcss": "^1.26.0",` : ''} + "@tailwindcss/vite": "workspace:^", + "vite": "^5.3.5" + } + } + `, + 'vite.config.ts': ts` + import tailwindcss from '@tailwindcss/vite' + import { defineConfig } from 'vite' - export default defineConfig({ - plugins: [tailwindcss()], - }) - `, - 'index.html': html` - - - - - - -
- - - - `, - 'src/main.ts': ts``, - 'src/app.css': css` - @import './dir-1/bar.css'; - @import './dir-1/dir-2/baz.css'; - `, - 'src/dir-1/bar.css': css` - .bar { - background-image: url('../../resources/image.png'); - } - `, - 'src/dir-1/dir-2/baz.css': css` - .baz { - background-image: url('../../../resources/image.png'); - } - `, - 'resources/image.png': binary(SIMPLE_IMAGE), - }, - }, - async ({ fs, exec }) => { - await exec('pnpm vite build') + export default defineConfig({ + plugins: [tailwindcss()], + build: { cssMinify: false }, + css: ${transformer === 'postcss' ? '{}' : "{ transformer: 'lightningcss' }"}, + }) + `, + 'index.html': html` + + + + + + +
+ + + + `, + 'src/main.ts': ts``, + 'src/app.css': css` + @import './dir-1/bar.css'; + @import './dir-1/dir-2/baz.css'; + `, + 'src/dir-1/bar.css': css` + .bar { + background-image: url('../../resources/image.png'); + } + `, + 'src/dir-1/dir-2/baz.css': css` + .baz { + background-image: url('../../../resources/image.png'); + } + `, + 'resources/image.png': binary(SIMPLE_IMAGE), + }, + }, + async ({ fs, exec }) => { + await exec('pnpm vite build') - let files = await fs.glob('dist/**/*.css') - expect(files).toHaveLength(1) + let files = await fs.glob('dist/**/*.css') + expect(files).toHaveLength(1) - await fs.expectFileToContain(files[0][0], [SIMPLE_IMAGE]) - }, -) + await fs.expectFileToContain(files[0][0], [SIMPLE_IMAGE]) + }, + ) + }) +}