pax_global_header00006660000000000000000000000064151513077400014515gustar00rootroot0000000000000052 comment=9a5cda3785913cce1eb5fa257e5994914b9ec599 cosmiconfig-9.0.1/000077500000000000000000000000001515130774000140245ustar00rootroot00000000000000cosmiconfig-9.0.1/.eslintignore000066400000000000000000000001161515130774000165250ustar00rootroot00000000000000.eslintrc.js node_modules coverage dist package-lock.json tsconfig.tsbuildinfocosmiconfig-9.0.1/.eslintrc.js000066400000000000000000000127771515130774000163010ustar00rootroot00000000000000'use strict'; // first node 8.x LTS release const supportedNodeVersion = '8.9'; const allExtensions = ['.js', '.jsx', '.ts', '.tsx', '.mjs', '.json']; module.exports = { parser: '@typescript-eslint/parser', parserOptions: { sourceType: 'module', project: './tsconfig.base.json', }, extends: [ 'eslint-config-davidtheclark-node', 'plugin:@typescript-eslint/eslint-recommended', 'plugin:@typescript-eslint/recommended', // This is making an annoying amount of unnecessary noise. // If anybody would like to turn it back on, PR welcome. // 'plugin:@typescript-eslint/recommended-requiring-type-checking', 'plugin:import/errors', 'plugin:import/warnings', 'plugin:import/typescript', 'plugin:vitest/recommended', 'prettier', ], plugins: ['vitest', '@typescript-eslint', 'import'], rules: { 'no-var': 'off', 'prefer-const': 'off', 'prefer-arrow-callback': 'off', 'func-names': ['error', 'always'], 'prefer-template': 'error', 'no-prototype-builtins': 'error', 'no-use-before-define': 'off', 'object-shorthand': [ 'error', 'always', { avoidExplicitReturnArrows: true }, ], /** * eslint-plugin-typescript */ '@typescript-eslint/explicit-function-return-type': [ 'error', { allowTypedFunctionExpressions: true, }, ], '@typescript-eslint/no-use-before-define': 'off', '@typescript-eslint/array-type': ['error', { default: 'generic' }], // requires type information rules '@typescript-eslint/await-thenable': 'error', '@typescript-eslint/no-floating-promises': 'error', '@typescript-eslint/no-for-in-array': 'error', '@typescript-eslint/no-unnecessary-qualifier': 'error', '@typescript-eslint/no-unnecessary-type-assertion': 'error', '@typescript-eslint/prefer-includes': 'error', '@typescript-eslint/prefer-namespace-keyword': 'error', // '@typescript-eslint/prefer-readonly': 'error', '@typescript-eslint/prefer-regexp-exec': 'error', '@typescript-eslint/prefer-string-starts-ends-with': 'error', '@typescript-eslint/promise-function-async': ['error', { allowAny: true }], '@typescript-eslint/require-array-sort-compare': 'error', '@typescript-eslint/restrict-plus-operands': 'error', // '@typescript-eslint/strict-boolean-expressions': 'error', '@typescript-eslint/unbound-method': 'error', // rules not in recommended '@typescript-eslint/ban-ts-ignore': 'off', // maybe enable? '@typescript-eslint/member-ordering': 'off', // maybe enable? '@typescript-eslint/no-empty-function': 'error', '@typescript-eslint/no-extraneous-class': 'error', '@typescript-eslint/no-require-imports': 'error', '@typescript-eslint/no-this-alias': 'error', '@typescript-eslint/no-useless-constructor': 'error', '@typescript-eslint/prefer-for-of': 'error', '@typescript-eslint/prefer-function-type': 'error', '@typescript-eslint/unified-signatures': 'error', /** * eslint-plugin-node */ 'node/no-unpublished-require': 'off', 'node/no-unsupported-features': 'off', 'node/no-unsupported-features/es-builtins': 'off', 'node/no-unsupported-features/es-syntax': 'off', 'node/no-unsupported-features/node-builtins': 'off', // Redundant with import/no-extraneous-dependencies 'node/no-extraneous-import': 'off', 'node/no-extraneous-require': 'off', // Redundant with import/no-unresolved 'node/no-missing-import': 'off', 'node/no-missing-require': 'off', /** * eslint-plugin-import */ 'import/no-default-export': 'error', 'import/no-named-export': 'off', 'import/prefer-default-export': 'off', /** * eslint-plugin-vitest */ 'vitest/consistent-test-it': ['error', { fn: 'test' }], 'vitest/valid-title': 'error', 'vitest/no-done-callback': 'error', // Many tests make assertions indirectly in a way the plugin // does not understand. 'vitest/expect-expect': 'off', // This is more performant. '@typescript-eslint/return-await': ['error', 'always'], // No bikeshedding. 'arrow-parens': ['error', 'always'], }, settings: { node: { convertPath: { 'src/**/*.{js,ts}': ['^src/(.+?)\\.(js|ts)$', 'dist/$1.js'], 'src/**/.*.{js,ts}': ['^src/(.+?)\\.(js|ts)$', 'dist/$1.js'], }, tryExtensions: allExtensions, }, 'import/resolver': { node: { extensions: allExtensions, }, typescript: {}, }, 'import/extensions': allExtensions, }, overrides: [ { files: ['*.test.{js,ts}', '.*.test.{js,ts}'], rules: { '@typescript-eslint/ban-ts-comment': 'off', '@typescript-eslint/no-var-requires': 'off', '@typescript-eslint/no-require-imports': 'off', '@typescript-eslint/explicit-function-return-type': 'off', '@typescript-eslint/no-explicit-any': 'off', }, }, { files: ['*.js', '.*.js'], excludedFiles: ['*/**', '*/.**'], parserOptions: { sourceType: 'script', }, rules: { strict: ['error', 'safe'], 'node/no-unpublished-require': 'off', 'node/no-unsupported-features/es-builtins': [ 'error', { version: supportedNodeVersion }, ], 'node/no-unsupported-features/es-syntax': [ 'error', { version: supportedNodeVersion }, ], 'node/no-unsupported-features/node-builtins': [ 'error', { version: supportedNodeVersion }, ], }, }, ], }; cosmiconfig-9.0.1/.github/000077500000000000000000000000001515130774000153645ustar00rootroot00000000000000cosmiconfig-9.0.1/.github/FUNDING.yml000066400000000000000000000000601515130774000171750ustar00rootroot00000000000000github: d-fischer custom: paypal.me/dfischerdev cosmiconfig-9.0.1/.github/workflows/000077500000000000000000000000001515130774000174215ustar00rootroot00000000000000cosmiconfig-9.0.1/.github/workflows/main.yml000066400000000000000000000016131515130774000210710ustar00rootroot00000000000000name: Node.js CI on: push: branches: - main pull_request: branches: - '**' env: FORCE_COLOR: 2 permissions: contents: read jobs: test: name: Test on Node.js ${{ matrix.node }} and ${{ matrix.os }} runs-on: ${{ matrix.os }} strategy: fail-fast: true matrix: node: [20, 22, 24, 25] os: [ubuntu-latest, windows-latest, macos-26] steps: - uses: actions/checkout@v3 - name: Set up Node.js uses: actions/setup-node@v5 with: node-version: ${{ matrix.node }} architecture: 'x64' - name: Install dependencies run: npm i - name: Test run: npm run test - name: Upload coverage to Codecov uses: codecov/codecov-action@v3 if: startsWith(matrix.os, 'ubuntu-latest') && matrix.node == 18 with: files: ./coverage/lcov.info cosmiconfig-9.0.1/.gitignore000066400000000000000000000001021515130774000160050ustar00rootroot00000000000000node_modules coverage dist package-lock.json tsconfig.tsbuildinfo cosmiconfig-9.0.1/.husky/000077500000000000000000000000001515130774000152455ustar00rootroot00000000000000cosmiconfig-9.0.1/.husky/pre-commit000077500000000000000000000001431515130774000172450ustar00rootroot00000000000000#!/usr/bin/env sh . "$(dirname -- "$0")/_/husky.sh" npx lint-staged --allow-empty && npm run test cosmiconfig-9.0.1/.husky/pre-push000077500000000000000000000001071515130774000167340ustar00rootroot00000000000000#!/usr/bin/env sh . "$(dirname -- "$0")/_/husky.sh" npm run check:all cosmiconfig-9.0.1/.prettierignore000066400000000000000000000001161515130774000170650ustar00rootroot00000000000000.eslintrc.js node_modules coverage dist package-lock.json tsconfig.tsbuildinfocosmiconfig-9.0.1/.prettierrc.cjs000066400000000000000000000001751515130774000167710ustar00rootroot00000000000000module.exports = { trailingComma: 'all', arrowParens: 'always', singleQuote: true, printWidth: 80, tabWidth: 2, }; cosmiconfig-9.0.1/CHANGELOG.md000066400000000000000000000310611515130774000156360ustar00rootroot00000000000000# Changelog ## 9.0.1 - Fixed a race condition where multiple instances existing simultaneously could cause cosmiconfig to fail to load TypeScript config files. - Fixed an issue on Windows where CWD being a short path (e.g. `C:\Users\USERNA~1`) would cause cosmiconfig to fail to load ESM config files. ## 9.0.0 - Added `searchStrategy` option: - The `none` value means that cosmiconfig does not traverse any directories upwards. - **Breaking change:** This is the default value if you don't pass a `stopDir` option, which means that cosmiconfig no longer traverses directories by default, and instead just looks in the current working directory. - If you want to achieve maximum backwards compatibility without adding an explicit `stopDir`, add the `searchStrategy: 'global'` option. - The `project` value means that cosmiconfig traverses upwards until it finds a `package.json` (or `.yaml`) file. - The `global` value means that cosmiconfig traverses upwards until the passed `stopDir`, or your home directory if no `stopDir` is given. - **Breaking change:** Meta config files (i.e. `config.js` and similar) are not looked for in the current working directory anymore. Instead, it looks in the `.config` subfolder. - **Breaking change:** When defining `searchPlaces` in a meta config file, the tool-defined `searchPlaces` are merged into this. Users may specify `mergeSearchPlaces: false` to disable this. - Added support for a special `$import` key which will import another configuration file - The imported file will act as a base file - all properties from that file will be applied to the configuration, but can be overridden by the importing file - For more information, read the [import section of the README](README.md#imports) - Added searching in OS conventional folders (XDG compatible on Linux, %APPDATA% on Windows, Library/Preferences on macOS) for `searchStrategy: 'global'` - Fixed crash when trying to load a file that is not readable due to file system permissions - Fixed wrong ERR_REQUIRE_ESM error being thrown when there is an issue loading an ESM file ## 8.3.6 - Ignore search place if accessing it causes ENOTDIR (i.e. if access of a subpath of a file is attempted) ## 8.3.5 - Fixed regression in transform option ## 8.3.4 - Fixed crash in older node versions ## 8.3.3 - Added back node 14 compat to package.json ## 8.3.2 - Fixed some issues with TypeScript config loading ## 8.3.1 - Fixed crash when `stopDir` was given but undefined ## 8.3.0 - Add support for TypeScript configuration files ## 8.2.0 - Add support for ECMAScript modules (ESM) to the [*asynchronous* API](./README.md#asynchronous-api). End users running Node versions that support ESM can provide `.mjs` files, or `.js` files whose nearest parent `package.json` file contains `"type": "module"`. - `${moduleName}rc.mjs` and `${moduleName}.config.mjs` are included in the default `searchPlaces` of the asynchronous API. - The [synchronous API](./README.md#synchronous-api) does not support ECMAScript modules, so does not look for `.mjs` files. - To learn more, read ["Loading JS modules"](./README.md#loading-js-modules). ## 8.1.3 - Fixed: existence of meta config breaking default loaders ## 8.1.2 - Fixed: generation of TypeScript types going to the wrong output path ## 8.1.1 - Fixed: meta config overriding original options completely (now merges correctly) ## 8.1.0 - Added: always look at `.config.{yml,yaml,json,js,cjs}` file to configure cosmiconfig itself, and look for tool configuration in it using `packageProp` (similar to package.json) - For more info on this, look at the [end user configuration section of the README](README.md#usage-for-end-users) ## 8.0.0 **No major breaking changes!** We dropped support for Node 10 and 12 -- which you're probably not using. And we swapped out the YAML parser -- which you probably won't notice. - **Breaking change:** Drop support for Node 10 and 12. - **Breaking change:** Use npm package [js-yaml](https://www.npmjs.com/package/js-yaml) to parse YAML instead of npm package [yaml](https://www.npmjs.com/package/yaml). - Added: Loader errors now include the path of the file that was tried to be loaded. ## 7.1.0 - Added: Additional default `searchPlaces` within a .config subdirectory (without leading dot in the file name) ## 7.0.1 - Fixed: If there was a directory that had the same name as a search place (e.g. "package.json"), we would try to read it as a file, which would cause an exception. ## 7.0.0 - **Breaking change:** Add `${moduleName}rc.cjs` and `${moduleName}.config.cjs` to the default `searchPlaces`, to support users of `"type": "module"` in recent versions of Node. - **Breaking change:** Drop support for Node 8. Now requires Node 10+. ## 6.0.0 - **Breaking change:** The package now has named exports. See examples below. - **Breaking change:** Separate async and sync APIs, accessible from different named exports. If you used `explorer.searchSync()` or `explorer.loadSync()`, you'll now create a sync explorer with `cosmiconfigSync()`, then use `explorerSync.search()` and `explorerSync.load()`. ```js // OLD: cosmiconfig v5 import cosmiconfig from 'cosmiconfig'; const explorer = cosmiconfig('example'); const searchAsyncResult = await explorer.search(); const loadAsyncResult = await explorer.load('./file/to/load'); const searchSyncResult = explorer.searchSync(); const loadSyncResult = explorer.loadSync('./file/to/load'); // NEW: cosmiconfig v6 import { cosmiconfig, cosmiconfigSync } from 'cosmiconfig'; const explorer = cosmiconfig('example'); const searchAsyncResult = await explorer.search(); const loadAsyncResult = await explorer.load('./file/to/load'); const explorerSync = cosmiconfigSync('example'); const searchSyncResult = explorerSync.search(); const loadSyncResult = explorerSync.load('./file/to/load'); ``` - **Breaking change:** Remove support for Node 4 and 6. Requires Node 8+. - **Breaking change:** Use npm package [yaml](https://www.npmjs.com/package/yaml) to parse YAML instead of npm package [js-yaml](https://www.npmjs.com/package/js-yaml). - **Breaking change:** Remove `cosmiconfig.loaders` and add named export `defaultLoaders` that exports the default loaders used for each extension. ```js import { defaultLoaders } from 'cosmiconfig'; console.log(Object.entries(defaultLoaders)); // [ // [ '.js', [Function: loadJs] ], // [ '.json', [Function: loadJson] ], // [ '.yaml', [Function: loadYaml] ], // [ '.yml', [Function: loadYaml] ], // [ 'noExt', [Function: loadYaml] ] // ] ``` - Migrate from Flowtype to Typescript. - Lazy load all default loaders. ## 5.2.1 - Chore: Upgrade `js-yaml` to avoid npm audit warning. ## 5.2.0 - Added: `packageProp` values can be arrays of strings, to allow for property names that include periods. (This was possible before, but not documented or deliberately supported.) - Chore: Replaced the `lodash.get` dependency with a locally defined function. - Chore: Upgrade `js-yaml` to avoid npm audit warning. ## 5.1.0 - Added: `packageProp` values can include periods to describe paths to nested objects within `package.json`. ## 5.0.7 - Fixed: JS loader bypasses Node's `require` cache, fixing a bug where updates to `.js` config files would not load even when Cosmiconfig was told not to cache. ## 5.0.6 - Fixed: Better error message if the end user tries an extension Cosmiconfig is not configured to understand. ## 5.0.5 - Fixed: `load` and `loadSync` work with paths relative to `process.cwd()`. ## 5.0.4 - Fixed: `rc` files with `.js` extensions included in default `searchPlaces`. ## 5.0.3 - Docs: Minor corrections to documentation. *Released to update package documentation on npm*. ## 5.0.2 - Fixed: Allow `searchSync` and `loadSync` to load JS configuration files whose export is a Promise. ## 5.0.1 The API has been completely revamped to increase clarity and enable a very wide range of new usage. **Please read the readme for all the details.** While the defaults remain just as useful as before — and you can still pass no options at all — now you can also do all kinds of wild and crazy things. - The `loaders` option allows you specify custom functions to derive config objects from files. Your loader functions could parse ES2015 modules or TypeScript, JSON5, even INI or XML. Whatever suits you. - The `searchPlaces` option allows you to specify exactly where cosmiconfig looks within each directory it searches. - The combination of `loaders` and `searchPlaces` means that you should be able to load pretty much any kind of configuration file you want, from wherever you want it to look. Additionally, the overloaded `load()` function has been split up into several clear and focused functions: - `search()` now searches up the directory tree, and `load()` loads a configuration file that you don't need to search for. - The `sync` option has been replaced with separate synchronous functions: `searchSync()` and `loadSync()`. - `clearFileCache()` and `clearDirectoryCache()` have been renamed to `clearLoadCache()` and `clearSearchPath()` respectively. More details: - The default JS loader uses `require`, instead of `require-from-string`. So you *could* use `require` hooks to control the loading of JS files (e.g. pass them through esm or Babel). In most cases it is probably preferable to use a custom loader. - The options `rc`, `js`, and `rcExtensions` have all been removed. You can accomplish the same and more with `searchPlaces`. - The default `searchPlaces` include `rc` files with extensions, e.g. `.thingrc.json`, `.thingrc.yaml`, `.thingrc.yml`. This is the equivalent of switching the default value of the old `rcExtensions` option to `true`. - The option `rcStrictJson` has been removed. To get the same effect, you can specify `noExt: cosmiconfig.loadJson` in your `loaders` object. - `packageProp` no longer accepts `false`. If you don't want to look in `package.json`, write a `searchPlaces` array that does not include it. - By default, empty files are ignored by `search()`. The new option `ignoreEmptySearchPlaces` allows you to load them, instead, in case you want to do something with empty files. - The option `configPath` has been removed. Just pass your filepaths directory to `load()`. - Removed the `format` option. Formats are now all handled via the file extensions specified in `loaders`. (If you're wondering with happened to 5.0.0 ... it was a silly publishing mistake.) ## 4.0.0 - Licensing improvement: updated `parse-json` from `3.0.0` to `4.0.0`(see [sindresorhus/parse-json#12][parse-json-pr-12]). - Changed: error message format for `JSON` parse errors(see [#101][pr-101]). If you were relying on the format of JSON-parsing error messages, this will be a breaking change for you. - Changed: set default for `searchPath` as `process.cwd()` in `explorer.load`. ## 3.1.0 - Added: infer format based on filePath ## 3.0.1 - Fixed: memory leak due to bug in `require-from-string`. - Added: for JSON files, append position to end of error message. ## 3.0.0 - Removed: support for loading config path using the `--config` flag. cosmiconfig will not parse command line arguments. Your application can parse command line arguments and pass them to cosmiconfig. - Removed: `argv` config option. - Removed: support for Node versions < 4. - Added: `sync` option. - Fixed: Throw a clear error on getting empty config file. - Fixed: when a `options.configPath` is `package.json`, return the package prop, not the entire JSON file. ## 2.2.2 - Fixed: `options.configPath` and `--config` flag are respected. ## 2.2.0, 2.2.1 - 2.2.0 included a number of improvements but somehow broke stylelint. The changes were reverted in 2.2.1, to be restored later. ## 2.1.3 - Licensing improvement: switched from `json-parse-helpfulerror` to `parse-json`. ## 2.1.2 - Fixed: bug where an `ENOENT` error would be thrown is `searchPath` referenced a non-existent file. - Fixed: JSON parsing errors in Node v7. ## 2.1.1 - Fixed: swapped `graceful-fs` for regular `fs`, fixing a garbage collection problem. ## 2.1.0 - Added: Node 0.12 support. ## 2.0.2 - Fixed: Node version specified in `package.json`. ## 2.0.1 - Fixed: no more infinite loop in Windows. ## 2.0.0 - Changed: module now creates cosmiconfig instances with `load` methods (see README). - Added: caching (enabled by the change above). - Removed: support for Node versions <4. ## 1.1.0 - Add `rcExtensions` option. ## 1.0.2 - Fix handling of `require()`'s within JS module configs. ## 1.0.1 - Switch Promise implementation to pinkie-promise. ## 1.0.0 - Initial release. [parse-json-pr-12]: https://github.com/sindresorhus/parse-json/pull/12 [pr-101]: https://github.com/cosmiconfig/cosmiconfig/pull/101 cosmiconfig-9.0.1/CODE_OF_CONDUCT.md000066400000000000000000000126111515130774000166240ustar00rootroot00000000000000# Contributor Covenant Code of Conduct ## Our Pledge We as members, contributors, and leaders pledge to make participation in our community a harassment-free experience for everyone, regardless of age, body size, visible or invisible disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, caste, color, religion, or sexual identity and orientation. We pledge to act and interact in ways that contribute to an open, welcoming, diverse, inclusive, and healthy community. ## Our Standards Examples of behavior that contributes to a positive environment for our community include: - Demonstrating empathy and kindness toward other people - Being respectful of differing opinions, viewpoints, and experiences - Giving and gracefully accepting constructive feedback - Accepting responsibility and apologizing to those affected by our mistakes, and learning from the experience - Focusing on what is best not just for us as individuals, but for the overall community Examples of unacceptable behavior include: - The use of sexualized language or imagery, and sexual attention or advances of any kind - Trolling, insulting or derogatory comments, and personal or political attacks - Public or private harassment - Publishing others' private information, such as a physical or email address, without their explicit permission - Other conduct which could reasonably be considered inappropriate in a professional setting ## Enforcement Responsibilities Community leaders are responsible for clarifying and enforcing our standards of acceptable behavior and will take appropriate and fair corrective action in response to any behavior that they deem inappropriate, threatening, offensive, or harmful. Community leaders have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, and will communicate reasons for moderation decisions when appropriate. ## Scope This Code of Conduct applies within all community spaces, and also applies when an individual is officially representing the community in public spaces. Examples of representing our community include using an official e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. ## Enforcement Instances of abusive, harassing, or otherwise unacceptable behavior may be reported to the community leaders responsible for enforcement at . All complaints will be reviewed and investigated promptly and fairly. All community leaders are obligated to respect the privacy and security of the reporter of any incident. ## Enforcement Guidelines Community leaders will follow these Community Impact Guidelines in determining the consequences for any action they deem in violation of this Code of Conduct: ### 1. Correction **Community Impact**: Use of inappropriate language or other behavior deemed unprofessional or unwelcome in the community. **Consequence**: A private, written warning from community leaders, providing clarity around the nature of the violation and an explanation of why the behavior was inappropriate. A public apology may be requested. ### 2. Warning **Community Impact**: A violation through a single incident or series of actions. **Consequence**: A warning with consequences for continued behavior. No interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, for a specified period of time. This includes avoiding interactions in community spaces as well as external channels like social media. Violating these terms may lead to a temporary or permanent ban. ### 3. Temporary Ban **Community Impact**: A serious violation of community standards, including sustained inappropriate behavior. **Consequence**: A temporary ban from any sort of interaction or public communication with the community for a specified period of time. No public or private interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, is allowed during this period. Violating these terms may lead to a permanent ban. ### 4. Permanent Ban **Community Impact**: Demonstrating a pattern of violation of community standards, including sustained inappropriate behavior, harassment of an individual, or aggression toward or disparagement of classes of individuals. **Consequence**: A permanent ban from any sort of public interaction within the community. ## Attribution This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 2.1, available at [https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1]. Community Impact Guidelines were inspired by [Mozilla's code of conduct enforcement ladder][Mozilla CoC]. For answers to common questions about this code of conduct, see the FAQ at [https://www.contributor-covenant.org/faq][FAQ]. Translations are available at [https://www.contributor-covenant.org/translations][translations]. [homepage]: https://www.contributor-covenant.org [v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html [Mozilla CoC]: https://github.com/mozilla/diversity [FAQ]: https://www.contributor-covenant.org/faq [translations]: https://www.contributor-covenant.org/translations cosmiconfig-9.0.1/LICENSE000066400000000000000000000020671515130774000150360ustar00rootroot00000000000000The MIT License (MIT) Copyright (c) 2015 David Clark Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. cosmiconfig-9.0.1/README.md000066400000000000000000000657511515130774000153210ustar00rootroot00000000000000# cosmiconfig [![codecov](https://codecov.io/gh/cosmiconfig/cosmiconfig/branch/main/graph/badge.svg)](https://codecov.io/gh/cosmiconfig/cosmiconfig) Cosmiconfig searches for and loads configuration for your program. It features smart defaults based on conventional expectations in the JavaScript ecosystem. But it's also flexible enough to search wherever you'd like to search, and load whatever you'd like to load. By default, Cosmiconfig will check the current directory for the following: - a `package.json` property - a JSON or YAML, extensionless "rc file" - an "rc file" with the extensions `.json`, `.yaml`, `.yml`, `.js`, `.ts`, `.mjs`, or `.cjs` - any of the above two inside a `.config` subdirectory - a `.config.js`, `.config.ts`, `.config.mjs`, or `.config.cjs` file For example, if your module's name is "myapp", cosmiconfig will search up the directory tree for configuration in the following places: - a `myapp` property in `package.json` - a `.myapprc` file in JSON or YAML format - a `.myapprc.json`, `.myapprc.yaml`, `.myapprc.yml`, `.myapprc.js`, `.myapprc.ts`, `.myapprc.mjs`, or `.myapprc.cjs` file - a `myapprc`, `myapprc.json`, `myapprc.yaml`, `myapprc.yml`, `myapprc.js`, `myapprc.ts`, `myapprc.mjs`, or `myapprc.cjs` file inside a `.config` subdirectory - a `myapp.config.js`, `myapp.config.ts`, `myapp.config.mjs`, or `myapp.config.cjs` file Optionally, you can tell it to search up the directory tree using [search strategies], checking each of these places in each directory, until it finds some acceptable configuration (or hits the home directory). ## Table of contents - [Installation](#installation) - [Usage for tooling developers](#usage-for-tooling-developers) - [Result](#result) - [Asynchronous API](#asynchronous-api) - [cosmiconfig()](#cosmiconfig-1) - [explorer.search()](#explorersearch) - [explorer.load()](#explorerload) - [explorer.clearLoadCache()](#explorerclearloadcache) - [explorer.clearSearchCache()](#explorerclearsearchcache) - [explorer.clearCaches()](#explorerclearcaches) - [Synchronous API](#synchronous-api) - [cosmiconfigSync()](#cosmiconfigsync) - [explorerSync.search()](#explorersyncsearch) - [explorerSync.load()](#explorersyncload) - [explorerSync.clearLoadCache()](#explorersyncclearloadcache) - [explorerSync.clearSearchCache()](#explorersyncclearsearchcache) - [explorerSync.clearCaches()](#explorersyncclearcaches) - [cosmiconfigOptions](#cosmiconfigoptions) - [searchStrategy](#searchstrategy) - [searchPlaces](#searchplaces) - [loaders](#loaders) - [packageProp](#packageprop) - [stopDir](#stopdir) - [cache](#cache) - [transform](#transform) - [ignoreEmptySearchPlaces](#ignoreemptysearchplaces) - [Loading JS modules](#loading-js-modules) - [Caching](#caching) - [Differences from rc](#differences-from-rc) - [Usage for end users](#usage-for-end-users) - [Imports](#imports) - [Contributing & Development](#contributing--development) ## Installation ``` npm install cosmiconfig ``` Tested in Node 14+. ## Usage for tooling developers *If you are an end user (i.e. a user of a tool that uses cosmiconfig, like `prettier` or `stylelint`), you can skip down to [the end user section](#usage-for-end-users).* Create a Cosmiconfig explorer, then either `search` for or directly `load` a configuration file. ```js const { cosmiconfig, cosmiconfigSync } = require('cosmiconfig'); // ... const explorer = cosmiconfig(moduleName); // Search for a configuration by walking up directories. // See documentation for search, below. explorer.search() .then((result) => { // result.config is the parsed configuration object. // result.filepath is the path to the config file that was found. // result.isEmpty is true if there was nothing to parse in the config file. }) .catch((error) => { // Do something constructive. }); // Load a configuration directly when you know where it should be. // The result object is the same as for search. // See documentation for load, below. explorer.load(pathToConfig).then(/* ... */); // You can also search and load synchronously. const explorerSync = cosmiconfigSync(moduleName); const searchedFor = explorerSync.search(); const loaded = explorerSync.load(pathToConfig); ``` ## Result The result object you get from `search` or `load` has the following properties: - **config:** The parsed configuration object. `undefined` if the file is empty. - **filepath:** The path to the configuration file that was found. - **isEmpty:** `true` if the configuration file is empty. This property will not be present if the configuration file is not empty. ## Asynchronous API ### cosmiconfig() ```js const { cosmiconfig } = require('cosmiconfig'); const explorer = cosmiconfig(moduleName, /* optional */ cosmiconfigOptions) ``` Creates a cosmiconfig instance ("explorer") configured according to the arguments, and initializes its caches. #### moduleName Type: `string`. **Required.** Your module name. This is used to create the default [`searchPlaces`] and [`packageProp`]. If your [`searchPlaces`] value will include files, as it does by default (e.g. `${moduleName}rc`), your `moduleName` must consist of characters allowed in filenames. That means you should not copy scoped package names, such as `@my-org/my-package`, directly into `moduleName`. **[`cosmiconfigOptions`] are documented below.** You may not need them, and should first read about the functions you'll use. ### explorer.search() ```js explorer.search([searchFrom]).then(result => { /* ... */ }) ``` Searches for a configuration file. Returns a Promise that resolves with a [result] or with `null`, if no configuration file is found. You can do the same thing synchronously with [`explorerSync.search()`]. Let's say your module name is `goldengrahams` so you initialized with `const explorer = cosmiconfig('goldengrahams');`. Here's how your default [`search()`] will work: - Starting from `process.cwd()` (or some other directory defined by the `searchFrom` argument to [`search()`]), look for configuration objects in the following places: 1. A `goldengrahams` property in a `package.json` file. 2. A `.goldengrahamsrc` file with JSON or YAML syntax. 3. A `.goldengrahamsrc.json`, `.goldengrahamsrc.yaml`, `.goldengrahamsrc.yml`, `.goldengrahamsrc.js`, `.goldengrahamsrc.ts`, `.goldengrahamsrc.mjs`, or `.goldengrahamsrc.cjs` file. (To learn more about how JS files are loaded, see ["Loading JS modules"].) 4. A `goldengrahamsrc`, `goldengrahamsrc.json`, `goldengrahamsrc.yaml`, `goldengrahamsrc.yml`, `goldengrahamsrc.js`, `goldengrahamsrc.ts`, `goldengrahamsrc.mjs`, or `goldengrahamsrc.cjs` file in the `.config` subdirectory. 5. A `goldengrahams.config.js`, `goldengrahams.config.ts`, `goldengrahams.config.mjs`, or `goldengrahams.config.cjs` file. (To learn more about how JS files are loaded, see ["Loading JS modules"].) - If none of those searches reveal a configuration object, continue depending on the current search strategy: - If it's `none` (which is the default if you don't specify a [`stopDir`] option), stop here and return/resolve with `null`. - If it's `global` (which is the default if you specify a [`stopDir`] option), move up one directory level and try again, recursing until arriving at the configured [`stopDir`] option, which defaults to the user's home directory. - After arriving at the [`stopDir`], the global configuration directory (as defined by [`env-paths`] without prefix) is also checked, looking at the files `config`, `config.json`, `config.yaml`, `config.yml`, `config.js`, `config.ts`, `config.cjs`, and `config.mjs` in the directory `~/.config/goldengrahams/` (on Linux; see [`env-paths`] documentation for other OSs). - If it's `project`, check whether a `package.json` file is present in the current directory, and if not, move up one directory level and try again, recursing until there is one. - If at any point a parsable configuration is found, the [`search()`] Promise resolves with its [result] \(or, with [`explorerSync.search()`], the [result] is returned). - If no configuration object is found, the [`search()`] Promise resolves with `null` (or, with [`explorerSync.search()`], `null` is returned). - If a configuration object is found *but is malformed* (causing a parsing error), the [`search()`] Promise rejects with that error (so you should `.catch()` it). (Or, with [`explorerSync.search()`], the error is thrown.) **If you know exactly where your configuration file should be, you can use [`load()`], instead.** **The search process is highly customizable.** Use the cosmiconfig options [`searchPlaces`] and [`loaders`] to precisely define where you want to look for configurations and how you want to load them. #### searchFrom Type: `string`. Default: `process.cwd()`. A filename. [`search()`] will start its search here. If the value is a directory, that's where the search starts. If it's a file, the search starts in that file's directory. ### explorer.load() ```js explorer.load(loadPath).then(result => { /* ... */ }) ``` Loads a configuration file. Returns a Promise that resolves with a [result] or rejects with an error (if the file does not exist or cannot be loaded). Use `load` if you already know where the configuration file is and you just need to load it. ```js explorer.load('load/this/file.json'); // Tries to load load/this/file.json. ``` If you load a `package.json` file, the result will be derived from whatever property is specified as your [`packageProp`]. `package.yaml` will work as well if you specify these file names in your [`searchPlaces`]. You can do the same thing synchronously with [`explorerSync.load()`]. ### explorer.clearLoadCache() Clears the cache used in [`load()`]. ### explorer.clearSearchCache() Clears the cache used in [`search()`]. ### explorer.clearCaches() Performs both [`clearLoadCache()`] and [`clearSearchCache()`]. ## Synchronous API ### cosmiconfigSync() ```js const { cosmiconfigSync } = require('cosmiconfig'); const explorerSync = cosmiconfigSync(moduleName, /* optional */ cosmiconfigOptions) ``` Creates a *synchronous* cosmiconfig instance ("explorerSync") configured according to the arguments, and initializes its caches. See [`cosmiconfig()`](#cosmiconfig-1). ### explorerSync.search() ```js const result = explorerSync.search([searchFrom]); ``` Synchronous version of [`explorer.search()`]. Returns a [result] or `null`. ### explorerSync.load() ```js const result = explorerSync.load(loadPath); ``` Synchronous version of [`explorer.load()`]. Returns a [result]. ### explorerSync.clearLoadCache() Clears the cache used in [`load()`]. ### explorerSync.clearSearchCache() Clears the cache used in [`search()`]. ### explorerSync.clearCaches() Performs both [`clearLoadCache()`] and [`clearSearchCache()`]. ## cosmiconfigOptions Type: `Object`. Possible options are documented below. ### searchStrategy Type: `string` Default: `global` if [`stopDir`] is specified, `none` otherwise. The strategy that should be used to determine which directories to check for configuration files. - `none`: Only checks in the current working directory. - `project`: Starts in the current working directory, traversing upwards until a `package.{json,yaml}` file is found. - `global`: Starts in the current working directory, traversing upwards until the configured [`stopDir`] (or the current user's home directory if none is given). Then, if no configuration is found, also look in the operating system's default configuration directory (according to [`env-paths`] without prefix), where a different set of file names is checked: ```js [ `config`, `config.json`, `config.yaml`, `config.yml`, `config.js`, `config.ts`, `config.cjs`, `config.mjs` ] ``` ### searchPlaces Type: `Array`. Default: See below. An array of places that [`search()`] will check in each directory as it moves up the directory tree. Each place is relative to the directory being searched, and the places are checked in the specified order. **Default `searchPlaces`:** For the [asynchronous API](#asynchronous-api), these are the default `searchPlaces`: ```js [ 'package.json', `.${moduleName}rc`, `.${moduleName}rc.json`, `.${moduleName}rc.yaml`, `.${moduleName}rc.yml`, `.${moduleName}rc.js`, `.${moduleName}rc.ts`, `.${moduleName}rc.mjs`, `.${moduleName}rc.cjs`, `.config/${moduleName}rc`, `.config/${moduleName}rc.json`, `.config/${moduleName}rc.yaml`, `.config/${moduleName}rc.yml`, `.config/${moduleName}rc.js`, `.config/${moduleName}rc.ts`, `.config/${moduleName}rc.mjs`, `.config/${moduleName}rc.cjs`, `${moduleName}.config.js`, `${moduleName}.config.ts`, `${moduleName}.config.mjs`, `${moduleName}.config.cjs`, ]; ``` For the [synchronous API](#synchronous-api), the only difference is that `.mjs` files are not included. See ["Loading JS modules"] for more information. Create your own array to search more, fewer, or altogether different places. Every item in `searchPlaces` needs to have a loader in [`loaders`] that corresponds to its extension. (Common extensions are covered by default loaders.) Read more about [`loaders`] below. `package.json` is a special value: When it is included in `searchPlaces`, Cosmiconfig will always parse it as JSON and load a property within it, not the whole file. That property is defined with the [`packageProp`] option, and defaults to your module name. `package.yaml` (used by pnpm) works the same way. Examples, with a module named `porgy`: ```js // Disallow extensions on rc files: ['package.json', '.porgyrc', 'porgy.config.js'] ``` ```js // Limit the options dramatically: ['package.json', '.porgyrc'] ``` ```js // Maybe you want to look for a wide variety of JS flavors: [ 'porgy.config.js', 'porgy.config.mjs', 'porgy.config.ts', 'porgy.config.coffee' ] // ^^ You will need to designate a custom loader to tell // Cosmiconfig how to handle `.coffee` files. ``` ```js // Look within a .config/ subdirectory of every searched directory: [ 'package.json', '.porgyrc', '.config/.porgyrc', '.porgyrc.json', '.config/.porgyrc.json' ] ``` ### loaders Type: `Object`. Default: See below. An object that maps extensions to the loader functions responsible for loading and parsing files with those extensions. Cosmiconfig exposes its default loaders on the named export `defaultLoaders` and `defaultLoadersSync`. **Default `loaders`:** ```js const { defaultLoaders, defaultLoadersSync } = require('cosmiconfig'); console.log(Object.entries(defaultLoaders)); // [ // [ '.mjs', [Function: loadJs] ], // [ '.cjs', [Function: loadJs] ], // [ '.js', [Function: loadJs] ], // [ '.ts', [Function: loadTs] ], // [ '.json', [Function: loadJson] ], // [ '.yaml', [Function: loadYaml] ], // [ '.yml', [Function: loadYaml] ], // [ 'noExt', [Function: loadYaml] ] // ] console.log(Object.entries(defaultLoadersSync)); // [ // [ '.cjs', [Function: loadJsSync] ], // [ '.js', [Function: loadJsSync] ], // [ '.ts', [Function: loadTsSync] ], // [ '.json', [Function: loadJson] ], // [ '.yaml', [Function: loadYaml] ], // [ '.yml', [Function: loadYaml] ], // [ 'noExt', [Function: loadYaml] ] // ] ``` (YAML is a superset of JSON; which means YAML parsers can parse JSON; which is how extensionless files can be either YAML *or* JSON with only one parser.) **If you provide a `loaders` object, your object will be *merged* with the defaults.** So you can override one or two without having to override them all. **Keys in `loaders`** are extensions (starting with a period), or `noExt` to specify the loader for files *without* extensions, like `.myapprc`. **Values in `loaders`** are a loader function (described below) whose values are loader functions. **The most common use case for custom loaders value is to load extensionless `rc` files as strict JSON**, instead of JSON *or* YAML (the default). To accomplish that, provide the following `loaders` value: ```js { noExt: defaultLoaders['.json']; } ``` If you want to load files that are not handled by the loader functions Cosmiconfig exposes, you can write a custom loader function or use one from NPM if it exists. **Use cases for custom loader function:** - Allow configuration syntaxes that aren't handled by Cosmiconfig's defaults, like JSON5, INI, or XML. - Parse JS files with Babel before deriving the configuration. **Custom loader functions** have the following signature: ```ts // Sync type SyncLoader = (filepath: string, content: string) => Object | null // Async type AsyncLoader = (filepath: string, content: string) => Object | null | Promise ``` Cosmiconfig reads the file when it checks whether the file exists, so it will provide you with both the file's path and its content. Do whatever you need to, and return either a configuration object or `null` (or, for async-only loaders, a Promise that resolves with one of those). `null` indicates that no real configuration was found and the search should continue. A few things to note: - If you use a custom loader, be aware of whether it's sync or async: you cannot use async customer loaders with the sync API ([`cosmiconfigSync()`]). - **Special JS syntax can also be handled by using a `require` hook**, because `defaultLoaders['.js']` just uses `require`. Whether you use custom loaders or a `require` hook is up to you. Examples: ```js // Allow JSON5 syntax: cosmiconfig('foo', { loaders: { '.json': json5Loader } }); // Allow a special configuration syntax of your own creation: cosmiconfig('foo', { loaders: { '.special': specialLoader } }); // Allow many flavors of JS, using custom loaders: cosmiconfig('foo', { loaders: { '.coffee': coffeeScriptLoader } }); // Allow many flavors of JS but rely on require hooks: cosmiconfig('foo', { loaders: { '.coffee': defaultLoaders['.js'] } }); ``` ### packageProp Type: `string | Array`. Default: `` `${moduleName}` ``. Name of the property in `package.json` (or `package.yaml`) to look for. Use a period-delimited string or an array of strings to describe a path to nested properties. For example, the value `'configs.myPackage'` or `['configs', 'myPackage']` will get you the `"myPackage"` value in a `package.json` like this: ```json { "configs": { "myPackage": {"option": "value"} } } ``` If nested property names within the path include periods, you need to use an array of strings. For example, the value `['configs', 'foo.bar', 'baz']` will get you the `"baz"` value in a `package.json` like this: ```json { "configs": { "foo.bar": { "baz": {"option": "value"} } } } ``` If a string includes period but corresponds to a top-level property name, it will not be interpreted as a period-delimited path. For example, the value `'one.two'` will get you the `"three"` value in a `package.json` like this: ```json { "one.two": "three", "one": { "two": "four" } } ``` ### stopDir Type: `string`. Default: Absolute path to your home directory. Directory where the search will stop. ### cache Type: `boolean`. Default: `true`. If `false`, no caches will be used. Read more about ["Caching"](#caching) below. ### transform Type: `(Result) => Promise | Result`. A function that transforms the parsed configuration. Receives the [result]. If using [`search()`] or [`load()`] \(which are async), the transform function can return the transformed result or return a Promise that resolves with the transformed result. If using `cosmiconfigSync`, [`search()`] or [`load()`], the function must be synchronous and return the transformed result. The reason you might use this option — instead of simply applying your transform function some other way — is that *the transformed result will be cached*. If your transformation involves additional filesystem I/O or other potentially slow processing, you can use this option to avoid repeating those steps every time a given configuration is searched or loaded. ### ignoreEmptySearchPlaces Type: `boolean`. Default: `true`. By default, if [`search()`] encounters an empty file (containing nothing but whitespace) in one of the [`searchPlaces`], it will ignore the empty file and move on. If you'd like to load empty configuration files, instead, set this option to `false`. Why might you want to load empty configuration files? If you want to throw an error, or if an empty configuration file means something to your program. ## Loading JS modules Your end users can provide JS configuration files as ECMAScript modules (ESM) under the following conditions: - You (the cosmiconfig user) use cosmiconfig's [asynchronous API](#asynchronous-api). - Your end user runs a version of Node that supports ESM ([>=12.17.0](https://nodejs.org/en/blog/release/v12.17.0/), or earlier with the `--experimental-modules` flag). - Your end user provides an `.mjs` configuration file, or a `.js` file whose nearest parent `package.json` file contains `"type": "module"`. (See [Node's method for determining a file's module system](https://nodejs.org/api/packages.html#packages_determining_module_system).) With cosmiconfig's [asynchronous API](#asynchronous-api), the default [`searchPlaces`] include `.js`, `.ts`, `.mjs`, and `.cjs` files. Cosmiconfig loads all these file types with the [dynamic `import` function](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/import#dynamic_imports). With the [synchronous API](#synchronous-api), JS configuration files are always treated as CommonJS, and `.mjs` files are ignored, because there is no synchronous API for the dynamic `import` function. ## Caching As of v2, cosmiconfig uses caching to reduce the need for repetitious reading of the filesystem or expensive transforms. Every new cosmiconfig instance (created with `cosmiconfig()`) has its own caches. To avoid or work around caching, you can do the following: - Set the `cosmiconfig` option [`cache`] to `false`. - Use the cache-clearing methods [`clearLoadCache()`], [`clearSearchCache()`], and [`clearCaches()`]. - Create separate instances of cosmiconfig (separate "explorers"). ## Differences from [rc](https://github.com/dominictarr/rc) [rc](https://github.com/dominictarr/rc) serves its focused purpose well. cosmiconfig differs in a few key ways — making it more useful for some projects, less useful for others: - Looks for configuration in some different places: in a `package.json` property, an rc file, a `.config.js` file, and rc files with extensions. - Built-in support for JSON, YAML, and CommonJS formats. - Stops at the first configuration found, instead of finding all that can be found up the directory tree and merging them automatically. - Options. - Asynchronous by default (though can be run synchronously). ## Usage for end users When configuring a tool, you can use multiple file formats and put these in multiple places. Usually, a tool would mention this in its own README file, but by default, these are the following places, where `{NAME}` represents the name of the tool: ``` package.json .{NAME}rc .{NAME}rc.json .{NAME}rc.yaml .{NAME}rc.yml .{NAME}rc.js .{NAME}rc.ts .{NAME}rc.cjs .config/{NAME}rc .config/{NAME}rc.json .config/{NAME}rc.yaml .config/{NAME}rc.yml .config/{NAME}rc.js .config/{NAME}rc.ts .config/{NAME}rc.mjs .config/{NAME}rc.cjs {NAME}.config.js {NAME}.config.ts {NAME}.config.mjs {NAME}.config.cjs ``` The contents of these files are defined by the tool. For example, you can configure prettier to enforce semicolons at the end of the line using a file named `.config/prettierrc.yml`: ```yaml semi: true ``` Additionally, you have the option to put a property named after the tool in your `package.json` file, with the contents of that property being the same as the file contents. To use the same example as above: ```json { "name": "your-project", "dependencies": {}, "prettier": { "semi": true } } ``` This has the advantage that you can put the configuration of all tools (at least the ones that use cosmiconfig) in one file. You can also add a `cosmiconfig` key within your `package.json` file or create one of the following files to configure `cosmiconfig` itself: ``` .config/config.json .config/config.yaml .config/config.yml .config/config.js .config/config.ts .config/config.cjs ``` The following properties are currently actively supported in these places: ```yaml cosmiconfig: # adds places where configuration files are being searched searchPlaces: - .config/{name}.yml # to enforce a custom naming convention and format, don't merge the above with the tool-defined search places # (`true` is the default setting) mergeSearchPlaces: false ``` > **Note:** technically, you can overwrite all options described in [cosmiconfigOptions](#cosmiconfigoptions) here, > but everything not listed above should be used at your own risk, as it has not been tested explicitly. > The only exceptions to this are the `loaders` property, which is explicitly not supported at this time, > and the `searchStrategy` property, which is intentionally disallowed. You can also add more root properties outside the `cosmiconfig` property to configure your tools, entirely eliminating the need to look for additional configuration files: ```yaml cosmiconfig: searchPlaces: [] prettier: semi: true ``` ### Imports Wherever you put your configuration (the package.json file, a root config file or a package-specific config file), you can use the special `$import` key to import another file as a base. For example, you can import from an npm package (in this example, `@foocorp/config`). `.prettierrc.base.yml` in said npm package could define some company-wide defaults: ```yaml printWidth: 120 semi: true tabWidth: 2 ``` And then, the `.prettierrc.yml` file in the project itself would just reference that file, optionally overriding the defaults with project-specific settings: ```yaml $import: node_modules/@foocorp/config/.prettierrc.base.yml # we want more space! printWidth: 200 ``` It is possible to import multiple base files by specifying an array of paths, which will be processed in declaration order; that means that the last entry will win if there are conflicting properties. It is also possible to import file formats other than the importing format as long as they are supported by the loaders specified by the developer of the tool you're configuring. ```yaml $import: [first.yml, second.json, third.config.js] ``` ## Contributing & Development Please note that this project is released with a [Contributor Code of Conduct](CODE_OF_CONDUCT.md). By participating in this project you agree to abide by its terms. And please do participate! [result]: #result [`load()`]: #explorerload [`search()`]: #explorersearch [`clearloadcache()`]: #explorerclearloadcache [`clearsearchcache()`]: #explorerclearsearchcache [`cosmiconfig()`]: #cosmiconfig [`cosmiconfigSync()`]: #cosmiconfigsync [`clearcaches()`]: #explorerclearcaches [`packageprop`]: #packageprop [`cache`]: #cache [`stopdir`]: #stopdir [`searchplaces`]: #searchplaces [`loaders`]: #loaders [`cosmiconfigoptions`]: #cosmiconfigoptions [`explorerSync.search()`]: #explorersyncsearch [`explorerSync.load()`]: #explorersyncload [`explorer.search()`]: #explorersearch [`explorer.load()`]: #explorerload ["Loading JS modules"]: #loading-js-modules [`env-paths`]: https://github.com/sindresorhus/env-paths [search strategies]: #searchstrategy cosmiconfig-9.0.1/codecov.yml000066400000000000000000000000171515130774000161670ustar00rootroot00000000000000comment: false cosmiconfig-9.0.1/package.json000066400000000000000000000057161515130774000163230ustar00rootroot00000000000000{ "name": "cosmiconfig", "version": "9.0.1", "description": "Find and load configuration from a package.json property, rc file, TypeScript module, and more!", "main": "dist/index.js", "types": "dist/index.d.ts", "files": [ "dist", "!dist/**/*.map" ], "scripts": { "clean": "git clean -Xdf -e '!node_modules' .", "build": "npm run build:tsc", "build:tsc": "cross-env NODE_ENV=production tsc -b", "dev": "npm run build:tsc -- --watch", "lint": "eslint --ext .js,.ts .", "lint:fix": "eslint --ext .js,.ts . --fix", "lint:md": "remark-preset-davidtheclark", "format": "prettier \"**/*.{js,ts,json,yml,yaml}\" --write", "format:md": "remark-preset-davidtheclark --format", "format:check": "prettier \"**/*.{js,ts,json,yml,yaml}\" --check", "test": "vitest run --coverage", "test:watch": "vitest", "check:all": "npm run test && npm run lint && npm run format:check", "prepublishOnly": "npm run check:all && npm run build", "prepare": "husky install" }, "lint-staged": { "*.{js,ts}": [ "eslint --fix", "prettier --write" ], "*.{json,yml,yaml}": [ "prettier --write" ], "*.md": [ "remark-preset-davidtheclark", "remark-preset-davidtheclark --format" ] }, "repository": { "type": "git", "url": "git+https://github.com/cosmiconfig/cosmiconfig.git" }, "keywords": [ "load", "configuration", "config" ], "author": "Daniel Fischer ", "contributors": [ "Randolf J ", "David Clark ", "Bogdan Chadkin ", "Suhas Karanth " ], "funding": "https://github.com/sponsors/d-fischer", "license": "MIT", "bugs": { "url": "https://github.com/cosmiconfig/cosmiconfig/issues" }, "homepage": "https://github.com/cosmiconfig/cosmiconfig#readme", "peerDependencies": { "typescript": ">=4.9.5" }, "peerDependenciesMeta": { "typescript": { "optional": true } }, "dependencies": { "env-paths": "^2.2.1", "import-fresh": "^3.3.0", "js-yaml": "^4.1.0", "parse-json": "^5.2.0" }, "devDependencies": { "@types/js-yaml": "^4.0.5", "@types/node": "^14", "@types/parse-json": "^4.0.0", "@typescript-eslint/eslint-plugin": "^6.5.0", "@typescript-eslint/parser": "^6.5.0", "@vitest/coverage-istanbul": "^0.34.3", "cross-env": "^7.0.3", "eslint": "^8.48.0", "eslint-config-davidtheclark-node": "^0.2.2", "eslint-config-prettier": "^9.0.0", "eslint-import-resolver-typescript": "^3.6.0", "eslint-plugin-import": "^2.28.1", "eslint-plugin-node": "^11.1.0", "eslint-plugin-vitest": "^0.2.8", "husky": "^8.0.3", "lint-staged": "^14.0.1", "parent-module": "^3.0.0", "prettier": "^3.0.3", "remark-preset-davidtheclark": "^0.12.0", "typescript": "^5.2.2", "vitest": "^0.34.3" }, "engines": { "node": ">=14" } } cosmiconfig-9.0.1/src/000077500000000000000000000000001515130774000146135ustar00rootroot00000000000000cosmiconfig-9.0.1/src/Explorer.ts000066400000000000000000000143461515130774000167730ustar00rootroot00000000000000import fs from 'fs/promises'; import path from 'path'; import { globalConfigSearchPlaces } from './defaults'; import { ExplorerBase, getExtensionDescription } from './ExplorerBase.js'; import { hasOwn, mergeAll } from './merge'; import { Config, CosmiconfigResult, InternalOptions, DirToSearch, } from './types.js'; import { emplace, getPropertyByPath, isDirectory } from './util.js'; /** * @internal */ export class Explorer extends ExplorerBase { public async load(filepath: string): Promise { filepath = path.resolve(filepath); const load = async (): Promise => { return await this.config.transform( await this.#readConfiguration(filepath), ); }; if (this.loadCache) { return await emplace(this.loadCache, filepath, load); } return await load(); } public async search(from = ''): Promise { if (this.config.metaConfigFilePath) { this.loadingMetaConfig = true; const config = await this.load(this.config.metaConfigFilePath); this.loadingMetaConfig = false; if (config && !config.isEmpty) { return config; } } from = path.resolve(from); const dirs = this.#getDirs(from); const firstDirIter = await dirs.next(); /* istanbul ignore if -- @preserve */ if (firstDirIter.done) { // this should never happen throw new Error( `Could not find any folders to iterate through (start from ${from})`, ); } let currentDir = firstDirIter.value; const search = async (): Promise => { /* istanbul ignore if -- @preserve */ if (await isDirectory(currentDir.path)) { for (const filepath of this.getSearchPlacesForDir( currentDir, globalConfigSearchPlaces, )) { try { const result = await this.#readConfiguration(filepath); if ( result !== null && !(result.isEmpty && this.config.ignoreEmptySearchPlaces) ) { return await this.config.transform(result); } } catch (error) { if ( error.code === 'ENOENT' || error.code === 'EISDIR' || error.code === 'ENOTDIR' || error.code === 'EACCES' ) { continue; } throw error; } } } const nextDirIter = await dirs.next(); if (!nextDirIter.done) { currentDir = nextDirIter.value; if (this.searchCache) { return await emplace(this.searchCache, currentDir.path, search); } return await search(); } return await this.config.transform(null); }; if (this.searchCache) { return await emplace(this.searchCache, from, search); } return await search(); } async #readConfiguration( filepath: string, importStack: Array = [], ): Promise { const contents = await fs.readFile(filepath, { encoding: 'utf-8' }); return this.toCosmiconfigResult( filepath, await this.#loadConfigFileWithImports(filepath, contents, importStack), ); } async #loadConfigFileWithImports( filepath: string, contents: string, importStack: Array, ): Promise { const loadedContent = await this.#loadConfiguration(filepath, contents); if (!loadedContent || !hasOwn(loadedContent, '$import')) { return loadedContent; } const fileDirectory = path.dirname(filepath); const { $import: imports, ...ownContent } = loadedContent; const importPaths = Array.isArray(imports) ? imports : [imports]; const newImportStack = [...importStack, filepath]; this.validateImports(filepath, importPaths, newImportStack); const importedConfigs = await Promise.all( importPaths.map(async (importPath) => { const fullPath = path.resolve(fileDirectory, importPath); const result = await this.#readConfiguration(fullPath, newImportStack); return result?.config; }), ); return mergeAll([...importedConfigs, ownContent], { mergeArrays: this.config.mergeImportArrays, }); } async #loadConfiguration( filepath: string, contents: string, ): Promise { if (contents.trim() === '') { return; } const extension = path.extname(filepath); const loader = this.config.loaders[extension || 'noExt'] ?? this.config.loaders['default']; if (!loader) { throw new Error( `No loader specified for ${getExtensionDescription(extension)}`, ); } try { const loadedContents = await loader(filepath, contents); if (path.basename(filepath, extension) !== 'package') { return loadedContents; } return ( getPropertyByPath( loadedContents, this.config.packageProp ?? this.config.moduleName, ) ?? null ); } catch (error) { error.filepath = filepath; throw error; } } async #fileExists(path: string): Promise { try { await fs.stat(path); return true; } catch (e) { return false; } } async *#getDirs(startDir: string): AsyncIterableIterator { switch (this.config.searchStrategy) { case 'none': { // only check in the passed directory (defaults to working directory) yield { path: startDir, isGlobalConfig: false }; return; } case 'project': { let currentDir = startDir; while (true) { yield { path: currentDir, isGlobalConfig: false }; for (const ext of ['json', 'yaml']) { const packageFile = path.join(currentDir, `package.${ext}`); if (await this.#fileExists(packageFile)) { break; } } const parentDir = path.dirname(currentDir); /* istanbul ignore if -- @preserve */ if (parentDir === currentDir) { // we're probably at the root of the directory structure break; } currentDir = parentDir; } return; } case 'global': { yield* this.getGlobalDirs(startDir); } } } } cosmiconfig-9.0.1/src/ExplorerBase.ts000066400000000000000000000110621515130774000175560ustar00rootroot00000000000000import envPaths from 'env-paths'; import os from 'os'; import path from 'path'; import { AsyncCache, Cache, Config, CosmiconfigResult, InternalOptions, InternalOptionsSync, DirToSearch, } from './types.js'; import { getPropertyByPath } from './util.js'; /** * @internal */ export abstract class ExplorerBase< T extends InternalOptions | InternalOptionsSync, > { #loadingMetaConfig = false; protected readonly config: T; protected readonly loadCache?: T extends InternalOptionsSync ? Cache : AsyncCache; protected readonly searchCache?: T extends InternalOptionsSync ? Cache : AsyncCache; public constructor(options: Readonly) { this.config = options; if (options.cache) { this.loadCache = new Map(); this.searchCache = new Map(); } this.#validateConfig(); } protected set loadingMetaConfig(value: boolean) { this.#loadingMetaConfig = value; } #validateConfig(): void { const config = this.config; for (const place of config.searchPlaces) { const extension = path.extname(place); const loader = this.config.loaders[extension || 'noExt'] ?? this.config.loaders['default']; if (loader === undefined) { throw new Error( `Missing loader for ${getExtensionDescription(place)}.`, ); } if (typeof loader !== 'function') { throw new Error( `Loader for ${getExtensionDescription( place, )} is not a function: Received ${typeof loader}.`, ); } } } public clearLoadCache(): void { if (this.loadCache) { this.loadCache.clear(); } } public clearSearchCache(): void { if (this.searchCache) { this.searchCache.clear(); } } public clearCaches(): void { this.clearLoadCache(); this.clearSearchCache(); } protected toCosmiconfigResult( filepath: string, config: Config, ): CosmiconfigResult { if (config === null) { return null; } if (config === undefined) { return { filepath, config: undefined, isEmpty: true }; } if ( this.config.applyPackagePropertyPathToConfiguration || this.#loadingMetaConfig ) { const packageProp = this.config.packageProp ?? this.config.moduleName; config = getPropertyByPath(config, packageProp); } if (config === undefined) { return { filepath, config: undefined, isEmpty: true }; } return { config, filepath }; } protected validateImports( containingFilePath: string, imports: Array, importStack: Array, ): asserts imports is Array { const fileDirectory = path.dirname(containingFilePath); for (const importPath of imports) { if (typeof importPath !== 'string') { throw new Error( `${containingFilePath}: Key $import must contain a string or a list of strings`, ); } const fullPath = path.resolve(fileDirectory, importPath); if (fullPath === containingFilePath) { throw new Error(`Self-import detected in ${containingFilePath}`); } const idx = importStack.indexOf(fullPath); if (idx !== -1) { throw new Error( `Circular import detected: ${[...importStack, fullPath] .map((path, i) => `${i + 1}. ${path}`) .join('\n')} (same as ${idx + 1}.)`, ); } } } protected getSearchPlacesForDir( dir: DirToSearch, globalConfigPlaces: Array, ): Array { return ( dir.isGlobalConfig ? globalConfigPlaces : this.config.searchPlaces ).map((place) => path.join(dir.path, place)); } protected getGlobalConfigDir(): string { return envPaths(this.config.moduleName, { suffix: '' }).config; } protected *getGlobalDirs(startDir: string): IterableIterator { const stopDir = path.resolve(this.config.stopDir ?? os.homedir()); yield { path: startDir, isGlobalConfig: false }; let currentDir = startDir; while (currentDir !== stopDir) { const parentDir = path.dirname(currentDir); /* istanbul ignore if -- @preserve */ if (parentDir === currentDir) { // we're probably at the root of the directory structure break; } yield { path: parentDir, isGlobalConfig: false }; currentDir = parentDir; } yield { path: this.getGlobalConfigDir(), isGlobalConfig: true }; } } /** * @internal */ export function getExtensionDescription(extension?: string): string { /* istanbul ignore next -- @preserve */ return extension ? `extension "${extension}"` : 'files without extensions'; } cosmiconfig-9.0.1/src/ExplorerSync.ts000066400000000000000000000143451515130774000176270ustar00rootroot00000000000000import fs from 'fs'; import path from 'path'; import { globalConfigSearchPlacesSync } from './defaults'; import { ExplorerBase, getExtensionDescription } from './ExplorerBase.js'; import { hasOwn, mergeAll } from './merge'; import { Config, CosmiconfigResult, InternalOptionsSync, DirToSearch, } from './types.js'; import { emplace, getPropertyByPath, isDirectorySync } from './util.js'; /** * @internal */ export class ExplorerSync extends ExplorerBase { public load(filepath: string): CosmiconfigResult { filepath = path.resolve(filepath); const load = (): CosmiconfigResult => { return this.config.transform(this.#readConfiguration(filepath)); }; if (this.loadCache) { return emplace(this.loadCache, filepath, load); } return load(); } public search(from = ''): CosmiconfigResult { if (this.config.metaConfigFilePath) { this.loadingMetaConfig = true; const config = this.load(this.config.metaConfigFilePath); this.loadingMetaConfig = false; if (config && !config.isEmpty) { return config; } } from = path.resolve(from); const dirs = this.#getDirs(from); const firstDirIter = dirs.next(); /* istanbul ignore if -- @preserve */ if (firstDirIter.done) { // this should never happen throw new Error( `Could not find any folders to iterate through (start from ${from})`, ); } let currentDir = firstDirIter.value; const search = (): CosmiconfigResult => { /* istanbul ignore if -- @preserve */ if (isDirectorySync(currentDir.path)) { for (const filepath of this.getSearchPlacesForDir( currentDir, globalConfigSearchPlacesSync, )) { try { const result = this.#readConfiguration(filepath); if ( result !== null && !(result.isEmpty && this.config.ignoreEmptySearchPlaces) ) { return this.config.transform(result); } } catch (error) { if ( error.code === 'ENOENT' || error.code === 'EISDIR' || error.code === 'ENOTDIR' || error.code === 'EACCES' ) { continue; } throw error; } } } const nextDirIter = dirs.next(); if (!nextDirIter.done) { currentDir = nextDirIter.value; if (this.searchCache) { return emplace(this.searchCache, currentDir.path, search); } return search(); } return this.config.transform(null); }; if (this.searchCache) { return emplace(this.searchCache, from, search); } return search(); } #readConfiguration( filepath: string, importStack: Array = [], ): CosmiconfigResult { const contents = fs.readFileSync(filepath, 'utf8'); return this.toCosmiconfigResult( filepath, this.#loadConfigFileWithImports(filepath, contents, importStack), ); } #loadConfigFileWithImports( filepath: string, contents: string, importStack: Array, ): Config | null | undefined { const loadedContent = this.#loadConfiguration(filepath, contents); if (!loadedContent || !hasOwn(loadedContent, '$import')) { return loadedContent; } const fileDirectory = path.dirname(filepath); const { $import: imports, ...ownContent } = loadedContent; const importPaths = Array.isArray(imports) ? imports : [imports]; const newImportStack = [...importStack, filepath]; this.validateImports(filepath, importPaths, newImportStack); const importedConfigs = importPaths.map((importPath) => { const fullPath = path.resolve(fileDirectory, importPath); const result = this.#readConfiguration(fullPath, newImportStack); return result?.config; }); return mergeAll([...importedConfigs, ownContent], { mergeArrays: this.config.mergeImportArrays, }); } #loadConfiguration(filepath: string, contents: string): Config { if (contents.trim() === '') { return; } const extension = path.extname(filepath); const loader = this.config.loaders[extension || 'noExt'] ?? this.config.loaders['default']; if (!loader) { throw new Error( `No loader specified for ${getExtensionDescription(extension)}`, ); } try { const loadedContents = loader(filepath, contents); if (path.basename(filepath, extension) !== 'package') { return loadedContents; } return ( getPropertyByPath( loadedContents, this.config.packageProp ?? this.config.moduleName, ) ?? null ); } catch (error) { error.filepath = filepath; throw error; } } #fileExists(path: string): boolean { try { fs.statSync(path); return true; } catch (e) { return false; } } *#getDirs(startDir: string): Iterator { switch (this.config.searchStrategy) { case 'none': { // there is no next dir yield { path: startDir, isGlobalConfig: false }; return; } case 'project': { let currentDir = startDir; while (true) { yield { path: currentDir, isGlobalConfig: false }; for (const ext of ['json', 'yaml']) { const packageFile = path.join(currentDir, `package.${ext}`); if (this.#fileExists(packageFile)) { break; } } const parentDir = path.dirname(currentDir); /* istanbul ignore if -- @preserve */ if (parentDir === currentDir) { // we're probably at the root of the directory structure break; } currentDir = parentDir; } return; } case 'global': { yield* this.getGlobalDirs(startDir); } } } /** * @deprecated Use {@link ExplorerSync.prototype.load}. */ /* istanbul ignore next */ public loadSync(filepath: string): CosmiconfigResult { return this.load(filepath); } /** * @deprecated Use {@link ExplorerSync.prototype.search}. */ /* istanbul ignore next */ public searchSync(from = ''): CosmiconfigResult { return this.search(from); } } cosmiconfig-9.0.1/src/defaults.ts000066400000000000000000000051421515130774000167740ustar00rootroot00000000000000import { loadJs, loadJson, loadJsSync, loadTs, loadTsSync, loadYaml, } from './loaders'; export function getDefaultSearchPlaces(moduleName: string): Array { return [ 'package.json', `.${moduleName}rc`, `.${moduleName}rc.json`, `.${moduleName}rc.yaml`, `.${moduleName}rc.yml`, `.${moduleName}rc.js`, `.${moduleName}rc.ts`, `.${moduleName}rc.cjs`, `.${moduleName}rc.mjs`, `.config/${moduleName}rc`, `.config/${moduleName}rc.json`, `.config/${moduleName}rc.yaml`, `.config/${moduleName}rc.yml`, `.config/${moduleName}rc.js`, `.config/${moduleName}rc.ts`, `.config/${moduleName}rc.cjs`, `.config/${moduleName}rc.mjs`, `${moduleName}.config.js`, `${moduleName}.config.ts`, `${moduleName}.config.cjs`, `${moduleName}.config.mjs`, ]; } export function getDefaultSearchPlacesSync(moduleName: string): Array { return [ 'package.json', `.${moduleName}rc`, `.${moduleName}rc.json`, `.${moduleName}rc.yaml`, `.${moduleName}rc.yml`, `.${moduleName}rc.js`, `.${moduleName}rc.ts`, `.${moduleName}rc.cjs`, `.config/${moduleName}rc`, `.config/${moduleName}rc.json`, `.config/${moduleName}rc.yaml`, `.config/${moduleName}rc.yml`, `.config/${moduleName}rc.js`, `.config/${moduleName}rc.ts`, `.config/${moduleName}rc.cjs`, `${moduleName}.config.js`, `${moduleName}.config.ts`, `${moduleName}.config.cjs`, ]; } export const globalConfigSearchPlaces = [ 'config', 'config.json', 'config.yaml', 'config.yml', 'config.js', 'config.ts', 'config.cjs', 'config.mjs', ]; export const globalConfigSearchPlacesSync = [ 'config', 'config.json', 'config.yaml', 'config.yml', 'config.js', 'config.ts', 'config.cjs', ]; // this needs to be hardcoded, as this is intended for end users, who can't supply options at this point export const metaSearchPlaces = [ 'package.json', 'package.yaml', '.config/config.json', '.config/config.yaml', '.config/config.yml', '.config/config.js', '.config/config.ts', '.config/config.cjs', '.config/config.mjs', ]; // do not allow mutation of default loaders. Make sure it is set inside options export const defaultLoaders = Object.freeze({ '.mjs': loadJs, '.cjs': loadJs, '.js': loadJs, '.ts': loadTs, '.json': loadJson, '.yaml': loadYaml, '.yml': loadYaml, noExt: loadYaml, } as const); export const defaultLoadersSync = Object.freeze({ '.cjs': loadJsSync, '.js': loadJsSync, '.ts': loadTsSync, '.json': loadJson, '.yaml': loadYaml, '.yml': loadYaml, noExt: loadYaml, } as const); cosmiconfig-9.0.1/src/index.ts000066400000000000000000000144141515130774000162760ustar00rootroot00000000000000import { defaultLoaders, defaultLoadersSync, getDefaultSearchPlaces, getDefaultSearchPlacesSync, metaSearchPlaces, globalConfigSearchPlaces, globalConfigSearchPlacesSync, } from './defaults'; import { Explorer } from './Explorer.js'; import { ExplorerSync } from './ExplorerSync.js'; import { CommonOptions, Config, CosmiconfigResult, InternalOptions, InternalOptionsSync, Loader, LoaderResult, Loaders, LoadersSync, LoaderSync, Options, OptionsSync, PublicExplorer, PublicExplorerBase, PublicExplorerSync, SearchStrategy, Transform, TransformSync, } from './types.js'; import { removeUndefinedValuesFromObject } from './util'; const identity: TransformSync = function identity(x) { return x; }; function getUserDefinedOptionsFromMetaConfig(): CosmiconfigResult { const metaExplorer = new ExplorerSync({ moduleName: 'cosmiconfig', stopDir: process.cwd(), searchPlaces: metaSearchPlaces, ignoreEmptySearchPlaces: false, applyPackagePropertyPathToConfiguration: true, loaders: defaultLoaders, transform: identity, cache: true, metaConfigFilePath: null, mergeImportArrays: true, mergeSearchPlaces: true, searchStrategy: 'none', }); const metaConfig = metaExplorer.search(); if (!metaConfig) { return null; } if (metaConfig.config?.loaders) { throw new Error('Can not specify loaders in meta config file'); } if (metaConfig.config?.searchStrategy) { throw new Error('Can not specify searchStrategy in meta config file'); } const overrideOptions: Partial = { mergeSearchPlaces: true, ...(metaConfig.config ?? {}), }; return { config: removeUndefinedValuesFromObject(overrideOptions) as Partial< Options | OptionsSync >, filepath: metaConfig.filepath, }; } function getResolvedSearchPlaces( moduleName: string, toolDefinedSearchPlaces: Array, userConfiguredOptions: T, ): Array { const userConfiguredSearchPlaces = userConfiguredOptions.searchPlaces?.map( (path: string) => path.replace('{name}', moduleName), ); if (userConfiguredOptions.mergeSearchPlaces) { return [...(userConfiguredSearchPlaces ?? []), ...toolDefinedSearchPlaces]; } return ( userConfiguredSearchPlaces ?? /* istanbul ignore next */ toolDefinedSearchPlaces ); } function mergeOptionsBase< IntOpts extends InternalOptions | InternalOptionsSync, Opts extends Options | OptionsSync, >( moduleName: string, defaults: IntOpts, options: Readonly>, ): IntOpts { const userDefinedConfig = getUserDefinedOptionsFromMetaConfig(); if (!userDefinedConfig) { return { ...defaults, ...removeUndefinedValuesFromObject(options), loaders: { ...defaults.loaders, ...options.loaders, }, }; } const userConfiguredOptions = userDefinedConfig.config as Readonly; const toolDefinedSearchPlaces = options.searchPlaces ?? defaults.searchPlaces; return { ...defaults, ...removeUndefinedValuesFromObject(options), metaConfigFilePath: userDefinedConfig.filepath, ...userConfiguredOptions, searchPlaces: getResolvedSearchPlaces( moduleName, toolDefinedSearchPlaces, userConfiguredOptions, ), loaders: { ...defaults.loaders, ...options.loaders, }, }; } function validateOptions( options: Readonly>, ): void { if ( options.searchStrategy != null && options.searchStrategy !== 'global' && options.stopDir ) { throw new Error( 'Can not supply `stopDir` option with `searchStrategy` other than "global"', ); } } function mergeOptions( moduleName: string, options: Readonly>, ): InternalOptions { validateOptions(options); const defaults = { moduleName, searchPlaces: getDefaultSearchPlaces(moduleName), ignoreEmptySearchPlaces: true, cache: true, transform: identity, loaders: defaultLoaders, metaConfigFilePath: null, mergeImportArrays: true, mergeSearchPlaces: true, searchStrategy: options.stopDir ? 'global' : 'none', } satisfies InternalOptions; return mergeOptionsBase(moduleName, defaults, options); } function mergeOptionsSync( moduleName: string, options: Readonly>, ): InternalOptionsSync { validateOptions(options); const defaults = { moduleName, searchPlaces: getDefaultSearchPlacesSync(moduleName), ignoreEmptySearchPlaces: true, cache: true, transform: identity, loaders: defaultLoadersSync, metaConfigFilePath: null, mergeImportArrays: true, mergeSearchPlaces: true, searchStrategy: options.stopDir ? 'global' : 'none', } satisfies InternalOptionsSync; return mergeOptionsBase(moduleName, defaults, options); } export function cosmiconfig( moduleName: string, options: Readonly> = {}, ): PublicExplorer { const normalizedOptions = mergeOptions(moduleName, options); const explorer = new Explorer(normalizedOptions); return { search: explorer.search.bind(explorer), load: explorer.load.bind(explorer), clearLoadCache: explorer.clearLoadCache.bind(explorer), clearSearchCache: explorer.clearSearchCache.bind(explorer), clearCaches: explorer.clearCaches.bind(explorer), }; } export function cosmiconfigSync( moduleName: string, options: Readonly> = {}, ): PublicExplorerSync { const normalizedOptions = mergeOptionsSync(moduleName, options); const explorerSync = new ExplorerSync(normalizedOptions); return { search: explorerSync.search.bind(explorerSync), load: explorerSync.load.bind(explorerSync), clearLoadCache: explorerSync.clearLoadCache.bind(explorerSync), clearSearchCache: explorerSync.clearSearchCache.bind(explorerSync), clearCaches: explorerSync.clearCaches.bind(explorerSync), }; } export { Config, CosmiconfigResult, LoaderResult, Loader, Loaders, LoaderSync, LoadersSync, Transform, TransformSync, SearchStrategy, CommonOptions, Options, OptionsSync, PublicExplorerBase, PublicExplorer, PublicExplorerSync, getDefaultSearchPlaces, getDefaultSearchPlacesSync, globalConfigSearchPlaces, globalConfigSearchPlacesSync, defaultLoaders, defaultLoadersSync, }; cosmiconfig-9.0.1/src/loaders.ts000066400000000000000000000106521515130774000166200ustar00rootroot00000000000000/* eslint-disable @typescript-eslint/no-require-imports */ import { existsSync, rmSync, writeFileSync } from 'fs'; import { rm, writeFile, realpath } from 'fs/promises'; import path from 'path'; import { pathToFileURL } from 'url'; import { Loader, LoaderSync } from './types.js'; import { randomUUID } from 'crypto'; let importFresh: typeof import('import-fresh'); export const loadJsSync: LoaderSync = function loadJsSync(filepath) { if (importFresh === undefined) { importFresh = require('import-fresh'); } return importFresh(filepath); }; export const loadJs: Loader = async function loadJs(filepath) { try { const { href } = pathToFileURL(await realpath(filepath)); return (await import(href)).default; } catch (error) { try { return loadJsSync(filepath, ''); } catch (requireError) { /* istanbul ignore next -- @preserve */ if ( requireError.code === 'ERR_REQUIRE_ESM' || (requireError instanceof SyntaxError && requireError .toString() .includes('Cannot use import statement outside a module')) ) { throw error; } throw requireError; } } }; let parseJson: typeof import('parse-json'); export const loadJson: LoaderSync = function loadJson(filepath, content) { if (parseJson === undefined) { parseJson = require('parse-json'); } try { return parseJson(content); } catch (error) { error.message = `JSON Error in ${filepath}:\n${error.message}`; throw error; } }; let yaml: typeof import('js-yaml'); export const loadYaml: LoaderSync = function loadYaml(filepath, content) { if (yaml === undefined) { yaml = require('js-yaml'); } try { return yaml.load(content); } catch (error) { error.message = `YAML Error in ${filepath}:\n${error.message}`; throw error; } }; let typescript: typeof import('typescript'); export const loadTsSync: LoaderSync = function loadTsSync(filepath, content) { /* istanbul ignore next -- @preserve */ if (typescript === undefined) { typescript = require('typescript'); } const compiledFilepath = `${filepath}.${randomUUID()}.cjs`; try { const config = resolveTsConfig(path.dirname(filepath)) ?? {}; config.compilerOptions = { ...config.compilerOptions, module: typescript.ModuleKind.NodeNext, moduleResolution: typescript.ModuleResolutionKind.NodeNext, target: typescript.ScriptTarget.ES2022, noEmit: false, }; content = typescript.transpileModule(content, config).outputText; writeFileSync(compiledFilepath, content); return loadJsSync(compiledFilepath, content).default; } catch (error) { error.message = `TypeScript Error in ${filepath}:\n${error.message}`; throw error; } finally { if (existsSync(compiledFilepath)) { rmSync(compiledFilepath); } } }; export const loadTs: Loader = async function loadTs(filepath, content) { if (typescript === undefined) { typescript = (await import('typescript')).default; } const compiledFilepath = `${filepath}.${randomUUID()}.mjs`; let transpiledContent; try { try { const config = resolveTsConfig(path.dirname(filepath)) ?? {}; config.compilerOptions = { ...config.compilerOptions, module: typescript.ModuleKind.ES2022, moduleResolution: typescript.ModuleResolutionKind.Bundler, target: typescript.ScriptTarget.ES2022, noEmit: false, }; transpiledContent = typescript.transpileModule( content, config, ).outputText; await writeFile(compiledFilepath, transpiledContent); } catch (error) { error.message = `TypeScript Error in ${filepath}:\n${error.message}`; throw error; } // eslint-disable-next-line @typescript-eslint/return-await return await loadJs(compiledFilepath, transpiledContent); } finally { if (existsSync(compiledFilepath)) { await rm(compiledFilepath); } } }; // eslint-disable-next-line @typescript-eslint/no-explicit-any function resolveTsConfig(directory: string): any { const filePath = typescript.findConfigFile(directory, (fileName) => { return typescript.sys.fileExists(fileName); }); if (filePath !== undefined) { const { config, error } = typescript.readConfigFile(filePath, (path) => typescript.sys.readFile(path), ); if (error) { throw new Error(`Error in ${filePath}: ${error.messageText.toString()}`); } return config; } return; } cosmiconfig-9.0.1/src/merge.ts000066400000000000000000000027371515130774000162730ustar00rootroot00000000000000/* eslint-disable @typescript-eslint/unbound-method */ export const hasOwn = Function.prototype.call.bind( Object.prototype.hasOwnProperty, ); const objToString = Function.prototype.call.bind(Object.prototype.toString); /* eslint-enable @typescript-eslint/unbound-method */ function isPlainObject(obj: unknown): boolean { return objToString(obj) === '[object Object]'; } export interface MergeOptions { mergeArrays: boolean; } // eslint-disable-next-line @typescript-eslint/no-explicit-any function merge(target: any, source: any, options: MergeOptions): any { for (const key of Object.keys(source)) { const newValue = source[key]; if (hasOwn(target, key)) { if (Array.isArray(target[key]) && Array.isArray(newValue)) { if (options.mergeArrays) { target[key].push(...newValue); continue; } } else if (isPlainObject(target[key]) && isPlainObject(newValue)) { target[key] = merge(target[key], newValue, options); continue; } } target[key] = newValue; } return target; } /** * Merges multiple objects. Doesn't care about cloning non-primitives, as we load all these objects fresh from a file. */ export function mergeAll( // eslint-disable-next-line @typescript-eslint/no-explicit-any objects: ReadonlyArray, options: MergeOptions, // eslint-disable-next-line @typescript-eslint/no-explicit-any ): any { return objects.reduce((target, source) => merge(target, source, options), {}); } cosmiconfig-9.0.1/src/types.ts000066400000000000000000000052441515130774000163340ustar00rootroot00000000000000/** * @public */ // eslint-disable-next-line @typescript-eslint/no-explicit-any export type Config = any; /** * @public */ export type CosmiconfigResult = { config: Config; filepath: string; isEmpty?: boolean; } | null; /** * @public */ export type LoaderResult = Config | null; /** * @public */ export type Loader = | ((filepath: string, content: string) => Promise) | LoaderSync; /** * @public */ export type LoaderSync = (filepath: string, content: string) => LoaderResult; /** * @public */ export type Transform = | ((CosmiconfigResult: CosmiconfigResult) => Promise) | TransformSync; /** * @public */ export type TransformSync = ( CosmiconfigResult: CosmiconfigResult, ) => CosmiconfigResult; /** * @public */ export type SearchStrategy = 'none' | 'project' | 'global'; /** * @public */ export interface CommonOptions { packageProp?: string | Array; searchPlaces: Array; ignoreEmptySearchPlaces: boolean; stopDir?: string; cache: boolean; mergeImportArrays: boolean; mergeSearchPlaces: boolean; searchStrategy: SearchStrategy; } /** * @public */ export interface Options extends CommonOptions { loaders: Loaders; transform: Transform; } /** * @public */ export interface OptionsSync extends CommonOptions { loaders: LoadersSync; transform: TransformSync; } /** * @internal */ export interface InternalOptions extends Options { applyPackagePropertyPathToConfiguration?: boolean; metaConfigFilePath: string | null; moduleName: string; } /** * @internal */ export interface InternalOptionsSync extends OptionsSync { applyPackagePropertyPathToConfiguration?: boolean; metaConfigFilePath: string | null; moduleName: string; } /** * @internal */ export type Cache = Map; /** * @internal */ export type AsyncCache = Map>; /** * @public */ export interface Loaders { [key: string]: Loader; } /** * @public */ export interface LoadersSync { [key: string]: LoaderSync; } /** * @public */ export interface PublicExplorerBase { clearLoadCache: () => void; clearSearchCache: () => void; clearCaches: () => void; } /** * @public */ export interface PublicExplorer extends PublicExplorerBase { search: (searchFrom?: string) => Promise; load: (filepath: string) => Promise; } /** * @public */ export interface PublicExplorerSync extends PublicExplorerBase { search: (searchFrom?: string) => CosmiconfigResult; load: (filepath: string) => CosmiconfigResult; } /** * @internal */ export interface DirToSearch { path: string; isGlobalConfig: boolean; } cosmiconfig-9.0.1/src/util.ts000066400000000000000000000037111515130774000161420ustar00rootroot00000000000000import fs, { promises as fsp } from 'fs'; /** * @internal */ export function emplace(map: Map, key: K, fn: () => V): V { const cached = map.get(key); if (cached !== undefined) { return cached; } const result = fn(); map.set(key, result); return result; } // Resolves property names or property paths defined with period-delimited // strings or arrays of strings. Property names that are found on the source // object are used directly (even if they include a period). // Nested property names that include periods, within a path, are only // understood in array paths. /** * @internal */ export function getPropertyByPath( source: { [key: string]: unknown }, path: string | Array, ): unknown { if ( typeof path === 'string' && Object.prototype.hasOwnProperty.call(source, path) ) { return source[path]; } const parsedPath = typeof path === 'string' ? path.split('.') : path; // eslint-disable-next-line @typescript-eslint/no-explicit-any return parsedPath.reduce((previous: any, key): unknown => { if (previous === undefined) { return previous; } return previous[key]; }, source); } /** @internal */ export function removeUndefinedValuesFromObject( options: Record, ): Record { return Object.fromEntries( Object.entries(options).filter(([, value]) => value !== undefined), ); } /** @internal */ /* istanbul ignore next -- @preserve */ export async function isDirectory(path: string): Promise { try { const stat = await fsp.stat(path); return stat.isDirectory(); } catch (e) { if (e.code === 'ENOENT') { return false; } throw e; } } /** @internal */ /* istanbul ignore next -- @preserve */ export function isDirectorySync(path: string): boolean { try { const stat = fs.statSync(path); return stat.isDirectory(); } catch (e) { if (e.code === 'ENOENT') { return false; } throw e; } } cosmiconfig-9.0.1/test/000077500000000000000000000000001515130774000150035ustar00rootroot00000000000000cosmiconfig-9.0.1/test/caches.test.ts000066400000000000000000000463701515130774000175710ustar00rootroot00000000000000import { beforeEach, afterEach, afterAll, describe, expect, test, vi, } from 'vitest'; import fs from 'fs'; import fsPromises from 'fs/promises'; import { cosmiconfig, cosmiconfigSync } from '../src'; import { isNotMjs, TempDir } from './util'; const temp = new TempDir(); beforeEach(() => { temp.clean(); temp.createDir('a/b/c/d/e/f/'); temp.createFile('a/b/c/d/package.json', '{ "false": "hope" }'); temp.createFile('a/b/c/d/.foorc', '{ "foundInD": true }'); temp.createFile('a/b/package.json', '{ "foo": { "foundInB": true } }'); }); afterEach(() => { vi.restoreAllMocks(); }); afterAll(() => { // Remove temp.dir created for tests temp.deleteTempDir(); }); describe('cache is not used initially', () => { const searchPath = temp.absolutePath('a/b/c/d/e'); const expectedFilesChecked = [ 'a/b/c/d/e/package.json', 'a/b/c/d/e/.foorc', 'a/b/c/d/e/.foorc.json', 'a/b/c/d/e/.foorc.yaml', 'a/b/c/d/e/.foorc.yml', 'a/b/c/d/e/.foorc.js', 'a/b/c/d/e/.foorc.ts', 'a/b/c/d/e/.foorc.cjs', 'a/b/c/d/e/.foorc.mjs', 'a/b/c/d/e/.config/foorc', 'a/b/c/d/e/.config/foorc.json', 'a/b/c/d/e/.config/foorc.yaml', 'a/b/c/d/e/.config/foorc.yml', 'a/b/c/d/e/.config/foorc.js', 'a/b/c/d/e/.config/foorc.ts', 'a/b/c/d/e/.config/foorc.cjs', 'a/b/c/d/e/.config/foorc.mjs', 'a/b/c/d/e/foo.config.js', 'a/b/c/d/e/foo.config.ts', 'a/b/c/d/e/foo.config.cjs', 'a/b/c/d/e/foo.config.mjs', 'a/b/c/d/package.json', 'a/b/c/d/.foorc', ]; const checkResult = (readFileSpy: any, result: any, files: any) => { const filesChecked = temp.getSpyPathCalls(readFileSpy); expect(filesChecked).toEqual(files); expect(result).toEqual({ filepath: temp.absolutePath('a/b/c/d/.foorc'), config: { foundInD: true }, }); }; test('async', async () => { const explorer = cosmiconfig('foo', { searchStrategy: 'global' }); const readFileSpy = vi.spyOn(fsPromises, 'readFile'); const cachedSearch = explorer.search; const result = await cachedSearch(searchPath); checkResult(readFileSpy, result, expectedFilesChecked); }); test('sync', () => { const explorer = cosmiconfigSync('foo', { searchStrategy: 'global' }); const readFileSpy = vi.spyOn(fs, 'readFileSync'); const cachedSearchSync = explorer.search; const result = cachedSearchSync(searchPath); checkResult(readFileSpy, result, expectedFilesChecked.filter(isNotMjs)); }); }); describe('cache is used for already-visited directories', () => { const searchPath = temp.absolutePath('a/b/c/d/e'); const checkResult = (readFileSpy: any, result: any) => { expect(readFileSpy).toHaveBeenCalledTimes(0); expect(result).toEqual({ filepath: temp.absolutePath('a/b/c/d/.foorc'), config: { foundInD: true }, }); }; test('async', async () => { const readFileSpy = vi.spyOn(fsPromises, 'readFile'); const cachedSearch = cosmiconfig('foo', { searchStrategy: 'global', }).search; // First pass, prime the cache ... await cachedSearch(searchPath); // Reset readFile mocks and search again. readFileSpy.mockClear(); const result = await cachedSearch(searchPath); checkResult(readFileSpy, result); }); test('sync', () => { const readFileSpy = vi.spyOn(fs, 'readFileSync'); const cachedSearchSync = cosmiconfigSync('foo', { searchStrategy: 'global', }).search; // First pass, prime the cache ... cachedSearchSync(searchPath); // Reset readFile mocks and search again. readFileSpy.mockClear(); const result = cachedSearchSync(searchPath); checkResult(readFileSpy, result); }); }); describe('cache is used for already-loaded file', () => { const loadPath = temp.absolutePath('a/b/c/d/.foorc'); const checkResult = (readFileSpy: any, result: any) => { expect(readFileSpy).toHaveBeenCalledTimes(0); expect(result).toEqual({ filepath: temp.absolutePath('a/b/c/d/.foorc'), config: { foundInD: true }, }); }; test('async', async () => { const cachedLoad = cosmiconfig('foo').load; // First pass, prime the cache ... await cachedLoad(loadPath); // Mock and search again. const readFileSpy = vi.spyOn(fsPromises, 'readFile'); const result = await cachedLoad(loadPath); checkResult(readFileSpy, result); }); test('sync', () => { const cachedLoadSync = cosmiconfigSync('foo').load; // First pass, prime the cache ... cachedLoadSync(loadPath); // Mock and search again. const readFileSpy = vi.spyOn(fs, 'readFileSync'); const result = cachedLoadSync(loadPath); checkResult(readFileSpy, result); }); }); describe('cache is used when some directories in search are already visted', () => { const firstSearchPath = temp.absolutePath('a/b/c/d/e'); const secondSearchPath = temp.absolutePath('a/b/c/d/e/f'); const expectedFilesChecked = [ 'a/b/c/d/e/f/package.json', 'a/b/c/d/e/f/.foorc', 'a/b/c/d/e/f/.foorc.json', 'a/b/c/d/e/f/.foorc.yaml', 'a/b/c/d/e/f/.foorc.yml', 'a/b/c/d/e/f/.foorc.js', 'a/b/c/d/e/f/.foorc.ts', 'a/b/c/d/e/f/.foorc.cjs', 'a/b/c/d/e/f/.foorc.mjs', 'a/b/c/d/e/f/.config/foorc', 'a/b/c/d/e/f/.config/foorc.json', 'a/b/c/d/e/f/.config/foorc.yaml', 'a/b/c/d/e/f/.config/foorc.yml', 'a/b/c/d/e/f/.config/foorc.js', 'a/b/c/d/e/f/.config/foorc.ts', 'a/b/c/d/e/f/.config/foorc.cjs', 'a/b/c/d/e/f/.config/foorc.mjs', 'a/b/c/d/e/f/foo.config.js', 'a/b/c/d/e/f/foo.config.ts', 'a/b/c/d/e/f/foo.config.cjs', 'a/b/c/d/e/f/foo.config.mjs', ]; const checkResult = (readFileSpy: any, result: any, files: any) => { const filesChecked = temp.getSpyPathCalls(readFileSpy); expect(filesChecked).toEqual(files); expect(result).toEqual({ filepath: temp.absolutePath('a/b/c/d/.foorc'), config: { foundInD: true }, }); }; test('async', async () => { const readFileSpy = vi.spyOn(fsPromises, 'readFile'); const cachedSearch = cosmiconfig('foo', { searchStrategy: 'global', }).search; // First pass, prime the cache ... await cachedSearch(firstSearchPath); // Reset readFile mocks and search again. readFileSpy.mockClear(); const result = await cachedSearch(secondSearchPath); checkResult(readFileSpy, result, expectedFilesChecked); }); test('sync', () => { const readFileSpy = vi.spyOn(fs, 'readFileSync'); const cachedSearchSync = cosmiconfigSync('foo', { searchStrategy: 'global', }).search; // First pass, prime the cache ... cachedSearchSync(firstSearchPath); // Reset readFile mocks and search again. readFileSpy.mockClear(); const result = cachedSearchSync(secondSearchPath); checkResult(readFileSpy, result, expectedFilesChecked.filter(isNotMjs)); }); }); describe('cache is not used when directly loading an unvisited file', () => { const firstSearchPath = temp.absolutePath('a/b/c/d/e'); const loadPath = temp.absolutePath('a/b/package.json'); const checkResult = (readFileSpy: any, result: any) => { expect(readFileSpy).toHaveBeenCalledTimes(1); expect(result).toEqual({ filepath: temp.absolutePath('a/b/package.json'), config: { foundInB: true }, }); }; test('async', async () => { const readFileSpy = vi.spyOn(fsPromises, 'readFile'); const explorer = cosmiconfig('foo'); // First pass, prime the cache ... await explorer.search(firstSearchPath); // Reset readFile mocks and search again. readFileSpy.mockClear(); const result = await explorer.load(loadPath); checkResult(readFileSpy, result); }); test('sync', () => { const readFileSpy = vi.spyOn(fs, 'readFileSync'); const explorer = cosmiconfigSync('foo'); // First pass, prime the cache ... explorer.search(firstSearchPath); // Reset readFile mocks and search again. readFileSpy.mockClear(); const result = explorer.load(loadPath); checkResult(readFileSpy, result); }); }); describe('cache is not used in a new cosmiconfig instance', () => { const searchPath = temp.absolutePath('a/b/c/d/e'); const expectedFilesChecked = [ 'a/b/c/d/e/package.json', 'a/b/c/d/e/.foorc', 'a/b/c/d/e/.foorc.json', 'a/b/c/d/e/.foorc.yaml', 'a/b/c/d/e/.foorc.yml', 'a/b/c/d/e/.foorc.js', 'a/b/c/d/e/.foorc.ts', 'a/b/c/d/e/.foorc.cjs', 'a/b/c/d/e/.foorc.mjs', 'a/b/c/d/e/.config/foorc', 'a/b/c/d/e/.config/foorc.json', 'a/b/c/d/e/.config/foorc.yaml', 'a/b/c/d/e/.config/foorc.yml', 'a/b/c/d/e/.config/foorc.js', 'a/b/c/d/e/.config/foorc.ts', 'a/b/c/d/e/.config/foorc.cjs', 'a/b/c/d/e/.config/foorc.mjs', 'a/b/c/d/e/foo.config.js', 'a/b/c/d/e/foo.config.ts', 'a/b/c/d/e/foo.config.cjs', 'a/b/c/d/e/foo.config.mjs', 'a/b/c/d/package.json', 'a/b/c/d/.foorc', ]; const checkResult = (readFileSpy: any, result: any, files: any) => { const filesChecked = temp.getSpyPathCalls(readFileSpy); expect(filesChecked).toEqual(files); expect(result).toEqual({ filepath: temp.absolutePath('a/b/c/d/.foorc'), config: { foundInD: true }, }); }; test('async', async () => { // First pass, prime the cache ... await cosmiconfig('foo', { searchStrategy: 'global' }).search(searchPath); // Search again. const explorer = cosmiconfig('foo', { searchStrategy: 'global' }); const readFileSpy = vi.spyOn(fsPromises, 'readFile'); const result = await explorer.search(searchPath); checkResult(readFileSpy, result, expectedFilesChecked); }); test('sync', () => { // First pass, prime the cache ... cosmiconfigSync('foo', { searchStrategy: 'global' }).search(searchPath); // Search again. const explorer = cosmiconfigSync('foo', { searchStrategy: 'global' }); const readFileSpy = vi.spyOn(fs, 'readFileSync'); const result = explorer.search(searchPath); checkResult(readFileSpy, result, expectedFilesChecked.filter(isNotMjs)); }); }); describe('clears file cache on calling clearLoadCache', () => { const loadPath = temp.absolutePath('a/b/c/d/.foorc'); const checkResult = (readFileSpy: any, result: any) => { const filesChecked = temp.getSpyPathCalls(readFileSpy); expect(filesChecked).toEqual(['a/b/c/d/.foorc']); expect(result).toEqual({ filepath: temp.absolutePath('a/b/c/d/.foorc'), config: { foundInD: true }, }); }; test('async', async () => { const readFileSpy = vi.spyOn(fsPromises, 'readFile'); const explorer = cosmiconfig('foo'); await explorer.load(loadPath); readFileSpy.mockClear(); explorer.clearLoadCache(); const result = await explorer.load(loadPath); checkResult(readFileSpy, result); }); test('sync', () => { const readFileSpy = vi.spyOn(fs, 'readFileSync'); const explorer = cosmiconfigSync('foo'); explorer.load(loadPath); // Reset readFile mocks and search again. readFileSpy.mockClear(); explorer.clearLoadCache(); const result = explorer.load(loadPath); checkResult(readFileSpy, result); }); }); describe('clears file cache on calling clearCaches', () => { const loadPath = temp.absolutePath('a/b/c/d/.foorc'); const checkResult = (readFileSpy: any, result: any) => { const filesChecked = temp.getSpyPathCalls(readFileSpy); expect(filesChecked).toEqual(['a/b/c/d/.foorc']); expect(result).toEqual({ filepath: temp.absolutePath('a/b/c/d/.foorc'), config: { foundInD: true }, }); }; test('async', async () => { const readFileSpy = vi.spyOn(fsPromises, 'readFile'); const explorer = cosmiconfig('foo'); await explorer.load(loadPath); readFileSpy.mockClear(); explorer.clearCaches(); const result = await explorer.load(loadPath); checkResult(readFileSpy, result); }); test('sync', () => { const readFileSpy = vi.spyOn(fs, 'readFileSync'); const explorer = cosmiconfigSync('foo'); explorer.load(loadPath); // Reset readFile mocks and search again. readFileSpy.mockClear(); explorer.clearCaches(); const result = explorer.load(loadPath); checkResult(readFileSpy, result); }); }); describe('clears directory cache on calling clearSearchCache', () => { const searchPath = temp.absolutePath('a/b/c/d/e'); const expectedFilesChecked = [ 'a/b/c/d/e/package.json', 'a/b/c/d/e/.foorc', 'a/b/c/d/e/.foorc.json', 'a/b/c/d/e/.foorc.yaml', 'a/b/c/d/e/.foorc.yml', 'a/b/c/d/e/.foorc.js', 'a/b/c/d/e/.foorc.ts', 'a/b/c/d/e/.foorc.cjs', 'a/b/c/d/e/.foorc.mjs', 'a/b/c/d/e/.config/foorc', 'a/b/c/d/e/.config/foorc.json', 'a/b/c/d/e/.config/foorc.yaml', 'a/b/c/d/e/.config/foorc.yml', 'a/b/c/d/e/.config/foorc.js', 'a/b/c/d/e/.config/foorc.ts', 'a/b/c/d/e/.config/foorc.cjs', 'a/b/c/d/e/.config/foorc.mjs', 'a/b/c/d/e/foo.config.js', 'a/b/c/d/e/foo.config.ts', 'a/b/c/d/e/foo.config.cjs', 'a/b/c/d/e/foo.config.mjs', 'a/b/c/d/package.json', 'a/b/c/d/.foorc', ]; const checkResult = (readFileSpy: any, result: any, files: any) => { const filesChecked = temp.getSpyPathCalls(readFileSpy); expect(filesChecked).toEqual(files); expect(result).toEqual({ filepath: temp.absolutePath('a/b/c/d/.foorc'), config: { foundInD: true }, }); }; test('async', async () => { const readFileSpy = vi.spyOn(fsPromises, 'readFile'); const explorer = cosmiconfig('foo', { searchStrategy: 'global' }); await explorer.search(searchPath); readFileSpy.mockClear(); explorer.clearSearchCache(); const result = await explorer.search(searchPath); checkResult(readFileSpy, result, expectedFilesChecked); }); test('sync', () => { const readFileSpy = vi.spyOn(fs, 'readFileSync'); const explorer = cosmiconfigSync('foo', { searchStrategy: 'global' }); explorer.search(searchPath); // Reset readFile mocks and search again. readFileSpy.mockClear(); explorer.clearSearchCache(); const result = explorer.search(searchPath); checkResult(readFileSpy, result, expectedFilesChecked.filter(isNotMjs)); }); }); describe('clears directory cache on calling clearCaches', () => { const searchPath = temp.absolutePath('a/b/c/d/e'); const expectedFilesChecked = [ 'a/b/c/d/e/package.json', 'a/b/c/d/e/.foorc', 'a/b/c/d/e/.foorc.json', 'a/b/c/d/e/.foorc.yaml', 'a/b/c/d/e/.foorc.yml', 'a/b/c/d/e/.foorc.js', 'a/b/c/d/e/.foorc.ts', 'a/b/c/d/e/.foorc.cjs', 'a/b/c/d/e/.foorc.mjs', 'a/b/c/d/e/.config/foorc', 'a/b/c/d/e/.config/foorc.json', 'a/b/c/d/e/.config/foorc.yaml', 'a/b/c/d/e/.config/foorc.yml', 'a/b/c/d/e/.config/foorc.js', 'a/b/c/d/e/.config/foorc.ts', 'a/b/c/d/e/.config/foorc.cjs', 'a/b/c/d/e/.config/foorc.mjs', 'a/b/c/d/e/foo.config.js', 'a/b/c/d/e/foo.config.ts', 'a/b/c/d/e/foo.config.cjs', 'a/b/c/d/e/foo.config.mjs', 'a/b/c/d/package.json', 'a/b/c/d/.foorc', ]; const checkResult = (readFileSpy: any, result: any, files: any) => { const filesChecked = temp.getSpyPathCalls(readFileSpy); expect(filesChecked).toEqual(files); expect(result).toEqual({ filepath: temp.absolutePath('a/b/c/d/.foorc'), config: { foundInD: true }, }); }; test('async', async () => { const readFileSpy = vi.spyOn(fsPromises, 'readFile'); const explorer = cosmiconfig('foo', { searchStrategy: 'global' }); await explorer.search(searchPath); readFileSpy.mockClear(); explorer.clearCaches(); const result = await explorer.search(searchPath); checkResult(readFileSpy, result, expectedFilesChecked); }); test('sync', () => { const readFileSpy = vi.spyOn(fs, 'readFileSync'); const explorer = cosmiconfigSync('foo', { searchStrategy: 'global' }); explorer.search(searchPath); // Reset readFile mocks and search again. readFileSpy.mockClear(); explorer.clearCaches(); const result = explorer.search(searchPath); checkResult(readFileSpy, result, expectedFilesChecked.filter(isNotMjs)); }); }); describe('with cache disabled', () => { const explorer = cosmiconfig('foo', { cache: false }); test('does not throw an error when clearFileCache is called', () => { expect(() => explorer.clearLoadCache()).not.toThrow(); }); test('does not throw an error when clearDirectoryCache is called', () => { expect(() => explorer.clearSearchCache()).not.toThrow(); }); test('does not throw an error when clearCaches is called', () => { expect(() => explorer.clearCaches()).not.toThrow(); }); }); describe('with cache disabled, does not cache directory results', () => { const searchPath = temp.absolutePath('a/b/c/d/e'); const expectedFilesChecked = [ 'a/b/c/d/e/package.json', 'a/b/c/d/e/.foorc', 'a/b/c/d/e/.foorc.json', 'a/b/c/d/e/.foorc.yaml', 'a/b/c/d/e/.foorc.yml', 'a/b/c/d/e/.foorc.js', 'a/b/c/d/e/.foorc.ts', 'a/b/c/d/e/.foorc.cjs', 'a/b/c/d/e/.foorc.mjs', 'a/b/c/d/e/.config/foorc', 'a/b/c/d/e/.config/foorc.json', 'a/b/c/d/e/.config/foorc.yaml', 'a/b/c/d/e/.config/foorc.yml', 'a/b/c/d/e/.config/foorc.js', 'a/b/c/d/e/.config/foorc.ts', 'a/b/c/d/e/.config/foorc.cjs', 'a/b/c/d/e/.config/foorc.mjs', 'a/b/c/d/e/foo.config.js', 'a/b/c/d/e/foo.config.ts', 'a/b/c/d/e/foo.config.cjs', 'a/b/c/d/e/foo.config.mjs', 'a/b/c/d/package.json', 'a/b/c/d/.foorc', ]; const checkResult = (readFileSpy: any, result: any, files: any) => { const filesChecked = temp.getSpyPathCalls(readFileSpy); expect(filesChecked).toEqual(files); expect(result).toEqual({ filepath: temp.absolutePath('a/b/c/d/.foorc'), config: { foundInD: true }, }); }; test('async', async () => { const readFileSpy = vi.spyOn(fsPromises, 'readFile'); const explorer = cosmiconfig('foo', { searchStrategy: 'global', cache: false, }); await explorer.search(searchPath); readFileSpy.mockClear(); const result = await explorer.search(searchPath); checkResult(readFileSpy, result, expectedFilesChecked); }); test('sync', () => { const readFileSpy = vi.spyOn(fs, 'readFileSync'); const explorer = cosmiconfigSync('foo', { searchStrategy: 'global', cache: false, }); explorer.search(searchPath); readFileSpy.mockClear(); const result = explorer.search(searchPath); checkResult(readFileSpy, result, expectedFilesChecked.filter(isNotMjs)); }); }); describe('with cache disabled, does not cache file results', () => { const loadPath = temp.absolutePath('a/b/c/d/.foorc'); const checkResult = (readFileSpy: any, result: any) => { const filesChecked = temp.getSpyPathCalls(readFileSpy); expect(filesChecked).toEqual(['a/b/c/d/.foorc']); expect(result).toEqual({ filepath: temp.absolutePath('a/b/c/d/.foorc'), config: { foundInD: true }, }); }; test('async', async () => { const readFileSpy = vi.spyOn(fsPromises, 'readFile'); const explorer = cosmiconfig('foo', { cache: false }); await explorer.load(loadPath); readFileSpy.mockClear(); const result = await explorer.load(loadPath); checkResult(readFileSpy, result); }); test('sync', () => { const readFileSpy = vi.spyOn(fs, 'readFileSync'); const explorer = cosmiconfigSync('foo', { cache: false }); explorer.load(loadPath); readFileSpy.mockClear(); const result = explorer.load(loadPath); checkResult(readFileSpy, result); }); }); cosmiconfig-9.0.1/test/failed-directories.test.ts000066400000000000000000000473221515130774000220770ustar00rootroot00000000000000import envPaths from 'env-paths'; import path from 'path'; import { beforeEach, afterAll, describe, expect, test, vi } from 'vitest'; import fs from 'fs'; import fsPromises from 'fs/promises'; import { cosmiconfig, cosmiconfigSync, OptionsSync, defaultLoaders, } from '../src'; import { isNotMjs, normalizeDirectorySlash, TempDir } from './util'; const temp = new TempDir(); beforeEach(() => { temp.clean(); temp.createDir('a/b/c/d/e/f/'); }); afterAll(() => { // Remove temp.dir created for tests temp.deleteTempDir(); }); describe('gives up if it cannot find the file', () => { const startDir = temp.absolutePath('a/b'); const explorerOptions = { stopDir: temp.absolutePath('.') }; const globalConfigPath = envPaths('foo', { suffix: '' }).config; // sorry, we have to create this folder even on a testing system, so we can spy on the reads properly fs.mkdirSync(globalConfigPath, { recursive: true }); const relativeGlobalConfigPath = path.relative(temp.dir, globalConfigPath); const expectedFilesChecked = [ 'a/b/package.json', 'a/b/.foorc', 'a/b/.foorc.json', 'a/b/.foorc.yaml', 'a/b/.foorc.yml', 'a/b/.foorc.js', 'a/b/.foorc.ts', 'a/b/.foorc.cjs', 'a/b/.foorc.mjs', 'a/b/.config/foorc', 'a/b/.config/foorc.json', 'a/b/.config/foorc.yaml', 'a/b/.config/foorc.yml', 'a/b/.config/foorc.js', 'a/b/.config/foorc.ts', 'a/b/.config/foorc.cjs', 'a/b/.config/foorc.mjs', 'a/b/foo.config.js', 'a/b/foo.config.ts', 'a/b/foo.config.cjs', 'a/b/foo.config.mjs', 'a/package.json', 'a/.foorc', 'a/.foorc.json', 'a/.foorc.yaml', 'a/.foorc.yml', 'a/.foorc.js', 'a/.foorc.ts', 'a/.foorc.cjs', 'a/.foorc.mjs', 'a/.config/foorc', 'a/.config/foorc.json', 'a/.config/foorc.yaml', 'a/.config/foorc.yml', 'a/.config/foorc.js', 'a/.config/foorc.ts', 'a/.config/foorc.cjs', 'a/.config/foorc.mjs', 'a/foo.config.js', 'a/foo.config.ts', 'a/foo.config.cjs', 'a/foo.config.mjs', 'package.json', '.foorc', '.foorc.json', '.foorc.yaml', '.foorc.yml', '.foorc.js', '.foorc.ts', '.foorc.cjs', '.foorc.mjs', '.config/foorc', '.config/foorc.json', '.config/foorc.yaml', '.config/foorc.yml', '.config/foorc.js', '.config/foorc.ts', '.config/foorc.cjs', '.config/foorc.mjs', 'foo.config.js', 'foo.config.ts', 'foo.config.cjs', 'foo.config.mjs', ...[ 'config', 'config.json', 'config.yaml', 'config.yml', 'config.js', 'config.ts', 'config.cjs', 'config.mjs', ].map((place) => normalizeDirectorySlash(path.join(relativeGlobalConfigPath, place)), ), ]; const checkResult = ( statSpy: any, readFileSpy: any, result: any, files: any, ) => { const statPath = temp.getSpyPathCalls(statSpy); expect(statPath).toEqual([ 'a/b', 'a', '', normalizeDirectorySlash(relativeGlobalConfigPath), ]); const filesChecked = temp.getSpyPathCalls(readFileSpy); expect(filesChecked).toEqual(files); expect(result).toBeNull(); }; test('async', async () => { const explorer = cosmiconfig('foo', explorerOptions); const readFileSpy = vi.spyOn(fsPromises, 'readFile'); const statSpy = vi.spyOn(fsPromises, 'stat'); const result = await explorer.search(startDir); checkResult(statSpy, readFileSpy, result, expectedFilesChecked); }); test('sync', () => { const explorer = cosmiconfigSync('foo', explorerOptions); const readFileSpy = vi.spyOn(fs, 'readFileSync'); const statSpy = vi.spyOn(fs, 'statSync'); const result = explorer.search(startDir); checkResult( statSpy, readFileSpy, result, expectedFilesChecked.filter(isNotMjs), ); }); }); describe('stops at stopDir and gives up', () => { const startDir = temp.absolutePath('a/b'); const explorerOptions = { stopDir: temp.absolutePath('a') }; const globalConfigPath = envPaths('foo', { suffix: '' }).config; // sorry, we have to create this folder even on a testing system, so we can spy on the reads properly fs.mkdirSync(globalConfigPath, { recursive: true }); const relativeGlobalConfigPath = path.relative(temp.dir, globalConfigPath); const expectedFilesChecked = [ 'a/b/package.json', 'a/b/.foorc', 'a/b/.foorc.json', 'a/b/.foorc.yaml', 'a/b/.foorc.yml', 'a/b/.foorc.js', 'a/b/.foorc.ts', 'a/b/.foorc.cjs', 'a/b/.foorc.mjs', 'a/b/.config/foorc', 'a/b/.config/foorc.json', 'a/b/.config/foorc.yaml', 'a/b/.config/foorc.yml', 'a/b/.config/foorc.js', 'a/b/.config/foorc.ts', 'a/b/.config/foorc.cjs', 'a/b/.config/foorc.mjs', 'a/b/foo.config.js', 'a/b/foo.config.ts', 'a/b/foo.config.cjs', 'a/b/foo.config.mjs', 'a/package.json', 'a/.foorc', 'a/.foorc.json', 'a/.foorc.yaml', 'a/.foorc.yml', 'a/.foorc.js', 'a/.foorc.ts', 'a/.foorc.cjs', 'a/.foorc.mjs', 'a/.config/foorc', 'a/.config/foorc.json', 'a/.config/foorc.yaml', 'a/.config/foorc.yml', 'a/.config/foorc.js', 'a/.config/foorc.ts', 'a/.config/foorc.cjs', 'a/.config/foorc.mjs', 'a/foo.config.js', 'a/foo.config.ts', 'a/foo.config.cjs', 'a/foo.config.mjs', ...[ 'config', 'config.json', 'config.yaml', 'config.yml', 'config.js', 'config.ts', 'config.cjs', 'config.mjs', ].map((place) => normalizeDirectorySlash(path.join(relativeGlobalConfigPath, place)), ), ]; const checkResult = (readFileSpy: any, result: any, files: any) => { const filesChecked = temp.getSpyPathCalls(readFileSpy); expect(filesChecked).toEqual(files); expect(result).toBeNull(); }; test('async', async () => { const explorer = cosmiconfig('foo', explorerOptions); const readFileSpy = vi.spyOn(fsPromises, 'readFile'); const result = await explorer.search(startDir); checkResult(readFileSpy, result, expectedFilesChecked); }); test('sync', () => { const explorer = cosmiconfigSync('foo', explorerOptions); const readFileSpy = vi.spyOn(fs, 'readFileSync'); const result = explorer.search(startDir); checkResult(readFileSpy, result, expectedFilesChecked.filter(isNotMjs)); }); }); describe('throws error for invalid YAML in rc file', () => { beforeEach(() => { temp.createFile('a/b/.foorc', 'found: true: broken'); }); const startDir = temp.absolutePath('a/b'); const explorerOptions = { stopDir: temp.absolutePath('a') }; const expectedError = `YAML Error in ${temp.absolutePath( 'a/b/.foorc', )}:\nbad indentation of a mapping entry (1:12)`; test('async', async () => { await expect( cosmiconfig('foo', explorerOptions).search(startDir), ).rejects.toThrow(expectedError); }); test('sync', () => { expect(() => cosmiconfigSync('foo', explorerOptions).search(startDir), ).toThrow(expectedError); }); }); describe('throws error for invalid JSON in extensionless rc file loaded as JSON', () => { beforeEach(() => { temp.createFile('a/b/.foorc', 'found: true: broken'); }); const startDir = temp.absolutePath('a/b'); const explorerOptions = { stopDir: temp.absolutePath('a'), loaders: { noExt: defaultLoaders['.json'], }, }; const expectedError = 'JSON Error in'; test('async', async () => { await expect( cosmiconfig('foo', explorerOptions).search(startDir), ).rejects.toThrow(expectedError); }); test('sync', () => { expect(() => cosmiconfigSync('foo', explorerOptions).search(startDir), ).toThrow(expectedError); }); }); describe('throws error for invalid package.json', () => { beforeEach(() => { temp.createFile('a/b/package.json', '{ "foo": "bar", }'); }); const startDir = temp.absolutePath('a/b'); const explorerOptions = { stopDir: temp.absolutePath('a') }; const expectedError = 'JSON Error in'; test('async', async () => { await expect( cosmiconfig('foo', explorerOptions).search(startDir), ).rejects.toThrow(expectedError); }); test('sync', () => { expect(() => cosmiconfigSync('foo', explorerOptions).search(startDir), ).toThrow(expectedError); }); }); describe('throws error for invalid JS in .config.js file', () => { beforeEach(() => { temp.createFile( 'a/b/foo.config.js', 'module.exports = { found: true: false,', ); }); const startDir = temp.absolutePath('a/b'); const explorerOptions = { stopDir: temp.absolutePath('a') }; test('async', async () => { await expect( cosmiconfig('foo', explorerOptions).search(startDir), ).rejects.toThrow(); }); test('sync', () => { expect(() => cosmiconfigSync('foo', explorerOptions).search(startDir), ).toThrow(); }); }); describe('throws error for invalid JSON in .foorc.json', () => { beforeEach(() => { temp.createFile('a/b/c/d/e/f/.foorc.json', '{ "found": true,, }'); }); const startDir = temp.absolutePath('a/b/c/d/e/f'); const explorerOptions = { stopDir: temp.absolutePath('.'), }; const expectedError = 'JSON Error in'; test('async', async () => { await expect( cosmiconfig('foo', explorerOptions).search(startDir), ).rejects.toThrow(expectedError); }); test('sync', () => { expect(() => cosmiconfigSync('foo', explorerOptions).search(startDir), ).toThrow(expectedError); }); }); describe('throws error for invalid YAML in .foorc.yml', () => { beforeEach(() => { temp.createFile('a/b/c/d/e/f/.foorc.yml', 'found: thing: true'); }); const startDir = temp.absolutePath('a/b/c/d/e/f'); const explorerOptions = { stopDir: temp.absolutePath('.'), }; const expectedError = `YAML Error in ${temp.absolutePath( 'a/b/c/d/e/f/.foorc.yml', )}:\nbad indentation of a mapping entry (1:13)`; test('async', async () => { await expect( cosmiconfig('foo', explorerOptions).search(startDir), ).rejects.toThrow(expectedError); }); test('sync', () => { expect(() => cosmiconfigSync('foo', explorerOptions).search(startDir), ).toThrow(expectedError); }); }); describe('searching for rc files with specified extensions, throws error for invalid JS in .foorc.js', () => { beforeEach(() => { temp.createFile('a/b/c/d/e/f/.foorc.js', 'module.exports = found: true };'); }); const startDir = temp.absolutePath('a/b/c/d/e/f'); const explorerOptions = { stopDir: temp.absolutePath('.'), searchPlaces: [ 'package.json', '.foorc', '.foorc.json', '.foorc.yaml', '.foorc.yml', '.foorc.js', 'foo.config.js', ], }; test('async', async () => { await expect( cosmiconfig('foo', explorerOptions).search(startDir), ).rejects.toThrow(); }); test('sync', () => { expect(() => cosmiconfigSync('foo', explorerOptions).search(startDir), ).toThrow(); }); }); describe('without ignoring empty files, returns an empty config result for an empty rc file', () => { beforeEach(() => { temp.createFile('a/b/.foorc', ''); }); const startDir = temp.absolutePath('a/b'); const explorerOptions = { stopDir: temp.absolutePath('a'), ignoreEmptySearchPlaces: false, }; const checkResult = (result: any) => { expect(result).toEqual({ config: undefined, filepath: temp.absolutePath('a/b/.foorc'), isEmpty: true, }); }; test('async', async () => { const result = await cosmiconfig('foo', explorerOptions).search(startDir); checkResult(result); }); test('sync', () => { const result = cosmiconfigSync('foo', explorerOptions).search(startDir); checkResult(result); }); }); describe('without ignoring empty files, returns an empty config result for an empty .config.js file', () => { beforeEach(() => { temp.createFile('a/b/foo.config.js', ''); }); const startDir = temp.absolutePath('a/b'); const explorerOptions = { stopDir: temp.absolutePath('a'), ignoreEmptySearchPlaces: false, }; const checkResult = (result: any) => { expect(result).toEqual({ config: undefined, filepath: temp.absolutePath('a/b/foo.config.js'), isEmpty: true, }); }; test('async', async () => { const result = await cosmiconfig('foo', explorerOptions).search(startDir); checkResult(result); }); test('sync', () => { const result = cosmiconfigSync('foo', explorerOptions).search(startDir); checkResult(result); }); }); describe('without ignoring empty files, returns an empty config result for an empty .json rc file', () => { beforeEach(() => { temp.createFile('a/b/c/d/e/f/.foorc.json', ''); }); const startDir = temp.absolutePath('a/b/c/d/e/f'); const explorerOptions = { stopDir: temp.absolutePath('a'), ignoreEmptySearchPlaces: false, }; const checkResult = (result: any) => { expect(result).toEqual({ config: undefined, filepath: temp.absolutePath('a/b/c/d/e/f/.foorc.json'), isEmpty: true, }); }; test('async', async () => { const result = await cosmiconfig('foo', explorerOptions).search(startDir); checkResult(result); }); test('sync', () => { const result = cosmiconfigSync('foo', explorerOptions).search(startDir); checkResult(result); }); }); describe('returns an empty config result for an empty .yaml rc file', () => { beforeEach(() => { temp.createFile('a/b/c/d/e/f/.foorc.yaml', ''); }); const startDir = temp.absolutePath('a/b/c/d/e/f'); const explorerOptions = { stopDir: temp.absolutePath('a'), ignoreEmptySearchPlaces: false, }; const checkResult = (result: any) => { expect(result).toEqual({ config: undefined, filepath: temp.absolutePath('a/b/c/d/e/f/.foorc.yaml'), isEmpty: true, }); }; test('async', async () => { const result = await cosmiconfig('foo', explorerOptions).search(startDir); checkResult(result); }); test('sync', () => { const result = cosmiconfigSync('foo', explorerOptions).search(startDir); checkResult(result); }); }); describe('without ignoring empty configs and searching for rc files with specified extensions, returns an empty config result for an empty .js rc file', () => { beforeEach(() => { temp.createFile('a/b/c/d/e/f/.foorc.js', ''); }); const startDir = temp.absolutePath('a/b/c/d/e/f'); const explorerOptions = { stopDir: temp.absolutePath('a'), ignoreEmptySearchPlaces: false, searchPlaces: [ 'package.json', '.foorc', '.foorc.json', '.foorc.yaml', '.foorc.yml', '.foorc.js', 'foo.config.js', ], }; const checkResult = (result: any) => { expect(result).toEqual({ config: undefined, filepath: temp.absolutePath('a/b/c/d/e/f/.foorc.js'), isEmpty: true, }); }; test('async', async () => { const result = await cosmiconfig('foo', explorerOptions).search(startDir); checkResult(result); }); test('sync', () => { const result = cosmiconfigSync('foo', explorerOptions).search(startDir); checkResult(result); }); }); describe('without ignoring empty configs and searching for rc files with specified extensions, returns an empty config result for an empty .js rc file with whitespace', () => { beforeEach(() => { temp.createFile('a/b/c/d/e/f/.foorc.js', ' \t\r\v\n\f'); }); const startDir = temp.absolutePath('a/b/c/d/e/f'); const explorerOptions = { stopDir: temp.absolutePath('a'), ignoreEmptySearchPlaces: false, searchPlaces: [ 'package.json', '.foorc', '.foorc.json', '.foorc.yaml', '.foorc.yml', '.foorc.js', 'foo.config.js', ], }; const checkResult = (result: any) => { expect(result).toEqual({ config: undefined, filepath: temp.absolutePath('a/b/c/d/e/f/.foorc.js'), isEmpty: true, }); }; test('async', async () => { const result = await cosmiconfig('foo', explorerOptions).search(startDir); checkResult(result); }); test('sync', () => { const result = cosmiconfigSync('foo', explorerOptions).search(startDir); checkResult(result); }); }); describe('throws error if a file in searchPlaces does not have a corresponding loader', () => { const explorerOptions = { stopDir: temp.absolutePath('.'), searchPlaces: [ 'package.json', '.foorc', '.foorc.things', '.foorc.js', 'foo.config.js', ], }; test('on instantiation', () => { expect(() => cosmiconfigSync('foo', explorerOptions)).toThrow( 'Missing loader for extension ".foorc.things"', ); }); }); describe('throws error if an extensionless file in searchPlaces does not have a corresponding loader', () => { beforeEach(() => { temp.createFile('a/b/c/d/e/f/.foorc', '{ "foo": "bar" }'); }); const explorerOptions = { stopDir: temp.absolutePath('.'), searchPlaces: ['package.json', '.foorc'], loaders: { noExt: undefined, }, } as unknown as OptionsSync; test('on instantiation', () => { expect(() => cosmiconfigSync('foo', explorerOptions)).toThrow( 'Missing loader for extension ".foorc".', ); }); }); describe('does not throw error when trying to access a folder that is actually a file', () => { beforeEach(() => { temp.createFile('.config', ''); }); const tempDir = temp.absolutePath('.'); const explorerOptions = { stopDir: tempDir, }; test('async', async () => { const result = await cosmiconfig('foo', explorerOptions).search(tempDir); expect(result).toBeNull(); }); test('sync', () => { const result = cosmiconfigSync('foo', explorerOptions).search(tempDir); expect(result).toBeNull(); }); }); describe('does not swallow errors from custom loaders', () => { beforeEach(() => { temp.createFile('a/b/c/d/e/f/.foorc.js', 'module.exports = {};'); }); const expectedError = new Error('Failed to load JS'); const loadJs = () => { throw expectedError; }; const startDir = temp.absolutePath('a/b/c/d/e/f'); const explorerOptions = { stopDir: temp.absolutePath('.'), searchPlaces: ['package.json', '.foorc', '.foorc.js'], loaders: { '.js': loadJs, }, }; test('async', async () => { await expect( cosmiconfig('foo', explorerOptions).search(startDir), ).rejects.toThrow(expectedError); }); test('sync', () => { expect(() => cosmiconfigSync('foo', explorerOptions).search(startDir), ).toThrow(expectedError); }); }); describe('errors not swallowed when async custom loader throws them', () => { const startDir = temp.absolutePath('a/b/c/d/e/f'); beforeEach(() => { temp.createFile( 'a/b/c/d/e/f/.foorc.things', 'one\ntwo\nthree\t\t\n four\n', ); }); const expectedError = new Error('loadThingsAsync error'); const loadThingsAsync = () => { throw expectedError; }; const explorerOptions = { stopDir: temp.absolutePath('.'), searchPlaces: ['.foorc.things'], loaders: { '.things': loadThingsAsync, }, }; test('async', async () => { await expect( cosmiconfig('foo', explorerOptions).search(startDir), ).rejects.toThrow(expectedError); }); }); describe('errors not swallowed when async custom loader rejects', () => { const startDir = temp.absolutePath('a/b/c/d/e/f'); beforeEach(() => { temp.createFile( 'a/b/c/d/e/f/.foorc.things', 'one\ntwo\nthree\t\t\n four\n', ); }); const expectedError = new Error('loadThingsAsync error'); const loadThingsAsync = async () => { throw expectedError; }; const explorerOptions = { stopDir: temp.absolutePath('.'), searchPlaces: ['.foorc.things'], loaders: { '.things': loadThingsAsync, }, }; test('async', async () => { await expect( cosmiconfig('foo', explorerOptions).search(startDir), ).rejects.toThrow(expectedError); }); }); cosmiconfig-9.0.1/test/failed-files.test.ts000066400000000000000000000202571515130774000206630ustar00rootroot00000000000000import { beforeEach, afterAll, describe, test, expect, vi } from 'vitest'; import { TempDir } from './util'; import { cosmiconfig, cosmiconfigSync } from '../src'; const temp = new TempDir(); beforeEach(() => { temp.clean(); }); afterAll(() => { // Remove temp.dir created for tests temp.deleteTempDir(); }); describe('throws error if defined file does not exist', () => { const file = temp.absolutePath('does/not/exist'); const expectedError = 'ENOENT: no such file or directory'; test('async', async () => { await expect(cosmiconfig('failed-files-tests').load(file)).rejects.toThrow( expectedError, ); }); test('sync', () => { expect(() => cosmiconfigSync('failed-files-tests').load(file)).toThrow( expectedError, ); }); }); describe('throws error if defined JSON file has syntax error', () => { beforeEach(() => { temp.createFile('foo-invalid.json', '{ "foo": true: }'); }); const file = temp.absolutePath('foo-invalid.json'); const expectedError = 'JSON Error in'; test('async', async () => { await expect(cosmiconfig('failed-files-tests').load(file)).rejects.toThrow( expectedError, ); }); test('sync', () => { expect(() => cosmiconfigSync('failed-files-tests').load(file)).toThrow( expectedError, ); }); }); describe('throws error if defined YAML file has syntax error', () => { beforeEach(() => { temp.createFile('foo-invalid.yaml', 'foo: true: false'); }); const file = temp.absolutePath('foo-invalid.yaml'); const expectedError = `YAML Error in ${file}:\nbad indentation of a mapping entry (1:10)`; test('async', async () => { await expect(cosmiconfig('failed-files-tests').load(file)).rejects.toThrow( expectedError, ); }); test('sync', () => { expect(() => cosmiconfigSync('failed-files-tests').load(file)).toThrow( expectedError, ); }); }); describe('throws error if defined JS file has syntax error', () => { beforeEach(() => { temp.createFile('foo-invalid.js', 'module.exports = { foo }'); }); const file = temp.absolutePath('foo-invalid.js'); const expectedError = 'foo is not defined'; test('async', async () => { await expect(cosmiconfig('failed-files-tests').load(file)).rejects.toThrow( expectedError, ); }); test('sync', () => { expect(() => cosmiconfigSync('failed-files-tests').load(file)).toThrow( expectedError, ); }); }); describe('throws error if defined TS file has syntax error', () => { beforeEach(() => { temp.createFile('foo-invalid.ts', 'export default {--}'); }); const file = temp.absolutePath('foo-invalid.ts'); test('async', async () => { await expect( cosmiconfig('failed-files-tests').load(file), ).rejects.toThrow(); }); test('sync', () => { expect(() => cosmiconfigSync('failed-files-tests').load(file)).toThrow(); }); }); describe('throws error if defined tsconfig.json file has syntax error', () => { beforeEach(() => { temp.createFile('foo.ts', 'export default {}'); temp.createFile('tsconfig.json', 'fdsfko'); }); const file = temp.absolutePath('foo.ts'); test('async', async () => { await expect(cosmiconfig('failed-files-tests').load(file)).rejects.toThrow( "tsconfig.json: '{' expected.", ); }); test('sync', () => { expect(() => cosmiconfigSync('failed-files-tests').load(file)).toThrow( "tsconfig.json: '{' expected.", ); }); }); describe('returns an empty config result for empty file, format JS', () => { beforeEach(() => { temp.createFile('foo-empty.js', ''); }); const file = temp.absolutePath('foo-empty.js'); const checkResult = (result: any) => { expect(result).toEqual({ config: undefined, filepath: file, isEmpty: true, }); }; test('async', async () => { const result = await cosmiconfig('failed-files-tests').load(file); checkResult(result); }); test('sync', () => { const result = cosmiconfigSync('failed-files-tests').load(file); checkResult(result); }); }); describe('returns an empty config result for empty file, format JSON', () => { beforeEach(() => { temp.createFile('foo-empty.json', ''); }); const file = temp.absolutePath('foo-empty.json'); const checkResult = (result: any) => { expect(result).toEqual({ config: undefined, filepath: file, isEmpty: true, }); }; test('async', async () => { const result = await cosmiconfig('failed-files-tests').load(file); checkResult(result); }); test('sync', () => { const result = cosmiconfigSync('failed-files-tests').load(file); checkResult(result); }); }); describe('returns an empty config result for empty file, format YAML', () => { beforeEach(() => { temp.createFile('foo-empty.yaml', ''); }); const file = temp.absolutePath('foo-empty.yaml'); const checkResult = (result: any) => { expect(result).toEqual({ config: undefined, filepath: file, isEmpty: true, }); }; test('async', async () => { const result = await cosmiconfig('failed-files-tests').load(file); checkResult(result); }); test('sync', () => { const result = cosmiconfigSync('failed-files-tests').load(file); checkResult(result); }); }); describe('[#325] transforms & returns null when no config file is found', () => { beforeEach(() => { // temp.createFile('package.json', '{"name": "failed-files-tests"}'); }); const checkTransformResult = vi.fn((result: any) => expect(result).toBeNull(), ); const checkResult = (result: any) => expect(result).toBeNull(); const startAndStopDir = temp.absolutePath('.'); test('async', async () => { const result = await cosmiconfig('failed-files-tests', { searchPlaces: ['package.json'], stopDir: startAndStopDir, async transform(innerResult) { checkTransformResult(innerResult); return innerResult; }, }).search(startAndStopDir); checkResult(result); expect(checkTransformResult).toHaveBeenCalledTimes(1); }); test('sync', () => { const result = cosmiconfigSync('failed-files-tests', { searchPlaces: ['package.json'], stopDir: startAndStopDir, transform(innerResult) { checkTransformResult(innerResult); return innerResult; }, }).search(startAndStopDir); checkResult(result); expect(checkTransformResult).toHaveBeenCalledTimes(1); }); }); describe('errors not swallowed when async custom loader throws them', () => { const file = temp.absolutePath('.foorc.things'); beforeEach(() => { temp.createFile('.foorc.things', 'one\ntwo\nthree\t\t\n four\n'); }); const expectedError = new Error('loadThingsAsync error'); const loadThingsAsync = async () => { throw expectedError; }; const explorerOptions = { loaders: { '.things': loadThingsAsync, }, }; test('async', async () => { await expect( cosmiconfig('not_exist_rc_name', explorerOptions).load(file), ).rejects.toThrow(expectedError); }); }); describe('errors not swallowed when async custom loader rejects', () => { const file = temp.absolutePath('.foorc.things'); beforeEach(() => { temp.createFile('.foorc.things', 'one\ntwo\nthree\t\t\n four\n'); }); const expectedError = new Error('loadThingsAsync error'); const loadThingsAsync = async () => { throw expectedError; }; const explorerOptions = { loaders: { '.things': loadThingsAsync, }, }; test('async', async () => { await expect( cosmiconfig('not_exist_rc_name', explorerOptions).load(file), ).rejects.toThrow(expectedError); }); }); describe('errors if no loader is set for loaded file', () => { const file = temp.absolutePath('.foorc.things'); beforeEach(() => { temp.createFile('.foorc.things', 'one\ntwo\nthree\t\t\n four\n'); }); const explorerOptions = { loaders: {}, }; test('async', async () => { await expect( cosmiconfig('not_exist_rc_name', explorerOptions).load(file), ).rejects.toThrow('No loader specified for extension ".things"'); }); test('sync', () => { expect(() => cosmiconfigSync('not_exist_rc_name', explorerOptions).load(file), ).toThrow('No loader specified for extension ".things"'); }); }); cosmiconfig-9.0.1/test/getPropertyByPath.test.ts000066400000000000000000000044431515130774000217720ustar00rootroot00000000000000import { describe, test, expect } from 'vitest'; import { getPropertyByPath } from '../src/util'; const source = { ant: { beetle: { cootie: { flea: 'foo', }, louse: { vermin: 'bar', }, }, 'fancy.name': { 'another.fancy.name': 9, }, }, 'ant.beetle.cootie': 333, }; describe('with a property name that includes a period', () => { test('does not treat it as a period-delimited path', () => { expect(getPropertyByPath(source, 'ant.beetle.cootie')).toBe(333); }); }); describe('with period-delimited string path', () => { test('returns a defined value', () => { expect(getPropertyByPath(source, 'ant')).toBe(source.ant); expect(getPropertyByPath(source, 'ant.beetle.cootie.flea')).toBe('foo'); expect(getPropertyByPath(source, 'ant.beetle.louse')).toBe( source.ant.beetle.louse, ); }); test('returns undefined', () => { expect(getPropertyByPath(source, 'beetle')).toBeUndefined(); expect( getPropertyByPath(source, 'ant.beetle.cootie.fleeee'), ).toBeUndefined(); expect(getPropertyByPath(source, 'ant.beetle.vermin')).toBeUndefined(); expect(getPropertyByPath(source, 'ant.fancy.name')).toBeUndefined(); }); }); describe('with array path', () => { test('returns a defined value', () => { expect(getPropertyByPath(source, ['ant'])).toBe(source.ant); expect(getPropertyByPath(source, ['ant', 'beetle', 'cootie', 'flea'])).toBe( 'foo', ); expect(getPropertyByPath(source, ['ant', 'beetle', 'louse'])).toBe( source.ant.beetle.louse, ); }); test('returns undefined', () => { expect(getPropertyByPath(source, ['beetle'])).toBeUndefined(); expect( getPropertyByPath(source, ['ant', 'beetle', 'cootie', 'fleeee']), ).toBeUndefined(); expect( getPropertyByPath(source, ['ant', 'beetle', 'vermin']), ).toBeUndefined(); }); test('handles property names with periods', () => { expect( getPropertyByPath(source, ['ant', 'fancy.name', 'another.fancy.name']), ).toBe(9); expect( getPropertyByPath(source, [ 'ant', 'fancy.name', 'another.fancy.name', 'foo', ]), ).toBeUndefined(); expect(getPropertyByPath(source, ['ant', 'fancy.namez'])).toBeUndefined(); }); }); cosmiconfig-9.0.1/test/import.test.ts000066400000000000000000000165701515130774000176540ustar00rootroot00000000000000import { afterAll, beforeEach, describe, expect, test } from 'vitest'; import { cosmiconfig, cosmiconfigSync } from '../src'; import { TempDir } from './util'; describe('imports', () => { const temp = new TempDir(); beforeEach(() => { temp.clean(); }); afterAll(() => { // Remove temp.dir created for tests temp.deleteTempDir(); }); describe('imports one file', () => { beforeEach(() => { temp.createFile('foo.base.yml', 'bar: 3\nbaz: 4'); temp.createFile('foo.yml', '$import: foo.base.yml\nfoo: 1\nbar: 2'); }); const file = temp.absolutePath('foo.yml'); const checkResult = (result: any) => { expect(result.config).toEqual({ foo: 1, bar: 2, baz: 4 }); }; test('async', async () => { const result = await cosmiconfig('import-tests').load(file); checkResult(result); }); test('sync', () => { const result = cosmiconfigSync('import-tests').load(file); checkResult(result); }); }); describe('imports multiple files', () => { beforeEach(() => { temp.createFile('foo.base.yml', 'bar: 3\nbaz: 4'); temp.createFile('foo.base2.yml', 'baz: 5'); temp.createFile( 'foo.yml', '$import:\n - foo.base.yml\n - foo.base2.yml\nfoo: 1\nbar: 2', ); }); const file = temp.absolutePath('foo.yml'); const checkResult = (result: any) => { expect(result.config).toEqual({ foo: 1, bar: 2, baz: 5 }); }; test('async', async () => { const result = await cosmiconfig('import-tests').load(file); checkResult(result); }); test('sync', () => { const result = cosmiconfigSync('import-tests').load(file); checkResult(result); }); }); describe('merges arrays by default', () => { beforeEach(() => { temp.createFile('foo.base.yml', 'foo: [1, 2]'); temp.createFile('foo.yml', '$import: foo.base.yml\nfoo: [3, 4]'); }); const file = temp.absolutePath('foo.yml'); const checkResult = (result: any) => { expect(result.config).toEqual({ foo: [1, 2, 3, 4] }); }; test('async', async () => { const result = await cosmiconfig('import-tests').load(file); checkResult(result); }); test('sync', () => { const result = cosmiconfigSync('import-tests').load(file); checkResult(result); }); }); describe('overwrites arrays if merge is disabled', () => { beforeEach(() => { temp.createFile('foo.base.yml', 'foo: [1, 2]'); temp.createFile('foo.yml', '$import: foo.base.yml\nfoo: [3, 4]'); }); const file = temp.absolutePath('foo.yml'); const checkResult = (result: any) => { expect(result.config).toEqual({ foo: [3, 4] }); }; test('async', async () => { const result = await cosmiconfig('import-tests', { mergeImportArrays: false, }).load(file); checkResult(result); }); test('sync', () => { const result = cosmiconfigSync('import-tests', { mergeImportArrays: false, }).load(file); checkResult(result); }); }); describe('merges nested objects by default', () => { beforeEach(() => { temp.createFile('foo.base.yml', 'outer:\n bar: 3\n baz: 4'); temp.createFile( 'foo.yml', '$import: foo.base.yml\nouter:\n foo: 1\n bar: 2', ); }); const file = temp.absolutePath('foo.yml'); const checkResult = (result: any) => { expect(result.config).toEqual({ outer: { foo: 1, bar: 2, baz: 4 } }); }; test('async', async () => { const result = await cosmiconfig('import-tests').load(file); checkResult(result); }); test('sync', () => { const result = cosmiconfigSync('import-tests').load(file); checkResult(result); }); }); describe('merges different file formats', () => { beforeEach(() => { temp.createFile('foo.base.json', '{"bar": 3, "baz": 4}'); temp.createFile('foo.yml', '$import: foo.base.json\nfoo: 1\nbar: 2'); }); const file = temp.absolutePath('foo.yml'); const checkResult = (result: any) => { expect(result.config).toEqual({ foo: 1, bar: 2, baz: 4 }); }; test('async', async () => { const result = await cosmiconfig('import-tests').load(file); checkResult(result); }); test('sync', () => { const result = cosmiconfigSync('import-tests').load(file); checkResult(result); }); }); describe('imports a file which in turn imports another file', () => { beforeEach(() => { temp.createFile('foo.base.base.yml', 'bar: 3\nbaz: 4'); temp.createFile('foo.base.yml', '$import: foo.base.base.yml\nbaz: 5'); temp.createFile('foo.yml', '$import: foo.base.yml\nfoo: 1'); }); const file = temp.absolutePath('foo.yml'); const checkResult = (result: any) => { expect(result.config).toEqual({ foo: 1, bar: 3, baz: 5 }); }; test('async', async () => { const result = await cosmiconfig('import-tests').load(file); checkResult(result); }); test('sync', () => { const result = cosmiconfigSync('import-tests').load(file); checkResult(result); }); }); describe("errors when importing something that's not a string", () => { beforeEach(() => { temp.createFile('foo.yml', '$import: 3\nfoo: 1\nbar: 2'); }); const file = temp.absolutePath('foo.yml'); test('async', async () => { await expect( async () => await cosmiconfig('import-tests').load(file), ).rejects.toThrow('a string or a list of strings'); }); test('sync', () => { expect(() => cosmiconfigSync('import-tests').load(file)).toThrow( 'a string or a list of strings', ); }); }); describe('errors when importing a file that does not exist', () => { beforeEach(() => { temp.createFile('foo.yml', '$import: foo.base.yml\nfoo: 1\nbar: 2'); }); const file = temp.absolutePath('foo.yml'); test('async', async () => { await expect( async () => await cosmiconfig('import-tests').load(file), ).rejects.toThrow('no such file or directory'); }); test('sync', () => { expect(() => cosmiconfigSync('import-tests').load(file)).toThrow( 'no such file or directory', ); }); }); describe('errors when trying to import itself', () => { beforeEach(() => { temp.createFile('foo.yml', '$import: foo.yml\nfoo: 1\nbar: 2'); }); const file = temp.absolutePath('foo.yml'); test('async', async () => { await expect( async () => await cosmiconfig('import-tests').load(file), ).rejects.toThrow('Self-import detected'); }); test('sync', () => { expect(() => cosmiconfigSync('import-tests').load(file)).toThrow( 'Self-import detected', ); }); }); describe('errors when trying to import circularly', () => { beforeEach(() => { temp.createFile('foo.base.yml', '$import: foo.base2.yml'); temp.createFile('foo.base2.yml', '$import: foo.base.yml'); temp.createFile('foo.yml', '$import: foo.base.yml\nfoo: 1\nbar: 2'); }); const file = temp.absolutePath('foo.yml'); test('async', async () => { await expect( async () => await cosmiconfig('import-tests').load(file), ).rejects.toThrow('Circular import detected'); }); test('sync', () => { expect(() => cosmiconfigSync('import-tests').load(file)).toThrow( 'Circular import detected', ); }); }); }); cosmiconfig-9.0.1/test/index.test.ts000066400000000000000000000365761515130774000174610ustar00rootroot00000000000000/* eslint-disable @typescript-eslint/no-extraneous-class,@typescript-eslint/explicit-member-accessibility,@typescript-eslint/no-empty-function*/ import path from 'path'; import { afterAll, afterEach, beforeEach, describe, expect, Mock, MockInstance, SpyInstance, test, vi, } from 'vitest'; import { Loader, Loaders, LoaderSync, OptionsSync, defaultLoaders, } from '../src'; import { Explorer } from '../src/Explorer'; import { ExplorerSync } from '../src/ExplorerSync'; import { InternalOptions, InternalOptionsSync, Options } from '../src/types'; import { TempDir } from './util'; vi.mock('../src/ExplorerSync', async () => { const { ExplorerSync } = await vi.importActual< typeof import('../src/ExplorerSync') >('../src/ExplorerSync'); const mock = vi.fn(); return { ExplorerSync: class FakeExplorerSync extends ExplorerSync { static mock = mock; constructor(options: InternalOptionsSync) { mock(options); super(options); } }, }; }); vi.mock('../src/Explorer', async () => { const { Explorer } = await vi.importActual('../src/Explorer'); const mock = vi.fn(); return { Explorer: class FakeExplorer extends Explorer { static mock = mock; constructor(options: InternalOptions) { mock(options); super(options); } }, }; }); const { cosmiconfig, cosmiconfigSync } = await import('../src/index.js'); const createExplorerSyncMock = (ExplorerSync as any).mock; const createExplorerMock = (Explorer as any).mock; const temp = new TempDir(); function getLoaderFunctionsByName(loaders: Loaders) { return Object.fromEntries( Object.entries(loaders).map(([extension, loader]) => [ extension, loader.name, ]), ); } const checkConfigResult = ( mock: SpyInstance, instanceNum: number, expectedLoaderNames: Record, expectedExplorerOptions: Omit< InternalOptions & InternalOptionsSync, 'transform' | 'loaders' >, ) => { const instanceIndex = instanceNum - 1; expect(mock.mock.calls.length).toBe(instanceNum); expect(mock.mock.calls[instanceIndex].length).toBe(1); const { transform, loaders, ...explorerOptions } = mock.mock.calls[instanceIndex][0]; expect(transform.name).toMatch(/identity/); const loaderFunctionsByName = getLoaderFunctionsByName(loaders); // Vitest adds a number suffix to our functions names, // so we can't compare them with strict equals for (const [key, value] of Object.entries(loaderFunctionsByName)) { expect(value).toContain(expectedLoaderNames[key]); } expect(explorerOptions).toStrictEqual(expectedExplorerOptions); }; describe('cosmiconfig', () => { const moduleName = 'foo'; beforeEach(() => { temp.clean(); }); afterAll(() => { // Remove temp.dir created for tests temp.deleteTempDir(); }); describe('creates explorer with default options if not specified', () => { const checkResult = ( mock: SpyInstance, instanceNum: number, expectedLoaders: any, expectedSearchPlaces: any, ) => { const instanceIndex = instanceNum - 1; expect(mock.mock.calls.length).toBe(instanceNum); expect(mock.mock.calls[instanceIndex].length).toBe(1); const { transform, loaders, ...explorerOptions } = mock.mock.calls[instanceIndex][0]; expect(transform.name).toMatch(/identity/); const loaderFunctionsByName = getLoaderFunctionsByName(loaders); // Vitest adds a number suffix to our functions names, // so we can't compare them with strict equals for (const [key, value] of Object.entries(loaderFunctionsByName)) { expect(value).toContain(expectedLoaders[key]); } expect(explorerOptions).toEqual({ moduleName, searchPlaces: expectedSearchPlaces, ignoreEmptySearchPlaces: true, mergeImportArrays: true, mergeSearchPlaces: true, searchStrategy: 'none', metaConfigFilePath: null, cache: true, }); }; test('async', () => { cosmiconfig(moduleName); checkResult( createExplorerMock, 1, { '.cjs': 'loadJs', '.mjs': 'loadJs', '.js': 'loadJs', '.ts': 'loadTs', '.json': 'loadJson', '.yaml': 'loadYaml', '.yml': 'loadYaml', noExt: 'loadYaml', }, [ 'package.json', `.${moduleName}rc`, `.${moduleName}rc.json`, `.${moduleName}rc.yaml`, `.${moduleName}rc.yml`, `.${moduleName}rc.js`, `.${moduleName}rc.ts`, `.${moduleName}rc.cjs`, `.${moduleName}rc.mjs`, `.config/${moduleName}rc`, `.config/${moduleName}rc.json`, `.config/${moduleName}rc.yaml`, `.config/${moduleName}rc.yml`, `.config/${moduleName}rc.js`, `.config/${moduleName}rc.ts`, `.config/${moduleName}rc.cjs`, `.config/${moduleName}rc.mjs`, `${moduleName}.config.js`, `${moduleName}.config.ts`, `${moduleName}.config.cjs`, `${moduleName}.config.mjs`, ], ); }); test('sync', () => { cosmiconfigSync(moduleName); checkResult( createExplorerSyncMock, 2, // ExplorerSync is called twice (once in getExplorerOptions) { '.mjs': 'loadJsSync', '.cjs': 'loadJsSync', '.js': 'loadJsSync', '.ts': 'loadTsSync', '.json': 'loadJson', '.yaml': 'loadYaml', '.yml': 'loadYaml', noExt: 'loadYaml', }, [ 'package.json', `.${moduleName}rc`, `.${moduleName}rc.json`, `.${moduleName}rc.yaml`, `.${moduleName}rc.yml`, `.${moduleName}rc.js`, `.${moduleName}rc.ts`, `.${moduleName}rc.cjs`, `.config/${moduleName}rc`, `.config/${moduleName}rc.json`, `.config/${moduleName}rc.yaml`, `.config/${moduleName}rc.yml`, `.config/${moduleName}rc.js`, `.config/${moduleName}rc.ts`, `.config/${moduleName}rc.cjs`, `${moduleName}.config.js`, `${moduleName}.config.ts`, `${moduleName}.config.cjs`, ], ); }); }); describe('defaults transform to sync identity function', () => { const checkResult = (mock: Mock) => { const explorerOptions = mock.mock.calls[0][0]; const x = {}; expect(explorerOptions.transform(x)).toBe(x); }; test('async', () => { cosmiconfig(moduleName); checkResult(createExplorerMock); }); test('sync', () => { cosmiconfigSync(moduleName); checkResult(createExplorerSyncMock); }); }); describe('creates explorer with preference for given options over defaults', () => { const noExtLoader: Loader = () => {}; const jsLoader: Loader = () => {}; const tsLoader: Loader = () => {}; const jsonLoader: Loader = () => {}; const yamlLoader: Loader = () => {}; const mjsLoader: Loader = () => {}; const options = { stopDir: __dirname, cache: false, searchPlaces: ['.config/foo.json'], moduleName: 'wildandfree', ignoreEmptySearchPlaces: false, mergeImportArrays: true, mergeSearchPlaces: true, loaders: { noExt: noExtLoader, '.mjs': mjsLoader, '.cjs': jsLoader, '.js': jsLoader, '.ts': tsLoader, '.json': jsonLoader, '.yaml': yamlLoader, '.yml': yamlLoader, }, }; const expectedLoaderNames = { '.mjs': 'mjsLoader', '.cjs': 'jsLoader', '.js': 'jsLoader', '.ts': 'tsLoader', '.json': 'jsonLoader', '.yaml': 'yamlLoader', '.yml': 'yamlLoader', noExt: 'noExtLoader', }; const expectedExplorerOptions = { moduleName: 'wildandfree', searchPlaces: ['.config/foo.json'], ignoreEmptySearchPlaces: false, mergeImportArrays: true, mergeSearchPlaces: true, searchStrategy: 'global', stopDir: __dirname, cache: false, metaConfigFilePath: null, } satisfies Partial; test('async', () => { cosmiconfig(moduleName, options); checkConfigResult( createExplorerMock, 1, expectedLoaderNames, expectedExplorerOptions, ); }); test('sync', () => { cosmiconfigSync(moduleName, options); checkConfigResult( createExplorerSyncMock, 2, expectedLoaderNames, expectedExplorerOptions, ); }); }); describe('creates explorer with preference of user options over consumer options', () => { const currentDir = process.cwd(); beforeEach(() => { process.chdir(temp.dir); }); afterEach(() => process.chdir(currentDir)); const noExtLoader: LoaderSync = () => {}; const jsLoader: LoaderSync = () => {}; const tsLoader: LoaderSync = () => {}; const jsonLoader: LoaderSync = () => {}; const yamlLoader: LoaderSync = () => {}; const options = { stopDir: __dirname, cache: false, searchPlaces: ['.foorc.json', 'wildandfree.js', '.config/foo.json'], moduleName: 'wildandfree', ignoreEmptySearchPlaces: false, metaConfigFilePath: `${temp.dir}/.config/config.json`, loaders: { noExt: noExtLoader, '.mjs': jsLoader, '.cjs': jsLoader, '.js': jsLoader, '.ts': tsLoader, '.json': jsonLoader, '.yaml': yamlLoader, }, }; const checkResultBase = ( mock: MockInstance, instanceNum: number, expectedLoaderFunctionNames: any, ) => { const instanceIndex = instanceNum - 1; expect(mock.mock.calls.length).toBe(instanceNum); expect(mock.mock.calls[instanceIndex]!.length).toBe(1); const { transform, loaders, ...explorerOptions } = mock.mock.calls[instanceIndex]![0]; expect(transform.name).toMatch(/identity/); const loaderFunctionsByName = getLoaderFunctionsByName(loaders); // Vitest adds a number suffix to our functions names, // so we can't compare them with strict equals for (const [key, value] of Object.entries(loaderFunctionsByName)) { expect(value).toContain(expectedLoaderFunctionNames[key]); } return explorerOptions; }; describe('merging search places', () => { beforeEach(() => { temp.createFile( '.config/config.json', '{"cosmiconfig": {"searchPlaces": [".config/{name}.json"]}}', ); }); const checkResult = ( mock: SpyInstance, instanceNum: number, expectedLoaderFunctionNames: any, ) => { const explorerOptions = checkResultBase( mock, instanceNum, expectedLoaderFunctionNames, ); expect(explorerOptions).toEqual({ moduleName: 'wildandfree', searchPlaces: [ '.config/foo.json', '.foorc.json', 'wildandfree.js', '.config/foo.json', ], ignoreEmptySearchPlaces: false, mergeImportArrays: true, mergeSearchPlaces: true, searchStrategy: 'global', metaConfigFilePath: path.join(temp.dir, '.config/config.json'), stopDir: __dirname, cache: false, }); }; test('async', () => { cosmiconfig(moduleName, options); checkResult(createExplorerMock, 1, { '.mjs': 'jsLoader', '.cjs': 'jsLoader', '.js': 'jsLoader', '.ts': 'tsLoader', '.json': 'jsonLoader', '.yaml': 'yamlLoader', '.yml': 'loadYaml', noExt: 'noExtLoader', }); }); test('sync', () => { cosmiconfigSync(moduleName, options); checkResult( createExplorerSyncMock, 2, // ExplorerSync is called twice (once in getExplorerOptions) { '.cjs': 'jsLoader', '.mjs': 'jsLoader', '.js': 'jsLoader', '.ts': 'tsLoader', '.json': 'jsonLoader', '.yaml': 'yamlLoader', '.yml': 'loadYaml', noExt: 'noExtLoader', }, ); }); }); describe('not merging search places', () => { beforeEach(() => { temp.createFile( '.config/config.json', '{"cosmiconfig": {"searchPlaces": [".config/{name}.json"], "mergeSearchPlaces": false}}', ); }); const checkResult = ( mock: SpyInstance, instanceNum: number, expectedLoaderFunctionNames: any, ) => { const explorerOptions = checkResultBase( mock, instanceNum, expectedLoaderFunctionNames, ); expect(explorerOptions).toEqual({ moduleName: 'wildandfree', searchPlaces: ['.config/foo.json'], ignoreEmptySearchPlaces: false, mergeImportArrays: true, mergeSearchPlaces: false, searchStrategy: 'global', metaConfigFilePath: path.join(temp.dir, '.config/config.json'), stopDir: __dirname, cache: false, }); }; test('async', () => { cosmiconfig(moduleName, options); checkResult(createExplorerMock, 1, { '.mjs': 'jsLoader', '.cjs': 'jsLoader', '.js': 'jsLoader', '.ts': 'tsLoader', '.json': 'jsonLoader', '.yaml': 'yamlLoader', '.yml': 'loadYaml', noExt: 'noExtLoader', }); }); test('sync', () => { cosmiconfigSync(moduleName, options); checkResult( createExplorerSyncMock, 2, // ExplorerSync is called twice (once in getExplorerOptions) { '.cjs': 'jsLoader', '.mjs': 'jsLoader', '.js': 'jsLoader', '.ts': 'tsLoader', '.json': 'jsonLoader', '.yaml': 'yamlLoader', '.yml': 'loadYaml', noExt: 'noExtLoader', }, ); }); }); }); describe('errors if loader is not passed a function', () => { beforeEach(() => { temp.createFile( 'a/b/c/d/e/f/.foorc.things', 'one\ntwo\nthree\t\t\n four\n', ); }); const explorerOptions = { stopDir: temp.absolutePath('.'), searchPlaces: ['.foorc.things'], loaders: { '.things': 1, }, }; const expectedError = 'Loader for extension ".foorc.things" is not a function: Received number.'; test('async', () => { expect(() => cosmiconfig('foo', explorerOptions as unknown as Partial), ).toThrow(expectedError); }); test('sync', () => { expect(() => cosmiconfigSync( 'foo', explorerOptions as unknown as Partial, ), ).toThrow(expectedError); }); }); test('errors with invalid combination of searchStrategy and stopDir', () => { expect(() => cosmiconfig('foo', { searchStrategy: 'none', stopDir: 'a', }), ).toThrow( 'Can not supply `stopDir` option with `searchStrategy` other than "global"', ); }); test('cannot mutate default loaders', () => { const expectedError = "Cannot delete property '.js' of #"; // @ts-ignore expect(() => delete defaultLoaders['.js']).toThrow(expectedError); }); }); cosmiconfig-9.0.1/test/meta-config.test.ts000066400000000000000000000143061515130774000205260ustar00rootroot00000000000000import { describe, beforeEach, afterAll, test, expect, afterEach, vi, SpyInstance, } from 'vitest'; import fs from 'fs'; import fsPromises from 'fs/promises'; import { cosmiconfig, cosmiconfigSync, Options, OptionsSync } from '../src'; import { TempDir } from './util'; describe('cosmiconfig meta config', () => { const temp = new TempDir(); beforeEach(() => { temp.clean(); }); afterAll(() => { // Remove temp.dir created for tests temp.deleteTempDir(); }); test('throws when trying to supply loaders', () => { temp.createFile('.config/config.yml', 'cosmiconfig:\n loaders: []'); const currentDir = process.cwd(); process.chdir(temp.dir); expect(() => cosmiconfigSync('foo')).toThrow(); process.chdir(currentDir); }); test('throws when trying to supply searchStrategy', () => { temp.createFile( '.config/config.yml', 'cosmiconfig:\n searchStrategy: "global"', ); const currentDir = process.cwd(); process.chdir(temp.dir); expect(() => cosmiconfigSync('foo')).toThrow(); process.chdir(currentDir); }); describe('uses user-configured searchPlaces without placeholders', () => { const currentDir = process.cwd(); beforeEach(() => { temp.createDir('sub'); temp.createFile('.foorc', 'a: b'); temp.createFile('.foo-config', 'a: c'); process.chdir(temp.dir); }); afterEach(() => { process.chdir(currentDir); }); async function runTest() { const file = temp.absolutePath('.foo-config'); const startDir = temp.absolutePath('sub'); const explorerOptions = { stopDir: temp.absolutePath('.') }; const readFileSyncSpy = vi.spyOn(fs, 'readFileSync'); const explorer = cosmiconfig('foo', explorerOptions); expect( temp .getSpyPathCalls(readFileSyncSpy) .filter((path) => !path.includes('/node_modules/')), ).toEqual([ 'package.json', 'package.yaml', '.config/config.json', '.config/config.yaml', '.config/config.yml', ]); readFileSyncSpy.mockClear(); const readFileSpy = vi.spyOn(fsPromises, 'readFile'); const result = await explorer.search(startDir); expect(temp.getSpyPathCalls(readFileSpy)).toEqual([ '.config/config.yml', 'sub/.foo-config', 'sub/.foo.config.yml', '.foo-config', ]); expect(result).toEqual({ config: { a: 'c' }, filepath: file, }); } test('without placeholder', async () => { temp.createFile( '.config/config.yml', 'cosmiconfig:\n searchPlaces: [".foo-config", ".foo.config.yml"]\n mergeSearchPlaces: false', ); await runTest(); }); test('with placeholder', async () => { temp.createFile( '.config/config.yml', 'cosmiconfig:\n searchPlaces: [".{name}-config", ".{name}.config.yml"]\n mergeSearchPlaces: false', ); await runTest(); }); }); describe('checks config in meta file', () => { const currentDir = process.cwd(); beforeEach(() => { temp.createDir('sub'); temp.createFile('.foo-config', 'a: c'); process.chdir(temp.dir); }); afterEach(() => { process.chdir(currentDir); }); describe('not existing', () => { beforeEach(() => { temp.createFile( '.config/config.yml', 'cosmiconfig:\n searchPlaces: [".foo-config"]', ); }); const file = temp.absolutePath('.foo-config'); const explorerOptions: Partial = { stopDir: temp.absolutePath('.'), ignoreEmptySearchPlaces: false, }; function checkResult( constructFiles: Array, readFileSpy: SpyInstance, result: any, ) { expect( constructFiles.filter((path) => !path.includes('/node_modules/')), ).toEqual([ 'package.json', 'package.yaml', '.config/config.json', '.config/config.yaml', '.config/config.yml', ]); expect( temp .getSpyPathCalls(readFileSpy) .filter((path) => !path.includes('/node_modules/')), ).toEqual(['.config/config.yml', '.foo-config']); expect(result).toEqual({ config: { a: 'c' }, filepath: file, }); } test('async', async () => { const readFileSyncSpy = vi.spyOn(fs, 'readFileSync'); const explorer = cosmiconfig('foo', explorerOptions); const constructFiles = temp.getSpyPathCalls(readFileSyncSpy); readFileSyncSpy.mockClear(); const readFileSpy = vi.spyOn(fsPromises, 'readFile'); const result = await explorer.search(temp.dir); checkResult(constructFiles, readFileSpy, result); }); test('sync', () => { const readFileSyncSpy = vi.spyOn(fs, 'readFileSync'); const explorer = cosmiconfigSync('foo', explorerOptions); const constructFiles = temp.getSpyPathCalls(readFileSyncSpy); readFileSyncSpy.mockClear(); const result = explorer.search(temp.dir); checkResult(constructFiles, readFileSyncSpy, result); }); }); describe('existing', () => { beforeEach(() => { temp.createFile('.config/config.yml', 'foo:\n a: d'); }); function checkResult(readFileSpy: SpyInstance, result: any) { expect(temp.getSpyPathCalls(readFileSpy)).toEqual([ '.config/config.yml', ]); expect(result).toEqual({ config: { a: 'd' }, filepath: temp.absolutePath('.config/config.yml'), }); } const explorerOptions = { stopDir: temp.absolutePath('.') }; test('async', async () => { const explorer = cosmiconfig('foo', explorerOptions); const readFileSpy = vi.spyOn(fsPromises, 'readFile'); const result = await explorer.search(temp.dir); checkResult(readFileSpy, result); }); test('sync', () => { const explorer = cosmiconfigSync('foo', explorerOptions); const readFileSpy = vi.spyOn(fs, 'readFileSync'); const result = explorer.search(temp.dir); checkResult(readFileSpy, result); }); }); }); }); cosmiconfig-9.0.1/test/search-strategies.test.ts000066400000000000000000000103451515130774000217510ustar00rootroot00000000000000import { afterEach, beforeEach, describe, expect, test } from 'vitest'; import { cosmiconfig, cosmiconfigSync, Options, OptionsSync } from '../src'; import { TempDir } from './util'; const temp = new TempDir(); describe('search strategies', () => { describe('none', () => { beforeEach(() => { temp.clean(); temp.createFile('a/b/.foorc.yml', 'result: 1'); temp.createFile('a/.foorc.yml', 'result: 2'); temp.createFile('.foorc.yml', 'result: 3'); }); const startDir = temp.absolutePath('a/b'); const explorerOptions = { searchStrategy: 'none' } satisfies Partial< Options | OptionsSync >; const checkResult = (result: any) => { expect(result).toEqual({ config: { result: 1 }, filepath: temp.absolutePath('a/b/.foorc.yml'), }); }; test('async', async () => { const explorer = cosmiconfig('foo', explorerOptions); const result = await explorer.search(startDir); checkResult(result); }); test('sync', () => { const explorer = cosmiconfigSync('foo', explorerOptions); const result = explorer.search(startDir); checkResult(result); }); }); describe('global', () => { describe('finds config in parent directories', () => { beforeEach(() => { temp.clean(); temp.createDir('a/b'); temp.createFile('a/.barrc.yml', 'result: 4'); temp.createFile('.barrc.yml', 'result: 5'); }); const startDir = temp.absolutePath('a/b'); const explorerOptions = { searchStrategy: 'global', stopDir: temp.absolutePath('.'), } satisfies Partial; const checkResult = (result: any) => { expect(result).toEqual({ config: { result: 4 }, filepath: temp.absolutePath('a/.barrc.yml'), }); }; test('async', async () => { const explorer = cosmiconfig('bar', explorerOptions); const result = await explorer.search(startDir); checkResult(result); }); test('sync', () => { const explorer = cosmiconfigSync('bar', explorerOptions); const result = explorer.search(startDir); checkResult(result); }); }); if (process.platform === 'linux') { describe('finds config in OS default directory (XDG)', () => { const origConfigFolder = process.env.XDG_CONFIG_HOME; beforeEach(() => { temp.clean(); temp.createDir('a/b'); temp.createFile('config/foo/config.yml', 'result: 6'); process.env.XDG_CONFIG_HOME = temp.absolutePath('config'); }); afterEach(() => { process.env.XDG_CONFIG_HOME = origConfigFolder; }); const startDir = temp.absolutePath('a/b'); const explorerOptions = { searchStrategy: 'global', stopDir: temp.absolutePath('.'), } satisfies Partial; const checkResult = (result: any) => { expect(result).toEqual({ config: { result: 6 }, filepath: temp.absolutePath('config/foo/config.yml'), }); }; test('async', async () => { const explorer = cosmiconfig('foo', explorerOptions); const result = await explorer.search(startDir); checkResult(result); }); test('sync', () => { const explorer = cosmiconfigSync('foo', explorerOptions); const result = explorer.search(startDir); checkResult(result); }); }); } }); describe('project', () => { beforeEach(() => { temp.clean(); temp.createDir('a/b'); temp.createFile('a/package.json', ''); temp.createFile('.bazrc.yml', 'result: 6'); }); const startDir = temp.absolutePath('a/b'); const explorerOptions = { searchStrategy: 'project', } satisfies Partial; test('async', async () => { const explorer = cosmiconfig('bar', explorerOptions); const result = await explorer.search(startDir); expect(result).toBeNull(); }); test('sync', () => { const explorer = cosmiconfigSync('bar', explorerOptions); const result = explorer.search(startDir); expect(result).toBeNull(); }); }); }); cosmiconfig-9.0.1/test/successful-directories.test.ts000066400000000000000000001652651515130774000230410ustar00rootroot00000000000000import fs from 'fs'; import fsPromises from 'fs/promises'; import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, test, vi, } from 'vitest'; import { cosmiconfig, cosmiconfigSync, Options, OptionsSync, defaultLoaders, } from '../src'; import { TempDir, isNotMjs } from './util'; const temp = new TempDir(); function randomString(): string { return Math.random().toString(36).substring(7); } beforeEach(() => { temp.clean(); temp.createDir('a/b/c/d/e/f/'); }); afterAll(() => { // Remove temp.dir created for tests temp.deleteTempDir(); }); beforeAll(() => { // Remove temp.dir created for tests temp.deleteTempDir(); }); describe('finds rc file in third searched dir, with a package.json lacking prop', () => { beforeEach(() => { temp.createFile('a/b/c/d/package.json', '{ "false": "hope" }'); temp.createFile('a/b/c/d/.foorc', '{ "found": true }'); }); const startDir = temp.absolutePath('a/b/c/d/e/f'); const explorerOptions = { stopDir: temp.absolutePath('.') }; const expectedFilesChecked = [ 'a/b/c/d/e/f/package.json', 'a/b/c/d/e/f/.foorc', 'a/b/c/d/e/f/.foorc.json', 'a/b/c/d/e/f/.foorc.yaml', 'a/b/c/d/e/f/.foorc.yml', 'a/b/c/d/e/f/.foorc.js', 'a/b/c/d/e/f/.foorc.ts', 'a/b/c/d/e/f/.foorc.cjs', 'a/b/c/d/e/f/.foorc.mjs', 'a/b/c/d/e/f/.config/foorc', 'a/b/c/d/e/f/.config/foorc.json', 'a/b/c/d/e/f/.config/foorc.yaml', 'a/b/c/d/e/f/.config/foorc.yml', 'a/b/c/d/e/f/.config/foorc.js', 'a/b/c/d/e/f/.config/foorc.ts', 'a/b/c/d/e/f/.config/foorc.cjs', 'a/b/c/d/e/f/.config/foorc.mjs', 'a/b/c/d/e/f/foo.config.js', 'a/b/c/d/e/f/foo.config.ts', 'a/b/c/d/e/f/foo.config.cjs', 'a/b/c/d/e/f/foo.config.mjs', 'a/b/c/d/e/package.json', 'a/b/c/d/e/.foorc', 'a/b/c/d/e/.foorc.json', 'a/b/c/d/e/.foorc.yaml', 'a/b/c/d/e/.foorc.yml', 'a/b/c/d/e/.foorc.js', 'a/b/c/d/e/.foorc.ts', 'a/b/c/d/e/.foorc.cjs', 'a/b/c/d/e/.foorc.mjs', 'a/b/c/d/e/.config/foorc', 'a/b/c/d/e/.config/foorc.json', 'a/b/c/d/e/.config/foorc.yaml', 'a/b/c/d/e/.config/foorc.yml', 'a/b/c/d/e/.config/foorc.js', 'a/b/c/d/e/.config/foorc.ts', 'a/b/c/d/e/.config/foorc.cjs', 'a/b/c/d/e/.config/foorc.mjs', 'a/b/c/d/e/foo.config.js', 'a/b/c/d/e/foo.config.ts', 'a/b/c/d/e/foo.config.cjs', 'a/b/c/d/e/foo.config.mjs', 'a/b/c/d/package.json', 'a/b/c/d/.foorc', ]; const checkResult = (readFileSpy: any, result: any, files: any) => { const filesChecked = temp.getSpyPathCalls(readFileSpy); expect(filesChecked).toEqual(files); expect(result).toEqual({ config: { found: true }, filepath: temp.absolutePath('a/b/c/d/.foorc'), }); }; test('async', async () => { const explorer = cosmiconfig('foo', explorerOptions); const readFileSpy = vi.spyOn(fsPromises, 'readFile'); const result = await explorer.search(startDir); checkResult(readFileSpy, result, expectedFilesChecked); }); test('sync', () => { const explorer = cosmiconfigSync('foo', explorerOptions); const readFileSpy = vi.spyOn(fs, 'readFileSync'); const result = explorer.search(startDir); checkResult(readFileSpy, result, expectedFilesChecked.filter(isNotMjs)); }); }); describe('finds package.json prop in second searched dir', () => { beforeEach(() => { temp.createFile( 'a/b/c/d/e/package.json', '{ "author": "Todd", "foo": { "found": true } }', ); }); const startDir = temp.absolutePath('a/b/c/d/e/f'); const explorerOptions = { stopDir: temp.absolutePath('.') }; const expectedFilesChecked = [ 'a/b/c/d/e/f/package.json', 'a/b/c/d/e/f/.foorc', 'a/b/c/d/e/f/.foorc.json', 'a/b/c/d/e/f/.foorc.yaml', 'a/b/c/d/e/f/.foorc.yml', 'a/b/c/d/e/f/.foorc.js', 'a/b/c/d/e/f/.foorc.ts', 'a/b/c/d/e/f/.foorc.cjs', 'a/b/c/d/e/f/.foorc.mjs', 'a/b/c/d/e/f/.config/foorc', 'a/b/c/d/e/f/.config/foorc.json', 'a/b/c/d/e/f/.config/foorc.yaml', 'a/b/c/d/e/f/.config/foorc.yml', 'a/b/c/d/e/f/.config/foorc.js', 'a/b/c/d/e/f/.config/foorc.ts', 'a/b/c/d/e/f/.config/foorc.cjs', 'a/b/c/d/e/f/.config/foorc.mjs', 'a/b/c/d/e/f/foo.config.js', 'a/b/c/d/e/f/foo.config.ts', 'a/b/c/d/e/f/foo.config.cjs', 'a/b/c/d/e/f/foo.config.mjs', 'a/b/c/d/e/package.json', ]; const checkResult = (readFileSpy: any, result: any, files: any) => { const filesChecked = temp.getSpyPathCalls(readFileSpy); expect(filesChecked).toEqual(files); expect(result).toEqual({ config: { found: true }, filepath: temp.absolutePath('a/b/c/d/e/package.json'), }); }; test('async', async () => { const explorer = cosmiconfig('foo', explorerOptions); const readFileSpy = vi.spyOn(fsPromises, 'readFile'); const result = await explorer.search(startDir); checkResult(readFileSpy, result, expectedFilesChecked); }); test('sync', () => { const explorer = cosmiconfigSync('foo', explorerOptions); const readFileSpy = vi.spyOn(fs, 'readFileSync'); const result = explorer.search(startDir); checkResult(readFileSpy, result, expectedFilesChecked.filter(isNotMjs)); }); }); describe('finds package.json with nested packageProp in second searched dir', () => { beforeEach(() => { // First package.json exists but does not include the nested packageProp. temp.createFile( 'a/b/c/d/e/f/package.json', '{ "author": "Todd", "configs": { "notYourPkg": { "yes": "ofcourse" } } }', ); temp.createFile( 'a/b/c/d/e/package.json', '{ "author": "Todd", "configs": { "pkg": { "please": "no" } } }', ); }); const startDir = temp.absolutePath('a/b/c/d/e/f'); const explorerOptions = { stopDir: temp.absolutePath('.'), packageProp: 'configs.pkg', }; const expectedFilesChecked = [ 'a/b/c/d/e/f/package.json', 'a/b/c/d/e/f/.foorc', 'a/b/c/d/e/f/.foorc.json', 'a/b/c/d/e/f/.foorc.yaml', 'a/b/c/d/e/f/.foorc.yml', 'a/b/c/d/e/f/.foorc.js', 'a/b/c/d/e/f/.foorc.ts', 'a/b/c/d/e/f/.foorc.cjs', 'a/b/c/d/e/f/.foorc.mjs', 'a/b/c/d/e/f/.config/foorc', 'a/b/c/d/e/f/.config/foorc.json', 'a/b/c/d/e/f/.config/foorc.yaml', 'a/b/c/d/e/f/.config/foorc.yml', 'a/b/c/d/e/f/.config/foorc.js', 'a/b/c/d/e/f/.config/foorc.ts', 'a/b/c/d/e/f/.config/foorc.cjs', 'a/b/c/d/e/f/.config/foorc.mjs', 'a/b/c/d/e/f/foo.config.js', 'a/b/c/d/e/f/foo.config.ts', 'a/b/c/d/e/f/foo.config.cjs', 'a/b/c/d/e/f/foo.config.mjs', 'a/b/c/d/e/package.json', ]; const checkResult = (readFileSpy: any, result: any, files: any) => { const filesChecked = temp.getSpyPathCalls(readFileSpy); expect(filesChecked).toEqual(files); expect(result).toEqual({ config: { please: 'no' }, filepath: temp.absolutePath('a/b/c/d/e/package.json'), }); }; test('async', async () => { const explorer = cosmiconfig('foo', explorerOptions); const readFileSpy = vi.spyOn(fsPromises, 'readFile'); const result = await explorer.search(startDir); checkResult(readFileSpy, result, expectedFilesChecked); }); test('sync', () => { const explorer = cosmiconfigSync('foo', explorerOptions); const readFileSpy = vi.spyOn(fs, 'readFileSync'); const result = explorer.search(startDir); checkResult(readFileSpy, result, expectedFilesChecked.filter(isNotMjs)); }); }); describe('finds JS file in first searched dir', () => { beforeEach(() => { temp.createFile( 'a/b/c/d/e/f/foo.config.js', 'module.exports = { found: true };', ); }); const startDir = temp.absolutePath('a/b/c/d/e/f'); const explorerOptions = { stopDir: temp.absolutePath('.') }; const expectedFilesChecked = [ 'a/b/c/d/e/f/package.json', 'a/b/c/d/e/f/.foorc', 'a/b/c/d/e/f/.foorc.json', 'a/b/c/d/e/f/.foorc.yaml', 'a/b/c/d/e/f/.foorc.yml', 'a/b/c/d/e/f/.foorc.js', 'a/b/c/d/e/f/.foorc.ts', 'a/b/c/d/e/f/.foorc.cjs', 'a/b/c/d/e/f/.foorc.mjs', 'a/b/c/d/e/f/.config/foorc', 'a/b/c/d/e/f/.config/foorc.json', 'a/b/c/d/e/f/.config/foorc.yaml', 'a/b/c/d/e/f/.config/foorc.yml', 'a/b/c/d/e/f/.config/foorc.js', 'a/b/c/d/e/f/.config/foorc.ts', 'a/b/c/d/e/f/.config/foorc.cjs', 'a/b/c/d/e/f/.config/foorc.mjs', 'a/b/c/d/e/f/foo.config.js', ]; const checkResult = (readFileSpy: any, result: any, files: any) => { const filesChecked = temp.getSpyPathCalls(readFileSpy); expect(filesChecked).toEqual(files); expect(result).toEqual({ config: { found: true }, filepath: temp.absolutePath('a/b/c/d/e/f/foo.config.js'), }); }; test('async', async () => { const explorer = cosmiconfig('foo', explorerOptions); const readFileSpy = vi.spyOn(fsPromises, 'readFile'); const result = await explorer.search(startDir); checkResult(readFileSpy, result, expectedFilesChecked); }); test('sync', () => { const explorer = cosmiconfigSync('foo', explorerOptions); const readFileSpy = vi.spyOn(fs, 'readFileSync'); const result = explorer.search(startDir); // Last call is from `require`. readFileSpy.mock.calls.pop(); checkResult(readFileSpy, result, expectedFilesChecked.filter(isNotMjs)); }); }); describe('finds CJS file in first searched dir', () => { beforeEach(() => { temp.createFile( 'a/b/c/d/e/f/foo.config.cjs', 'module.exports = { found: true };', ); }); const startDir = temp.absolutePath('a/b/c/d/e/f'); const explorerOptions = { stopDir: temp.absolutePath('.') }; const expectedFilesChecked = [ 'a/b/c/d/e/f/package.json', 'a/b/c/d/e/f/.foorc', 'a/b/c/d/e/f/.foorc.json', 'a/b/c/d/e/f/.foorc.yaml', 'a/b/c/d/e/f/.foorc.yml', 'a/b/c/d/e/f/.foorc.js', 'a/b/c/d/e/f/.foorc.ts', 'a/b/c/d/e/f/.foorc.cjs', 'a/b/c/d/e/f/.foorc.mjs', 'a/b/c/d/e/f/.config/foorc', 'a/b/c/d/e/f/.config/foorc.json', 'a/b/c/d/e/f/.config/foorc.yaml', 'a/b/c/d/e/f/.config/foorc.yml', 'a/b/c/d/e/f/.config/foorc.js', 'a/b/c/d/e/f/.config/foorc.ts', 'a/b/c/d/e/f/.config/foorc.cjs', 'a/b/c/d/e/f/.config/foorc.mjs', 'a/b/c/d/e/f/foo.config.js', 'a/b/c/d/e/f/foo.config.ts', 'a/b/c/d/e/f/foo.config.cjs', ]; const checkResult = (readFileSpy: any, result: any, files: any) => { const filesChecked = temp.getSpyPathCalls(readFileSpy); expect(filesChecked).toEqual(files); expect(result).toEqual({ config: { found: true }, filepath: temp.absolutePath('a/b/c/d/e/f/foo.config.cjs'), }); }; test('async', async () => { const explorer = cosmiconfig('foo', explorerOptions); const readFileSpy = vi.spyOn(fsPromises, 'readFile'); const result = await explorer.search(startDir); checkResult(readFileSpy, result, expectedFilesChecked); }); test('sync', () => { const explorer = cosmiconfigSync('foo', explorerOptions); const readFileSpy = vi.spyOn(fs, 'readFileSync'); const result = explorer.search(startDir); // Last call is from `require`. readFileSpy.mock.calls.pop(); checkResult(readFileSpy, result, expectedFilesChecked.filter(isNotMjs)); }); }); describe('finds ESM foo.config.mjs file in first searched dir', () => { // This prefix works around our inability to clear the ESM loader cache. const dirPrefix = randomString(); const startDir = temp.absolutePath(`${dirPrefix}/a/b/c/d/e/f`); const explorerOptions = { stopDir: temp.absolutePath('.') }; const expectedFilesChecked = [ `${dirPrefix}/a/b/c/d/e/f/package.json`, `${dirPrefix}/a/b/c/d/e/f/.foorc`, `${dirPrefix}/a/b/c/d/e/f/.foorc.json`, `${dirPrefix}/a/b/c/d/e/f/.foorc.yaml`, `${dirPrefix}/a/b/c/d/e/f/.foorc.yml`, `${dirPrefix}/a/b/c/d/e/f/.foorc.js`, `${dirPrefix}/a/b/c/d/e/f/.foorc.ts`, `${dirPrefix}/a/b/c/d/e/f/.foorc.cjs`, `${dirPrefix}/a/b/c/d/e/f/.foorc.mjs`, `${dirPrefix}/a/b/c/d/e/f/.config/foorc`, `${dirPrefix}/a/b/c/d/e/f/.config/foorc.json`, `${dirPrefix}/a/b/c/d/e/f/.config/foorc.yaml`, `${dirPrefix}/a/b/c/d/e/f/.config/foorc.yml`, `${dirPrefix}/a/b/c/d/e/f/.config/foorc.js`, `${dirPrefix}/a/b/c/d/e/f/.config/foorc.ts`, `${dirPrefix}/a/b/c/d/e/f/.config/foorc.cjs`, `${dirPrefix}/a/b/c/d/e/f/.config/foorc.mjs`, `${dirPrefix}/a/b/c/d/e/f/foo.config.js`, `${dirPrefix}/a/b/c/d/e/f/foo.config.ts`, `${dirPrefix}/a/b/c/d/e/f/foo.config.cjs`, `${dirPrefix}/a/b/c/d/e/f/foo.config.mjs`, ]; const checkResult = (readFileSpy: any, result: any, files: any) => { const filesChecked = temp.getSpyPathCalls(readFileSpy); expect(filesChecked).toEqual(files); expect(result).toEqual({ config: { found: true }, filepath: temp.absolutePath(`${dirPrefix}/a/b/c/d/e/f/foo.config.mjs`), }); }; test('async', async () => { temp.createDir(`${dirPrefix}/a/b/c/d/e/f`); temp.createFile( `${dirPrefix}/a/b/c/d/e/f/foo.config.mjs`, 'export default { found: true };', ); const readFileSpy = vi.spyOn(fsPromises, 'readFile'); const result = await cosmiconfig('foo', explorerOptions).search(startDir); checkResult(readFileSpy, result, expectedFilesChecked); }); }); describe('finds ESM foo.config.js file in first searched dir', () => { // This prefix works around our inability to clear the ESM loader cache. const dirPrefix = randomString(); const startDir = temp.absolutePath(`${dirPrefix}/a/b/c/d/e/f`); const explorerOptions = { stopDir: temp.absolutePath('.') }; const expectedFilesChecked = [ `${dirPrefix}/a/b/c/d/e/f/package.json`, `${dirPrefix}/a/b/c/d/e/f/.foorc`, `${dirPrefix}/a/b/c/d/e/f/.foorc.json`, `${dirPrefix}/a/b/c/d/e/f/.foorc.yaml`, `${dirPrefix}/a/b/c/d/e/f/.foorc.yml`, `${dirPrefix}/a/b/c/d/e/f/.foorc.js`, `${dirPrefix}/a/b/c/d/e/f/.foorc.ts`, `${dirPrefix}/a/b/c/d/e/f/.foorc.cjs`, `${dirPrefix}/a/b/c/d/e/f/.foorc.mjs`, `${dirPrefix}/a/b/c/d/e/f/.config/foorc`, `${dirPrefix}/a/b/c/d/e/f/.config/foorc.json`, `${dirPrefix}/a/b/c/d/e/f/.config/foorc.yaml`, `${dirPrefix}/a/b/c/d/e/f/.config/foorc.yml`, `${dirPrefix}/a/b/c/d/e/f/.config/foorc.js`, `${dirPrefix}/a/b/c/d/e/f/.config/foorc.ts`, `${dirPrefix}/a/b/c/d/e/f/.config/foorc.cjs`, `${dirPrefix}/a/b/c/d/e/f/.config/foorc.mjs`, `${dirPrefix}/a/b/c/d/e/f/foo.config.js`, ]; const checkResult = (readFileSpy: any, result: any, files: any) => { const filesChecked = temp.getSpyPathCalls(readFileSpy); expect(filesChecked).toEqual(files); expect(result).toEqual({ config: { found: true }, filepath: temp.absolutePath(`${dirPrefix}/a/b/c/d/e/f/foo.config.js`), }); }; // eslint-disable-next-line vitest/no-identical-title test('async', async () => { temp.createDir(`${dirPrefix}/a/b/c/d/e/f`); temp.createFile( `${dirPrefix}/a/b/c/d/e/f/foo.config.js`, 'export default { found: true };', ); temp.createFile( `${dirPrefix}/a/b/c/d/e/f/package.json`, '{ "type": "module" }', ); const readFileSpy = vi.spyOn(fsPromises, 'readFile'); const result = await cosmiconfig('foo', explorerOptions).search(startDir); checkResult(readFileSpy, result, expectedFilesChecked); }); }); describe('finds .foorc.js file in first searched dir', () => { beforeEach(() => { temp.createFile( 'a/b/c/d/e/f/.foorc.js', 'module.exports = { found: true };', ); }); const startDir = temp.absolutePath('a/b/c/d/e/f'); const explorerOptions = { stopDir: temp.absolutePath('.') }; const expectedFilesChecked = [ 'a/b/c/d/e/f/package.json', 'a/b/c/d/e/f/.foorc', 'a/b/c/d/e/f/.foorc.json', 'a/b/c/d/e/f/.foorc.yaml', 'a/b/c/d/e/f/.foorc.yml', 'a/b/c/d/e/f/.foorc.js', ]; const checkResult = (readFileSpy: any, result: any, files: any) => { const filesChecked = temp.getSpyPathCalls(readFileSpy); expect(filesChecked).toEqual(files); expect(result).toEqual({ config: { found: true }, filepath: temp.absolutePath('a/b/c/d/e/f/.foorc.js'), }); }; test('async', async () => { const explorer = cosmiconfig('foo', explorerOptions); const readFileSpy = vi.spyOn(fsPromises, 'readFile'); const result = await explorer.search(startDir); checkResult(readFileSpy, result, expectedFilesChecked); }); test('sync', () => { const explorer = cosmiconfigSync('foo', explorerOptions); const readFileSpy = vi.spyOn(fs, 'readFileSync'); const result = explorer.search(startDir); // Last call is from `require`. readFileSpy.mock.calls.pop(); checkResult(readFileSpy, result, expectedFilesChecked.filter(isNotMjs)); }); }); describe('finds ESM .foorc.mjs file in first searched dir', () => { // This prefix works around our inability to clear the ESM loader cache. const dirPrefix = randomString(); const startDir = temp.absolutePath(`${dirPrefix}/a/b/c/d/e/f`); const explorerOptions = { stopDir: temp.absolutePath('.') }; const expectedFilesChecked = [ `${dirPrefix}/a/b/c/d/e/f/package.json`, `${dirPrefix}/a/b/c/d/e/f/.foorc`, `${dirPrefix}/a/b/c/d/e/f/.foorc.json`, `${dirPrefix}/a/b/c/d/e/f/.foorc.yaml`, `${dirPrefix}/a/b/c/d/e/f/.foorc.yml`, `${dirPrefix}/a/b/c/d/e/f/.foorc.js`, `${dirPrefix}/a/b/c/d/e/f/.foorc.ts`, `${dirPrefix}/a/b/c/d/e/f/.foorc.cjs`, `${dirPrefix}/a/b/c/d/e/f/.foorc.mjs`, ]; const checkResult = (readFileSpy: any, result: any, files: any) => { const filesChecked = temp.getSpyPathCalls(readFileSpy); expect(filesChecked).toEqual(files); expect(result).toEqual({ config: { found: true }, filepath: temp.absolutePath(`${dirPrefix}/a/b/c/d/e/f/.foorc.mjs`), }); }; test('.foorc.mjs: async', async () => { temp.createDir(`${dirPrefix}/a/b/c/d/e/f`); temp.createFile( `${dirPrefix}/a/b/c/d/e/f/.foorc.mjs`, 'export default { found: true };', ); const readFileSpy = vi.spyOn(fsPromises, 'readFile'); const result = await cosmiconfig('foo', explorerOptions).search(startDir); checkResult(readFileSpy, result, expectedFilesChecked); }); }); describe('finds ESM .foorc.js file in first searched dir', () => { // This prefix works around our inability to clear the ESM loader cache. const dirPrefix = randomString(); const startDir = temp.absolutePath(`${dirPrefix}/a/b/c/d/e/f`); const explorerOptions = { stopDir: temp.absolutePath('.') }; const expectedFilesChecked = [ `${dirPrefix}/a/b/c/d/e/f/package.json`, `${dirPrefix}/a/b/c/d/e/f/.foorc`, `${dirPrefix}/a/b/c/d/e/f/.foorc.json`, `${dirPrefix}/a/b/c/d/e/f/.foorc.yaml`, `${dirPrefix}/a/b/c/d/e/f/.foorc.yml`, `${dirPrefix}/a/b/c/d/e/f/.foorc.js`, ]; const checkResult = (readFileSpy: any, result: any, files: any) => { const filesChecked = temp.getSpyPathCalls(readFileSpy); expect(filesChecked).toEqual(files); expect(result).toEqual({ config: { found: true }, filepath: temp.absolutePath(`${dirPrefix}/a/b/c/d/e/f/.foorc.js`), }); }; test('.foorc.js: async', async () => { temp.createDir(`${dirPrefix}/a/b/c/d/e/f`); temp.createFile( `${dirPrefix}/a/b/c/d/e/f/.foorc.js`, 'export default { found: true };', ); temp.createFile( `${dirPrefix}/a/b/c/d/e/f/package.json`, '{ "type": "module" }', ); const readFileSpy = vi.spyOn(fsPromises, 'readFile'); const result = await cosmiconfig('foo', explorerOptions).search(startDir); checkResult(readFileSpy, result, expectedFilesChecked); }); }); describe('finds .foorc.cjs file in first searched dir', () => { beforeEach(() => { temp.createFile( 'a/b/c/d/e/f/.foorc.cjs', 'module.exports = { found: true };', ); }); const startDir = temp.absolutePath('a/b/c/d/e/f'); const explorerOptions = { stopDir: temp.absolutePath('.') }; const expectedFilesChecked = [ 'a/b/c/d/e/f/package.json', 'a/b/c/d/e/f/.foorc', 'a/b/c/d/e/f/.foorc.json', 'a/b/c/d/e/f/.foorc.yaml', 'a/b/c/d/e/f/.foorc.yml', 'a/b/c/d/e/f/.foorc.js', 'a/b/c/d/e/f/.foorc.ts', 'a/b/c/d/e/f/.foorc.cjs', ]; const checkResult = ( readFileSpy: any, result: any, expectedResult: Array, ) => { const filesChecked = temp.getSpyPathCalls(readFileSpy); expect(filesChecked).toEqual(expectedResult); expect(result).toEqual({ config: { found: true }, filepath: temp.absolutePath('a/b/c/d/e/f/.foorc.cjs'), }); }; test('async', async () => { const explorer = cosmiconfig('foo', explorerOptions); const readFileSpy = vi.spyOn(fsPromises, 'readFile'); const result = await explorer.search(startDir); checkResult(readFileSpy, result, expectedFilesChecked); }); test('sync', () => { const explorer = cosmiconfigSync('foo', explorerOptions); const readFileSpy = vi.spyOn(fs, 'readFileSync'); const result = explorer.search(startDir); // Last call is from `require`. readFileSpy.mock.calls.pop(); checkResult(readFileSpy, result, expectedFilesChecked.filter(isNotMjs)); }); }); describe("finds foorc file in first searched dir's .config subdir", () => { beforeEach(() => { temp.createFile('a/b/c/d/e/f/.config/foorc', 'found: true'); }); const startDir = temp.absolutePath('a/b/c/d/e/f'); const explorerOptions = { stopDir: temp.absolutePath('.') }; const expectedFilesChecked = [ 'a/b/c/d/e/f/package.json', 'a/b/c/d/e/f/.foorc', 'a/b/c/d/e/f/.foorc.json', 'a/b/c/d/e/f/.foorc.yaml', 'a/b/c/d/e/f/.foorc.yml', 'a/b/c/d/e/f/.foorc.js', 'a/b/c/d/e/f/.foorc.ts', 'a/b/c/d/e/f/.foorc.cjs', 'a/b/c/d/e/f/.foorc.mjs', 'a/b/c/d/e/f/.config/foorc', ]; const checkResult = ( readFileSpy: any, result: any, expectedResult: Array, ) => { const filesChecked = temp.getSpyPathCalls(readFileSpy); expect(filesChecked).toEqual(expectedResult); expect(result).toEqual({ config: { found: true }, filepath: temp.absolutePath('a/b/c/d/e/f/.config/foorc'), }); }; test('async', async () => { const explorer = cosmiconfig('foo', explorerOptions); const readFileSpy = vi.spyOn(fsPromises, 'readFile'); const result = await explorer.search(startDir); checkResult(readFileSpy, result, expectedFilesChecked); }); test('sync', () => { const explorer = cosmiconfigSync('foo', explorerOptions); const readFileSpy = vi.spyOn(fs, 'readFileSync'); const result = explorer.search(startDir); checkResult(readFileSpy, result, expectedFilesChecked.filter(isNotMjs)); }); }); describe("finds foorc.json file in first searched dir's .config subdir", () => { beforeEach(() => { temp.createFile('a/b/c/d/e/f/.config/foorc.json', '{ "found": true }'); }); const startDir = temp.absolutePath('a/b/c/d/e/f'); const explorerOptions = { stopDir: temp.absolutePath('.') }; const expectedFilesChecked = [ 'a/b/c/d/e/f/package.json', 'a/b/c/d/e/f/.foorc', 'a/b/c/d/e/f/.foorc.json', 'a/b/c/d/e/f/.foorc.yaml', 'a/b/c/d/e/f/.foorc.yml', 'a/b/c/d/e/f/.foorc.js', 'a/b/c/d/e/f/.foorc.ts', 'a/b/c/d/e/f/.foorc.cjs', 'a/b/c/d/e/f/.foorc.mjs', 'a/b/c/d/e/f/.config/foorc', 'a/b/c/d/e/f/.config/foorc.json', ]; const checkResult = ( readFileSpy: any, result: any, expectedResult: Array, ) => { const filesChecked = temp.getSpyPathCalls(readFileSpy); expect(filesChecked).toEqual(expectedResult); expect(result).toEqual({ config: { found: true }, filepath: temp.absolutePath('a/b/c/d/e/f/.config/foorc.json'), }); }; test('async', async () => { const explorer = cosmiconfig('foo', explorerOptions); const readFileSpy = vi.spyOn(fsPromises, 'readFile'); const result = await explorer.search(startDir); checkResult(readFileSpy, result, expectedFilesChecked); }); test('sync', () => { const explorer = cosmiconfigSync('foo', explorerOptions); const readFileSpy = vi.spyOn(fs, 'readFileSync'); const result = explorer.search(startDir); checkResult(readFileSpy, result, expectedFilesChecked.filter(isNotMjs)); }); }); describe("finds foorc.yaml file in first searched dir's .config subdir", () => { beforeEach(() => { temp.createFile('a/b/c/d/e/f/.config/foorc.yaml', 'found: true'); }); const startDir = temp.absolutePath('a/b/c/d/e/f'); const explorerOptions = { stopDir: temp.absolutePath('.') }; const expectedFilesChecked = [ 'a/b/c/d/e/f/package.json', 'a/b/c/d/e/f/.foorc', 'a/b/c/d/e/f/.foorc.json', 'a/b/c/d/e/f/.foorc.yaml', 'a/b/c/d/e/f/.foorc.yml', 'a/b/c/d/e/f/.foorc.js', 'a/b/c/d/e/f/.foorc.ts', 'a/b/c/d/e/f/.foorc.cjs', 'a/b/c/d/e/f/.foorc.mjs', 'a/b/c/d/e/f/.config/foorc', 'a/b/c/d/e/f/.config/foorc.json', 'a/b/c/d/e/f/.config/foorc.yaml', ]; const checkResult = ( readFileSpy: any, result: any, expectedResult: Array, ) => { const filesChecked = temp.getSpyPathCalls(readFileSpy); expect(filesChecked).toEqual(expectedResult); expect(result).toEqual({ config: { found: true }, filepath: temp.absolutePath('a/b/c/d/e/f/.config/foorc.yaml'), }); }; test('async', async () => { const explorer = cosmiconfig('foo', explorerOptions); const readFileSpy = vi.spyOn(fsPromises, 'readFile'); const result = await explorer.search(startDir); checkResult(readFileSpy, result, expectedFilesChecked); }); test('sync', () => { const explorer = cosmiconfigSync('foo', explorerOptions); const readFileSpy = vi.spyOn(fs, 'readFileSync'); const result = explorer.search(startDir); checkResult(readFileSpy, result, expectedFilesChecked.filter(isNotMjs)); }); }); describe("finds foorc.yml file in first searched dir's .config subdir", () => { beforeEach(() => { temp.createFile('a/b/c/d/e/f/.config/foorc.yml', 'found: true'); }); const startDir = temp.absolutePath('a/b/c/d/e/f'); const explorerOptions = { stopDir: temp.absolutePath('.') }; const expectedFilesChecked = [ 'a/b/c/d/e/f/package.json', 'a/b/c/d/e/f/.foorc', 'a/b/c/d/e/f/.foorc.json', 'a/b/c/d/e/f/.foorc.yaml', 'a/b/c/d/e/f/.foorc.yml', 'a/b/c/d/e/f/.foorc.js', 'a/b/c/d/e/f/.foorc.ts', 'a/b/c/d/e/f/.foorc.cjs', 'a/b/c/d/e/f/.foorc.mjs', 'a/b/c/d/e/f/.config/foorc', 'a/b/c/d/e/f/.config/foorc.json', 'a/b/c/d/e/f/.config/foorc.yaml', 'a/b/c/d/e/f/.config/foorc.yml', ]; const checkResult = ( readFileSpy: any, result: any, expectedResult: Array, ) => { const filesChecked = temp.getSpyPathCalls(readFileSpy); expect(filesChecked).toEqual(expectedResult); expect(result).toEqual({ config: { found: true }, filepath: temp.absolutePath('a/b/c/d/e/f/.config/foorc.yml'), }); }; test('async', async () => { const explorer = cosmiconfig('foo', explorerOptions); const readFileSpy = vi.spyOn(fsPromises, 'readFile'); const result = await explorer.search(startDir); checkResult(readFileSpy, result, expectedFilesChecked); }); test('sync', () => { const explorer = cosmiconfigSync('foo', explorerOptions); const readFileSpy = vi.spyOn(fs, 'readFileSync'); const result = explorer.search(startDir); checkResult(readFileSpy, result, expectedFilesChecked.filter(isNotMjs)); }); }); describe("finds foorc.js file in first searched dir's .config subdir", () => { beforeEach(() => { temp.createFile( 'a/b/c/d/e/f/.config/foorc.js', 'module.exports = { found: true };', ); }); const startDir = temp.absolutePath('a/b/c/d/e/f'); const explorerOptions = { stopDir: temp.absolutePath('.') }; const expectedFilesChecked = [ 'a/b/c/d/e/f/package.json', 'a/b/c/d/e/f/.foorc', 'a/b/c/d/e/f/.foorc.json', 'a/b/c/d/e/f/.foorc.yaml', 'a/b/c/d/e/f/.foorc.yml', 'a/b/c/d/e/f/.foorc.js', 'a/b/c/d/e/f/.foorc.ts', 'a/b/c/d/e/f/.foorc.cjs', 'a/b/c/d/e/f/.foorc.mjs', 'a/b/c/d/e/f/.config/foorc', 'a/b/c/d/e/f/.config/foorc.json', 'a/b/c/d/e/f/.config/foorc.yaml', 'a/b/c/d/e/f/.config/foorc.yml', 'a/b/c/d/e/f/.config/foorc.js', ]; const checkResult = ( readFileSpy: any, result: any, extectedResult: Array, ) => { const filesChecked = temp.getSpyPathCalls(readFileSpy); expect(filesChecked).toEqual(extectedResult); expect(result).toEqual({ config: { found: true }, filepath: temp.absolutePath('a/b/c/d/e/f/.config/foorc.js'), }); }; test('async', async () => { const explorer = cosmiconfig('foo', explorerOptions); const readFileSpy = vi.spyOn(fsPromises, 'readFile'); const result = await explorer.search(startDir); checkResult(readFileSpy, result, expectedFilesChecked); }); test('sync', () => { const explorer = cosmiconfigSync('foo', explorerOptions); const readFileSpy = vi.spyOn(fs, 'readFileSync'); const result = explorer.search(startDir); // Last call is from `require`. readFileSpy.mock.calls.pop(); checkResult(readFileSpy, result, expectedFilesChecked.filter(isNotMjs)); }); }); describe("finds foorc.cjs file in first searched dir's .config subdir", () => { beforeEach(() => { temp.createFile( 'a/b/c/d/e/f/.config/foorc.cjs', 'module.exports = { found: true };', ); }); const startDir = temp.absolutePath('a/b/c/d/e/f'); const explorerOptions = { stopDir: temp.absolutePath('.') }; const expectedFilesChecked = [ 'a/b/c/d/e/f/package.json', 'a/b/c/d/e/f/.foorc', 'a/b/c/d/e/f/.foorc.json', 'a/b/c/d/e/f/.foorc.yaml', 'a/b/c/d/e/f/.foorc.yml', 'a/b/c/d/e/f/.foorc.js', 'a/b/c/d/e/f/.foorc.ts', 'a/b/c/d/e/f/.foorc.cjs', 'a/b/c/d/e/f/.foorc.mjs', 'a/b/c/d/e/f/.config/foorc', 'a/b/c/d/e/f/.config/foorc.json', 'a/b/c/d/e/f/.config/foorc.yaml', 'a/b/c/d/e/f/.config/foorc.yml', 'a/b/c/d/e/f/.config/foorc.js', 'a/b/c/d/e/f/.config/foorc.ts', 'a/b/c/d/e/f/.config/foorc.cjs', ]; const checkResult = ( readFileSpy: any, result: any, extectedResult: Array, ) => { const filesChecked = temp.getSpyPathCalls(readFileSpy); expect(filesChecked).toEqual(extectedResult); expect(result).toEqual({ config: { found: true }, filepath: temp.absolutePath('a/b/c/d/e/f/.config/foorc.cjs'), }); }; test('async', async () => { const explorer = cosmiconfig('foo', explorerOptions); const readFileSpy = vi.spyOn(fsPromises, 'readFile'); const result = await explorer.search(startDir); checkResult(readFileSpy, result, expectedFilesChecked); }); test('sync', () => { const explorer = cosmiconfigSync('foo', explorerOptions); const readFileSpy = vi.spyOn(fs, 'readFileSync'); const result = explorer.search(startDir); // Last call is from `require`. readFileSpy.mock.calls.pop(); checkResult(readFileSpy, result, expectedFilesChecked.filter(isNotMjs)); }); }); describe('skips over empty file to find JS file in first searched dir', () => { beforeEach(() => { temp.createFile( 'a/b/c/d/e/f/foo.config.js', 'module.exports = { found: true };', ); temp.createFile('a/b/c/d/e/f/.foorc', ''); }); const startDir = temp.absolutePath('a/b/c/d/e/f'); const explorerOptions = { stopDir: temp.absolutePath('.') }; const expectedFilesChecked = [ 'a/b/c/d/e/f/package.json', 'a/b/c/d/e/f/.foorc', 'a/b/c/d/e/f/.foorc.json', 'a/b/c/d/e/f/.foorc.yaml', 'a/b/c/d/e/f/.foorc.yml', 'a/b/c/d/e/f/.foorc.js', 'a/b/c/d/e/f/.foorc.ts', 'a/b/c/d/e/f/.foorc.cjs', 'a/b/c/d/e/f/.foorc.mjs', 'a/b/c/d/e/f/.config/foorc', 'a/b/c/d/e/f/.config/foorc.json', 'a/b/c/d/e/f/.config/foorc.yaml', 'a/b/c/d/e/f/.config/foorc.yml', 'a/b/c/d/e/f/.config/foorc.js', 'a/b/c/d/e/f/.config/foorc.ts', 'a/b/c/d/e/f/.config/foorc.cjs', 'a/b/c/d/e/f/.config/foorc.mjs', 'a/b/c/d/e/f/foo.config.js', ]; const checkResult = (readFileSpy: any, result: any, files: any) => { const filesChecked = temp.getSpyPathCalls(readFileSpy); expect(filesChecked).toEqual(files); expect(result).toEqual({ config: { found: true }, filepath: temp.absolutePath('a/b/c/d/e/f/foo.config.js'), }); }; test('async', async () => { const explorer = cosmiconfig('foo', explorerOptions); const readFileSpy = vi.spyOn(fsPromises, 'readFile'); const result = await explorer.search(startDir); checkResult(readFileSpy, result, expectedFilesChecked); }); test('sync', () => { const explorer = cosmiconfigSync('foo', explorerOptions); const readFileSpy = vi.spyOn(fs, 'readFileSync'); const result = explorer.search(startDir); // Last call is from `require`. readFileSpy.mock.calls.pop(); checkResult(readFileSpy, result, expectedFilesChecked.filter(isNotMjs)); }); }); describe('finds package.json in second dir searched, with alternate names', () => { beforeEach(() => { temp.createFile('a/b/c/d/e/package.json', '{ "heeha": { "found": true } }'); }); const startDir = temp.absolutePath('a/b/c/d/e/f'); const explorerOptions = { stopDir: temp.absolutePath('.'), packageProp: 'heeha', searchPlaces: ['package.json', '.wowza', 'wowzaConfig.js'], }; const expectedFilesChecked = [ 'a/b/c/d/e/f/package.json', 'a/b/c/d/e/f/.wowza', 'a/b/c/d/e/f/wowzaConfig.js', 'a/b/c/d/e/package.json', ]; const checkResult = (readFileSpy: any, result: any, files: any) => { const filesChecked = temp.getSpyPathCalls(readFileSpy); expect(filesChecked).toEqual(files); expect(result).toEqual({ config: { found: true }, filepath: temp.absolutePath('a/b/c/d/e/package.json'), }); }; test('async', async () => { const explorer = cosmiconfig('foo', explorerOptions); const readFileSpy = vi.spyOn(fsPromises, 'readFile'); const result = await explorer.search(startDir); checkResult(readFileSpy, result, expectedFilesChecked); }); test('sync', () => { const explorer = cosmiconfigSync('foo', explorerOptions); const readFileSpy = vi.spyOn(fs, 'readFileSync'); const result = explorer.search(startDir); checkResult(readFileSpy, result, expectedFilesChecked.filter(isNotMjs)); }); }); describe('finds rc file in third searched dir, skipping packageProp, parsing extensionless files as JSON', () => { beforeEach(() => { temp.createFile('a/b/c/d/.foorc', '{ "found": true }'); }); const startDir = temp.absolutePath('a/b/c/d/e/f'); const explorerOptions = { stopDir: temp.absolutePath('.'), loaders: { noExt: defaultLoaders['.json'], }, searchPlaces: ['.foorc', 'foo.config.js'], }; const expectedFilesChecked = [ 'a/b/c/d/e/f/.foorc', 'a/b/c/d/e/f/foo.config.js', 'a/b/c/d/e/.foorc', 'a/b/c/d/e/foo.config.js', 'a/b/c/d/.foorc', ]; const checkResult = (readFileSpy: any, result: any, files: any) => { const filesChecked = temp.getSpyPathCalls(readFileSpy); expect(filesChecked).toEqual(files); expect(result).toEqual({ config: { found: true }, filepath: temp.absolutePath('a/b/c/d/.foorc'), }); }; test('async', async () => { const explorer = cosmiconfig('foo', explorerOptions); const readFileSpy = vi.spyOn(fsPromises, 'readFile'); const result = await explorer.search(startDir); checkResult(readFileSpy, result, expectedFilesChecked); }); test('sync', () => { const explorer = cosmiconfigSync('foo', explorerOptions); const readFileSpy = vi.spyOn(fs, 'readFileSync'); const result = explorer.search(startDir); checkResult(readFileSpy, result, expectedFilesChecked.filter(isNotMjs)); }); }); describe('finds package.json file in second searched dir, skipping JS and RC files', () => { beforeEach(() => { temp.createFile( 'a/b/c/d/e/package.json', '{ "author": "Todd", "foo": { "found": true } }', ); }); const startDir = temp.absolutePath('a/b/c/d/e/f'); const explorerOptions = { searchStrategy: 'global', searchPlaces: ['package.json'], } satisfies Partial; const expectedFilesChecked = [ 'a/b/c/d/e/f/package.json', 'a/b/c/d/e/package.json', ]; const checkResult = (readFileSpy: any, result: any, files: any) => { const filesChecked = temp.getSpyPathCalls(readFileSpy); expect(filesChecked).toEqual(files); expect(result).toEqual({ config: { found: true }, filepath: temp.absolutePath('a/b/c/d/e/package.json'), }); }; test('async', async () => { const explorer = cosmiconfig('foo', explorerOptions); const readFileSpy = vi.spyOn(fsPromises, 'readFile'); const result = await explorer.search(startDir); checkResult(readFileSpy, result, expectedFilesChecked); }); test('sync', () => { const explorer = cosmiconfigSync('foo', explorerOptions); const readFileSpy = vi.spyOn(fs, 'readFileSync'); const result = explorer.search(startDir); checkResult(readFileSpy, result, expectedFilesChecked.filter(isNotMjs)); }); }); describe('finds .foorc.json in second searched dir', () => { beforeEach(() => { temp.createFile('a/b/c/d/e/.foorc.json', '{ "found": true }'); }); const startDir = temp.absolutePath('a/b/c/d/e/f'); const explorerOptions = { stopDir: temp.absolutePath('.'), }; const expectedFilesChecked = [ 'a/b/c/d/e/f/package.json', 'a/b/c/d/e/f/.foorc', 'a/b/c/d/e/f/.foorc.json', 'a/b/c/d/e/f/.foorc.yaml', 'a/b/c/d/e/f/.foorc.yml', 'a/b/c/d/e/f/.foorc.js', 'a/b/c/d/e/f/.foorc.ts', 'a/b/c/d/e/f/.foorc.cjs', 'a/b/c/d/e/f/.foorc.mjs', 'a/b/c/d/e/f/.config/foorc', 'a/b/c/d/e/f/.config/foorc.json', 'a/b/c/d/e/f/.config/foorc.yaml', 'a/b/c/d/e/f/.config/foorc.yml', 'a/b/c/d/e/f/.config/foorc.js', 'a/b/c/d/e/f/.config/foorc.ts', 'a/b/c/d/e/f/.config/foorc.cjs', 'a/b/c/d/e/f/.config/foorc.mjs', 'a/b/c/d/e/f/foo.config.js', 'a/b/c/d/e/f/foo.config.ts', 'a/b/c/d/e/f/foo.config.cjs', 'a/b/c/d/e/f/foo.config.mjs', 'a/b/c/d/e/package.json', 'a/b/c/d/e/.foorc', 'a/b/c/d/e/.foorc.json', ]; const checkResult = (readFileSpy: any, result: any, files: any) => { const filesChecked = temp.getSpyPathCalls(readFileSpy); expect(filesChecked).toEqual(files); expect(result).toEqual({ config: { found: true }, filepath: temp.absolutePath('a/b/c/d/e/.foorc.json'), }); }; test('async', async () => { const explorer = cosmiconfig('foo', explorerOptions); const readFileSpy = vi.spyOn(fsPromises, 'readFile'); const result = await explorer.search(startDir); checkResult(readFileSpy, result, expectedFilesChecked); }); test('sync', () => { const explorer = cosmiconfigSync('foo', explorerOptions); const readFileSpy = vi.spyOn(fs, 'readFileSync'); const result = explorer.search(startDir); checkResult(readFileSpy, result, expectedFilesChecked.filter(isNotMjs)); }); }); describe('finds .foorc.yaml in first searched dir', () => { beforeEach(() => { temp.createFile('a/b/c/d/e/f/.foorc.yaml', 'found: true'); }); const startDir = temp.absolutePath('a/b/c/d/e/f'); const explorerOptions = { stopDir: temp.absolutePath('.'), }; const expectedFilesChecked = [ 'a/b/c/d/e/f/package.json', 'a/b/c/d/e/f/.foorc', 'a/b/c/d/e/f/.foorc.json', 'a/b/c/d/e/f/.foorc.yaml', ]; const checkResult = (readFileSpy: any, result: any, files: any) => { const filesChecked = temp.getSpyPathCalls(readFileSpy); expect(filesChecked).toEqual(files); expect(result).toEqual({ config: { found: true }, filepath: temp.absolutePath('a/b/c/d/e/f/.foorc.yaml'), }); }; test('async', async () => { const explorer = cosmiconfig('foo', explorerOptions); const readFileSpy = vi.spyOn(fsPromises, 'readFile'); const result = await explorer.search(startDir); checkResult(readFileSpy, result, expectedFilesChecked); }); test('sync', () => { const explorer = cosmiconfigSync('foo', explorerOptions); const readFileSpy = vi.spyOn(fs, 'readFileSync'); const result = explorer.search(startDir); checkResult(readFileSpy, result, expectedFilesChecked.filter(isNotMjs)); }); }); describe('finds .foorc.yml in first searched dir', () => { beforeEach(() => { temp.createFile('a/b/c/d/e/f/.foorc.yml', 'found: true'); }); const startDir = temp.absolutePath('a/b/c/d/e/f'); const explorerOptions = { stopDir: temp.absolutePath('.'), }; const expectedFilesChecked = [ 'a/b/c/d/e/f/package.json', 'a/b/c/d/e/f/.foorc', 'a/b/c/d/e/f/.foorc.json', 'a/b/c/d/e/f/.foorc.yaml', 'a/b/c/d/e/f/.foorc.yml', ]; const checkResult = (readFileSpy: any, result: any, files: any) => { const filesChecked = temp.getSpyPathCalls(readFileSpy); expect(filesChecked).toEqual(files); expect(result).toEqual({ config: { found: true }, filepath: temp.absolutePath('a/b/c/d/e/f/.foorc.yml'), }); }; test('async', async () => { const explorer = cosmiconfig('foo', explorerOptions); const readFileSpy = vi.spyOn(fsPromises, 'readFile'); const result = await explorer.search(startDir); checkResult(readFileSpy, result, expectedFilesChecked); }); test('sync', () => { const explorer = cosmiconfigSync('foo', explorerOptions); const readFileSpy = vi.spyOn(fs, 'readFileSync'); const result = explorer.search(startDir); checkResult(readFileSpy, result, expectedFilesChecked.filter(isNotMjs)); }); }); describe('adding myfooconfig.js to searchPlaces, finds it in first searched dir', () => { beforeEach(() => { temp.createFile( 'a/b/c/d/e/f/myfooconfig.js', 'module.exports = { found: true };', ); }); const startDir = temp.absolutePath('a/b/c/d/e/f'); const explorerOptions = { stopDir: temp.absolutePath('.'), searchPlaces: [ 'package.json', '.foorc', '.foorc.json', '.foorc.yaml', '.foorc.yml', '.foorc.cjs', 'foo.config.cjs', '.foorc.js', 'foo.config.js', 'myfooconfig.js', ], }; const expectedFilesChecked = [ 'a/b/c/d/e/f/package.json', 'a/b/c/d/e/f/.foorc', 'a/b/c/d/e/f/.foorc.json', 'a/b/c/d/e/f/.foorc.yaml', 'a/b/c/d/e/f/.foorc.yml', 'a/b/c/d/e/f/.foorc.cjs', 'a/b/c/d/e/f/foo.config.cjs', 'a/b/c/d/e/f/.foorc.js', 'a/b/c/d/e/f/foo.config.js', 'a/b/c/d/e/f/myfooconfig.js', ]; const checkResult = (readFileSpy: any, result: any, files: any) => { const filesChecked = temp.getSpyPathCalls(readFileSpy); expect(filesChecked).toEqual(files); expect(result).toEqual({ config: { found: true }, filepath: temp.absolutePath('a/b/c/d/e/f/myfooconfig.js'), }); }; test('async', async () => { const explorer = cosmiconfig('foo', explorerOptions); const readFileSpy = vi.spyOn(fsPromises, 'readFile'); const result = await explorer.search(startDir); checkResult(readFileSpy, result, expectedFilesChecked); }); test('sync', () => { const explorer = cosmiconfigSync('foo', explorerOptions); const readFileSpy = vi.spyOn(fs, 'readFileSync'); const result = explorer.search(startDir); // Last call is from `require`. readFileSpy.mock.calls.pop(); checkResult(readFileSpy, result, expectedFilesChecked.filter(isNotMjs)); }); }); describe('finds JS file traversing from cwd', () => { const originalCwd = process.cwd(); beforeEach(() => { temp.createFile( 'a/b/c/d/e/foo.config.js', 'module.exports = { found: true };', ); process.chdir(temp.absolutePath('a/b/c/d/e/f')); }); afterEach(() => { process.chdir(originalCwd); }); const explorerOptions = { stopDir: temp.absolutePath('.'), }; const expectedFilesChecked = [ 'a/b/c/d/e/f/package.json', 'a/b/c/d/e/f/.foorc', 'a/b/c/d/e/f/.foorc.json', 'a/b/c/d/e/f/.foorc.yaml', 'a/b/c/d/e/f/.foorc.yml', 'a/b/c/d/e/f/.foorc.js', 'a/b/c/d/e/f/.foorc.ts', 'a/b/c/d/e/f/.foorc.cjs', 'a/b/c/d/e/f/.foorc.mjs', 'a/b/c/d/e/f/.config/foorc', 'a/b/c/d/e/f/.config/foorc.json', 'a/b/c/d/e/f/.config/foorc.yaml', 'a/b/c/d/e/f/.config/foorc.yml', 'a/b/c/d/e/f/.config/foorc.js', 'a/b/c/d/e/f/.config/foorc.ts', 'a/b/c/d/e/f/.config/foorc.cjs', 'a/b/c/d/e/f/.config/foorc.mjs', 'a/b/c/d/e/f/foo.config.js', 'a/b/c/d/e/f/foo.config.ts', 'a/b/c/d/e/f/foo.config.cjs', 'a/b/c/d/e/f/foo.config.mjs', 'a/b/c/d/e/package.json', 'a/b/c/d/e/.foorc', 'a/b/c/d/e/.foorc.json', 'a/b/c/d/e/.foorc.yaml', 'a/b/c/d/e/.foorc.yml', 'a/b/c/d/e/.foorc.js', 'a/b/c/d/e/.foorc.ts', 'a/b/c/d/e/.foorc.cjs', 'a/b/c/d/e/.foorc.mjs', 'a/b/c/d/e/.config/foorc', 'a/b/c/d/e/.config/foorc.json', 'a/b/c/d/e/.config/foorc.yaml', 'a/b/c/d/e/.config/foorc.yml', 'a/b/c/d/e/.config/foorc.js', 'a/b/c/d/e/.config/foorc.ts', 'a/b/c/d/e/.config/foorc.cjs', 'a/b/c/d/e/.config/foorc.mjs', 'a/b/c/d/e/foo.config.js', ]; const checkResult = (readFileSpy: any, result: any, files: any) => { const filesChecked = temp.getSpyPathCalls(readFileSpy); expect(filesChecked).toEqual(files); expect(result).toEqual({ config: { found: true }, filepath: temp.absolutePath('a/b/c/d/e/foo.config.js'), }); }; test('async', async () => { const explorer = cosmiconfig('foo', explorerOptions); const readFileSpy = vi.spyOn(fsPromises, 'readFile'); const result = await explorer.search(); checkResult(readFileSpy, result, expectedFilesChecked); }); test('sync', () => { const explorer = cosmiconfigSync('foo', explorerOptions); const readFileSpy = vi.spyOn(fs, 'readFileSync'); const result = explorer.search(); // Last call is from `require`. readFileSpy.mock.calls.pop(); checkResult(readFileSpy, result, expectedFilesChecked.filter(isNotMjs)); }); }); describe('searchPlaces can include subdirectories', () => { beforeEach(() => { temp.createFile('a/b/c/d/e/.config/.foorc.json', '{ "found": true }'); }); const startDir = temp.absolutePath('a/b/c/d/e/f'); const explorerOptions = { stopDir: temp.absolutePath('.'), searchPlaces: [ '.foorc.json', 'package.json', '.config/.foorc.json', '.config/foo/config.json', ], }; const expectedFilesChecked = [ 'a/b/c/d/e/f/.foorc.json', 'a/b/c/d/e/f/package.json', 'a/b/c/d/e/f/.config/.foorc.json', 'a/b/c/d/e/f/.config/foo/config.json', 'a/b/c/d/e/.foorc.json', 'a/b/c/d/e/package.json', 'a/b/c/d/e/.config/.foorc.json', ]; const checkResult = (readFileSpy: any, result: any, files: any) => { const filesChecked = temp.getSpyPathCalls(readFileSpy); expect(filesChecked).toEqual(files); expect(result).toEqual({ config: { found: true }, filepath: temp.absolutePath('a/b/c/d/e/.config/.foorc.json'), }); }; test('async', async () => { const explorer = cosmiconfig('foo', explorerOptions); const readFileSpy = vi.spyOn(fsPromises, 'readFile'); const result = await explorer.search(startDir); checkResult(readFileSpy, result, expectedFilesChecked); }); test('sync', () => { const explorer = cosmiconfigSync('foo', explorerOptions); const readFileSpy = vi.spyOn(fs, 'readFileSync'); const result = explorer.search(startDir); checkResult(readFileSpy, result, expectedFilesChecked.filter(isNotMjs)); }); }); describe('directories with the same name as a search place are not treated as files', () => { beforeEach(() => { temp.createFile('a/.foorc.json', '{ "found": true }'); temp.createDir('a/b/package.json/c'); }); const startDir = temp.absolutePath('a/b/package.json/c'); const explorerOptions = { stopDir: temp.absolutePath('.'), searchPlaces: ['package.json', '.foorc.json'], }; const checkResult = (readFileSpy: any, result: any) => { const filesChecked = temp.getSpyPathCalls(readFileSpy); expect(filesChecked).toEqual([ 'a/b/package.json/c/package.json', 'a/b/package.json/c/.foorc.json', 'a/b/package.json/package.json', 'a/b/package.json/.foorc.json', 'a/b/package.json', 'a/b/.foorc.json', 'a/package.json', 'a/.foorc.json', ]); expect(result).toEqual({ config: { found: true }, filepath: temp.absolutePath('a/.foorc.json'), }); }; test('async', async () => { const explorer = cosmiconfig('foo', explorerOptions); const readFileSpy = vi.spyOn(fsPromises, 'readFile'); const result = await explorer.search(startDir); checkResult(readFileSpy, result); }); test('sync', () => { const explorer = cosmiconfigSync('foo', explorerOptions); const readFileSpy = vi.spyOn(fs, 'readFileSync'); const result = explorer.search(startDir); checkResult(readFileSpy, result); }); }); describe('custom loaders allow non-default file types', () => { const loadThings = (filepath: any, content: any) => { return { things: content .split('\n') .map((x: any) => x.trim()) .filter((x: any) => !!x), }; }; const loadGrumbly = () => ({ grumbly: true }); beforeEach(() => { temp.createFile('a/b/c/d/e/.foorc.things', 'one\ntwo\nthree\t\t\n four\n'); }); const startDir = temp.absolutePath('a/b/c/d/e/f'); const explorerOptions = { stopDir: temp.absolutePath('.'), searchPlaces: [ 'package.json', '.foorc.json', '.foorc.yml', '.foorc.things', '.foorc.grumbly', ], loaders: { '.things': loadThings, '.grumbly': loadGrumbly, }, }; const expectedFilesChecked = [ 'a/b/c/d/e/f/package.json', 'a/b/c/d/e/f/.foorc.json', 'a/b/c/d/e/f/.foorc.yml', 'a/b/c/d/e/f/.foorc.things', 'a/b/c/d/e/f/.foorc.grumbly', 'a/b/c/d/e/package.json', 'a/b/c/d/e/.foorc.json', 'a/b/c/d/e/.foorc.yml', 'a/b/c/d/e/.foorc.things', ]; const checkResult = (readFileSpy: any, result: any, files: any) => { const filesChecked = temp.getSpyPathCalls(readFileSpy); expect(filesChecked).toEqual(files); expect(result).toEqual({ config: { things: ['one', 'two', 'three', 'four'] }, filepath: temp.absolutePath('a/b/c/d/e/.foorc.things'), }); }; test('async', async () => { const explorer = cosmiconfig('foo', explorerOptions); const readFileSpy = vi.spyOn(fsPromises, 'readFile'); const result = await explorer.search(startDir); checkResult(readFileSpy, result, expectedFilesChecked); }); test('sync', () => { const explorer = cosmiconfigSync('foo', explorerOptions); const readFileSpy = vi.spyOn(fs, 'readFileSync'); const result = explorer.search(startDir); checkResult(readFileSpy, result, expectedFilesChecked.filter(isNotMjs)); }); }); describe('adding custom loaders allows for default and non-default file types', () => { const loadThings = (filepath: any, content: any) => { return { things: content .split('\n') .map((x: any) => x.trim()) .filter((x: any) => !!x), }; }; const loadGrumbly = () => ({ grumbly: true }); beforeEach(() => { temp.createFile('a/b/c/d/e/.foorc.things', 'one\ntwo\nthree\t\t\n four\n'); }); const startDir = temp.absolutePath('a/b/c/d/e/f'); const explorerOptions = { stopDir: temp.absolutePath('.'), searchPlaces: [ 'package.json', '.foorc.json', '.foorc.yml', '.foorc.things', '.foorc.grumbly', ], loaders: { '.things': loadThings, '.grumbly': loadGrumbly, }, }; const expectedFilesChecked = [ 'a/b/c/d/e/f/package.json', 'a/b/c/d/e/f/.foorc.json', 'a/b/c/d/e/f/.foorc.yml', 'a/b/c/d/e/f/.foorc.things', 'a/b/c/d/e/f/.foorc.grumbly', 'a/b/c/d/e/package.json', 'a/b/c/d/e/.foorc.json', 'a/b/c/d/e/.foorc.yml', 'a/b/c/d/e/.foorc.things', ]; const checkResult = (readFileSpy: any, result: any, files: any) => { const filesChecked = temp.getSpyPathCalls(readFileSpy); expect(filesChecked).toEqual(files); expect(result).toEqual({ config: { things: ['one', 'two', 'three', 'four'] }, filepath: temp.absolutePath('a/b/c/d/e/.foorc.things'), }); }; test('async', async () => { const explorer = cosmiconfig('foo', explorerOptions); const readFileSpy = vi.spyOn(fsPromises, 'readFile'); const result = await explorer.search(startDir); checkResult(readFileSpy, result, expectedFilesChecked); }); test('sync', () => { const explorer = cosmiconfigSync('foo', explorerOptions); const readFileSpy = vi.spyOn(fs, 'readFileSync'); const result = explorer.search(startDir); checkResult(readFileSpy, result, expectedFilesChecked.filter(isNotMjs)); }); }); describe('defaults loaders can be overridden', () => { const loadGrumbly = () => ({ grumbly: true }); beforeEach(() => { temp.createFile('a/b/c/d/e/foo.config.js', 'foo'); }); const startDir = temp.absolutePath('a/b/c/d/e/f'); const explorerOptions = { stopDir: temp.absolutePath('.'), searchPlaces: [ 'package.json', '.foorc.json', 'foo.config.cjs', 'foo.config.js', 'foo.config.ts', '.foorc.yml', ], loaders: { '.js': loadGrumbly, }, }; const expectedFilesChecked = [ 'a/b/c/d/e/f/package.json', 'a/b/c/d/e/f/.foorc.json', 'a/b/c/d/e/f/foo.config.cjs', 'a/b/c/d/e/f/foo.config.js', 'a/b/c/d/e/f/foo.config.ts', 'a/b/c/d/e/f/.foorc.yml', 'a/b/c/d/e/package.json', 'a/b/c/d/e/.foorc.json', 'a/b/c/d/e/foo.config.cjs', 'a/b/c/d/e/foo.config.js', ]; const checkResult = (readFileSpy: any, result: any, files: any) => { const filesChecked = temp.getSpyPathCalls(readFileSpy); expect(filesChecked).toEqual(files); expect(result).toEqual({ config: { grumbly: true }, filepath: temp.absolutePath('a/b/c/d/e/foo.config.js'), }); }; test('async', async () => { const explorer = cosmiconfig('foo', explorerOptions); const readFileSpy = vi.spyOn(fsPromises, 'readFile'); const result = await explorer.search(startDir); checkResult(readFileSpy, result, expectedFilesChecked); }); test('sync', () => { const explorer = cosmiconfigSync('foo', explorerOptions); const readFileSpy = vi.spyOn(fs, 'readFileSync'); const result = explorer.search(startDir); checkResult(readFileSpy, result, expectedFilesChecked.filter(isNotMjs)); }); }); describe('custom loaders can be async', () => { const startDir = temp.absolutePath('a/b/c/d/e/f'); let loadThingsSync: any; let loadThingsAsync: any; const baseOptions = { stopDir: temp.absolutePath('.'), searchPlaces: ['.foorc.things'], }; beforeEach(() => { temp.createFile( 'a/b/c/d/e/f/.foorc.things', 'one\ntwo\nthree\t\t\n four\n', ); loadThingsSync = vi.fn(() => ({ things: true })); loadThingsAsync = vi.fn(async () => ({ things: true })); }); const checkResult = (readFileSpy: any, result: any) => { const filesChecked = temp.getSpyPathCalls(readFileSpy); expect(filesChecked).toEqual(['a/b/c/d/e/f/.foorc.things']); expect(result).toEqual({ config: { things: true }, filepath: temp.absolutePath('a/b/c/d/e/f/.foorc.things'), }); }; test('async', async () => { const explorerOptions = { ...baseOptions, loaders: { '.things': loadThingsAsync, }, }; const explorer = cosmiconfig('foo', explorerOptions); const readFileSpy = vi.spyOn(fsPromises, 'readFile'); const result = await explorer.search(startDir); expect(loadThingsSync).not.toHaveBeenCalled(); expect(loadThingsAsync).toHaveBeenCalled(); checkResult(readFileSpy, result); }); test('sync', () => { const explorerOptions = { ...baseOptions, loaders: { '.things': loadThingsSync, }, }; const explorer = cosmiconfigSync('foo', explorerOptions); const readFileSpy = vi.spyOn(fs, 'readFileSync'); const result = explorer.search(startDir); expect(loadThingsSync).toHaveBeenCalled(); expect(loadThingsAsync).not.toHaveBeenCalled(); checkResult(readFileSpy, result); }); }); describe('a custom loader entry can include just an async loader', () => { const startDir = temp.absolutePath('a/b/c/d/e/f'); beforeEach(() => { temp.createFile( 'a/b/c/d/e/f/.foorc.things', 'one\ntwo\nthree\t\t\n four\n', ); }); const loadThingsAsync = async () => ({ things: true }); const explorerOptions = { stopDir: temp.absolutePath('.'), searchPlaces: ['.foorc.things'], loaders: { '.things': loadThingsAsync, }, }; const checkResult = (readFileSpy: any, result: any) => { const filesChecked = temp.getSpyPathCalls(readFileSpy); expect(filesChecked).toEqual(['a/b/c/d/e/f/.foorc.things']); expect(result).toEqual({ config: { things: true }, filepath: temp.absolutePath('a/b/c/d/e/f/.foorc.things'), }); }; test('async', async () => { const explorer = cosmiconfig('foo', explorerOptions); const readFileSpy = vi.spyOn(fsPromises, 'readFile'); const result = await explorer.search(startDir); checkResult(readFileSpy, result); }); }); describe('a custom loader entry can include only a sync loader and work for both sync and async functions', () => { const startDir = temp.absolutePath('a/b/c/d/e/f'); beforeEach(() => { temp.createFile( 'a/b/c/d/e/f/.foorc.things', 'one\ntwo\nthree\t\t\n four\n', ); }); const loadThingsSync = () => { return { things: true }; }; const explorerOptions = { stopDir: temp.absolutePath('.'), searchPlaces: ['.foorc.things'], loaders: { '.things': loadThingsSync, }, }; const checkResult = (readFileSpy: any, result: any) => { const filesChecked = temp.getSpyPathCalls(readFileSpy); expect(filesChecked).toEqual(['a/b/c/d/e/f/.foorc.things']); expect(result).toEqual({ config: { things: true }, filepath: temp.absolutePath('a/b/c/d/e/f/.foorc.things'), }); }; test('async', async () => { const explorer = cosmiconfig('foo', explorerOptions); const readFileSpy = vi.spyOn(fsPromises, 'readFile'); const result = await explorer.search(startDir); checkResult(readFileSpy, result); }); test('sync', () => { const explorer = cosmiconfigSync('foo', explorerOptions); const readFileSpy = vi.spyOn(fs, 'readFileSync'); const result = explorer.search(startDir); checkResult(readFileSpy, result); }); }); describe('works fine if sync loader returns a Promise from a JS file', () => { const startDir = temp.absolutePath('a/b/c/d/e/f'); beforeEach(() => { temp.createFile( 'a/b/c/d/e/f/bar.config.js', 'module.exports = Promise.resolve({ a: 1 });', ); }); const explorerOptions = { stopDir: temp.absolutePath('.'), searchPlaces: ['bar.config.js'], }; test('sync', async () => { const result = cosmiconfigSync('bar', explorerOptions).search(startDir); expect(result).toEqual({ filepath: temp.absolutePath('a/b/c/d/e/f/bar.config.js'), config: expect.any(Promise), }); if (result === null) { throw new Error('is null'); } const resolvedConfig = await result.config; expect(resolvedConfig).toEqual({ a: 1 }); }); }); cosmiconfig-9.0.1/test/successful-files.test.ts000066400000000000000000000426301515130774000216150ustar00rootroot00000000000000import { afterAll, afterEach, beforeEach, describe, expect, test, vi, } from 'vitest'; import { cosmiconfig, cosmiconfigSync } from '../src'; import { TempDir } from './util'; const temp = new TempDir(); function randomString(): string { return Math.random().toString(36).substring(7); } beforeEach(() => { temp.clean(); }); afterAll(() => { // Remove temp.dir created for tests temp.deleteTempDir(); }); describe('loads defined JSON config path', () => { beforeEach(() => { temp.createFile('foo.json', '{ "foo": true }'); }); const file = temp.absolutePath('foo.json'); const checkResult = (result: any) => { expect(result.config).toEqual({ foo: true }); expect(result.filepath).toBe(file); }; test('async', async () => { const result = await cosmiconfig('successful-files-tests').load(file); checkResult(result); }); test('sync', () => { const result = cosmiconfigSync('successful-files-tests').load(file); checkResult(result); }); }); describe('loads defined YAML config path', () => { beforeEach(() => { temp.createFile('foo.yaml', 'foo: true'); }); const file = temp.absolutePath('foo.yaml'); const checkResult = (result: any) => { expect(result.config).toEqual({ foo: true }); expect(result.filepath).toBe(file); }; test('async', async () => { const result = await cosmiconfig('successful-files-tests').load(file); checkResult(result); }); test('sync', () => { const result = cosmiconfigSync('successful-files-tests').load(file); checkResult(result); }); }); describe('loads defined JS config path', () => { beforeEach(() => { temp.createFile('foo.js', 'module.exports = { foo: true };'); }); const file = temp.absolutePath('foo.js'); const checkResult = (result: any) => { expect(result.config).toEqual({ foo: true }); expect(result.filepath).toBe(file); }; test('async', async () => { const result = await cosmiconfig('successful-files-tests').load(file); checkResult(result); }); }); describe('loads defined TS config path', () => { beforeEach(() => { temp.createFile('goo.ts', 'export default { foo: true } as any;'); }); const file = temp.absolutePath('goo.ts'); const checkResult = (result: any) => { expect(result.config).toEqual({ foo: true }); expect(result.filepath).toBe(file); }; test('async', async () => { const result = await cosmiconfig('successful-files-tests').load(file); checkResult(result); }, 20000); test('sync', async () => { const result = cosmiconfigSync('successful-files-tests').load(file); checkResult(result); }); }); describe('loads defined TS config path with tsconfig.json', () => { beforeEach(() => { temp.createFile('goo.ts', 'export default { foo: true } as any;'); temp.createFile('tsconfig.json', '{}'); }); const file = temp.absolutePath('goo.ts'); const checkResult = (result: any) => { expect(result.config).toEqual({ foo: true }); expect(result.filepath).toBe(file); }; test('async', async () => { const result = await cosmiconfig('successful-files-tests').load(file); checkResult(result); }); test('sync', async () => { const result = cosmiconfigSync('successful-files-tests').load(file); checkResult(result); }); }); describe('loads ESM from defined .js config path (nearest package.json says it is ESM)', () => { // Random basename works around our inability to clear the ESM loader cache. const fileBasename = `${randomString()}.js`; const file = temp.absolutePath(fileBasename); const checkResult = (result: any) => { expect(result.config).toEqual({ foo: true }); expect(result.filepath).toBe(file); }; beforeEach(() => { temp.createFile('package.json', '{ "type": "module" }'); temp.createFile(fileBasename, 'export default { foo: true };'); }); test('async', async () => { const result = await cosmiconfig('successful-files-tests').load(file); checkResult(result); }); }); describe('loads ESM from defined .ts config path (nearest package.json says it is ESM)', () => { // Random basename works around our inability to clear the ESM loader cache. const fileBasename = `${randomString()}.ts`; const file = temp.absolutePath(fileBasename); const checkResult = (result: any) => { expect(result.config).toEqual({ foo: true }); expect(result.filepath).toBe(file); }; beforeEach(() => { temp.createFile('package.json', '{ "type": "module" }'); temp.createFile(fileBasename, 'export default { foo: true } as any;'); }); // eslint-disable-next-line vitest/no-identical-title test('async', async () => { const result = await cosmiconfig('successful-files-tests').load(file); checkResult(result); }, 20000); }); describe('loads CommonJS with its own dependency', () => { beforeEach(() => { temp.createFile('foo.js', 'module.exports = { foo: true };'); temp.createFile('foo-module.js', 'module.exports = require("./foo");'); }); const file = temp.absolutePath('foo-module.js'); const checkResult = (result: any) => { expect(result.config).toEqual({ foo: true }); expect(result.filepath).toBe(file); }; test('async', async () => { const result = await cosmiconfig('successful-files-tests').load(file); checkResult(result); }); test('sync', () => { const result = cosmiconfigSync('successful-files-tests').load(file); checkResult(result); }); }); describe('loads ESM with its own dependency', () => { // Random basename works around our inability to clear the ESM loader cache. const depFileBasename = `${randomString()}.mjs`; const fileBasename = `${randomString()}.mjs`; const file = temp.absolutePath(fileBasename); const checkResult = (result: any) => { expect(result.config).toEqual({ foo: true }); expect(result.filepath).toBe(file); }; beforeEach(() => { temp.createFile(depFileBasename, 'export default { foo: true };'); temp.createFile( fileBasename, `import config from "./${depFileBasename}"; export default config;`, ); }); // eslint-disable-next-line vitest/no-identical-title test('async', async () => { const result = await cosmiconfig('successful-files-tests').load(file); checkResult(result); }); }); describe('loads yaml-like JS config path', () => { beforeEach(() => { temp.createFile('foo-yaml-like.js', 'module.exports = { foo: true };'); }); const file = temp.absolutePath('foo-yaml-like.js'); const checkResult = (result: any) => { expect(result.config).toEqual({ foo: true }); expect(result.filepath).toBe(file); }; test('async', async () => { const result = await cosmiconfig('successful-files-tests').load(file); checkResult(result); }); test('sync', () => { const result = cosmiconfigSync('successful-files-tests').load(file); checkResult(result); }); }); describe('loads package prop from package.yaml', () => { beforeEach(() => { temp.createFile( 'package.yaml', ` foo: bar: baz otherPackage: please: no `, ); }); const configPath = temp.absolutePath('package.yaml'); const checkResult = (result: any, expectedConfig: any) => { expect(result.config).toEqual(expectedConfig); expect(result.filepath).toBe(configPath); }; const expectedConfig = { bar: 'baz' }; test('async', async () => { const result = await cosmiconfig('foo', { searchPlaces: ['package.yaml'], }).load(configPath); checkResult(result, expectedConfig); }); test('sync', () => { const result = cosmiconfigSync('foo', { searchPlaces: ['package.yaml'], }).load(configPath); checkResult(result, expectedConfig); }); }); describe('loads package prop when configPath is package.json', () => { beforeEach(() => { temp.createFile( 'package.json', `{ "foo": { "bar": "baz" }, "otherPackage": { "please": "no" } }`, ); }); const configPath = temp.absolutePath('package.json'); const checkResult = (result: any, expectedConfig: any) => { expect(result.config).toEqual(expectedConfig); expect(result.filepath).toBe(configPath); }; describe('default package prop', () => { const expectedConfig = { bar: 'baz' }; test('async', async () => { const result = await cosmiconfig('foo').load(configPath); checkResult(result, expectedConfig); }); test('sync', () => { const result = cosmiconfigSync('foo').load(configPath); checkResult(result, expectedConfig); }); }); describe('specified packageProp', () => { const expectedConfig = { please: 'no' }; const explorerOptions = { packageProp: 'otherPackage' }; test('async', async () => { const result = await cosmiconfig('foo', explorerOptions).load(configPath); checkResult(result, expectedConfig); }); test('sync', () => { const result = cosmiconfigSync('foo', explorerOptions).load(configPath); checkResult(result, expectedConfig); }); }); describe('nested packageProp', () => { const expectedConfig = 'baz'; const explorerOptions = { packageProp: 'foo.bar' }; test('async', async () => { const result = await cosmiconfig('foo', explorerOptions).load(configPath); checkResult(result, expectedConfig); }); test('sync', () => { const result = cosmiconfigSync('foo', explorerOptions).load(configPath); checkResult(result, expectedConfig); }); }); describe('inaccurate packageProp returns undefined, does not error', () => { const options = { packageProp: 'otherrrPackage' }; test('async', async () => { const result = await cosmiconfig('foo', options).load(configPath); expect(result).toBeNull(); }); test('sync', () => { const result = cosmiconfigSync('foo', options).load(configPath); expect(result).toBeNull(); }); }); describe('inaccurate nested packageProp returns undefined, does not error', () => { const options = { packageProp: 'foo.baz' }; test('async', async () => { const result = await cosmiconfig('foo', options).load(configPath); expect(result).toBeNull(); }); test('sync', () => { const result = cosmiconfigSync('foo', options).load(configPath); expect(result).toBeNull(); }); }); }); describe('runs transform', () => { beforeEach(() => { temp.createFile('foo.json', '{ "foo": true }'); }); const configPath = temp.absolutePath('foo.json'); const transform = (result: any) => { result.config.foo = [result.config.foo]; return result; }; const checkResult = (result: any) => { expect(result.config).toEqual({ foo: [true] }); }; test('async', async () => { const result = await cosmiconfig('successful-files-tests', { transform, }).load(configPath); checkResult(result); }); test('sync', () => { const result = cosmiconfigSync('successful-files-tests', { transform, }).load(configPath); checkResult(result); }); }); describe('does not swallow transform errors', () => { beforeEach(() => { temp.createFile('foo.json', '{ "foo": true }'); }); const configPath = temp.absolutePath('foo.json'); const expectedError = new Error('These pretzels are making me thirsty!'); const transform = () => { throw expectedError; }; test('async', async () => { await expect( cosmiconfig('successful-files-tests', { transform }).load(configPath), ).rejects.toThrow(expectedError); }); test('sync', () => { expect(() => cosmiconfigSync('successful-files-tests', { transform }).load(configPath), ).toThrow(expectedError); }); }); describe('loads defined JSON file with no extension', () => { beforeEach(() => { temp.createFile('foo-valid-json', '{ "json": true }'); }); const file = temp.absolutePath('foo-valid-json'); const checkResult = (result: any) => { expect(result.config).toEqual({ json: true }); expect(result.filepath).toBe(file); }; test('async', async () => { const result = await cosmiconfig('successful-files-tests').load(file); checkResult(result); }); test('sync', () => { const result = cosmiconfigSync('successful-files-tests').load(file); checkResult(result); }); }); describe('loads defined YAML file with no extension', () => { beforeEach(() => { temp.createFile('foo-valid-yaml', 'yaml: true'); }); const file = temp.absolutePath('foo-valid-yaml'); const checkResult = (result: any) => { expect(result.config).toEqual({ yaml: true }); expect(result.filepath).toBe(file); }; test('async', async () => { const result = await cosmiconfig('successful-files-tests').load(file); checkResult(result); }); test('sync', () => { const result = cosmiconfigSync('successful-files-tests').load(file); checkResult(result); }); }); describe('custom loaders can be async', () => { let loadThingsSync: any; let loadThingsAsync: any; beforeEach(() => { temp.createFile('.foorc.things', 'one\ntwo\nthree\t\t\n four\n'); loadThingsSync = vi.fn(() => ({ things: true })); loadThingsAsync = vi.fn(async () => ({ things: true })); }); const file = temp.absolutePath('.foorc.things'); const checkResult = (result: any) => { expect(result.config).toEqual({ things: true }); expect(result.filepath).toBe(file); }; test('async', async () => { const explorerOptions = { loaders: { '.things': loadThingsAsync, }, }; const result = await cosmiconfig('foo', explorerOptions).load(file); checkResult(result); }); test('sync', () => { const explorerOptions = { loaders: { '.things': loadThingsSync, }, }; const result = cosmiconfigSync('foo', explorerOptions).load(file); checkResult(result); }); }); describe('a custom loader entry can include just an async loader', () => { beforeEach(() => { temp.createFile('.foorc.things', 'one\ntwo\nthree\t\t\n four\n'); }); const loadThingsAsync = async () => ({ things: true }); const explorerOptions = { loaders: { '.things': loadThingsAsync, }, }; const file = temp.absolutePath('.foorc.things'); const checkResult = (result: any) => { expect(result.config).toEqual({ things: true }); expect(result.filepath).toBe(file); }; test('async', async () => { const result = await cosmiconfig('foo', explorerOptions).load(file); checkResult(result); }); }); describe('a custom loader entry can include only a sync loader and work for both sync and async functions', () => { beforeEach(() => { temp.createFile('.foorc.things', 'one\ntwo\nthree\t\t\n four\n'); }); const loadThingsSync = () => { return { things: true }; }; const explorerOptions = { loaders: { '.things': loadThingsSync, }, }; const file = temp.absolutePath('.foorc.things'); const checkResult = (result: any) => { expect(result.config).toEqual({ things: true }); expect(result.filepath).toBe(file); }; test('async', async () => { const result = await cosmiconfig('foo', explorerOptions).load(file); checkResult(result); }); test('sync', () => { const result = cosmiconfigSync('foo', explorerOptions).load(file); checkResult(result); }); }); describe('works fine if sync loader returns a Promise from a JS file', () => { beforeEach(() => { temp.createFile( 'foo.config.js', 'module.exports = Promise.resolve({ a: 1 });', ); }); const file = temp.absolutePath('foo.config.js'); const explorerOptions = { stopDir: temp.absolutePath('.'), searchPlaces: ['foo.config.js'], }; test('sync', async () => { const result = cosmiconfigSync('foo', explorerOptions).load(file); expect(result).toEqual({ filepath: file, config: expect.any(Promise), }); if (result === null) { throw new Error('test is broken'); } const resolvedConfig = await result.config; expect(resolvedConfig).toEqual({ a: 1 }); }); }); describe('loads defined JS config relative path', () => { const currentDir = process.cwd(); beforeEach(() => { temp.createFile('config/bar.js', 'module.exports = { bar: true };'); process.chdir(temp.dir); }); afterEach(() => { process.chdir(currentDir); }); const relativeFile = './config/bar.js'; const absoluteFile = temp.absolutePath(relativeFile); const checkResult = (result: any) => { expect(result.config).toEqual({ bar: true }); expect(result.filepath).toBe(absoluteFile); }; test('async', async () => { const result = await cosmiconfig('successful-files-tests').load( relativeFile, ); checkResult(result); }); test('sync', () => { const result = cosmiconfigSync('successful-files-tests').load(relativeFile); checkResult(result); }); }); describe('[#313] config search does not fail if there is a file named .config', () => { beforeEach(() => { temp.createFile('.config', ''); }); test('sync', () => { const result = cosmiconfigSync('tester').search(); expect(result).toBeNull(); }); test('async', async () => { const result = await cosmiconfig('tester').search(); expect(result).toBeNull(); }); }); describe('[#317] `explorer.search(...)` should not crashes with `{stopDir: undefined}` option', () => { test('sync', () => { const result = cosmiconfigSync('tester', { stopDir: undefined }).search(); expect(result).toBeNull(); }); test('async', async () => { const result = await cosmiconfig('tester', { stopDir: undefined }).search(); expect(result).toBeNull(); }); }); cosmiconfig-9.0.1/test/tsconfig.json000066400000000000000000000002071515130774000175110ustar00rootroot00000000000000{ "extends": "../tsconfig.base.json", "compilerOptions": { "outDir": "../build/test" }, "references": [{ "path": ".." }] } cosmiconfig-9.0.1/test/util.ts000066400000000000000000000066471515130774000163450ustar00rootroot00000000000000import os from 'os'; import parentModule from 'parent-module'; import path from 'path'; import { Mock, SpyInstance, vi } from 'vitest'; const fs = await vi.importActual('fs'); export function normalizeDirectorySlash(pathname: string): string { const normalizeCrossPlatform = pathname.replace(/\\/g, '/'); return normalizeCrossPlatform; } export class TempDir { public dir: string; public constructor() { /** * Get the actual path for temp directories that are symlinks (MacOS). * Without the actual path, tests that use process.chdir will unexpectedly * return the real path instead of symlink path. */ const tempDir = fs.realpathSync(os.tmpdir()); /** * Get the pathname of the file that imported util.js. * Used to create a unique directory name for each test suite. */ const parent = parentModule() || 'cosmiconfig'; const relativeParent = path.relative(process.cwd(), parent); /** * Each temp directory will be unique to the test file. * This ensures that temp files/dirs won't cause side effects for other tests. */ this.dir = path.resolve(tempDir, 'cosmiconfig', `${relativeParent}-dir`); // create directory fs.mkdirSync(this.dir, { recursive: true }); // re-enable once: https://github.com/typescript-eslint/typescript-eslint/issues/636 /* eslint-disable @typescript-eslint/unbound-method */ this.absolutePath = this.absolutePath.bind(this); this.createDir = this.createDir.bind(this); this.createFile = this.createFile.bind(this); this.clean = this.clean.bind(this); this.deleteTempDir = this.deleteTempDir.bind(this); /* eslint-enable @typescript-eslint/unbound-method */ } public absolutePath(dir: string): string { // Use path.join to ensure dir is always inside the working temp directory const absolutePath = path.join(this.dir, dir); return absolutePath; } public createDir(dir: string): void { const dirname = this.absolutePath(dir); fs.mkdirSync(dirname, { recursive: true }); } public createFile(file: string, contents: string): void { const filePath = this.absolutePath(file); const fileDir = path.parse(filePath).dir; fs.mkdirSync(fileDir, { recursive: true }); fs.writeFileSync(filePath, `${contents}\n`); } public getSpyPathCalls(spy: Mock | SpyInstance): Array { const calls = spy.mock.calls; const result = calls .map((call): string => { const filePath = call[0]; const relativePath = path.relative(this.dir, filePath); /** * Replace Windows backslash directory separators with forward slashes * so expected paths will be consistent cross platform */ const normalizeCrossPlatform = normalizeDirectorySlash(relativePath); return normalizeCrossPlatform; }) .filter((filePath): boolean => { /** * Filter out `fs` calls that are made within cosmiconfig's dependency * tree. */ return !filePath.includes('/cosmiconfig/node_modules/'); }); return result; } public deleteTempDir(): void { fs.rmSync(normalizeDirectorySlash(this.dir), { force: true, recursive: true, }); } public clean(): void { this.deleteTempDir(); fs.mkdirSync(this.dir, { recursive: true }); } } export function isNotMjs(filePath: string): boolean { return path.extname(filePath) !== '.mjs'; } cosmiconfig-9.0.1/tsconfig.base.json000066400000000000000000000012661515130774000174510ustar00rootroot00000000000000{ "compilerOptions": { "allowJs": true, "alwaysStrict": true, "checkJs": true, "composite": true, "declaration": true, "declarationMap": true, "esModuleInterop": true, "incremental": true, "module": "NodeNext", "moduleResolution": "NodeNext", "noFallthroughCasesInSwitch": true, "noImplicitOverride": true, "noImplicitReturns": true, "noPropertyAccessFromIndexSignature": true, "noUncheckedIndexedAccess": true, "noUnusedLocals": true, "noUnusedParameters": true, "sourceMap": true, "strict": true, "target": "ES2022", "skipLibCheck": true, // XXX: Enable. "useUnknownInCatchVariables": false } } cosmiconfig-9.0.1/tsconfig.json000066400000000000000000000002441515130774000165330ustar00rootroot00000000000000{ "extends": "./tsconfig.base.json", "compilerOptions": { "rootDir": "./src", "outDir": "./dist", "stripInternal": true }, "include": ["src"] } cosmiconfig-9.0.1/vite.config.ts000066400000000000000000000010221515130774000166020ustar00rootroot00000000000000import { defineConfig } from 'vitest/config'; const vitestConfig = defineConfig({ test: { threads: false, environment: 'node', restoreMocks: true, mockReset: true, includeSource: ['src/**/*.{js,ts}'], coverage: { provider: 'istanbul', reporter: ['text', 'html', 'lcov'], include: ['src/**/*.{js,ts}'], branches: 100, functions: 100, lines: 100, statements: 100, }, }, }); // eslint-disable-next-line import/no-default-export export default vitestConfig;