Implement cosmiconfig for workspace configuration (#2434)

* Implement cosmiconfig

* Add resolveOptions API and extname support

* Add --resolve-config and --config, rename resolveOptions to resolveConfig

* Move color to top-level CLI options

* Fix unknown param warning

* Change from {} to null when no config is found

* Change override API to emulate eslint

* Add test for eslint-style overrides

* Delete overrides from resolveConfig
master
Lucas Azzola 2017-07-10 22:26:36 +10:00 committed by GitHub
parent 3728749d92
commit dcccfed366
21 changed files with 525 additions and 64 deletions

View File

@ -2,24 +2,41 @@
"use strict";
const chalk = require("chalk");
const dashify = require("dashify");
const fs = require("fs");
const getStream = require("get-stream");
const globby = require("globby");
const chalk = require("chalk");
const minimist = require("minimist");
const path = require("path");
const readline = require("readline");
const prettier = eval("require")("../index");
const cleanAST = require("../src/clean-ast.js").cleanAST;
const argv = minimist(process.argv.slice(2), {
const prettier = eval("require")("../index");
const cleanAST = require("../src/clean-ast").cleanAST;
const resolver = require("../src/resolve-config");
const args = process.argv.slice(2);
const booleanOptionNames = [
"use-tabs",
"semi",
"single-quote",
"bracket-spacing",
"jsx-bracket-same-line",
// Deprecated in 0.0.10
"flow-parser"
];
const stringOptionNames = [
"print-width",
"tab-width",
"parser",
"trailing-comma"
];
const argv = minimist(args, {
boolean: [
"write",
"stdin",
"use-tabs",
"semi",
"single-quote",
"bracket-spacing",
"jsx-bracket-same-line",
// The supports-color package (a sub sub dependency) looks directly at
// `process.argv` for `--no-color` and such-like options. The reason it is
// listed here is to avoid "Ignored unknown option: --no-color" warnings.
@ -30,31 +47,34 @@ const argv = minimist(process.argv.slice(2), {
"version",
"debug-print-doc",
"debug-check",
"with-node-modules",
// Deprecated in 0.0.10
"flow-parser"
"with-node-modules"
],
string: [
"print-width",
"tab-width",
"parser",
"trailing-comma",
"cursor-offset",
"range-start",
"range-end",
"stdin-filepath"
"stdin-filepath",
"config",
"resolve-config"
],
default: {
semi: true,
color: true,
"bracket-spacing": true,
parser: "babylon"
color: true
},
alias: {
help: "h",
version: "v",
"list-different": "l"
},
alias: { help: "h", version: "v", "list-different": "l" },
unknown: param => {
if (param.startsWith("-")) {
console.warn("Ignored unknown option: " + param + "\n");
return false;
const paramName = param.substring(2);
if (
booleanOptionNames.indexOf(paramName) === -1 &&
stringOptionNames.indexOf(paramName) === -1
) {
console.warn("Ignored unknown option: " + param + "\n");
return false;
}
}
}
});
@ -78,9 +98,56 @@ if (write && argv["debug-check"]) {
process.exit(1);
}
function getParserOption() {
const optionName = "parser";
const value = argv[optionName];
if (argv["resolve-config"] && filepatterns.length) {
console.error("Cannot use --resolve-config with multiple files");
process.exit(1);
}
function getOptionsForFile(filePath) {
return resolver
.resolveConfig(filePath, { configFile: argv["config"] })
.then(options => {
const parsedArgs = minimist(args, {
boolean: booleanOptionNames,
string: stringOptionNames,
default: Object.assign(
{
semi: true,
"bracket-spacing": true,
parser: "babylon"
},
dashifyObject(options)
)
});
return getOptions(Object.assign({}, argv, parsedArgs));
})
.catch(error => {
console.error("Invalid configuration file:", error.toString());
process.exit(2);
});
}
function getOptions(argv) {
return {
cursorOffset: getIntOption(argv, "cursor-offset"),
rangeStart: getIntOption(argv, "range-start"),
rangeEnd: getIntOption(argv, "range-end"),
useTabs: argv["use-tabs"],
semi: argv["semi"],
printWidth: getIntOption(argv, "print-width"),
tabWidth: getIntOption(argv, "tab-width"),
bracketSpacing: argv["bracket-spacing"],
singleQuote: argv["single-quote"],
jsxBracketSameLine: argv["jsx-bracket-same-line"],
filepath: argv["stdin-filepath"],
trailingComma: getTrailingComma(argv),
parser: getParserOption(argv)
};
}
function getParserOption(argv) {
const value = argv.parser;
if (value === undefined) {
return value;
@ -95,7 +162,7 @@ function getParserOption() {
return value;
}
function getIntOption(optionName) {
function getIntOption(argv, optionName) {
const value = argv[optionName];
if (value === undefined) {
@ -115,7 +182,7 @@ function getIntOption(optionName) {
process.exit(1);
}
function getTrailingComma() {
function getTrailingComma(argv) {
switch (argv["trailing-comma"]) {
case undefined:
case "none":
@ -135,21 +202,12 @@ function getTrailingComma() {
}
}
const options = {
cursorOffset: getIntOption("cursor-offset"),
rangeStart: getIntOption("range-start"),
rangeEnd: getIntOption("range-end"),
useTabs: argv["use-tabs"],
semi: argv["semi"],
printWidth: getIntOption("print-width"),
tabWidth: getIntOption("tab-width"),
bracketSpacing: argv["bracket-spacing"],
singleQuote: argv["single-quote"],
jsxBracketSameLine: argv["jsx-bracket-same-line"],
filepath: argv["stdin-filepath"],
trailingComma: getTrailingComma(),
parser: getParserOption()
};
function dashifyObject(object) {
return Object.keys(object || {}).reduce((output, key) => {
output[dashify(key)] = object[key];
return output;
}, {});
}
function diff(a, b) {
return require("diff").createTwoFilesPatch("", "", a, b, "", "", {
@ -214,12 +272,17 @@ function handleError(filename, e) {
process.exitCode = 2;
}
if (argv["help"] || (!filepatterns.length && !stdin)) {
if (
argv["help"] ||
(!filepatterns.length && !stdin && !argv["resolve-config"])
) {
console.log(
"Usage: prettier [opts] [filename ...]\n\n" +
"Available options:\n" +
" --write Edit the file in-place. (Beware!)\n" +
" --list-different or -l Print filenames of files that are different from Prettier formatting.\n" +
" --config Path to a prettier configuration file (.prettierrc, package.json, prettier.config.js).\n" +
" --resolve-config <path> Resolve the path to a configuration file for a given input file.\n" +
" --stdin Read input from stdin.\n" +
" --stdin-filepath Path to the file used to read from stdin.\n" +
" --print-width <int> Specify the length of line that the printer will wrap on. Defaults to 80.\n" +
@ -251,20 +314,24 @@ if (argv["help"] || (!filepatterns.length && !stdin)) {
process.exit(argv["help"] ? 0 : 1);
}
if (stdin) {
if (argv["resolve-config"]) {
resolveConfig(argv["resolve-config"]);
} else if (stdin) {
getStream(process.stdin).then(input => {
if (listDifferent(input, options, "(stdin)")) {
return;
}
getOptionsForFile(process.cwd()).then(options => {
if (listDifferent(input, options, "(stdin)")) {
return;
}
try {
writeOutput(format(input, options));
} catch (e) {
handleError("stdin", e);
}
try {
writeOutput(format(input, options), options);
} catch (e) {
handleError("stdin", e);
}
});
});
} else {
eachFilename(filepatterns, filename => {
eachFilename(filepatterns, (filename, options) => {
if (write) {
// Don't use `console.log` here since we need to replace this line.
process.stdout.write(filename);
@ -337,7 +404,7 @@ if (stdin) {
process.exitCode = 2;
}
} else if (!argv["list-different"]) {
writeOutput(result);
writeOutput(result, options);
}
});
}
@ -359,7 +426,17 @@ function listDifferent(input, options, filename) {
return true;
}
function writeOutput(result) {
function resolveConfig(filePath) {
resolver.resolveConfigFile(filePath).then(configFile => {
if (configFile) {
console.log(path.relative(process.cwd(), configFile));
} else {
process.exitCode = 1;
}
});
}
function writeOutput(result, options) {
// Don't use `console.log` here since it adds an extra newline at the end.
process.stdout.write(result.formatted);
@ -382,9 +459,11 @@ function eachFilename(patterns, callback) {
process.exitCode = 2;
return;
}
filePaths.forEach(filePath => {
return callback(filePath);
// Use map series to ensure idempotency
mapSeries(filePaths, filePath => {
return getOptionsForFile(filePath).then(options =>
callback(filePath, options)
);
});
})
.catch(err => {
@ -395,3 +474,16 @@ function eachFilename(patterns, callback) {
process.exitCode = 2;
});
}
function mapSeries(array, iteratee) {
let current = Promise.resolve();
const promises = array.map((item, i) => {
current = current.then(() => {
return iteratee(item, i, array);
});
return current;
});
return Promise.all(promises);
}

View File

@ -9,6 +9,7 @@ const printDocToString = require("./src/doc-printer").printDocToString;
const normalizeOptions = require("./src/options").normalize;
const parser = require("./src/parser");
const printDocToDebug = require("./src/doc-debug").printDocToDebug;
const resolveConfig = require("./src/resolve-config").resolveConfig;
function guessLineEnding(text) {
const index = text.indexOf("\n");
@ -323,9 +324,11 @@ module.exports = {
formatWithCursor: function(text, opts) {
return formatWithCursor(text, normalizeOptions(opts));
},
format: function(text, opts) {
return format(text, normalizeOptions(opts));
},
check: function(text, opts) {
try {
const formatted = format(text, normalizeOptions(opts));
@ -334,7 +337,11 @@ module.exports = {
return false;
}
},
version: version,
resolveConfig,
version,
__debug: {
parse: function(text, opts) {
return parser.parse(text, opts);

View File

@ -14,6 +14,8 @@
"babel-code-frame": "7.0.0-alpha.12",
"babylon": "7.0.0-beta.13",
"chalk": "2.0.1",
"cosmiconfig": "2.1.3",
"dashify": "0.2.2",
"diff": "3.2.0",
"esutils": "2.0.2",
"flow-parser": "0.47.0",
@ -22,6 +24,7 @@
"graphql": "0.10.1",
"jest-validate": "20.0.3",
"json-to-ast": "2.0.0-alpha1.2",
"minimatch": "3.0.4",
"minimist": "1.2.0",
"parse5": "3.0.2",
"postcss": "^6.0.1",

68
src/resolve-config.js Normal file
View File

@ -0,0 +1,68 @@
"use strict";
const cosmiconfig = require("cosmiconfig");
const minimatch = require("minimatch");
const path = require("path");
const withCache = cosmiconfig("prettier");
const noCache = cosmiconfig("prettier", { cache: false });
function resolveConfig(filePath, opts) {
const useCache = !(opts && opts.useCache === false);
const fileDir = filePath ? path.dirname(filePath) : undefined;
return (
(useCache ? withCache : noCache)
// https://github.com/davidtheclark/cosmiconfig/pull/68
.load(fileDir, opts && opts.configFile)
.then(result => {
if (!result) {
return null;
}
return mergeOverrides(result.config, filePath);
})
);
}
function resolveConfigFile(filePath) {
return noCache.load(filePath).then(result => {
if (result) {
return result.filepath;
}
return null;
});
}
function mergeOverrides(config, filePath) {
const options = Object.assign({}, config);
if (filePath && options.overrides) {
for (const override of options.overrides) {
if (pathMatchesGlobs(filePath, override.files, override.excludeFiles)) {
Object.assign(options, override.options);
}
}
}
delete options.overrides;
return options;
}
// Based on eslint: https://github.com/eslint/eslint/blob/master/lib/config/config-ops.js
function pathMatchesGlobs(filePath, patterns, excludedPatterns) {
const patternList = [].concat(patterns);
const excludedPatternList = [].concat(excludedPatterns || []);
const opts = { matchBase: true };
return (
patternList.some(pattern => minimatch(filePath, pattern, opts)) &&
!excludedPatternList.some(excludedPattern =>
minimatch(filePath, excludedPattern, opts)
)
);
}
module.exports = {
resolveConfig,
resolveConfigFile
};

View File

@ -0,0 +1,118 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`CLI overrides take precedence 1`] = `
"/* eslint-disable */
console.log(
\\"should have semi\\"
);
/* eslint-disable */
console.log(
\\"should not have semi\\"
)
/* eslint-disable */
console.log(
\\"should have semi\\"
);
/* eslint-disable */
function f() {
console.log(
\\"should have tab width 8\\"
);
}
\\"use strict\\";
module.exports = {
tabWidth: 8
};
/* eslint-disable */
function f() {
console.log(
\\"should have no semicolons\\"
)
}
/* eslint-disable */
function f() {
console.log(
\\"should have tab width 3\\"
);
}
/* eslint-disable */
function f() {
console.log.apply(
null,
[
'this file',
'should have trailing comma',
'and single quotes',
],
);
}
"
`;
exports[`accepts configuration from --config 1`] = `
"/* eslint-disable */
function f() {
console.log(\\"should have tab width 8\\")
}
"
`;
exports[`resolves configuration file with --resolve-config file 1`] = `
".prettierrc
"
`;
exports[`resolves configuration from external files 1`] = `
"/* eslint-disable */
console.log(\\"should have semi\\");
/* eslint-disable */
console.log(\\"should not have semi\\")
/* eslint-disable */
console.log(\\"should have semi\\");
/* eslint-disable */
function f() {
console.log(\\"should have tab width 8\\");
}
\\"use strict\\";
module.exports = {
tabWidth: 8
};
/* eslint-disable */
function f() {
console.log(\\"should have no semicolons\\")
}
/* eslint-disable */
function f() {
console.log(\\"should have tab width 3\\");
}
/* eslint-disable */
function f() {
console.log.apply(null, [
'this file',
'should have trailing comma',
'and single quotes',
]);
}
"
`;
exports[`resolves configuration from external files and overrides by extname 1`] = `
"function g() {
console.log(\\"should have semicolons because it has a .ts extension\\");
}
function g() {
console.log(\\"should have tab width 5 because it has .ts extension\\");
}
"
`;

View File

@ -0,0 +1,75 @@
"use strict";
const path = require("path");
const runPrettier = require("../runPrettier");
const prettier = require("../../");
test("resolves configuration from external files", () => {
const output = runPrettier("cli/config/", ["**/*.js"]);
expect(output.stdout).toMatchSnapshot();
expect(output.status).toEqual(0);
});
test("resolves configuration from external files and overrides by extname", () => {
const output = runPrettier("cli/config/", ["**/*.ts"]);
expect(output.stdout).toMatchSnapshot();
expect(output.status).toEqual(0);
});
test("accepts configuration from --config", () => {
const output = runPrettier("cli/config/", [
"--config",
".prettierrc",
"./js/file.js"
]);
expect(output.stdout).toMatchSnapshot();
expect(output.status).toEqual(0);
});
test("resolves configuration file with --resolve-config file", () => {
const output = runPrettier("cli/config/", [
"--resolve-config",
"no-config/file.js"
]);
expect(output.stdout).toMatchSnapshot();
expect(output.status).toEqual(0);
});
test("prints nothing when no file found with --resolve-config", () => {
const output = runPrettier("cli/config/", ["--resolve-config", ".."]);
expect(output.stdout).toEqual("");
expect(output.status).toEqual(1);
});
test("CLI overrides take precedence", () => {
const output = runPrettier("cli/config/", ["--print-width", "1", "**/*.js"]);
expect(output.stdout).toMatchSnapshot();
expect(output.status).toEqual(0);
});
test("API resolveConfig with no args", () => {
return prettier.resolveConfig().then(result => {
expect(result).toBeNull();
});
});
test("API resolveConfig with file arg", () => {
const file = path.resolve(path.join(__dirname, "../cli/config/js/file.js"));
return prettier.resolveConfig(file).then(result => {
expect(result).toMatchObject({
tabWidth: 8
});
});
});
test("API resolveConfig with file arg and extension override", () => {
const file = path.resolve(
path.join(__dirname, "../cli/config/no-config/file.ts")
);
return prettier.resolveConfig(file).then(result => {
expect(result).toMatchObject({
semi: true
});
});
});

View File

@ -0,0 +1,6 @@
semi: false
overrides:
- files: "*.ts"
options:
semi: true

View File

@ -0,0 +1,8 @@
semi: false
overrides:
- files:
- "*.test.js"
- "**/__best-tests__/*.js"
options:
semi: true

View File

@ -0,0 +1,2 @@
/* eslint-disable */
console.log("should not have semi")

View File

@ -0,0 +1,2 @@
/* eslint-disable */
console.log("should have semi");

View File

@ -0,0 +1,2 @@
/* eslint-disable */
console.log("should have semi");

View File

@ -0,0 +1,5 @@
/* eslint-disable */
function f() {
console.log("should have tab width 8");
}

View File

@ -0,0 +1,5 @@
"use strict";
module.exports = {
tabWidth: 8
};

View File

@ -0,0 +1,5 @@
/* eslint-disable */
function f() {
console.log("should have no semicolons");
}

View File

@ -0,0 +1,3 @@
function g() {
console.log("should have semicolons because it has a .ts extension");
}

View File

@ -0,0 +1,5 @@
/* eslint-disable */
function f() {
console.log("should have tab width 3");
}

View File

@ -0,0 +1,3 @@
function g() {
console.log("should have tab width 5 because it has .ts extension");
}

View File

@ -0,0 +1,15 @@
{
"name": "my-package",
"version": "9000",
"prettier": {
"tabWidth": 3,
"overrides": [
{
"files": "*.ts",
"options": {
"tabWidth": 5
}
}
]
}
}

View File

@ -0,0 +1,4 @@
{
"trailingComma": "all",
"singleQuote": true
}

View File

@ -0,0 +1,9 @@
/* eslint-disable */
function f() {
console.log.apply(null, [
"this file",
"should have trailing comma",
"and single quotes",
]);
}

View File

@ -1004,6 +1004,18 @@ core-util-is@~1.0.0:
version "1.0.2"
resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7"
cosmiconfig@2.1.3:
version "2.1.3"
resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-2.1.3.tgz#952771eb0dddc1cb3fa2f6fbe51a522e93b3ee0a"
dependencies:
is-directory "^0.3.1"
js-yaml "^3.4.3"
minimist "^1.2.0"
object-assign "^4.1.0"
os-homedir "^1.0.1"
parse-json "^2.2.0"
require-from-string "^1.1.0"
create-ecdh@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/create-ecdh/-/create-ecdh-4.0.0.tgz#888c723596cdf7612f6498233eebd7a35301737d"
@ -1083,6 +1095,10 @@ dashdash@^1.12.0:
dependencies:
assert-plus "^1.0.0"
dashify@0.2.2:
version "0.2.2"
resolved "https://registry.yarnpkg.com/dashify/-/dashify-0.2.2.tgz#6a07415a01c91faf4a32e38d9dfba71f61cb20fe"
date-now@^0.1.4:
version "0.1.4"
resolved "https://registry.yarnpkg.com/date-now/-/date-now-0.1.4.tgz#eaf439fd4d4848ad74e5cc7dbef200672b9e345b"
@ -1892,6 +1908,10 @@ is-ci@^1.0.10:
dependencies:
ci-info "^1.0.0"
is-directory@^0.3.1:
version "0.3.1"
resolved "https://registry.yarnpkg.com/is-directory/-/is-directory-0.3.1.tgz#61339b6f2475fc772fd9c9d83f5c8575dc154ae1"
is-dotfile@^1.0.0:
version "1.0.3"
resolved "https://registry.yarnpkg.com/is-dotfile/-/is-dotfile-1.0.3.tgz#a6a2f32ffd2dfb04f5ca25ecd0f6b83cf798a1e1"
@ -2311,7 +2331,7 @@ js-tokens@^3.0.0:
version "3.0.1"
resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-3.0.1.tgz#08e9f132484a2c45a30907e9dc4d5567b7f114d7"
js-yaml@^3.7.0, js-yaml@^3.8.4:
js-yaml@^3.4.3, js-yaml@^3.7.0, js-yaml@^3.8.4:
version "3.8.4"
resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.8.4.tgz#520b4564f86573ba96662af85a8cafa7b4b5a6f6"
dependencies:
@ -2588,7 +2608,7 @@ minimalistic-crypto-utils@^1.0.0, minimalistic-crypto-utils@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz#f6c00c1c0b082246e5c4d99dfb8c7c083b2b582a"
minimatch@^3.0.0, minimatch@^3.0.2, minimatch@^3.0.3, minimatch@^3.0.4:
minimatch@3.0.4, minimatch@^3.0.0, minimatch@^3.0.2, minimatch@^3.0.3, minimatch@^3.0.4:
version "3.0.4"
resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083"
dependencies:
@ -2767,7 +2787,7 @@ os-browserify@^0.2.0:
version "0.2.1"
resolved "https://registry.yarnpkg.com/os-browserify/-/os-browserify-0.2.1.tgz#63fc4ccee5d2d7763d26bbf8601078e6c2e0044f"
os-homedir@^1.0.0:
os-homedir@^1.0.0, os-homedir@^1.0.1:
version "1.0.2"
resolved "https://registry.yarnpkg.com/os-homedir/-/os-homedir-1.0.2.tgz#ffbc4988336e0e833de0c168c7ef152121aa7fb3"
@ -3228,6 +3248,10 @@ require-directory@^2.1.1:
version "2.1.1"
resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42"
require-from-string@^1.1.0:
version "1.2.1"
resolved "https://registry.yarnpkg.com/require-from-string/-/require-from-string-1.2.1.tgz#529c9ccef27380adfec9a2f965b649bbee636418"
require-main-filename@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/require-main-filename/-/require-main-filename-1.0.1.tgz#97f717b69d48784f5f526a6c5aa8ffdda055a4d1"