Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
188 changes: 188 additions & 0 deletions integrations/vite/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -427,6 +427,194 @@ for (let transformer of ['postcss', 'lightningcss']) {
})
},
)

test(
`source(none) disables looking at the module graph`,
{
fs: {
'package.json': json`{}`,
'pnpm-workspace.yaml': yaml`
#
packages:
- project-a
`,
'project-a/package.json': txt`
{
"type": "module",
"dependencies": {
"@tailwindcss/vite": "workspace:^",
"tailwindcss": "workspace:^"
},
"devDependencies": {
${transformer === 'lightningcss' ? `"lightningcss": "^1.26.0",` : ''}
"vite": "^5.3.5"
}
}
`,
'project-a/vite.config.ts': ts`
import tailwindcss from '@tailwindcss/vite'
import { defineConfig } from 'vite'

export default defineConfig({
css: ${transformer === 'postcss' ? '{}' : "{ transformer: 'lightningcss' }"},
build: { cssMinify: false },
plugins: [tailwindcss()],
})
`,
'project-a/index.html': html`
<head>
<link rel="stylesheet" href="./src/index.css" />
</head>
<body>
<div class="underline m-2">Hello, world!</div>
</body>
`,
'project-a/src/index.css': css`
@import 'tailwindcss' source(none);
@source '../../project-b/src/**/*.html';
`,
'project-b/src/index.html': html`
<div class="flex" />
`,
'project-b/src/index.js': js`
const className = "content-['project-b/src/index.js']"
module.exports = { className }
`,
},
},
async ({ root, fs, exec }) => {
console.log(await exec('pnpm vite build', { cwd: path.join(root, 'project-a') }))

let files = await fs.glob('project-a/dist/**/*.css')
expect(files).toHaveLength(1)
let [filename] = files[0]

// `underline` and `m-2` are only present from files in the module graph
// which we've explicitly disabled with source(none) so they should not
// be present
await fs.expectFileNotToContain(filename, [
//
candidate`underline`,
candidate`m-2`,
])

// The files from `project-b` should be included because there is an
// explicit `@source` directive for it
await fs.expectFileToContain(filename, [
//
candidate`flex`,
])

// The explicit source directive only covers HTML files, so the JS file
// should not be included
await fs.expectFileNotToContain(filename, [
//
candidate`content-['project-b/src/index.js']`,
])
},
)

test(
`source("…") filters the module graph`,
{
fs: {
'package.json': json`{}`,
'pnpm-workspace.yaml': yaml`
#
packages:
- project-a
`,
'project-a/package.json': txt`
{
"type": "module",
"dependencies": {
"@tailwindcss/vite": "workspace:^",
"tailwindcss": "workspace:^"
},
"devDependencies": {
${transformer === 'lightningcss' ? `"lightningcss": "^1.26.0",` : ''}
"vite": "^5.3.5"
}
}
`,
'project-a/vite.config.ts': ts`
import tailwindcss from '@tailwindcss/vite'
import { defineConfig } from 'vite'

export default defineConfig({
css: ${transformer === 'postcss' ? '{}' : "{ transformer: 'lightningcss' }"},
build: { cssMinify: false },
plugins: [tailwindcss()],
})
`,
'project-a/index.html': html`
<head>
<link rel="stylesheet" href="/src/index.css" />
</head>
<body>
<div class="underline m-2 content-['project-a/index.html']">Hello, world!</div>
<script type="module" src="/app/index.js"></script>
</body>
`,
'project-a/app/index.js': js`
const className = "content-['project-a/app/index.js']"
export default { className }
`,
'project-a/src/index.css': css`
@import 'tailwindcss' source('../app');
@source '../../project-b/src/**/*.html';
`,
'project-b/src/index.html': html`
<div
class="content-['project-b/src/index.html']"
/>
`,
'project-b/src/index.js': js`
const className = "content-['project-b/src/index.js']"
module.exports = { className }
`,
},
},
async ({ root, fs, exec }) => {
await exec('pnpm vite build', { cwd: path.join(root, 'project-a') })

let files = await fs.glob('project-a/dist/**/*.css')
expect(files).toHaveLength(1)
let [filename] = files[0]

// `underline` and `m-2` are present in files in the module graph but
// we've filtered the module graph such that we only look in
// `./app/**/*` so they should not be present
await fs.expectFileNotToContain(filename, [
//
candidate`underline`,
candidate`m-2`,
candidate`content-['project-a/index.html']`,
])

// We've filtered the module graph to only look in ./app/**/* so the
// candidates from that project should be present
await fs.expectFileToContain(filename, [
//
candidate`content-['project-a/app/index.js']`,
])

// Even through we're filtering the module graph explicit sources are
// additive and as such files from `project-b` should be included
// because there is an explicit `@source` directive for it
await fs.expectFileToContain(filename, [
//
candidate`content-['project-b/src/index.html']`,
])

// The explicit source directive only covers HTML files, so the JS file
// should not be included
await fs.expectFileNotToContain(filename, [
//
candidate`content-['project-b/src/index.js']`,
])
},
)
})
}

