版博士V2.0程序
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

index.mjs 9.0 KiB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292
  1. import { existsSync, promises } from 'node:fs';
  2. import { resolve, extname, dirname } from 'pathe';
  3. import * as dotenv from 'dotenv';
  4. import { rmdir } from 'node:fs/promises';
  5. import { homedir } from 'node:os';
  6. import createJiti from 'jiti';
  7. import * as rc9 from 'rc9';
  8. import { defu } from 'defu';
  9. import { findWorkspaceDir, readPackageJSON } from 'pkg-types';
  10. async function setupDotenv(options) {
  11. const targetEnvironment = options.env ?? process.env;
  12. const environment = await loadDotenv({
  13. cwd: options.cwd,
  14. fileName: options.fileName ?? ".env",
  15. env: targetEnvironment,
  16. interpolate: options.interpolate ?? true
  17. });
  18. for (const key in environment) {
  19. if (!key.startsWith("_") && targetEnvironment[key] === void 0) {
  20. targetEnvironment[key] = environment[key];
  21. }
  22. }
  23. return environment;
  24. }
  25. async function loadDotenv(options) {
  26. const environment = /* @__PURE__ */ Object.create(null);
  27. const dotenvFile = resolve(options.cwd, options.fileName);
  28. if (existsSync(dotenvFile)) {
  29. const parsed = dotenv.parse(await promises.readFile(dotenvFile, "utf8"));
  30. Object.assign(environment, parsed);
  31. }
  32. if (!options.env._applied) {
  33. Object.assign(environment, options.env);
  34. environment._applied = true;
  35. }
  36. if (options.interpolate) {
  37. interpolate(environment);
  38. }
  39. return environment;
  40. }
  41. function interpolate(target, source = {}, parse = (v) => v) {
  42. function getValue(key) {
  43. return source[key] !== void 0 ? source[key] : target[key];
  44. }
  45. function interpolate2(value, parents = []) {
  46. if (typeof value !== "string") {
  47. return value;
  48. }
  49. const matches = value.match(/(.?\${?(?:[\w:]+)?}?)/g) || [];
  50. return parse(
  51. // eslint-disable-next-line unicorn/no-array-reduce
  52. matches.reduce((newValue, match) => {
  53. const parts = /(.?)\${?([\w:]+)?}?/g.exec(match);
  54. const prefix = parts[1];
  55. let value2, replacePart;
  56. if (prefix === "\\") {
  57. replacePart = parts[0];
  58. value2 = replacePart.replace("\\$", "$");
  59. } else {
  60. const key = parts[2];
  61. replacePart = parts[0].slice(prefix.length);
  62. if (parents.includes(key)) {
  63. console.warn(
  64. `Please avoid recursive environment variables ( loop: ${parents.join(
  65. " > "
  66. )} > ${key} )`
  67. );
  68. return "";
  69. }
  70. value2 = getValue(key);
  71. value2 = interpolate2(value2, [...parents, key]);
  72. }
  73. return value2 !== void 0 ? newValue.replace(replacePart, value2) : newValue;
  74. }, value)
  75. );
  76. }
  77. for (const key in target) {
  78. target[key] = interpolate2(getValue(key));
  79. }
  80. }
  81. async function loadConfig(options) {
  82. options.cwd = resolve(process.cwd(), options.cwd || ".");
  83. options.name = options.name || "config";
  84. options.envName = options.envName ?? process.env.NODE_ENV;
  85. options.configFile = options.configFile ?? (options.name !== "config" ? `${options.name}.config` : "config");
  86. options.rcFile = options.rcFile ?? `.${options.name}rc`;
  87. if (options.extend !== false) {
  88. options.extend = {
  89. extendKey: "extends",
  90. ...options.extend
  91. };
  92. }
  93. options.jiti = options.jiti || createJiti(void 0, {
  94. interopDefault: true,
  95. requireCache: false,
  96. esmResolve: true,
  97. ...options.jitiOptions
  98. });
  99. const r = {
  100. config: {},
  101. cwd: options.cwd,
  102. configFile: resolve(options.cwd, options.configFile),
  103. layers: []
  104. };
  105. if (options.dotenv) {
  106. await setupDotenv({
  107. cwd: options.cwd,
  108. ...options.dotenv === true ? {} : options.dotenv
  109. });
  110. }
  111. const { config, configFile } = await resolveConfig(".", options);
  112. if (configFile) {
  113. r.configFile = configFile;
  114. }
  115. const configRC = {};
  116. if (options.rcFile) {
  117. if (options.globalRc) {
  118. Object.assign(
  119. configRC,
  120. rc9.readUser({ name: options.rcFile, dir: options.cwd })
  121. );
  122. const workspaceDir = await findWorkspaceDir(options.cwd).catch(() => {
  123. });
  124. if (workspaceDir) {
  125. Object.assign(
  126. configRC,
  127. rc9.read({ name: options.rcFile, dir: workspaceDir })
  128. );
  129. }
  130. }
  131. Object.assign(
  132. configRC,
  133. rc9.read({ name: options.rcFile, dir: options.cwd })
  134. );
  135. }
  136. const pkgJson = {};
  137. if (options.packageJson) {
  138. const keys = (Array.isArray(options.packageJson) ? options.packageJson : [
  139. typeof options.packageJson === "string" ? options.packageJson : options.name
  140. ]).filter((t) => t && typeof t === "string");
  141. const pkgJsonFile = await readPackageJSON(options.cwd).catch(() => {
  142. });
  143. const values = keys.map((key) => pkgJsonFile?.[key]);
  144. Object.assign(pkgJson, defu({}, ...values));
  145. }
  146. r.config = defu(
  147. options.overrides,
  148. config,
  149. configRC,
  150. pkgJson,
  151. options.defaultConfig
  152. );
  153. if (options.extend) {
  154. await extendConfig(r.config, options);
  155. r.layers = r.config._layers;
  156. delete r.config._layers;
  157. r.config = defu(r.config, ...r.layers.map((e) => e.config));
  158. }
  159. const baseLayers = [
  160. options.overrides && {
  161. config: options.overrides,
  162. configFile: void 0,
  163. cwd: void 0
  164. },
  165. { config, configFile: options.configFile, cwd: options.cwd },
  166. options.rcFile && { config: configRC, configFile: options.rcFile },
  167. options.packageJson && { config: pkgJson, configFile: "package.json" }
  168. ].filter((l) => l && l.config);
  169. r.layers = [...baseLayers, ...r.layers];
  170. if (options.defaults) {
  171. r.config = defu(r.config, options.defaults);
  172. }
  173. return r;
  174. }
  175. async function extendConfig(config, options) {
  176. config._layers = config._layers || [];
  177. if (!options.extend) {
  178. return;
  179. }
  180. let keys = options.extend.extendKey;
  181. if (typeof keys === "string") {
  182. keys = [keys];
  183. }
  184. const extendSources = [];
  185. for (const key of keys) {
  186. extendSources.push(
  187. ...(Array.isArray(config[key]) ? config[key] : [config[key]]).filter(
  188. Boolean
  189. )
  190. );
  191. delete config[key];
  192. }
  193. for (let extendSource of extendSources) {
  194. const originalExtendSource = extendSource;
  195. let sourceOptions = {};
  196. if (extendSource.source) {
  197. sourceOptions = extendSource.options || {};
  198. extendSource = extendSource.source;
  199. }
  200. if (Array.isArray(extendSource)) {
  201. sourceOptions = extendSource[1] || {};
  202. extendSource = extendSource[0];
  203. }
  204. if (typeof extendSource !== "string") {
  205. console.warn(
  206. `Cannot extend config from \`${JSON.stringify(
  207. originalExtendSource
  208. )}\` in ${options.cwd}`
  209. );
  210. continue;
  211. }
  212. const _config = await resolveConfig(extendSource, options, sourceOptions);
  213. if (!_config.config) {
  214. console.warn(
  215. `Cannot extend config from \`${extendSource}\` in ${options.cwd}`
  216. );
  217. continue;
  218. }
  219. await extendConfig(_config.config, { ...options, cwd: _config.cwd });
  220. config._layers.push(_config);
  221. if (_config.config._layers) {
  222. config._layers.push(..._config.config._layers);
  223. delete _config.config._layers;
  224. }
  225. }
  226. }
  227. const GIT_PREFIXES = ["github:", "gitlab:", "bitbucket:", "https://"];
  228. const NPM_PACKAGE_RE = /^(@[\da-z~-][\d._a-z~-]*\/)?[\da-z~-][\d._a-z~-]*($|\/.*)/;
  229. async function resolveConfig(source, options, sourceOptions = {}) {
  230. if (options.resolve) {
  231. const res2 = await options.resolve(source, options);
  232. if (res2) {
  233. return res2;
  234. }
  235. }
  236. if (GIT_PREFIXES.some((prefix) => source.startsWith(prefix))) {
  237. const { downloadTemplate } = await import('giget');
  238. const url = new URL(source);
  239. const gitRepo = url.protocol + url.pathname.split("/").slice(0, 2).join("/");
  240. const name = gitRepo.replace(/[#/:@\\]/g, "_");
  241. const tmpDir = process.env.XDG_CACHE_HOME ? resolve(process.env.XDG_CACHE_HOME, "c12", name) : resolve(homedir(), ".cache/c12", name);
  242. if (existsSync(tmpDir)) {
  243. await rmdir(tmpDir, { recursive: true });
  244. }
  245. const cloned = await downloadTemplate(source, { dir: tmpDir });
  246. source = cloned.dir;
  247. }
  248. if (NPM_PACKAGE_RE.test(source)) {
  249. try {
  250. source = options.jiti.resolve(source, { paths: [options.cwd] });
  251. } catch {
  252. }
  253. }
  254. const isDir = !extname(source);
  255. const cwd = resolve(options.cwd, isDir ? source : dirname(source));
  256. if (isDir) {
  257. source = options.configFile;
  258. }
  259. const res = { config: void 0, cwd, source, sourceOptions };
  260. try {
  261. res.configFile = options.jiti.resolve(resolve(cwd, source), {
  262. paths: [cwd]
  263. });
  264. } catch {
  265. }
  266. if (!existsSync(res.configFile)) {
  267. return res;
  268. }
  269. res.config = options.jiti(res.configFile);
  270. if (res.config instanceof Function) {
  271. res.config = await res.config();
  272. }
  273. if (options.envName) {
  274. const envConfig = {
  275. ...res.config["$" + options.envName],
  276. ...res.config.$env?.[options.envName]
  277. };
  278. if (Object.keys(envConfig).length > 0) {
  279. res.config = defu(envConfig, res.config);
  280. }
  281. }
  282. res.meta = defu(res.sourceOptions.meta, res.config.$meta);
  283. delete res.config.$meta;
  284. if (res.sourceOptions.overrides) {
  285. res.config = defu(res.sourceOptions.overrides, res.config);
  286. }
  287. return res;
  288. }
  289. export { loadConfig, loadDotenv, setupDotenv };