babel-plugin-alias-config
Version:
Babel 7 plugin for aliases
345 lines (272 loc) • 13.2 kB
JavaScript
;
function _slicedToArray(arr, i) { return _arrayWithHoles(arr) || _iterableToArrayLimit(arr, i) || _unsupportedIterableToArray(arr, i) || _nonIterableRest(); }
function _nonIterableRest() { throw new TypeError("Invalid attempt to destructure non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); }
function _unsupportedIterableToArray(o, minLen) { if (!o) return; if (typeof o === "string") return _arrayLikeToArray(o, minLen); var n = Object.prototype.toString.call(o).slice(8, -1); if (n === "Object" && o.constructor) n = o.constructor.name; if (n === "Map" || n === "Set") return Array.from(o); if (n === "Arguments" || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)) return _arrayLikeToArray(o, minLen); }
function _arrayLikeToArray(arr, len) { if (len == null || len > arr.length) len = arr.length; for (var i = 0, arr2 = new Array(len); i < len; i++) arr2[i] = arr[i]; return arr2; }
function _iterableToArrayLimit(arr, i) { var _i = arr == null ? null : typeof Symbol !== "undefined" && arr[Symbol.iterator] || arr["@@iterator"]; if (_i == null) return; var _arr = []; var _n = true; var _d = false; var _s, _e; try { for (_i = _i.call(arr); !(_n = (_s = _i.next()).done); _n = true) { _arr.push(_s.value); if (i && _arr.length === i) break; } } catch (err) { _d = true; _e = err; } finally { try { if (!_n && _i["return"] != null) _i["return"](); } finally { if (_d) throw _e; } } return _arr; }
function _arrayWithHoles(arr) { if (Array.isArray(arr)) return arr; }
const _require = require('path'),
join = _require.join,
resolve = _require.resolve,
relative = _require.relative,
isAbsolute = _require.isAbsolute,
dirname = _require.dirname,
basename = _require.basename;
const _require2 = require('@babel/helper-plugin-utils'),
declare = _require2.declare;
const _require3 = require('@babel/core'),
t = _require3.types;
const fs = require('fs');
const template = require('lodash.template');
const some = require('lodash.some');
const findUp = require('find-up');
const escapeStringRegexp = require('escape-string-regexp');
const requireJSON5 = require('require-json5');
const REQUIRE = 'require';
const DEFAULT_CONFIG_NAMES = ['alias.config.js', 'app.config.js', 'tsconfig.json', 'jsconfig.json', 'webpack.config.js', 'webpack.config.babel.js'];
function fileExists(path) {
try {
return !fs.accessSync(path, fs.F_OK);
} catch (e) {
return false;
}
}
function getConfigPath(filename, configPaths, findConfig) {
let conf = null; // Try all config paths and return for the first found one
some(configPaths, configPath => {
if (!configPath) return false; // Compile config using environment variables
const compiledConfigPath = template(configPath)(process.env);
let resolvedConfigPath;
if (!findConfig) {
// Get webpack config
resolvedConfigPath = resolve(process.cwd(), compiledConfigPath);
} else {
resolvedConfigPath = findUp.sync(compiledConfigPath, {
cwd: dirname(filename),
type: 'file'
});
}
if (resolvedConfigPath && fileExists(resolvedConfigPath)) {
conf = resolvedConfigPath;
}
return conf;
}, false);
return conf;
}
const cached = {};
/**
*
* @param {string} filename
* @param {string} source
* @param {{
* configPath?: string,
* findConfig?: boolean,
* noOutputExtension?: boolean,
* }} options
* @returns
*/
function resolveAlias(filename, source) {
let _ref = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {},
_ref$configPath = _ref.configPath,
configPath = _ref$configPath === void 0 ? '' : _ref$configPath,
_ref$findConfig = _ref.findConfig,
findConfig = _ref$findConfig === void 0 ? false : _ref$findConfig,
_ref$noOutputExtensio = _ref.noOutputExtension,
noOutputExtension = _ref$noOutputExtensio === void 0 ? false : _ref$noOutputExtensio;
const configPaths = configPath ? [configPath].concat(DEFAULT_CONFIG_NAMES) : DEFAULT_CONFIG_NAMES; // Get webpack config
const confPath = getConfigPath(filename, configPaths, findConfig); // If the config comes back as null, we didn't find it, so throw an exception.
if (!confPath) return; // Because of babel-register, babel is actually run on webpack config files using themselves
// as config, leading to odd errors
if (filename === resolve(confPath)) return;
const isTsconfig = /[jt]sconfig.json$/.test(confPath);
let aliasConf;
let extensionsConf;
let cwd;
let aliases;
let cache = cached[confPath];
if (cache) {
if (!cache.conf) return;
if (cache.error) throw cache.error; // eslint-disable-next-line
aliasConf = cache.aliasConf; // eslint-disable-next-line
extensionsConf = cache.extensionsConf; // eslint-disable-next-line
cwd = cache.cwd; // eslint-disable-next-line
aliases = cache.aliases;
} else {
// Require the config
let conf = isTsconfig ? requireJSON5(confPath) : require(confPath); // if the object is empty, we might be in a dependency of the config - bail without warning
if (!Object.keys(conf).length) {
return;
}
cwd = dirname(confPath);
cache = {
conf,
cwd
};
cached[confPath] = cache; // In the case the webpack config is an es6 config, we need to get the default
// eslint-disable-next-line
if (conf && conf.__esModule && conf.default) {
conf = conf.default;
}
if (isTsconfig) {
aliasConf = conf.compilerOptions && conf.compilerOptions.paths;
const baseUrl = conf.compilerOptions && conf.compilerOptions.baseUrl || '.';
if (aliasConf) {
const tsconfigRegx = /(.*[^/*])(\/\*)?$/;
aliasConf = Object.keys(aliasConf).reduce((p, key) => {
const _ref2 = key.match(tsconfigRegx) || [],
_ref3 = _slicedToArray(_ref2, 3),
name = _ref3[1],
nameSuffix = _ref3[2];
if (!name) return p;
let aliasValue = aliasConf[key];
if (Array.isArray(aliasValue)) aliasValue = aliasValue[0] || '';
const _ref4 = aliasValue.match(tsconfigRegx) || [],
_ref5 = _slicedToArray(_ref4, 3),
_ref5$ = _ref5[1],
value = _ref5$ === void 0 ? '' : _ref5$,
_ref5$2 = _ref5[2],
valueSuffix = _ref5$2 === void 0 ? '' : _ref5$2;
p[`${name}${nameSuffix && valueSuffix ? '' : '$'}`] = resolve(cwd, baseUrl, value);
return p;
}, {});
}
cache.extensionsConf = null;
} else if (Array.isArray(conf)) {
// Get the webpack alias config
// the exported webpack config is an array ...
// (i.e., the project is using webpack's multicompile feature) ...
// reduce the configs to a single alias object
aliasConf = conf.reduce((prev, curr) => {
const next = Object.assign({}, prev);
const alias = curr.alias || curr.resolve && curr.resolve.alias;
if (alias) {
Object.assign(next, alias);
}
return next;
}, {}); // reduce the configs to a single extensions array
extensionsConf = conf.reduce((prev, curr) => {
const next = [].concat(prev);
const extensions = curr.extensions || curr.resolve && curr.resolve.extensions || [];
if (extensions.length) {
extensions.forEach(ext => {
if (next.indexOf(ext) === -1) {
next.push(ext);
}
});
}
return next;
}, []);
if (!extensionsConf.length) {
extensionsConf = null;
}
} else {
// the exported webpack config is a single object...
// use it's resolve.alias property
aliasConf = conf.alias || conf.resolve && conf.resolve.alias; // use it's resolve.extensions property, if available
extensionsConf = conf.extensions || conf.resolve && conf.resolve.extensions || [];
if (!extensionsConf) extensionsConf = null;
}
aliases = aliasConf ? Object.keys(aliasConf) : [];
cache.aliases = aliases;
cache.aliasConf = aliasConf;
cache.extensionsConf = extensionsConf;
}
for (let alias of aliases) {
let aliasDestination = aliasConf[alias];
const isFull = /\$$/.test(alias);
if (isFull) alias = alias.substr(0, alias.length - 1);
const regex = new RegExp(`^${escapeStringRegexp(alias)}${isFull ? '$' : '(\/|$)'}`);
if (regex.test(source)) {
// notModuleRegExp from https://github.com/webpack/enhanced-resolve/blob/master/lib/Resolver.js
const notModuleRegExp = /^\.$|^\.[\\\/]|^\.\.$|^\.\.[\/\\]|^\/|^[A-Z]:[\\\/]/i;
const isModule = !notModuleRegExp.test(aliasDestination);
if (isModule) {
return source.replace(alias, aliasDestination);
}
if (!isAbsolute(aliasDestination)) {
aliasDestination = join(cwd, aliasDestination);
}
let relativeFilePath = relative(dirname(filename), aliasDestination); // In case the file path is the root of the alias, need to put a dot to avoid having an absolute path
if (relativeFilePath.length === 0) relativeFilePath = '.';
let requiredFilePath = source.replace(alias, relativeFilePath); // If the file is requiring the current directory which is the alias, add an extra slash
if (requiredFilePath === '.') requiredFilePath = './'; // In the case of a file requiring a child directory of the current directory, we need to add a dot slash
if (['.', '/'].indexOf(requiredFilePath[0]) === -1) {
requiredFilePath = `./${requiredFilePath}`;
} // TODO: should honor enforceExtension and then use extensionConf to make sure extension
// In case the extension option is passed
if (extensionsConf && !noOutputExtension) {
// Get an absolute path to the file
const absoluteRequire = join(aliasDestination, basename(source));
let extension = null;
some(extensionsConf, ext => {
if (!ext) return false; // If the file with this extension exists set it
if (fileExists(absoluteRequire + ext)) {
extension = ext;
}
return extension;
}, false); // Set the extension to the file path, or keep the original one
requiredFilePath += extension || '';
}
return requiredFilePath.replace(/\\/g, '/');
}
}
}
const plugin = declare(api => {
api.assertVersion(7);
return {
name: 'babel-plugin-alias-config',
visitor: {
ImportDeclaration(path, state) {
const filename = state.filename,
_state$opts = state.opts,
_state$opts2 = _state$opts === void 0 ? {} : _state$opts,
_state$opts2$config = _state$opts2.config,
configPath = _state$opts2$config === void 0 ? '' : _state$opts2$config,
_state$opts2$findConf = _state$opts2.findConfig,
findConfig = _state$opts2$findConf === void 0 ? false : _state$opts2$findConf,
_state$opts2$noOutput = _state$opts2.noOutputExtension,
noOutputExtension = _state$opts2$noOutput === void 0 ? false : _state$opts2$noOutput;
const node = path.node;
const filePath = node.source.value;
const newFilename = resolveAlias(filename, filePath, {
configPath,
findConfig,
noOutputExtension
});
if (newFilename) node.source = t.StringLiteral(newFilename);
},
CallExpression(path, _ref6) {
let filename = _ref6.file.opts.filename,
_ref6$opts = _ref6.opts,
_ref6$opts2 = _ref6$opts === void 0 ? {} : _ref6$opts,
_ref6$opts2$config = _ref6$opts2.config,
configPath = _ref6$opts2$config === void 0 ? '' : _ref6$opts2$config,
_ref6$opts2$findConfi = _ref6$opts2.findConfig,
findConfig = _ref6$opts2$findConfi === void 0 ? false : _ref6$opts2$findConfi,
_ref6$opts2$noOutputE = _ref6$opts2.noOutputExtension,
noOutputExtension = _ref6$opts2$noOutputE === void 0 ? false : _ref6$opts2$noOutputE,
_ref6$opts2$dynamicIm = _ref6$opts2.dynamicImport,
dynamicImport = _ref6$opts2$dynamicIm === void 0 ? true : _ref6$opts2$dynamicIm;
const nodeArguments = path.node.arguments; // If not a require statement do nothing
if (t.isIdentifier(path.node.callee, {
name: REQUIRE
}) || dynamicImport && path.node.callee.type === 'Import') {
// Make sure required value is a string
if (nodeArguments.length === 0 || !t.isStringLiteral(nodeArguments[0])) {
return;
}
const _nodeArguments = _slicedToArray(nodeArguments, 1),
filePath = _nodeArguments[0].value;
const newFilename = resolveAlias(filename, filePath, {
configPath,
findConfig,
noOutputExtension
});
if (newFilename) path.node.arguments = [t.StringLiteral(newFilename)];
}
}
}
};
});
module.exports = plugin;
module.exports.resolveAlias = resolveAlias;