diff --git a/package-lock.json b/package-lock.json index 7ffd8f3..260f774 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,6 +15,7 @@ "lodash.topath": "^4.5.2", "normalize-path": "^3.0.0", "object-hash": "^2.1.1", + "parse-glob": "^3.0.4", "postcss-selector-parser": "^6.0.4", "quick-lru": "^5.1.1" }, @@ -3159,6 +3160,45 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/glob-base": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/glob-base/-/glob-base-0.3.0.tgz", + "integrity": "sha1-27Fk9iIbHAscz4Kuoyi0l98Oo8Q=", + "dependencies": { + "glob-parent": "^2.0.0", + "is-glob": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/glob-base/node_modules/glob-parent": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-2.0.0.tgz", + "integrity": "sha1-gTg9ctsFT8zPUzbaqQLxgvbtuyg=", + "dependencies": { + "is-glob": "^2.0.0" + } + }, + "node_modules/glob-base/node_modules/is-extglob": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-1.0.0.tgz", + "integrity": "sha1-rEaBd8SUNAWgkvyPKXYMb/xiBsA=", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/glob-base/node_modules/is-glob": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-2.0.1.tgz", + "integrity": "sha1-0Jb5JqPe1WAPP9/ZEZjLCIjC2GM=", + "dependencies": { + "is-extglob": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/glob-parent": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.1.tgz", @@ -3602,6 +3642,14 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/is-dotfile": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/is-dotfile/-/is-dotfile-1.0.3.tgz", + "integrity": "sha1-pqLzL/0t+wT1yiXs0Pa4PPeYoeE=", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/is-extendable": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", @@ -6368,6 +6416,39 @@ "node": ">=6" } }, + "node_modules/parse-glob": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/parse-glob/-/parse-glob-3.0.4.tgz", + "integrity": "sha1-ssN2z7EfNVE7rdFz7wu246OIORw=", + "dependencies": { + "glob-base": "^0.3.0", + "is-dotfile": "^1.0.0", + "is-extglob": "^1.0.0", + "is-glob": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/parse-glob/node_modules/is-extglob": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-1.0.0.tgz", + "integrity": "sha1-rEaBd8SUNAWgkvyPKXYMb/xiBsA=", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/parse-glob/node_modules/is-glob": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-2.0.1.tgz", + "integrity": "sha1-0Jb5JqPe1WAPP9/ZEZjLCIjC2GM=", + "dependencies": { + "is-extglob": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/parse-json": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", @@ -11472,6 +11553,38 @@ "path-is-absolute": "^1.0.0" } }, + "glob-base": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/glob-base/-/glob-base-0.3.0.tgz", + "integrity": "sha1-27Fk9iIbHAscz4Kuoyi0l98Oo8Q=", + "requires": { + "glob-parent": "^2.0.0", + "is-glob": "^2.0.0" + }, + "dependencies": { + "glob-parent": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-2.0.0.tgz", + "integrity": "sha1-gTg9ctsFT8zPUzbaqQLxgvbtuyg=", + "requires": { + "is-glob": "^2.0.0" + } + }, + "is-extglob": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-1.0.0.tgz", + "integrity": "sha1-rEaBd8SUNAWgkvyPKXYMb/xiBsA=" + }, + "is-glob": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-2.0.1.tgz", + "integrity": "sha1-0Jb5JqPe1WAPP9/ZEZjLCIjC2GM=", + "requires": { + "is-extglob": "^1.0.0" + } + } + } + }, "glob-parent": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.1.tgz", @@ -11808,6 +11921,11 @@ "dev": true, "optional": true }, + "is-dotfile": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/is-dotfile/-/is-dotfile-1.0.3.tgz", + "integrity": "sha1-pqLzL/0t+wT1yiXs0Pa4PPeYoeE=" + }, "is-extendable": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", @@ -13933,6 +14051,32 @@ "callsites": "^3.0.0" } }, + "parse-glob": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/parse-glob/-/parse-glob-3.0.4.tgz", + "integrity": "sha1-ssN2z7EfNVE7rdFz7wu246OIORw=", + "requires": { + "glob-base": "^0.3.0", + "is-dotfile": "^1.0.0", + "is-extglob": "^1.0.0", + "is-glob": "^2.0.0" + }, + "dependencies": { + "is-extglob": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-1.0.0.tgz", + "integrity": "sha1-rEaBd8SUNAWgkvyPKXYMb/xiBsA=" + }, + "is-glob": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-2.0.1.tgz", + "integrity": "sha1-0Jb5JqPe1WAPP9/ZEZjLCIjC2GM=", + "requires": { + "is-extglob": "^1.0.0" + } + } + } + }, "parse-json": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", diff --git a/package.json b/package.json index 0c9c944..2339303 100644 --- a/package.json +++ b/package.json @@ -20,6 +20,7 @@ "lodash.topath": "^4.5.2", "normalize-path": "^3.0.0", "object-hash": "^2.1.1", + "parse-glob": "^3.0.4", "postcss-selector-parser": "^6.0.4", "quick-lru": "^5.1.1" }, diff --git a/src/index.js b/src/index.js index a86dc09..3d21d2c 100644 --- a/src/index.js +++ b/src/index.js @@ -23,9 +23,9 @@ module.exports = (configOrPath = {}) => { return root }, function (root, result) { - function registerDependency(fileName) { + function registerDependency(fileName, type = 'dependency') { result.messages.push({ - type: 'dependency', + type, plugin: 'tailwindcss-jit', parent: result.opts.from, file: fileName, @@ -36,8 +36,10 @@ module.exports = (configOrPath = {}) => { let context = setupContext(configOrPath)(result, root) - if (context.configPath !== null) { - registerDependency(context.configPath) + if (!env.TAILWIND_DISABLE_TOUCH) { + if (context.configPath !== null) { + registerDependency(context.configPath) + } } return postcss([ diff --git a/src/lib/expandTailwindAtRules.js b/src/lib/expandTailwindAtRules.js index dfcaf70..bb7bb41 100644 --- a/src/lib/expandTailwindAtRules.js +++ b/src/lib/expandTailwindAtRules.js @@ -1,6 +1,7 @@ const fs = require('fs') const path = require('path') const fastGlob = require('fast-glob') +const parseGlob = require('parse-glob') const sharedState = require('./sharedState') const { generateRules } = require('./generateRules') const { bigSign, cloneNodes } = require('./utils') @@ -141,21 +142,55 @@ function expandTailwindAtRules(context, registerDependency) { // --- - // Register our temp file as a dependency — we write to this file - // to trigger rebuilds. - if (context.touchFile) { - registerDependency(context.touchFile) - } + if (sharedState.env.TAILWIND_DISABLE_TOUCH) { + for (let maybeGlob of context.candidateFiles) { + let { + is: { glob: isGlob }, + base, + } = parseGlob(maybeGlob) + + if (isGlob) { + // register base dir as `dependency` _and_ `context-dependency` for + // increased compatibility + registerDependency(path.resolve(base)) + registerDependency(path.resolve(base), 'context-dependency') + } else { + registerDependency(path.resolve(maybeGlob)) + } + } - // If we're not set up and watching files ourselves, we need to do - // the work of grabbing all of the template files for candidate - // detection. - if (!context.scannedContent) { + env.DEBUG && console.time('Finding changed files') let files = fastGlob.sync(context.candidateFiles) for (let file of files) { - context.changedFiles.add(file) + let prevModified = sharedState.fileModifiedCache.has(file) + ? sharedState.fileModifiedCache.get(file) + : -Infinity + let modified = fs.statSync(file).mtimeMs + + if (!context.scannedContent || modified > prevModified) { + context.changedFiles.add(file) + sharedState.fileModifiedCache.set(file, modified) + } } context.scannedContent = true + env.DEBUG && console.timeEnd('Finding changed files') + } else { + // Register our temp file as a dependency — we write to this file + // to trigger rebuilds. + if (context.touchFile) { + registerDependency(context.touchFile) + } + + // If we're not set up and watching files ourselves, we need to do + // the work of grabbing all of the template files for candidate + // detection. + if (!context.scannedContent) { + let files = fastGlob.sync(context.candidateFiles) + for (let file of files) { + context.changedFiles.add(file) + } + context.scannedContent = true + } } // --- diff --git a/src/lib/setupContext.js b/src/lib/setupContext.js index c2a4133..cd7663d 100644 --- a/src/lib/setupContext.js +++ b/src/lib/setupContext.js @@ -34,12 +34,14 @@ let env = sharedState.env const touchDir = path.join(os.homedir() || os.tmpdir(), '.tailwindcss', 'touch') -if (fs.existsSync(touchDir)) { - for (let file of fs.readdirSync(touchDir)) { - fs.unlinkSync(path.join(touchDir, file)) +if (!sharedState.env.TAILWIND_DISABLE_TOUCH) { + if (fs.existsSync(touchDir)) { + for (let file of fs.readdirSync(touchDir)) { + fs.unlinkSync(path.join(touchDir, file)) + } + } else { + fs.mkdirSync(touchDir, { recursive: true }) } -} else { - fs.mkdirSync(touchDir, { recursive: true }) } // This is used to trigger rebuilds. Just updating the timestamp @@ -152,6 +154,46 @@ let configPathCache = new LRU({ maxSize: 100 }) function getTailwindConfig(configOrPath) { let userConfigPath = resolveConfigPath(configOrPath) + if (sharedState.env.TAILWIND_DISABLE_TOUCH) { + if (userConfigPath !== null) { + let [prevConfig, prevConfigHash, prevDeps, prevModified] = + configPathCache.get(userConfigPath) || [] + + let newDeps = getModuleDependencies(userConfigPath).map((dep) => dep.file) + + let modified = false + let newModified = new Map() + for (let file of newDeps) { + let time = fs.statSync(file).mtimeMs + newModified.set(file, time) + if (!prevModified || !prevModified.has(file) || time > prevModified.get(file)) { + modified = true + } + } + + // It hasn't changed (based on timestamps) + if (!modified) { + return [prevConfig, userConfigPath, prevConfigHash, prevDeps] + } + + // It has changed (based on timestamps), or first run + for (let file of newDeps) { + delete require.cache[file] + } + let newConfig = resolveConfig(require(userConfigPath)) + let newHash = hash(newConfig) + configPathCache.set(userConfigPath, [newConfig, newHash, newDeps, newModified]) + return [newConfig, userConfigPath, newHash, newDeps] + } + + // It's a plain object, not a path + let newConfig = resolveConfig( + configOrPath.config === undefined ? configOrPath : configOrPath.config + ) + + return [newConfig, null, hash(newConfig), []] + } + if (userConfigPath !== null) { let [prevConfig, prevModified = -Infinity, prevConfigHash] = configPathCache.get(userConfigPath) || [] @@ -627,10 +669,17 @@ function setupContext(configOrPath) { }) let sourcePath = result.opts.from - let [tailwindConfig, userConfigPath, tailwindConfigHash] = getTailwindConfig(configOrPath) + let [ + tailwindConfig, + userConfigPath, + tailwindConfigHash, + configDependencies, + ] = getTailwindConfig(configOrPath) let isConfigFile = userConfigPath !== null - let contextDependencies = new Set() + let contextDependencies = new Set( + sharedState.env.TAILWIND_DISABLE_TOUCH ? configDependencies : [] + ) // If there are no @tailwind rules, we don't consider this CSS file or it's dependencies // to be dependencies of the context. Can reuse the context even if they change. @@ -646,8 +695,19 @@ function setupContext(configOrPath) { } } - if (isConfigFile) { - contextDependencies.add(userConfigPath) + if (sharedState.env.TAILWIND_DISABLE_TOUCH) { + for (let file of configDependencies) { + result.messages.push({ + type: 'dependency', + plugin: 'tailwindcss-jit', + parent: result.opts.from, + file, + }) + } + } else { + if (isConfigFile) { + contextDependencies.add(userConfigPath) + } } let contextDependenciesChanged = trackModified([...contextDependencies]) @@ -725,7 +785,7 @@ function setupContext(configOrPath) { // --- - if (isConfigFile) { + if (isConfigFile && !sharedState.env.TAILWIND_DISABLE_TOUCH) { for (let dependency of getModuleDependencies(userConfigPath)) { if (dependency.file === userConfigPath) { continue diff --git a/src/lib/sharedState.js b/src/lib/sharedState.js index 51f4f61..c59800f 100644 --- a/src/lib/sharedState.js +++ b/src/lib/sharedState.js @@ -5,9 +5,11 @@ module.exports = { TAILWIND_MODE: process.env.TAILWIND_MODE, NODE_ENV: process.env.NODE_ENV, DEBUG: process.env.DEBUG !== undefined, + TAILWIND_DISABLE_TOUCH: process.env.TAILWIND_DISABLE_TOUCH !== undefined, }, contextMap: new Map(), configContextMap: new Map(), contextSourcesMap: new Map(), contentMatchCache: new LRU({ maxSize: 25000 }), + fileModifiedCache: new Map(), }