Skip to content
This repository was archived by the owner on Apr 6, 2021. It is now read-only.
Next Next commit
use context dependencies and remove file watcher and touch files
  • Loading branch information
bradlc committed Mar 5, 2021
commit df5de3f0dd2a1a0192da6316c91d910048c1a537
4 changes: 2 additions & 2 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
37 changes: 26 additions & 11 deletions src/lib/expandTailwindAtRules.js
Original file line number Diff line number Diff line change
@@ -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')
Expand Down Expand Up @@ -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
}

// ---
Expand Down
141 changes: 20 additions & 121 deletions src/lib/setupContext.js
Original file line number Diff line number Diff line change
@@ -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')
Expand All @@ -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
}
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand All @@ -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

Expand All @@ -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(),
Expand All @@ -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
Expand Down
1 change: 1 addition & 0 deletions src/lib/sharedState.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,5 @@ module.exports = {
},
contextMap: new Map(),
contentMatchCache: new LRU({ maxSize: 25000 }),
fileModifiedCache: new Map(),
}