├── .gitattributes ├── .gitignore ├── .prettierrc ├── tests ├── fixtures │ ├── has-json-file │ │ └── foo.json │ ├── package-json │ │ ├── foo.json │ │ └── package.json │ └── package-json-no-key │ │ ├── foo.json │ │ └── package.json ├── sync.test.js └── index.test.js ├── .babelrc ├── .editorconfig ├── types ├── test.ts └── index.d.ts ├── circle.yml ├── package.json ├── LICENSE ├── .github └── workflows │ └── ci.yml ├── README.md ├── src └── index.js └── lib └── index.js /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | *.log 3 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | "@egoist/prettier-config" 2 | -------------------------------------------------------------------------------- /tests/fixtures/has-json-file/foo.json: -------------------------------------------------------------------------------- 1 | { 2 | "foo": "foo" 3 | } 4 | -------------------------------------------------------------------------------- /tests/fixtures/package-json/foo.json: -------------------------------------------------------------------------------- 1 | { 2 | "foo": true 3 | } 4 | -------------------------------------------------------------------------------- /tests/fixtures/package-json-no-key/foo.json: -------------------------------------------------------------------------------- 1 | { 2 | "foo": true 3 | } 4 | -------------------------------------------------------------------------------- /tests/fixtures/package-json-no-key/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "what": true 3 | } 4 | -------------------------------------------------------------------------------- /tests/fixtures/package-json/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "what": "is this" 3 | } 4 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | [ 4 | "@babel/preset-env", 5 | { 6 | "targets": { 7 | "node": "12" 8 | } 9 | } 10 | ] 11 | ], 12 | "plugins": ["sync"] 13 | } 14 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | 11 | [*.md] 12 | trim_trailing_whitespace = false -------------------------------------------------------------------------------- /types/test.ts: -------------------------------------------------------------------------------- 1 | import JoyCon from '..' 2 | 3 | const joycon = new JoyCon({ 4 | files: ['foo.js'] 5 | }) 6 | 7 | joycon.resolve() 8 | .then(path => { 9 | console.log(path) 10 | }) 11 | 12 | joycon.load() 13 | .then(res => { 14 | console.log(res.path) 15 | console.log(res.data) 16 | }) 17 | 18 | joycon.loadSync() 19 | 20 | joycon.addLoader({ 21 | test: /\.ts$/, 22 | loadSync(fp) { 23 | return 123 24 | } 25 | }) 26 | 27 | joycon.clearCache() 28 | -------------------------------------------------------------------------------- /circle.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | jobs: 3 | build: 4 | docker: 5 | - image: circleci/node:10 6 | branches: 7 | ignore: 8 | - gh-pages # list of branches to ignore 9 | - /release\/.*/ # or ignore regexes 10 | steps: 11 | - checkout 12 | - restore_cache: 13 | key: dependency-cache-{{ checksum "yarn.lock" }} 14 | - run: 15 | name: install dependences 16 | command: yarn 17 | - save_cache: 18 | key: dependency-cache-{{ checksum "yarn.lock" }} 19 | paths: 20 | - ./node_modules 21 | - run: 22 | name: test 23 | command: yarn test 24 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "joycon", 3 | "version": "3.0.0", 4 | "description": "Load config with ease.", 5 | "repository": { 6 | "url": "egoist/joycon", 7 | "type": "git" 8 | }, 9 | "main": "lib/index.js", 10 | "types": "types/index.d.ts", 11 | "files": [ 12 | "lib", 13 | "types/index.d.ts" 14 | ], 15 | "scripts": { 16 | "test": "jest --testPathPattern tests", 17 | "build": "babel src -d lib --no-comments", 18 | "prepublishOnly": "npm run build" 19 | }, 20 | "author": "egoist <0x142857@gmail.com>", 21 | "license": "MIT", 22 | "jest": { 23 | "testEnvironment": "node" 24 | }, 25 | "devDependencies": { 26 | "@babel/cli": "^7.13.10", 27 | "@babel/core": "^7.13.10", 28 | "@babel/preset-env": "^7.13.10", 29 | "@egoist/prettier-config": "^0.1.0", 30 | "@types/node": "^14.14.33", 31 | "babel-jest": "^26.6.3", 32 | "babel-plugin-sync": "^0.1.0", 33 | "jest-cli": "^26.6.3", 34 | "prettier": "^2.2.1" 35 | }, 36 | "engines": { 37 | "node": ">=10" 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) egoist <0x142857@gmail.com> (https://github.com/egoist) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /types/index.d.ts: -------------------------------------------------------------------------------- 1 | export interface Options { 2 | /* a list of files to search */ 3 | files?: string[] 4 | /* the directory to search from */ 5 | cwd?: string 6 | /* the directory to stop searching */ 7 | stopDir?: string 8 | /* the key in package.json to read data at */ 9 | packageKey?: string 10 | /* the function used to parse json */ 11 | parseJSON?: (str: string) => any 12 | } 13 | 14 | export interface LoadResult { 15 | /* file path */ 16 | path?: string 17 | /* file data */ 18 | data?: any 19 | } 20 | 21 | export interface AsyncLoader { 22 | /** Optional loader name */ 23 | name?: string 24 | test: RegExp 25 | load(filepath: string): Promise 26 | } 27 | 28 | export interface SyncLoader { 29 | /** Optional loader name */ 30 | name?: string 31 | test: RegExp 32 | loadSync(filepath: string): any 33 | } 34 | 35 | export interface MultiLoader { 36 | /** Optional loader name */ 37 | name?: string 38 | test: RegExp 39 | load(filepath: string): Promise 40 | loadSync(filepath: string): any 41 | } 42 | 43 | declare class JoyCon { 44 | constructor(options?: Options) 45 | 46 | options: Options 47 | 48 | resolve(files?: string[] | Options, cwd?: string, stopDir?: string): Promise 49 | resolveSync(files?: string[] | Options, cwd?: string, stopDir?: string): string | null 50 | 51 | load(files?: string[] | Options, cwd?: string, stopDir?: string): Promise 52 | loadSync(files?: string[] | Options, cwd?: string, stopDir?: string): LoadResult 53 | 54 | addLoader(loader: AsyncLoader | SyncLoader | MultiLoader): this 55 | removeLoader(name: string): this 56 | 57 | /** Clear internal cache */ 58 | clearCache(): this 59 | } 60 | 61 | 62 | export default JoyCon 63 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: Buidd, test and Release 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | jobs: 10 | test: 11 | strategy: 12 | matrix: 13 | os: [ubuntu-latest] 14 | node_version: [12.x,14.x,16.x] 15 | 16 | runs-on: ${{ matrix.os }} 17 | 18 | # Steps represent a sequence of tasks that will be executed as part of the job 19 | steps: 20 | # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it 21 | - uses: actions/checkout@v2 22 | 23 | - uses: actions/setup-node@v1 24 | with: 25 | node-version: ${{matrix.node_version}} 26 | 27 | - name: Get yarn cache directory path 28 | id: yarn-cache-dir-path 29 | run: echo "::set-output name=dir::$(yarn cache dir)" 30 | 31 | - uses: actions/cache@v1 32 | id: yarn-cache # use this to check for `cache-hit` (`steps.yarn-cache.outputs.cache-hit != 'true'`) 33 | with: 34 | path: ${{ steps.yarn-cache-dir-path.outputs.dir }} 35 | key: ${{ runner.os }}-${{runner.node_version}}-yarn-${{ hashFiles('**/yarn.lock') }} 36 | restore-keys: | 37 | ${{ runner.os }}-yarn- 38 | 39 | - name: Install 40 | run: yarn 41 | 42 | # Runs a set of commands using the runners shell 43 | - name: Build and Test 44 | run: yarn test 45 | 46 | release: 47 | needs: [test] 48 | runs-on: ubuntu-latest 49 | steps: 50 | - uses: actions/checkout@v2 51 | - uses: actions/setup-node@v1 52 | with: 53 | node-version: 16.x 54 | - name: Get yarn cache directory path 55 | id: yarn-cache-dir-path 56 | run: echo "::set-output name=dir::$(yarn cache dir)" 57 | - uses: actions/cache@v1 58 | id: yarn-cache # use this to check for `cache-hit` (`steps.yarn-cache.outputs.cache-hit != 'true'`) 59 | with: 60 | path: ${{ steps.yarn-cache-dir-path.outputs.dir }} 61 | key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }} 62 | restore-keys: | 63 | ${{ runner.os }}-yarn- 64 | - name: Install 65 | run: yarn 66 | - name: Release 67 | run: npx -y semantic-release 68 | env: 69 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 70 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 71 | -------------------------------------------------------------------------------- /tests/sync.test.js: -------------------------------------------------------------------------------- 1 | import path from 'path' 2 | import JoyCon from '../src' 3 | 4 | const fixture = name => path.join(__dirname, 'fixtures', name) 5 | 6 | describe('resolve', () => { 7 | it('has json file', () => { 8 | const fp = new JoyCon().resolveSync({ 9 | files: ['foo.json'], 10 | cwd: fixture('has-json-file') 11 | }) 12 | expect(fp.endsWith('foo.json')).toBe(true) 13 | }) 14 | 15 | it('resolves next file', () => { 16 | const fp = new JoyCon().resolveSync({ 17 | files: ['bar.json', 'foo.json'], 18 | cwd: fixture('has-json-file') 19 | }) 20 | expect(fp.endsWith('foo.json')).toBe(true) 21 | }) 22 | 23 | it('returns null when not found', () => { 24 | const fp = new JoyCon().resolveSync({ 25 | files: ['hehe.json'], 26 | cwd: fixture('has-json-file') 27 | }) 28 | expect(fp).toBe(null) 29 | }) 30 | 31 | it('package.json but packageKey does not exist', () => { 32 | const fp = new JoyCon({ packageKey: 'name' }).resolveSync({ 33 | files: ['package.json', 'foo.json'], 34 | cwd: fixture('package-json-no-key') 35 | }) 36 | expect(fp.endsWith('foo.json')).toBe(true) 37 | }) 38 | 39 | it('package.json', () => { 40 | const fp = new JoyCon({ packageKey: 'what' }).resolveSync({ 41 | files: ['package.json', 'foo.json'], 42 | cwd: fixture('package-json') 43 | }) 44 | expect(fp.endsWith('package.json')).toBe(true) 45 | }) 46 | }) 47 | 48 | describe('load', () => { 49 | it('has json file', () => { 50 | const { data } = new JoyCon().loadSync({ 51 | files: ['foo.json'], 52 | cwd: fixture('has-json-file') 53 | }) 54 | expect(data).toEqual({ foo: 'foo' }) 55 | }) 56 | 57 | it('resolves next file', () => { 58 | const { data } = new JoyCon().loadSync({ 59 | files: ['bar.json', 'foo.json'], 60 | cwd: fixture('has-json-file') 61 | }) 62 | expect(data).toEqual({ foo: 'foo' }) 63 | }) 64 | 65 | it('returns {} when not found', () => { 66 | const res = new JoyCon().loadSync({ 67 | files: ['hehe.json'], 68 | cwd: fixture('has-json-file') 69 | }) 70 | expect(res).toEqual({}) 71 | }) 72 | 73 | it('package.json but packageKey does not exist', () => { 74 | const { data } = new JoyCon({ packageKey: 'name' }).loadSync({ 75 | files: ['package.json', 'foo.json'], 76 | cwd: fixture('package-json-no-key') 77 | }) 78 | expect(data).toEqual({ foo: true }) 79 | }) 80 | 81 | it('package.json', () => { 82 | const { data } = new JoyCon({ packageKey: 'what' }).loadSync({ 83 | files: ['package.json', 'foo.json'], 84 | cwd: fixture('package-json') 85 | }) 86 | expect(data).toEqual('is this') 87 | }) 88 | }) 89 | -------------------------------------------------------------------------------- /tests/index.test.js: -------------------------------------------------------------------------------- 1 | import path from 'path' 2 | import JoyCon from '../src' 3 | 4 | const fixture = name => path.join(__dirname, 'fixtures', name) 5 | 6 | describe('resolve', () => { 7 | it('has json file', async () => { 8 | const fp = await new JoyCon().resolve({ 9 | files: ['foo.json'], 10 | cwd: fixture('has-json-file') 11 | }) 12 | expect(fp.endsWith('foo.json')).toBe(true) 13 | }) 14 | 15 | it('resolves next file', async () => { 16 | const fp = await new JoyCon().resolve({ 17 | files: ['bar.json', 'foo.json'], 18 | cwd: fixture('has-json-file') 19 | }) 20 | expect(fp.endsWith('foo.json')).toBe(true) 21 | }) 22 | 23 | it('returns null when not found', async () => { 24 | const fp = await new JoyCon().resolve({ 25 | files: ['hehe.json'], 26 | cwd: fixture('has-json-file') 27 | }) 28 | expect(fp).toBe(null) 29 | }) 30 | 31 | it('package.json but packageKey does not exist', async () => { 32 | const fp = await new JoyCon({ packageKey: 'name' }).resolve({ 33 | files: ['package.json', 'foo.json'], 34 | cwd: fixture('package-json-no-key') 35 | }) 36 | expect(fp.endsWith('foo.json')).toBe(true) 37 | }) 38 | 39 | it('package.json', async () => { 40 | const fp = await new JoyCon({ packageKey: 'what' }).resolve({ 41 | files: ['package.json', 'foo.json'], 42 | cwd: fixture('package-json') 43 | }) 44 | expect(fp.endsWith('package.json')).toBe(true) 45 | }) 46 | }) 47 | 48 | describe('load', () => { 49 | it('has json file', async () => { 50 | const { data } = await new JoyCon().load({ 51 | files: ['foo.json'], 52 | cwd: fixture('has-json-file') 53 | }) 54 | expect(data).toEqual({ foo: 'foo' }) 55 | }) 56 | 57 | it('resolves next file', async () => { 58 | const { data } = await new JoyCon().load({ 59 | files: ['bar.json', 'foo.json'], 60 | cwd: fixture('has-json-file') 61 | }) 62 | expect(data).toEqual({ foo: 'foo' }) 63 | }) 64 | 65 | it('returns {} when not found', async () => { 66 | const res = await new JoyCon().load({ 67 | files: ['hehe.json'], 68 | cwd: fixture('has-json-file') 69 | }) 70 | expect(res).toEqual({}) 71 | }) 72 | 73 | it('package.json but packageKey does not exist', async () => { 74 | const { data } = await new JoyCon({ packageKey: 'name' }).load({ 75 | files: ['package.json', 'foo.json'], 76 | cwd: fixture('package-json-no-key') 77 | }) 78 | expect(data).toEqual({ foo: true }) 79 | }) 80 | 81 | it('package.json', async () => { 82 | const { data } = await new JoyCon({ packageKey: 'what' }).load({ 83 | files: ['package.json', 'foo.json'], 84 | cwd: fixture('package-json') 85 | }) 86 | expect(data).toEqual('is this') 87 | }) 88 | }) 89 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # joycon 3 | 4 | [![NPM version](https://img.shields.io/npm/v/joycon.svg?style=flat)](https://npmjs.com/package/joycon) [![NPM downloads](https://img.shields.io/npm/dm/joycon.svg?style=flat)](https://npmjs.com/package/joycon) [![install size](https://packagephobia.now.sh/badge?p=joycon@2.0.0)](https://packagephobia.now.sh/result?p=joycon@2.0.0) [![CircleCI](https://circleci.com/gh/egoist/joycon/tree/master.svg?style=shield)](https://circleci.com/gh/egoist/joycon/tree/master) [![donate](https://img.shields.io/badge/$-donate-ff69b4.svg?maxAge=2592000&style=flat)](https://github.com/egoist/donate) [![chat](https://img.shields.io/badge/chat-on%20discord-7289DA.svg?style=flat)](https://chat.egoist.moe) 5 | 6 | ## Differences with [cosmiconfig](https://github.com/davidtheclark/cosmiconfig)? 7 | 8 | JoyCon is zero-dependency but feature-complete. 9 | 10 | ## Install 11 | 12 | ```bash 13 | yarn add joycon 14 | ``` 15 | 16 | ## Usage 17 | 18 | ```js 19 | const JoyCon = require('joycon') 20 | 21 | const joycon = new JoyCon() 22 | 23 | joycon.load(['package-lock.json', 'yarn.lock']) 24 | .then(result => { 25 | // result is {} when files do not exist 26 | // otherwise { path, data } 27 | }) 28 | ``` 29 | 30 | By default non-js files are parsed as JSON, if you want something different you can add a loader: 31 | 32 | ```js 33 | const joycon = new JoyCon() 34 | 35 | joycon.addLoader({ 36 | test: /\.toml$/, 37 | load(filepath) { 38 | return require('toml').parse(filepath) 39 | } 40 | }) 41 | 42 | joycon.load(['cargo.toml']) 43 | ``` 44 | 45 | ## API 46 | 47 | ### constructor([options]) 48 | 49 | #### options 50 | 51 | ##### files 52 | 53 | - Type: `string[]` 54 | 55 | The files to search. 56 | 57 | ##### cwd 58 | 59 | The directory to search files. 60 | 61 | ##### stopDir 62 | 63 | The directory to stop searching. 64 | 65 | ##### packageKey 66 | 67 | You can load config from certain property in a `package.json` file. For example, when you set `packageKey: 'babel'`, it will load the `babel` property in `package.json` instead of the entire data. 68 | 69 | ##### parseJSON 70 | 71 | - Type: `(str: string) => any` 72 | - Default: `JSON.parse` 73 | 74 | The function used to parse JSON string. 75 | 76 | ### resolve([files], [cwd], [stopDir]) 77 | ### resolve([options]) 78 | 79 | `files` defaults to `options.files`. 80 | 81 | `cwd` defaults to `options.cwd`. 82 | 83 | `stopDir` defaults to `options.stopDir` then `path.parse(cwd).root`. 84 | 85 | If using a single object `options`, it will be the same as constructor options. 86 | 87 | Search files and resolve the path of the file we found. 88 | 89 | There's also `.resolveSync` method. 90 | 91 | ### load(...args) 92 | 93 | The signature is the same as [resolve](#resolvefiles-cwd-stopdir). 94 | 95 | Search files and resolve `{ path, data }` of the file we found. 96 | 97 | There's also `.loadSync` method. 98 | 99 | ### addLoader(Loader) 100 | 101 | ```typescript 102 | interface Loader { 103 | name?: string 104 | test: RegExp 105 | load(filepath: string)?: Promise 106 | loadSync(filepath: string)?: any 107 | } 108 | ``` 109 | 110 | At least one of `load` and `loadSync` is required, depending on whether you're calling the synchonous methods or not. 111 | 112 | ### removeLoader(name) 113 | 114 | Remove loaders by loader name. 115 | 116 | ### clearCache() 117 | 118 | Each JoyCon instance uses its own cache. 119 | 120 | ## Contributing 121 | 122 | 1. Fork it! 123 | 2. Create your feature branch: `git checkout -b my-new-feature` 124 | 3. Commit your changes: `git commit -am 'Add some feature'` 125 | 4. Push to the branch: `git push origin my-new-feature` 126 | 5. Submit a pull request :D 127 | 128 | ## Author 129 | 130 | **joycon** © [egoist](https://github.com/egoist), Released under the [MIT](./LICENSE) License.
131 | Authored and maintained by egoist with help from contributors ([list](https://github.com/egoist/joycon/contributors)). 132 | 133 | > [github.com/egoist](https://github.com/egoist) · GitHub [@egoist](https://github.com/egoist) · Twitter [@_egoistlily](https://twitter.com/_egoistlily) 134 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs' 2 | import path from 'path' 3 | 4 | /** 5 | * @param {string} fp file path 6 | */ 7 | // eslint-disable-next-line no-unused-vars 8 | const readFileSync = (fp) => { 9 | return fs.readFileSync(fp, 'utf8') 10 | } 11 | 12 | /** 13 | * Check if a file exists 14 | * @param {string} fp file path 15 | * @return {Promise} whether it exists 16 | */ 17 | const pathExists = (fp) => 18 | new Promise((resolve) => { 19 | fs.access(fp, (err) => { 20 | resolve(!err) 21 | }) 22 | }) 23 | 24 | // eslint-disable-next-line no-unused-vars 25 | const pathExistsSync = fs.existsSync 26 | 27 | export default class JoyCon { 28 | constructor({ 29 | files, 30 | cwd = process.cwd(), 31 | stopDir, 32 | packageKey, 33 | parseJSON = JSON.parse, 34 | } = {}) { 35 | this.options = { 36 | files, 37 | cwd, 38 | stopDir, 39 | packageKey, 40 | parseJSON, 41 | } 42 | /** @type {Map} */ 43 | this.existsCache = new Map() 44 | /** @type {Set} */ 45 | this.loaders = new Set() 46 | /** 47 | * We need to read package json data in `.resolve` method to check if `packageKey` exists in the file 48 | * So it makes sense to cache it if the `.resolve` method is called by `.load` method 49 | * @type {Set} 50 | */ 51 | this.packageJsonCache = new Map() 52 | this.loadCache = new Map() 53 | } 54 | 55 | /** 56 | * Add a loader 57 | * @public 58 | * @param {Loader} loader 59 | */ 60 | addLoader(loader) { 61 | this.loaders.add(loader) 62 | 63 | return this 64 | } 65 | 66 | removeLoader(name) { 67 | for (const loader of this.loaders) { 68 | if (name && loader.name === name) { 69 | this.loaders.delete(loader) 70 | } 71 | } 72 | 73 | return this 74 | } 75 | 76 | // $MakeMeSync 77 | async recusivelyResolve(options) { 78 | // Don't traverse above the module root 79 | if ( 80 | options.cwd === options.stopDir || 81 | path.basename(options.cwd) === 'node_modules' 82 | ) { 83 | return null 84 | } 85 | 86 | for (const filename of options.files) { 87 | const file = path.resolve(options.cwd, filename) 88 | const exists = 89 | // Disable cache in tests 90 | process.env.NODE_ENV !== 'test' && this.existsCache.has(file) 91 | ? this.existsCache.get(file) 92 | : await pathExists(file) // eslint-disable-line no-await-in-loop 93 | 94 | this.existsCache.set(file, exists) 95 | 96 | if (exists) { 97 | // If there's no `packageKey` option or this is not a `package.json` file 98 | if (!options.packageKey || path.basename(file) !== 'package.json') { 99 | return file 100 | } 101 | 102 | // For `package.json` and `packageKey` option 103 | // We only consider it to exist when the `packageKey` exists 104 | const data = require(file) 105 | delete require.cache[file] 106 | const hasPackageKey = Object.prototype.hasOwnProperty.call( 107 | data, 108 | options.packageKey, 109 | ) 110 | // The cache will be usd in `.load` method 111 | // But not in the next `require(filepath)` call since we deleted it after require 112 | // For `package.json` 113 | // If you specified the `packageKey` option 114 | // It will only be considered existing when the property exists 115 | if (hasPackageKey) { 116 | this.packageJsonCache.set(file, data) 117 | return file 118 | } 119 | } 120 | 121 | continue 122 | } 123 | 124 | // Continue in the parent directory 125 | return this.recusivelyResolve( 126 | Object.assign({}, options, { cwd: path.dirname(options.cwd) }), 127 | ) // $MakeMeSync 128 | } 129 | 130 | // $MakeMeSync 131 | async resolve(...args) { 132 | const options = this.normalizeOptions(args) 133 | return this.recusivelyResolve(options) // $MakeMeSync 134 | } 135 | 136 | runLoaderSync(loader, filepath) { 137 | return loader.loadSync(filepath) 138 | } 139 | 140 | runLoader(loader, filepath) { 141 | if (!loader.load) return loader.loadSync(filepath) 142 | return loader.load(filepath) 143 | } 144 | 145 | // $MakeMeSync 146 | async load(...args) { 147 | const options = this.normalizeOptions(args) 148 | const filepath = await this.recusivelyResolve(options) 149 | 150 | if (filepath) { 151 | const defaultLoader = { 152 | test: /\.+/, 153 | loadSync: (filepath) => { 154 | const extname = path.extname(filepath).slice(1) 155 | if (extname === 'js' || extname === 'cjs') { 156 | delete require.cache[filepath] 157 | return require(filepath) 158 | } 159 | 160 | if (this.packageJsonCache.has(filepath)) { 161 | return this.packageJsonCache.get(filepath)[options.packageKey] 162 | } 163 | 164 | const data = this.options.parseJSON(readFileSync(filepath)) 165 | return data 166 | }, 167 | } 168 | const loader = this.findLoader(filepath) || defaultLoader 169 | 170 | let data 171 | if (this.loadCache.has(filepath)) { 172 | data = this.loadCache.get(filepath) 173 | } else { 174 | data = await this.runLoader(loader, filepath) 175 | this.loadCache.set(filepath, data) 176 | } 177 | 178 | return { 179 | path: filepath, 180 | data, 181 | } 182 | } 183 | 184 | return {} 185 | } 186 | 187 | /** 188 | * Find a loader for given path 189 | * @param {string} filepath file path 190 | * @return {Loader|null} 191 | */ 192 | findLoader(filepath) { 193 | for (const loader of this.loaders) { 194 | if (loader.test && loader.test.test(filepath)) { 195 | return loader 196 | } 197 | } 198 | 199 | return null 200 | } 201 | 202 | /** Clear cache used by this instance */ 203 | clearCache() { 204 | this.existsCache.clear() 205 | this.packageJsonCache.clear() 206 | this.loadCache.clear() 207 | 208 | return this 209 | } 210 | 211 | normalizeOptions(args) { 212 | const options = Object.assign({}, this.options) 213 | 214 | if (Object.prototype.toString.call(args[0]) === '[object Object]') { 215 | Object.assign(options, args[0]) 216 | } else { 217 | if (args[0]) { 218 | options.files = args[0] 219 | } 220 | if (args[1]) { 221 | options.cwd = args[1] 222 | } 223 | if (args[2]) { 224 | options.stopDir = args[2] 225 | } 226 | } 227 | 228 | options.cwd = path.resolve(options.cwd) 229 | options.stopDir = options.stopDir 230 | ? path.resolve(options.stopDir) 231 | : path.parse(options.cwd).root 232 | 233 | if (!options.files || options.files.length === 0) { 234 | throw new Error('[joycon] files must be an non-empty array!') 235 | } 236 | 237 | options.__normalized__ = true 238 | 239 | return options 240 | } 241 | } 242 | 243 | module.exports = JoyCon 244 | // For TypeScript 245 | module.exports.default = JoyCon 246 | -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | exports.default = void 0; 7 | 8 | var _fs = _interopRequireDefault(require("fs")); 9 | 10 | var _path = _interopRequireDefault(require("path")); 11 | 12 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } 13 | 14 | const readFileSync = fp => { 15 | return _fs.default.readFileSync(fp, 'utf8'); 16 | }; 17 | 18 | const pathExists = fp => new Promise(resolve => { 19 | _fs.default.access(fp, err => { 20 | resolve(!err); 21 | }); 22 | }); 23 | 24 | const pathExistsSync = _fs.default.existsSync; 25 | 26 | class JoyCon { 27 | constructor({ 28 | files, 29 | cwd = process.cwd(), 30 | stopDir, 31 | packageKey, 32 | parseJSON = JSON.parse 33 | } = {}) { 34 | this.options = { 35 | files, 36 | cwd, 37 | stopDir, 38 | packageKey, 39 | parseJSON 40 | }; 41 | this.existsCache = new Map(); 42 | this.loaders = new Set(); 43 | this.packageJsonCache = new Map(); 44 | this.loadCache = new Map(); 45 | } 46 | 47 | addLoader(loader) { 48 | this.loaders.add(loader); 49 | return this; 50 | } 51 | 52 | removeLoader(name) { 53 | for (const loader of this.loaders) { 54 | if (name && loader.name === name) { 55 | this.loaders.delete(loader); 56 | } 57 | } 58 | 59 | return this; 60 | } 61 | 62 | async recusivelyResolve(options) { 63 | if (options.cwd === options.stopDir || _path.default.basename(options.cwd) === 'node_modules') { 64 | return null; 65 | } 66 | 67 | for (const filename of options.files) { 68 | const file = _path.default.resolve(options.cwd, filename); 69 | 70 | const exists = process.env.NODE_ENV !== 'test' && this.existsCache.has(file) ? this.existsCache.get(file) : await pathExists(file); 71 | this.existsCache.set(file, exists); 72 | 73 | if (exists) { 74 | if (!options.packageKey || _path.default.basename(file) !== 'package.json') { 75 | return file; 76 | } 77 | 78 | const data = require(file); 79 | 80 | delete require.cache[file]; 81 | const hasPackageKey = Object.prototype.hasOwnProperty.call(data, options.packageKey); 82 | 83 | if (hasPackageKey) { 84 | this.packageJsonCache.set(file, data); 85 | return file; 86 | } 87 | } 88 | 89 | continue; 90 | } 91 | 92 | return this.recusivelyResolve(Object.assign({}, options, { 93 | cwd: _path.default.dirname(options.cwd) 94 | })); 95 | } 96 | 97 | recusivelyResolveSync(options) { 98 | if (options.cwd === options.stopDir || _path.default.basename(options.cwd) === 'node_modules') { 99 | return null; 100 | } 101 | 102 | for (const filename of options.files) { 103 | const file = _path.default.resolve(options.cwd, filename); 104 | 105 | const exists = process.env.NODE_ENV !== 'test' && this.existsCache.has(file) ? this.existsCache.get(file) : pathExistsSync(file); 106 | this.existsCache.set(file, exists); 107 | 108 | if (exists) { 109 | if (!options.packageKey || _path.default.basename(file) !== 'package.json') { 110 | return file; 111 | } 112 | 113 | const data = require(file); 114 | 115 | delete require.cache[file]; 116 | const hasPackageKey = Object.prototype.hasOwnProperty.call(data, options.packageKey); 117 | 118 | if (hasPackageKey) { 119 | this.packageJsonCache.set(file, data); 120 | return file; 121 | } 122 | } 123 | 124 | continue; 125 | } 126 | 127 | return this.recusivelyResolveSync(Object.assign({}, options, { 128 | cwd: _path.default.dirname(options.cwd) 129 | })); 130 | } 131 | 132 | async resolve(...args) { 133 | const options = this.normalizeOptions(args); 134 | return this.recusivelyResolve(options); 135 | } 136 | 137 | resolveSync(...args) { 138 | const options = this.normalizeOptions(args); 139 | return this.recusivelyResolveSync(options); 140 | } 141 | 142 | runLoaderSync(loader, filepath) { 143 | return loader.loadSync(filepath); 144 | } 145 | 146 | runLoader(loader, filepath) { 147 | if (!loader.load) return loader.loadSync(filepath); 148 | return loader.load(filepath); 149 | } 150 | 151 | async load(...args) { 152 | const options = this.normalizeOptions(args); 153 | const filepath = await this.recusivelyResolve(options); 154 | 155 | if (filepath) { 156 | const defaultLoader = { 157 | test: /\.+/, 158 | loadSync: filepath => { 159 | const extname = _path.default.extname(filepath).slice(1); 160 | 161 | if (extname === 'js' || extname === 'cjs') { 162 | delete require.cache[filepath]; 163 | return require(filepath); 164 | } 165 | 166 | if (extname === 'json') { 167 | if (this.packageJsonCache.has(filepath)) { 168 | return this.packageJsonCache.get(filepath)[options.packageKey]; 169 | } 170 | 171 | const data = this.options.parseJSON(readFileSync(filepath)); 172 | return data; 173 | } 174 | 175 | return readFileSync(filepath); 176 | } 177 | }; 178 | const loader = this.findLoader(filepath) || defaultLoader; 179 | let data; 180 | 181 | if (this.loadCache.has(filepath)) { 182 | data = this.loadCache.get(filepath); 183 | } else { 184 | data = await this.runLoader(loader, filepath); 185 | this.loadCache.set(filepath, data); 186 | } 187 | 188 | return { 189 | path: filepath, 190 | data 191 | }; 192 | } 193 | 194 | return {}; 195 | } 196 | 197 | loadSync(...args) { 198 | const options = this.normalizeOptions(args); 199 | const filepath = this.recusivelyResolveSync(options); 200 | 201 | if (filepath) { 202 | const defaultLoader = { 203 | test: /\.+/, 204 | loadSync: filepath => { 205 | const extname = _path.default.extname(filepath).slice(1); 206 | 207 | if (extname === 'js' || extname === 'cjs') { 208 | delete require.cache[filepath]; 209 | return require(filepath); 210 | } 211 | 212 | if (extname === 'json') { 213 | if (this.packageJsonCache.has(filepath)) { 214 | return this.packageJsonCache.get(filepath)[options.packageKey]; 215 | } 216 | 217 | const data = this.options.parseJSON(readFileSync(filepath)); 218 | return data; 219 | } 220 | 221 | return readFileSync(filepath); 222 | } 223 | }; 224 | const loader = this.findLoader(filepath) || defaultLoader; 225 | let data; 226 | 227 | if (this.loadCache.has(filepath)) { 228 | data = this.loadCache.get(filepath); 229 | } else { 230 | data = this.runLoaderSync(loader, filepath); 231 | this.loadCache.set(filepath, data); 232 | } 233 | 234 | return { 235 | path: filepath, 236 | data 237 | }; 238 | } 239 | 240 | return {}; 241 | } 242 | 243 | findLoader(filepath) { 244 | for (const loader of this.loaders) { 245 | if (loader.test && loader.test.test(filepath)) { 246 | return loader; 247 | } 248 | } 249 | 250 | return null; 251 | } 252 | 253 | clearCache() { 254 | this.existsCache.clear(); 255 | this.packageJsonCache.clear(); 256 | this.loadCache.clear(); 257 | return this; 258 | } 259 | 260 | normalizeOptions(args) { 261 | const options = Object.assign({}, this.options); 262 | 263 | if (Object.prototype.toString.call(args[0]) === '[object Object]') { 264 | Object.assign(options, args[0]); 265 | } else { 266 | if (args[0]) { 267 | options.files = args[0]; 268 | } 269 | 270 | if (args[1]) { 271 | options.cwd = args[1]; 272 | } 273 | 274 | if (args[2]) { 275 | options.stopDir = args[2]; 276 | } 277 | } 278 | 279 | options.cwd = _path.default.resolve(options.cwd); 280 | options.stopDir = options.stopDir ? _path.default.resolve(options.stopDir) : _path.default.parse(options.cwd).root; 281 | 282 | if (!options.files || options.files.length === 0) { 283 | throw new Error('[joycon] files must be an non-empty array!'); 284 | } 285 | 286 | options.__normalized__ = true; 287 | return options; 288 | } 289 | 290 | } 291 | 292 | exports.default = JoyCon; 293 | module.exports = JoyCon; 294 | module.exports.default = JoyCon; --------------------------------------------------------------------------------