From 209cfa2b3d51479b63a99b255e251bb867fb5eac Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Thu, 31 Oct 2024 14:33:22 -0400 Subject: [PATCH 1/8] 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 277b3bda4b29..5de32b3252d0 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 { @@ -280,8 +280,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) @@ -292,8 +298,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, 'utf-8') }, @@ -495,6 +499,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 b1d41e77abbe51cb7fa525222527e30a84ccda20 Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Thu, 31 Oct 2024 15:30:17 -0400 Subject: [PATCH 2/8] Add failing test for Vite URL rewriting Add stub test for lightningcss This fails so its marked as a `todo` test. --- integrations/utils.ts | 1 + integrations/vite/url-rewriting.test.ts | 97 +++++++++++++++++++++++++ 2 files changed, 98 insertions(+) create mode 100644 integrations/vite/url-rewriting.test.ts diff --git a/integrations/utils.ts b/integrations/utils.ts index 5de32b3252d0..9b06d8dc1a18 100644 --- a/integrations/utils.ts +++ b/integrations/utils.ts @@ -491,6 +491,7 @@ function testIfPortTaken(port: number): Promise { }) } +export let svg = dedent export let css = dedent export let html = dedent export let ts = dedent diff --git a/integrations/vite/url-rewriting.test.ts b/integrations/vite/url-rewriting.test.ts new file mode 100644 index 000000000000..bb4b4b5047e2 --- /dev/null +++ b/integrations/vite/url-rewriting.test.ts @@ -0,0 +1,97 @@ +import { describe, expect } from 'vitest' +import { binary, css, html, svg, test, ts, txt } from '../utils' + +const SIMPLE_IMAGE = `iVBORw0KGgoAAAANSUhEUgAAADAAAAAlAQAAAAAsYlcCAAAACklEQVR4AWMYBQABAwABRUEDtQAAAABJRU5ErkJggg==` + +for (let transformer of ['postcss', 'lightningcss']) { + describe(transformer, () => { + test( + 'can rewrite urls in production builds', + { + 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()], + build: { + assetsInlineLimit: 256, + 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'; + @import './dir-1/dir-2/vector.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'); + } + `, + 'src/dir-1/dir-2/vector.css': css` + .baz { + background-image: url('../../../resources/vector.svg'); + } + `, + 'resources/image.png': binary(SIMPLE_IMAGE), + 'resources/vector.svg': svg` + + + + + + + `, + }, + }, + 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]) + + let images = await fs.glob('dist/**/*.svg') + expect(images).toHaveLength(1) + + await fs.expectFileToContain(files[0][0], [/\/assets\/vector-.*?\.svg/]) + }, + ) + }) +} From fc52f685b213005d5a46bb07e3ad62c97686f1ce Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Tue, 5 Nov 2024 10:26:52 -0500 Subject: [PATCH 3/8] Rewrite urls in CSS files in Vite Co-authored-by: Philipp Spiess --- packages/@tailwindcss-node/src/compile.ts | 23 ++- packages/@tailwindcss-node/src/urls.test.ts | 127 ++++++++++++ packages/@tailwindcss-node/src/urls.ts | 203 ++++++++++++++++++++ packages/@tailwindcss-vite/src/index.ts | 1 + packages/tailwindcss/src/ast.ts | 3 +- 5 files changed, 354 insertions(+), 3 deletions(-) create mode 100644 packages/@tailwindcss-node/src/urls.test.ts create mode 100644 packages/@tailwindcss-node/src/urls.ts diff --git a/packages/@tailwindcss-node/src/compile.ts b/packages/@tailwindcss-node/src/compile.ts index 40bb9ef1fea4..8a5f8ee20cc5 100644 --- a/packages/@tailwindcss-node/src/compile.ts +++ b/packages/@tailwindcss-node/src/compile.ts @@ -9,10 +9,19 @@ import { compile as _compile, } from 'tailwindcss' import { getModuleDependencies } from './get-module-dependencies' +import { rewriteUrls } from './urls' export async function compile( css: string, - { base, onDependency }: { base: string; onDependency: (path: string) => void }, + { + base, + onDependency, + shouldRewriteUrls, + }: { + base: string + onDependency: (path: string) => void + shouldRewriteUrls?: boolean + }, ) { let compiler = await _compile(css, { base, @@ -20,7 +29,17 @@ export async function compile( return loadModule(id, base, onDependency) }, async loadStylesheet(id, base) { - return loadStylesheet(id, base, onDependency) + let sheet = await loadStylesheet(id, base, onDependency) + + if (shouldRewriteUrls) { + sheet.content = await rewriteUrls({ + css: sheet.content, + root: base, + base: sheet.base, + }) + } + + return sheet }, }) diff --git a/packages/@tailwindcss-node/src/urls.test.ts b/packages/@tailwindcss-node/src/urls.test.ts new file mode 100644 index 000000000000..d6d9fcb3a3d6 --- /dev/null +++ b/packages/@tailwindcss-node/src/urls.test.ts @@ -0,0 +1,127 @@ +import { expect, test } from 'vitest' +import { rewriteUrls } from './urls' + +const css = String.raw + +test('URLs can be rewritten', async () => { + let root = '/root' + + let result = await rewriteUrls({ + root, + base: '/root/foo/bar', + // prettier-ignore + css: css` + .foo { + /* Relative URLs: replaced */ + background: url(./image.jpg); + background: url(../image.jpg); + background: url('./image.jpg'); + background: url("./image.jpg"); + + /* External URL: ignored */ + background: url(http://example.com/image.jpg); + background: url('http://example.com/image.jpg'); + background: url("http://example.com/image.jpg"); + + /* Data URI: ignored */ + /* background: url(data:image/png;base64,abc==); */ + background: url('data:image/png;base64,abc=='); + background: url("data:image/png;base64,abc=="); + + /* Function calls: ignored */ + background: url(var(--foo)); + background: url(var(--foo, './image.jpg')); + background: url(var(--foo, "./image.jpg")); + + /* Fragments: ignored */ + background: url(#dont-touch-this); + + /* Image Sets - Raw URL: replaced */ + background: image-set( + image1.jpg 1x, + image2.jpg 2x + ); + background: image-set( + 'image1.jpg' 1x, + 'image2.jpg' 2x + ); + background: image-set( + "image1.jpg" 1x, + "image2.jpg" 2x + ); + + /* Image Sets - Relative URLs: replaced */ + background: image-set( + url('image1.jpg') 1x, + url('image2.jpg') 2x + ); + background: image-set( + url("image1.jpg") 1x, + url("image2.jpg") 2x + ); + background: image-set( + url('image1.avif') type('image/avif'), + url('image2.jpg') type('image/jpeg') + ); + background: image-set( + url("image1.avif") type('image/avif'), + url("image2.jpg") type('image/jpeg') + ); + + /* Image Sets - Function calls: ignored */ + background: image-set( + linear-gradient(blue, white) 1x, + linear-gradient(blue, green) 2x + ); + + /* Image Sets - Mixed: replaced */ + background: image-set( + linear-gradient(blue, white) 1x, + url("image2.jpg") 2x + ); + } + + /* Fonts - Multiple URLs: replaced */ + @font-face { + font-family: "Newman"; + src: + local("Newman"), + url("newman-COLRv1.otf") format("opentype") tech(color-COLRv1), + url("newman-outline.otf") format("opentype"), + url("newman-outline.woff") format("woff"); + } + `, + }) + + expect(result).toMatchInlineSnapshot(` + ".foo { + background: url(./foo/bar/image.jpg); + background: url(./foo/image.jpg); + background: url('./foo/bar/image.jpg'); + background: url("./foo/bar/image.jpg"); + background: url(http://example.com/image.jpg); + background: url('http://example.com/image.jpg'); + background: url("http://example.com/image.jpg"); + background: url('data:image/png;base64,abc=='); + background: url("data:image/png;base64,abc=="); + background: url(var(--foo)); + background: url(var(--foo, './image.jpg')); + background: url(var(--foo, "./image.jpg")); + background: url(#dont-touch-this); + background: image-set(url(./foo/bar/image1.jpg) 1x, url(./foo/bar/image2.jpg) 2x); + background: image-set(url('./foo/bar/image1.jpg') 1x, url('./foo/bar/image2.jpg') 2x); + background: image-set(url("./foo/bar/image1.jpg") 1x, url("./foo/bar/image2.jpg") 2x); + background: image-set(url('./foo/bar/image1.jpg') 1x, url('./foo/bar/image2.jpg') 2x); + background: image-set(url("./foo/bar/image1.jpg") 1x, url("./foo/bar/image2.jpg") 2x); + background: image-set(url('./foo/bar/image1.avif') type('image/avif'), url('./foo/bar/image2.jpg') type('image/jpeg')); + background: image-set(url("./foo/bar/image1.avif") type('image/avif'), url("./foo/bar/image2.jpg") type('image/jpeg')); + background: image-set(linear-gradient(blue, white) 1x, linear-gradient(blue, green) 2x); + background: image-set(linear-gradient(blue, white) 1x, url("./foo/bar/image2.jpg") 2x); + } + @font-face { + font-family: "Newman"; + src: local("Newman"), url("./foo/bar/newman-COLRv1.otf") format("opentype") tech(color-COLRv1), url("./foo/bar/newman-outline.otf") format("opentype"), url("./foo/bar/newman-outline.woff") format("woff"); + } + " + `) +}) diff --git a/packages/@tailwindcss-node/src/urls.ts b/packages/@tailwindcss-node/src/urls.ts new file mode 100644 index 000000000000..f033c606bb9b --- /dev/null +++ b/packages/@tailwindcss-node/src/urls.ts @@ -0,0 +1,203 @@ +// Inlined version of code from Vite +// Copyright (c) 2019-present, VoidZero Inc. and Vite contributors +// Released under the MIT License. +// +// Minor modifications have been made to work with the Tailwind CSS codebase + +import * as path from 'node:path' +import { toCss, walk } from '../../tailwindcss/src/ast' +import { parse } from '../../tailwindcss/src/css-parser' +import { normalizePath } from './normalize-path' + +const cssUrlRE = + /(?[\w-]+\([^)]*\)|"[^"]*"|'[^']*'|[^,]\S*[^,])\s*(?:\s(?\w[^,]+))?(?:,|$)/g +const nonEscapedDoubleQuoteRE = /(? dataUrlRE.test(url) +const isExternalUrl = (url: string): boolean => externalRE.test(url) + +type CssUrlReplacer = (url: string, importer?: string) => string | Promise + +interface ImageCandidate { + url: string + descriptor: string +} + +export async function rewriteUrls({ + css, + base, + root, +}: { + css: string + base: string + root: string +}) { + if (!css.includes('url(') && !css.includes('image-set(')) { + return css + } + + let ast = parse(css) + + let promises: Promise[] = [] + + function replacerForDeclaration(url: string) { + let absoluteUrl = path.posix.join(normalizePath(base), url) + let relativeUrl = path.posix.relative(normalizePath(root), absoluteUrl) + + // If the path points to a file in the same directory, `path.relative` will + // remove the leading `./` and we need to add it back in order to still + // consider the path relative + if (!relativeUrl.startsWith('.')) { + relativeUrl = './' + relativeUrl + } + + return relativeUrl + } + + walk(ast, (node) => { + if (node.kind !== 'declaration') return + if (!node.value) return + + let isCssUrl = cssUrlRE.test(node.value) + let isCssImageSet = cssImageSetRE.test(node.value) + + if (isCssUrl || isCssImageSet) { + let rewriterToUse = isCssImageSet ? rewriteCssImageSet : rewriteCssUrls + + promises.push( + rewriterToUse(node.value, replacerForDeclaration).then((url) => { + node.value = url + }), + ) + } + }) + + if (promises.length) { + await Promise.all(promises) + } + + return toCss(ast, { + printUtilitiesNode: true, + }) +} + +function rewriteCssUrls(css: string, replacer: CssUrlReplacer): Promise { + return asyncReplace(css, cssUrlRE, async (match) => { + const [matched, rawUrl] = match + return await doUrlReplace(rawUrl.trim(), matched, replacer) + }) +} + +async function rewriteCssImageSet(css: string, replacer: CssUrlReplacer): Promise { + return await asyncReplace(css, cssImageSetRE, async (match) => { + const [, rawUrl] = match + const url = await processSrcSet(rawUrl, async ({ url }) => { + // the url maybe url(...) + if (cssUrlRE.test(url)) { + return await rewriteCssUrls(url, replacer) + } + if (!cssNotProcessedRE.test(url)) { + return await doUrlReplace(url, url, replacer) + } + return url + }) + return url + }) +} + +async function doUrlReplace( + rawUrl: string, + matched: string, + replacer: CssUrlReplacer, + funcName: string = 'url', +) { + let wrap = '' + const first = rawUrl[0] + if (first === `"` || first === `'`) { + wrap = first + rawUrl = rawUrl.slice(1, -1) + } + + if (skipUrlReplacer(rawUrl)) { + return matched + } + + let newUrl = await replacer(rawUrl) + // The new url might need wrapping even if the original did not have it, e.g. if a space was added during replacement + if (wrap === '' && newUrl !== encodeURI(newUrl)) { + wrap = '"' + } + // If wrapping in single quotes and newUrl also contains single quotes, switch to double quotes. + // Give preference to double quotes since SVG inlining converts double quotes to single quotes. + if (wrap === "'" && newUrl.includes("'")) { + wrap = '"' + } + // Escape double quotes if they exist (they also tend to be rarer than single quotes) + if (wrap === '"' && newUrl.includes('"')) { + newUrl = newUrl.replace(nonEscapedDoubleQuoteRE, '\\"') + } + return `${funcName}(${wrap}${newUrl}${wrap})` +} + +function skipUrlReplacer(rawUrl: string) { + return ( + isExternalUrl(rawUrl) || isDataUrl(rawUrl) || rawUrl[0] === '#' || functionCallRE.test(rawUrl) + ) +} + +function processSrcSet( + srcs: string, + replacer: (arg: ImageCandidate) => Promise, +): Promise { + return Promise.all( + parseSrcset(srcs).map(async ({ url, descriptor }) => ({ + url: await replacer({ url, descriptor }), + descriptor, + })), + ).then(joinSrcset) +} + +function parseSrcset(string: string): ImageCandidate[] { + const matches = string + .trim() + .replace(escapedSpaceCharactersRE, ' ') + .replace(/\r?\n/, '') + .replace(/,\s+/, ', ') + .replaceAll(/\s+/g, ' ') + .matchAll(imageCandidateRE) + return Array.from(matches, ({ groups }) => ({ + url: groups?.url?.trim() ?? '', + descriptor: groups?.descriptor?.trim() ?? '', + })).filter(({ url }) => !!url) +} + +function joinSrcset(ret: ImageCandidate[]) { + return ret.map(({ url, descriptor }) => url + (descriptor ? ` ${descriptor}` : '')).join(', ') +} + +async function asyncReplace( + input: string, + re: RegExp, + replacer: (match: RegExpExecArray) => string | Promise, +): Promise { + let match: RegExpExecArray | null + let remaining = input + let rewritten = '' + while ((match = re.exec(remaining))) { + rewritten += remaining.slice(0, match.index) + rewritten += await replacer(match) + remaining = remaining.slice(match.index + match[0].length) + } + rewritten += remaining + return rewritten +} diff --git a/packages/@tailwindcss-vite/src/index.ts b/packages/@tailwindcss-vite/src/index.ts index 6442288912a5..3a7d46c25620 100644 --- a/packages/@tailwindcss-vite/src/index.ts +++ b/packages/@tailwindcss-vite/src/index.ts @@ -375,6 +375,7 @@ class Root { env.DEBUG && console.time('[@tailwindcss/vite] Setup compiler') this.compiler = await compile(content, { base: inputBase, + shouldRewriteUrls: true, onDependency: (path) => { addWatchFile(path) this.dependencies.add(path) diff --git a/packages/tailwindcss/src/ast.ts b/packages/tailwindcss/src/ast.ts index 9f31105891f3..b23cd7a695a0 100644 --- a/packages/tailwindcss/src/ast.ts +++ b/packages/tailwindcss/src/ast.ts @@ -202,7 +202,7 @@ export function walkDepth( } } -export function toCss(ast: AstNode[]) { +export function toCss(ast: AstNode[], { printUtilitiesNode = false } = {}) { let atRoots: string = '' let seenAtProperties = new Set() let propertyFallbacksRoot: Declaration[] = [] @@ -224,6 +224,7 @@ export function toCss(ast: AstNode[]) { // AtRule else if (node.kind === 'at-rule') { if ( + !printUtilitiesNode && node.name === '@tailwind' && (node.params === 'utilities' || node.params.startsWith('utilities')) ) { From 9d25aadc8beb16c072d8f1b9732b92c801130526 Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Tue, 5 Nov 2024 10:36:03 -0500 Subject: [PATCH 4/8] Update changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 92aea0dfe418..e3d88c7b5e94 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Fix crash when using `@source` containing `..` ([#14831](https://github.com/tailwindlabs/tailwindcss/pull/14831)) - Ensure instances of the same variant with different values are always sorted deterministically (e.g. `data-focus:flex` and `data-active:flex`) ([#14835](https://github.com/tailwindlabs/tailwindcss/pull/14835)) - Ensure `--inset-ring=*` and `--inset-shadow-*` variables are ignored by `inset-*` utilities ([#14855](https://github.com/tailwindlabs/tailwindcss/pull/14855)) +- Rewrite urls in CSS files when using Vite ([#14877](https://github.com/tailwindlabs/tailwindcss/pull/14877)) - _Upgrade (experimental)_: Install `@tailwindcss/postcss` next to `tailwindcss` ([#14830](https://github.com/tailwindlabs/tailwindcss/pull/14830)) - _Upgrade (experimental)_: Remove whitespace around `,` separator when print arbitrary values ([#14838](https://github.com/tailwindlabs/tailwindcss/pull/14838)) From 78dca9a7e9968fad7ba3f2601afe0e22b171db05 Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Wed, 6 Nov 2024 07:40:07 -0500 Subject: [PATCH 5/8] =?UTF-8?q?Don=E2=80=99t=20special=20case=20`@tailwind?= =?UTF-8?q?=20utilities`=20in=20toCss?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/@tailwindcss-node/src/urls.ts | 4 +--- packages/tailwindcss/src/ast.ts | 15 ++------------- packages/tailwindcss/src/index.ts | 18 ++++++++++++++++++ 3 files changed, 21 insertions(+), 16 deletions(-) diff --git a/packages/@tailwindcss-node/src/urls.ts b/packages/@tailwindcss-node/src/urls.ts index f033c606bb9b..c6009a927105 100644 --- a/packages/@tailwindcss-node/src/urls.ts +++ b/packages/@tailwindcss-node/src/urls.ts @@ -86,9 +86,7 @@ export async function rewriteUrls({ await Promise.all(promises) } - return toCss(ast, { - printUtilitiesNode: true, - }) + return toCss(ast) } function rewriteCssUrls(css: string, replacer: CssUrlReplacer): Promise { diff --git a/packages/tailwindcss/src/ast.ts b/packages/tailwindcss/src/ast.ts index b23cd7a695a0..7315bb09bfe0 100644 --- a/packages/tailwindcss/src/ast.ts +++ b/packages/tailwindcss/src/ast.ts @@ -202,7 +202,7 @@ export function walkDepth( } } -export function toCss(ast: AstNode[], { printUtilitiesNode = false } = {}) { +export function toCss(ast: AstNode[]) { let atRoots: string = '' let seenAtProperties = new Set() let propertyFallbacksRoot: Declaration[] = [] @@ -223,17 +223,6 @@ export function toCss(ast: AstNode[], { printUtilitiesNode = false } = {}) { // AtRule else if (node.kind === 'at-rule') { - if ( - !printUtilitiesNode && - node.name === '@tailwind' && - (node.params === 'utilities' || node.params.startsWith('utilities')) - ) { - for (let child of node.nodes) { - css += stringify(child, depth) - } - return css - } - // Print at-rules without nodes with a `;` instead of an empty block. // // E.g.: @@ -241,7 +230,7 @@ export function toCss(ast: AstNode[], { printUtilitiesNode = false } = {}) { // ```css // @layer base, components, utilities; // ``` - else if (node.nodes.length === 0) { + if (node.nodes.length === 0) { return `${indent}${node.name} ${node.params};\n` } diff --git a/packages/tailwindcss/src/index.ts b/packages/tailwindcss/src/index.ts index 402411861e48..c7ca4770504a 100644 --- a/packages/tailwindcss/src/index.ts +++ b/packages/tailwindcss/src/index.ts @@ -13,6 +13,7 @@ import { WalkAction, type AstNode, type AtRule, + type Context, type StyleRule, } from './ast' import { substituteAtImports } from './at-import' @@ -452,6 +453,23 @@ async function parseCss( firstThemeRule.nodes = nodes } + // Replace the `@tailwind utilities` node with a conext since it prints + // children directly. + if (utilitiesNode) { + let node = utilitiesNode as AstNode as Context + node.kind = 'context' + node.context = {} + + // Remove additional `@tailwind utilities` nodes + walk(ast, (node, { replaceWith }) => { + if (node.kind !== 'at-rule') return + if (node.name !== '@tailwind') return + if (!node.params.startsWith('utilities')) return + + replaceWith([]) + }) + } + // Replace `@apply` rules with the actual utility classes. substituteAtApply(ast, designSystem) From 5db1c388c0f3df4437fea3259911e9d51d72eba8 Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Wed, 6 Nov 2024 12:26:27 -0500 Subject: [PATCH 6/8] Update packages/tailwindcss/src/index.ts Co-authored-by: Philipp Spiess --- packages/tailwindcss/src/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/tailwindcss/src/index.ts b/packages/tailwindcss/src/index.ts index c7ca4770504a..77320a46fd50 100644 --- a/packages/tailwindcss/src/index.ts +++ b/packages/tailwindcss/src/index.ts @@ -453,7 +453,7 @@ async function parseCss( firstThemeRule.nodes = nodes } - // Replace the `@tailwind utilities` node with a conext since it prints + // Replace the `@tailwind utilities` node with a context since it prints // children directly. if (utilitiesNode) { let node = utilitiesNode as AstNode as Context From 0da9bda04ea36ab5478736937758e2ab50f96de7 Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Wed, 6 Nov 2024 12:42:18 -0500 Subject: [PATCH 7/8] Update CHANGELOG.md Co-authored-by: Philipp Spiess --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e3d88c7b5e94..226a9183bf5a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,7 +18,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Fix crash when using `@source` containing `..` ([#14831](https://github.com/tailwindlabs/tailwindcss/pull/14831)) - Ensure instances of the same variant with different values are always sorted deterministically (e.g. `data-focus:flex` and `data-active:flex`) ([#14835](https://github.com/tailwindlabs/tailwindcss/pull/14835)) - Ensure `--inset-ring=*` and `--inset-shadow-*` variables are ignored by `inset-*` utilities ([#14855](https://github.com/tailwindlabs/tailwindcss/pull/14855)) -- Rewrite urls in CSS files when using Vite ([#14877](https://github.com/tailwindlabs/tailwindcss/pull/14877)) +- Rebase `url()` inside imported CSS files when using Vite ([#14877](https://github.com/tailwindlabs/tailwindcss/pull/14877)) - _Upgrade (experimental)_: Install `@tailwindcss/postcss` next to `tailwindcss` ([#14830](https://github.com/tailwindlabs/tailwindcss/pull/14830)) - _Upgrade (experimental)_: Remove whitespace around `,` separator when print arbitrary values ([#14838](https://github.com/tailwindlabs/tailwindcss/pull/14838)) From ce0e72b371ab81c760e6f917097130541eef6d04 Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Thu, 7 Nov 2024 07:32:48 -0500 Subject: [PATCH 8/8] Perform removal in existing walk --- packages/tailwindcss/src/index.ts | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/packages/tailwindcss/src/index.ts b/packages/tailwindcss/src/index.ts index 77320a46fd50..40663f4883b8 100644 --- a/packages/tailwindcss/src/index.ts +++ b/packages/tailwindcss/src/index.ts @@ -101,10 +101,15 @@ async function parseCss( // Find `@tailwind utilities` so that we can later replace it with the // actual generated utility class CSS. if ( - utilitiesNode === null && node.name === '@tailwind' && (node.params === 'utilities' || node.params.startsWith('utilities')) ) { + // Any additional `@tailwind utilities` nodes can be removed + if (utilitiesNode !== null) { + replaceWith([]) + return + } + let params = segment(node.params, ' ') for (let param of params) { if (param.startsWith('source(')) { @@ -459,15 +464,6 @@ async function parseCss( let node = utilitiesNode as AstNode as Context node.kind = 'context' node.context = {} - - // Remove additional `@tailwind utilities` nodes - walk(ast, (node, { replaceWith }) => { - if (node.kind !== 'at-rule') return - if (node.name !== '@tailwind') return - if (!node.params.startsWith('utilities')) return - - replaceWith([]) - }) } // Replace `@apply` rules with the actual utility classes.