From 3c373e28d3a1755a5a82f6f154e44a658826d829 Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Mon, 28 Oct 2024 16:15:07 -0400 Subject: [PATCH 1/3] Store candidates per-module --- packages/@tailwindcss-vite/src/index.ts | 38 ++++++++++++------------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/packages/@tailwindcss-vite/src/index.ts b/packages/@tailwindcss-vite/src/index.ts index b045ab3505ae..e0874d58f0d7 100644 --- a/packages/@tailwindcss-vite/src/index.ts +++ b/packages/@tailwindcss-vite/src/index.ts @@ -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() + let moduleGraphCandidates = new DefaultMap>(() => new Set()) let moduleGraphScanner = new Scanner({}) let roots: DefaultMap = new DefaultMap( @@ -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) { @@ -348,14 +348,9 @@ class Root { // root. private dependencies = new Set() - // 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, + private getSharedCandidates: () => Map>, private base: string, ) {} @@ -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) @@ -440,13 +429,24 @@ 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 { + if (!this.compiler) return new Set() + if (this.compiler.root === 'none') return new Set() + + let shared = new Set() + + for (let [id, candidates] of this.getSharedCandidates()) { + for (let candidate of candidates) { + shared.add(candidate) + } + } + + return shared + } } From 2305f7ae168764057054069d97e1358ecc18297d Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Mon, 28 Oct 2024 17:12:54 -0400 Subject: [PATCH 2/3] Add test for `source(none)` in Vite --- integrations/vite/index.test.ts | 86 +++++++++++++++++++++++++++++++++ 1 file changed, 86 insertions(+) diff --git a/integrations/vite/index.test.ts b/integrations/vite/index.test.ts index 89814ccc6168..c22f4cfb5a41 100644 --- a/integrations/vite/index.test.ts +++ b/integrations/vite/index.test.ts @@ -427,6 +427,92 @@ 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` + + + + +
Hello, world!
+ + `, + 'project-a/src/index.css': css` + @import 'tailwindcss' source(none); + @source '../../project-b/src/**/*.html'; + `, + 'project-b/src/index.html': html` +
+ `, + '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']`, + ]) + }, + ) }) } From 97bdca85b5386e0384370c8b7ea4d18f3bacda9c Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Mon, 28 Oct 2024 17:13:37 -0400 Subject: [PATCH 3/3] =?UTF-8?q?Filter=20module=20graph=20based=20on=20`sou?= =?UTF-8?q?rce(=E2=80=A6)`?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- integrations/vite/index.test.ts | 102 ++++++++++++++++++++++++ packages/@tailwindcss-vite/src/index.ts | 15 ++++ 2 files changed, 117 insertions(+) diff --git a/integrations/vite/index.test.ts b/integrations/vite/index.test.ts index c22f4cfb5a41..0cef7ebe1da9 100644 --- a/integrations/vite/index.test.ts +++ b/integrations/vite/index.test.ts @@ -513,6 +513,108 @@ for (let transformer of ['postcss', 'lightningcss']) { ]) }, ) + + 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` + + + + +
Hello, world!
+ + + `, + '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` +
+ `, + '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']`, + ]) + }, + ) }) } diff --git a/packages/@tailwindcss-vite/src/index.ts b/packages/@tailwindcss-vite/src/index.ts index e0874d58f0d7..b1fffb9d128d 100644 --- a/packages/@tailwindcss-vite/src/index.ts +++ b/packages/@tailwindcss-vite/src/index.ts @@ -439,9 +439,24 @@ class Root { 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() for (let [id, candidates] of this.getSharedCandidates()) { + if (!moduleIdIsAllowed(id)) continue + for (let candidate of candidates) { shared.add(candidate) }