mirror of
https://github.com/tailwindlabs/tailwindcss.git
synced 2025-12-08 21:36:08 +00:00
Rewrite urls in CSS files when using Vite (#14877)
Fixes #14784 This is an alternative to #14850 in which we actually perform url rewriting / rebasing ourselves. We ported a large portion of the URL-rewriting code from Vite (with attribution) to use here with some minor modifications. We've added test cases for the url rewriting so verifying individual cases is easy. We also wrote integration tests for Vite that use PostCSS and Lightning CSS that verify that files are found and inlined or relocated/renamed as necessary. We also did some manual testing in the Playground to verify that this works as expected across several CSS files and directories which you can see a screenshot from here: <img width="1344" alt="Screenshot 2024-11-05 at 10 25 16" src="https://github.com/user-attachments/assets/ff0b3ac8-cdc9-4e26-af79-36396a5b77b9"> --------- Co-authored-by: Philipp Spiess <hello@philippspiess.com>
This commit is contained in:
parent
75eeed85b6
commit
e82b316c61
@ -27,6 +27,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
- Ensure `--inset-ring=*` and `--inset-shadow-*` variables are ignored by `inset-*` utilities ([#14855](https://github.com/tailwindlabs/tailwindcss/pull/14855))
|
||||
- Ensure `url(…)` containing special characters such as `;` or `{}` end up in one declaration ([#14879](https://github.com/tailwindlabs/tailwindcss/pull/14879))
|
||||
- Ensure adjacent rules are merged together after handling nesting when generating optimized CSS ([#14873](https://github.com/tailwindlabs/tailwindcss/pull/14873))
|
||||
- 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))
|
||||
- _Upgrade (experimental)_: Fix crash during upgrade when content globs escape root of project ([#14896](https://github.com/tailwindlabs/tailwindcss/pull/14896))
|
||||
|
||||
@ -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<void> {
|
||||
async write(filename: string, content: string | Uint8Array): Promise<void> {
|
||||
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')
|
||||
},
|
||||
|
||||
@ -487,6 +491,7 @@ function testIfPortTaken(port: number): Promise<boolean> {
|
||||
})
|
||||
}
|
||||
|
||||
export let svg = dedent
|
||||
export let css = dedent
|
||||
export let html = dedent
|
||||
export let ts = dedent
|
||||
@ -495,6 +500,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++) {
|
||||
|
||||
97
integrations/vite/url-rewriting.test.ts
Normal file
97
integrations/vite/url-rewriting.test.ts
Normal file
@ -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`
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<link rel="stylesheet" href="./src/app.css" />
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="./src/main.ts"></script>
|
||||
</body>
|
||||
</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`
|
||||
<svg width="400" height="400" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="100%" height="100%" fill="red" />
|
||||
<circle cx="200" cy="100" r="80" fill="green" />
|
||||
<rect width="100%" height="100%" fill="red" />
|
||||
<circle cx="200" cy="100" r="80" fill="green" />
|
||||
</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/])
|
||||
},
|
||||
)
|
||||
})
|
||||
}
|
||||
@ -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
|
||||
},
|
||||
})
|
||||
|
||||
|
||||
127
packages/@tailwindcss-node/src/urls.test.ts
Normal file
127
packages/@tailwindcss-node/src/urls.test.ts
Normal file
@ -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");
|
||||
}
|
||||
"
|
||||
`)
|
||||
})
|
||||
201
packages/@tailwindcss-node/src/urls.ts
Normal file
201
packages/@tailwindcss-node/src/urls.ts
Normal file
@ -0,0 +1,201 @@
|
||||
// Inlined version of code from Vite <https://github.com/vitejs/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 =
|
||||
/(?<!@import\s+)(?<=^|[^\w\-\u0080-\uffff])url\((\s*('[^']+'|"[^"]+")\s*|[^'")]+)\)/
|
||||
const cssImageSetRE = /(?<=image-set\()((?:[\w-]{1,256}\([^)]*\)|[^)])*)(?=\))/
|
||||
const cssNotProcessedRE = /(?:gradient|element|cross-fade|image)\(/
|
||||
|
||||
const dataUrlRE = /^\s*data:/i
|
||||
const externalRE = /^([a-z]+:)?\/\//
|
||||
const functionCallRE = /^[A-Z_][.\w-]*\(/i
|
||||
|
||||
const imageCandidateRE =
|
||||
/(?:^|\s)(?<url>[\w-]+\([^)]*\)|"[^"]*"|'[^']*'|[^,]\S*[^,])\s*(?:\s(?<descriptor>\w[^,]+))?(?:,|$)/g
|
||||
const nonEscapedDoubleQuoteRE = /(?<!\\)"/g
|
||||
const escapedSpaceCharactersRE = /(?: |\\t|\\n|\\f|\\r)+/g
|
||||
|
||||
const isDataUrl = (url: string): boolean => dataUrlRE.test(url)
|
||||
const isExternalUrl = (url: string): boolean => externalRE.test(url)
|
||||
|
||||
type CssUrlReplacer = (url: string, importer?: string) => string | Promise<string>
|
||||
|
||||
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<void>[] = []
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
function rewriteCssUrls(css: string, replacer: CssUrlReplacer): Promise<string> {
|
||||
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<string> {
|
||||
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<string>,
|
||||
): Promise<string> {
|
||||
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<string>,
|
||||
): Promise<string> {
|
||||
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
|
||||
}
|
||||
@ -381,6 +381,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)
|
||||
|
||||
@ -223,16 +223,6 @@ export function toCss(ast: AstNode[]) {
|
||||
|
||||
// AtRule
|
||||
else if (node.kind === 'at-rule') {
|
||||
if (
|
||||
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.:
|
||||
@ -240,7 +230,7 @@ export function toCss(ast: AstNode[]) {
|
||||
// ```css
|
||||
// @layer base, components, utilities;
|
||||
// ```
|
||||
else if (node.nodes.length === 0) {
|
||||
if (node.nodes.length === 0) {
|
||||
return `${indent}${node.name} ${node.params};\n`
|
||||
}
|
||||
|
||||
|
||||
@ -13,6 +13,7 @@ import {
|
||||
WalkAction,
|
||||
type AstNode,
|
||||
type AtRule,
|
||||
type Context,
|
||||
type StyleRule,
|
||||
} from './ast'
|
||||
import { substituteAtImports } from './at-import'
|
||||
@ -100,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(')) {
|
||||
@ -452,6 +458,14 @@ async function parseCss(
|
||||
firstThemeRule.nodes = nodes
|
||||
}
|
||||
|
||||
// Replace the `@tailwind utilities` node with a context since it prints
|
||||
// children directly.
|
||||
if (utilitiesNode) {
|
||||
let node = utilitiesNode as AstNode as Context
|
||||
node.kind = 'context'
|
||||
node.context = {}
|
||||
}
|
||||
|
||||
// Replace `@apply` rules with the actual utility classes.
|
||||
substituteAtApply(ast, designSystem)
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user