Skip to content

Commit 636874f

Browse files
author
andrei
committed
feat(configLoader): added fully compatible cosmiconfig support
Added fully compatible cosmiconfig support with BOM and encoding checks. Added json5 config format parser. Added tests. Some minor code style changes; Closes: commitizen#773
1 parent 2e57fd0 commit 636874f

12 files changed

+940
-226
lines changed

.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,4 +6,5 @@ artifacts/
66
npm-debug.log
77
.nyc_output
88
test/tools/trigger-appveyor-tests.sh
9-
logo/*.png
9+
logo/*.png
10+
.idea

package-lock.json

Lines changed: 146 additions & 111 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@
7474
},
7575
"dependencies": {
7676
"cachedir": "2.3.0",
77+
"cosmiconfig": "8.3.6",
7778
"cz-conventional-changelog": "3.3.0",
7879
"dedent": "0.7.0",
7980
"detect-indent": "6.1.0",
@@ -83,6 +84,7 @@
8384
"glob": "7.2.3",
8485
"inquirer": "8.2.5",
8586
"is-utf8": "^0.2.1",
87+
"json5": "2.2.3",
8688
"lodash": "4.17.21",
8789
"minimist": "1.2.7",
8890
"strip-bom": "4.0.0",

src/commitizen/configLoader.js

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,12 @@ import { loader } from '../configLoader';
22

33
export { load };
44

5-
// Configuration sources in priority order.
6-
var configs = ['.czrc', '.cz.json', 'package.json'];
7-
5+
/**
6+
* Get content of the configuration file
7+
* @param {string} [config] - partial path to configuration file
8+
* @param {string} [cwd] - directory path which will be joined with config argument
9+
* @return {Object|undefined}
10+
*/
811
function load (config, cwd) {
9-
return loader(configs, config, cwd);
12+
return loader(config, cwd);
1013
}

src/configLoader/cosmiconfigLoader.js

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
import { cosmiconfigSync, defaultLoadersSync } from 'cosmiconfig';
2+
import JSON5 from 'json5';
3+
import stripBom from 'strip-bom';
4+
import isUTF8 from "is-utf8";
5+
6+
const moduleName = 'cz';
7+
const fullModuleName = 'commitizen';
8+
9+
const searchPlaces = [
10+
`.${moduleName}rc`, // .czrc
11+
`.${moduleName}rc.json`,
12+
`.${moduleName}rc.json5`,
13+
`.${moduleName}rc.yaml`,
14+
`.${moduleName}rc.yml`,
15+
`.${moduleName}rc.js`,
16+
`.${moduleName}rc.cjs`,
17+
`.${moduleName}rc.ts`,
18+
`.config/${moduleName}rc`,
19+
`.config/${moduleName}rc.json`,
20+
`.config/${moduleName}rc.json5`,
21+
`.config/${moduleName}rc.yaml`,
22+
`.config/${moduleName}rc.yml`,
23+
`.config/${moduleName}rc.js`,
24+
`.config/${moduleName}rc.ts`,
25+
`.config/${moduleName}rc.cjs`,
26+
`${moduleName}.config.js`,
27+
`${moduleName}.config.ts`,
28+
`${moduleName}.config.cjs`,
29+
`.${moduleName}.json`, // .cz.json
30+
`.${moduleName}.json5`,
31+
'package.json',
32+
];
33+
34+
function withSafeContentLoader(loader) {
35+
return function (filePath, content) {
36+
if (!isUTF8(Buffer.from(content, 'utf8'))) {
37+
throw new Error(`The config file at "${filePath}" contains invalid charset, expect utf8`);
38+
}
39+
return loader(filePath, stripBom(content));
40+
}
41+
}
42+
43+
function json5Loader(filePath, content) {
44+
try {
45+
return JSON5.parse(content) || null;
46+
} catch (err) {
47+
err.message = `Error parsing json at ${filePath}:\n${err.message}`;
48+
throw err;
49+
}
50+
}
51+
52+
// no '.ts': withSafeContentLoader(defaultLoadersSync['.ts']),
53+
const loaders = {
54+
'.cjs': withSafeContentLoader(defaultLoadersSync['.js']),
55+
'.js': withSafeContentLoader(defaultLoadersSync['.js']),
56+
'.yml': withSafeContentLoader(defaultLoadersSync['.yaml']),
57+
'.json': withSafeContentLoader(json5Loader),
58+
'.json5': withSafeContentLoader(json5Loader),
59+
'.yaml': withSafeContentLoader(defaultLoadersSync['.yaml']),
60+
'.ts': withSafeContentLoader(defaultLoadersSync['.ts']),
61+
noExt: withSafeContentLoader(json5Loader)
62+
}
63+
64+
const defaultConfigExplorer = cosmiconfigSync(moduleName, {
65+
packageProp: ['configs', fullModuleName],
66+
searchPlaces: searchPlaces,
67+
loaders: loaders,
68+
cache: false,
69+
});
70+
71+
/**
72+
* @deprecated
73+
*/
74+
const deprecatedConfigExplorerFallback = cosmiconfigSync(moduleName, {
75+
packageProp: ['czConfig'],
76+
searchPlaces: ['package.json'],
77+
loaders: loaders,
78+
cache: false,
79+
});
80+
81+
export { searchPlaces, moduleName, defaultConfigExplorer, deprecatedConfigExplorerFallback };

