From c2551f7d107772bbb589add2fc36bcab8ffcc8f9 Mon Sep 17 00:00:00 2001 From: Adam Wathan Date: Sat, 6 Mar 2021 10:45:06 -0500 Subject: [PATCH 1/3] WIP This is hard, no idea if we can make this work, I can't figure out how to share contexts between Vue files that use the same config. --- src/index.js | 13 +++++- src/lib/expandTailwindAtRules.js | 1 + src/lib/setupContext.js | 74 +++++++++++++++++++++++++++----- src/lib/sharedState.js | 2 + 4 files changed, 78 insertions(+), 12 deletions(-) diff --git a/src/index.js b/src/index.js index 9863d63..a03cf06 100644 --- a/src/index.js +++ b/src/index.js @@ -31,7 +31,18 @@ module.exports = (configOrPath = {}) => { }) } - let context = setupContext(configOrPath)(result, root) + // TODO: Maybe we only set up a context + run context dependent plugins + // on files that contain @tailwind rules? I don't know. + let foundTailwind = false + root.walkAtRules('tailwind', (rule) => { + foundTailwind = true + }) + + let context = null + + if (foundTailwind) { + context = setupContext(configOrPath)(result, root) + } if (context.configPath !== null) { registerDependency(context.configPath) diff --git a/src/lib/expandTailwindAtRules.js b/src/lib/expandTailwindAtRules.js index e372c80..d261092 100644 --- a/src/lib/expandTailwindAtRules.js +++ b/src/lib/expandTailwindAtRules.js @@ -207,6 +207,7 @@ function expandTailwindAtRules(context, registerDependency) { console.log('Changed files: ', context.changedFiles.size) console.log('Potential classes: ', candidates.size) console.log('Active contexts: ', sharedState.contextMap.size) + // console.log('Context source map: ', sharedState.contextSourcesMap) console.log('Content match entries', contentMatchCache.size) } diff --git a/src/lib/setupContext.js b/src/lib/setupContext.js index 3ab31b6..5724625 100644 --- a/src/lib/setupContext.js +++ b/src/lib/setupContext.js @@ -1,9 +1,11 @@ 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') const selectorParser = require('postcss-selector-parser') const LRU = require('quick-lru') @@ -21,6 +23,8 @@ const { isPlainObject } = require('./utils') const { isBuffer } = require('util') let contextMap = sharedState.contextMap +let configContextMap = sharedState.configContextMap +let contextSourcesMap = sharedState.contextSourcesMap let env = sharedState.env // Earmarks a directory for our touch files. @@ -148,19 +152,21 @@ function getTailwindConfig(configOrPath) { let userConfigPath = resolveConfigPath(configOrPath) if (userConfigPath !== null) { - let [prevConfig, prevModified = -Infinity] = configPathCache.get(userConfigPath) ?? [] + 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] + return [prevConfig, userConfigPath, prevConfigHash] } // It has changed (based on timestamp), or first run delete require.cache[userConfigPath] let newConfig = resolveConfig(require(userConfigPath)) - configPathCache.set(userConfigPath, [newConfig, modified]) - return [newConfig, userConfigPath] + let newHash = hash(newConfig) + configPathCache.set(userConfigPath, [newConfig, modified, newHash]) + return [newConfig, userConfigPath, newHash] } // It's a plain object, not a path @@ -168,7 +174,7 @@ function getTailwindConfig(configOrPath) { configOrPath.config === undefined ? configOrPath : configOrPath.config ) - return [newConfig, null] + return [newConfig, null, hash(newConfig)] } let fileModifiedMap = new Map() @@ -177,13 +183,14 @@ function trackModified(files) { let changed = false for (let file of files) { - let newModified = fs.statSync(file).mtimeMs + let pathname = url.parse(file).pathname + let newModified = fs.statSync(pathname).mtimeMs - if (!fileModifiedMap.has(file) || newModified > fileModifiedMap.get(file)) { + if (!fileModifiedMap.has(pathname) || newModified > fileModifiedMap.get(pathname)) { changed = true } - fileModifiedMap.set(file, newModified) + fileModifiedMap.set(pathname, newModified) } return changed @@ -538,7 +545,7 @@ function cleanupContext(context) { function setupContext(configOrPath) { return (result, root) => { let sourcePath = result.opts.from - let [tailwindConfig, userConfigPath] = getTailwindConfig(configOrPath) + let [tailwindConfig, userConfigPath, tailwindConfigHash] = getTailwindConfig(configOrPath) let contextDependencies = new Set() contextDependencies.add(sourcePath) @@ -557,12 +564,38 @@ function setupContext(configOrPath) { trackModified([...contextDependencies]) || userConfigPath === null process.env.DEBUG && console.log('Source path:', sourcePath) + + // If this file already has a context in the cache and we don't need to + // reset the context, return the cached context. if (contextMap.has(sourcePath) && !contextDependenciesChanged) { return contextMap.get(sourcePath) } + // If the config file used already exists in the cache, return that. + console.log('dependencies changed: ', contextDependenciesChanged) + console.log(tailwindConfigHash) + console.log(configContextMap.keys()) + console.log(configContextMap.has(tailwindConfigHash)) + + // DAMN YOU CONTEXTDEPENDENCIESCHANGED + // Ideas: + // - Can we incorporate the presence of @tailwind rules somehow? If it has no Tailwind rules, be + // more lenient about the dependencies changing? + if (!contextDependenciesChanged && configContextMap.has(tailwindConfigHash)) { + let context = configContextMap.get(tailwindConfigHash) + contextSourcesMap.get(context).add(sourcePath) + contextMap.set(sourcePath, context) + return context + } + + // TODO: Update this to no longer associate this path with its old context + // and clean up that context if no one else is using it. if (contextMap.has(sourcePath)) { - cleanupContext(contextMap.get(sourcePath)) + let oldContext = contextMap.get(sourcePath) + contextSourcesMap.get(oldContext).remove(sourcePath) + if (contextSourcesMap.get(oldContext).size === 0) { + cleanupContext(oldContext) + } } process.env.DEBUG && console.log('Setting up new context...') @@ -579,7 +612,6 @@ function setupContext(configOrPath) { newPostCssNodeCache: new Map(), candidateRuleMap: new Map(), configPath: userConfigPath, - sourcePath: sourcePath, tailwindConfig: tailwindConfig, configDependencies: new Set(), candidateFiles: Array.isArray(tailwindConfig.purge) @@ -588,8 +620,22 @@ function setupContext(configOrPath) { variantMap: new Map(), stylesheetCache: null, } + + // --- + + // Update all context tracking state + + configContextMap.set(tailwindConfigHash, context) contextMap.set(sourcePath, context) + if (!contextSourcesMap.has(context)) { + contextSourcesMap.set(context, new Set()) + } + + contextSourcesMap.get(context).add(sourcePath) + + // --- + if (userConfigPath !== null) { for (let dependency of getModuleDependencies(userConfigPath)) { if (dependency.file === userConfigPath) { @@ -600,6 +646,12 @@ function setupContext(configOrPath) { } } + let prev = null + for (let [key, value] of contextSourcesMap) { + console.log(key === prev) + prev = key + } + rebootWatcher(context) let corePluginList = Object.entries(corePlugins) diff --git a/src/lib/sharedState.js b/src/lib/sharedState.js index 32c593e..51f4f61 100644 --- a/src/lib/sharedState.js +++ b/src/lib/sharedState.js @@ -7,5 +7,7 @@ module.exports = { DEBUG: process.env.DEBUG !== undefined, }, contextMap: new Map(), + configContextMap: new Map(), + contextSourcesMap: new Map(), contentMatchCache: new LRU({ maxSize: 25000 }), } From 6de7f0480939fa47c39134d8f20257e1b7b48ea4 Mon Sep 17 00:00:00 2001 From: Adam Wathan Date: Sat, 6 Mar 2021 19:19:44 -0500 Subject: [PATCH 2/3] If CSS file has no `@tailwind` rules, don't consider it a context dependency --- src/index.js | 13 +------ src/lib/expandTailwindAtRules.js | 3 +- src/lib/setupContext.js | 58 +++++++++++++++++--------------- 3 files changed, 33 insertions(+), 41 deletions(-) diff --git a/src/index.js b/src/index.js index a03cf06..9863d63 100644 --- a/src/index.js +++ b/src/index.js @@ -31,18 +31,7 @@ module.exports = (configOrPath = {}) => { }) } - // TODO: Maybe we only set up a context + run context dependent plugins - // on files that contain @tailwind rules? I don't know. - let foundTailwind = false - root.walkAtRules('tailwind', (rule) => { - foundTailwind = true - }) - - let context = null - - if (foundTailwind) { - context = setupContext(configOrPath)(result, root) - } + let context = setupContext(configOrPath)(result, root) if (context.configPath !== null) { registerDependency(context.configPath) diff --git a/src/lib/expandTailwindAtRules.js b/src/lib/expandTailwindAtRules.js index d261092..0e19b6d 100644 --- a/src/lib/expandTailwindAtRules.js +++ b/src/lib/expandTailwindAtRules.js @@ -206,8 +206,7 @@ function expandTailwindAtRules(context, registerDependency) { if (env.DEBUG) { console.log('Changed files: ', context.changedFiles.size) console.log('Potential classes: ', candidates.size) - console.log('Active contexts: ', sharedState.contextMap.size) - // console.log('Context source map: ', sharedState.contextSourcesMap) + console.log('Active contexts: ', sharedState.contextSourcesMap.size) console.log('Content match entries', contentMatchCache.size) } diff --git a/src/lib/setupContext.js b/src/lib/setupContext.js index 5724625..29fdd6c 100644 --- a/src/lib/setupContext.js +++ b/src/lib/setupContext.js @@ -544,20 +544,33 @@ function cleanupContext(context) { // plugins) then return it function setupContext(configOrPath) { return (result, root) => { + let foundTailwind = false + + root.walkAtRules('tailwind', (rule) => { + foundTailwind = true + }) + let sourcePath = result.opts.from let [tailwindConfig, userConfigPath, tailwindConfigHash] = getTailwindConfig(configOrPath) let contextDependencies = new Set() - contextDependencies.add(sourcePath) - if (userConfigPath !== null) { - contextDependencies.add(userConfigPath) + // 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. + // We may want to think about `@layer` being part of this trigger too, but it's tough + // because it's impossible for a layer in one file to end up in the actual @tailwind rule + // in another file since independent sources are effectively isolated. + if (foundTailwind) { + contextDependencies.add(sourcePath) + for (let message of result.messages) { + if (message.type === 'dependency') { + contextDependencies.add(message.file) + } + } } - for (let message of result.messages) { - if (message.type === 'dependency') { - contextDependencies.add(message.file) - } + if (userConfigPath !== null) { + contextDependencies.add(userConfigPath) } let contextDependenciesChanged = @@ -572,15 +585,6 @@ function setupContext(configOrPath) { } // If the config file used already exists in the cache, return that. - console.log('dependencies changed: ', contextDependenciesChanged) - console.log(tailwindConfigHash) - console.log(configContextMap.keys()) - console.log(configContextMap.has(tailwindConfigHash)) - - // DAMN YOU CONTEXTDEPENDENCIESCHANGED - // Ideas: - // - Can we incorporate the presence of @tailwind rules somehow? If it has no Tailwind rules, be - // more lenient about the dependencies changing? if (!contextDependenciesChanged && configContextMap.has(tailwindConfigHash)) { let context = configContextMap.get(tailwindConfigHash) contextSourcesMap.get(context).add(sourcePath) @@ -588,13 +592,19 @@ function setupContext(configOrPath) { return context } - // TODO: Update this to no longer associate this path with its old context - // and clean up that context if no one else is using it. + // If this source is in the context map, get the old context. + // Remove this source from the context sources for the old context, + // and clean up that context if no one else is using it. This can be + // called by many processes in rapid succession, so we check for presence + // first because the first process to run this code will wipe it out first. if (contextMap.has(sourcePath)) { let oldContext = contextMap.get(sourcePath) - contextSourcesMap.get(oldContext).remove(sourcePath) - if (contextSourcesMap.get(oldContext).size === 0) { - cleanupContext(oldContext) + if (contextSourcesMap.has(oldContext)) { + contextSourcesMap.get(oldContext).remove(sourcePath) + if (contextSourcesMap.get(oldContext).size === 0) { + contextSourcesMap.delete(oldContext) + cleanupContext(oldContext) + } } } @@ -646,12 +656,6 @@ function setupContext(configOrPath) { } } - let prev = null - for (let [key, value] of contextSourcesMap) { - console.log(key === prev) - prev = key - } - rebootWatcher(context) let corePluginList = Object.entries(corePlugins) From 53e5b526f610b33c434c7bc471f054503289d4a2 Mon Sep 17 00:00:00 2001 From: Adam Wathan Date: Sat, 6 Mar 2021 19:21:10 -0500 Subject: [PATCH 3/3] Use file name as keys, even including query string --- src/lib/setupContext.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/lib/setupContext.js b/src/lib/setupContext.js index 29fdd6c..8c50c0c 100644 --- a/src/lib/setupContext.js +++ b/src/lib/setupContext.js @@ -186,11 +186,11 @@ function trackModified(files) { let pathname = url.parse(file).pathname let newModified = fs.statSync(pathname).mtimeMs - if (!fileModifiedMap.has(pathname) || newModified > fileModifiedMap.get(pathname)) { + if (!fileModifiedMap.has(file) || newModified > fileModifiedMap.get(file)) { changed = true } - fileModifiedMap.set(pathname, newModified) + fileModifiedMap.set(file, newModified) } return changed