You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
916 lines
25 KiB
916 lines
25 KiB
3 years ago
|
#!/usr/bin/env node
|
||
|
|
||
|
import { lazyPostcss, lazyPostcssImport, lazyCssnano, lazyAutoprefixer } from '../peers/index.js'
|
||
|
|
||
|
import chokidar from 'chokidar'
|
||
|
import path from 'path'
|
||
|
import arg from 'arg'
|
||
|
import fs from 'fs'
|
||
|
import postcssrc from 'postcss-load-config'
|
||
|
import { lilconfig } from 'lilconfig'
|
||
|
import loadPlugins from 'postcss-load-config/src/plugins' // Little bit scary, looking at private/internal API
|
||
|
import loadOptions from 'postcss-load-config/src/options' // Little bit scary, looking at private/internal API
|
||
|
import tailwind from './processTailwindFeatures'
|
||
|
import resolveConfigInternal from '../resolveConfig'
|
||
|
import fastGlob from 'fast-glob'
|
||
|
import getModuleDependencies from './lib/getModuleDependencies'
|
||
|
import log from './util/log'
|
||
|
import packageJson from '../package.json'
|
||
|
import normalizePath from 'normalize-path'
|
||
|
import { validateConfig } from './util/validateConfig.js'
|
||
|
|
||
|
let env = {
|
||
|
DEBUG: process.env.DEBUG !== undefined && process.env.DEBUG !== '0',
|
||
|
}
|
||
|
|
||
|
function isESM() {
|
||
|
const pkgPath = path.resolve('./package.json')
|
||
|
|
||
|
try {
|
||
|
let pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'))
|
||
|
return pkg.type && pkg.type === 'module'
|
||
|
} catch (err) {
|
||
|
return false
|
||
|
}
|
||
|
}
|
||
|
|
||
|
let configs = isESM()
|
||
|
? {
|
||
|
tailwind: 'tailwind.config.cjs',
|
||
|
postcss: 'postcss.config.cjs',
|
||
|
}
|
||
|
: {
|
||
|
tailwind: 'tailwind.config.js',
|
||
|
postcss: 'postcss.config.js',
|
||
|
}
|
||
|
|
||
|
// ---
|
||
|
|
||
|
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 = ''
|
||
|
}
|
||
|
}
|
||
|
|
||
|
async function outputFile(file, contents) {
|
||
|
if (fs.existsSync(file) && (await fs.promises.readFile(file, 'utf8')) === contents) {
|
||
|
return // Skip writing the file
|
||
|
}
|
||
|
|
||
|
// Write the file
|
||
|
await fs.promises.writeFile(file, contents, 'utf8')
|
||
|
}
|
||
|
|
||
|
function drainStdin() {
|
||
|
return new Promise((resolve, reject) => {
|
||
|
let result = ''
|
||
|
process.stdin.on('data', (chunk) => {
|
||
|
result += chunk
|
||
|
})
|
||
|
process.stdin.on('end', () => resolve(result))
|
||
|
process.stdin.on('error', (err) => reject(err))
|
||
|
})
|
||
|
}
|
||
|
|
||
|
function help({ message, usage, commands, options }) {
|
||
|
let indent = 2
|
||
|
|
||
|
// Render header
|
||
|
console.log()
|
||
|
console.log(`${packageJson.name} v${packageJson.version}`)
|
||
|
|
||
|
// Render message
|
||
|
if (message) {
|
||
|
console.log()
|
||
|
for (let msg of message.split('\n')) {
|
||
|
console.log(msg)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Render usage
|
||
|
if (usage && usage.length > 0) {
|
||
|
console.log()
|
||
|
console.log('Usage:')
|
||
|
for (let example of usage) {
|
||
|
console.log(' '.repeat(indent), example)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Render commands
|
||
|
if (commands && commands.length > 0) {
|
||
|
console.log()
|
||
|
console.log('Commands:')
|
||
|
for (let command of commands) {
|
||
|
console.log(' '.repeat(indent), 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, deprecated } of Object.values(groupedOptions)) {
|
||
|
if (deprecated) continue
|
||
|
|
||
|
if (flags.length === 1) {
|
||
|
console.log(
|
||
|
' '.repeat(indent + 4 /* 4 = "-i, ".length */),
|
||
|
flags.slice().reverse().join(', ').padEnd(20, ' '),
|
||
|
description
|
||
|
)
|
||
|
} else {
|
||
|
console.log(
|
||
|
' '.repeat(indent),
|
||
|
flags.slice().reverse().join(', ').padEnd(24, ' '),
|
||
|
description
|
||
|
)
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
console.log()
|
||
|
}
|
||
|
|
||
|
function oneOf(...options) {
|
||
|
return Object.assign(
|
||
|
(value = true) => {
|
||
|
for (let option of options) {
|
||
|
let parsed = option(value)
|
||
|
if (parsed === value) {
|
||
|
return parsed
|
||
|
}
|
||
|
}
|
||
|
|
||
|
throw new Error('...')
|
||
|
},
|
||
|
{ manualParsing: true }
|
||
|
)
|
||
|
}
|
||
|
|
||
|
function loadPostcss() {
|
||
|
// Try to load a local `postcss` version first
|
||
|
try {
|
||
|
return require('postcss')
|
||
|
} catch {}
|
||
|
|
||
|
return lazyPostcss()
|
||
|
}
|
||
|
|
||
|
let commands = {
|
||
|
init: {
|
||
|
run: init,
|
||
|
args: {
|
||
|
'--full': { type: Boolean, description: `Initialize a full \`${configs.tailwind}\` file` },
|
||
|
'--postcss': { type: Boolean, description: `Initialize a \`${configs.postcss}\` file` },
|
||
|
'-f': '--full',
|
||
|
'-p': '--postcss',
|
||
|
},
|
||
|
},
|
||
|
build: {
|
||
|
run: build,
|
||
|
args: {
|
||
|
'--input': { type: String, description: 'Input file' },
|
||
|
'--output': { type: String, description: 'Output file' },
|
||
|
'--watch': { type: Boolean, description: 'Watch for changes and rebuild as needed' },
|
||
|
'--poll': {
|
||
|
type: Boolean,
|
||
|
description: 'Use polling instead of filesystem events when watching',
|
||
|
},
|
||
|
'--content': {
|
||
|
type: String,
|
||
|
description: 'Content paths to use for removing unused classes',
|
||
|
},
|
||
|
'--purge': {
|
||
|
type: String,
|
||
|
deprecated: true,
|
||
|
},
|
||
|
'--postcss': {
|
||
|
type: oneOf(String, Boolean),
|
||
|
description: 'Load custom PostCSS configuration',
|
||
|
},
|
||
|
'--minify': { type: Boolean, description: 'Minify the output' },
|
||
|
'--config': {
|
||
|
type: String,
|
||
|
description: 'Path to a custom config file',
|
||
|
},
|
||
|
'--no-autoprefixer': {
|
||
|
type: Boolean,
|
||
|
description: 'Disable autoprefixer',
|
||
|
},
|
||
|
'-c': '--config',
|
||
|
'-i': '--input',
|
||
|
'-o': '--output',
|
||
|
'-m': '--minify',
|
||
|
'-w': '--watch',
|
||
|
'-p': '--poll',
|
||
|
},
|
||
|
},
|
||
|
}
|
||
|
|
||
|
let sharedFlags = {
|
||
|
'--help': { type: Boolean, description: 'Display usage information' },
|
||
|
'-h': '--help',
|
||
|
}
|
||
|
|
||
|
if (
|
||
|
process.stdout.isTTY /* Detect redirecting output to a file */ &&
|
||
|
(process.argv[2] === undefined ||
|
||
|
process.argv.slice(2).every((flag) => sharedFlags[flag] !== undefined))
|
||
|
) {
|
||
|
help({
|
||
|
usage: [
|
||
|
'tailwindcss [--input input.css] [--output output.css] [--watch] [options...]',
|
||
|
'tailwindcss init [--full] [--postcss] [options...]',
|
||
|
],
|
||
|
commands: Object.keys(commands)
|
||
|
.filter((command) => command !== 'build')
|
||
|
.map((command) => `${command} [options]`),
|
||
|
options: { ...commands.build.args, ...sharedFlags },
|
||
|
})
|
||
|
process.exit(0)
|
||
|
}
|
||
|
|
||
|
let command = ((arg = '') => (arg.startsWith('-') ? undefined : arg))(process.argv[2]) || 'build'
|
||
|
|
||
|
if (commands[command] === undefined) {
|
||
|
if (fs.existsSync(path.resolve(command))) {
|
||
|
// TODO: Deprecate this in future versions
|
||
|
// Check if non-existing command, might be a file.
|
||
|
command = 'build'
|
||
|
} else {
|
||
|
help({
|
||
|
message: `Invalid command: ${command}`,
|
||
|
usage: ['tailwindcss <command> [options]'],
|
||
|
commands: Object.keys(commands)
|
||
|
.filter((command) => command !== 'build')
|
||
|
.map((command) => `${command} [options]`),
|
||
|
options: sharedFlags,
|
||
|
})
|
||
|
process.exit(1)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Execute command
|
||
|
let { args: flags, run } = commands[command]
|
||
|
let args = (() => {
|
||
|
try {
|
||
|
let result = arg(
|
||
|
Object.fromEntries(
|
||
|
Object.entries({ ...flags, ...sharedFlags })
|
||
|
.filter(([_key, value]) => !value?.type?.manualParsing)
|
||
|
.map(([key, value]) => [key, typeof value === 'object' ? value.type : value])
|
||
|
),
|
||
|
{ permissive: true }
|
||
|
)
|
||
|
|
||
|
// Manual parsing of flags to allow for special flags like oneOf(Boolean, String)
|
||
|
for (let i = result['_'].length - 1; i >= 0; --i) {
|
||
|
let flag = result['_'][i]
|
||
|
if (!flag.startsWith('-')) continue
|
||
|
|
||
|
let flagName = flag
|
||
|
let handler = flags[flag]
|
||
|
|
||
|
// Resolve flagName & handler
|
||
|
while (typeof handler === 'string') {
|
||
|
flagName = handler
|
||
|
handler = flags[handler]
|
||
|
}
|
||
|
|
||
|
if (!handler) continue
|
||
|
|
||
|
let args = []
|
||
|
let offset = i + 1
|
||
|
|
||
|
// Parse args for current flag
|
||
|
while (result['_'][offset] && !result['_'][offset].startsWith('-')) {
|
||
|
args.push(result['_'][offset++])
|
||
|
}
|
||
|
|
||
|
// Cleanup manually parsed flags + args
|
||
|
result['_'].splice(i, 1 + args.length)
|
||
|
|
||
|
// Set the resolved value in the `result` object
|
||
|
result[flagName] = handler.type(
|
||
|
args.length === 0 ? undefined : args.length === 1 ? args[0] : args,
|
||
|
flagName
|
||
|
)
|
||
|
}
|
||
|
|
||
|
// Ensure that the `command` is always the first argument in the `args`.
|
||
|
// This is important so that we don't have to check if a default command
|
||
|
// (build) was used or not from within each plugin.
|
||
|
//
|
||
|
// E.g.: tailwindcss input.css -> _: ['build', 'input.css']
|
||
|
// E.g.: tailwindcss build input.css -> _: ['build', 'input.css']
|
||
|
if (result['_'][0] !== command) {
|
||
|
result['_'].unshift(command)
|
||
|
}
|
||
|
|
||
|
return result
|
||
|
} catch (err) {
|
||
|
if (err.code === 'ARG_UNKNOWN_OPTION') {
|
||
|
help({
|
||
|
message: err.message,
|
||
|
usage: ['tailwindcss <command> [options]'],
|
||
|
options: sharedFlags,
|
||
|
})
|
||
|
process.exit(1)
|
||
|
}
|
||
|
throw err
|
||
|
}
|
||
|
})()
|
||
|
|
||
|
if (args['--help']) {
|
||
|
help({
|
||
|
options: { ...flags, ...sharedFlags },
|
||
|
usage: [`tailwindcss ${command} [options]`],
|
||
|
})
|
||
|
process.exit(0)
|
||
|
}
|
||
|
|
||
|
run()
|
||
|
|
||
|
// ---
|
||
|
|
||
|
function init() {
|
||
|
let messages = []
|
||
|
|
||
|
let tailwindConfigLocation = path.resolve(args['_'][1] ?? `./${configs.tailwind}`)
|
||
|
if (fs.existsSync(tailwindConfigLocation)) {
|
||
|
messages.push(`${path.basename(tailwindConfigLocation)} 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')
|
||
|
|
||
|
fs.writeFileSync(tailwindConfigLocation, stubFile, 'utf8')
|
||
|
|
||
|
messages.push(`Created Tailwind CSS config file: ${path.basename(tailwindConfigLocation)}`)
|
||
|
}
|
||
|
|
||
|
if (args['--postcss']) {
|
||
|
let postcssConfigLocation = path.resolve(`./${configs.postcss}`)
|
||
|
if (fs.existsSync(postcssConfigLocation)) {
|
||
|
messages.push(`${path.basename(postcssConfigLocation)} already exists.`)
|
||
|
} else {
|
||
|
let stubFile = fs.readFileSync(
|
||
|
path.resolve(__dirname, '../stubs/defaultPostCssConfig.stub.js'),
|
||
|
'utf8'
|
||
|
)
|
||
|
|
||
|
fs.writeFileSync(postcssConfigLocation, stubFile, 'utf8')
|
||
|
|
||
|
messages.push(`Created PostCSS config file: ${path.basename(postcssConfigLocation)}`)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if (messages.length > 0) {
|
||
|
console.log()
|
||
|
for (let message of messages) {
|
||
|
console.log(message)
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
async function build() {
|
||
|
let input = args['--input']
|
||
|
let output = args['--output']
|
||
|
let shouldWatch = args['--watch']
|
||
|
let shouldPoll = args['--poll']
|
||
|
let shouldCoalesceWriteEvents = shouldPoll || process.platform === 'win32'
|
||
|
let includePostCss = args['--postcss']
|
||
|
|
||
|
// Polling interval in milliseconds
|
||
|
// Used only when polling or coalescing add/change events on Windows
|
||
|
let pollInterval = 10
|
||
|
|
||
|
// TODO: Deprecate this in future versions
|
||
|
if (!input && args['_'][1]) {
|
||
|
console.error('[deprecation] Running tailwindcss without -i, please provide an input file.')
|
||
|
input = args['--input'] = args['_'][1]
|
||
|
}
|
||
|
|
||
|
if (input && input !== '-' && !fs.existsSync((input = path.resolve(input)))) {
|
||
|
console.error(`Specified input file ${args['--input']} does not exist.`)
|
||
|
process.exit(9)
|
||
|
}
|
||
|
|
||
|
if (args['--config'] && !fs.existsSync((args['--config'] = path.resolve(args['--config'])))) {
|
||
|
console.error(`Specified config file ${args['--config']} does not exist.`)
|
||
|
process.exit(9)
|
||
|
}
|
||
|
|
||
|
let configPath = args['--config']
|
||
|
? args['--config']
|
||
|
: ((defaultPath) => (fs.existsSync(defaultPath) ? defaultPath : null))(
|
||
|
path.resolve(`./${configs.tailwind}`)
|
||
|
)
|
||
|
|
||
|
async function loadPostCssPlugins() {
|
||
|
let customPostCssPath = typeof args['--postcss'] === 'string' ? args['--postcss'] : undefined
|
||
|
let config = customPostCssPath
|
||
|
? await (async () => {
|
||
|
let file = path.resolve(customPostCssPath)
|
||
|
|
||
|
// Implementation, see: https://unpkg.com/browse/postcss-load-config@3.1.0/src/index.js
|
||
|
let { config = {} } = await lilconfig('postcss').load(file)
|
||
|
if (typeof config === 'function') {
|
||
|
config = config()
|
||
|
} else {
|
||
|
config = Object.assign({}, config)
|
||
|
}
|
||
|
|
||
|
if (!config.plugins) {
|
||
|
config.plugins = []
|
||
|
}
|
||
|
|
||
|
return {
|
||
|
file,
|
||
|
plugins: loadPlugins(config, file),
|
||
|
options: loadOptions(config, file),
|
||
|
}
|
||
|
})()
|
||
|
: await postcssrc()
|
||
|
|
||
|
let configPlugins = config.plugins
|
||
|
|
||
|
let configPluginTailwindIdx = configPlugins.findIndex((plugin) => {
|
||
|
if (typeof plugin === 'function' && plugin.name === 'tailwindcss') {
|
||
|
return true
|
||
|
}
|
||
|
|
||
|
if (typeof plugin === 'object' && plugin !== null && plugin.postcssPlugin === 'tailwindcss') {
|
||
|
return true
|
||
|
}
|
||
|
|
||
|
return false
|
||
|
})
|
||
|
|
||
|
let beforePlugins =
|
||
|
configPluginTailwindIdx === -1 ? [] : configPlugins.slice(0, configPluginTailwindIdx)
|
||
|
let afterPlugins =
|
||
|
configPluginTailwindIdx === -1
|
||
|
? configPlugins
|
||
|
: configPlugins.slice(configPluginTailwindIdx + 1)
|
||
|
|
||
|
return [beforePlugins, afterPlugins, config.options]
|
||
|
}
|
||
|
|
||
|
function loadBuiltinPostcssPlugins() {
|
||
|
let postcss = loadPostcss()
|
||
|
let IMPORT_COMMENT = '__TAILWIND_RESTORE_IMPORT__: '
|
||
|
return [
|
||
|
[
|
||
|
(root) => {
|
||
|
root.walkAtRules('import', (rule) => {
|
||
|
if (rule.params.slice(1).startsWith('tailwindcss/')) {
|
||
|
rule.after(postcss.comment({ text: IMPORT_COMMENT + rule.params }))
|
||
|
rule.remove()
|
||
|
}
|
||
|
})
|
||
|
},
|
||
|
(() => {
|
||
|
try {
|
||
|
return require('postcss-import')
|
||
|
} catch {}
|
||
|
|
||
|
return lazyPostcssImport()
|
||
|
})(),
|
||
|
(root) => {
|
||
|
root.walkComments((rule) => {
|
||
|
if (rule.text.startsWith(IMPORT_COMMENT)) {
|
||
|
rule.after(
|
||
|
postcss.atRule({
|
||
|
name: 'import',
|
||
|
params: rule.text.replace(IMPORT_COMMENT, ''),
|
||
|
})
|
||
|
)
|
||
|
rule.remove()
|
||
|
}
|
||
|
})
|
||
|
},
|
||
|
],
|
||
|
[],
|
||
|
{},
|
||
|
]
|
||
|
}
|
||
|
|
||
|
function resolveConfig() {
|
||
|
let config = configPath ? require(configPath) : {}
|
||
|
|
||
|
if (args['--purge']) {
|
||
|
log.warn('purge-flag-deprecated', [
|
||
|
'The `--purge` flag has been deprecated.',
|
||
|
'Please use `--content` instead.',
|
||
|
])
|
||
|
if (!args['--content']) {
|
||
|
args['--content'] = args['--purge']
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if (args['--content']) {
|
||
|
let files = args['--content'].split(/(?<!{[^}]+),/)
|
||
|
let resolvedConfig = resolveConfigInternal(config, { content: { files } })
|
||
|
resolvedConfig.content.files = files
|
||
|
resolvedConfig = validateConfig(resolvedConfig)
|
||
|
return resolvedConfig
|
||
|
}
|
||
|
|
||
|
let resolvedConfig = resolveConfigInternal(config)
|
||
|
resolvedConfig = validateConfig(resolvedConfig)
|
||
|
return resolvedConfig
|
||
|
}
|
||
|
|
||
|
function extractFileGlobs(config) {
|
||
|
return config.content.files
|
||
|
.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'
|
||
|
})
|
||
|
.map((glob) => normalizePath(glob))
|
||
|
}
|
||
|
|
||
|
function extractRawContent(config) {
|
||
|
return config.content.files.filter((file) => {
|
||
|
return typeof file === 'object' && file !== null
|
||
|
})
|
||
|
}
|
||
|
|
||
|
function getChangedContent(config) {
|
||
|
let changedContent = []
|
||
|
|
||
|
// Resolve globs from the content 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).slice(1),
|
||
|
})
|
||
|
}
|
||
|
|
||
|
// Resolve raw content in the tailwind config
|
||
|
for (let { raw: content, extension = 'html' } of extractRawContent(config)) {
|
||
|
changedContent.push({ content, extension })
|
||
|
}
|
||
|
|
||
|
return changedContent
|
||
|
}
|
||
|
|
||
|
async function buildOnce() {
|
||
|
let config = resolveConfig()
|
||
|
let changedContent = getChangedContent(config)
|
||
|
|
||
|
let tailwindPlugin = () => {
|
||
|
return {
|
||
|
postcssPlugin: 'tailwindcss',
|
||
|
Once(root, { result }) {
|
||
|
tailwind(({ createContext }) => {
|
||
|
return () => {
|
||
|
return createContext(config, changedContent)
|
||
|
}
|
||
|
})(root, result)
|
||
|
},
|
||
|
}
|
||
|
}
|
||
|
|
||
|
tailwindPlugin.postcss = true
|
||
|
|
||
|
let [beforePlugins, afterPlugins, postcssOptions] = includePostCss
|
||
|
? await loadPostCssPlugins()
|
||
|
: loadBuiltinPostcssPlugins()
|
||
|
|
||
|
let plugins = [
|
||
|
...beforePlugins,
|
||
|
tailwindPlugin,
|
||
|
!args['--minify'] && formatNodes,
|
||
|
...afterPlugins,
|
||
|
!args['--no-autoprefixer'] &&
|
||
|
(() => {
|
||
|
// Try to load a local `autoprefixer` version first
|
||
|
try {
|
||
|
return require('autoprefixer')
|
||
|
} catch {}
|
||
|
|
||
|
return lazyAutoprefixer()
|
||
|
})(),
|
||
|
args['--minify'] &&
|
||
|
(() => {
|
||
|
let options = { preset: ['default', { cssDeclarationSorter: false }] }
|
||
|
|
||
|
// Try to load a local `cssnano` version first
|
||
|
try {
|
||
|
return require('cssnano')
|
||
|
} catch {}
|
||
|
|
||
|
return lazyCssnano()(options)
|
||
|
})(),
|
||
|
].filter(Boolean)
|
||
|
|
||
|
let postcss = loadPostcss()
|
||
|
let processor = postcss(plugins)
|
||
|
|
||
|
function processCSS(css) {
|
||
|
let start = process.hrtime.bigint()
|
||
|
return Promise.resolve()
|
||
|
.then(() => (output ? fs.promises.mkdir(path.dirname(output), { recursive: true }) : null))
|
||
|
.then(() => processor.process(css, { ...postcssOptions, from: input, to: output }))
|
||
|
.then((result) => {
|
||
|
if (!output) {
|
||
|
return process.stdout.write(result.css)
|
||
|
}
|
||
|
|
||
|
return Promise.all(
|
||
|
[
|
||
|
outputFile(output, result.css),
|
||
|
result.map && outputFile(output + '.map', result.map.toString()),
|
||
|
].filter(Boolean)
|
||
|
)
|
||
|
})
|
||
|
.then(() => {
|
||
|
let end = process.hrtime.bigint()
|
||
|
console.error()
|
||
|
console.error('Done in', (end - start) / BigInt(1e6) + 'ms.')
|
||
|
})
|
||
|
}
|
||
|
|
||
|
let css = await (() => {
|
||
|
// Piping in data, let's drain the stdin
|
||
|
if (input === '-') {
|
||
|
return drainStdin()
|
||
|
}
|
||
|
|
||
|
// Input file has been provided
|
||
|
if (input) {
|
||
|
return fs.readFileSync(path.resolve(input), 'utf8')
|
||
|
}
|
||
|
|
||
|
// No input file provided, fallback to default atrules
|
||
|
return '@tailwind base; @tailwind components; @tailwind utilities'
|
||
|
})()
|
||
|
|
||
|
return processCSS(css)
|
||
|
}
|
||
|
|
||
|
let context = null
|
||
|
|
||
|
async 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()
|
||
|
}
|
||
|
|
||
|
let [beforePlugins, afterPlugins] = includePostCss
|
||
|
? await loadPostCssPlugins()
|
||
|
: loadBuiltinPostcssPlugins()
|
||
|
|
||
|
let plugins = [
|
||
|
...beforePlugins,
|
||
|
'__TAILWIND_PLUGIN_POSITION__',
|
||
|
!args['--minify'] && formatNodes,
|
||
|
...afterPlugins,
|
||
|
!args['--no-autoprefixer'] &&
|
||
|
(() => {
|
||
|
// Try to load a local `autoprefixer` version first
|
||
|
try {
|
||
|
return require('autoprefixer')
|
||
|
} catch {}
|
||
|
|
||
|
return lazyAutoprefixer()
|
||
|
})(),
|
||
|
args['--minify'] &&
|
||
|
(() => {
|
||
|
let options = { preset: ['default', { cssDeclarationSorter: false }] }
|
||
|
|
||
|
// Try to load a local `cssnano` version first
|
||
|
try {
|
||
|
return require('cssnano')
|
||
|
} catch {}
|
||
|
|
||
|
return lazyCssnano()(options)
|
||
|
})(),
|
||
|
].filter(Boolean)
|
||
|
|
||
|
async function rebuild(config) {
|
||
|
env.DEBUG && console.time('Finished in')
|
||
|
|
||
|
let tailwindPlugin = () => {
|
||
|
return {
|
||
|
postcssPlugin: 'tailwindcss',
|
||
|
Once(root, { result }) {
|
||
|
env.DEBUG && console.time('Compiling CSS')
|
||
|
tailwind(({ createContext }) => {
|
||
|
console.error()
|
||
|
console.error('Rebuilding...')
|
||
|
|
||
|
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')
|
||
|
},
|
||
|
}
|
||
|
}
|
||
|
|
||
|
tailwindPlugin.postcss = true
|
||
|
|
||
|
let tailwindPluginIdx = plugins.indexOf('__TAILWIND_PLUGIN_POSITION__')
|
||
|
let copy = plugins.slice()
|
||
|
copy.splice(tailwindPluginIdx, 1, tailwindPlugin)
|
||
|
let postcss = loadPostcss()
|
||
|
let processor = postcss(copy)
|
||
|
|
||
|
function processCSS(css) {
|
||
|
let start = process.hrtime.bigint()
|
||
|
return Promise.resolve()
|
||
|
.then(() =>
|
||
|
output ? fs.promises.mkdir(path.dirname(output), { recursive: true }) : null
|
||
|
)
|
||
|
.then(() => processor.process(css, { from: input, to: output }))
|
||
|
.then(async (result) => {
|
||
|
for (let message of result.messages) {
|
||
|
if (message.type === 'dependency') {
|
||
|
contextDependencies.add(message.file)
|
||
|
}
|
||
|
}
|
||
|
watcher.add([...contextDependencies])
|
||
|
|
||
|
if (!output) {
|
||
|
return process.stdout.write(result.css)
|
||
|
}
|
||
|
|
||
|
return Promise.all(
|
||
|
[
|
||
|
outputFile(output, result.css),
|
||
|
result.map && outputFile(output + '.map', result.map.toString()),
|
||
|
].filter(Boolean)
|
||
|
)
|
||
|
})
|
||
|
.then(() => {
|
||
|
let end = process.hrtime.bigint()
|
||
|
console.error('Done in', (end - start) / BigInt(1e6) + 'ms.')
|
||
|
})
|
||
|
.catch((err) => {
|
||
|
if (err.name === 'CssSyntaxError') {
|
||
|
console.error(err.toString())
|
||
|
} else {
|
||
|
console.error(err)
|
||
|
}
|
||
|
})
|
||
|
}
|
||
|
|
||
|
let css = await (() => {
|
||
|
// Piping in data, let's drain the stdin
|
||
|
if (input === '-') {
|
||
|
return drainStdin()
|
||
|
}
|
||
|
|
||
|
// Input file has been provided
|
||
|
if (input) {
|
||
|
return fs.readFileSync(path.resolve(input), 'utf8')
|
||
|
}
|
||
|
|
||
|
// No input file provided, fallback to default atrules
|
||
|
return '@tailwind base; @tailwind components; @tailwind utilities'
|
||
|
})()
|
||
|
|
||
|
let result = await processCSS(css)
|
||
|
env.DEBUG && console.timeEnd('Finished in')
|
||
|
return result
|
||
|
}
|
||
|
|
||
|
let config = refreshConfig(configPath)
|
||
|
|
||
|
if (input) {
|
||
|
contextDependencies.add(path.resolve(input))
|
||
|
}
|
||
|
|
||
|
watcher = chokidar.watch([...contextDependencies, ...extractFileGlobs(config)], {
|
||
|
usePolling: shouldPoll,
|
||
|
interval: shouldPoll ? pollInterval : undefined,
|
||
|
ignoreInitial: true,
|
||
|
awaitWriteFinish: shouldCoalesceWriteEvents
|
||
|
? {
|
||
|
stabilityThreshold: 50,
|
||
|
pollInterval: pollInterval,
|
||
|
}
|
||
|
: false,
|
||
|
})
|
||
|
|
||
|
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).slice(1),
|
||
|
})
|
||
|
|
||
|
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).slice(1),
|
||
|
})
|
||
|
|
||
|
await rebuild(config)
|
||
|
})
|
||
|
})
|
||
|
|
||
|
chain = chain.then(() => {
|
||
|
changedContent.push(...getChangedContent(config))
|
||
|
return rebuild(config)
|
||
|
})
|
||
|
}
|
||
|
|
||
|
if (shouldWatch) {
|
||
|
/* Abort the watcher if stdin is closed to avoid zombie processes */
|
||
|
if (process.stdin.isTTY) {
|
||
|
process.stdin.on('end', () => process.exit(0))
|
||
|
process.stdin.resume()
|
||
|
}
|
||
|
startWatcher()
|
||
|
} else {
|
||
|
buildOnce()
|
||
|
}
|
||
|
}
|