From df5de3f0dd2a1a0192da6316c91d910048c1a537 Mon Sep 17 00:00:00 2001 From: Brad Cornes Date: Fri, 5 Mar 2021 19:23:15 +0000 Subject: [PATCH 01/10] use context dependencies and remove file watcher and touch files --- src/index.js | 4 +- src/lib/expandTailwindAtRules.js | 37 +++++--- src/lib/setupContext.js | 141 +++++-------------------------- src/lib/sharedState.js | 1 + 4 files changed, 49 insertions(+), 134 deletions(-) diff --git a/src/index.js b/src/index.js index 9863d63..883f774 100644 --- a/src/index.js +++ b/src/index.js @@ -22,9 +22,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, diff --git a/src/lib/expandTailwindAtRules.js b/src/lib/expandTailwindAtRules.js index f368065..cbbcdc8 100644 --- a/src/lib/expandTailwindAtRules.js +++ b/src/lib/expandTailwindAtRules.js @@ -1,5 +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, toPostCssNode } = require('./utils') @@ -111,21 +113,34 @@ 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) + for (let maybeGlob of context.candidateFiles) { + let { + is: { glob: isGlob }, + base, + } = parseGlob(maybeGlob) + + if (isGlob) { + 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) { - let files = fastGlob.sync(context.candidateFiles) - for (let file of files) { + env.DEBUG && console.time('Finding changed files') + let files = fastGlob.sync(context.candidateFiles) + for (let file of files) { + let prevModified = sharedState.fileModifiedCache.get(file) ?? -Infinity + let modified = fs.statSync(file).mtimeMs + + if (modified > prevModified) { context.changedFiles.add(file) + sharedState.fileModifiedCache.set(file, modified) } - context.scannedContent = true + } + env.DEBUG && console.timeEnd('Finding changed files') + + if (context.changedFiles.size === 0) { + return root } // --- diff --git a/src/lib/setupContext.js b/src/lib/setupContext.js index 849505d..52458b2 100644 --- a/src/lib/setupContext.js +++ b/src/lib/setupContext.js @@ -1,8 +1,5 @@ const fs = require('fs') -const os = require('os') const path = require('path') -const crypto = require('crypto') -const chokidar = require('chokidar') const postcss = require('postcss') const dlv = require('dlv') const selectorParser = require('postcss-selector-parser') @@ -23,33 +20,6 @@ const { isBuffer } = require('util') let contextMap = sharedState.contextMap let env = sharedState.env -// Earmarks a directory for our touch files. -// If the directory already exists we delete any existing touch files, -// invalidating any caches associated with them. - -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)) - } -} else { - fs.mkdirSync(touchDir, { recursive: true }) -} - -// This is used to trigger rebuilds. Just updating the timestamp -// is significantly faster than actually writing to the file (10x). - -function touch(filename) { - let time = new Date() - - try { - fs.utimesSync(filename, time, time) - } catch (err) { - fs.closeSync(fs.openSync(filename, 'w')) - } -} - function isObject(value) { return typeof value === 'object' && value !== null } @@ -171,97 +141,25 @@ function getTailwindConfig(configOrPath) { return [newConfig, null] } -let fileModifiedMap = new Map() - function trackModified(files) { let changed = false for (let file of files) { let newModified = fs.statSync(file).mtimeMs - if (!fileModifiedMap.has(file) || newModified > fileModifiedMap.get(file)) { + if ( + !sharedState.fileModifiedCache.has(file) || + newModified > sharedState.fileModifiedCache.get(file) + ) { changed = true } - fileModifiedMap.set(file, newModified) + sharedState.fileModifiedCache.set(file, newModified) } return changed } -function generateTouchFileName() { - let chars = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz' - let randomChars = '' - let randomCharsLength = 12 - let bytes = null - - try { - bytes = crypto.randomBytes(randomCharsLength) - } catch (_error) { - bytes = crypto.pseudoRandomBytes(randomCharsLength) - } - - for (let i = 0; i < randomCharsLength; i++) { - randomChars += chars[bytes[i] % chars.length] - } - - return path.join(touchDir, `touch-${process.pid}-${randomChars}`) -} - -function rebootWatcher(context) { - if (context.touchFile === null) { - context.touchFile = generateTouchFileName() - touch(context.touchFile) - } - - if (env.TAILWIND_MODE === 'build') { - return - } - - if ( - env.TAILWIND_MODE === 'watch' || - (env.TAILWIND_MODE === undefined && env.NODE_ENV === 'development') - ) { - Promise.resolve(context.watcher ? context.watcher.close() : null).then(() => { - context.watcher = chokidar.watch([...context.candidateFiles, ...context.configDependencies], { - ignoreInitial: true, - }) - - context.watcher.on('add', (file) => { - context.changedFiles.add(path.resolve('.', file)) - touch(context.touchFile) - }) - - context.watcher.on('change', (file) => { - // If it was a config dependency, touch the config file to trigger a new context. - // This is not really that clean of a solution but it's the fastest, because we - // can do a very quick check on each build to see if the config has changed instead - // of having to get all of the module dependencies and check every timestamp each - // time. - if (context.configDependencies.has(file)) { - for (let dependency of context.configDependencies) { - delete require.cache[require.resolve(dependency)] - } - touch(context.configPath) - } else { - context.changedFiles.add(path.resolve('.', file)) - touch(context.touchFile) - } - }) - - context.watcher.on('unlink', (file) => { - // Touch the config file if any of the dependencies are deleted. - if (context.configDependencies.has(file)) { - for (let dependency of context.configDependencies) { - delete require.cache[require.resolve(dependency)] - } - touch(context.configPath) - } - }) - }) - } -} - function insertInto(list, value, { before = [] } = {}) { if (before.length <= 0) { list.push(value) @@ -533,12 +431,6 @@ function registerPlugins(tailwindConfig, plugins, context) { } } -function cleanupContext(context) { - if (context.watcher) { - context.watcher.close() - } -} - // Retrieve an existing context from cache if possible (since contexts are unique per // source path), or set up a new one (including setting up watchers and registering // plugins) then return it @@ -560,6 +452,12 @@ function setupContext(configOrPath) { } } + if (contextMap.has(sourcePath)) { + for (let dependency of contextMap.get(sourcePath).configDependencies) { + contextDependencies.add(dependency) + } + } + let contextDependenciesChanged = trackModified([...contextDependencies]) || userConfigPath === null @@ -568,18 +466,12 @@ function setupContext(configOrPath) { return contextMap.get(sourcePath) } - if (contextMap.has(sourcePath)) { - cleanupContext(contextMap.get(sourcePath)) - } - process.env.DEBUG && console.log('Setting up new context...') let context = { changedFiles: new Set(), ruleCache: new Set(), - watcher: null, scannedContent: false, - touchFile: null, classCache: new Map(), notClassCache: new Set(), postCssNodeCache: new Map(), @@ -604,11 +496,18 @@ function setupContext(configOrPath) { } context.configDependencies.add(dependency.file) + + result.messages.push({ + type: 'dependency', + plugin: 'tailwindcss-jit', + parent: result.opts.from, + file: dependency.file, + }) + + trackModified([dependency.file]) } } - rebootWatcher(context) - let corePluginList = Object.entries(corePlugins) .map(([name, plugin]) => { // TODO: Make variants a real core plugin so we don't special case it diff --git a/src/lib/sharedState.js b/src/lib/sharedState.js index 32c593e..31c702b 100644 --- a/src/lib/sharedState.js +++ b/src/lib/sharedState.js @@ -8,4 +8,5 @@ module.exports = { }, contextMap: new Map(), contentMatchCache: new LRU({ maxSize: 25000 }), + fileModifiedCache: new Map(), } From 70c5489422077d710684784bbf13d3b652101115 Mon Sep 17 00:00:00 2001 From: Brad Cornes Date: Sat, 6 Mar 2021 17:19:15 +0000 Subject: [PATCH 02/10] remove broken bail out --- src/lib/expandTailwindAtRules.js | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/lib/expandTailwindAtRules.js b/src/lib/expandTailwindAtRules.js index cbbcdc8..731627e 100644 --- a/src/lib/expandTailwindAtRules.js +++ b/src/lib/expandTailwindAtRules.js @@ -139,10 +139,6 @@ function expandTailwindAtRules(context, registerDependency) { } env.DEBUG && console.timeEnd('Finding changed files') - if (context.changedFiles.size === 0) { - return root - } - // --- // Find potential rules in changed files From 88c334c1f727ff698941933878c7637d854bc39b Mon Sep 17 00:00:00 2001 From: Brad Cornes Date: Sun, 7 Mar 2021 10:41:53 +0000 Subject: [PATCH 03/10] register glob base dirs as dependency as well as context-dependency improves tooling compatibility: now works with postcss-cli and rollup, as well as webpack --- src/lib/expandTailwindAtRules.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/lib/expandTailwindAtRules.js b/src/lib/expandTailwindAtRules.js index 731627e..dfe8899 100644 --- a/src/lib/expandTailwindAtRules.js +++ b/src/lib/expandTailwindAtRules.js @@ -120,6 +120,9 @@ function expandTailwindAtRules(context, registerDependency) { } = 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)) From 4d2a25fc151dacbbca371c8d661879e190ab5c08 Mon Sep 17 00:00:00 2001 From: Brad Cornes Date: Fri, 26 Mar 2021 12:18:36 +0000 Subject: [PATCH 04/10] add parse-glob dependency --- package-lock.json | 144 ++++++++++++++++++++++++++++++++++++++++++++++ package.json | 1 + 2 files changed, 145 insertions(+) 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" }, From 457a1412ef395f7c128ee1e83cf6dd0ef6577d8a Mon Sep 17 00:00:00 2001 From: Brad Cornes Date: Fri, 26 Mar 2021 12:19:59 +0000 Subject: [PATCH 05/10] fix fresh context build --- src/lib/expandTailwindAtRules.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/lib/expandTailwindAtRules.js b/src/lib/expandTailwindAtRules.js index 53f443a..7278d55 100644 --- a/src/lib/expandTailwindAtRules.js +++ b/src/lib/expandTailwindAtRules.js @@ -164,11 +164,12 @@ function expandTailwindAtRules(context, registerDependency) { let prevModified = sharedState.fileModifiedCache.get(file) ?? -Infinity let modified = fs.statSync(file).mtimeMs - if (modified > prevModified) { + if (!context.scannedContent || modified > prevModified) { context.changedFiles.add(file) sharedState.fileModifiedCache.set(file, modified) } } + context.scannedContent = true env.DEBUG && console.timeEnd('Finding changed files') // --- From 314fe20a451f523c520dbe5528374c43c01334b8 Mon Sep 17 00:00:00 2001 From: Brad Cornes Date: Fri, 26 Mar 2021 13:23:23 +0000 Subject: [PATCH 06/10] rejig config dependency tracking --- src/index.js | 4 --- src/lib/setupContext.js | 77 ++++++++++++++++++++--------------------- 2 files changed, 37 insertions(+), 44 deletions(-) diff --git a/src/index.js b/src/index.js index 0ccb0dd..82fb68b 100644 --- a/src/index.js +++ b/src/index.js @@ -36,10 +36,6 @@ module.exports = (configOrPath = {}) => { let context = setupContext(configOrPath)(result, root) - if (context.configPath !== null) { - registerDependency(context.configPath) - } - return postcss([ removeLayerAtRules(context), expandTailwindAtRules(context, registerDependency), diff --git a/src/lib/setupContext.js b/src/lib/setupContext.js index 11dbec8..9dd7d1e 100644 --- a/src/lib/setupContext.js +++ b/src/lib/setupContext.js @@ -123,21 +123,34 @@ function getTailwindConfig(configOrPath) { let userConfigPath = resolveConfigPath(configOrPath) if (userConfigPath !== null) { - let [prevConfig, prevModified = -Infinity, prevConfigHash] = + let [prevConfig, prevConfigHash, prevDeps, prevModified] = configPathCache.get(userConfigPath) || [] - let modified = fs.statSync(userConfigPath).mtimeMs - // It hasn't changed (based on timestamp) - if (modified <= prevModified) { - return [prevConfig, userConfigPath, prevConfigHash] + 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 has changed (based on timestamp), or first run - delete require.cache[userConfigPath] + // 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, modified, newHash]) - return [newConfig, userConfigPath, newHash] + configPathCache.set(userConfigPath, [newConfig, newHash, newDeps, newModified]) + return [newConfig, userConfigPath, newHash, newDeps] } // It's a plain object, not a path @@ -145,7 +158,7 @@ function getTailwindConfig(configOrPath) { configOrPath.config === undefined ? configOrPath : configOrPath.config ) - return [newConfig, null, hash(newConfig)] + return [newConfig, null, hash(newConfig), []] } function trackModified(files) { @@ -519,10 +532,15 @@ 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(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. @@ -538,14 +556,13 @@ function setupContext(configOrPath) { } } - if (isConfigFile) { - contextDependencies.add(userConfigPath) - } - - if (contextMap.has(sourcePath)) { - for (let dependency of contextMap.get(sourcePath).configDependencies) { - contextDependencies.add(dependency) - } + for (let file of configDependencies) { + result.messages.push({ + type: 'dependency', + plugin: 'tailwindcss-jit', + parent: result.opts.from, + file, + }) } let contextDependenciesChanged = trackModified([...contextDependencies]) @@ -596,7 +613,6 @@ function setupContext(configOrPath) { candidateRuleMap: new Map(), configPath: userConfigPath, tailwindConfig: tailwindConfig, - configDependencies: new Set(), candidateFiles: (Array.isArray(tailwindConfig.purge) ? tailwindConfig.purge : tailwindConfig.purge.content @@ -620,25 +636,6 @@ function setupContext(configOrPath) { // --- - if (isConfigFile) { - for (let dependency of getModuleDependencies(userConfigPath)) { - if (dependency.file === userConfigPath) { - continue - } - - context.configDependencies.add(dependency.file) - - result.messages.push({ - type: 'dependency', - plugin: 'tailwindcss-jit', - parent: result.opts.from, - file: dependency.file, - }) - - trackModified([dependency.file]) - } - } - let corePluginList = Object.entries(corePlugins) .map(([name, plugin]) => { if (!tailwindConfig.corePlugins.includes(name)) { From 5207bfdf288275ff152bbc4d730c8e315a50bc5a Mon Sep 17 00:00:00 2001 From: Brad Cornes Date: Fri, 26 Mar 2021 13:27:08 +0000 Subject: [PATCH 07/10] replace ?? --- src/lib/expandTailwindAtRules.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/lib/expandTailwindAtRules.js b/src/lib/expandTailwindAtRules.js index 7278d55..bac3924 100644 --- a/src/lib/expandTailwindAtRules.js +++ b/src/lib/expandTailwindAtRules.js @@ -161,7 +161,9 @@ function expandTailwindAtRules(context, registerDependency) { env.DEBUG && console.time('Finding changed files') let files = fastGlob.sync(context.candidateFiles) for (let file of files) { - let prevModified = sharedState.fileModifiedCache.get(file) ?? -Infinity + let prevModified = sharedState.fileModifiedCache.has(file) + ? sharedState.fileModifiedCache.get(file) + : -Infinity let modified = fs.statSync(file).mtimeMs if (!context.scannedContent || modified > prevModified) { From eeef28ae4f0b0d352b5904d2d38eafd24a636b7f Mon Sep 17 00:00:00 2001 From: Brad Cornes Date: Fri, 26 Mar 2021 16:02:33 +0000 Subject: [PATCH 08/10] put context dependencies behind TAILWIND_DISABLE_TOUCH flag --- src/index.js | 6 + src/lib/expandTailwindAtRules.js | 73 ++++++---- src/lib/setupContext.js | 229 ++++++++++++++++++++++++++----- src/lib/sharedState.js | 1 + 4 files changed, 248 insertions(+), 61 deletions(-) diff --git a/src/index.js b/src/index.js index 82fb68b..3d21d2c 100644 --- a/src/index.js +++ b/src/index.js @@ -36,6 +36,12 @@ module.exports = (configOrPath = {}) => { let context = setupContext(configOrPath)(result, root) + if (!env.TAILWIND_DISABLE_TOUCH) { + if (context.configPath !== null) { + registerDependency(context.configPath) + } + } + return postcss([ removeLayerAtRules(context), expandTailwindAtRules(context, registerDependency), diff --git a/src/lib/expandTailwindAtRules.js b/src/lib/expandTailwindAtRules.js index bac3924..bb7bb41 100644 --- a/src/lib/expandTailwindAtRules.js +++ b/src/lib/expandTailwindAtRules.js @@ -142,37 +142,56 @@ function expandTailwindAtRules(context, registerDependency) { // --- - 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 (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)) + } + } + + env.DEBUG && console.time('Finding changed files') + let files = fastGlob.sync(context.candidateFiles) + for (let file of files) { + 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) } - } - env.DEBUG && console.time('Finding changed files') - let files = fastGlob.sync(context.candidateFiles) - for (let file of files) { - 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) + // 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 } } - context.scannedContent = true - env.DEBUG && console.timeEnd('Finding changed files') // --- diff --git a/src/lib/setupContext.js b/src/lib/setupContext.js index 9dd7d1e..abac575 100644 --- a/src/lib/setupContext.js +++ b/src/lib/setupContext.js @@ -1,6 +1,9 @@ const fs = require('fs') const url = require('url') +const os = require('os') const path = require('path') +const crypto = require('crypto') +const chokidar = require('chokidar') const postcss = require('postcss') const hash = require('object-hash') const dlv = require('dlv') @@ -25,6 +28,35 @@ let configContextMap = sharedState.configContextMap let contextSourcesMap = sharedState.contextSourcesMap let env = sharedState.env +// Earmarks a directory for our touch files. +// If the directory already exists we delete any existing touch files, +// invalidating any caches associated with them. + +const touchDir = path.join(os.homedir() || os.tmpdir(), '.tailwindcss', 'touch') + +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 }) + } +} + +// This is used to trigger rebuilds. Just updating the timestamp +// is significantly faster than actually writing to the file (10x). + +function touch(filename) { + let time = new Date() + + try { + fs.utimesSync(filename, time, time) + } catch (err) { + fs.closeSync(fs.openSync(filename, 'w')) + } +} + function isObject(value) { return typeof value === 'object' && value !== null } @@ -122,35 +154,62 @@ let configPathCache = new LRU({ maxSize: 100 }) function getTailwindConfig(configOrPath) { let userConfigPath = resolveConfigPath(configOrPath) - if (userConfigPath !== null) { - let [prevConfig, prevConfigHash, prevDeps, prevModified] = - configPathCache.get(userConfigPath) || [] + 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 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 + 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 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 has changed (based on timestamps), or first run - for (let file of newDeps) { - delete require.cache[file] + // 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) || [] + let modified = fs.statSync(userConfigPath).mtimeMs + + // It hasn't changed (based on timestamp) + if (modified <= prevModified) { + return [prevConfig, userConfigPath, prevConfigHash] } + + // It has changed (based on timestamp), or first run + delete require.cache[userConfigPath] let newConfig = resolveConfig(require(userConfigPath)) let newHash = hash(newConfig) - configPathCache.set(userConfigPath, [newConfig, newHash, newDeps, newModified]) - return [newConfig, userConfigPath, newHash, newDeps] + configPathCache.set(userConfigPath, [newConfig, modified, newHash]) + return [newConfig, userConfigPath, newHash] } // It's a plain object, not a path @@ -158,9 +217,11 @@ function getTailwindConfig(configOrPath) { configOrPath.config === undefined ? configOrPath : configOrPath.config ) - return [newConfig, null, hash(newConfig), []] + return [newConfig, null, hash(newConfig)] } +let fileModifiedMap = new Map() + function trackModified(files) { let changed = false @@ -168,19 +229,89 @@ function trackModified(files) { let pathname = url.parse(file).pathname let newModified = fs.statSync(decodeURIComponent(pathname)).mtimeMs - if ( - !sharedState.fileModifiedCache.has(file) || - newModified > sharedState.fileModifiedCache.get(file) - ) { + if (!fileModifiedMap.has(file) || newModified > fileModifiedMap.get(file)) { changed = true } - sharedState.fileModifiedCache.set(file, newModified) + fileModifiedMap.set(file, newModified) } return changed } +function generateTouchFileName() { + let chars = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz' + let randomChars = '' + let randomCharsLength = 12 + let bytes = null + + try { + bytes = crypto.randomBytes(randomCharsLength) + } catch (_error) { + bytes = crypto.pseudoRandomBytes(randomCharsLength) + } + + for (let i = 0; i < randomCharsLength; i++) { + randomChars += chars[bytes[i] % chars.length] + } + + return path.join(touchDir, `touch-${process.pid}-${randomChars}`) +} + +function rebootWatcher(context) { + if (context.touchFile === null) { + context.touchFile = generateTouchFileName() + touch(context.touchFile) + } + + if (env.TAILWIND_MODE === 'build') { + return + } + + if ( + env.TAILWIND_MODE === 'watch' || + (env.TAILWIND_MODE === undefined && env.NODE_ENV === 'development') + ) { + Promise.resolve(context.watcher ? context.watcher.close() : null).then(() => { + context.watcher = chokidar.watch([...context.candidateFiles, ...context.configDependencies], { + ignoreInitial: true, + }) + + context.watcher.on('add', (file) => { + context.changedFiles.add(path.resolve('.', file)) + touch(context.touchFile) + }) + + context.watcher.on('change', (file) => { + // If it was a config dependency, touch the config file to trigger a new context. + // This is not really that clean of a solution but it's the fastest, because we + // can do a very quick check on each build to see if the config has changed instead + // of having to get all of the module dependencies and check every timestamp each + // time. + if (context.configDependencies.has(file)) { + for (let dependency of context.configDependencies) { + delete require.cache[require.resolve(dependency)] + } + touch(context.configPath) + } else { + context.changedFiles.add(path.resolve('.', file)) + touch(context.touchFile) + } + }) + + context.watcher.on('unlink', (file) => { + // Touch the config file if any of the dependencies are deleted. + if (context.configDependencies.has(file)) { + for (let dependency of context.configDependencies) { + delete require.cache[require.resolve(dependency)] + } + touch(context.configPath) + } + }) + }) + } +} + function insertInto(list, value, { before = [] } = {}) { before = [].concat(before) @@ -520,6 +651,12 @@ function registerPlugins(tailwindConfig, plugins, context) { } } +function cleanupContext(context) { + if (context.watcher) { + context.watcher.close() + } +} + // Retrieve an existing context from cache if possible (since contexts are unique per // source path), or set up a new one (including setting up watchers and registering // plugins) then return it @@ -540,7 +677,9 @@ function setupContext(configOrPath) { ] = getTailwindConfig(configOrPath) let isConfigFile = userConfigPath !== null - let contextDependencies = new Set(configDependencies) + 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. @@ -556,13 +695,19 @@ function setupContext(configOrPath) { } } - for (let file of configDependencies) { - result.messages.push({ - type: 'dependency', - plugin: 'tailwindcss-jit', - parent: result.opts.from, - file, - }) + 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]) @@ -596,6 +741,7 @@ function setupContext(configOrPath) { contextSourcesMap.get(oldContext).delete(sourcePath) if (contextSourcesMap.get(oldContext).size === 0) { contextSourcesMap.delete(oldContext) + cleanupContext(oldContext) } } } @@ -605,7 +751,9 @@ function setupContext(configOrPath) { let context = { changedFiles: new Set(), ruleCache: new Set(), + watcher: null, scannedContent: false, + touchFile: null, classCache: new Map(), applyClassCache: new Map(), notClassCache: new Set(), @@ -613,6 +761,7 @@ function setupContext(configOrPath) { candidateRuleMap: new Map(), configPath: userConfigPath, tailwindConfig: tailwindConfig, + configDependencies: new Set(), candidateFiles: (Array.isArray(tailwindConfig.purge) ? tailwindConfig.purge : tailwindConfig.purge.content @@ -636,6 +785,18 @@ function setupContext(configOrPath) { // --- + if (isConfigFile && !sharedState.env.TAILWIND_DISABLE_TOUCH) { + for (let dependency of getModuleDependencies(userConfigPath)) { + if (dependency.file === userConfigPath) { + continue + } + + context.configDependencies.add(dependency.file) + } + } + + rebootWatcher(context) + let corePluginList = Object.entries(corePlugins) .map(([name, plugin]) => { if (!tailwindConfig.corePlugins.includes(name)) { diff --git a/src/lib/sharedState.js b/src/lib/sharedState.js index b523603..3d5fb63 100644 --- a/src/lib/sharedState.js +++ b/src/lib/sharedState.js @@ -5,6 +5,7 @@ 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, }, contextMap: new Map(), configContextMap: new Map(), From 7debb6076c17b3f9f71103dac13132ee763bd607 Mon Sep 17 00:00:00 2001 From: Brad Cornes Date: Fri, 26 Mar 2021 16:05:22 +0000 Subject: [PATCH 09/10] fix flag check --- src/lib/setupContext.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib/setupContext.js b/src/lib/setupContext.js index abac575..cd7663d 100644 --- a/src/lib/setupContext.js +++ b/src/lib/setupContext.js @@ -34,7 +34,7 @@ let env = sharedState.env const touchDir = path.join(os.homedir() || os.tmpdir(), '.tailwindcss', 'touch') -if (sharedState.env.TAILWIND_DISABLE_TOUCH) { +if (!sharedState.env.TAILWIND_DISABLE_TOUCH) { if (fs.existsSync(touchDir)) { for (let file of fs.readdirSync(touchDir)) { fs.unlinkSync(path.join(touchDir, file)) From 0c4f1e508edb9fc8a133ef46437d3155d51a6fc9 Mon Sep 17 00:00:00 2001 From: Brad Cornes Date: Fri, 26 Mar 2021 16:05:32 +0000 Subject: [PATCH 10/10] update flag value --- src/lib/sharedState.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib/sharedState.js b/src/lib/sharedState.js index 3d5fb63..c59800f 100644 --- a/src/lib/sharedState.js +++ b/src/lib/sharedState.js @@ -5,7 +5,7 @@ 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, + TAILWIND_DISABLE_TOUCH: process.env.TAILWIND_DISABLE_TOUCH !== undefined, }, contextMap: new Map(), configContextMap: new Map(),