diff --git a/CHANGELOG.md b/CHANGELOG.md index 46d3ea3b17c7..e7143ff1b106 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/integrations/vite/astro.test.ts b/integrations/vite/astro.test.ts index 5b7407e4714b..81db3e82c6d5 100644 --- a/integrations/vite/astro.test.ts +++ b/integrations/vite/astro.test.ts @@ -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`
Hello, world!
@@ -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'; + --- + +
Hello, world!
+ + + + + `, + 'src/pages/client-only.jsx': js` + export default function ClientOnly() { + return
Hello, world!
+ } + `, + }, + }, + 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`]) + }, +) diff --git a/integrations/vite/index.test.ts b/integrations/vite/index.test.ts index c0bee2b878cc..afc58c558bf5 100644 --- a/integrations/vite/index.test.ts +++ b/integrations/vite/index.test.ts @@ -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`) }) diff --git a/integrations/vite/react-router.test.ts b/integrations/vite/react-router.test.ts new file mode 100644 index 000000000000..5b9574fd658b --- /dev/null +++ b/integrations/vite/react-router.test.ts @@ -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

Welcome to React Router

+ } + `, + '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 ( + + + + + + + + + {children} + + + + + ) + } + + export default function App() { + return + } + `, +} + +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

Welcome to React Router

+ } + `, + ) + + 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 ( + + + + + + + + + {children} + + + + + ) + } + + export default function App() { + return + } + `, + '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`]) + }, +) diff --git a/integrations/vite/ssr.test.ts b/integrations/vite/ssr.test.ts index 3c3f1cc4beec..d4d227b8e931 100644 --- a/integrations/vite/ssr.test.ts +++ b/integrations/vite/ssr.test.ts @@ -1,5 +1,30 @@ import { candidate, css, html, json, test, ts } from '../utils' +const WORKSPACE = { + 'index.html': html` + +
+ + + `, + 'src/index.css': css`@import 'tailwindcss';`, + 'src/index.ts': ts` + import './index.css' + + document.querySelector('#app').innerHTML = \` +
Hello, world!
+ \` + `, + 'server.ts': ts` + import css from './src/index.css?url' + + document.querySelector('#app').innerHTML = \` + +
Hello, world!
+ \` + `, +} + test( 'Vite 5', { @@ -27,31 +52,9 @@ test( ssrEmitAssets: true, }, plugins: [tailwindcss()], - ssr: { resolve: { conditions: [] } }, }) `, - 'index.html': html` - -
- - - `, - 'src/index.css': css`@import 'tailwindcss';`, - 'src/index.ts': ts` - import './index.css' - - document.querySelector('#app').innerHTML = \` -
Hello, world!
- \` - `, - 'server.ts': ts` - import css from './src/index.css?url' - - document.querySelector('#app').innerHTML = \` - -
Hello, world!
- \` - `, + ...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` - -
- - - `, - 'src/index.css': css`@import 'tailwindcss';`, - 'src/index.ts': ts` - import './index.css' - - document.querySelector('#app').innerHTML = \` -
Hello, world!
- \` - `, - 'server.ts': ts` - import css from './src/index.css?url' - - document.querySelector('#app').innerHTML = \` - -
Hello, world!
- \` - `, + ...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`, ]) }, ) diff --git a/packages/@tailwindcss-vite/src/index.ts b/packages/@tailwindcss-vite/src/index.ts index ce61fdf9b64c..8e3d2348ea16 100644 --- a/packages/@tailwindcss-vite/src/index.ts +++ b/packages/@tailwindcss-vite/src/index.ts @@ -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>(() => new Set()) - let moduleGraphScanner = new Scanner({}) - let roots: DefaultMap = 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,49 +90,22 @@ 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 } - return { code: generated } - }, + DEBUG && I.end('[@tailwindcss/vite] Generate CSS (build)') - // `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 - } + DEBUG && I.start('[@tailwindcss/vite] Optimize CSS') + generated = optimizeCss(generated, { minify }) + DEBUG && I.end('[@tailwindcss/vite] Optimize CSS') - // 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) - } + return { code: 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 extends Map { } 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> - 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 = new Set() - // 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() - - // 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() constructor( private id: string, - private getSharedCandidates: () => Map>, private base: string, private customCssResolver: (id: string, base: string) => Promise, @@ -429,38 +220,43 @@ class Root { addWatchFile: (file: string) => void, I: Instrumentation, ): Promise { - 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[] = [] 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 { - if (!this.compiler) return new Set() - if (this.compiler.root === 'none') return new Set() - - 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() - - for (let [id, candidates] of this.getSharedCandidates()) { - if (!shouldIncludeCandidatesFrom(id)) continue + private async addBuildDependency(path: string) { + let stat = await fs.stat(path) + this.buildDependencies.set(path, stat.mtimeMs) + } - for (let candidate of candidates) { - shared.add(candidate) + private async requiresBuild(): Promise { + for (let [path, mtime] of this.buildDependencies) { + let stat = await fs.stat(path) + if (stat.mtimeMs > mtime) { + return true } } - - return shared + return false } }