#!/usr/bin/env node "use strict"; var _index = 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 = _interopRequireDefault(require("postcss-load-config/src/plugins")); var _processTailwindFeatures = _interopRequireDefault(require("./jit/processTailwindFeatures")); var _processTailwindFeatures2 = _interopRequireDefault(require("./processTailwindFeatures")); var _resolveConfig = _interopRequireDefault(require("../resolveConfig")); var _fastGlob = _interopRequireDefault(require("fast-glob")); var _getModuleDependencies = _interopRequireDefault(require("./lib/getModuleDependencies")); var _package = _interopRequireDefault(require("../package.json")); function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } // Little bit scary, looking at private/internal API 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 }) { let indent = 2; // Render header console.log(); console.log(`${_package.default.name} v${_package.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 (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 } of Object.values(groupedOptions)) { 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 }); } let commands = { init: { run: init, args: { '--jit': { type: Boolean, description: 'Initialize for JIT mode' }, '--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' }, '--jit': { type: Boolean, description: 'Build using JIT mode' }, '--purge': { type: String, description: 'Content paths to use for removing unused classes' }, '--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(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.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(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 = (0, _arg.default)(Object.fromEntries(Object.entries({ ...flags, ...sharedFlags }).filter(([_key, value]) => { var _value$type; return !(value !== null && value !== void 0 && (_value$type = value.type) !== null && _value$type !== void 0 && _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 [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() { var _args$_$; let messages = []; let tailwindConfigLocation = _path.default.resolve((_args$_$ = args['_'][1]) !== null && _args$_$ !== void 0 ? _args$_$ : './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'); // --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.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 && !_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) : {}; let resolvedConfig = (0, _resolveConfig.default)(config); if (args['--purge']) { resolvedConfig.purge = { enabled: true, content: args['--purge'].split(/(? { if (typeof content === 'string') { return { raw: content, extension: 'html' }; } if (content instanceof RegExp) { throw new Error("Values inside 'purge.safelist' can only be of type 'string', found 'regex'."); } throw new Error(`Values inside 'purge.safelist' can only be of type 'string', found '${typeof 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.default.sync(globs); for (let file of files) { changedContent.push({ content: _fs.default.readFileSync(_path.default.resolve(file), 'utf8'), extension: _path.default.extname(file) }); } // 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 = config.mode === 'jit' ? () => { return { postcssPlugin: 'tailwindcss', Once(root, { result }) { (0, _processTailwindFeatures.default)(({ createContext }) => { return () => { return createContext(config, changedContent); }; })(root, result); } }; } : () => { return { postcssPlugin: 'tailwindcss', plugins: [(0, _processTailwindFeatures2.default)(() => config, configPath)] }; }; 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, _index.lazyAutoprefixer)(); })(), args['--minify'] && (() => { let options = { preset: ['default', { cssDeclarationSorter: false }] }; // Try to load a local `cssnano` version first try { return require('cssnano'); } catch {} return (0, _index.lazyCssnano)()(options); })()].filter(Boolean); let processor = (0, _index.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([_fs.default.promises.writeFile(output, result.css, () => true), result.map && _fs.default.writeFile(output + '.map', result.map.toString(), () => true)].filter(Boolean)); }).then(() => { let end = process.hrtime.bigint(); console.error(); console.error('Done in', (end - start) / BigInt(1e6) + 'ms.'); }); } let css = input ? _fs.default.readFileSync(_path.default.resolve(input), 'utf8') : '@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 = (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, _index.lazyAutoprefixer)(); })(), args['--minify'] && (() => { let options = { preset: ['default', { cssDeclarationSorter: false }] }; // Try to load a local `cssnano` version first try { return require('cssnano'); } catch {} return (0, _index.lazyCssnano)()(options); })()].filter(Boolean); async function rebuild(config) { env.DEBUG && console.time('Finished in'); let tailwindPlugin = config.mode === 'jit' ? () => { 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'); } }; } : () => { return { postcssPlugin: 'tailwindcss', plugins: [(0, _processTailwindFeatures2.default)(() => config, configPath)] }; }; tailwindPlugin.postcss = true; let tailwindPluginIdx = plugins.indexOf('__TAILWIND_PLUGIN_POSITION__'); let copy = plugins.slice(); copy.splice(tailwindPluginIdx, 1, tailwindPlugin); let processor = (0, _index.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); } await Promise.all([_fs.default.promises.writeFile(output, result.css, () => true), result.map && _fs.default.writeFile(output + '.map', result.map.toString(), () => true)].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 = input ? _fs.default.readFileSync(_path.default.resolve(input), 'utf8') : '@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.default.resolve(input)); } watcher = _chokidar.default.watch([...contextDependencies, ...extractFileGlobs(config)], { 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; 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.default.readFileSync(_path.default.resolve(file), 'utf8'), extension: _path.default.extname(file) }); await rebuild(config); }); } }); 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) }); 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 */ process.stdin.on('end', () => process.exit(0)); process.stdin.resume(); startWatcher(); } else { buildOnce(); } }