From 377a9244202d88995fea1a2a458eb7238946f6fd Mon Sep 17 00:00:00 2001 From: Matt Stypa Date: Mon, 18 Mar 2019 21:08:44 -0500 Subject: [PATCH 1/4] Added update CLI command --- __tests__/cli.test.js | 71 +++-- __tests__/cli.update.transform.test.js | 69 +++++ __tests__/cli.update.validate.test.js | 9 + __tests__/cli.utils.test.js | 14 + __tests__/customConfig.test.js | 14 +- jest/runInTempDirectory.js | 21 ++ src/cli/colors.js | 46 +++ src/cli/commands/build.js | 19 +- src/cli/commands/help.js | 14 +- src/cli/commands/index.js | 3 +- src/cli/commands/init.js | 13 +- src/cli/commands/update.js | 121 ++++++++ src/cli/commands/update/transform.js | 76 +++++ src/cli/commands/update/validator.js | 96 ++++++ src/cli/utils.js | 30 +- src/constants.js | 4 + stubs/defaultConfig.stub.js | 1 + stubs/oldDefaultConfig.stub.js | 413 +++++++++++++++++++++++++ 18 files changed, 970 insertions(+), 64 deletions(-) create mode 100644 __tests__/cli.update.transform.test.js create mode 100644 __tests__/cli.update.validate.test.js create mode 100644 jest/runInTempDirectory.js create mode 100644 src/cli/colors.js create mode 100644 src/cli/commands/update.js create mode 100644 src/cli/commands/update/transform.js create mode 100644 src/cli/commands/update/validator.js create mode 100644 stubs/oldDefaultConfig.stub.js diff --git a/__tests__/cli.test.js b/__tests__/cli.test.js index 2e04a0234cda..b7791158bcde 100644 --- a/__tests__/cli.test.js +++ b/__tests__/cli.test.js @@ -3,6 +3,7 @@ import path from 'path' import cli from '../src/cli/main' import * as constants from '../src/constants' import * as utils from '../src/cli/utils' +import runInTempDirectory from '../jest/runInTempDirectory' describe('cli', () => { const inputCssPath = path.resolve(__dirname, 'fixtures/tailwind-input.css') @@ -13,37 +14,30 @@ describe('cli', () => { beforeEach(() => { console.log = jest.fn() process.stdout.write = jest.fn() - utils.writeFile = jest.fn() }) describe('init', () => { it('creates a Tailwind config file', () => { - return cli(['init']).then(() => { - expect(utils.writeFile.mock.calls[0][0]).toEqual(constants.defaultConfigFile) + return runInTempDirectory(() => { + return cli(['init']).then(() => { + expect(utils.readFile(constants.defaultConfigFile)).toEqual(simpleConfigFixture) + }) }) }) - it('creates a Tailwind config file in a custom location', () => { - return cli(['init', 'custom.js']).then(() => { - expect(utils.writeFile.mock.calls[0][0]).toEqual('custom.js') - }) - }) - - it('creates a Tailwind config file without comments', () => { - return cli(['init', '--no-comments']).then(() => { - expect(utils.writeFile.mock.calls[0][1]).not.toContain('/**') - }) - }) - - it('creates a simple Tailwind config file', () => { - return cli(['init']).then(() => { - expect(utils.writeFile.mock.calls[0][1]).toEqual(simpleConfigFixture) + it('creates a full Tailwind config file', () => { + return runInTempDirectory(() => { + return cli(['init', '--full']).then(() => { + expect(utils.readFile(constants.defaultConfigFile)).toEqual(defaultConfigFixture) + }) }) }) - it('creates a full Tailwind config file', () => { - return cli(['init', '--full']).then(() => { - expect(utils.writeFile.mock.calls[0][1]).toEqual(defaultConfigFixture) + it('creates a Tailwind config file in a custom location', () => { + return runInTempDirectory(() => { + return cli(['init', 'custom.js']).then(() => { + expect(utils.exists('custom.js')).toEqual(true) + }) }) }) }) @@ -62,9 +56,10 @@ describe('cli', () => { }) it('creates compiled CSS file', () => { - return 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') + return runInTempDirectory(() => { + return cli(['build', inputCssPath, '--output', 'output.css']).then(() => { + expect(utils.readFile('output.css')).toContain('.example') + }) }) }) @@ -80,4 +75,32 @@ describe('cli', () => { }) }) }) + + describe('update', () => { + it('updates the configuration file', () => { + return runInTempDirectory(() => { + utils.copyFile(constants.oldDefaultConfigStubFile, constants.oldDefaultConfigFile) + + return cli(['update']).then(() => { + expect(utils.exists(constants.defaultConfigFile)).toEqual(true) + }) + }) + }) + + it('updates the configuration file from custom location', () => { + return runInTempDirectory(() => { + return cli(['update', constants.oldDefaultConfigStubFile]).then(() => { + expect(utils.exists(constants.defaultConfigFile)).toEqual(true) + }) + }) + }) + + it('updates the configuration file from custom location to custom location', () => { + return runInTempDirectory(() => { + return cli(['update', constants.oldDefaultConfigStubFile, 'custom.js']).then(() => { + expect(utils.exists('custom.js')).toEqual(true) + }) + }) + }) + }) }) diff --git a/__tests__/cli.update.transform.test.js b/__tests__/cli.update.transform.test.js new file mode 100644 index 000000000000..6c7eb9951640 --- /dev/null +++ b/__tests__/cli.update.transform.test.js @@ -0,0 +1,69 @@ +import transform from '../src/cli/commands/update/transform' +import defaultConfig from '../stubs/defaultConfig.stub.js' + +describe('cli update transform', () => { + let oldConfig + let containerPlugin + + beforeEach(() => { + containerPlugin = { + plugin: 'container', + options: {}, + } + + oldConfig = { + options: {}, + modules: {}, + plugins: [containerPlugin], + } + }) + + it('returns complete configuration', () => { + expect(transform(oldConfig)).toEqual(defaultConfig) + }) + + it('populates theme', () => { + oldConfig.colors = 'test' + expect(transform(oldConfig).theme.colors).toEqual('test') + }) + + it('transforms theme values that have changed', () => { + oldConfig.shadows = 'test' + expect(transform(oldConfig).theme.boxShadow).toEqual('test') + }) + + it('populates variants', () => { + oldConfig.modules.appearance = 'test' + expect(transform(oldConfig).variants.appearance).toEqual('test') + }) + + it('transforms variants that have changed', () => { + oldConfig.modules.svgFill = 'test' + expect(transform(oldConfig).variants.fill).toEqual('test') + }) + + it('disables variants using corePlugins', () => { + oldConfig.modules.float = false + expect(transform(oldConfig).corePlugins.float).toEqual(false) + }) + + it('populates options', () => { + oldConfig.options.important = 'test' + expect(transform(oldConfig).important).toEqual('test') + }) + + it('populates plugins', () => { + oldConfig.plugins = [jest.fn()] + expect(transform(oldConfig).plugins).toEqual(oldConfig.plugins) + }) + + it('transforms container plugin', () => { + containerPlugin.options = { padding: 'test' } + expect(transform(oldConfig).theme.container).toEqual(containerPlugin.options) + }) + + it('disables container plugin', () => { + oldConfig.plugins = [] + expect(transform(oldConfig).corePlugins.container).toEqual(false) + }) +}) diff --git a/__tests__/cli.update.validate.test.js b/__tests__/cli.update.validate.test.js new file mode 100644 index 000000000000..3c0fe53e500f --- /dev/null +++ b/__tests__/cli.update.validate.test.js @@ -0,0 +1,9 @@ +import { getMissingRequiredProperties } from '../src/cli/commands/update/validator' + +describe('cli update validator', () => { + describe('getMissingRequiredProperties', () => { + it('gets a list of missing required properties', () => { + expect(getMissingRequiredProperties({})).toEqual(expect.arrayContaining(['options.prefix'])) + }) + }) +}) diff --git a/__tests__/cli.utils.test.js b/__tests__/cli.utils.test.js index 2d9082d5ce34..ee167c8da14e 100644 --- a/__tests__/cli.utils.test.js +++ b/__tests__/cli.utils.test.js @@ -56,4 +56,18 @@ describe('cli utils', () => { expect(result).toEqual({ test: ['c', 'd', 'h'] }) }) }) + + describe('getSimplePath', () => { + it('strips leading ./', () => { + const result = utils.getSimplePath('./test') + + expect(result).toEqual('test') + }) + + it('returns unchanged path if it does not begin with ./', () => { + const result = utils.getSimplePath('../test') + + expect(result).toEqual('../test') + }) + }) }) diff --git a/__tests__/customConfig.test.js b/__tests__/customConfig.test.js index 97e15cfcf799..0d9cd1581db5 100644 --- a/__tests__/customConfig.test.js +++ b/__tests__/customConfig.test.js @@ -1,21 +1,9 @@ import fs from 'fs' import path from 'path' -import rimraf from 'rimraf' import postcss from 'postcss' import tailwind from '../src/index' import { defaultConfigFile } from '../src/constants' - -function inTempDirectory(callback) { - return new Promise(resolve => { - rimraf.sync('./__tmp') - fs.mkdirSync(path.resolve('./__tmp')) - process.chdir(path.resolve('./__tmp')) - callback().then(() => { - process.chdir(path.resolve('../')) - rimraf('./__tmp', resolve) - }) - }) -} +import inTempDirectory from '../jest/runInTempDirectory' test('it uses the values from the custom config file', () => { return postcss([tailwind(path.resolve(`${__dirname}/fixtures/custom-config.js`))]) diff --git a/jest/runInTempDirectory.js b/jest/runInTempDirectory.js new file mode 100644 index 000000000000..c6d025eb3b83 --- /dev/null +++ b/jest/runInTempDirectory.js @@ -0,0 +1,21 @@ +import fs from 'fs' +import path from 'path' + +import rimraf from 'rimraf' + +const tmpPath = path.resolve(__dirname, '../__tmp') + +export default function(callback) { + return new Promise(resolve => { + const currentPath = process.cwd() + + rimraf.sync(tmpPath) + fs.mkdirSync(tmpPath) + process.chdir(tmpPath) + + callback().then(() => { + process.chdir(currentPath) + rimraf(tmpPath, resolve) + }) + }) +} diff --git a/src/cli/colors.js b/src/cli/colors.js new file mode 100644 index 000000000000..8ba560ae2c08 --- /dev/null +++ b/src/cli/colors.js @@ -0,0 +1,46 @@ +import chalk from 'chalk' + +/** + * Applies colors to emphasize + * + * @param {...string} msgs + */ +export function bold(...msgs) { + return chalk.bold(...msgs) +} + +/** + * Applies colors to inform + * + * @param {...string} msgs + */ +export function info(...msgs) { + return chalk.bold.cyan(...msgs) +} + +/** + * Applies colors to signify error + * + * @param {...string} msgs + */ +export function error(...msgs) { + return chalk.bold.red(...msgs) +} + +/** + * Applies colors to represent a command + * + * @param {...string} msgs + */ +export function command(...msgs) { + return chalk.bold.magenta(...msgs) +} + +/** + * Applies colors to represent a file + * + * @param {...string} msgs + */ +export function file(...msgs) { + return chalk.bold.magenta(...msgs) +} diff --git a/src/cli/commands/build.js b/src/cli/commands/build.js index 99bbcf12608c..6d79b48f7c2c 100644 --- a/src/cli/commands/build.js +++ b/src/cli/commands/build.js @@ -1,12 +1,12 @@ import autoprefixer from 'autoprefixer' import bytes from 'bytes' -import chalk from 'chalk' import prettyHrtime from 'pretty-hrtime' import tailwind from '../..' import commands from '.' import compile from '../compile' +import * as colors from '../colors' import * as emoji from '../emoji' import * as utils from '../utils' @@ -75,9 +75,12 @@ function buildToStdout(compileOptions) { * @return {Promise} */ function buildToFile(compileOptions, startTime) { + const inputFileSimplePath = utils.getSimplePath(compileOptions.inputFile) + const outputFileSimplePath = utils.getSimplePath(compileOptions.outputFile) + utils.header() utils.log() - utils.log(emoji.go, 'Building...', chalk.bold.cyan(compileOptions.inputFile)) + utils.log(emoji.go, 'Building...', colors.file(inputFileSimplePath)) return compile(compileOptions).then(result => { utils.writeFile(compileOptions.outputFile, result.css) @@ -85,9 +88,9 @@ function buildToFile(compileOptions, startTime) { 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(compileOptions.outputFile)) + utils.log(emoji.yes, 'Finished in', colors.info(prettyTime)) + utils.log(emoji.pack, 'Size:', colors.info(bytes(result.css.length))) + utils.log(emoji.disk, 'Saved to', colors.file(outputFileSimplePath)) utils.footer() }) } @@ -106,13 +109,15 @@ export function run(cliParams, cliOptions) { const configFile = cliOptions.config && cliOptions.config[0] const outputFile = cliOptions.output && cliOptions.output[0] const autoprefix = !cliOptions.noAutoprefixer + const inputFileSimplePath = utils.getSimplePath(inputFile) + const configFileSimplePath = utils.getSimplePath(configFile) !inputFile && stopWithHelp('CSS file is required.') - !utils.exists(inputFile) && stop(chalk.bold.magenta(inputFile), 'does not exist.') + !utils.exists(inputFile) && stop(colors.file(inputFileSimplePath), 'does not exist.') configFile && !utils.exists(configFile) && - stop(chalk.bold.magenta(configFile), 'does not exist.') + stop(colors.file(configFileSimplePath), 'does not exist.') const compileOptions = { inputFile, diff --git a/src/cli/commands/help.js b/src/cli/commands/help.js index 321bca399745..3c63e884ea7a 100644 --- a/src/cli/commands/help.js +++ b/src/cli/commands/help.js @@ -1,8 +1,8 @@ -import chalk from 'chalk' import { forEach, map, padEnd } from 'lodash' import commands from '.' import * as constants from '../../constants' +import * as colors from '../colors' import * as utils from '../utils' export const usage = 'help [command]' @@ -18,11 +18,11 @@ export function forApp() { utils.log() utils.log('Usage:') - utils.log(' ', chalk.bold(constants.cli + ' [options]')) + utils.log(' ', colors.bold(constants.cli + ' [options]')) utils.log() utils.log('Commands:') forEach(commands, command => { - utils.log(' ', chalk.bold(padEnd(command.usage, pad)), command.description) + utils.log(' ', colors.bold(padEnd(command.usage, pad)), command.description) }) } @@ -34,10 +34,10 @@ export function forApp() { export function forCommand(command) { utils.log() utils.log('Usage:') - utils.log(' ', chalk.bold(constants.cli, command.usage)) + utils.log(' ', colors.bold(constants.cli, command.usage)) utils.log() utils.log('Description:') - utils.log(' ', chalk.bold(command.description)) + utils.log(' ', colors.bold(command.description)) if (command.options) { const pad = Math.max(...map(command.options, 'usage.length')) + PADDING_SIZE @@ -45,7 +45,7 @@ export function forCommand(command) { utils.log() utils.log('Options:') forEach(command.options, option => { - utils.log(' ', chalk.bold(padEnd(option.usage, pad)), option.description) + utils.log(' ', colors.bold(padEnd(option.usage, pad)), option.description) }) } } @@ -56,7 +56,7 @@ export function forCommand(command) { * @param {string} commandName */ export function invalidCommand(commandName) { - utils.error('Invalid command:', chalk.bold.magenta(commandName)) + utils.error('Invalid command:', colors.command(commandName)) forApp() utils.die() } diff --git a/src/cli/commands/index.js b/src/cli/commands/index.js index 180121917cac..fb97b993507c 100644 --- a/src/cli/commands/index.js +++ b/src/cli/commands/index.js @@ -1,5 +1,6 @@ import * as help from './help' import * as init from './init' import * as build from './build' +import * as update from './update' -export default { help, init, build } +export default { help, init, build, update } diff --git a/src/cli/commands/init.js b/src/cli/commands/init.js index 0b3b3458489d..0e3b8c659644 100644 --- a/src/cli/commands/init.js +++ b/src/cli/commands/init.js @@ -1,12 +1,11 @@ -import chalk from 'chalk' - import * as constants from '../../constants' +import * as colors from '../colors' import * as emoji from '../emoji' import * as utils from '../utils' export const usage = 'init [file]' export const description = - 'Creates Tailwind config file. Default: ' + chalk.bold.magenta(constants.defaultConfigFile) + 'Creates Tailwind config file. Default: ' + colors.file(constants.defaultConfigFile) export const options = [ { @@ -32,16 +31,16 @@ export function run(cliParams, cliOptions) { const full = cliOptions.full const file = cliParams[0] || constants.defaultConfigFile + const simplePath = utils.getSimplePath(file) - utils.exists(file) && utils.die(chalk.bold.magenta(file), 'already exists.') + utils.exists(file) && utils.die(colors.file(simplePath), 'already exists.') const stubFile = full ? constants.defaultConfigStubFile : constants.simpleConfigStubFile - const stub = utils.readFile(stubFile) - utils.writeFile(file, stub) + utils.copyFile(stubFile, file) utils.log() - utils.log(emoji.yes, 'Created Tailwind config file:', chalk.bold.magenta(file)) + utils.log(emoji.yes, 'Created Tailwind config file:', colors.file(simplePath)) utils.footer() diff --git a/src/cli/commands/update.js b/src/cli/commands/update.js new file mode 100644 index 000000000000..b9d190d156f3 --- /dev/null +++ b/src/cli/commands/update.js @@ -0,0 +1,121 @@ +import module from 'module' +import nodePath from 'path' + +import transform from './update/transform' +import * as validator from './update/validator' +import * as constants from '../../constants' +import * as colors from '../colors' +import * as emoji from '../emoji' +import * as utils from '../utils' +import oldDefaultConfig from '../../../stubs/oldDefaultConfig.stub.js' + +export const usage = 'update [source] [target]' +export const description = + 'Updates Tailwind configuration file. Default: ' + colors.file(constants.defaultConfigFile) + +/** + * Prints error messages and information about getting support to console + * + * @param {...string} msgs + */ +function dieWithSupport(...msgs) { + utils.error(...msgs) + utils.log() + utils.log(colors.bold('We were unable to automatically upgrade your configuration file.')) + utils.log(colors.bold('This could be the result of a non-standard format.')) + utils.log(colors.bold('We would love to learn more so we can improve this tool.')) + utils.log() + utils.log(colors.bold('Please open a ticket here:')) + utils.log(colors.info('https://github.com/tailwindcss/tailwindcss/issues/new')) + utils.die() +} + +/** + * Loads old configuration file by replacing expected dependencies with standins + * + * @param {string} file + * @return {object} + */ +function loadOldConfig(file) { + const originalRequire = module.prototype.require + + module.prototype.require = function(moduleName) { + switch (moduleName) { + case 'tailwindcss/defaultConfig': + return () => oldDefaultConfig + case 'tailwindcss/plugins/container': + return options => ({ plugin: 'container', options }) + default: + return originalRequire.apply(this, arguments) + } + } + + const ret = require(file) + + module.prototype.require = originalRequire + + return ret +} + +/** + * Formats the configuration object as importable Javascript + * + * @param {object} obj + * @return {string} + */ +function format(obj) { + return `module.exports = ${JSON.stringify(obj, null, 2)}` +} + +/** + * Runs the command. + * + * @param {string[]} cliParams + * @param {object} cliOptions + * @return {Promise} + */ +export function run(cliParams) { + return new Promise(resolve => { + utils.header() + + const inputFile = cliParams[0] || constants.oldDefaultConfigFile + const outputFile = cliParams[1] || constants.defaultConfigFile + const inputFileSimplePath = utils.getSimplePath(inputFile) + const outputFileSimplePath = utils.getSimplePath(outputFile) + + !utils.exists(inputFile) && utils.die(colors.file(inputFileSimplePath), 'does not exist.') + utils.exists(outputFile) && utils.die(colors.file(outputFileSimplePath), 'already exists.') + + let oldConfig = {} + let newConfig = {} + + try { + oldConfig = loadOldConfig(nodePath.resolve(inputFile)) + } catch (e) { + dieWithSupport('Unable to load:', colors.file(inputFileSimplePath)) + } + + const missingProperties = validator.getMissingRequiredProperties(oldConfig) + + if (missingProperties.length) { + utils.error(colors.file(inputFile), 'is missing these properties:') + missingProperties.forEach(property => utils.log(colors.info(`- ${property}`))) + utils.die() + } + + try { + newConfig = transform(oldConfig) + } catch (e) { + dieWithSupport('Unable to update:', colors.file(inputFileSimplePath)) + } + + utils.writeFile(outputFile, format(newConfig)) + + utils.log() + utils.log(emoji.yes, 'Created Tailwind config file:', colors.file(outputFileSimplePath)) + + utils.footer() + + resolve() + }) +} diff --git a/src/cli/commands/update/transform.js b/src/cli/commands/update/transform.js new file mode 100644 index 000000000000..1d4cdae4d739 --- /dev/null +++ b/src/cli/commands/update/transform.js @@ -0,0 +1,76 @@ +import { cloneDeep, get, isFunction } from 'lodash' + +import defaultConfig from '../../../../stubs/defaultConfig.stub.js' + +const keyMap = { + backgroundColor: 'backgroundColors', + borderColor: 'borderColors', + borderWidth: 'borderWidths', + flexDirection: 'flex', + flexWrap: 'flex', + alignItems: 'flex', + alignSelf: 'flex', + justifyContent: 'flex', + alignContent: 'flex', + flexGrow: 'flex', + flexShrink: 'flex', + fontFamily: 'fonts', + lineHeight: 'leading', + listStylePosition: 'lists', + listStyleType: 'lists', + boxShadow: 'shadows', + fill: 'svgFill', + stroke: 'svgStroke', + textColor: 'textColors', + fontSize: 'textSizes', + fontStyle: 'textStyle', + textTransform: 'textStyle', + textDecoration: 'textStyle', + fontSmoothing: 'textStyle', + letterSpacing: 'tracking', + wordBreak: 'whitespace', +} + +/** + * Checks if the provided object is a container plugin standin + * + * @param {object} obj + * @return {boolean} + */ +function isContainerPlugin(obj) { + return get(obj, 'plugin') === 'container' +} + +/** + * Transforms old configuration format to new configuration format + * + * @param {object} oldConfig + * @return {object} + */ +export default function(oldConfig) { + const newConfig = cloneDeep(defaultConfig) + + // Theme + Object.keys(newConfig.theme).forEach(key => { + newConfig.theme[key] = get(oldConfig, get(keyMap, key, key), defaultConfig.theme[key]) + }) + + // Variants + Object.keys(newConfig.variants).forEach(key => { + const value = get(oldConfig.modules, get(keyMap, key, key), defaultConfig.variants[key]) + value ? (newConfig.variants[key] = value) : (newConfig.corePlugins[key] = false) + }) + + // Options and plugins + Object.assign(newConfig, { + ...oldConfig.options, + plugins: oldConfig.plugins.filter(isFunction), + }) + + // Container plugin + const containerPlugin = oldConfig.plugins.find(isContainerPlugin) + !containerPlugin && (newConfig.corePlugins.container = false) + newConfig.theme.container = get(containerPlugin, 'options', {}) + + return newConfig +} diff --git a/src/cli/commands/update/validator.js b/src/cli/commands/update/validator.js new file mode 100644 index 000000000000..19087e3b5309 --- /dev/null +++ b/src/cli/commands/update/validator.js @@ -0,0 +1,96 @@ +import { has } from 'lodash' + +const paths = [ + 'options.prefix', + 'options.important', + 'options.separator', + 'colors', + 'screens', + 'fonts', + 'textSizes', + 'fontWeights', + 'leading', + 'tracking', + 'textColors', + 'backgroundColors', + 'backgroundPosition', + 'backgroundSize', + 'borderWidths', + 'borderColors', + 'borderRadius', + 'width', + 'height', + 'minWidth', + 'minHeight', + 'maxWidth', + 'maxHeight', + 'padding', + 'margin', + 'negativeMargin', + 'shadows', + 'zIndex', + 'opacity', + 'svgFill', + 'svgStroke', + 'modules.appearance', + 'modules.backgroundAttachment', + 'modules.backgroundColors', + 'modules.backgroundPosition', + 'modules.backgroundRepeat', + 'modules.backgroundSize', + 'modules.borderCollapse', + 'modules.borderColors', + 'modules.borderRadius', + 'modules.borderStyle', + 'modules.borderWidths', + 'modules.cursor', + 'modules.display', + 'modules.flexbox', + 'modules.float', + 'modules.fonts', + 'modules.fontWeights', + 'modules.height', + 'modules.leading', + 'modules.lists', + 'modules.margin', + 'modules.maxHeight', + 'modules.maxWidth', + 'modules.minHeight', + 'modules.minWidth', + 'modules.negativeMargin', + 'modules.objectFit', + 'modules.objectPosition', + 'modules.opacity', + 'modules.outline', + 'modules.overflow', + 'modules.padding', + 'modules.pointerEvents', + 'modules.position', + 'modules.resize', + 'modules.shadows', + 'modules.svgFill', + 'modules.svgStroke', + 'modules.tableLayout', + 'modules.textAlign', + 'modules.textColors', + 'modules.textSizes', + 'modules.textStyle', + 'modules.tracking', + 'modules.userSelect', + 'modules.verticalAlign', + 'modules.visibility', + 'modules.whitespace', + 'modules.width', + 'modules.zIndex', + 'plugins', +] + +/** + * Gets a list of paths that do not exist in the provided object + * + * @param {object} config + * @return {string[]} + */ +export function getMissingRequiredProperties(config) { + return paths.filter(path => !has(config, path)) +} diff --git a/src/cli/utils.js b/src/cli/utils.js index ac138b854c25..0224a7b1b525 100644 --- a/src/cli/utils.js +++ b/src/cli/utils.js @@ -1,7 +1,7 @@ -import chalk from 'chalk' -import { ensureFileSync, existsSync, outputFileSync, readFileSync } from 'fs-extra' -import { findKey, mapValues, trimStart } from 'lodash' +import { copyFileSync, ensureFileSync, existsSync, outputFileSync, readFileSync } from 'fs-extra' +import { findKey, mapValues, startsWith, trimStart } from 'lodash' +import * as colors from './colors' import * as emoji from './emoji' import packageJson from '../../package.json' @@ -58,7 +58,7 @@ export function log(...msgs) { */ export function header() { log() - log(chalk.bold(packageJson.name), chalk.bold.cyan(packageJson.version)) + log(colors.bold(packageJson.name), colors.info(packageJson.version)) } /** @@ -75,7 +75,7 @@ export function footer() { */ export function error(...msgs) { log() - console.error(' ', emoji.no, chalk.bold.red(msgs.join(' '))) + console.error(' ', emoji.no, colors.error(msgs.join(' '))) } /** @@ -99,6 +99,16 @@ export function exists(path) { return existsSync(path) } +/** + * Copies file source to destination. + * + * @param {string} source + * @param {string} destination + */ +export function copyFile(source, destination) { + copyFileSync(source, destination) +} + /** * Gets file content. * @@ -121,3 +131,13 @@ export function writeFile(path, content) { return outputFileSync(path, content) } + +/** + * Strips leading ./ from path + * + * @param {string} path + * @return {string} + */ +export function getSimplePath(path) { + return startsWith(path, './') ? path.slice(2) : path +} diff --git a/src/constants.js b/src/constants.js index 260a5b66d47f..63e3a3e1b112 100644 --- a/src/constants.js +++ b/src/constants.js @@ -4,3 +4,7 @@ export const cli = 'tailwind' export const defaultConfigFile = './tailwind.config.js' export const defaultConfigStubFile = path.resolve(__dirname, '../stubs/defaultConfig.stub.js') export const simpleConfigStubFile = path.resolve(__dirname, '../stubs/simpleConfig.stub.js') + +// These are used by the update process +export const oldDefaultConfigFile = './tailwind.js' +export const oldDefaultConfigStubFile = path.resolve(__dirname, '../stubs/oldDefaultConfig.stub.js') diff --git a/stubs/defaultConfig.stub.js b/stubs/defaultConfig.stub.js index fd96ee39e377..93295d9d3b68 100644 --- a/stubs/defaultConfig.stub.js +++ b/stubs/defaultConfig.stub.js @@ -373,6 +373,7 @@ module.exports = { '0': 0, auto: 'auto', }, + container: {}, }, variants: { appearance: ['responsive'], diff --git a/stubs/oldDefaultConfig.stub.js b/stubs/oldDefaultConfig.stub.js new file mode 100644 index 000000000000..c2b0d039aae6 --- /dev/null +++ b/stubs/oldDefaultConfig.stub.js @@ -0,0 +1,413 @@ +let colors = { + 'transparent': 'transparent', + + 'black': '#22292f', + 'grey-darkest': '#3d4852', + 'grey-darker': '#606f7b', + 'grey-dark': '#8795a1', + 'grey': '#b8c2cc', + 'grey-light': '#dae1e7', + 'grey-lighter': '#f1f5f8', + 'grey-lightest': '#f8fafc', + 'white': '#ffffff', + + 'red-darkest': '#3b0d0c', + 'red-darker': '#621b18', + 'red-dark': '#cc1f1a', + 'red': '#e3342f', + 'red-light': '#ef5753', + 'red-lighter': '#f9acaa', + 'red-lightest': '#fcebea', + + 'orange-darkest': '#462a16', + 'orange-darker': '#613b1f', + 'orange-dark': '#de751f', + 'orange': '#f6993f', + 'orange-light': '#faad63', + 'orange-lighter': '#fcd9b6', + 'orange-lightest': '#fff5eb', + + 'yellow-darkest': '#453411', + 'yellow-darker': '#684f1d', + 'yellow-dark': '#f2d024', + 'yellow': '#ffed4a', + 'yellow-light': '#fff382', + 'yellow-lighter': '#fff9c2', + 'yellow-lightest': '#fcfbeb', + + 'green-darkest': '#0f2f21', + 'green-darker': '#1a4731', + 'green-dark': '#1f9d55', + 'green': '#38c172', + 'green-light': '#51d88a', + 'green-lighter': '#a2f5bf', + 'green-lightest': '#e3fcec', + + 'teal-darkest': '#0d3331', + 'teal-darker': '#20504f', + 'teal-dark': '#38a89d', + 'teal': '#4dc0b5', + 'teal-light': '#64d5ca', + 'teal-lighter': '#a0f0ed', + 'teal-lightest': '#e8fffe', + + 'blue-darkest': '#12283a', + 'blue-darker': '#1c3d5a', + 'blue-dark': '#2779bd', + 'blue': '#3490dc', + 'blue-light': '#6cb2eb', + 'blue-lighter': '#bcdefa', + 'blue-lightest': '#eff8ff', + + 'indigo-darkest': '#191e38', + 'indigo-darker': '#2f365f', + 'indigo-dark': '#5661b3', + 'indigo': '#6574cd', + 'indigo-light': '#7886d7', + 'indigo-lighter': '#b2b7ff', + 'indigo-lightest': '#e6e8ff', + + 'purple-darkest': '#21183c', + 'purple-darker': '#382b5f', + 'purple-dark': '#794acf', + 'purple': '#9561e2', + 'purple-light': '#a779e9', + 'purple-lighter': '#d6bbfc', + 'purple-lightest': '#f3ebff', + + 'pink-darkest': '#451225', + 'pink-darker': '#6f213f', + 'pink-dark': '#eb5286', + 'pink': '#f66d9b', + 'pink-light': '#fa7ea8', + 'pink-lighter': '#ffbbca', + 'pink-lightest': '#ffebef', +} + +module.exports = { + options: { + prefix: '', + important: false, + separator: ':', + }, + colors: colors, + screens: { + 'sm': '576px', + 'md': '768px', + 'lg': '992px', + 'xl': '1200px', + }, + fonts: { + 'sans': [ + 'system-ui', + 'BlinkMacSystemFont', + '-apple-system', + 'Segoe UI', + 'Roboto', + 'Oxygen', + 'Ubuntu', + 'Cantarell', + 'Fira Sans', + 'Droid Sans', + 'Helvetica Neue', + 'sans-serif', + ], + 'serif': [ + 'Constantia', + 'Lucida Bright', + 'Lucidabright', + 'Lucida Serif', + 'Lucida', + 'DejaVu Serif', + 'Bitstream Vera Serif', + 'Liberation Serif', + 'Georgia', + 'serif', + ], + 'mono': [ + 'Menlo', + 'Monaco', + 'Consolas', + 'Liberation Mono', + 'Courier New', + 'monospace', + ], + }, + textSizes: { + 'xs': '.75rem', // 12px + 'sm': '.875rem', // 14px + 'base': '1rem', // 16px + 'lg': '1.125rem', // 18px + 'xl': '1.25rem', // 20px + '2xl': '1.5rem', // 24px + '3xl': '1.875rem', // 30px + '4xl': '2.25rem', // 36px + '5xl': '3rem', // 48px + }, + fontWeights: { + 'hairline': 100, + 'thin': 200, + 'light': 300, + 'normal': 400, + 'medium': 500, + 'semibold': 600, + 'bold': 700, + 'extrabold': 800, + 'black': 900, + }, + leading: { + 'none': 1, + 'tight': 1.25, + 'normal': 1.5, + 'loose': 2, + }, + tracking: { + 'tight': '-0.05em', + 'normal': '0', + 'wide': '0.05em', + }, + textColors: colors, + backgroundColors: colors, + backgroundPosition: { + 'bottom': 'bottom', + 'center': 'center', + 'left': 'left', + 'left-bottom': 'left bottom', + 'left-top': 'left top', + 'right': 'right', + 'right-bottom': 'right bottom', + 'right-top': 'right top', + 'top': 'top', + }, + backgroundSize: { + 'auto': 'auto', + 'cover': 'cover', + 'contain': 'contain', + }, + borderWidths: { + default: '1px', + '0': '0', + '2': '2px', + '4': '4px', + '8': '8px', + }, + borderColors: global.Object.assign({ default: colors['grey-light'] }, colors), + borderRadius: { + 'none': '0', + 'sm': '.125rem', + default: '.25rem', + 'lg': '.5rem', + 'full': '9999px', + }, + width: { + 'auto': 'auto', + 'px': '1px', + '1': '0.25rem', + '2': '0.5rem', + '3': '0.75rem', + '4': '1rem', + '5': '1.25rem', + '6': '1.5rem', + '8': '2rem', + '10': '2.5rem', + '12': '3rem', + '16': '4rem', + '24': '6rem', + '32': '8rem', + '48': '12rem', + '64': '16rem', + '1/2': '50%', + '1/3': '33.33333%', + '2/3': '66.66667%', + '1/4': '25%', + '3/4': '75%', + '1/5': '20%', + '2/5': '40%', + '3/5': '60%', + '4/5': '80%', + '1/6': '16.66667%', + '5/6': '83.33333%', + 'full': '100%', + 'screen': '100vw', + }, + height: { + 'auto': 'auto', + 'px': '1px', + '1': '0.25rem', + '2': '0.5rem', + '3': '0.75rem', + '4': '1rem', + '5': '1.25rem', + '6': '1.5rem', + '8': '2rem', + '10': '2.5rem', + '12': '3rem', + '16': '4rem', + '24': '6rem', + '32': '8rem', + '48': '12rem', + '64': '16rem', + 'full': '100%', + 'screen': '100vh', + }, + minWidth: { + '0': '0', + 'full': '100%', + }, + minHeight: { + '0': '0', + 'full': '100%', + 'screen': '100vh', + }, + maxWidth: { + 'xs': '20rem', + 'sm': '30rem', + 'md': '40rem', + 'lg': '50rem', + 'xl': '60rem', + '2xl': '70rem', + '3xl': '80rem', + '4xl': '90rem', + '5xl': '100rem', + 'full': '100%', + }, + maxHeight: { + 'full': '100%', + 'screen': '100vh', + }, + padding: { + 'px': '1px', + '0': '0', + '1': '0.25rem', + '2': '0.5rem', + '3': '0.75rem', + '4': '1rem', + '5': '1.25rem', + '6': '1.5rem', + '8': '2rem', + '10': '2.5rem', + '12': '3rem', + '16': '4rem', + '20': '5rem', + '24': '6rem', + '32': '8rem', + }, + margin: { + 'auto': 'auto', + 'px': '1px', + '0': '0', + '1': '0.25rem', + '2': '0.5rem', + '3': '0.75rem', + '4': '1rem', + '5': '1.25rem', + '6': '1.5rem', + '8': '2rem', + '10': '2.5rem', + '12': '3rem', + '16': '4rem', + '20': '5rem', + '24': '6rem', + '32': '8rem', + }, + negativeMargin: { + 'px': '1px', + '0': '0', + '1': '0.25rem', + '2': '0.5rem', + '3': '0.75rem', + '4': '1rem', + '5': '1.25rem', + '6': '1.5rem', + '8': '2rem', + '10': '2.5rem', + '12': '3rem', + '16': '4rem', + '20': '5rem', + '24': '6rem', + '32': '8rem', + }, + shadows: { + default: '0 2px 4px 0 rgba(0,0,0,0.10)', + 'md': '0 4px 8px 0 rgba(0,0,0,0.12), 0 2px 4px 0 rgba(0,0,0,0.08)', + 'lg': '0 15px 30px 0 rgba(0,0,0,0.11), 0 5px 15px 0 rgba(0,0,0,0.08)', + 'inner': 'inset 0 2px 4px 0 rgba(0,0,0,0.06)', + 'outline': '0 0 0 3px rgba(52,144,220,0.5)', + 'none': 'none', + }, + zIndex: { + 'auto': 'auto', + '0': 0, + '10': 10, + '20': 20, + '30': 30, + '40': 40, + '50': 50, + }, + opacity: { + '0': '0', + '25': '.25', + '50': '.5', + '75': '.75', + '100': '1', + }, + svgFill: { + 'current': 'currentColor', + }, + svgStroke: { + 'current': 'currentColor', + }, + modules: { + appearance: ['responsive'], + backgroundAttachment: ['responsive'], + backgroundColors: ['responsive', 'hover', 'focus'], + backgroundPosition: ['responsive'], + backgroundRepeat: ['responsive'], + backgroundSize: ['responsive'], + borderCollapse: [], + borderColors: ['responsive', 'hover', 'focus'], + borderRadius: ['responsive'], + borderStyle: ['responsive'], + borderWidths: ['responsive'], + cursor: ['responsive'], + display: ['responsive'], + flexbox: ['responsive'], + float: ['responsive'], + fonts: ['responsive'], + fontWeights: ['responsive', 'hover', 'focus'], + height: ['responsive'], + leading: ['responsive'], + lists: ['responsive'], + margin: ['responsive'], + maxHeight: ['responsive'], + maxWidth: ['responsive'], + minHeight: ['responsive'], + minWidth: ['responsive'], + negativeMargin: ['responsive'], + objectFit: false, + objectPosition: false, + opacity: ['responsive'], + outline: ['focus'], + overflow: ['responsive'], + padding: ['responsive'], + pointerEvents: ['responsive'], + position: ['responsive'], + resize: ['responsive'], + shadows: ['responsive', 'hover', 'focus'], + svgFill: [], + svgStroke: [], + tableLayout: ['responsive'], + textAlign: ['responsive'], + textColors: ['responsive', 'hover', 'focus'], + textSizes: ['responsive'], + textStyle: ['responsive', 'hover', 'focus'], + tracking: ['responsive'], + userSelect: ['responsive'], + verticalAlign: ['responsive'], + visibility: ['responsive'], + whitespace: ['responsive'], + width: ['responsive'], + zIndex: ['responsive'], + }, + plugins: [], +} From fa2b450170591d1074b5f5e75bb055ab441f35cf Mon Sep 17 00:00:00 2001 From: Matt Stypa Date: Mon, 18 Mar 2019 21:31:41 -0500 Subject: [PATCH 2/4] Updated description of CLI commands to look nicer --- src/cli/commands/init.js | 3 ++- src/cli/commands/update.js | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/cli/commands/init.js b/src/cli/commands/init.js index 0e3b8c659644..f13160d821fd 100644 --- a/src/cli/commands/init.js +++ b/src/cli/commands/init.js @@ -5,7 +5,8 @@ import * as utils from '../utils' export const usage = 'init [file]' export const description = - 'Creates Tailwind config file. Default: ' + colors.file(constants.defaultConfigFile) + 'Creates Tailwind config file. Default: ' + + colors.file(utils.getSimplePath(constants.defaultConfigFile)) export const options = [ { diff --git a/src/cli/commands/update.js b/src/cli/commands/update.js index b9d190d156f3..f14609fee705 100644 --- a/src/cli/commands/update.js +++ b/src/cli/commands/update.js @@ -11,7 +11,8 @@ import oldDefaultConfig from '../../../stubs/oldDefaultConfig.stub.js' export const usage = 'update [source] [target]' export const description = - 'Updates Tailwind configuration file. Default: ' + colors.file(constants.defaultConfigFile) + 'Updates Tailwind configuration file. Default: ' + + colors.file(utils.getSimplePath(constants.defaultConfigFile)) /** * Prints error messages and information about getting support to console From 35d4f2e94c0c898871abe5ce3dac7fe5e64a2beb Mon Sep 17 00:00:00 2001 From: Matt Stypa Date: Tue, 19 Mar 2019 10:55:45 -0500 Subject: [PATCH 3/4] Removed backgroundPosition from CLI update validator --- src/cli/commands/update/validator.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/cli/commands/update/validator.js b/src/cli/commands/update/validator.js index 19087e3b5309..a3da7b059c8f 100644 --- a/src/cli/commands/update/validator.js +++ b/src/cli/commands/update/validator.js @@ -13,7 +13,6 @@ const paths = [ 'tracking', 'textColors', 'backgroundColors', - 'backgroundPosition', 'backgroundSize', 'borderWidths', 'borderColors', From 70f6059a0569e0e40ea232ab0432e0431f4c3a69 Mon Sep 17 00:00:00 2001 From: Matt Stypa Date: Tue, 19 Mar 2019 11:13:37 -0500 Subject: [PATCH 4/4] Relaxed CLI update validation for modules --- src/cli/commands/update/validator.js | 57 ++-------------------------- 1 file changed, 4 insertions(+), 53 deletions(-) diff --git a/src/cli/commands/update/validator.js b/src/cli/commands/update/validator.js index a3da7b059c8f..68831f1ecb0f 100644 --- a/src/cli/commands/update/validator.js +++ b/src/cli/commands/update/validator.js @@ -1,9 +1,6 @@ import { has } from 'lodash' const paths = [ - 'options.prefix', - 'options.important', - 'options.separator', 'colors', 'screens', 'fonts', @@ -31,56 +28,10 @@ const paths = [ 'opacity', 'svgFill', 'svgStroke', - 'modules.appearance', - 'modules.backgroundAttachment', - 'modules.backgroundColors', - 'modules.backgroundPosition', - 'modules.backgroundRepeat', - 'modules.backgroundSize', - 'modules.borderCollapse', - 'modules.borderColors', - 'modules.borderRadius', - 'modules.borderStyle', - 'modules.borderWidths', - 'modules.cursor', - 'modules.display', - 'modules.flexbox', - 'modules.float', - 'modules.fonts', - 'modules.fontWeights', - 'modules.height', - 'modules.leading', - 'modules.lists', - 'modules.margin', - 'modules.maxHeight', - 'modules.maxWidth', - 'modules.minHeight', - 'modules.minWidth', - 'modules.negativeMargin', - 'modules.objectFit', - 'modules.objectPosition', - 'modules.opacity', - 'modules.outline', - 'modules.overflow', - 'modules.padding', - 'modules.pointerEvents', - 'modules.position', - 'modules.resize', - 'modules.shadows', - 'modules.svgFill', - 'modules.svgStroke', - 'modules.tableLayout', - 'modules.textAlign', - 'modules.textColors', - 'modules.textSizes', - 'modules.textStyle', - 'modules.tracking', - 'modules.userSelect', - 'modules.verticalAlign', - 'modules.visibility', - 'modules.whitespace', - 'modules.width', - 'modules.zIndex', + 'options.prefix', + 'options.important', + 'options.separator', + 'modules', 'plugins', ]