Skip to content
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down
19 changes: 15 additions & 4 deletions integrations/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ interface ExecOptions {

interface TestConfig {
fs: {
[filePath: string]: string
[filePath: string]: string | Uint8Array
}
}
interface TestContext {
Expand Down Expand Up @@ -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)
Expand All @@ -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')
},

Expand Down Expand Up @@ -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
Expand All @@ -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++) {
Expand Down
97 changes: 97 additions & 0 deletions integrations/vite/url-rewriting.test.ts
Original file line number Diff line number Diff line change
@@ -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/])
},
)
})
}
23 changes: 21 additions & 2 deletions packages/@tailwindcss-node/src/compile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,18 +9,37 @@ 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,
async loadModule(id, base) {
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
},
})

Expand Down
127 changes: 127 additions & 0 deletions packages/@tailwindcss-node/src/urls.test.ts
Original file line number Diff line number Diff line change
@@ -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");
}
"
`)
})
Loading