Skip to content

Commit

Permalink
Merge pull request #554 from MattStypa/cli
Browse files Browse the repository at this point in the history
CLI Beautification
  • Loading branch information
adamwathan authored Sep 24, 2018
2 parents 0138fc6 + eb4da80 commit 4fc519f
Show file tree
Hide file tree
Showing 15 changed files with 486 additions and 118 deletions.
62 changes: 47 additions & 15 deletions __tests__/cli.test.js
Original file line number Diff line number Diff line change
@@ -1,21 +1,53 @@
import { spawnSync } from 'child_process'
import fs from 'fs'
import path from 'path'

function runCli(task, options) {
return spawnSync('node', [`${path.join(process.cwd(), 'lib/cli.js')}`, `${task}`, ...options])
}
import cli from '../src/cli/main'
import * as constants from '../src/cli/constants'
import * as utils from '../src/cli/utils'

function pathToFixture(fixture) {
return path.resolve(`${__dirname}/fixtures/${fixture}`)
}
describe('cli', () => {
const inputCssPath = path.resolve(__dirname, 'fixtures/tailwind-input.css')
const customConfigPath = path.resolve(__dirname, 'fixtures/custom-config.js')

function readFixture(fixture) {
return fs.readFileSync(pathToFixture(fixture), 'utf8')
}
beforeEach(() => {
console.log = jest.fn()
process.stdout.write = jest.fn()
utils.writeFile = jest.fn()
})

test('stdout only contains processed output', () => {
const expected = readFixture('tailwind-cli-output.css')
const result = runCli('build', [pathToFixture('tailwind-cli-input.css')])
expect(result.stdout.toString()).toEqual(expected)
describe('init', () => {
it('creates a Tailwind config file', () => {
cli(['init']).then(() => {
expect(utils.writeFile.mock.calls[0][0]).toEqual(constants.defaultConfigFile)
expect(utils.writeFile.mock.calls[0][1]).toContain('defaultConfig')
})
})

it('creates a Tailwind config file in a custom location', () => {
cli(['init', 'custom.js']).then(() => {
expect(utils.writeFile.mock.calls[0][0]).toEqual('custom.js')
expect(utils.writeFile.mock.calls[0][1]).toContain('defaultConfig')
})
})
})

describe('build', () => {
it('compiles CSS file', () => {
cli(['build', inputCssPath]).then(() => {
expect(process.stdout.write.mock.calls[0][0]).toContain('.example')
})
})

it('compiles CSS file using custom configuration', () => {
cli(['build', inputCssPath, '--config', customConfigPath]).then(() => {
expect(process.stdout.write.mock.calls[0][0]).toContain('400px')
})
})

it('creates compiled CSS file', () => {
cli(['build', inputCssPath, '--output', 'output.css']).then(() => {
expect(utils.writeFile.mock.calls[0][0]).toEqual('output.css')
expect(utils.writeFile.mock.calls[0][1]).toContain('.example')
})
})
})
})
2 changes: 1 addition & 1 deletion __tests__/customConfig.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import postcss from 'postcss'
import tailwind from '../src/index'

test('it uses the values from the custom config file', () => {
return postcss([tailwind(path.resolve(`${__dirname}/fixtures/customConfig.js`))])
return postcss([tailwind(path.resolve(`${__dirname}/fixtures/custom-config.js`))])
.process(
`
@responsive {
Expand Down
File renamed without changes.
3 changes: 0 additions & 3 deletions __tests__/fixtures/tailwind-cli-input.css

This file was deleted.

3 changes: 0 additions & 3 deletions __tests__/fixtures/tailwind-cli-output.css

This file was deleted.

9 changes: 6 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@
"test": "jest && eslint . && nsp check"
},
"devDependencies": {
"autoprefixer": "^7.1.6",
"babel-cli": "^6.6.5",
"babel-core": "^6.7.2",
"babel-jest": "^20.0.3",
Expand All @@ -43,16 +42,20 @@
"rimraf": "^2.6.1"
},
"dependencies": {
"commander": "^2.11.0",
"autoprefixer": "^7.1.6",
"bytes": "^3.0.0",
"chalk": "^2.4.1",
"css.escape": "^1.5.1",
"fs-extra": "^4.0.2",
"lodash": "^4.17.5",
"node-emoji": "^1.8.1",
"perfectionist": "^2.4.0",
"postcss": "^6.0.9",
"postcss-functions": "^3.0.0",
"postcss-js": "^1.0.1",
"postcss-nested": "^3.0.0",
"postcss-selector-parser": "^3.1.1"
"postcss-selector-parser": "^3.1.1",
"pretty-hrtime": "^1.0.3"
},
"browserslist": [
"> 1%"
Expand Down
96 changes: 3 additions & 93 deletions src/cli.js
Original file line number Diff line number Diff line change
@@ -1,96 +1,6 @@
#!/usr/bin/env node
/* eslint-disable no-process-exit */

import path from 'path'
import fs from 'fs-extra'
import tailwind from '..'
import postcss from 'postcss'
import process from 'process'
import program from 'commander'
import main from './cli/main'
import * as utils from './cli/utils'

function writeStrategy(options) {
if (options.output === undefined) {
return output => {
process.stdout.write(output)
}
}
return output => {
fs.outputFileSync(options.output, output)
}
}

function buildTailwind(inputFile, config, write) {
console.warn('Building Tailwind!')

const input = fs.readFileSync(inputFile, 'utf8')

return postcss([tailwind(config)])
.process(input, { from: inputFile })
.then(result => {
write(result.css)
console.warn('Finished building Tailwind!')
})
.catch(error => console.error(error))
}

const packageJson = require(path.resolve(__dirname, '../package.json'))

program.version(packageJson.version).usage('<command> [<args>]')

program
.command('init [filename]')
.usage('[options] [filename]')
.action((filename = 'tailwind.js') => {
let destination = path.resolve(filename)

if (!path.extname(filename).includes('.js')) {
destination += '.js'
}

if (fs.existsSync(destination)) {
console.error(`Destination ${destination} already exists, aborting.`)
process.exit(1)
}

const output = fs.readFileSync(path.resolve(__dirname, '../defaultConfig.stub.js'), 'utf8')
fs.outputFileSync(destination, output.replace('// let defaultConfig', 'let defaultConfig'))
fs.outputFileSync(
destination,
output.replace("require('./plugins/container')", "require('tailwindcss/plugins/container')")
)
console.warn(`Generated Tailwind config: ${destination}`)
process.exit()
})

program
.command('build')
.usage('[options] <file ...>')
.option('-c, --config [path]', 'Path to config file')
.option('-o, --output [path]', 'Output file')
.action((file, options) => {
let inputFile = program.args[0]

if (!inputFile) {
console.error('No input file given!')
process.exit(1)
}

buildTailwind(inputFile, options.config, writeStrategy(options)).then(() => {
process.exit()
})
})

program
.command('*', null, {
noHelp: true,
})
.action(() => {
program.help()
})

program.parse(process.argv)

if (program.args.length === 0) {
program.help()
process.exit()
}
main(process.argv.slice(2)).catch(error => utils.die(error.stack))
143 changes: 143 additions & 0 deletions src/cli/commands/build.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
import autoprefixer from 'autoprefixer'
import bytes from 'bytes'
import chalk from 'chalk'
import postcss from 'postcss'
import prettyHrtime from 'pretty-hrtime'

import tailwind from '../..'

import commands from '.'
import * as emoji from '../emoji'
import * as utils from '../utils'

export const usage = 'build <file> [options]'
export const description = 'Compiles Tailwind CSS file.'

export const options = [
{
usage: '-o, --output <file>',
description: 'Output file.',
},
{
usage: '-c, --config <file>',
description: 'Tailwind config file.',
},
]

export const optionMap = {
output: ['output', 'o'],
config: ['config', 'c'],
}

/**
* Prints the error message and stops the process.
*
* @param {...string} [msgs]
*/
function stop(...msgs) {
utils.header()
utils.error(...msgs)
utils.die()
}

/**
* Prints the error message and help for this command, then stops the process.
*
* @param {...string} [msgs]
*/
function stopWithHelp(...msgs) {
utils.header()
utils.error(...msgs)
commands.help.forCommand(commands.build)
utils.die()
}

/**
* Compiles CSS file.
*
* @param {string} inputFile
* @param {string} configFile
* @param {string} outputFile
* @return {Promise}
*/
function build(inputFile, configFile, outputFile) {
const css = utils.readFile(inputFile)

return new Promise((resolve, reject) => {
postcss([tailwind(configFile), autoprefixer])
.process(css, {
from: inputFile,
to: outputFile,
})
.then(resolve)
.catch(reject)
})
}

/**
* Compiles CSS file and writes it to stdout.
*
* @param {string} inputFile
* @param {string} configFile
* @param {string} outputFile
* @return {Promise}
*/
function buildToStdout(inputFile, configFile, outputFile) {
return build(inputFile, configFile, outputFile).then(result => process.stdout.write(result.css))
}

/**
* Compiles CSS file and writes it to a file.
*
* @param {string} inputFile
* @param {string} configFile
* @param {string} outputFile
* @param {int[]} startTime
* @return {Promise}
*/
function buildToFile(inputFile, configFile, outputFile, startTime) {
utils.header()
utils.log()
utils.log(emoji.go, 'Building...', chalk.bold.cyan(inputFile))

return build(inputFile, configFile, outputFile).then(result => {
utils.writeFile(outputFile, result.css)

const prettyTime = prettyHrtime(process.hrtime(startTime))

utils.log()
utils.log(emoji.yes, 'Finished in', chalk.bold.magenta(prettyTime))
utils.log(emoji.pack, 'Size:', chalk.bold.magenta(bytes(result.css.length)))
utils.log(emoji.disk, 'Saved to', chalk.bold.cyan(outputFile))
utils.footer()
})
}

/**
* Runs the command.
*
* @param {string[]} cliParams
* @param {object} cliOptions
* @return {Promise}
*/
export function run(cliParams, cliOptions) {
return new Promise((resolve, reject) => {
const startTime = process.hrtime()
const inputFile = cliParams[0]
const configFile = cliOptions.config && cliOptions.config[0]
const outputFile = cliOptions.output && cliOptions.output[0]

!inputFile && stopWithHelp('CSS file is required.')
!utils.exists(inputFile) && stop(chalk.bold.magenta(inputFile), 'does not exist.')

configFile &&
!utils.exists(configFile) &&
stop(chalk.bold.magenta(configFile), 'does not exist.')

const buildPromise = outputFile
? buildToFile(inputFile, configFile, outputFile, startTime)
: buildToStdout(inputFile, configFile, outputFile)

buildPromise.then(resolve).catch(reject)
})
}
Loading

0 comments on commit 4fc519f

Please sign in to comment.