Expand Down
53 changes: 34 additions & 19 deletions packages/@tailwindcss-vite/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ export default function tailwindcss(): Plugin[] {
// 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 Set<string>()
let moduleGraphCandidates = new DefaultMap<string, Set<string>>(() => new Set<string>())
let moduleGraphScanner = new Scanner({})

let roots: DefaultMap<string, Root> = new DefaultMap(
Expand All @@ -46,7 +46,7 @@ export default function tailwindcss(): Plugin[] {
let updated = false
for (let candidate of moduleGraphScanner.scanFiles([{ content, extension }])) {
updated = true
moduleGraphCandidates.add(candidate)
moduleGraphCandidates.get(id).add(candidate)
}

if (updated) {
Expand Down Expand Up @@ -348,14 +348,9 @@ class Root {
// root.
private dependencies = new Set<string>()

// Whether to include candidates from the module graph. This is disabled when
// the user provides `source(none)` to essentially disable auto source
// detection.
private includeCandidatesFromModuleGraph = true

constructor(
private id: string,
private getSharedCandidates: () => Set<string>,
private getSharedCandidates: () => Map<string, Set<string>>,
private base: string,
) {}

Expand Down Expand Up @@ -387,20 +382,14 @@ class Root {
let sources = (() => {
// Disable auto source detection
if (this.compiler.root === 'none') {
this.includeCandidatesFromModuleGraph = false
return []
}

// No root specified, use the module graph
if (this.compiler.root === null) {
this.includeCandidatesFromModuleGraph = true

return []
}

// TODO: In a follow up PR we want this filter this against the module graph.
this.includeCandidatesFromModuleGraph = true

// Use the specified root
return [this.compiler.root]
})().concat(this.compiler.globs)
Expand Down Expand Up @@ -440,13 +429,39 @@ class Root {
this.requiresRebuild = true

env.DEBUG && console.time('[@tailwindcss/vite] Build CSS')
let result = this.compiler.build(
this.includeCandidatesFromModuleGraph
? [...this.getSharedCandidates(), ...this.candidates]
: Array.from(this.candidates),
)
let result = this.compiler.build([...this.sharedCandidates(), ...this.candidates])
env.DEBUG && console.timeEnd('[@tailwindcss/vite] Build CSS')

return result
}

private sharedCandidates(): Set<string> {
if (!this.compiler) return new Set()
if (this.compiler.root === 'none') return new Set()

let root = this.compiler.root
let basePath = root ? path.resolve(root.base, root.pattern) : null

function moduleIdIsAllowed(id: string) {
if (basePath === null) return true

// This a virtual module that's not on the file system
// TODO: What should we do here?
if (!id.startsWith('/')) return true

return id.startsWith(basePath)
}

let shared = new Set<string>()

for (let [id, candidates] of this.getSharedCandidates()) {
if (!moduleIdIsAllowed(id)) continue

for (let candidate of candidates) {
shared.add(candidate)
}
}

return shared
}
}
Loading