18

I am creating an npm package that needs to be able to read config files from the project root. I'm not sure how to do this.

For example,

  • Next.js is able to read ./pages/ and ./next.config.js from the project root
  • Jest is able to read ./jest.config.js from the project root
  • ESLint is able to read ./.eslintrc.json from the project root
  • Prettier is able to read ./.prettierrc.js from the project root
  • Typescript is able to read ./tsconfig.json from the project root
  • Babel is able to read ./.babelrc from the project root

I've tried looking at their source code to see how they do it but the projects are so large that I can't find the relevant section.

How do they achieve this?

Jake
  • 3,865
  • 5
  • 25
  • 58
  • 2
    Via methods such as; [`require()`](https://nodejs.org/api/modules.html#modules_require_id), [`fs.readFile()`](https://nodejs.org/api/fs.html#fs_fs_readfile_path_options_callback), or [`fs.readFileSync()`](https://nodejs.org/api/fs.html#fs_fs_readfilesync_path_options). If either of the last two methods are used they subsequently `JSON.parse()` content if config file is JSON. For instance _eslintrc.json_ utilizes [`fs.readFileSync`](https://github.com/eslint/eslint/blob/b5bde0669bd6a7a6b8e38cdf204d8d4b932cea63/lib/cli-engine/config-array-factory.js#106). – RobC Jun 30 '19 at 16:47
  • There is no standard for what constitutes a _project root_. That is why e.g. ESLint will look up for `.eslintrc` files starting from the current directory (or from another directory provided by the user) up to the file system root. npm does something similar when searching for `package.json`, except that it stops on the first match. – GOTO 0 Feb 10 '22 at 15:16

5 Answers5

3

First search in path.dirname(process.mainModule.filename) then go up the directory tree ../, ../../, ../../../ and so on until you find the configuration file.

Here is the code I use stolen from rc (https://github.com/dominictarr/rc) package, it will read and json parse configuration from a file named .projectrc:

const fs = require('fs');
const path = require('path');

// Utils shamefully stolen from
// https://github.com/dominictarr/rc/blob/master/lib/utils.js

find(...args) {
  const rel = path.join.apply(null, [].slice.call(args));
  return findStartingWith(path.dirname(process.mainModule.filename), rel);
}

findStartingWith(start, rel) {
  const file = path.join(start, rel);
  try {
    fs.statSync(file);
    return file;
  } catch (err) {
    // They are equal for root dir
    if (path.dirname(start) !== start) {
      return findStartingWith(path.dirname(start), rel);
    }
  }
}

parse(content) {
  if (/^\s*{/.test(content)) {
    return JSON.parse(content);
  }
  return undefined;
}

file(...args) {
  const nonNullArgs = [].slice.call(args).filter(arg => arg != null);

  // path.join breaks if it's a not a string, so just skip this.
  for (let i = 0; i < nonNullArgs.length; i++) {
    if (typeof nonNullArgs[i] !== 'string') {
      return;
    }
  }

  const file = path.join.apply(null, nonNullArgs);
  try {
    return fs.readFileSync(file, 'utf-8');
  } catch (err) {
    return undefined;
  }
}

json(...args) {
  const content = file.apply(null, args);
  return content ? parse(content) : null;
}

// Find the rc file path
const rcPath = find('.projectrc');
// Or
// const rcPath = find('/.config', '.projectrc');

// Read the contents as json
const rcObject = json(rcPath);
console.log(rcObject);

You can also use the rc package as a dependency npm i rc, then in your code:

var configuration = require('rc')(appname, {
  // Default configuration goes here
  port: 2468
});

This will read config from a file named .${appname}rc.

Andrei
  • 4,289
  • 4
  • 25
  • 23
3

Ran into this problem when I made my first npm package. The findup-sync library solves this nicely:

const findup = require('findup-sync');
const filePath = findup('filename');

https://www.npmjs.com/package/findup-sync

kronosapiens
  • 1,333
  • 1
  • 10
  • 19
0

They start from the directory the file lives in and recursively looks upwards in the file system tree until it finds the file it's looks for.

Something like this:

const FILE_NAME = 'target-file.json';

const fsp = require('fs').promises,
      path = require('path');

let find = async (dir=__dirname) => {
  let ls = await fsp.readdir(dir);
  if(ls.includes(FILE_NAME))
    return path.join(dir,FILE_NAME);
  else if(dir == '/')
    throw new Error(`Could not find ${FILE_NAME}`);
  else
    return find(path.resolve(dir,'..'));
}

Or if you were looking for a standard node "project root" you might want to recurse up and find a directory containing a directory names 'node_modules' like this:

const fsp = require('fs').promises,
      path = require('path');

let find = async (dir=__dirname) => {
  let ls = await fsp.readdir(dir);
  if(ls.includes('node_modules'))
    return dir;
  else if(dir == '/')
    throw new Error(`Could not find project root`);
  else
    return find(path.resolve(dir,'..'));
}
leitning
  • 1,081
  • 1
  • 6
  • 10
0

I was also looking to solve similar problem with my npm package and found out these 2 modules. I have used lilconfig as its smaller in size and satisfies my use case.

lilconfig (16.6 kB)

const {lilconfigSync} = require('lilconfig');
const configSync =  lilconfigSync("filename");
const config = configSync.search();

cosmiconfig (111 kB)

const { cosmiconfigSync } = require('cosmiconfig');
const explorerSync = cosmiconfigSync("filename");
const searchedFor = explorerSync.search();
shashank
  • 439
  • 1
  • 4
  • 17
-1

There are multiple ways to do it. I've created a test-package, and a demo project node-package-test to test it.

For ready reference providing the main code here:

project-main\node_modules\test-package\index.js :

const path = require('path');
const fs = require('fs');

const CONFIG_NAME = 'cfg.json';

function init(rootDir = null) {
  console.log(`test-package: process.cwd(): ${process.cwd()}`);
  console.log(`test-package: path.resolve('./'): ${path.resolve('./')}`);

  if (!rootDir) {
    //rootDir = path.resolve('./');
    // OR
    rootDir = process.cwd();
  }

  //const configPath = path.resolve('./', CONFIG_NAME);
  // OR
  const configPath = path.join(rootDir, CONFIG_NAME);


  if (fs.existsSync(configPath)) {
    console.log(`test-package: Reading config from: ${configPath}`);
    try {
      //const data = fs.readFileSync(configPath, 'utf8');
      //const config = JSON.parse(data);
      // OR
      const config = require(configPath);
      console.log(config);
    } catch (err) {
      console.error(err)
    }
  } else {

    console.log(`test-package: Couldn't find config file ${configPath}. Using default.`)
  }

  console.log('\n')
}

//init()
const features = {
  init: init,
  message: `Hello from test-package! `
}


module.exports = features;

project-main\main.js :

const utils = require('@onkarruikar/test-package')

utils.init();
// OR use
//utils.init('/path/to/rootdir');

console.log(`${utils.message}`);

Output:

E:\node-package-test-main>npm install

added 1 package, and audited 2 packages in 4s

found 0 vulnerabilities

E:\node-package-test-main>npm start

> start
> node .

test-package: process.cwd(): E:\node-package-test-main
test-package: path.resolve('./'): E:\node-package-test-main
test-package: Reading config from: E:\node-package-test-main\cfg.json
{ compilerOptions: { allowJs: true, checkJs: true, noEmit: true } }


Hello from test-package! 
the Hutt
  • 16,980
  • 2
  • 14
  • 44
  • 1
    if its a js or json file, it would be easier to use require. Then you cann access it like an object straight away. No need to go for json.parse. – The Fool Feb 07 '22 at 15:35
  • Yes it can be used. If you are allowing other extensions then you need to check first which one exists in the root and then load accordingly. – the Hutt Feb 07 '22 at 15:45
  • The package that we talked about is in the node_modules of the user package, so if it will run this script, it will have a side effect on the user package(for example if we install this package inside of a nextjs app, it will run in the client side, and cause error), in addition to that, we can't read files in the user package from the node_modules using this script – marzzy Feb 09 '22 at 07:09
  • By 'client side', do you mean in user's web browser? This is [how](https://github.com/facebook/jest/blob/432dfb686e7586a9ce89c086b261eab432424960/packages/jest-cli/src/init/index.ts#L41) JestJs does it. To understand better let us know what features your package is going to provide. Is it related to web applications? I've updated the answer. Install and run the [node-package-test](https://github.com/OnkarRuikar/node-package-test) application and let us know if you are looking for different behavior. – the Hutt Feb 09 '22 at 10:27