src/configLoader/findContent.js

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import { defaultConfigExplorer, deprecatedConfigExplorerFallback } from "./cosmiconfigLoader";
2+
import { isInTest } from "../common/util";
3+
4+
export default findConfigContent;
5+
/**
6+
* Find content of the configuration file
7+
* @param {String} cwd - partial path to configuration file
8+
* @return {Object|undefined}
9+
*/
10+
function findConfigContent(cwd) {
11+
const maybeConfig = defaultConfigExplorer.search(cwd);
12+
if (maybeConfig) {
13+
return maybeConfig.config
14+
} else {
15+
const deprecatedConfig = findOldCzConfig(cwd);
16+
if (deprecatedConfig) {
17+
return deprecatedConfig
18+
}
19+
}
20+
21+
return undefined;
22+
}
23+
24+
/**
25+
* find old czConfig
26+
*
27+
* @deprecated
28+
* @param {string} [searchFrom]
29+
* @return {Object|undefined}
30+
*/
31+
function findOldCzConfig(searchFrom) {
32+
const maybeDeprecatedConfig = deprecatedConfigExplorerFallback.search(searchFrom);
33+
if (maybeDeprecatedConfig) {
34+
showOldCzConfigDeprecationWarning();
35+
return maybeDeprecatedConfig.config;
36+
}
37+
38+
return undefined;
39+
}
40+
41+
/**
42+
* @deprecated
43+
* @return void
44+
*/
45+
function showOldCzConfigDeprecationWarning() {
46+
// Suppress during test
47+
if (!isInTest()) {
48+
console.error("\n********\nWARNING: This repository's package.json is using czConfig. czConfig will be deprecated in Commitizen 3. \nPlease use this instead:\n{\n \"config\": {\n \"commitizen\": {\n \"path\": \"./path/to/adapter\"\n }\n }\n}\nFor more information, see: http://commitizen.github.io/cz-cli/\n********\n");
49+
}
50+
}

src/configLoader/findup.js

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,16 @@ import glob from 'glob';
33

44
export default findup;
55

