From 4528bdc2d8c5636dea39ef52af6d5574224e1697 Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Tue, 1 Jun 2021 14:02:23 +0200 Subject: [PATCH] Start of new CLI (#4526) * Ignore workspace settings * Parameterize setting up the context * WIP * WIP * WIP * WIP * wip * WIP Co-Authored-By: Jesse Katsumata * WIP Co-Authored-By: Jesse Katsumata * WIP Co-Authored-By: Jesse Katsumata * Update some comments Co-Authored-By: Jesse Katsumata * Fix bug * WIP * WIP' * more things * log console.time calls conditionally based on process.env.DEBUG * add `init` command * clean up when using --jit * Make config file optional * cleanup path.resolve calls path.resolve('.') is the same as path.resolve(process.cwd(), '.') * implement `--help` * shush eslint * drop unnecessary file Co-authored-by: Adam Wathan Co-authored-by: Jesse Katsumata --- .gitignore | 1 + package-lock.json | 27 +- package.json | 1 + src/cli.js | 539 +++++++++++++++++++++++++++- src/cli/index.js | 6 + src/jit/index.js | 20 +- src/jit/lib/setupContextUtils.js | 37 +- src/jit/lib/setupTrackingContext.js | 124 ++++--- src/jit/lib/setupWatchingContext.js | 132 +++---- src/jit/processTailwindFeatures.js | 23 +- 10 files changed, 745 insertions(+), 165 deletions(-) mode change 100755 => 100644 src/cli.js create mode 100755 src/cli/index.js diff --git a/.gitignore b/.gitignore index 9673e8596e1a..1c4337030da6 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,7 @@ /cli /lib /example +.vscode tailwind.config.js index.html yarn.lock diff --git a/package-lock.json b/package-lock.json index 24cc6cb8240f..b708b35e9e06 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5,11 +5,11 @@ "requires": true, "packages": { "": { - "name": "tailwindcss", "version": "2.1.2", "license": "MIT", "dependencies": { "@fullhuman/postcss-purgecss": "^4.0.3", + "arg": "^5.0.0", "bytes": "^3.0.0", "chalk": "^4.1.1", "chokidar": "^3.5.1", @@ -2323,6 +2323,11 @@ "node": ">=0.10.0" } }, + "node_modules/arg": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.0.tgz", + "integrity": "sha512-4P8Zm2H+BRS+c/xX1LrHw0qKpEhdlZjLCgWy+d78T9vqa2Z2SiD2wMrYuWIAFy5IZUD7nnNXroRttz+0RzlrzQ==" + }, "node_modules/argparse": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", @@ -11652,7 +11657,8 @@ "version": "0.2.1", "resolved": "https://registry.npmjs.org/@tailwindcss/aspect-ratio/-/aspect-ratio-0.2.1.tgz", "integrity": "sha512-aDFi80aHQ3JM3symJ5iKU70lm151ugIGFCI0yRZGpyjgQSDS+Fbe93QwypC1tCEllQE8p0S7TUu20ih1b9IKLA==", - "dev": true + "dev": true, + "requires": {} }, "@types/babel__core": { "version": "7.1.14", @@ -11804,7 +11810,8 @@ "version": "5.3.1", "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.1.tgz", "integrity": "sha512-K0Ptm/47OKfQRpNQ2J/oIN/3QYiK6FwW+eJbILhsdxh2WTLdl+30o8aGdTbm5JbffpFFAg/g+zi1E+jvJha5ng==", - "dev": true + "dev": true, + "requires": {} }, "acorn-node": { "version": "1.8.2", @@ -11883,6 +11890,11 @@ } } }, + "arg": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.0.tgz", + "integrity": "sha512-4P8Zm2H+BRS+c/xX1LrHw0qKpEhdlZjLCgWy+d78T9vqa2Z2SiD2wMrYuWIAFy5IZUD7nnNXroRttz+0RzlrzQ==" + }, "argparse": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", @@ -13135,7 +13147,8 @@ "version": "8.3.0", "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-8.3.0.tgz", "integrity": "sha512-BgZuLUSeKzvlL/VUjx/Yb787VQ26RU3gGjA3iiFvdsp/2bMfVIWUVP7tjxtjS0e+HP409cPlPvNkQloz8C91ew==", - "dev": true + "dev": true, + "requires": {} }, "eslint-plugin-prettier": { "version": "3.4.0", @@ -14727,7 +14740,8 @@ "version": "1.2.2", "resolved": "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.2.tgz", "integrity": "sha512-olV41bKSMm8BdnuMsewT4jqlZ8+3TCARAXjZGT9jcoSnrfUnRCqnMoF9XEeoWjbzObpqF9dRhHQj0Xb9QdF6/w==", - "dev": true + "dev": true, + "requires": {} }, "jest-regex-util": { "version": "26.0.0", @@ -17760,7 +17774,8 @@ "version": "7.4.4", "resolved": "https://registry.npmjs.org/ws/-/ws-7.4.4.tgz", "integrity": "sha512-Qm8k8ojNQIMx7S+Zp8u/uHOx7Qazv3Yv4q68MiWWWOJhiwG5W3x7iqmRtJo8xxrciZUY4vRxUTJCKuRnF28ZZw==", - "dev": true + "dev": true, + "requires": {} }, "xml-name-validator": { "version": "3.0.0", diff --git a/package.json b/package.json index c23858b5bfd9..3a107b943490 100644 --- a/package.json +++ b/package.json @@ -65,6 +65,7 @@ }, "dependencies": { "@fullhuman/postcss-purgecss": "^4.0.3", + "arg": "^5.0.0", "bytes": "^3.0.0", "chalk": "^4.1.1", "chokidar": "^3.5.1", diff --git a/src/cli.js b/src/cli.js old mode 100755 new mode 100644 index 05bc8c8df16c..78663cca5725 --- a/src/cli.js +++ b/src/cli.js @@ -1,6 +1,539 @@ #!/usr/bin/env node -import main from './cli/main' -import * as utils from './cli/utils' +/* eslint-disable */ -main(process.argv.slice(2)).catch((error) => utils.die(error.stack)) +// import autoprefixer from 'autoprefixer' +import chokidar from 'chokidar' +import postcss from 'postcss' +import chalk from 'chalk' +import path from 'path' +import arg from 'arg' +import fs from 'fs' +import tailwindJit from './jit/processTailwindFeatures' +import tailwindAot from './processTailwindFeatures' +import resolveConfigInternal from '../resolveConfig' +import fastGlob from 'fast-glob' +import getModuleDependencies from './lib/getModuleDependencies' +import packageJson from '../package.json' + +let env = { + DEBUG: process.env.DEBUG !== undefined, +} + +// --- + +function indentRecursive(node, indent = 0) { + node.each && + node.each((child, i) => { + if (!child.raws.before || !child.raws.before.trim() || child.raws.before.includes('\n')) { + child.raws.before = `\n${node.type !== 'rule' && i > 0 ? '\n' : ''}${' '.repeat(indent)}` + } + child.raws.after = `\n${' '.repeat(indent)}` + indentRecursive(child, indent + 1) + }) +} + +function formatNodes(root) { + indentRecursive(root) + if (root.first) { + root.first.raws.before = '' + } +} + +function help({ message, usage, commands, options }) { + // Render header + console.log() + console.log(' ', packageJson.name, packageJson.version) + + // Render message + if (message) { + console.log() + console.log(' ', message) + } + + // Render usage + if (usage && usage.length > 0) { + console.log() + console.log(' ', 'Usage:') + for (let example of usage) { + console.log(' ', ' ', example) + } + } + + // Render commands + if (commands && commands.length > 0) { + console.log() + console.log(' ', 'Commands:') + for (let command of commands) { + console.log(' ', ' ', command) + } + } + + // Render options + if (options) { + let groupedOptions = {} + for (let [key, value] of Object.entries(options)) { + if (typeof value === 'object') { + groupedOptions[key] = { ...value, flags: [key] } + } else { + groupedOptions[value].flags.push(key) + } + } + + console.log() + console.log(' ', 'Options:') + for (let { flags, description } of Object.values(groupedOptions)) { + console.log(' ', ' ', flags.slice().reverse().join(', ').padEnd(15, ' '), description) + } + } +} + +// --- + +/* + TODOs: + - [x] Reduce getModuleDependencies calls (make configDeps global?) + - [x] Detect new files + - [x] Support raw content in purge config + - [x] Scaffold tailwind.config.js file (with postcss.config.js) + - [x] Support passing globs from command line + - [x] Make config file optional + - [ ] Support AOT mode + - [ ] Prebundle peer-dependencies + - [ ] Make minification work + - [x] --help option + - [x] conditional flags based on arguments + init -f, --full + build -f, --files + - [ ] --jit + + Future: + - Detect project type, add sensible purge defaults +*/ +let commands = { + init: { + run: init, + args: { + '--jit': { type: Boolean, description: 'Enable `JIT` mode' }, + '--full': { type: Boolean, description: 'Generate a full tailwind.config.js file' }, + '--postcss': { type: Boolean, description: 'Generate a PostCSS file' }, + '-f': '--full', + '-p': '--postcss', + }, + }, + build: { + run: build, + args: { + '--jit': { type: Boolean, description: 'Build using `JIT` mode' }, + '--files': { type: String, description: 'Use a glob as files to use' }, + '--config': { + type: String, + description: 'Provide a custom config file, default: ./tailwind.config.js', + }, + '--input': { type: String, description: 'The input css file' }, + '--output': { type: String, description: 'The output css file' }, + '--minify': { type: Boolean, description: 'Whether or not the result should be minified' }, + '--watch': { type: Boolean, description: 'Start watching for changes' }, + '-f': '--files', + '-c': '--config', + '-i': '--input', + '-o': '--output', + '-m': '--minify', + '-w': '--watch', + }, + }, +} + +let sharedFlags = { + '--help': { type: Boolean, description: 'Prints this help message' }, + '-h': '--help', +} +let command = process.argv.slice(2).find((arg) => !arg.startsWith('-')) || 'build' + +if (commands[command] === undefined) { + help({ + message: `Invalid command: ${command}`, + usage: ['tailwind [options]'], + commands: ['init [file]', 'build [options]'], + options: sharedFlags, + }) + process.exit(1) +} + +// Execute command +let { args: flags, run } = commands[command] +let args = (() => { + try { + return arg( + Object.fromEntries( + Object.entries({ ...flags, ...sharedFlags }).map(([key, value]) => [ + key, + typeof value === 'object' ? value.type : value, + ]) + ) + ) + } catch (err) { + if (err.code === 'ARG_UNKNOWN_OPTION') { + help({ + message: err.message, + usage: ['tailwind [options]'], + options: sharedFlags, + }) + process.exit(1) + } + throw err + } +})() + +if (args['--help']) { + help({ + options: { ...flags, ...sharedFlags }, + usage: [`tailwind ${command} [options]`], + }) + process.exit(0) +} + +run() + +function init() { + let tailwindConfigLocation = path.resolve('./tailwind.config.js') + if (fs.existsSync(tailwindConfigLocation)) { + console.log('tailwind.config.js already exists.') + } else { + let stubFile = fs.readFileSync( + args['--full'] + ? path.resolve(__dirname, '../stubs/defaultConfig.stub.js') + : path.resolve(__dirname, '../stubs/simpleConfig.stub.js'), + 'utf8' + ) + + // Change colors import + stubFile = stubFile.replace('../colors', 'tailwindcss/colors') + + // --jit mode + if (args['--jit']) { + // Add jit mode + stubFile = stubFile.replace('module.exports = {', "module.exports = {\n mode: 'jit',") + + // Deleting variants + stubFile = stubFile.replace(/variants: {(.*)},\n /gs, '') + } + + fs.writeFileSync(tailwindConfigLocation, stubFile, 'utf8') + + console.log('Created Tailwind config file:', 'tailwind.config.js') + } + + if (args['--postcss']) { + let postcssConfigLocation = path.resolve('./postcss.config.js') + if (fs.existsSync(postcssConfigLocation)) { + console.log('postcss.config.js already exists.') + } else { + let stubFile = fs.readFileSync( + path.resolve(__dirname, '../stubs/defaultPostCssConfig.stub.js'), + 'utf8' + ) + + fs.writeFileSync(postcssConfigLocation, stubFile, 'utf8') + + console.log('Created PostCSS config file:', 'tailwind.config.js') + } + } +} + +function build() { + let input = args['--input'] + let output = args['--output'] + let shouldWatch = args['--watch'] + let shouldMinify = args['--minify'] + + if (args['--config'] && !fs.existsSync(args['--config'])) { + console.error(`Specified config file ${args['--config']} does not exist.`) + process.exit(9) + } + + let configPath = + args['--config'] ?? + ((defaultPath) => (fs.existsSync(defaultPath) ? defaultPath : null))( + path.resolve('./tailwind.config.js') + ) + + function resolveConfig() { + let config = require(configPath) + let resolvedConfig = resolveConfigInternal(config) + + if (args['--files']) { + resolvedConfig.purge = args['--files'].split(',') + } + + if (args['--jit']) { + resolvedConfig.mode = 'jit' + } + + return resolvedConfig + } + + if (!output) { + help({ + message: 'Missing required output file: --output, -o, or first argument', + usage: [`tailwind ${command} [options]`], + options: { ...flags, ...sharedFlags }, + }) + process.exit(1) + } + + function extractContent(config) { + return Array.isArray(config.purge) ? config.purge : config.purge.content + } + + function extractFileGlobs(config) { + return extractContent(config).filter((file) => { + // Strings in this case are files / globs. If it is something else, + // like an object it's probably a raw content object. But this object + // is not watchable, so let's remove it. + return typeof file === 'string' + }) + } + + function extractRawContent(config) { + return extractContent(config).filter((file) => { + return typeof file === 'object' && file !== null + }) + } + + function getChangedContent(config) { + let changedContent = [] + + // Resolve globs from the purge config + let globs = extractFileGlobs(config) + let files = fastGlob.sync(globs) + + for (let file of files) { + changedContent.push({ + content: fs.readFileSync(path.resolve(file), 'utf8'), + extension: path.extname(file), + }) + } + + // Resolve raw content in the tailwind config + for (let { raw: content, extension = 'html' } of extractRawContent(config)) { + changedContent.push({ content, extension }) + } + + return changedContent + } + + function buildOnce() { + let config = resolveConfig() + let changedContent = getChangedContent(config) + + let tailwindPlugin = + config.mode === 'jit' + ? (opts = {}) => { + return { + postcssPlugin: 'tailwindcss', + Once(root, { result }) { + tailwindJit(({ createContext }) => { + return () => { + return createContext(config, changedContent) + } + })(root, result) + }, + } + } + : (opts = {}) => { + return { + postcssPlugin: 'tailwindcss', + plugins: [tailwindAot(() => config, configPath)], + } + } + + tailwindPlugin.postcss = true + + let plugins = [ + // TODO: Bake in postcss-import support? + // TODO: Bake in postcss-nested support? + tailwindPlugin, + // require('autoprefixer'), + formatNodes, + ] + + let processor = postcss(plugins) + + function processCSS(css) { + return processor.process(css, { from: input, to: output }).then((result) => { + fs.writeFile(output, result.css, () => true) + if (result.map) { + fs.writeFile(output + '.map', result.map.toString(), () => true) + } + }) + } + + let css = input + ? fs.readFileSync(path.resolve(input), 'utf8') + : '@tailwind base; @tailwind components; @tailwind utilities' + return processCSS(css) + } + + let context = null + + function startWatcher() { + let changedContent = [] + let configDependencies = [] + let contextDependencies = new Set() + let watcher = null + + function refreshConfig() { + env.DEBUG && console.time('Module dependencies') + for (let file of configDependencies) { + delete require.cache[require.resolve(file)] + } + + if (configPath) { + configDependencies = getModuleDependencies(configPath).map(({ file }) => file) + + for (let dependency of configDependencies) { + contextDependencies.add(dependency) + } + } + env.DEBUG && console.timeEnd('Module dependencies') + + return resolveConfig() + } + + async function rebuild(config) { + console.log('\nRebuilding...') + env.DEBUG && console.time('Finished in') + + let tailwindPlugin = + config.mode === 'jit' + ? (opts = {}) => { + return { + postcssPlugin: 'tailwindcss', + Once(root, { result }) { + env.DEBUG && console.time('Compiling CSS') + tailwindJit(({ createContext }) => { + return () => { + if (context !== null) { + context.changedContent = changedContent.splice(0) + return context + } + + env.DEBUG && console.time('Creating context') + context = createContext(config, changedContent.splice(0)) + env.DEBUG && console.timeEnd('Creating context') + return context + } + })(root, result) + env.DEBUG && console.timeEnd('Compiling CSS') + }, + } + } + : (opts = {}) => { + return { + postcssPlugin: 'tailwindcss', + plugins: [tailwindAot(() => config, configPath)], + } + } + + tailwindPlugin.postcss = true + + let plugins = [ + // TODO: Bake in postcss-import support? + // TODO: Bake in postcss-nested support? + tailwindPlugin, + // require('autoprefixer'), + formatNodes, + ] + + let processor = postcss(plugins) + + function processCSS(css) { + return processor.process(css, { from: input, to: output }).then((result) => { + for (let message of result.messages) { + if (message.type === 'dependency') { + contextDependencies.add(message.file) + } + } + watcher.add([...contextDependencies]) + + fs.writeFile(output, result.css, () => true) + if (result.map) { + fs.writeFile(output + '.map', result.map.toString(), () => true) + } + }) + } + + let css = input + ? fs.readFileSync(path.resolve(input), 'utf8') + : '@tailwind base; @tailwind components; @tailwind utilities' + let result = await processCSS(css) + env.DEBUG && console.timeEnd('Finished in') + return result + } + + let configPath = args['--config'] ?? path.resolve('./tailwind.config.js') + let config = refreshConfig(configPath) + + if (input) { + contextDependencies.add(path.resolve(input)) + } + + watcher = chokidar.watch([...contextDependencies, ...extractFileGlobs(config)], { + ignoreInitial: true, + }) + + let chain = Promise.resolve() + + watcher.on('change', async (file) => { + if (contextDependencies.has(file)) { + env.DEBUG && console.time('Resolve config') + context = null + config = refreshConfig(configPath) + env.DEBUG && console.timeEnd('Resolve config') + + env.DEBUG && console.time('Watch new files') + let globs = extractFileGlobs(config) + watcher.add(configDependencies) + watcher.add(globs) + env.DEBUG && console.timeEnd('Watch new files') + + chain = chain.then(async () => { + changedContent.push(...getChangedContent(config)) + await rebuild(config) + }) + } else { + chain = chain.then(async () => { + changedContent.push({ + content: fs.readFileSync(path.resolve(file), 'utf8'), + extension: path.extname(file), + }) + + await rebuild(config) + }) + } + }) + + watcher.on('add', async (file) => { + chain = chain.then(async () => { + changedContent.push({ + content: fs.readFileSync(path.resolve(file), 'utf8'), + extension: path.extname(file), + }) + + await rebuild(config) + }) + }) + + chain = chain.then(() => { + changedContent.push(...getChangedContent(config)) + return rebuild(config) + }) + } + + if (shouldWatch) { + startWatcher() + } else { + buildOnce() + } +} diff --git a/src/cli/index.js b/src/cli/index.js new file mode 100755 index 000000000000..c30f2c6f3665 --- /dev/null +++ b/src/cli/index.js @@ -0,0 +1,6 @@ +#!/usr/bin/env node + +import main from './main' +import * as utils from './utils' + +main(process.argv.slice(2)).catch((error) => utils.die(error.stack)) diff --git a/src/jit/index.js b/src/jit/index.js index 48830150741e..1f531846fe95 100644 --- a/src/jit/index.js +++ b/src/jit/index.js @@ -1,4 +1,3 @@ -import normalizeTailwindDirectives from './lib/normalizeTailwindDirectives' import setupTrackingContext from './lib/setupTrackingContext' import setupWatchingContext from './lib/setupWatchingContext' import { env } from './lib/sharedState' @@ -13,23 +12,12 @@ export default function (configOrPath = {}) { return root }, function (root, result) { - function registerDependency(fileName, type = 'dependency') { - result.messages.push({ - type, - plugin: 'tailwindcss', - parent: result.opts.from, - [type === 'dir-dependency' ? 'dir' : 'file']: fileName, - }) - } - - let tailwindDirectives = normalizeTailwindDirectives(root) - - let context = + let setupContext = env.TAILWIND_MODE === 'watch' - ? setupWatchingContext(configOrPath, tailwindDirectives, registerDependency)(result, root) - : setupTrackingContext(configOrPath, tailwindDirectives, registerDependency)(result, root) + ? setupWatchingContext(configOrPath) + : setupTrackingContext(configOrPath) - processTailwindFeatures(context)(root, result) + processTailwindFeatures(setupContext)(root, result) }, env.DEBUG && function (root) { diff --git a/src/jit/lib/setupContextUtils.js b/src/jit/lib/setupContextUtils.js index bdc148161c67..89080e075df1 100644 --- a/src/jit/lib/setupContextUtils.js +++ b/src/jit/lib/setupContextUtils.js @@ -479,6 +479,27 @@ function registerPlugins(plugins, context) { } } +export function createContext(tailwindConfig, changedContent, tailwindDirectives, root) { + let context = { + disposables: [], + ruleCache: new Set(), + classCache: new Map(), + applyClassCache: new Map(), + notClassCache: new Set(), + postCssNodeCache: new Map(), + candidateRuleMap: new Map(), + tailwindConfig, + changedContent: changedContent, + variantMap: new Map(), + stylesheetCache: null, + } + + let resolvedPlugins = resolvePlugins(context, tailwindDirectives, root) + registerPlugins(resolvedPlugins, context) + + return context +} + let contextMap = sharedState.contextMap let configContextMap = sharedState.configContextMap let contextSourcesMap = sharedState.contextSourcesMap @@ -541,19 +562,7 @@ export function getContext( env.DEBUG && console.log('Setting up new context...') - let context = { - 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, - } + let context = createContext(tailwindConfig, [], tailwindDirectives, root) trackModified([...contextDependencies], getFileModifiedMap(context)) @@ -570,7 +579,5 @@ export function getContext( contextSourcesMap.get(context).add(sourcePath) - registerPlugins(resolvePlugins(context, tailwindDirectives, root), context) - return [context, true] } diff --git a/src/jit/lib/setupTrackingContext.js b/src/jit/lib/setupTrackingContext.js index 0c1330307bc6..bba0057c1526 100644 --- a/src/jit/lib/setupTrackingContext.js +++ b/src/jit/lib/setupTrackingContext.js @@ -121,73 +121,79 @@ function resolveChangedFiles(candidateFiles, fileModifiedMap) { // 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 -export default function setupTrackingContext(configOrPath, tailwindDirectives, registerDependency) { - return (result, root) => { - 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. - // 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) +export default function setupTrackingContext(configOrPath) { + return ({ tailwindDirectives, registerDependency }) => { + return (root, result) => { + 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. + // 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) + } } } - } - 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 candidateFiles) { - if (isGlob(maybeGlob)) { - // rollup-plugin-postcss does not support dir-dependency messages - // but directories can be watched in the same way as files - registerDependency( - path.resolve(globParent(maybeGlob)), - env.ROLLUP_WATCH === 'true' ? 'dependency' : 'dir-dependency' - ) - } else { - registerDependency(path.resolve(maybeGlob)) + 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 candidateFiles) { + if (isGlob(maybeGlob)) { + // rollup-plugin-postcss does not support dir-dependency messages + // but directories can be watched in the same way as files + registerDependency( + path.resolve(globParent(maybeGlob)), + env.ROLLUP_WATCH === 'true' ? 'dependency' : 'dir-dependency' + ) + } else { + registerDependency(path.resolve(maybeGlob)) + } + } + + for (let changedContent of resolvedChangedContent( + context, + candidateFiles, + fileModifiedMap + )) { + context.changedContent.push(changedContent) } } - for (let changedContent of resolvedChangedContent(context, candidateFiles, fileModifiedMap)) { - context.changedContent.push(changedContent) + for (let file of configDependencies) { + registerDependency(file) } - } - for (let file of configDependencies) { - registerDependency(file) + return context } - - return context } } diff --git a/src/jit/lib/setupWatchingContext.js b/src/jit/lib/setupWatchingContext.js index 9eb83743a03a..33165931b87c 100644 --- a/src/jit/lib/setupWatchingContext.js +++ b/src/jit/lib/setupWatchingContext.js @@ -269,83 +269,85 @@ function resolveChangedFiles(context, candidateFiles) { // 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 -export default function setupWatchingContext(configOrPath, tailwindDirectives, registerDependency) { - return (result, root) => { - 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. - // 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) +export default function setupWatchingContext(configOrPath) { + return ({ tailwindDirectives, registerDependency }) => { + return (root, result) => { + 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. + // 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) + } } } - } - - 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 [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) } - }) - let configPath = getConfigPath(context, configOrPath) - - if (configPath !== null) { - for (let dependency of getModuleDependencies(configPath)) { - if (dependency.file === configPath) { - continue + context.disposables.push((oldContext) => { + let watcher = getWatcher(oldContext) + if (watcher !== null) { + watcher.close() } + }) + + let configPath = getConfigPath(context, configOrPath) - contextConfigDependencies.add(dependency.file) + 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) - } + if (isNewContext) { + rebootWatcher(context, configPath, contextConfigDependencies, candidateFiles) + } - // Register our temp file as a dependency — we write to this file - // to trigger rebuilds. - let touchFile = getTouchFile(context) - if (touchFile) { - registerDependency(touchFile) - } + // Register our temp file as a dependency — we write to this file + // to trigger rebuilds. + let touchFile = getTouchFile(context) + if (touchFile) { + registerDependency(touchFile) + } - if (tailwindDirectives.size > 0) { - for (let changedContent of resolvedChangedContent(context, candidateFiles)) { - context.changedContent.push(changedContent) + if (tailwindDirectives.size > 0) { + for (let changedContent of resolvedChangedContent(context, candidateFiles)) { + context.changedContent.push(changedContent) + } } - } - return context + return context + } } } diff --git a/src/jit/processTailwindFeatures.js b/src/jit/processTailwindFeatures.js index c3072b9911bb..634e760be7ed 100644 --- a/src/jit/processTailwindFeatures.js +++ b/src/jit/processTailwindFeatures.js @@ -1,11 +1,32 @@ +import normalizeTailwindDirectives from './lib/normalizeTailwindDirectives' import expandTailwindAtRules from './lib/expandTailwindAtRules' import expandApplyAtRules from './lib/expandApplyAtRules' import evaluateTailwindFunctions from '../lib/evaluateTailwindFunctions' import substituteScreenAtRules from '../lib/substituteScreenAtRules' import collapseAdjacentRules from './lib/collapseAdjacentRules' +import { createContext } from './lib/setupContextUtils' -export default function processTailwindFeatures(context) { +export default function processTailwindFeatures(setupContext) { return function (root, result) { + function registerDependency(fileName, type = 'dependency') { + result.messages.push({ + type, + plugin: 'tailwindcss', + parent: result.opts.from, + [type === 'dir-dependency' ? 'dir' : 'file']: fileName, + }) + } + + let tailwindDirectives = normalizeTailwindDirectives(root) + + let context = setupContext({ + tailwindDirectives, + registerDependency, + createContext(tailwindConfig, changedContent) { + return createContext(tailwindConfig, changedContent, tailwindDirectives, root) + }, + })(root, result) + expandTailwindAtRules(context)(root, result) expandApplyAtRules(context)(root, result) evaluateTailwindFunctions(context)(root, result)