UNPKG

6.5 kBPlain TextView Raw
1// MIT © 2017 azu
2// MIT © Zoltan Kochan
3// Original https://github.com/zkochan/rcfile
4import path from "path";
5import fs from "fs";
6import requireFromString from "require-from-string";
7import JSON5 from "json5";
8import type {
9 PossibleUndefined,
10 rcConfigLoaderOption,
11 rcConfigResult,
12 RequiredOption,
13 ExtensionName,
14 ExtensionLoaderMap,
15 Loader
16} from "./types";
17
18const debug = require("debug")("rc-config-loader");
19
20const defaultLoaderByExt: ExtensionLoaderMap = {
21 ".cjs": loadJSConfigFile,
22 ".js": loadJSConfigFile,
23 ".json": loadJSONConfigFile,
24 ".yaml": loadYAMLConfigFile,
25 ".yml": loadYAMLConfigFile
26};
27
28const defaultOptions: Required<Pick<rcConfigLoaderOption, RequiredOption>> &
29 Omit<rcConfigLoaderOption, RequiredOption> = {
30 packageJSON: false,
31 defaultExtension: [".json", ".yaml", ".yml", ".js", ".cjs"],
32 cwd: process.cwd()
33};
34
35const selectLoader = (defaultLoaderByExt: ExtensionLoaderMap, extension: ExtensionName): Loader => {
36 if (!defaultOptions.defaultExtension.includes(extension)) {
37 throw new Error(`${extension} is not supported.`);
38 }
39 return defaultLoaderByExt[extension];
40};
41
42/**
43 * Find and load rcfile, return { config, filePath }
44 * If not found any rcfile, throw an Error.
45 * @param {string} pkgName
46 * @param {rcConfigLoaderOption} [opts]
47 * @returns {{ config: Object, filePath:string } | undefined}
48 */
49export function rcFile<R extends {}>(
50 pkgName: string,
51 opts: rcConfigLoaderOption = {}
52): PossibleUndefined<rcConfigResult<R>> {
53 // path/to/config or basename of config file.
54 const configFileName = opts.configFileName || `.${pkgName}rc`;
55 const defaultExtension = opts.defaultExtension || defaultOptions.defaultExtension;
56 const cwd = opts.cwd || defaultOptions.cwd;
57 const packageJSON = opts.packageJSON || defaultOptions.packageJSON;
58 const packageJSONFieldName = typeof packageJSON === "object" ? packageJSON.fieldName : pkgName;
59
60 const parts = splitPath(cwd);
61 const loadersByOrder = Array.isArray(defaultExtension)
62 ? defaultExtension.map((extension) => selectLoader(defaultLoaderByExt, extension))
63 : selectLoader(defaultLoaderByExt, defaultExtension);
64
65 const loaderByExt = {
66 ...defaultLoaderByExt,
67 "": loadersByOrder
68 };
69 return findConfig<R>({
70 parts,
71 loaderByExt,
72 loadersByOrder,
73 configFileName,
74 packageJSON,
75 packageJSONFieldName
76 });
77}
78
79/**
80 *
81 * @returns {{
82 * config: string,
83 * filePath: string
84 * }}
85 */
86function findConfig<R extends {}>({
87 parts,
88 loaderByExt,
89 loadersByOrder,
90 configFileName,
91 packageJSON,
92 packageJSONFieldName
93}: {
94 parts: string[];
95 loaderByExt: {
96 [index: string]: Loader | Loader[];
97 };
98 loadersByOrder: Loader | Loader[];
99 configFileName: string;
100 packageJSON: boolean | { fieldName: string };
101 packageJSONFieldName: string;
102}):
103 | {
104 config: R;
105 filePath: string;
106 }
107 | undefined {
108 const extensions = Object.keys(loaderByExt);
109 while (extensions.length) {
110 const ext = extensions.shift();
111 // may be ext is "". if it .<product>rc
112 const configLocation = join(parts, configFileName + ext);
113 if (!fs.existsSync(configLocation)) {
114 continue;
115 }
116 // if ext === ""(empty string):, use ordered loaders
117 const loaders = ext ? loaderByExt[ext] : loadersByOrder;
118 if (!Array.isArray(loaders)) {
119 const loader = loaders;
120 const result = loader<R>(configLocation, false);
121 if (!result) {
122 continue;
123 }
124 return {
125 config: result,
126 filePath: configLocation
127 };
128 }
129 for (let i = 0; i < loaders.length; i++) {
130 const loader = loaders[i];
131 const result = loader<R>(configLocation, true);
132 if (!result) {
133 continue;
134 }
135 return {
136 config: result,
137 filePath: configLocation
138 };
139 }
140 }
141
142 if (packageJSON) {
143 const pkgJSONLoc = join(parts, "package.json");
144 if (fs.existsSync(pkgJSONLoc)) {
145 const pkgJSON = JSON5.parse(readFile(pkgJSONLoc));
146 if (pkgJSON[packageJSONFieldName]) {
147 return {
148 config: pkgJSON[packageJSONFieldName],
149 filePath: pkgJSONLoc
150 };
151 }
152 }
153 }
154 if (parts.pop()) {
155 return findConfig({ parts, loaderByExt, loadersByOrder, configFileName, packageJSON, packageJSONFieldName });
156 }
157 return;
158}
159
160function splitPath(x: string): string[] {
161 return path.resolve(x || "").split(path.sep);
162}
163
164function join(parts: string[], filename: string) {
165 return path.resolve(parts.join(path.sep) + path.sep, filename);
166}
167
168function loadJSConfigFile(filePath: string, suppress: boolean) {
169 debug(`Loading JavaScript config file: ${filePath}`);
170 try {
171 const content = fs.readFileSync(filePath, "utf-8");
172 return requireFromString(content, filePath);
173 } catch (error: any) {
174 debug(`Error reading JavaScript file: ${filePath}`);
175 if (!suppress) {
176 error.message = `Cannot read config file: ${filePath}\nError: ${error.message}`;
177 throw error;
178 }
179 }
180}
181
182function loadJSONConfigFile(filePath: string, suppress: boolean) {
183 debug(`Loading JSON config file: ${filePath}`);
184
185 try {
186 return JSON5.parse(readFile(filePath));
187 } catch (error: any) {
188 debug(`Error reading JSON file: ${filePath}`);
189 if (!suppress) {
190 error.message = `Cannot read config file: ${filePath}\nError: ${error.message}`;
191 throw error;
192 }
193 }
194}
195
196function readFile(filePath: string) {
197 return fs.readFileSync(filePath, "utf8");
198}
199
200function loadYAMLConfigFile(filePath: string, suppress: boolean) {
201 debug(`Loading YAML config file: ${filePath}`);
202 // lazy load YAML to improve performance when not used
203 const yaml = require("js-yaml");
204 try {
205 // empty YAML file can be null, so always use
206 return yaml.load(readFile(filePath)) || {};
207 } catch (error: any) {
208 debug(`Error reading YAML file: ${filePath}`);
209 if (!suppress) {
210 error.message = `Cannot read config file: ${filePath}\nError: ${error.message}`;
211 throw error;
212 }
213 }
214}