#!/usr/bin/env node "use strict"; var _indexJs = require("../peers/index.js"); var _chokidar = _interopRequireDefault(require("chokidar")); var _path = _interopRequireDefault(require("path")); var _arg = _interopRequireDefault(require("arg")); var _fs = _interopRequireDefault(require("fs")); var _postcssLoadConfig = _interopRequireDefault(require("postcss-load-config")); var _cosmiconfig = require("cosmiconfig"); var _plugins // Little bit scary, looking at private/internal API = _interopRequireDefault(require("postcss-load-config/src/plugins")); var _processTailwindFeatures = _interopRequireDefault(require("./processTailwindFeatures")); var _resolveConfig = _interopRequireDefault(require("../resolveConfig")); var _fastGlob = _interopRequireDefault(require("fast-glob")); var _getModuleDependencies = _interopRequireDefault(require("./lib/getModuleDependencies")); var _log = _interopRequireDefault(require("./util/log")); var _packageJson = _interopRequireDefault(require("../package.json")); var _normalizePath = _interopRequireDefault(require("normalize-path")); function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } let env = { DEBUG: process.env.DEBUG !== undefined && process.env.DEBUG !== '0' }; // --- 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.default.existsSync(file) && await _fs.default.promises.readFile(file, 'utf8') === contents) { return; // Skip writing the file } // Write the file await _fs.default.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: commands1 , options }) { let indent = 2; // Render header console.log(); console.log(`${_packageJson.default.name} v${_packageJson.default.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 (commands1 && commands1.length > 0) { console.log(); console.log('Commands:'); for (let command1 of commands1){ console.log(' '.repeat(indent), command1); } } // 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: flags1 , description , deprecated } of Object.values(groupedOptions)){ if (deprecated) continue; if (flags1.length === 1) { console.log(' '.repeat(indent + 4 /* 4 = "-i, ".length */ ), flags1.slice().reverse().join(', ').padEnd(20, ' '), description); } else { console.log(' '.repeat(indent), flags1.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 }); } let commands = { init: { run: init, args: { '--full': { type: Boolean, description: 'Initialize a full `tailwind.config.js` file' }, '--postcss': { type: Boolean, description: 'Initialize a `postcss.config.js` 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' }, '--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' } } }; 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((command2)=>command2 !== 'build' ).map((command3)=>`${command3} [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.default.existsSync(_path.default.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 [options]' ], commands: Object.keys(commands).filter((command4)=>command4 !== 'build' ).map((command5)=>`${command5} [options]` ), options: sharedFlags }); process.exit(1); } } // Execute command let { args: flags , run } = commands[command]; let args = (()=>{ try { let result = (0, _arg).default(Object.fromEntries(Object.entries({ ...flags, ...sharedFlags }).filter(([_key, value])=>{ var ref; return !(value === null || value === void 0 ? void 0 : (ref = value.type) === null || ref === void 0 ? void 0 : ref.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 args1 = []; let offset = i + 1; // Parse args for current flag while(result['_'][offset] && !result['_'][offset].startsWith('-')){ args1.push(result['_'][offset++]); } // Cleanup manually parsed flags + args result['_'].splice(i, 1 + args1.length); // Set the resolved value in the `result` object result[flagName] = handler.type(args1.length === 0 ? undefined : args1.length === 1 ? args1[0] : args1, 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 [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 = []; var ref; let tailwindConfigLocation = _path.default.resolve((ref = args['_'][1]) !== null && ref !== void 0 ? ref : './tailwind.config.js'); if (_fs.default.existsSync(tailwindConfigLocation)) { messages.push(`${_path.default.basename(tailwindConfigLocation)} already exists.`); } else { let stubFile = _fs.default.readFileSync(args['--full'] ? _path.default.resolve(__dirname, '../stubs/defaultConfig.stub.js') : _path.default.resolve(__dirname, '../stubs/simpleConfig.stub.js'), 'utf8'); // Change colors import stubFile = stubFile.replace('../colors', 'tailwindcss/colors'); _fs.default.writeFileSync(tailwindConfigLocation, stubFile, 'utf8'); messages.push(`Created Tailwind CSS config file: ${_path.default.basename(tailwindConfigLocation)}`); } if (args['--postcss']) { let postcssConfigLocation = _path.default.resolve('./postcss.config.js'); if (_fs.default.existsSync(postcssConfigLocation)) { messages.push(`${_path.default.basename(postcssConfigLocation)} already exists.`); } else { let stubFile = _fs.default.readFileSync(_path.default.resolve(__dirname, '../stubs/defaultPostCssConfig.stub.js'), 'utf8'); _fs.default.writeFileSync(postcssConfigLocation, stubFile, 'utf8'); messages.push(`Created PostCSS config file: ${_path.default.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 includePostCss = args['--postcss']; // 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.default.existsSync(input = _path.default.resolve(input))) { console.error(`Specified input file ${args['--input']} does not exist.`); process.exit(9); } if (args['--config'] && !_fs.default.existsSync(args['--config'] = _path.default.resolve(args['--config']))) { console.error(`Specified config file ${args['--config']} does not exist.`); process.exit(9); } let configPath = args['--config'] ? args['--config'] : ((defaultPath)=>_fs.default.existsSync(defaultPath) ? defaultPath : null )(_path.default.resolve('./tailwind.config.js')); async function loadPostCssPlugins() { let customPostCssPath = typeof args['--postcss'] === 'string' ? args['--postcss'] : undefined; let { plugins: configPlugins } = customPostCssPath ? await (async ()=>{ let file = _path.default.resolve(customPostCssPath); // Implementation, see: https://unpkg.com/browse/postcss-load-config@3.0.1/src/index.js let { config ={} } = await (0, _cosmiconfig).cosmiconfig('postcss').load(file); if (typeof config === 'function') { config = config(); } else { config = Object.assign({}, config); } if (!config.plugins) { config.plugins = []; } return { plugins: (0, _plugins).default(config, file) }; })() : await (0, _postcssLoadConfig).default(); 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 ]; } function resolveConfig() { let config = configPath ? require(configPath) : {}; if (args['--purge']) { _log.default.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(/(?{ // 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)=>(0, _normalizePath).default(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.default.sync(globs); for (let file of files){ changedContent.push({ content: _fs.default.readFileSync(_path.default.resolve(file), 'utf8'), extension: _path.default.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 }) { (0, _processTailwindFeatures).default(({ createContext })=>{ return ()=>{ return createContext(config, changedContent); }; })(root, result); } }; }; tailwindPlugin.postcss = true; let [beforePlugins, afterPlugins] = includePostCss ? await loadPostCssPlugins() : [ [], [] ]; 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 (0, _indexJs).lazyAutoprefixer(); })(), args['--minify'] && (()=>{ let options = { preset: [ 'default', { cssDeclarationSorter: false } ] }; // Try to load a local `cssnano` version first try { return require('cssnano'); } catch {} return (0, _indexJs).lazyCssnano()(options); })(), ].filter(Boolean); let processor = (0, _indexJs).postcss(plugins); function processCSS(css) { let start = process.hrtime.bigint(); return Promise.resolve().then(()=>output ? _fs.default.promises.mkdir(_path.default.dirname(output), { recursive: true }) : null ).then(()=>processor.process(css, { 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(1000000) + 'ms.'); }); } let css1 = await (()=>{ // Piping in data, let's drain the stdin if (input === '-') { return drainStdin(); } // Input file has been provided if (input) { return _fs.default.readFileSync(_path.default.resolve(input), 'utf8'); } // No input file provided, fallback to default atrules return '@tailwind base; @tailwind components; @tailwind utilities'; })(); return processCSS(css1); } 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 file1 of configDependencies){ delete require.cache[require.resolve(file1)]; } if (configPath) { configDependencies = (0, _getModuleDependencies).default(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() : [ [], [] ]; 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 (0, _indexJs).lazyAutoprefixer(); })(), args['--minify'] && (()=>{ let options = { preset: [ 'default', { cssDeclarationSorter: false } ] }; // Try to load a local `cssnano` version first try { return require('cssnano'); } catch {} return (0, _indexJs).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'); (0, _processTailwindFeatures).default(({ 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 processor = (0, _indexJs).postcss(copy); function processCSS(css) { let start = process.hrtime.bigint(); return Promise.resolve().then(()=>output ? _fs.default.promises.mkdir(_path.default.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(1000000) + 'ms.'); }).catch((err)=>{ if (err.name === 'CssSyntaxError') { console.error(err.toString()); } else { console.error(err); } }); } let css2 = await (()=>{ // Piping in data, let's drain the stdin if (input === '-') { return drainStdin(); } // Input file has been provided if (input) { return _fs.default.readFileSync(_path.default.resolve(input), 'utf8'); } // No input file provided, fallback to default atrules return '@tailwind base; @tailwind components; @tailwind utilities'; })(); let result1 = await processCSS(css2); env.DEBUG && console.timeEnd('Finished in'); return result1; } let config1 = refreshConfig(configPath); if (input) { contextDependencies.add(_path.default.resolve(input)); } watcher = _chokidar.default.watch([ ...contextDependencies, ...extractFileGlobs(config1) ], { ignoreInitial: true, awaitWriteFinish: process.platform === 'win32' ? { stabilityThreshold: 50, pollInterval: 10 } : false }); let chain = Promise.resolve(); watcher.on('change', async (file)=>{ if (contextDependencies.has(file)) { env.DEBUG && console.time('Resolve config'); context = null; config1 = refreshConfig(configPath); env.DEBUG && console.timeEnd('Resolve config'); env.DEBUG && console.time('Watch new files'); let globs = extractFileGlobs(config1); watcher.add(configDependencies); watcher.add(globs); env.DEBUG && console.timeEnd('Watch new files'); chain = chain.then(async ()=>{ changedContent.push(...getChangedContent(config1)); await rebuild(config1); }); } else { chain = chain.then(async ()=>{ changedContent.push({ content: _fs.default.readFileSync(_path.default.resolve(file), 'utf8'), extension: _path.default.extname(file).slice(1) }); await rebuild(config1); }); } }); watcher.on('add', async (file)=>{ chain = chain.then(async ()=>{ changedContent.push({ content: _fs.default.readFileSync(_path.default.resolve(file), 'utf8'), extension: _path.default.extname(file).slice(1) }); await rebuild(config1); }); }); chain = chain.then(()=>{ changedContent.push(...getChangedContent(config1)); return rebuild(config1); }); } if (shouldWatch) { /* Abort the watcher if stdin is closed to avoid zombie processes */ process.stdin.on('end', ()=>process.exit(0) ); process.stdin.resume(); startWatcher(); } else { buildOnce(); } }