-
Notifications
You must be signed in to change notification settings - Fork 4.4k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Vite: Remove module-graph scanner (#16631)
Alternative to #16425 Fixes #16585 Fixes #16389 Fixes #16252 Fixes #15794 Fixes #16646 Fixes #16358 This PR changes the Vite plugin to use the file-system to discover potential class names instead of relying on the module-graph. This comes after a lot of testing and various issue reports where builds that span different Vite instances were missing class names. Because we now scan for candidates using the file-system, we can also remove a lot of the bookkeeping necessary to make production builds and development builds work as we no longer have to change the resulting stylesheet based on the `transform` callbacks of other files that might happen later. This change comes at a small performance penalty that is noticeable especially on very large projects with many files to scan. However, we offset that change by fixing an issue that I found in the current Vite integration that did a needless rebuild of the whole Tailwind root whenever any source file changed. Because of how impactful this change is, I expect many normal to medium sized projects to actually see a performance improvement after these changes. Furthermore we do plan to continue to use the module-graph to further improve the performance in dev mode. ## Test plan - Added new integration tests with cases found across the issues above. - Manual testing by adding a local version of the Vite plugin to repos from the issue list above and the [tailwindcss playgrounds](https://github.com/philipp-spiess/tailwindcss-playgrounds).
- Loading branch information
1 parent
b9af722
commit 88b762b
Showing
6 changed files
with
313 additions
and
351 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,178 @@ | ||
import { candidate, css, fetchStyles, json, retryAssertion, test, ts, txt } from '../utils' | ||
|
||
const WORKSPACE = { | ||
'package.json': json` | ||
{ | ||
"type": "module", | ||
"dependencies": { | ||
"@react-router/dev": "^7", | ||
"@react-router/node": "^7", | ||
"@react-router/serve": "^7", | ||
"@tailwindcss/vite": "workspace:^", | ||
"@types/node": "^20", | ||
"@types/react-dom": "^19", | ||
"@types/react": "^19", | ||
"isbot": "^5", | ||
"react-dom": "^19", | ||
"react-router": "^7", | ||
"react": "^19", | ||
"tailwindcss": "workspace:^", | ||
"vite": "^5" | ||
} | ||
} | ||
`, | ||
'react-router.config.ts': ts` | ||
import type { Config } from '@react-router/dev/config' | ||
export default { ssr: true } satisfies Config | ||
`, | ||
'vite.config.ts': ts` | ||
import { defineConfig } from 'vite' | ||
import { reactRouter } from '@react-router/dev/vite' | ||
import tailwindcss from '@tailwindcss/vite' | ||
export default defineConfig({ | ||
plugins: [tailwindcss(), reactRouter()], | ||
}) | ||
`, | ||
'app/routes/home.tsx': ts` | ||
export default function Home() { | ||
return <h1 className="font-bold">Welcome to React Router</h1> | ||
} | ||
`, | ||
'app/app.css': css`@import 'tailwindcss';`, | ||
'app/routes.ts': ts` | ||
import { type RouteConfig, index } from '@react-router/dev/routes' | ||
export default [index('routes/home.tsx')] satisfies RouteConfig | ||
`, | ||
'app/root.tsx': ts` | ||
import { Links, Meta, Outlet, Scripts, ScrollRestoration } from 'react-router' | ||
import './app.css' | ||
export function Layout({ children }: { children: React.ReactNode }) { | ||
return ( | ||
<html lang="en"> | ||
<head> | ||
<meta charSet="utf-8" /> | ||
<meta name="viewport" content="width=device-width, initial-scale=1" /> | ||
<Meta /> | ||
<Links /> | ||
</head> | ||
<body> | ||
{children} | ||
<ScrollRestoration /> | ||
<Scripts /> | ||
</body> | ||
</html> | ||
) | ||
} | ||
export default function App() { | ||
return <Outlet /> | ||
} | ||
`, | ||
} | ||
|
||
test('dev mode', { fs: WORKSPACE }, async ({ fs, spawn, expect }) => { | ||
let process = await spawn('pnpm react-router dev') | ||
|
||
let url = '' | ||
await process.onStdout((m) => { | ||
let match = /Local:\s*(http.*)\//.exec(m) | ||
if (match) url = match[1] | ||
return Boolean(url) | ||
}) | ||
|
||
await retryAssertion(async () => { | ||
let css = await fetchStyles(url) | ||
expect(css).toContain(candidate`font-bold`) | ||
}) | ||
|
||
await retryAssertion(async () => { | ||
await fs.write( | ||
'app/routes/home.tsx', | ||
ts` | ||
export default function Home() { | ||
return <h1 className="font-bold underline">Welcome to React Router</h1> | ||
} | ||
`, | ||
) | ||
|
||
let css = await fetchStyles(url) | ||
expect(css).toContain(candidate`underline`) | ||
expect(css).toContain(candidate`font-bold`) | ||
}) | ||
}) | ||
|
||
test('build mode', { fs: WORKSPACE }, async ({ spawn, exec, expect }) => { | ||
await exec('pnpm react-router build') | ||
let process = await spawn('pnpm react-router-serve ./build/server/index.js') | ||
|
||
let url = '' | ||
await process.onStdout((m) => { | ||
let match = /\[react-router-serve\]\s*(http.*)\ \/?/.exec(m) | ||
if (match) url = match[1] | ||
return url != '' | ||
}) | ||
|
||
await retryAssertion(async () => { | ||
let css = await fetchStyles(url) | ||
expect(css).toContain(candidate`font-bold`) | ||
}) | ||
}) | ||
|
||
test( | ||
'build mode using ?url stylesheet imports should only build one stylesheet (requires `file-system` scanner)', | ||
{ | ||
fs: { | ||
...WORKSPACE, | ||
'app/root.tsx': ts` | ||
import { Links, Meta, Outlet, Scripts, ScrollRestoration } from 'react-router' | ||
import styles from './app.css?url' | ||
export const links: Route.LinksFunction = () => [{ rel: 'stylesheet', href: styles }] | ||
export function Layout({ children }: { children: React.ReactNode }) { | ||
return ( | ||
<html lang="en"> | ||
<head> | ||
<meta charSet="utf-8" /> | ||
<meta name="viewport" content="width=device-width, initial-scale=1" /> | ||
<Meta /> | ||
<Links /> | ||
</head> | ||
<body class="dark"> | ||
{children} | ||
<ScrollRestoration /> | ||
<Scripts /> | ||
</body> | ||
</html> | ||
) | ||
} | ||
export default function App() { | ||
return <Outlet /> | ||
} | ||
`, | ||
'vite.config.ts': ts` | ||
import { defineConfig } from 'vite' | ||
import { reactRouter } from '@react-router/dev/vite' | ||
import tailwindcss from '@tailwindcss/vite' | ||
export default defineConfig({ | ||
plugins: [tailwindcss(), reactRouter()], | ||
}) | ||
`, | ||
'.gitignore': txt` | ||
node_modules/ | ||
build/ | ||
`, | ||
}, | ||
}, | ||
async ({ fs, exec, expect }) => { | ||
await exec('pnpm react-router build') | ||
|
||
let files = await fs.glob('build/client/assets/**/*.css') | ||
|
||
expect(files).toHaveLength(1) | ||
let [filename] = files[0] | ||
|
||
await fs.expectFileToContain(filename, [candidate`font-bold`]) | ||
}, | ||
) |
Oops, something went wrong.