#!/usr/bin/env node import path from 'path' import arg from 'arg' import fs from 'fs' import { build } from './build' import { help } from './help' import { init } from './init' // --- 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: { '--esm': { type: Boolean, description: `Initialize configuration file as ESM` }, '--ts': { type: Boolean, description: `Initialize configuration file as TypeScript` }, '--full': { type: Boolean, description: `Include the default values for all options in the generated configuration file`, }, '-f': '--full', }, }, build: { run: build, args: { '--input': { type: String, description: 'Input file' }, '--output': { type: String, description: 'Output file' }, '--watch': { type: oneOf(String, 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', }, '--minify': { type: Boolean, description: 'Minify the output' }, '--config': { type: String, description: 'Path to a custom config file', }, '-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] [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 [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, flagValue] = flag.split('=') let handler = flags[flagName] // Resolve flagName & handler while (typeof handler === 'string') { flagName = handler handler = flags[handler] } if (!handler) continue let args = [] let offset = i + 1 // --flag value syntax was used so we need to pull `value` from `args` if (flagValue === undefined) { // 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) // No args were provided, use default value defined in handler // One arg was provided, use that directly // Multiple args were provided so pass them all in an array flagValue = args.length === 0 ? undefined : args.length === 1 ? args[0] : args } else { // Remove the whole flag from the args array result['_'].splice(i, 1) } // Set the resolved value in the `result` object result[flagName] = handler.type(flagValue, 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(args)