1 |
|
2 |
|
3 |
|
4 | import path from "path";
|
5 | import fs from "fs";
|
6 | import requireFromString from "require-from-string";
|
7 | import JSON5 from "json5";
|
8 | import type {
|
9 | PossibleUndefined,
|
10 | rcConfigLoaderOption,
|
11 | rcConfigResult,
|
12 | RequiredOption,
|
13 | ExtensionName,
|
14 | ExtensionLoaderMap,
|
15 | Loader
|
16 | } from "./types";
|
17 |
|
18 | const debug = require("debug")("rc-config-loader");
|
19 |
|
20 | const defaultLoaderByExt: ExtensionLoaderMap = {
|
21 | ".cjs": loadJSConfigFile,
|
22 | ".js": loadJSConfigFile,
|
23 | ".json": loadJSONConfigFile,
|
24 | ".yaml": loadYAMLConfigFile,
|
25 | ".yml": loadYAMLConfigFile
|
26 | };
|
27 |
|
28 | const 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 |
|
35 | const 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 |
|
44 |
|
45 |
|
46 |
|
47 |
|
48 |
|
49 | export function rcFile<R extends {}>(
|
50 | pkgName: string,
|
51 | opts: rcConfigLoaderOption = {}
|
52 | ): PossibleUndefined<rcConfigResult<R>> {
|
53 |
|
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 |
|
82 |
|
83 |
|
84 |
|
85 |
|
86 | function 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 |
|
112 | const configLocation = join(parts, configFileName + ext);
|
113 | if (!fs.existsSync(configLocation)) {
|
114 | continue;
|
115 | }
|
116 |
|
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 |
|
160 | function splitPath(x: string): string[] {
|
161 | return path.resolve(x || "").split(path.sep);
|
162 | }
|
163 |
|
164 | function join(parts: string[], filename: string) {
|
165 | return path.resolve(parts.join(path.sep) + path.sep, filename);
|
166 | }
|
167 |
|
168 | function 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 |
|
182 | function 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 |
|
196 | function readFile(filePath: string) {
|
197 | return fs.readFileSync(filePath, "utf8");
|
198 | }
|
199 |
|
200 | function loadYAMLConfigFile(filePath: string, suppress: boolean) {
|
201 | debug(`Loading YAML config file: ${filePath}`);
|
202 |
|
203 | const yaml = require("js-yaml");
|
204 | try {
|
205 |
|
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 | }
|