Skip to content

Commit

Permalink
Expose getRealPath and normalizeOptions (#194)
Browse files Browse the repository at this point in the history
* Expose getRealPath

* Expose normalizeOptions

* Rename "Methods" to "Functions" for consistency

* Document the exposed API

* Rename getRealPath to resolvePath and hide normalizeOptions

* Improve wording

* Remove the context from the resolvePath call

* Use reselect to memoize normalizeOptions
  • Loading branch information
fatfisz authored and tleunen committed Jun 20, 2017
1 parent feefe9e commit 547578f
Show file tree
Hide file tree
Showing 9 changed files with 171 additions and 77 deletions.
17 changes: 17 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@ Specify the plugin in your `.babelrc` with the custom root or alias. Here's an e
}
```

Are you a plugin author (e.g. IDE integration)? We have [documented the exposed functions](#for-plugin-authors) for use in your plugins!

### Options

- `root`: A string or an array of root directories. Specify the paths or a glob path (eg. `./src/**/components`)
Expand Down Expand Up @@ -138,6 +140,21 @@ More configuration options are located in [the Flow documentation](https://flowt
- Atom: Uses [atom-autocomplete-modules][atom-autocomplete-modules] and enable the `babel-plugin-module-resolver` option.
- IntelliJ/WebStorm: You can add custom resources root directories, make sure it matches what you have in this plugin.

## For plugin authors

Aside from the main export, which is the plugin itself as needed by Babel, there is a function used internally that is exposed:

```js
import { resolvePath } from 'babel-plugin-module-resolver';

