get-es-imports-exports
Version:
Recursively get imports and exports for files using ES import/export syntax
235 lines (190 loc) • 6.78 kB
JavaScript
import {
map, get, flow, set, filter, over, fromPairs, assignWith, union, includes, keys, reject, first,
partial, cond, matchesProperty, constant, some, castArray, isEmpty, equals, sortBy, identity,
mapValues, flatMap, groupBy, overSome, toPairs, update, reduce, concat, curry,
} from 'lodash/fp';
import resolve from 'resolve';
import minimatch from 'minimatch';
import { relative, dirname, extname } from 'path';
import { readFile } from 'fs';
import promisify from 'tiny-promisify';
const readFilePromise = promisify(readFile);
export const defaultParser = 'espree';
export const defaultParserOptions = {
ecmaVersion: 6,
sourceType: 'module',
ecmaFeatures: {
jsx: true,
},
};
const relativeToNodeModulesRe = /^[./\\]+\/node_modules[/\\]/;
const pointsToNodeModules = value => relativeToNodeModulesRe.test(value);
const getDeclarationFilename = get(['source', 'value']);
const getAstDeclarationImports = flow(
get('body'),
filter({ type: 'ImportDeclaration' })
);
const getImportDeclarationImportedNames = flow(
get('specifiers'),
map(cond([
[matchesProperty('type', 'ImportDefaultSpecifier'), constant('default')],
[matchesProperty('type', 'ImportNamespaceSpecifier'), constant('*')],
[matchesProperty('type', 'ImportSpecifier'), get(['imported', 'name'])],
]))
);
const getAstImportsFromImportStatements = flow(
getAstDeclarationImports,
groupBy(getDeclarationFilename),
mapValues(flatMap(getImportDeclarationImportedNames))
);
// Exports
const getVariableDeclarationExportedNames = cond([
[matchesProperty('type', 'FunctionDeclaration'), get(['id', 'name'])],
[matchesProperty('type', 'ClassDeclaration'), get(['id', 'name'])],
[matchesProperty('type', 'VariableDeclaration'), flow(get('declarations'), flatMap('id.name'))],
]);
const exportNamedDeclarationExportedNames = flow(get('specifiers'), flatMap('exported.name'));
const exportNamedDeclarationImportNames = flow(get('specifiers'), flatMap('local.name'));
const getExportNamedDeclarationExportedNames = cond([
[matchesProperty('declaration', null), exportNamedDeclarationExportedNames],
[constant(true), flow(get('declaration'), getVariableDeclarationExportedNames)],
]);
const isExportDefaultDeclaration = matchesProperty('type', 'ExportDefaultDeclaration');
const isExportAllDeclaration = matchesProperty('type', 'ExportAllDeclaration');
const isExportNamedDeclaration = matchesProperty('type', 'ExportNamedDeclaration');
const getExportDeclarationExportedNames = cond([
[isExportDefaultDeclaration, constant(['default'])],
[isExportAllDeclaration, constant(['*'])],
[isExportNamedDeclaration, getExportNamedDeclarationExportedNames],
]);
const getExportDeclarationImportedNames = cond([
[isExportNamedDeclaration, exportNamedDeclarationImportNames],
[isExportAllDeclaration, constant(['*'])],
]);
const getAstExportsDeclarations = flow(
get('body'),
filter(overSome([
isExportDefaultDeclaration,
isExportAllDeclaration,
isExportNamedDeclaration,
]))
);
const getAstImportsFromExportStatements = flow(
getAstExportsDeclarations,
filter(getDeclarationFilename),
groupBy(getDeclarationFilename),
mapValues(flatMap(getExportDeclarationImportedNames))
);
const getAstLocalExports = flow(
getAstExportsDeclarations,
flatMap(getExportDeclarationExportedNames)
);
const getAstLocalImports = flow(
over([
getAstImportsFromImportStatements,
getAstImportsFromExportStatements,
]),
reduce(assignWith(union), {})
);
const resolveDeclrationFilenamePair = (basedir, resolveOptions, [dependency, value]) => (
new Promise((res, rej) => {
resolve(dependency, set('basedir', basedir, resolveOptions), (err, resolvedFilename) => (
!err ? res([resolvedFilename, value]) : rej(err)
));
})
);
const match = curry((file, matcher) => minimatch(file, matcher, { matchBase: true }));
export default async function getDependencies({
files = [],
recurse = true,
exclude = [],
parser = defaultParser,
parserOptions = defaultParserOptions,
resolveOptions = {},
}) {
const { parse } = require(parser); // eslint-disable-line
const excludeValues = castArray(exclude);
const fileIsExcluded = isEmpty(excludeValues)
? constant(false)
: file => some(cond([
[includes('*'), partial(match, [file])],
[constant(true), equals(file)],
]), excludeValues);
const addFile = async (state, filename) => {
const { imports, exports, loadedFiles, stats } = state;
const skip = includes(filename, loadedFiles) || fileIsExcluded(filename);
if (skip) return { localImports: [], state };
let contents;
try {
contents = String(await readFilePromise(filename, 'utf-8'));
} catch (e) {
if (e.code === 'ENOENT') {
throw new Error(`Failed to read file ${filename}`);
} else {
throw e;
}
}
const basedir = dirname(filename);
let ast;
try {
ast = parse(contents, parserOptions);
} catch (e) {
if (extname(filename) === '.js') {
throw e;
}
return update(['state', 'stats'], concat([`Failed to open file ${filename}`]), state);
}
const localExports = getAstLocalExports(ast);
const allUnresolvedImportsPairsPromises = flow(
getAstLocalImports,
toPairs,
map(partial(resolveDeclrationFilenamePair, [basedir, resolveOptions]))
)(ast);
const allImportsPairs = await Promise.all(allUnresolvedImportsPairsPromises);
const allImports = fromPairs(allImportsPairs);
const pairIsNodeModule = flow(
first,
partial(relative, [filename]),
pointsToNodeModules
);
const localImports = flow(
reject(pairIsNodeModule),
fromPairs
)(allImportsPairs);
return {
localImports: keys(localImports),
state: {
imports: assignWith(union, allImports, imports),
exports: set([filename], localExports, exports),
loadedFiles: union([filename], loadedFiles),
stats,
},
};
};
let state = {
imports: {},
exports: {},
loadedFiles: [],
stats: [],
};
const getDependenciesFromFile = async (inputState, filename) => {
let { localImports, state } = await addFile(inputState, filename); // eslint-disable-line
if (!recurse) {
return state;
}
for (const otherImport of localImports) {
state = await getDependenciesFromFile(state, otherImport);
}
return state;
};
const filesValues = castArray(files);
for (const filename of filesValues) {
state = await getDependenciesFromFile(state, filename);
}
const sortValues = mapValues(sortBy(identity));
state = flow(
update('imports', sortValues),
update('exports', sortValues)
)(state);
return state;
}