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).
This commit is contained in:
Philipp Spiess 2025-02-20 15:23:44 +01:00 committed by GitHub
parent b9af722d13
commit 88b762b539
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 314 additions and 352 deletions

View File

@ -14,6 +14,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Fixed
- Remove invalid `!important` on CSS variable declarations ([#16668](https://github.com/tailwindlabs/tailwindcss/pull/16668))
- Vite: Automatic source detection now ignores files and directories specified in your `.gitignore` file ([#16631](https://github.com/tailwindlabs/tailwindcss/pull/16631))
- Vite: Ensure setups with multiple Vite builds work as expected ([#16631](https://github.com/tailwindlabs/tailwindcss/pull/16631))
- Vite: Ensure Astro production builds contain classes for client-only components ([#16631](https://github.com/tailwindlabs/tailwindcss/pull/16631))
- Vite: Ensure utility classes are read without escaping special characters ([#16631](https://github.com/tailwindlabs/tailwindcss/pull/16631))
## [4.0.7] - 2025-02-18

View File

@ -1,4 +1,4 @@
import { candidate, fetchStyles, html, json, retryAssertion, test, ts } from '../utils'
import { candidate, fetchStyles, html, js, json, retryAssertion, test, ts } from '../utils'
test(
'dev mode',
@ -19,11 +19,7 @@ test(
import { defineConfig } from 'astro/config'
// https://astro.build/config
export default defineConfig({
vite: {
plugins: [tailwindcss()],
},
})
export default defineConfig({ vite: { plugins: [tailwindcss()] } })
`,
'src/pages/index.astro': html`
<div class="underline">Hello, world!</div>
@ -70,3 +66,58 @@ test(
})
},
)
test(
'build mode',
{
fs: {
'package.json': json`
{
"type": "module",
"dependencies": {
"astro": "^4.15.2",
"react": "^19",
"react-dom": "^19",
"@astrojs/react": "^4",
"@tailwindcss/vite": "workspace:^",
"tailwindcss": "workspace:^"
}
}
`,
'astro.config.mjs': ts`
import tailwindcss from '@tailwindcss/vite'
import react from '@astrojs/react'
import { defineConfig } from 'astro/config'
// https://astro.build/config
export default defineConfig({ vite: { plugins: [tailwindcss()] }, integrations: [react()] })
`,
'src/pages/index.astro': html`
---
import ClientOnly from './client-only';
---
<div class="underline">Hello, world!</div>
<ClientOnly client:only="react" />
<style is:global>
@import 'tailwindcss';
</style>
`,
'src/pages/client-only.jsx': js`
export default function ClientOnly() {
return <div className="overline">Hello, world!</div>
}
`,
},
},
async ({ fs, exec, expect }) => {
await exec('pnpm astro build')
let files = await fs.glob('dist/**/*.css')
expect(files).toHaveLength(1)
await fs.expectFileToContain(files[0][0], [candidate`underline`, candidate`overline`])
},
)

View File

@ -174,21 +174,10 @@ describe.each(['postcss', 'lightningcss'])('%s', (transformer) => {
return Boolean(url)
})
// Candidates are resolved lazily, so the first visit of index.html
// will only have candidates from this file.
await retryAssertion(async () => {
let styles = await fetchStyles(url, '/index.html')
expect(styles).toContain(candidate`underline`)
expect(styles).toContain(candidate`flex`)
expect(styles).not.toContain(candidate`font-bold`)
})
// Going to about.html will extend the candidate list to include
// candidates from about.html.
await retryAssertion(async () => {
let styles = await fetchStyles(url, '/about.html')
expect(styles).toContain(candidate`underline`)
expect(styles).toContain(candidate`flex`)
expect(styles).toContain(candidate`font-bold`)
})
@ -696,7 +685,7 @@ describe.each(['postcss', 'lightningcss'])('%s', (transformer) => {
})
test(
`demote Tailwind roots to regular CSS files and back to Tailwind roots while restoring all candidates`,
`demote Tailwind roots to regular CSS files and back to Tailwind roots`,
{
fs: {
'package.json': json`
@ -750,19 +739,9 @@ test(
return Boolean(url)
})
// Candidates are resolved lazily, so the first visit of index.html
// will only have candidates from this file.
await retryAssertion(async () => {
let styles = await fetchStyles(url, '/index.html')
expect(styles).toContain(candidate`underline`)
expect(styles).not.toContain(candidate`font-bold`)
})
// Going to about.html will extend the candidate list to include
// candidates from about.html.
await retryAssertion(async () => {
let styles = await fetchStyles(url, '/about.html')
expect(styles).toContain(candidate`underline`)
expect(styles).toContain(candidate`font-bold`)
})

View File

@ -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`])
},
)

View File

@ -1,5 +1,30 @@
import { candidate, css, html, json, test, ts } from '../utils'
const WORKSPACE = {
'index.html': html`
<body>
<div id="app"></div>
<script type="module" src="./src/index.ts"></script>
</body>
`,
'src/index.css': css`@import 'tailwindcss';`,
'src/index.ts': ts`
import './index.css'
document.querySelector('#app').innerHTML = \`
<div class="underline m-2">Hello, world!</div>
\`
`,
'server.ts': ts`
import css from './src/index.css?url'
document.querySelector('#app').innerHTML = \`
<link rel="stylesheet" href="\${css}">
<div class="overline m-3">Hello, world!</div>
\`
`,
}
test(
'Vite 5',
{
@ -27,31 +52,9 @@ test(
ssrEmitAssets: true,
},
plugins: [tailwindcss()],
ssr: { resolve: { conditions: [] } },
})
`,
'index.html': html`
<body>
<div id="app"></div>
<script type="module" src="./src/index.ts"></script>
</body>
`,
'src/index.css': css`@import 'tailwindcss';`,
'src/index.ts': ts`
import './index.css'
document.querySelector('#app').innerHTML = \`
<div class="underline m-2">Hello, world!</div>
\`
`,
'server.ts': ts`
import css from './src/index.css?url'
document.querySelector('#app').innerHTML = \`
<link rel="stylesheet" href="\${css}">
<div class="underline m-2">Hello, world!</div>
\`
`,
...WORKSPACE,
},
},
async ({ fs, exec, expect }) => {
@ -62,9 +65,10 @@ test(
let [filename] = files[0]
await fs.expectFileToContain(filename, [
//
candidate`underline`,
candidate`m-2`,
candidate`overline`,
candidate`m-3`,
])
},
)
@ -95,31 +99,9 @@ test(
ssrEmitAssets: true,
},
plugins: [tailwindcss()],
ssr: { resolve: { conditions: [] } },
})
`,
'index.html': html`
<body>
<div id="app"></div>
<script type="module" src="./src/index.ts"></script>
</body>
`,
'src/index.css': css`@import 'tailwindcss';`,
'src/index.ts': ts`
import './index.css'
document.querySelector('#app').innerHTML = \`
<div class="underline m-2">Hello, world!</div>
\`
`,
'server.ts': ts`
import css from './src/index.css?url'
document.querySelector('#app').innerHTML = \`
<link rel="stylesheet" href="\${css}">
<div class="underline m-2">Hello, world!</div>
\`
`,
...WORKSPACE,
},
},
async ({ fs, exec, expect }) => {
@ -130,9 +112,10 @@ test(
let [filename] = files[0]
await fs.expectFileToContain(filename, [
//
candidate`underline`,
candidate`m-2`,
candidate`overline`,
candidate`m-3`,
])
},
)

View File

@ -4,15 +4,13 @@ import { Scanner } from '@tailwindcss/oxide'
import { Features as LightningCssFeatures, transform } from 'lightningcss'
import fs from 'node:fs/promises'
import path from 'node:path'
import type { Plugin, ResolvedConfig, Rollup, Update, ViteDevServer } from 'vite'
import type { Plugin, ResolvedConfig, ViteDevServer } from 'vite'
const DEBUG = env.DEBUG
const SPECIAL_QUERY_RE = /[?&](?:worker|sharedworker|raw|url)\b/
const COMMON_JS_PROXY_RE = /\?commonjs-proxy/
const INLINE_STYLE_ID_RE = /[?&]index\=\d+\.css$/
const IGNORED_DEPENDENCIES = ['tailwind-merge']
export default function tailwindcss(): Plugin[] {
let servers: ViteDevServer[] = []
let config: ResolvedConfig | null = null
@ -20,25 +18,6 @@ export default function tailwindcss(): Plugin[] {
let isSSR = false
let minify = false
// The Vite extension has two types of sources for candidates:
//
// 1. The module graph: These are all modules that vite transforms and we want
// them to be automatically scanned for candidates.
// 2. Root defined `@source`s
//
// Module graph candidates are global to the Vite extension since we do not
// know which CSS roots will be used for the modules. We are using a custom
// scanner instance with auto source discovery disabled to parse these.
//
// For candidates coming from custom `@source` directives of the CSS roots, we
// create an individual scanner for each root.
//
// Note: To improve performance, we do not remove candidates from this set.
// This means a longer-ongoing dev mode session might contain candidates that
// are no longer referenced in code.
let moduleGraphCandidates = new DefaultMap<string, Set<string>>(() => new Set<string>())
let moduleGraphScanner = new Scanner({})
let roots: DefaultMap<string, Root> = new DefaultMap((id) => {
let cssResolver = config!.createResolver({
...config!.resolve,
@ -56,133 +35,9 @@ export default function tailwindcss(): Plugin[] {
function customJsResolver(id: string, base: string) {
return jsResolver(id, base, true, isSSR)
}
return new Root(
id,
() => moduleGraphCandidates,
config!.base,
customCssResolver,
customJsResolver,
)
return new Root(id, config!.root, customCssResolver, customJsResolver)
})
function scanFile(id: string, content: string, extension: string) {
for (let dependency of IGNORED_DEPENDENCIES) {
// We validated that Vite IDs always use posix style path separators, even on Windows.
// In dev build, Vite precompiles dependencies
if (id.includes(`.vite/deps/${dependency}.js`)) {
return
}
// In prod builds, use the node_modules path
if (id.includes(`/node_modules/${dependency}/`)) {
return
}
}
let updated = false
for (let candidate of moduleGraphScanner.scanFiles([{ content, extension }])) {
updated = true
moduleGraphCandidates.get(id).add(candidate)
}
if (updated) {
invalidateAllRoots()
}
}
function invalidateAllRoots() {
for (let server of servers) {
let updates: Update[] = []
for (let [id] of roots.entries()) {
let module = server.moduleGraph.getModuleById(id)
if (!module) continue
roots.get(id).requiresRebuild = false
server.moduleGraph.invalidateModule(module)
updates.push({
type: `${module.type}-update`,
path: module.url,
acceptedPath: module.url,
timestamp: Date.now(),
})
}
if (updates.length > 0) {
server.hot.send({ type: 'update', updates })
}
}
}
async function regenerateOptimizedCss(
root: Root,
addWatchFile: (file: string) => void,
I: Instrumentation,
) {
let content = root.lastContent
let generated = await root.generate(content, addWatchFile, I)
if (generated === false) {
return
}
DEBUG && I.start('Optimize CSS')
let result = optimizeCss(generated, { minify })
DEBUG && I.end('Optimize CSS')
return result
}
// Manually run the transform functions of non-Tailwind plugins on the given CSS
async function transformWithPlugins(context: Rollup.PluginContext, id: string, css: string) {
let transformPluginContext = {
...context,
getCombinedSourcemap: () => {
throw new Error('getCombinedSourcemap not implemented')
},
}
for (let plugin of config!.plugins) {
if (!plugin.transform) continue
if (plugin.name.startsWith('@tailwindcss/')) {
// We do not run any Tailwind transforms anymore
continue
} else if (
plugin.name.startsWith('vite:') &&
// Apply the vite:css plugin to generated CSS for transformations like
// URL path rewriting and image inlining.
plugin.name !== 'vite:css' &&
// In build mode, since `renderStart` runs after all transformations, we
// need to also apply vite:css-post.
plugin.name !== 'vite:css-post' &&
// The vite:vue plugin handles CSS specific post-processing for Vue
plugin.name !== 'vite:vue'
) {
continue
} else if (plugin.name === 'ssr-styles') {
// The Nuxt ssr-styles plugin emits styles from server-side rendered
// components, we can't run it in the `renderStart` phase so we're
// skipping it.
continue
}
let transformHandler =
'handler' in plugin.transform! ? plugin.transform.handler : plugin.transform!
try {
// Directly call the plugin's transform function to process the
// generated CSS. In build mode, this updates the chunks later used to
// generate the bundle. In serve mode, the transformed source should be
// applied in transform.
let result = await transformHandler.call(transformPluginContext, css, id)
if (!result) continue
if (typeof result === 'string') {
css = result
} else if (result.code) {
css = result.code
}
} catch (e) {
console.error(`Error running ${plugin.name} on Tailwind CSS output. Skipping.`)
}
}
return css
}
return [
{
// Step 1: Scan source files for candidates
@ -198,19 +53,6 @@ export default function tailwindcss(): Plugin[] {
minify = config.build.cssMinify !== false
isSSR = config.build.ssr !== false && config.build.ssr !== undefined
},
// Scan all non-CSS files for candidates
transformIndexHtml(html, { path }) {
// SolidStart emits HTML chunks with an undefined path and the html content of `\`.
if (!path) return
scanFile(path, html, 'html')
},
transform(src, id, options) {
let extension = getExtension(id)
if (isPotentialCssRootFile(id)) return
scanFile(id, src, extension)
},
},
{
@ -223,26 +65,17 @@ export default function tailwindcss(): Plugin[] {
if (!isPotentialCssRootFile(id)) return
using I = new Instrumentation()
I.start('[@tailwindcss/vite] Generate CSS (serve)')
DEBUG && I.start('[@tailwindcss/vite] Generate CSS (serve)')
let root = roots.get(id)
if (!options?.ssr) {
// Wait until all other files have been processed, so we can extract
// all candidates before generating CSS. This must not be called
// during SSR or it will block the server.
//
// The reason why we can not rely on the invalidation here is that the
// users would otherwise see a flicker in the styles as the CSS might
// be loaded with an invalid set of candidates first.
await Promise.all(servers.map((server) => server.waitForRequestsIdle(id)))
}
let generated = await root.generate(src, (file) => this.addWatchFile(file), I)
if (!generated) {
roots.delete(id)
return src
}
DEBUG && I.end('[@tailwindcss/vite] Generate CSS (serve)')
return { code: generated }
},
},
@ -257,50 +90,23 @@ export default function tailwindcss(): Plugin[] {
if (!isPotentialCssRootFile(id)) return
using I = new Instrumentation()
I.start('[@tailwindcss/vite] Generate CSS (build)')
DEBUG && I.start('[@tailwindcss/vite] Generate CSS (build)')
let root = roots.get(id)
// We do a first pass to generate valid CSS for the downstream plugins.
// However, since not all candidates are guaranteed to be extracted by
// this time, we have to re-run a transform for the root later.
let generated = await root.generate(src, (file) => this.addWatchFile(file), I)
if (!generated) {
roots.delete(id)
return src
}
DEBUG && I.end('[@tailwindcss/vite] Generate CSS (build)')
DEBUG && I.start('[@tailwindcss/vite] Optimize CSS')
generated = optimizeCss(generated, { minify })
DEBUG && I.end('[@tailwindcss/vite] Optimize CSS')
return { code: generated }
},
// `renderStart` runs in the bundle generation stage after all transforms.
// We must run before `enforce: post` so the updated chunks are picked up
// by vite:css-post.
async renderStart() {
using I = new Instrumentation()
I.start('[@tailwindcss/vite] (render start)')
for (let [id, root] of roots.entries()) {
let generated = await regenerateOptimizedCss(
root,
// During the renderStart phase, we can not add watch files since
// those would not be causing a refresh of the right CSS file. This
// should not be an issue since we did already process the CSS file
// before and the dependencies should not be changed (only the
// candidate list might have)
() => {},
I,
)
if (!generated) {
roots.delete(id)
continue
}
// These plugins have side effects which, during build, results in CSS
// being written to the output dir. We need to run them here to ensure
// the CSS is written before the bundle is generated.
await transformWithPlugins(this, id, generated)
}
},
},
] satisfies Plugin[]
}
@ -325,7 +131,7 @@ function optimizeCss(
input: string,
{ file = 'input.css', minify = false }: { file?: string; minify?: boolean } = {},
) {
function optimize(code: Buffer | Uint8Array) {
function optimize(code: Buffer | Uint8Array | any) {
return transform({
filename: file,
code,
@ -383,39 +189,24 @@ class DefaultMap<K, V> extends Map<K, V> {
}
class Root {
// Content is only used in serve mode where we need to capture the initial
// contents of the root file so that we can restore it during the
// `renderStart` hook.
public lastContent: string = ''
// The lazily-initialized Tailwind compiler components. These are persisted
// throughout rebuilds but will be re-initialized if the rebuild strategy is
// set to `full`.
private compiler?: Awaited<ReturnType<typeof compile>>
public requiresRebuild: boolean = true
// This is the compiler-specific scanner instance that is used only to scan
// files for custom @source paths. All other modules we scan for candidates
// will use the shared moduleGraphScanner instance.
// The lazily-initialized Tailwind scanner.
private scanner?: Scanner
// List of all candidates that were being returned by the root scanner during
// the lifetime of the root.
private candidates: Set<string> = new Set<string>()
// List of all dependencies captured while generating the root. These are
// retained so we can clear the require cache when we rebuild the root.
private dependencies = new Set<string>()
// The resolved path given to `source(…)`. When not given this is `null`.
private basePath: string | null = null
public overwriteCandidates: string[] | null = null
// List of all build dependencies (e.g. imported stylesheets or plugins) and
// their last modification timestamp
private buildDependencies = new Map<string, number>()
constructor(
private id: string,
private getSharedCandidates: () => Map<string, Set<string>>,
private base: string,
private customCssResolver: (id: string, base: string) => Promise<string | false | undefined>,
@ -429,38 +220,43 @@ class Root {
addWatchFile: (file: string) => void,
I: Instrumentation,
): Promise<string | false> {
this.lastContent = content
let requiresBuildPromise = this.requiresBuild()
let inputPath = idToPath(this.id)
let inputBase = path.dirname(path.resolve(inputPath))
if (!this.compiler || !this.scanner || this.requiresRebuild) {
clearRequireCache(Array.from(this.dependencies))
this.dependencies = new Set([idToPath(inputPath)])
if (!this.compiler || !this.scanner || (await requiresBuildPromise)) {
clearRequireCache(Array.from(this.buildDependencies.keys()))
this.buildDependencies.clear()
this.addBuildDependency(idToPath(inputPath))
DEBUG && I.start('Setup compiler')
let addBuildDependenciesPromises: Promise<void>[] = []
this.compiler = await compile(content, {
base: inputBase,
shouldRewriteUrls: true,
onDependency: (path) => {
addWatchFile(path)
this.dependencies.add(path)
addBuildDependenciesPromises.push(this.addBuildDependency(path))
},
customCssResolver: this.customCssResolver,
customJsResolver: this.customJsResolver,
})
await Promise.all(addBuildDependenciesPromises)
DEBUG && I.end('Setup compiler')
DEBUG && I.start('Setup scanner')
let sources = (() => {
// Disable auto source detection
if (this.compiler.root === 'none') {
return []
}
// No root specified, use the module graph
// No root specified, auto-detect based on the `**/*` pattern
if (this.compiler.root === null) {
return []
return [{ base: this.base, pattern: '**/*' }]
}
// Use the specified root
@ -468,6 +264,7 @@ class Root {
})().concat(this.compiler.globs)
this.scanner = new Scanner({ sources })
DEBUG && I.end('Setup scanner')
}
if (
@ -479,7 +276,7 @@ class Root {
return false
}
if (!this.overwriteCandidates || this.compiler.features & Features.Utilities) {
if (this.compiler.features & Features.Utilities) {
// This should not be here, but right now the Vite plugin is setup where we
// setup a new scanner and compiler every time we request the CSS file
// (regardless whether it actually changed or not).
@ -525,59 +322,29 @@ class Root {
`The path given to \`source(…)\` must be a directory but got \`source(${basePath})\` instead.`,
)
}
this.basePath = basePath
} else if (root === null) {
this.basePath = null
}
}
}
this.requiresRebuild = true
DEBUG && I.start('Build CSS')
let result = this.compiler.build(
this.overwriteCandidates
? this.overwriteCandidates
: [...this.sharedCandidates(), ...this.candidates],
)
let result = this.compiler.build([...this.candidates])
DEBUG && I.end('Build CSS')
return result
}
private sharedCandidates(): Set<string> {
if (!this.compiler) return new Set()
if (this.compiler.root === 'none') return new Set()
private async addBuildDependency(path: string) {
let stat = await fs.stat(path)
this.buildDependencies.set(path, stat.mtimeMs)
}
const HAS_DRIVE_LETTER = /^[A-Z]:/
let shouldIncludeCandidatesFrom = (id: string) => {
if (this.basePath === null) return true
if (id.startsWith(this.basePath)) return true
// This is a windows absolute path that doesn't match so return false
if (HAS_DRIVE_LETTER.test(id)) return false
// We've got a path that's not absolute and not on Windows
// TODO: this is probably a virtual module -- not sure if we need to scan it
if (!id.startsWith('/')) return true
// This is an absolute path on POSIX and it does not match
return false
}
let shared = new Set<string>()
for (let [id, candidates] of this.getSharedCandidates()) {
if (!shouldIncludeCandidatesFrom(id)) continue
for (let candidate of candidates) {
shared.add(candidate)
private async requiresBuild(): Promise<boolean> {
for (let [path, mtime] of this.buildDependencies) {
let stat = await fs.stat(path)
if (stat.mtimeMs > mtime) {
return true
}
}
return shared
return false
}
}