// `opts` are the options as passed to the Babel config (should have keys like "root", "alias", etc.)
const realPath = resolvePath(sourcePath, currentFile, opts);
```

For each path in the file you can use `resolvePath` to get the same path that module-resolver will output.

`currentFile` can be either a relative path (will be resolved with respect to the CWD, not `opts.cwd`), or an absolute path.

## License

MIT, see [LICENSE.md](/LICENSE.md) for details.
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
"find-babel-config": "^1.0.1",
"glob": "^7.1.1",
"pkg-up": "^2.0.0",
"reselect": "^3.0.1",
"resolve": "^1.3.2"
},
"devDependencies": {
Expand Down
9 changes: 8 additions & 1 deletion src/index.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,13 @@
import normalizeOptions from './normalizeOptions';
import resolvePath from './resolvePath';
import transformCall from './transformers/call';
import transformImport from './transformers/import';


// Public API for external plugins
export { resolvePath };


const importVisitors = {
CallExpression: transformCall,
'ImportDeclaration|ExportDeclaration': transformImport,
Expand All @@ -19,7 +24,9 @@ const visitor = {
export default ({ types }) => ({
pre(file) {
this.types = types;
normalizeOptions(this.opts, file);

const currentFile = file.opts.filename;
this.normalizedOpts = normalizeOptions(currentFile, this.opts);
},

visitor,
Expand Down
139 changes: 74 additions & 65 deletions src/normalizeOptions.js
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
import fs from 'fs';
import path from 'path';
import { createSelector } from 'reselect';

import findBabelConfig from 'find-babel-config';
import glob from 'glob';
import pkgUp from 'pkg-up';


const defaultExtensions = ['.js', '.jsx', '.es', '.es6', '.mjs'];
const defaultTransformedMethods = [
const defaultTransformedFunctions = [
'require',
'require.resolve',
'System.import',
Expand All @@ -22,53 +23,52 @@ function isRegExp(string) {
return string.startsWith('^') || string.endsWith('$');
}

function normalizeCwd(opts, file) {
if (opts.cwd === 'babelrc') {
const startPath = (file.opts.filename === 'unknown')
? './'
: file.opts.filename;
const specialCwd = {
babelrc: startPath => findBabelConfig.sync(startPath).file,
packagejson: startPath => pkgUp.sync(startPath),
};

const { file: babelPath } = findBabelConfig.sync(startPath);
function normalizeCwd(optsCwd, currentFile) {
let cwd;

opts.cwd = babelPath
? path.dirname(babelPath)
: null;
} else if (opts.cwd === 'packagejson') {
const startPath = (file.opts.filename === 'unknown')
if (optsCwd in specialCwd) {
const startPath = (currentFile === 'unknown')
? './'
: file.opts.filename;
: currentFile;

const pkgPath = pkgUp.sync(startPath);
const computedCwd = specialCwd[optsCwd](startPath);

opts.cwd = pkgPath
? path.dirname(pkgPath)
cwd = computedCwd
? path.dirname(computedCwd)
: null;
} else {
cwd = optsCwd;
}
if (!opts.cwd) {
opts.cwd = process.cwd();
}

return cwd || process.cwd();
}

function normalizeRoot(opts) {
if (opts.root) {
if (!Array.isArray(opts.root)) {
opts.root = [opts.root];
}
opts.root = opts.root
.map(dirPath => path.resolve(opts.cwd, dirPath))
.reduce((resolvedDirs, absDirPath) => {
if (glob.hasMagic(absDirPath)) {
const roots = glob.sync(absDirPath)
.filter(resolvedPath => fs.lstatSync(resolvedPath).isDirectory());

return [...resolvedDirs, ...roots];
}

return [...resolvedDirs, absDirPath];
}, []);
} else {
opts.root = [];
function normalizeRoot(optsRoot, cwd) {
if (!optsRoot) {
return [];
}

const rootArray = Array.isArray(optsRoot)
? optsRoot
: [optsRoot];

return rootArray
.map(dirPath => path.resolve(cwd, dirPath))
.reduce((resolvedDirs, absDirPath) => {
if (glob.hasMagic(absDirPath)) {
const roots = glob.sync(absDirPath)
.filter(resolvedPath => fs.lstatSync(resolvedPath).isDirectory());

return [...resolvedDirs, ...roots];
}

return [...resolvedDirs, absDirPath];
}, []);
}

function getAliasPair(key, value) {
Expand All @@ -85,36 +85,45 @@ function getAliasPair(key, value) {
return [new RegExp(key), substitute];
}

function normalizeAlias(opts) {
if (opts.alias) {
const { alias } = opts;
const aliasKeys = Object.keys(alias);

opts.alias = aliasKeys.map(key => (
isRegExp(key) ?
getAliasPair(key, alias[key]) :
getAliasPair(`^${key}(/.*|)$`, `${alias[key]}\\1`)
));
} else {
opts.alias = [];
function normalizeAlias(optsAlias) {
if (!optsAlias) {
return [];
}
}

function normalizeTransformedMethods(opts) {
if (opts.transformFunctions) {
opts.transformFunctions = [...defaultTransformedMethods, ...opts.transformFunctions];
} else {
opts.transformFunctions = defaultTransformedMethods;
}
}
const aliasKeys = Object.keys(optsAlias);

export default function normalizeOptions(opts, file) {
normalizeCwd(opts, file); // This has to go first because other options rely on cwd
normalizeRoot(opts);
normalizeAlias(opts);
normalizeTransformedMethods(opts);
return aliasKeys.map(key => (
isRegExp(key) ?
getAliasPair(key, optsAlias[key]) :
getAliasPair(`^${key}(/.*|)$`, `${optsAlias[key]}\\1`)
));
}

if (!opts.extensions) {
opts.extensions = defaultExtensions;
function normalizeTransformedFunctions(optsTransformFunctions) {
if (!optsTransformFunctions) {
return defaultTransformedFunctions;
}

return [...defaultTransformedFunctions, ...optsTransformFunctions];
}

export default createSelector(
// The currentFile should have an extension; otherwise it's considered a special value
currentFile => (currentFile.includes('.') ? path.dirname(currentFile) : currentFile),
(_, opts) => opts,
(currentFile, opts) => {
const cwd = normalizeCwd(opts.cwd, currentFile);
const root = normalizeRoot(opts.root, cwd);
const alias = normalizeAlias(opts.alias);
const transformFunctions = normalizeTransformedFunctions(opts.transformFunctions);
const extensions = opts.extensions || defaultExtensions;

return {
cwd,
root,
alias,
transformFunctions,
extensions,
};
},
);
17 changes: 10 additions & 7 deletions src/getRealPath.js → src/resolvePath.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import path from 'path';

import { warn } from './log';
import mapToRelative from './mapToRelative';
import normalizeOptions from './normalizeOptions';
import { nodeResolvePath, replaceExtension, toLocalPath, toPosixPath } from './utils';


Expand All @@ -17,7 +18,7 @@ function findPathInRoots(sourcePath, { extensions, root }) {
return resolvedSourceFile;
}

function getRealPathFromRootConfig(sourcePath, currentFile, opts) {
function resolvePathFromRootConfig(sourcePath, currentFile, opts) {
const absFileInRoot = findPathInRoots(sourcePath, opts);

if (!absFileInRoot) {
Expand All @@ -42,7 +43,7 @@ function checkIfPackageExists(modulePath, currentFile, extensions) {
}
}

function getRealPathFromAliasConfig(sourcePath, currentFile, opts) {
function resolvePathFromAliasConfig(sourcePath, currentFile, opts) {
let aliasedSourceFile;

opts.alias.find(([regExp, substitute]) => {
Expand Down Expand Up @@ -74,22 +75,24 @@ function getRealPathFromAliasConfig(sourcePath, currentFile, opts) {
}

const resolvers = [
getRealPathFromRootConfig,
getRealPathFromAliasConfig,
resolvePathFromRootConfig,
resolvePathFromAliasConfig,
];

export default function getRealPath(sourcePath, { file, opts }) {
export default function resolvePath(sourcePath, currentFile, opts) {
if (sourcePath[0] === '.') {
return sourcePath;
}

const normalizedOpts = normalizeOptions(currentFile, opts);

// File param is a relative path from the environment current working directory
// (not from cwd param)
const currentFile = path.resolve(file.opts.filename);
const absoluteCurrentFile = path.resolve(currentFile);
let resolvedPath = null;

resolvers.some((resolver) => {
resolvedPath = resolver(sourcePath, currentFile, opts);
resolvedPath = resolver(sourcePath, absoluteCurrentFile, normalizedOpts);
return resolvedPath !== null;
});

Expand Down
2 changes: 1 addition & 1 deletion src/transformers/call.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import {

export default function transformCall(nodePath, state) {
const calleePath = nodePath.get('callee');
const isNormalCall = state.opts.transformFunctions.some(
const isNormalCall = state.normalizedOpts.transformFunctions.some(
pattern => matchesPattern(state.types, calleePath, pattern),
);

Expand Down
8 changes: 6 additions & 2 deletions src/utils.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import path from 'path';

import resolve from 'resolve';
import getRealPath from './getRealPath';
import resolvePath from './resolvePath';


export function nodeResolvePath(modulePath, basedir, extensions) {
Expand Down Expand Up @@ -48,7 +48,11 @@ export function mapPathString(nodePath, state) {
return;
}

const modulePath = getRealPath(nodePath.node.value, state);
const sourcePath = nodePath.node.value;
const currentFile = state.file.opts.filename;
const opts = state.opts;

const modulePath = resolvePath(sourcePath, currentFile, opts);
if (modulePath) {
if (nodePath.node.pathResolved) {
return;
Expand Down
19 changes: 18 additions & 1 deletion test/index.test.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import path from 'path';

import { transform } from 'babel-core';
import plugin from '../src';
import plugin, { resolvePath } from '../src';


describe('module-resolver', () => {
Expand All @@ -12,6 +12,23 @@ describe('module-resolver', () => {
expect(result.code).toBe(`import something from "${output}";`);
}

describe('exports', () => {
describe('resolvePath', () => {
it('should be a function', () => {
expect(resolvePath).toEqual(expect.any(Function));
});

it('should resolve the file path', () => {
const opts = {
root: ['./test/testproject/src'],
};
const result = resolvePath('app', './test/testproject/src/app', opts);

expect(result).toBe('./app');
});
});
});

describe('root', () => {
describe('simple root', () => {
const rootTransformerOpts = {
Expand Down
36 changes: 36 additions & 0 deletions test/normalizeOptions.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import normalizeOptions from '../src/normalizeOptions';


describe('normalizeOptions', () => {
beforeEach(() => {
normalizeOptions.resetRecomputations();
});

it('should return the memoized options when the dirnames are the same', () => {
const options = {};
const result = normalizeOptions('path/a.js', options);
const result2 = normalizeOptions('path/b.js', options);

expect(result).toBe(result2);
expect(normalizeOptions.recomputations()).toEqual(1);
});

it('should return the memoized options when the special paths are the same', () => {
const options = {};
const result = normalizeOptions('unknown', options);
const result2 = normalizeOptions('unknown', options);

expect(result).toBe(result2);
expect(normalizeOptions.recomputations()).toEqual(1);
});

it('should recompute when the options object is not the same', () => {
const options = {};
const options2 = {};
const result = normalizeOptions('path/to/file.js', options);
const result2 = normalizeOptions('path/to/file.js', options2);

expect(result).not.toBe(result2);
expect(normalizeOptions.recomputations()).toEqual(2);
});
});

0 comments on commit 547578f

Please sign in to comment.