Skip to content

Commit

Permalink
feat(fs): search for config in usual places; closes #83
Browse files Browse the repository at this point in the history
This will additionally search for an `.rtkrc.js` or `rtk.config.js` in
`$HOME`, `$(npm prefix)/etc`, and if on POSIX OS, XDG config dir(s) and
`/etc`, in that order.

I note that it's not particularly ergonomic to do this using
`cosmiconfig`'s API.
  • Loading branch information
boneskull committed Feb 13, 2020
1 parent 209b955 commit b11a329
Show file tree
Hide file tree
Showing 4 changed files with 107 additions and 18 deletions.
18 changes: 18 additions & 0 deletions packages/fs/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 3 additions & 1 deletion packages/fs/package.json
Expand Up @@ -5,7 +5,9 @@
"author": "Christopher Hiller <christopher.hiller@ibm.com> (https://boneskull.com/)",
"dependencies": {
"@report-toolkit/common": "^0.5.1",
"cosmiconfig": "^5.2.1"
"cosmiconfig": "^5.2.1",
"global-dirs": "^2.0.1",
"xdg-basedir": "^4.0.0"
},
"engines": {
"node": ">=10"
Expand Down
54 changes: 44 additions & 10 deletions packages/fs/src/fs-config-loader.js
@@ -1,5 +1,9 @@
import {_, createDebugPipe, error, observable} from '@report-toolkit/common';
import path from 'path';
import cosmiconfig from 'cosmiconfig';
import xdgBasedir from 'xdg-basedir';
import globalDirs from 'global-dirs';
import os from 'os';

import {RC_NAMESPACE} from './constants.js';

Expand All @@ -8,14 +12,26 @@ const {
map,
mapTo,
mergeMap,
filter,
concatMap,
concatMapTo,
of,
pipeIf,
switchMapTo,
take,
throwRTkError
} = observable;

const debug = createDebugPipe('cli', 'loaders', 'config');

const EXTRA_SEARCH_DIRS = [
os.homedir(),
path.join(globalDirs.npm.prefix, 'etc')
];
if (os.platform() !== 'win32') {
EXTRA_SEARCH_DIRS.push(...xdgBasedir.configDirs, '/etc');
}

const getExplorer = _.memoize(opts =>
cosmiconfig(
RC_NAMESPACE,
Expand All @@ -29,7 +45,12 @@ const getExplorer = _.memoize(opts =>
)
);

const toConfigFromSearchPath = (opts = {}) => {
/**
*
* @param {cosmiconfig.ExplorerOptions} [opts] - Extra opts for cosmiconfig
* @returns {import('rxjs').OperatorFunction<string,object>}
*/
function toConfigFromSearchPath(opts = {}) {
const explorer = getExplorer(opts);
return observable =>
observable.pipe(
Expand All @@ -40,34 +61,46 @@ const toConfigFromSearchPath = (opts = {}) => {
map(_.get('config.config'))
)
);
};
}

/**
*
* @param {cosmiconfig.ExplorerOptions} [opts] - Extra opts for cosmiconfig
* @returns {import('rxjs').OperatorFunction<string,object>}
*/
const toConfigFromFilepath = (opts = {}) => {
function toConfigFromFilepath(opts = {}) {
const explorer = getExplorer(opts);
return observable =>
observable.pipe(
mergeMap(filepath => explorer.load(filepath)),
map(_.get('config.config'))
);
};
}

export const fromFilesystemToConfig = ({
config: rawConfigOrFilepath = {},
searchPath = process.cwd(),
search = true
} = {}) =>
of(rawConfigOrFilepath).pipe(
} = {}) => {
const allSearchPaths = [searchPath, ...EXTRA_SEARCH_DIRS];
return of(rawConfigOrFilepath).pipe(
pipeIf(_.isString, toConfigFromFilepath()),
pipeIf(
rawConfig => _.isEmpty(rawConfig) && search,
debug(() => `searching in ${searchPath} for config`),
mapTo(searchPath),
toConfigFromSearchPath()
/** @param {object} rawConfig */ rawConfig =>
_.isEmpty(rawConfig) && search,
concatMapTo(allSearchPaths),
debug(allSearchPath => `searching in ${allSearchPath} for config`),
concatMap(allSearchPath =>
of(allSearchPath).pipe(
pipeIf(allSearchPath === searchPath, toConfigFromSearchPath()),
pipeIf(
allSearchPath !== searchPath,
toConfigFromSearchPath({stopDir: allSearchPath})
)
)
),
filter(_.negate(_.isEmpty)),
take(1)
),
pipeIf(
_.isEmpty && _.isString(rawConfigOrFilepath),
Expand All @@ -80,3 +113,4 @@ export const fromFilesystemToConfig = ({
),
pipeIf(_.isEmpty, mapTo({}))
);
};
49 changes: 42 additions & 7 deletions packages/fs/test/fs-config-loader.spec.js
Expand Up @@ -10,6 +10,15 @@ const defaultConfig = [
}
];

const etcConfig = [
'report-toolkit:recommended',
{
rules: {
'long-timeout': ['on', {timeout: 1000}]
}
}
];

describe('@report-toolkit/fs:fs-config-loader', function() {
let sandbox;

Expand All @@ -27,15 +36,17 @@ describe('@report-toolkit/fs:fs-config-loader', function() {
let subject;

beforeEach(function() {
const searchStub = sandbox.stub();
searchStub
.withArgs(process.cwd())
.resolves({config: {config: defaultConfig}});
searchStub
.withArgs(join(__dirname, 'fixture', 'config'))
.resolves({config: {config: customConfig}});
searchStub.withArgs('/etc').resolves({config: {config: etcConfig}});
subject = proxyquire(require.resolve('../src/fs-config-loader.js'), {
cosmiconfig: () => ({
search: sandbox
.stub()
.callsFake(searchPath =>
searchPath === process.cwd()
? Promise.resolve({config: {config: defaultConfig}})
: Promise.resolve({config: {config: customConfig}})
)
search: searchStub
})
}).fromFilesystemToConfig;
});
Expand All @@ -48,6 +59,30 @@ describe('@report-toolkit/fs:fs-config-loader', function() {
defaultConfig
).and('to emit once');
});

describe('when not found at default directory', function() {
beforeEach(function() {
const searchStub = sandbox.stub();
searchStub.resolves();
searchStub
.withArgs('/etc')
.resolves({config: {config: etcConfig}});
subject = proxyquire(
require.resolve('../src/fs-config-loader.js'),
{
cosmiconfig: () => ({
search: searchStub
})
}
).fromFilesystemToConfig;
});

it('should try other directories', function() {
return expect(subject(), 'to complete with value', etcConfig).and(
'to emit once'
);
});
});
});

describe('when passed an explicit search path', function() {
Expand Down

0 comments on commit b11a329

Please sign in to comment.