348 lines
10 KiB
JavaScript
348 lines
10 KiB
JavaScript
|
'use strict';
|
||
|
|
||
|
const path = require('path');
|
||
|
const debug = require('debug')('cabinet');
|
||
|
|
||
|
/*
|
||
|
* most js resolver are lazy-loaded (only required when needed)
|
||
|
* e.g. dont load requirejs when we only have commonjs modules to resolve
|
||
|
* this makes testing your code using this lib much easier
|
||
|
*/
|
||
|
|
||
|
let getModuleType;
|
||
|
let resolve;
|
||
|
|
||
|
let amdLookup;
|
||
|
const stylusLookup = require('stylus-lookup');
|
||
|
const sassLookup = require('sass-lookup');
|
||
|
let ts;
|
||
|
|
||
|
let resolveDependencyPath;
|
||
|
const appModulePath = require('app-module-path');
|
||
|
let webpackResolve;
|
||
|
const isRelative = require('is-relative-path');
|
||
|
|
||
|
const defaultLookups = {
|
||
|
'.js': jsLookup,
|
||
|
'.jsx': jsLookup,
|
||
|
'.ts': tsLookup,
|
||
|
'.tsx': tsLookup,
|
||
|
'.scss': sassLookup,
|
||
|
'.sass': sassLookup,
|
||
|
'.styl': stylusLookup,
|
||
|
// Less and Sass imports are very similar
|
||
|
'.less': sassLookup
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
* @param {Object} options
|
||
|
* @param {String} options.partial The dependency being looked up
|
||
|
* @param {String} options.filename The file that contains the dependency being looked up
|
||
|
* @param {String|Object} [options.config] Path to a requirejs config
|
||
|
* @param {String} [options.configPath] For AMD resolution, if the config is an object, this represents the location of the config file.
|
||
|
* @param {Object} [options.nodeModulesConfig] Config for overriding the entry point defined in a package json file
|
||
|
* @param {String} [options.nodeModulesConfig.entry] The new value for "main" in package json
|
||
|
* @param {String} [options.webpackConfig] Path to the webpack config
|
||
|
* @param {Object} [options.ast] A preparsed AST for the file identified by filename.
|
||
|
* @param {Object} [options.tsConfig] Path to a typescript config file
|
||
|
* @param {boolean} [options.noTypeDefinitions] Whether to return '.d.ts' files or '.js' files for a dependency
|
||
|
*/
|
||
|
module.exports = function cabinet(options) {
|
||
|
const {
|
||
|
partial,
|
||
|
filename,
|
||
|
} = options;
|
||
|
|
||
|
debug('Given filename: ' + filename);
|
||
|
|
||
|
const ext = path.extname(filename);
|
||
|
debug('which has the extension: ' + ext);
|
||
|
|
||
|
let resolver = defaultLookups[ext];
|
||
|
|
||
|
if (!resolver) {
|
||
|
debug('using generic resolver');
|
||
|
if (!resolveDependencyPath) {
|
||
|
resolveDependencyPath = require('resolve-dependency-path');
|
||
|
}
|
||
|
|
||
|
resolver = resolveDependencyPath;
|
||
|
}
|
||
|
|
||
|
debug(`found a resolver for ${ext}`);
|
||
|
|
||
|
options.dependency = partial;
|
||
|
const result = resolver(options);
|
||
|
|
||
|
debug(`resolved path for ${partial}: ${result}`);
|
||
|
return result;
|
||
|
};
|
||
|
|
||
|
module.exports.supportedFileExtensions = Object.keys(defaultLookups);
|
||
|
|
||
|
/**
|
||
|
* Register a custom lookup resolver for a file extension
|
||
|
*
|
||
|
* @param {String} extension - The file extension that should use the resolver
|
||
|
* @param {Function} lookupStrategy - A resolver of partial paths
|
||
|
*/
|
||
|
module.exports.register = function(extension, lookupStrategy) {
|
||
|
defaultLookups[extension] = lookupStrategy;
|
||
|
|
||
|
if (!this.supportedFileExtensions.includes(extension)) {
|
||
|
this.supportedFileExtensions.push(extension);
|
||
|
}
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
* Exposed for testing
|
||
|
*
|
||
|
* @param {Object} options
|
||
|
* @param {String} options.config
|
||
|
* @param {String} options.webpackConfig
|
||
|
* @param {String} options.filename
|
||
|
* @param {Object} options.ast
|
||
|
* @return {String}
|
||
|
*/
|
||
|
module.exports._getJSType = function(options = {}) {
|
||
|
if (!getModuleType) {
|
||
|
getModuleType = require('module-definition');
|
||
|
}
|
||
|
|
||
|
if (options.config) {
|
||
|
return 'amd';
|
||
|
}
|
||
|
|
||
|
if (options.webpackConfig) {
|
||
|
return 'webpack';
|
||
|
}
|
||
|
|
||
|
if (options.ast) {
|
||
|
debug('reusing the given ast');
|
||
|
return getModuleType.fromSource(options.ast);
|
||
|
}
|
||
|
|
||
|
debug('using the filename to find the module type');
|
||
|
return getModuleType.sync(options.filename);
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
* @private
|
||
|
* @param {Object} options
|
||
|
* @param {String} options.dependency
|
||
|
* @param {String} options.filename
|
||
|
* @param {String} options.directory
|
||
|
* @param {String} [options.config]
|
||
|
* @param {String} [options.webpackConfig]
|
||
|
* @param {String} [options.configPath]
|
||
|
* @param {Object} [options.nodeModulesConfig]
|
||
|
* @param {Object} [options.ast]
|
||
|
* @return {String}
|
||
|
*/
|
||
|
function jsLookup({dependency, filename, directory, config, webpackConfig, configPath, nodeModulesConfig, ast}) {
|
||
|
const type = module.exports._getJSType({
|
||
|
config: config,
|
||
|
webpackConfig: webpackConfig,
|
||
|
filename: filename,
|
||
|
ast: ast
|
||
|
});
|
||
|
|
||
|
switch (type) {
|
||
|
case 'amd':
|
||
|
debug('using amd resolver');
|
||
|
if (!amdLookup) {
|
||
|
amdLookup = require('module-lookup-amd');
|
||
|
}
|
||
|
|
||
|
return amdLookup({
|
||
|
config: config,
|
||
|
// Optional in case a pre-parsed config is being passed in
|
||
|
configPath: configPath,
|
||
|
partial: dependency,
|
||
|
directory: directory,
|
||
|
filename: filename
|
||
|
});
|
||
|
|
||
|
case 'commonjs':
|
||
|
debug('using commonjs resolver');
|
||
|
return commonJSLookup({dependency, filename, directory, nodeModulesConfig});
|
||
|
|
||
|
case 'webpack':
|
||
|
debug('using webpack resolver for es6');
|
||
|
return resolveWebpackPath({dependency, filename, directory, webpackConfig});
|
||
|
|
||
|
case 'es6':
|
||
|
default:
|
||
|
debug('using commonjs resolver for es6');
|
||
|
return commonJSLookup({dependency, filename, directory, nodeModulesConfig});
|
||
|
}
|
||
|
}
|
||
|
|
||
|
function tsLookup({dependency, filename, tsConfig, noTypeDefinitions}) {
|
||
|
debug('performing a typescript lookup');
|
||
|
|
||
|
let compilerOptions = {};
|
||
|
|
||
|
if (!ts) {
|
||
|
ts = require('typescript');
|
||
|
}
|
||
|
|
||
|
debug('given typescript config: ', tsConfig);
|
||
|
|
||
|
if (!tsConfig) {
|
||
|
debug('no tsconfig given, defaulting');
|
||
|
|
||
|
} else if (typeof tsConfig === 'string') {
|
||
|
debug('string tsconfig given, parsing');
|
||
|
|
||
|
try {
|
||
|
const tsParsedConfig = ts.readJsonConfigFile(tsConfig, ts.sys.readFile);
|
||
|
compilerOptions = ts.parseJsonSourceFileConfigFileContent(tsParsedConfig, ts.sys, path.dirname(tsConfig)).options;
|
||
|
debug('successfully parsed tsconfig');
|
||
|
} catch (e) {
|
||
|
debug('could not parse tsconfig');
|
||
|
throw new Error('could not read tsconfig');
|
||
|
}
|
||
|
} else {
|
||
|
compilerOptions = ts.convertCompilerOptionsFromJson(tsConfig.compilerOptions).options;
|
||
|
}
|
||
|
|
||
|
debug('processed typescript config: ', tsConfig);
|
||
|
debug('processed typescript config type: ', typeof tsConfig);
|
||
|
|
||
|
// Preserve for backcompat. Consider removing this as a breaking change.
|
||
|
if (!compilerOptions.module) {
|
||
|
compilerOptions.module = ts.ModuleKind.AMD;
|
||
|
}
|
||
|
|
||
|
const host = ts.createCompilerHost({});
|
||
|
debug('with options: ', compilerOptions);
|
||
|
|
||
|
const namedModule = ts.resolveModuleName(dependency, filename, compilerOptions, host);
|
||
|
let result = '';
|
||
|
|
||
|
if (namedModule.resolvedModule) {
|
||
|
result = namedModule.resolvedModule.resolvedFileName;
|
||
|
if (namedModule.resolvedModule.extension === '.d.ts' && noTypeDefinitions) {
|
||
|
result = ts.resolveJSModule(dependency, path.dirname(filename), host) || result;
|
||
|
}
|
||
|
} else {
|
||
|
const suffix = '.d.ts';
|
||
|
const lookUpLocations = namedModule.failedLookupLocations
|
||
|
.filter((string) => string.endsWith(suffix))
|
||
|
.map((string) => string.substr(0, string.length - suffix.length));
|
||
|
|
||
|
result = lookUpLocations.find(ts.sys.fileExists) || '';
|
||
|
}
|
||
|
|
||
|
debug('result: ' + result);
|
||
|
return result ? path.resolve(result) : '';
|
||
|
}
|
||
|
|
||
|
function commonJSLookup({dependency, filename, directory, nodeModulesConfig}) {
|
||
|
if (!resolve) {
|
||
|
resolve = require('resolve');
|
||
|
}
|
||
|
|
||
|
if (!dependency) {
|
||
|
debug('blank dependency given. Returning early.');
|
||
|
return '';
|
||
|
}
|
||
|
|
||
|
// Need to resolve partials within the directory of the module, not filing-cabinet
|
||
|
const moduleLookupDir = path.join(directory, 'node_modules');
|
||
|
|
||
|
debug('adding ' + moduleLookupDir + ' to the require resolution paths');
|
||
|
|
||
|
appModulePath.addPath(moduleLookupDir);
|
||
|
|
||
|
// Make sure the partial is being resolved to the filename's context
|
||
|
// 3rd party modules will not be relative
|
||
|
if (dependency[0] === '.') {
|
||
|
dependency = path.resolve(path.dirname(filename), dependency);
|
||
|
}
|
||
|
|
||
|
let result = '';
|
||
|
|
||
|
// Allows us to configure what is used as the "main" entry point
|
||
|
function packageFilter(packageJson) {
|
||
|
packageJson.main = packageJson[nodeModulesConfig.entry] ? packageJson[nodeModulesConfig.entry] : packageJson.main;
|
||
|
return packageJson;
|
||
|
}
|
||
|
|
||
|
try {
|
||
|
result = resolve.sync(dependency, {
|
||
|
extensions: ['.js', '.jsx'],
|
||
|
basedir: directory,
|
||
|
packageFilter: nodeModulesConfig && nodeModulesConfig.entry ? packageFilter : undefined,
|
||
|
// Add fileDir to resolve index.js files in that dir
|
||
|
moduleDirectory: ['node_modules', directory]
|
||
|
});
|
||
|
debug('resolved path: ' + result);
|
||
|
} catch (e) {
|
||
|
debug('could not resolve ' + dependency);
|
||
|
}
|
||
|
|
||
|
return result;
|
||
|
}
|
||
|
|
||
|
function resolveWebpackPath({dependency, filename, directory, webpackConfig}) {
|
||
|
if (!webpackResolve) {
|
||
|
webpackResolve = require('enhanced-resolve');
|
||
|
}
|
||
|
webpackConfig = path.resolve(webpackConfig);
|
||
|
let loadedConfig;
|
||
|
|
||
|
try {
|
||
|
loadedConfig = require(webpackConfig);
|
||
|
|
||
|
if (typeof loadedConfig === 'function') {
|
||
|
loadedConfig = loadedConfig();
|
||
|
}
|
||
|
} catch (e) {
|
||
|
debug('error loading the webpack config at ' + webpackConfig);
|
||
|
debug(e.message);
|
||
|
debug(e.stack);
|
||
|
return '';
|
||
|
}
|
||
|
|
||
|
const resolveConfig = Object.assign({}, loadedConfig.resolve);
|
||
|
|
||
|
if (!resolveConfig.modules && (resolveConfig.root || resolveConfig.modulesDirectories)) {
|
||
|
resolveConfig.modules = [];
|
||
|
|
||
|
if (resolveConfig.root) {
|
||
|
resolveConfig.modules = resolveConfig.modules.concat(resolveConfig.root);
|
||
|
}
|
||
|
|
||
|
if (resolveConfig.modulesDirectories) {
|
||
|
resolveConfig.modules = resolveConfig.modules.concat(resolveConfig.modulesDirectories);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
try {
|
||
|
const resolver = webpackResolve.create.sync(resolveConfig);
|
||
|
|
||
|
// We don't care about what the loader resolves the dependency to
|
||
|
// we only wnat the path of the resolved file
|
||
|
dependency = stripLoader(dependency);
|
||
|
|
||
|
const lookupPath = isRelative(dependency) ? path.dirname(filename) : directory;
|
||
|
|
||
|
return resolver(lookupPath, dependency);
|
||
|
} catch (e) {
|
||
|
debug('error when resolving ' + dependency);
|
||
|
debug(e.message);
|
||
|
debug(e.stack);
|
||
|
return '';
|
||
|
}
|
||
|
}
|
||
|
|
||
|
function stripLoader(dependency) {
|
||
|
const exclamationLocation = dependency.indexOf('!');
|
||
|
|
||
|
if (exclamationLocation === -1) { return dependency; }
|
||
|
|
||
|
return dependency.slice(exclamationLocation + 1);
|
||
|
}
|