6-
// Before, "findup-sync" package was used,
7-
// but it does not provide filter callback
6+
/**
7+
* Before, "findup-sync" package was used,
8+
* but it does not provide filter callback
9+
*
10+
* @param {string[]} patterns
11+
* @param {Object} options
12+
* @param {string} options.cwd
13+
* @param {(baseName: string) => boolean} fn
14+
* @return {string}
15+
*/
816
function findup (patterns, options, fn) {
917
/* jshint -W083 */
1018

src/configLoader/getContent.js

Lines changed: 40 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -1,82 +1,60 @@
1-
import fs from 'fs';
21
import path from 'path';
32

4-
import stripJSONComments from 'strip-json-comments';
5-
import isUTF8 from 'is-utf8';
6-
import stripBom from 'strip-bom';
7-
8-
import { getNormalizedConfig } from '../configLoader';
3+
import { defaultConfigExplorer, deprecatedConfigExplorerFallback } from "./cosmiconfigLoader";
4+
import { isInTest } from "../common/util";
95

106
export default getConfigContent;
117

12-
/**
13-
* Read the content of a configuration file
14-
* - if not js or json: strip any comments
15-
* - if js or json: require it
16-
* @param {String} configPath - full path to configuration file
17-
* @return {Object}
18-
*/
19-
function readConfigContent (configPath) {
20-
const parsedPath = path.parse(configPath)
21-
const isRcFile = parsedPath.ext !== '.js' && parsedPath.ext !== '.json';
22-
const jsonString = readConfigFileContent(configPath);
23-
const parse = isRcFile ?
24-
(contents) => JSON.parse(stripJSONComments(contents)) :
25-
(contents) => JSON.parse(contents);
26-
27-
try {
28-
const parsed = parse(jsonString);
29-
30-
Object.defineProperty(parsed, 'configPath', {
31-
value: configPath
32-
});
33-
34-
return parsed;
35-
} catch (error) {
36-
error.message = [
37-
`Parsing JSON at ${configPath} for commitizen config failed:`,
38-
error.mesasge
39-
].join('\n');
40-
41-
throw error;
42-
}
43-
}
44-
458
/**
469
* Get content of the configuration file
47-
* @param {String} configPath - partial path to configuration file
48-
* @param {String} directory - directory path which will be joined with config argument
49-
* @return {Object}
10+
* @param {String} [configPath] - partial path to configuration file
11+
* @param {String} [baseDirectory] - directory path which will be joined with config argument
12+
* @return {Object|undefined}
5013
*/
5114
function getConfigContent (configPath, baseDirectory) {
52-
if (!configPath) {
53-
return;
54-
}
15+
if (!configPath) {
16+
return;
17+
}
5518

56-
const resolvedPath = path.resolve(baseDirectory, configPath);
57-
const configBasename = path.basename(resolvedPath);
19+
const resolvedPath = path.resolve(baseDirectory, configPath);
5820

59-
if (!fs.existsSync(resolvedPath)) {
60-
return getNormalizedConfig(resolvedPath);
21+
const maybeConfig = defaultConfigExplorer.load(resolvedPath);
22+
if (maybeConfig) {
23+
return maybeConfig.config
24+
} else {
25+
const deprecatedConfig = loadOldCzConfig(resolvedPath);
26+
if (deprecatedConfig) {
27+
return deprecatedConfig
6128
}
29+
}
6230

63-
const content = readConfigContent(resolvedPath);
64-
return getNormalizedConfig(configBasename, content);
65-
};
31+
return undefined;
32+
}
6633

6734
/**
68-
* Read proper content from config file.
69-
* If the chartset of the config file is not utf-8, one error will be thrown.
70-
* @param {String} configPath
71-
* @return {String}
35+
* load old czConfig from known place
36+
*
37+
* @deprecated
38+
* @param {string} [fullPath]
39+
* @return {Object|undefined}
7240
*/
73-
function readConfigFileContent (configPath) {
41+
function loadOldCzConfig(fullPath) {
42+
const maybeDeprecatedConfig = deprecatedConfigExplorerFallback.load(fullPath);
43+
if (maybeDeprecatedConfig) {
44+
showOldCzConfigDeprecationWarning();
45+
return maybeDeprecatedConfig.config;
46+
}
7447

75-
let rawBufContent = fs.readFileSync(configPath);
48+
return undefined;
49+
}
7650

77-
if (!isUTF8(rawBufContent)) {
78-
throw new Error(`The config file at "${configPath}" contains invalid charset, expect utf8`);
51+
/**
52+
* @deprecated
53+
* @return void
54+
*/
55+
function showOldCzConfigDeprecationWarning() {
56+
// Suppress during test
57+
if (!isInTest()) {
58+
console.error("\n********\nWARNING: This repository's package.json is using czConfig. czConfig will be deprecated in Commitizen 3. \nPlease use this instead:\n{\n \"config\": {\n \"commitizen\": {\n \"path\": \"./path/to/adapter\"\n }\n }\n}\nFor more information, see: http://commitizen.github.io/cz-cli/\n********\n");
7959
}
80-
81-
return stripBom(rawBufContent.toString("utf8"));
8260
}

src/configLoader/getNormalizedConfig.js

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
11
export default getNormalizedConfig;
22

3-
// Given a config and content, plucks the actual
4-
// settings that we're interested in
5-
function getNormalizedConfig (config, content) {
3+
/**
4+
* @deprecated no need this function with cosmiconfig.
5+
*
6+
* Given a config and content, plucks the actual settings that we're interested in
7+
*/
8+
function getNormalizedConfig (baseName, content) {
69

7-
if (content && (config === 'package.json')) {
10+
if (content && (baseName === 'package.json')) {
811

912
// PACKAGE.JSON
1013

0 commit comments

Comments
 (0)