Start of new CLI (#4526)

* Ignore workspace settings

* Parameterize setting up the context

* WIP

* WIP

* WIP

* WIP

* wip

* WIP

Co-Authored-By: Jesse Katsumata <niconico.clarinet@gmail.com>

* WIP

Co-Authored-By: Jesse Katsumata <niconico.clarinet@gmail.com>

* WIP

Co-Authored-By: Jesse Katsumata <niconico.clarinet@gmail.com>

* Update some comments

Co-Authored-By: Jesse Katsumata <niconico.clarinet@gmail.com>

* 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 <adam.wathan@gmail.com>
Co-authored-by: Jesse Katsumata <niconico.clarinet@gmail.com>
This commit is contained in:
Robin Malfait 2021-06-01 14:02:23 +02:00 • committed by GitHub
parent b514a5c9c2
commit 4528bdc2d8
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 742 additions and 162 deletions

1
.gitignore vendored
View file

@ -3,6 +3,7 @@
/cli
/lib
/example
.vscode
tailwind.config.js
index.html
yarn.lock

27
package-lock.json generated
View file

@ -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",

View file

@ -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",

539
src/cli.js Executable file → Normal file
View file

@ -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 <command> [options]'],
commands: ['init [file]', 'build <file> [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 <command> [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()
}
}

6
src/cli/index.js Executable file
View file

@ -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))

View file

@ -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) {

View file

@ -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]
}

View file

@ -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)
export default function setupTrackingContext(configOrPath) {
return ({ tailwindDirectives, registerDependency }) => {
return (root, result) => {
let [tailwindConfig, userConfigPath, tailwindConfigHash, configDependencies] =
getTailwindConfig(configOrPath)
let contextDependencies = new Set(configDependencies)
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)
// 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))
// Add all css @import dependencies as context dependencies.
for (let message of result.messages) {
if (message.type === 'dependency') {
contextDependencies.add(message.file)
}
}
}
for (let changedContent of resolvedChangedContent(context, candidateFiles, fileModifiedMap)) {
context.changedContent.push(changedContent)
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 file of configDependencies) {
registerDependency(file)
}
for (let file of configDependencies) {
registerDependency(file)
}
return context
return context
}
}
}

View file

@ -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)
export default function setupWatchingContext(configOrPath) {
return ({ tailwindDirectives, registerDependency }) => {
return (root, result) => {
let [tailwindConfig, userConfigPath, tailwindConfigHash, configDependencies] =
getTailwindConfig(configOrPath)
let contextDependencies = new Set(configDependencies)
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)
// 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)
// 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 [context, isNewContext] = getContext(
tailwindDirectives,
root,
result,
tailwindConfig,
userConfigPath,
tailwindConfigHash,
contextDependencies
)
let candidateFiles = getCandidateFiles(context, userConfigPath, tailwindConfig)
let contextConfigDependencies = getConfigDependencies(context)
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()
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()
}
})
contextConfigDependencies.add(dependency.file)
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.
let touchFile = getTouchFile(context)
if (touchFile) {
registerDependency(touchFile)
}
if (tailwindDirectives.size > 0) {
for (let changedContent of resolvedChangedContent(context, candidateFiles)) {
context.changedContent.push(changedContent)
if (isNewContext) {
rebootWatcher(context, configPath, contextConfigDependencies, candidateFiles)
}
}
return context
// 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)
}
}
return context
}
}
}

View file

@ -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)