mirror of
https://github.com/tailwindlabs/tailwindcss.git
synced 2025-12-08 21:36:08 +00:00
Split context (#4489)
* Remove watcher from context * WIP * WIP * format * abstract configDependencies * abstract fileModifiedMap * abstract candidateFiles * abstract changedContent to each strategy * little bit of cleanup * formatting Co-authored-by: Adam Wathan <adam.wathan@gmail.com>
This commit is contained in:
parent
d1e9632064
commit
dfdfba25a2
@ -1,124 +0,0 @@
|
||||
// @ts-check
|
||||
|
||||
import chokidar from 'chokidar'
|
||||
import crypto from 'crypto'
|
||||
import fs from 'fs'
|
||||
import os from 'os'
|
||||
import path from 'path'
|
||||
import log from '../../util/log'
|
||||
import { env } from './sharedState'
|
||||
|
||||
// Earmarks a directory for our touch files.
|
||||
// If the directory already exists we delete any existing touch files,
|
||||
// invalidating any caches associated with them.
|
||||
let touchDir =
|
||||
env.TAILWIND_TOUCH_DIR || path.join(os.homedir() || os.tmpdir(), '.tailwindcss', 'touch')
|
||||
|
||||
if (!env.TAILWIND_DISABLE_TOUCH) {
|
||||
if (fs.existsSync(touchDir)) {
|
||||
for (let file of fs.readdirSync(touchDir)) {
|
||||
try {
|
||||
fs.unlinkSync(path.join(touchDir, file))
|
||||
} catch (_err) {}
|
||||
}
|
||||
} 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'))
|
||||
}
|
||||
}
|
||||
|
||||
export 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(() => {
|
||||
log.info([
|
||||
'Tailwind CSS is watching for changes...',
|
||||
'https://tailwindcss.com/docs/just-in-time-mode#watch-mode-and-one-off-builds',
|
||||
])
|
||||
|
||||
context.watcher = chokidar.watch([...context.candidateFiles, ...context.configDependencies], {
|
||||
ignoreInitial: true,
|
||||
})
|
||||
|
||||
context.watcher.on('add', (file) => {
|
||||
let changedFile = path.resolve('.', file)
|
||||
let content = fs.readFileSync(changedFile, 'utf8')
|
||||
let extension = path.extname(changedFile).slice(1)
|
||||
context.changedContent.push({ content, extension })
|
||||
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 {
|
||||
let changedFile = path.resolve('.', file)
|
||||
let content = fs.readFileSync(changedFile, 'utf8')
|
||||
let extension = path.extname(changedFile).slice(1)
|
||||
context.changedContent.push({ content, extension })
|
||||
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 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}`)
|
||||
}
|
||||
@ -1,10 +1,8 @@
|
||||
import fs from 'fs'
|
||||
import url from 'url'
|
||||
import path from 'path'
|
||||
import postcss from 'postcss'
|
||||
import dlv from 'dlv'
|
||||
import selectorParser from 'postcss-selector-parser'
|
||||
import normalizePath from 'normalize-path'
|
||||
|
||||
import transformThemeValue from '../../util/transformThemeValue'
|
||||
import parseObjectStyles from '../../util/parseObjectStyles'
|
||||
@ -304,7 +302,15 @@ function buildPluginApi(tailwindConfig, context, { variantList, variantMap, offs
|
||||
}
|
||||
}
|
||||
|
||||
function trackModified(files, context) {
|
||||
let fileModifiedMapCache = new WeakMap()
|
||||
export function getFileModifiedMap(context) {
|
||||
if (!fileModifiedMapCache.has(context)) {
|
||||
fileModifiedMapCache.set(context, new Map())
|
||||
}
|
||||
return fileModifiedMapCache.get(context)
|
||||
}
|
||||
|
||||
function trackModified(files, fileModifiedMap) {
|
||||
let changed = false
|
||||
|
||||
for (let file of files) {
|
||||
@ -314,11 +320,11 @@ function trackModified(files, context) {
|
||||
let pathname = parsed.href.replace(parsed.hash, '').replace(parsed.search, '')
|
||||
let newModified = fs.statSync(decodeURIComponent(pathname)).mtimeMs
|
||||
|
||||
if (!context.fileModifiedMap.has(file) || newModified > context.fileModifiedMap.get(file)) {
|
||||
if (!fileModifiedMap.has(file) || newModified > fileModifiedMap.get(file)) {
|
||||
changed = true
|
||||
}
|
||||
|
||||
context.fileModifiedMap.set(file, newModified)
|
||||
fileModifiedMap.set(file, newModified)
|
||||
}
|
||||
|
||||
return changed
|
||||
@ -477,48 +483,18 @@ let contextMap = sharedState.contextMap
|
||||
let configContextMap = sharedState.configContextMap
|
||||
let contextSourcesMap = sharedState.contextSourcesMap
|
||||
|
||||
function cleanupContext(context) {
|
||||
if (context.watcher) {
|
||||
context.watcher.close()
|
||||
}
|
||||
}
|
||||
|
||||
export function getContext(
|
||||
configOrPath,
|
||||
tailwindDirectives,
|
||||
registerDependency,
|
||||
root,
|
||||
result,
|
||||
getTailwindConfig
|
||||
tailwindConfig,
|
||||
userConfigPath,
|
||||
tailwindConfigHash,
|
||||
contextDependencies
|
||||
) {
|
||||
let sourcePath = result.opts.from
|
||||
let [tailwindConfig, userConfigPath, tailwindConfigHash, configDependencies] =
|
||||
getTailwindConfig(configOrPath)
|
||||
let isConfigFile = userConfigPath !== null
|
||||
|
||||
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.
|
||||
// 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 (tailwindDirectives.size > 0) {
|
||||
// Add current css file as a context dependencies.
|
||||
contextDependencies.add(sourcePath)
|
||||
|
||||
// Add all css @import dependencies as context dependencies.
|
||||
for (let message of result.messages) {
|
||||
if (message.type === 'dependency') {
|
||||
contextDependencies.add(message.file)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (let file of configDependencies) {
|
||||
registerDependency(file)
|
||||
}
|
||||
|
||||
env.DEBUG && console.log('Source path:', sourcePath)
|
||||
|
||||
let existingContext
|
||||
@ -536,7 +512,10 @@ export function getContext(
|
||||
// If there's already a context in the cache and we don't need to
|
||||
// reset the context, return the cached context.
|
||||
if (existingContext) {
|
||||
let contextDependenciesChanged = trackModified([...contextDependencies], existingContext)
|
||||
let contextDependenciesChanged = trackModified(
|
||||
[...contextDependencies],
|
||||
getFileModifiedMap(existingContext)
|
||||
)
|
||||
if (!contextDependenciesChanged) {
|
||||
return [existingContext, false]
|
||||
}
|
||||
@ -553,49 +532,30 @@ export function getContext(
|
||||
contextSourcesMap.get(oldContext).delete(sourcePath)
|
||||
if (contextSourcesMap.get(oldContext).size === 0) {
|
||||
contextSourcesMap.delete(oldContext)
|
||||
cleanupContext(oldContext)
|
||||
for (let disposable of oldContext.disposables.splice(0)) {
|
||||
disposable(oldContext)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
env.DEBUG && console.log('Setting up new context...')
|
||||
|
||||
let purgeContent = Array.isArray(tailwindConfig.purge)
|
||||
? tailwindConfig.purge
|
||||
: tailwindConfig.purge.content
|
||||
|
||||
let context = {
|
||||
watcher: null,
|
||||
touchFile: null,
|
||||
configPath: userConfigPath,
|
||||
configDependencies: new Set(),
|
||||
candidateFiles: purgeContent
|
||||
.filter((item) => typeof item === 'string')
|
||||
.map((purgePath) =>
|
||||
normalizePath(
|
||||
path.resolve(
|
||||
userConfigPath === null ? process.cwd() : path.dirname(userConfigPath),
|
||||
purgePath
|
||||
)
|
||||
)
|
||||
),
|
||||
fileModifiedMap: new Map(),
|
||||
// ---
|
||||
ruleCache: new Set(), // Hit
|
||||
classCache: new Map(), // Hit
|
||||
applyClassCache: new Map(), // Hit
|
||||
notClassCache: new Set(), // Hit
|
||||
postCssNodeCache: new Map(), // Hit
|
||||
candidateRuleMap: new Map(), // Hit
|
||||
tailwindConfig: tailwindConfig, // Hit
|
||||
changedContent: purgeContent // Hit
|
||||
.filter((item) => typeof item.raw === 'string')
|
||||
.map(({ raw, extension }) => ({ content: raw, extension })),
|
||||
variantMap: new Map(), // Hit
|
||||
stylesheetCache: null, // Hit
|
||||
disposables: [],
|
||||
ruleCache: new Set(),
|
||||
classCache: new Map(),
|
||||
applyClassCache: new Map(),
|
||||
notClassCache: new Set(),
|
||||
postCssNodeCache: new Map(),
|
||||
candidateRuleMap: new Map(),
|
||||
tailwindConfig,
|
||||
changedContent: [],
|
||||
variantMap: new Map(),
|
||||
stylesheetCache: null,
|
||||
}
|
||||
|
||||
trackModified([...contextDependencies], context)
|
||||
trackModified([...contextDependencies], getFileModifiedMap(context))
|
||||
|
||||
// ---
|
||||
|
||||
|
||||
@ -5,6 +5,7 @@ import fastGlob from 'fast-glob'
|
||||
import isGlob from 'is-glob'
|
||||
import globParent from 'glob-parent'
|
||||
import LRU from 'quick-lru'
|
||||
import normalizePath from 'normalize-path'
|
||||
|
||||
import hash from '../../util/hashConfig'
|
||||
import getModuleDependencies from '../../lib/getModuleDependencies'
|
||||
@ -15,10 +16,29 @@ import resolveConfigPath from '../../util/resolveConfigPath'
|
||||
|
||||
import { env } from './sharedState'
|
||||
|
||||
import { getContext } from './setupContextUtils'
|
||||
import { getContext, getFileModifiedMap } from './setupContextUtils'
|
||||
|
||||
let configPathCache = new LRU({ maxSize: 100 })
|
||||
|
||||
let candidateFilesCache = new WeakMap()
|
||||
|
||||
function getCandidateFiles(context, userConfigPath, tailwindConfig) {
|
||||
if (candidateFilesCache.has(context)) {
|
||||
return candidateFilesCache.get(context)
|
||||
}
|
||||
|
||||
let purgeContent = Array.isArray(tailwindConfig.purge)
|
||||
? tailwindConfig.purge
|
||||
: tailwindConfig.purge.content
|
||||
|
||||
let basePath = userConfigPath === null ? process.cwd() : path.dirname(userConfigPath)
|
||||
let candidateFiles = purgeContent
|
||||
.filter((item) => typeof item === 'string')
|
||||
.map((purgePath) => normalizePath(path.resolve(basePath, purgePath)))
|
||||
|
||||
return candidateFilesCache.set(context, candidateFiles).get(context)
|
||||
}
|
||||
|
||||
// Get the config object based on a path
|
||||
function getTailwindConfig(configOrPath) {
|
||||
let userConfigPath = resolveConfigPath(configOrPath)
|
||||
@ -62,19 +82,34 @@ function getTailwindConfig(configOrPath) {
|
||||
return [newConfig, null, hash(newConfig), []]
|
||||
}
|
||||
|
||||
function resolveChangedFiles(context) {
|
||||
function resolvedChangedContent(context, candidateFiles, fileModifiedMap) {
|
||||
let changedContent = (
|
||||
Array.isArray(context.tailwindConfig.purge)
|
||||
? context.tailwindConfig.purge
|
||||
: context.tailwindConfig.purge.content
|
||||
)
|
||||
.filter((item) => typeof item.raw === 'string')
|
||||
.map(({ raw, extension }) => ({ content: raw, extension }))
|
||||
|
||||
for (let changedFile of resolveChangedFiles(candidateFiles, fileModifiedMap)) {
|
||||
let content = fs.readFileSync(changedFile, 'utf8')
|
||||
let extension = path.extname(changedFile).slice(1)
|
||||
changedContent.push({ content, extension })
|
||||
}
|
||||
return changedContent
|
||||
}
|
||||
|
||||
function resolveChangedFiles(candidateFiles, fileModifiedMap) {
|
||||
let changedFiles = new Set()
|
||||
env.DEBUG && console.time('Finding changed files')
|
||||
let files = fastGlob.sync(context.candidateFiles)
|
||||
let files = fastGlob.sync(candidateFiles)
|
||||
for (let file of files) {
|
||||
let prevModified = context.fileModifiedMap.has(file)
|
||||
? context.fileModifiedMap.get(file)
|
||||
: -Infinity
|
||||
let prevModified = fileModifiedMap.has(file) ? fileModifiedMap.get(file) : -Infinity
|
||||
let modified = fs.statSync(file).mtimeMs
|
||||
|
||||
if (modified > prevModified) {
|
||||
changedFiles.add(file)
|
||||
context.fileModifiedMap.set(file, modified)
|
||||
fileModifiedMap.set(file, modified)
|
||||
}
|
||||
}
|
||||
env.DEBUG && console.timeEnd('Finding changed files')
|
||||
@ -88,14 +123,10 @@ function resolveChangedFiles(context) {
|
||||
// plugins) then return it
|
||||
export default function setupTrackingContext(configOrPath, tailwindDirectives, registerDependency) {
|
||||
return (result, root) => {
|
||||
let [context] = getContext(
|
||||
configOrPath,
|
||||
tailwindDirectives,
|
||||
registerDependency,
|
||||
root,
|
||||
result,
|
||||
getTailwindConfig
|
||||
)
|
||||
let [tailwindConfig, userConfigPath, tailwindConfigHash, configDependencies] =
|
||||
getTailwindConfig(configOrPath)
|
||||
|
||||
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.
|
||||
@ -103,8 +134,39 @@ export default function setupTrackingContext(configOrPath, tailwindDirectives, r
|
||||
// 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 (tailwindDirectives.size > 0) {
|
||||
// Add current css file as a context dependencies.
|
||||
contextDependencies.add(result.opts.from)
|
||||
|
||||
// Add all css @import dependencies as context dependencies.
|
||||
for (let message of result.messages) {
|
||||
if (message.type === 'dependency') {
|
||||
contextDependencies.add(message.file)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let [context] = getContext(
|
||||
tailwindDirectives,
|
||||
root,
|
||||
result,
|
||||
tailwindConfig,
|
||||
userConfigPath,
|
||||
tailwindConfigHash,
|
||||
contextDependencies
|
||||
)
|
||||
|
||||
let candidateFiles = getCandidateFiles(context, userConfigPath, tailwindConfig)
|
||||
|
||||
// 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 (tailwindDirectives.size > 0) {
|
||||
let fileModifiedMap = getFileModifiedMap(context)
|
||||
|
||||
// Add template paths as postcss dependencies.
|
||||
for (let maybeGlob of context.candidateFiles) {
|
||||
for (let maybeGlob of candidateFiles) {
|
||||
if (isGlob(maybeGlob)) {
|
||||
// rollup-plugin-postcss does not support dir-dependency messages
|
||||
// but directories can be watched in the same way as files
|
||||
@ -117,13 +179,15 @@ export default function setupTrackingContext(configOrPath, tailwindDirectives, r
|
||||
}
|
||||
}
|
||||
|
||||
for (let changedFile of resolveChangedFiles(context)) {
|
||||
let content = fs.readFileSync(changedFile, 'utf8')
|
||||
let extension = path.extname(changedFile).slice(1)
|
||||
context.changedContent.push({ content, extension })
|
||||
for (let changedContent of resolvedChangedContent(context, candidateFiles, fileModifiedMap)) {
|
||||
context.changedContent.push(changedContent)
|
||||
}
|
||||
}
|
||||
|
||||
for (let file of configDependencies) {
|
||||
registerDependency(file)
|
||||
}
|
||||
|
||||
return context
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,21 +1,212 @@
|
||||
import fs from 'fs'
|
||||
import path from 'path'
|
||||
import crypto from 'crypto'
|
||||
import os from 'os'
|
||||
|
||||
import chokidar from 'chokidar'
|
||||
import fastGlob from 'fast-glob'
|
||||
import LRU from 'quick-lru'
|
||||
import normalizePath from 'normalize-path'
|
||||
|
||||
import hash from '../../util/hashConfig'
|
||||
import log from '../../util/log'
|
||||
import getModuleDependencies from '../../lib/getModuleDependencies'
|
||||
|
||||
import resolveConfig from '../../../resolveConfig'
|
||||
|
||||
import resolveConfigPath from '../../util/resolveConfigPath'
|
||||
|
||||
import { rebootWatcher } from './rebootWatcher'
|
||||
import { getContext } from './setupContextUtils'
|
||||
import { env } from './sharedState'
|
||||
|
||||
// Earmarks a directory for our touch files.
|
||||
// If the directory already exists we delete any existing touch files,
|
||||
// invalidating any caches associated with them.
|
||||
let touchDir =
|
||||
env.TAILWIND_TOUCH_DIR || path.join(os.homedir() || os.tmpdir(), '.tailwindcss', 'touch')
|
||||
|
||||
if (!env.TAILWIND_DISABLE_TOUCH) {
|
||||
if (fs.existsSync(touchDir)) {
|
||||
for (let file of fs.readdirSync(touchDir)) {
|
||||
try {
|
||||
fs.unlinkSync(path.join(touchDir, file))
|
||||
} catch (_err) {}
|
||||
}
|
||||
} 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'))
|
||||
}
|
||||
}
|
||||
|
||||
let watchers = new WeakMap()
|
||||
|
||||
function getWatcher(context) {
|
||||
if (watchers.has(context)) {
|
||||
return watchers.get(context)
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
function setWatcher(context, watcher) {
|
||||
return watchers.set(context, watcher)
|
||||
}
|
||||
|
||||
let touchFiles = new WeakMap()
|
||||
|
||||
function getTouchFile(context) {
|
||||
if (touchFiles.has(context)) {
|
||||
return touchFiles.get(context)
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
function setTouchFile(context, touchFile) {
|
||||
return touchFiles.set(context, touchFile)
|
||||
}
|
||||
|
||||
let configPaths = new WeakMap()
|
||||
|
||||
function getConfigPath(context, configOrPath) {
|
||||
if (!configPaths.has(context)) {
|
||||
configPaths.set(context, resolveConfigPath(configOrPath))
|
||||
}
|
||||
|
||||
return configPaths.get(context)
|
||||
}
|
||||
|
||||
function rebootWatcher(context, configPath, configDependencies, candidateFiles) {
|
||||
let touchFile = getTouchFile(context)
|
||||
|
||||
if (touchFile === null) {
|
||||
touchFile = generateTouchFileName()
|
||||
setTouchFile(context, touchFile)
|
||||
touch(touchFile)
|
||||
}
|
||||
|
||||
if (env.TAILWIND_MODE === 'build') {
|
||||
return
|
||||
}
|
||||
|
||||
if (
|
||||
env.TAILWIND_MODE === 'watch' ||
|
||||
(env.TAILWIND_MODE === undefined && env.NODE_ENV === 'development')
|
||||
) {
|
||||
let watcher = getWatcher(context)
|
||||
|
||||
Promise.resolve(watcher ? watcher.close() : null).then(() => {
|
||||
log.info([
|
||||
'Tailwind CSS is watching for changes...',
|
||||
'https://tailwindcss.com/docs/just-in-time-mode#watch-mode-and-one-off-builds',
|
||||
])
|
||||
|
||||
watcher = chokidar.watch([...candidateFiles, ...configDependencies], {
|
||||
ignoreInitial: true,
|
||||
})
|
||||
|
||||
setWatcher(context, watcher)
|
||||
|
||||
watcher.on('add', (file) => {
|
||||
let changedFile = path.resolve('.', file)
|
||||
let content = fs.readFileSync(changedFile, 'utf8')
|
||||
let extension = path.extname(changedFile).slice(1)
|
||||
context.changedContent.push({ content, extension })
|
||||
touch(touchFile)
|
||||
})
|
||||
|
||||
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 (configDependencies.has(file)) {
|
||||
for (let dependency of configDependencies) {
|
||||
delete require.cache[require.resolve(dependency)]
|
||||
}
|
||||
touch(configPath)
|
||||
} else {
|
||||
let changedFile = path.resolve('.', file)
|
||||
let content = fs.readFileSync(changedFile, 'utf8')
|
||||
let extension = path.extname(changedFile).slice(1)
|
||||
context.changedContent.push({ content, extension })
|
||||
touch(touchFile)
|
||||
}
|
||||
})
|
||||
|
||||
watcher.on('unlink', (file) => {
|
||||
// Touch the config file if any of the dependencies are deleted.
|
||||
if (configDependencies.has(file)) {
|
||||
for (let dependency of configDependencies) {
|
||||
delete require.cache[require.resolve(dependency)]
|
||||
}
|
||||
touch(configPath)
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
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}`)
|
||||
}
|
||||
|
||||
let configPathCache = new LRU({ maxSize: 100 })
|
||||
|
||||
let configDependenciesCache = new WeakMap()
|
||||
|
||||
function getConfigDependencies(context) {
|
||||
if (!configDependenciesCache.has(context)) {
|
||||
configDependenciesCache.set(context, new Set())
|
||||
}
|
||||
|
||||
return configDependenciesCache.get(context)
|
||||
}
|
||||
|
||||
let candidateFilesCache = new WeakMap()
|
||||
|
||||
function getCandidateFiles(context, userConfigPath, tailwindConfig) {
|
||||
if (candidateFilesCache.has(context)) {
|
||||
return candidateFilesCache.get(context)
|
||||
}
|
||||
|
||||
let purgeContent = Array.isArray(tailwindConfig.purge)
|
||||
? tailwindConfig.purge
|
||||
: tailwindConfig.purge.content
|
||||
|
||||
let basePath = userConfigPath === null ? process.cwd() : path.dirname(userConfigPath)
|
||||
let candidateFiles = purgeContent
|
||||
.filter((item) => typeof item === 'string')
|
||||
.map((purgePath) => normalizePath(path.resolve(basePath, purgePath)))
|
||||
|
||||
return candidateFilesCache.set(context, candidateFiles).get(context)
|
||||
}
|
||||
|
||||
// Get the config object based on a path
|
||||
function getTailwindConfig(configOrPath) {
|
||||
let userConfigPath = resolveConfigPath(configOrPath)
|
||||
@ -46,18 +237,37 @@ function getTailwindConfig(configOrPath) {
|
||||
return [newConfig, null, hash(newConfig), [userConfigPath]]
|
||||
}
|
||||
|
||||
function resolveChangedFiles(context) {
|
||||
function resolvedChangedContent(context, candidateFiles) {
|
||||
let changedContent = (
|
||||
Array.isArray(context.tailwindConfig.purge)
|
||||
? context.tailwindConfig.purge
|
||||
: context.tailwindConfig.purge.content
|
||||
)
|
||||
.filter((item) => typeof item.raw === 'string')
|
||||
.map(({ raw, extension }) => ({ content: raw, extension }))
|
||||
|
||||
for (let changedFile of resolveChangedFiles(context, candidateFiles)) {
|
||||
let content = fs.readFileSync(changedFile, 'utf8')
|
||||
let extension = path.extname(changedFile).slice(1)
|
||||
changedContent.push({ content, extension })
|
||||
}
|
||||
return changedContent
|
||||
}
|
||||
|
||||
let scannedContentCache = new WeakMap()
|
||||
|
||||
function resolveChangedFiles(context, candidateFiles) {
|
||||
let changedFiles = new Set()
|
||||
|
||||
// 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)
|
||||
if (!scannedContentCache.has(context)) {
|
||||
let files = fastGlob.sync(candidateFiles)
|
||||
for (let file of files) {
|
||||
changedFiles.add(file)
|
||||
}
|
||||
context.scannedContent = true
|
||||
scannedContentCache.set(context, true)
|
||||
}
|
||||
|
||||
return changedFiles
|
||||
@ -70,40 +280,78 @@ function resolveChangedFiles(context) {
|
||||
// plugins) then return it
|
||||
export default function setupWatchingContext(configOrPath, tailwindDirectives, registerDependency) {
|
||||
return (result, root) => {
|
||||
let [context, newContext] = getContext(
|
||||
configOrPath,
|
||||
tailwindDirectives,
|
||||
registerDependency,
|
||||
root,
|
||||
result,
|
||||
getTailwindConfig
|
||||
)
|
||||
let [tailwindConfig, userConfigPath, tailwindConfigHash, configDependencies] =
|
||||
getTailwindConfig(configOrPath)
|
||||
|
||||
if (context.configPath !== null) {
|
||||
for (let dependency of getModuleDependencies(context.configPath)) {
|
||||
if (dependency.file === context.configPath) {
|
||||
continue
|
||||
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.
|
||||
// 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 (tailwindDirectives.size > 0) {
|
||||
// Add current css file as a context dependencies.
|
||||
contextDependencies.add(result.opts.from)
|
||||
|
||||
// Add all css @import dependencies as context dependencies.
|
||||
for (let message of result.messages) {
|
||||
if (message.type === 'dependency') {
|
||||
contextDependencies.add(message.file)
|
||||
}
|
||||
|
||||
context.configDependencies.add(dependency.file)
|
||||
}
|
||||
}
|
||||
|
||||
if (newContext) {
|
||||
rebootWatcher(context)
|
||||
let [context, isNewContext] = getContext(
|
||||
tailwindDirectives,
|
||||
root,
|
||||
result,
|
||||
tailwindConfig,
|
||||
userConfigPath,
|
||||
tailwindConfigHash,
|
||||
contextDependencies
|
||||
)
|
||||
|
||||
let candidateFiles = getCandidateFiles(context, userConfigPath, tailwindConfig)
|
||||
let contextConfigDependencies = getConfigDependencies(context)
|
||||
|
||||
for (let file of configDependencies) {
|
||||
registerDependency(file)
|
||||
}
|
||||
|
||||
context.disposables.push((oldContext) => {
|
||||
let watcher = getWatcher(oldContext)
|
||||
if (watcher !== null) {
|
||||
watcher.close()
|
||||
}
|
||||
})
|
||||
|
||||
let configPath = getConfigPath(context, configOrPath)
|
||||
|
||||
if (configPath !== null) {
|
||||
for (let dependency of getModuleDependencies(configPath)) {
|
||||
if (dependency.file === configPath) {
|
||||
continue
|
||||
}
|
||||
|
||||
contextConfigDependencies.add(dependency.file)
|
||||
}
|
||||
}
|
||||
|
||||
if (isNewContext) {
|
||||
rebootWatcher(context, configPath, contextConfigDependencies, candidateFiles)
|
||||
}
|
||||
|
||||
// Register our temp file as a dependency — we write to this file
|
||||
// to trigger rebuilds.
|
||||
if (context.touchFile) {
|
||||
registerDependency(context.touchFile)
|
||||
let touchFile = getTouchFile(context)
|
||||
if (touchFile) {
|
||||
registerDependency(touchFile)
|
||||
}
|
||||
|
||||
if (tailwindDirectives.size > 0) {
|
||||
for (let changedFile of resolveChangedFiles(context)) {
|
||||
let content = fs.readFileSync(changedFile, 'utf8')
|
||||
let extension = path.extname(changedFile).slice(1)
|
||||
context.changedContent.push({ content, extension })
|
||||
for (let changedContent of resolvedChangedContent(context, candidateFiles)) {
|
||||
context.changedContent.push(changedContent)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user