#!/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 _lilconfig = require("lilconfig"); var _plugins // Little bit scary, looking at private/internal API = _interopRequireDefault(require("postcss-load-config/src/plugins")); var _options // Little bit scary, looking at private/internal API = _interopRequireDefault(require("postcss-load-config/src/options")); 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")); var _validateConfigJs = require("./util/validateConfig.js"); function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } let env = { DEBUG: process.env.DEBUG !== undefined && process.env.DEBUG !== "0" }; function isESM() { const pkgPath = _path.default.resolve("./package.json"); try { let pkg = JSON.parse(_fs.default.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.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 }); } function loadPostcss() { // Try to load a local `postcss` version first try { return require("postcss"); } catch {} return (0, _indexJs).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((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 : `./${configs.tailwind}`); 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(`./${configs.postcss}`); 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 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.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(`./${configs.tailwind}`)); async function loadPostCssPlugins() { let customPostCssPath = typeof args["--postcss"] === "string" ? args["--postcss"] : undefined; let config1 = customPostCssPath ? await (async ()=>{ let file = _path.default.resolve(customPostCssPath); // Implementation, see: https://unpkg.com/browse/postcss-load-config@3.1.0/src/index.js let { config ={} } = await (0, _lilconfig).lilconfig("postcss").load(file); if (typeof config === "function") { config = config(); } else { config = Object.assign({}, config); } if (!config.plugins) { config.plugins = []; } return { file, plugins: (0, _plugins).default(config, file), options: (0, _options).default(config, file) }; })() : await (0, _postcssLoadConfig).default(); let configPlugins = config1.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, config1.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 (0, _indexJs).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.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, 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 (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 postcss = loadPostcss(); let processor = 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, { ...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 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() : 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 (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 postcss = loadPostcss(); let processor = 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(1e6) + "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 config2 = refreshConfig(configPath); if (input) { contextDependencies.add(_path.default.resolve(input)); } watcher = _chokidar.default.watch([ ...contextDependencies, ...extractFileGlobs(config2) ], { 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; config2 = refreshConfig(configPath); env.DEBUG && console.timeEnd("Resolve config"); env.DEBUG && console.time("Watch new files"); let globs = extractFileGlobs(config2); watcher.add(configDependencies); watcher.add(globs); env.DEBUG && console.timeEnd("Watch new files"); chain = chain.then(async ()=>{ changedContent.push(...getChangedContent(config2)); await rebuild(config2); }); } 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(config2); }); } }); 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(config2); }); }); chain = chain.then(()=>{ changedContent.push(...getChangedContent(config2)); return rebuild(config2); }); } 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(); } }