Vite: Fix issues when loading files via static asset queries (#14716)

Fixes: #14558

This PR fixes an issue where our Vite plugin would crash when trying to load stylesheets via certain static asset query parameters:

```ts
import raw from './style.css?raw'
import url from './style.css?url'
```

The proper behavior for our extension is to _not touch these file at all_. The `?raw` identifier should never transform anything and the `?url` one will emit a module which points to the asset URL. However, if that URL is loaded as a stylesheet, another transform hook is called and the file is properly transformed. I verified this in the Vite setup and have added an integration test ensuring these two features work as expected.

I've also greatly reduced the complexity of the Vite playground to make it easier to set up examples like this in the future.
This commit is contained in:
philipp-spiess 2024-10-22 16:03:07 +00:00
parent 557ed8ccec
commit d59f1b3e5d
15 changed files with 75 additions and 88 deletions

View file

@ -18,6 +18,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Ensure color opacity modifiers work with OKLCH colors ([#14741](https://github.com/tailwindlabs/tailwindcss/pull/14741))
- Ensure changes to the input CSS file result in a full rebuild ([#14744](https://github.com/tailwindlabs/tailwindcss/pull/14744))
- Add `postcss` as a dependency of `@tailwindcss/postcss` ([#14750](https://github.com/tailwindlabs/tailwindcss/pull/14750))
- Ensure loading stylesheets via the `?raw` and `?url` static asset query works when using the Vite plugin ([#14716](https://github.com/tailwindlabs/tailwindcss/pull/14716))
- _Upgrade (experimental)_: Migrate `flex-grow` to `grow` and `flex-shrink` to `shrink` ([#14721](https://github.com/tailwindlabs/tailwindcss/pull/14721))
- _Upgrade (experimental)_: Minify arbitrary values when printing candidates ([#14720](https://github.com/tailwindlabs/tailwindcss/pull/14720))
- _Upgrade (experimental)_: Ensure legacy theme values ending in `1` (like `theme(spacing.1)`) are correctly migrated to custom properties ([#14724](https://github.com/tailwindlabs/tailwindcss/pull/14724))

View file

@ -504,3 +504,64 @@ test(
})
},
)
test(
`does not interfere with ?raw and ?url static asset handling`,
{
fs: {
'package.json': json`
{
"type": "module",
"dependencies": {
"@tailwindcss/vite": "workspace:^",
"tailwindcss": "workspace:^"
},
"devDependencies": {
"vite": "^5.3.5"
}
}
`,
'vite.config.ts': ts`
import tailwindcss from '@tailwindcss/vite'
import { defineConfig } from 'vite'
export default defineConfig({
build: { cssMinify: false },
plugins: [tailwindcss()],
})
`,
'index.html': html`
<head>
<script type="module" src="./src/index.js"></script>
</head>
`,
'src/index.js': js`
import url from './index.css?url'
import raw from './index.css?raw'
`,
'src/index.css': css`@import 'tailwindcss';`,
},
},
async ({ spawn, getFreePort }) => {
let port = await getFreePort()
await spawn(`pnpm vite dev --port ${port}`)
await retryAssertion(async () => {
// We have to load the .js file first so that the static assets are
// resolved
await fetch(`http://localhost:${port}/src/index.js`).then((r) => r.text())
let [raw, url] = await Promise.all([
fetch(`http://localhost:${port}/src/index.css?raw`).then((r) => r.text()),
fetch(`http://localhost:${port}/src/index.css?url`).then((r) => r.text()),
])
expect(firstLine(raw)).toBe(`export default "@import 'tailwindcss';"`)
expect(firstLine(url)).toBe(`export default "/src/index.css"`)
})
},
)
function firstLine(str: string) {
return str.split('\n')[0]
}

View file

@ -5,6 +5,8 @@ import { Features, transform } from 'lightningcss'
import path from 'path'
import type { Plugin, ResolvedConfig, Rollup, Update, ViteDevServer } from 'vite'
const SPECIAL_QUERY_RE = /[?&](raw|url)\b/
export default function tailwindcss(): Plugin[] {
let servers: ViteDevServer[] = []
let config: ResolvedConfig | null = null
@ -261,9 +263,12 @@ function getExtension(id: string) {
function isPotentialCssRootFile(id: string) {
let extension = getExtension(id)
let isCssFile =
extension === 'css' ||
(extension === 'vue' && id.includes('&lang.css')) ||
(extension === 'astro' && id.includes('&lang.css'))
(extension === 'css' ||
(extension === 'vue' && id.includes('&lang.css')) ||
(extension === 'astro' && id.includes('&lang.css'))) &&
// Don't intercept special static asset resources
!SPECIAL_QUERY_RE.test(id)
return isCssFile
}

View file

@ -19,7 +19,6 @@
"@types/react": "^18.3.9",
"@types/react-dom": "^18.3.1",
"bun": "^1.1.29",
"vite": "catalog:",
"vite-plugin-handlebars": "^2.0.0"
"vite": "catalog:"
}
}

View file

@ -1 +0,0 @@
module.exports = require('tailwindcss-animate')

View file

@ -1,11 +1,7 @@
import { Foo } from './foo'
export function App() {
return (
<div className="m-3 p-3 border">
<h1 className="text-blue-500">Hello World</h1>
<button className="hocus:underline">Click me</button>
<Foo />
</div>
)
}

View file

@ -1,7 +0,0 @@
export function Bar() {
return (
<div>
<h2 className="text-red-500 underline">Bar</h2>
</div>
)
}

View file

@ -1,10 +0,0 @@
import { Bar } from './bar'
export function Foo() {
return (
<div>
<h2 className="text-blue-500">Foo</h2>
<Bar />
</div>
)
}

View file

@ -1 +0,0 @@
module.exports = require('@tailwindcss/forms')

View file

@ -1,2 +1 @@
@import 'tailwindcss';
@plugin "./plugin.js";

View file

@ -4,7 +4,6 @@
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>≈ Playground</title>
<link rel="stylesheet" href="./app.css" />
</head>
<body class="h-full">
<div id="app"></div>

View file

@ -2,6 +2,8 @@ import React from 'react'
import ReactDOM from 'react-dom/client'
import { App } from './app'
import './index.css'
ReactDOM.createRoot(document.getElementById('app')!).render(
<React.StrictMode>
<App />

View file

@ -1,4 +0,0 @@
module.exports = function ({ addVariant }) {
addVariant('inverted', '@media (inverted-colors: inverted)')
addVariant('hocus', ['&:focus', '&:hover'])
}

View file

@ -1 +0,0 @@
module.exports = require('@tailwindcss/typography')

55
pnpm-lock.yaml generated
View file

@ -435,9 +435,6 @@ importers:
vite:
specifier: 'catalog:'
version: 5.4.0(@types/node@20.14.13)(lightningcss@1.26.0(patch_hash=5hwfyehqvg5wjb7mwtdvubqbl4))(terser@5.31.6)
vite-plugin-handlebars:
specifier: ^2.0.0
version: 2.0.0(@types/node@20.14.13)(lightningcss@1.26.0(patch_hash=5hwfyehqvg5wjb7mwtdvubqbl4))(terser@5.31.6)
packages:
@ -1941,11 +1938,6 @@ packages:
graceful-fs@4.2.11:
resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==}
handlebars@4.7.8:
resolution: {integrity: sha512-vafaFqs8MZkRrSX7sFVUdo3ap/eNiLnb4IakshzvP56X5Nr1iGKAIqdX6tMlm6HcNRIkr6AxO5jFEoJzzpT8aQ==}
engines: {node: '>=0.4.7'}
hasBin: true
has-bigints@1.0.2:
resolution: {integrity: sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==}
@ -2352,9 +2344,6 @@ packages:
natural-compare@1.4.0:
resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==}
neo-async@2.6.2:
resolution: {integrity: sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==}
next@14.1.0:
resolution: {integrity: sha512-wlzrsbfeSU48YQBjZhDzOwhWhGsy+uQycR8bHAOt1LY1bn3zZEcDyHQOEoN3aWzQ8LHCAJ1nqrWCc9XF2+O45Q==}
engines: {node: '>=18.17.0'}
@ -3007,11 +2996,6 @@ packages:
engines: {node: '>=14.17'}
hasBin: true
uglify-js@3.19.1:
resolution: {integrity: sha512-y/2wiW+ceTYR2TSSptAhfnEtpLaQ4Ups5zrjB2d3kuVxHj16j/QJwPl5PvuGy9uARb39J0+iKxcRPvtpsx4A4A==}
engines: {node: '>=0.8.0'}
hasBin: true
unbox-primitive@1.0.2:
resolution: {integrity: sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==}
@ -3039,9 +3023,6 @@ packages:
engines: {node: ^18.0.0 || >=20.0.0}
hasBin: true
vite-plugin-handlebars@2.0.0:
resolution: {integrity: sha512-+J3It0nyhPzx4nT1I+fnWH+jShTEXzm6X0Tgsggdm9IYFD7/eJ6a3ROI13HTe0CVoyaxm/fPxH5HDAKyfz7T0g==}
vite@5.4.0:
resolution: {integrity: sha512-5xokfMX0PIiwCMCMb9ZJcMyh5wbBun0zUzKib+L65vAZ8GY9ePZMXxFrHbr/Kyll2+LSCY7xtERPpxkBDKngwg==}
engines: {node: ^18.0.0 || >=20.0.0}
@ -3133,9 +3114,6 @@ packages:
resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==}
engines: {node: '>=0.10.0'}
wordwrap@1.0.0:
resolution: {integrity: sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==}
wrap-ansi@7.0.0:
resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==}
engines: {node: '>=10'}
@ -4739,15 +4717,6 @@ snapshots:
graceful-fs@4.2.11: {}
handlebars@4.7.8:
dependencies:
minimist: 1.2.8
neo-async: 2.6.2
source-map: 0.6.1
wordwrap: 1.0.0
optionalDependencies:
uglify-js: 3.19.1
has-bigints@1.0.2: {}
has-flag@3.0.0: {}
@ -5088,8 +5057,6 @@ snapshots:
natural-compare@1.4.0: {}
neo-async@2.6.2: {}
next@14.1.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1):
dependencies:
'@next/env': 14.1.0
@ -5486,7 +5453,8 @@ snapshots:
source-map: 0.6.1
optional: true
source-map@0.6.1: {}
source-map@0.6.1:
optional: true
source-map@0.8.0-beta.0:
dependencies:
@ -5761,9 +5729,6 @@ snapshots:
typescript@5.5.4: {}
uglify-js@3.19.1:
optional: true
unbox-primitive@1.0.2:
dependencies:
call-bind: 1.0.7
@ -5805,20 +5770,6 @@ snapshots:
- supports-color
- terser
vite-plugin-handlebars@2.0.0(@types/node@20.14.13)(lightningcss@1.26.0(patch_hash=5hwfyehqvg5wjb7mwtdvubqbl4))(terser@5.31.6):
dependencies:
handlebars: 4.7.8
vite: 5.4.0(@types/node@20.14.13)(lightningcss@1.26.0(patch_hash=5hwfyehqvg5wjb7mwtdvubqbl4))(terser@5.31.6)
transitivePeerDependencies:
- '@types/node'
- less
- lightningcss
- sass
- sass-embedded
- stylus
- sugarss
- terser
vite@5.4.0(@types/node@20.14.13)(lightningcss@1.26.0(patch_hash=5hwfyehqvg5wjb7mwtdvubqbl4))(terser@5.31.6):
dependencies:
esbuild: 0.21.5
@ -5920,8 +5871,6 @@ snapshots:
word-wrap@1.2.5: {}
wordwrap@1.0.0: {}
wrap-ansi@7.0.0:
dependencies:
ansi-styles: 4.3.0