You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

591 lines
21 KiB

"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.parseMerge = exports.parseTopScope = exports.removeProperty = exports.addProperty = exports.getRequire = exports.findObjWithOneOfKeys = exports.createIdentifierOrLiteral = exports.createLiteral = exports.isType = exports.findPluginsArrayAndRemoveIfEmpty = exports.findVariableToPlugin = exports.createOrUpdatePluginByName = exports.findAndRemovePluginByName = exports.addOrUpdateConfigObject = exports.findRootNodesByName = exports.findPluginsByName = exports.createProperty = exports.safeTraverseAndGetType = exports.safeTraverse = void 0;
const validate_identifier_1 = require("./validate-identifier");
function isImportPresent(j, ast, path) {
if (typeof path !== 'string') {
throw new Error(`path parameter should be string, received ${typeof path}`);
}
let importExists = false;
ast.find(j.CallExpression).forEach((callExp) => {
if (callExp.value.callee.name === 'require' && callExp.value.arguments[0].value === path) {
importExists = true;
}
});
return importExists;
}
/**
*
* Traverse safely over a path object for array for paths
* @param {Object} obj - Object on which we traverse
* @param {Array} paths - Array of strings containing the traversal path
* @returns {Node} Value at given traversal path
*/
function safeTraverse(obj, paths) {
// TODO: to revisit the type of this function, it's not clear what should return.
// Sometimes its return type is used as string
let val = obj;
let idx = 0;
while (idx < paths.length) {
if (!val) {
return null;
}
val = val[paths[idx]];
idx++;
}
return val;
}
exports.safeTraverse = safeTraverse;
/**
*
* Traverse safely and return `type` for path object with value.value property
* @param {Node} path - AST node
* @returns {String|Boolean} type at given path.
*/
function safeTraverseAndGetType(path) {
const pathValue = safeTraverse(path, ['value', 'value']);
return pathValue ? pathValue.type : false;
}
exports.safeTraverseAndGetType = safeTraverseAndGetType;
/**
* Convert nested MemberExpressions to strings like webpack.optimize.DedupePlugin
* @param {Node} path - AST node
* @returns {String} member expression string.
*/
function memberExpressionToPathString(path) {
if (path && path.object) {
return [memberExpressionToPathString(path.object), path.property.name].join('.');
}
return path.name;
}
// Convert Array<string> like ['webpack', 'optimize', 'DedupePlugin'] to nested MemberExpressions
function pathsToMemberExpression(j, paths) {
if (!paths.length) {
return null;
}
else if (paths.length === 1) {
return j.identifier(paths[0]);
}
else {
const first = paths.slice(0, 1);
const rest = paths.slice(1);
return j.memberExpression(pathsToMemberExpression(j, rest), pathsToMemberExpression(j, first));
}
}
/**
*
* Find paths that match `new name.space.PluginName()` for a
* given array of plugin names
*
* @param {any} j — jscodeshift API
* @param {Node} node - Node to start search from
* @param {String[]} pluginNamesArray - Array of plugin names like `webpack.LoaderOptionsPlugin`
* @returns {Node} Node that has the pluginName
*/
function findPluginsByName(j, node, pluginNamesArray) {
return node.find(j.NewExpression).filter((path) => {
return pluginNamesArray.some((plugin) => memberExpressionToPathString(path.get('callee').value) === plugin);
});
}
exports.findPluginsByName = findPluginsByName;
/**
* It lookouts for the plugins property and, if the array is empty, it removes it from the AST
* @param {any} j - jscodeshift API
* @param {Node} rootNode - node to start search from
* @returns {Node} rootNode modified AST.
*/
function findPluginsArrayAndRemoveIfEmpty(j, rootNode) {
return rootNode.find(j.Identifier, { name: 'plugins' }).forEach((node) => {
const elements = safeTraverse(node, ['parent', 'value', 'value', 'elements']);
if (!elements.length) {
j(node.parent).remove();
}
});
}
exports.findPluginsArrayAndRemoveIfEmpty = findPluginsArrayAndRemoveIfEmpty;
/**
*
* Finds the path to the `name: []` node
*
* @param {any} j — jscodeshift API
* @param {Node} node - Node to start search from
* @param {String} propName - property to search for
* @returns {Node} found node and
*/
function findRootNodesByName(j, node, propName) {
return node.find(j.Property, { key: { name: propName } });
}
exports.findRootNodesByName = findRootNodesByName;
/**
*
* Creates an appropriate identifier or literal property
*
* @param {any} j — jscodeshift API
* @param {String | Boolean | Number} val
* @returns {Node}
*/
function createIdentifierOrLiteral(j, val) {
// IPath<IIdentifier> | IPath<ILiteral> doesn't work, find another way
let literalVal = val;
// We'll need String to native type conversions
if (!Array.isArray(val)) {
if (typeof val === 'string' || (typeof val === 'object' && val.__paths)) {
// 'true' => true
if (val === 'true') {
literalVal = true;
return j.literal(literalVal);
}
// 'false' => false
if (val === 'false') {
literalVal = false;
return j.literal(literalVal);
}
// '1' => 1
if (!isNaN(Number(val))) {
literalVal = Number(val);
return j.literal(literalVal);
}
if (typeof val === 'object' && val.__paths) {
const regExpVal = val.__paths[0].value.program.body[0].expression;
return j.literal(regExpVal.value);
}
else if (typeof literalVal === 'string') {
// Use identifier instead
if (!validate_identifier_1.isKeyword(literalVal) || !validate_identifier_1.isIdentifierStart(literalVal) || !validate_identifier_1.isIdentifierChar(literalVal)) {
return j.identifier(literalVal);
}
}
}
}
return j.literal(literalVal);
}
exports.createIdentifierOrLiteral = createIdentifierOrLiteral;
/**
*
* Creates an appropriate literal property
*
* @param {any} j — jscodeshift API
* @param {String | Boolean | Number} val
* @returns {Node}
*/
function createLiteral(j, val) {
let literalVal = val;
// We'll need String to native type conversions
if (typeof val === 'string') {
// 'true' => true
if (val === 'true') {
literalVal = true;
}
// 'false' => false
else if (val === 'false') {
literalVal = false;
}
// '1' => 1
if (!isNaN(Number(val))) {
literalVal = Number(val);
}
}
return j.literal(literalVal);
}
exports.createLiteral = createLiteral;
/**
*
* Creates an Object's property with a given key and value
*
* @param {any} j — jscodeshift API
* @param {String | Number} key - Property key
* @param {String | Number | Boolean} value - Property value
* @returns {Node}
*/
function createProperty(j, key, value) {
return j.property('init', createIdentifierOrLiteral(j, key), createLiteral(j, value));
}
exports.createProperty = createProperty;
/**
*
* Adds or updates the value of a key within a root
* webpack configuration property that's of type Object.
*
* @param {any} j — jscodeshift API
* @param {Node} rootNode - node of root webpack configuration
* @param {String} configProperty - key of an Object webpack configuration property
* @param {String} key - key within the configuration object to update
* @param {Object} value - the value to set for the key
* @returns {Void}
*/
function addOrUpdateConfigObject(j, rootNode, configProperty, key, value) {
const propertyExists = rootNode.properties.filter((node) => node.key.name === configProperty).length;
if (propertyExists) {
rootNode.properties
.filter((path) => path.key.name === configProperty)
.forEach((path) => {
const newProperties = path.value.properties.filter((p) => p.key.name !== key);
newProperties.push(j.objectProperty(j.identifier(key), value));
path.value.properties = newProperties;
});
}
else {
rootNode.properties.push(j.objectProperty(j.identifier(configProperty), j.objectExpression([j.objectProperty(j.identifier(key), value)])));
}
}
exports.addOrUpdateConfigObject = addOrUpdateConfigObject;
/**
*
* Finds and removes a node for a given plugin name. If the plugin
* is the last in the plugins array, the array is also removed.
*
* @param {any} j — jscodeshift API
* @param {Node} node - node to start search from
* @param {String} pluginName - name of the plugin to remove
* @returns {Node | Void} - path to the root webpack configuration object if plugin is found
*/
function findAndRemovePluginByName(j, node, pluginName) {
let rootPath;
findPluginsByName(j, node, [pluginName])
.filter((path) => !!safeTraverse(path, ['parent', 'value']))
.forEach((path) => {
rootPath = safeTraverse(path, ['parent', 'parent', 'parent', 'value']);
const arrayPath = path.parent.value;
if (arrayPath.elements && arrayPath.elements.length === 1) {
j(path.parent.parent).remove();
}
else {
j(path).remove();
}
});
return rootPath;
}
exports.findAndRemovePluginByName = findAndRemovePluginByName;
/**
*
* Finds or creates a node for a given plugin name string with options object
* If plugin declaration already exist, options are merged.
*
* @param {any} j — jscodeshift API
* @param {Node} rootNodePath - `plugins: []` NodePath where plugin should be added.
* See https://github.com/facebook/jscodeshift/wiki/jscodeshift-Documentation#nodepaths
* @param {String} pluginName - ex. `webpack.LoaderOptionsPlugin`
* @param {Object} options - plugin options
* @returns {Void}
*/
function createOrUpdatePluginByName(j, rootNodePath, pluginName, options) {
const pluginInstancePath = findPluginsByName(j, j(rootNodePath), [pluginName]);
let optionsProps;
if (options) {
optionsProps = Object.keys(options).map((key) => {
return createProperty(j, key, options[key]);
});
}
// If plugin declaration already exist
if (pluginInstancePath.size()) {
pluginInstancePath.forEach((path) => {
// There are options we want to pass as argument
if (optionsProps) {
const args = path.value.arguments;
if (args.length) {
// Plugin is called with object as arguments
// we will merge those objects
const currentProps = j(path).find(j.ObjectExpression).get('properties');
optionsProps.forEach((opt) => {
// Search for same keys in the existing object
const existingProps = j(currentProps)
.find(j.Identifier)
.filter((p) => opt.key.value === p.value.name);
if (existingProps.size()) {
// Replacing values for the same key
existingProps.forEach((p) => {
j(p.parent).replaceWith(opt);
});
}
else {
// Adding new key:values
currentProps.value.push(opt);
}
});
}
else {
// Plugin is called without arguments
args.push(j.objectExpression(optionsProps));
}
}
});
}
else {
let argumentsArray = [];
if (optionsProps) {
argumentsArray = [j.objectExpression(optionsProps)];
}
const loaderPluginInstance = j.newExpression(pathsToMemberExpression(j, pluginName.split('.').reverse()), argumentsArray);
rootNodePath.value.elements.push(loaderPluginInstance);
}
}
exports.createOrUpdatePluginByName = createOrUpdatePluginByName;
/**
*
* Finds the variable to which a third party plugin is assigned to
*
* @param {any} j — jscodeshift API
* @param {Node} rootNode - `plugins: []` Root Node.
* See https://github.com/facebook/jscodeshift/wiki/jscodeshift-Documentation#nodepaths
* @param {String} pluginPackageName - ex. `extract-text-plugin`
* @returns {String} variable name - ex. 'const s = require(s) gives "s"`
*/
function findVariableToPlugin(j, rootNode, pluginPackageName) {
const moduleVarNames = rootNode
.find(j.VariableDeclarator)
.filter(j.filters.VariableDeclarator.requiresModule(pluginPackageName))
.nodes();
if (moduleVarNames.length === 0) {
return null;
}
return moduleVarNames.pop().id.name;
}
exports.findVariableToPlugin = findVariableToPlugin;
/**
*
* Returns true if type is given type
* @param {Node} path - AST node
* @param {String} type - node type
* @returns {Boolean}
*/
function isType(path, type) {
return path.type === type;
}
exports.isType = isType;
function findObjWithOneOfKeys(p, keyNames) {
return p.value.properties.reduce((predicate, prop) => {
const name = prop.key.name;
return keyNames.includes(name) || predicate;
}, false);
}
exports.findObjWithOneOfKeys = findObjWithOneOfKeys;
/**
*
* Returns constructed require symbol
* @param {any} j — jscodeshift API
* @param {String} constName - Name of require
* @param {String} packagePath - path of required package
* @returns {Node} - the created ast
*/
function getRequire(j, constName, packagePath) {
return j.variableDeclaration('const', [
j.variableDeclarator(j.identifier(constName), j.callExpression(j.identifier('require'), [j.literal(packagePath)])),
]);
}
exports.getRequire = getRequire;
/**
*
* Recursively adds an object/property to a node
* @param {any} j — jscodeshift API
* @param {Node} p - AST node
* @param {String} key - key of a key/val object
* @param {Any} value - Any type of object
* @param {String} action - Action to be done on the given ast
* @returns {Node} - the created ast
*/
function addProperty(j, p, key, value, action) {
if (!p) {
return;
}
let valForNode;
if (Array.isArray(value)) {
let arrExp = j.arrayExpression([]);
if (safeTraverseAndGetType(p) === 'ArrayExpression') {
arrExp = p.value.value;
}
value.forEach((val) => {
addProperty(j, arrExp, null, val);
});
valForNode = arrExp;
}
else if (typeof value === 'object' && !(value.__paths || value instanceof RegExp)) {
let objectExp = j.objectExpression([]);
if (safeTraverseAndGetType(p) === 'ObjectExpression') {
objectExp = p.value.value;
}
// object -> loop through it
Object.keys(value).forEach((prop) => {
addProperty(j, objectExp, prop, value[prop]);
});
valForNode = objectExp;
}
else {
valForNode = createIdentifierOrLiteral(j, value);
}
let pushVal;
if (key) {
pushVal = j.property('init', j.identifier(key), valForNode);
}
else {
pushVal = valForNode;
}
// we only return the generated pushVal which will be replace the node path
if (action === 'add') {
return pushVal;
}
if (p.properties) {
p.properties.push(pushVal);
return p;
}
if (p.value && p.value.properties) {
p.value.properties.push(pushVal);
return p;
}
if (p.elements) {
p.elements.push(pushVal);
return p;
}
return;
}
exports.addProperty = addProperty;
/**
*
* Removes an object/property from the config
* @param {any} j — jscodeshift API
* @param {Node} ast - AST node
* @param {String} key - key of a key/val object
* @param {Any} value - Any type of object
* @returns {Node} - the created ast
*/
function removeProperty(j, ast, key, value) {
if (typeof value === 'object' && !Array.isArray(value)) {
// override for module.rules / loaders
if (key === 'module' && value.rules) {
return ast
.find(j.Property, {
value: {
type: 'Literal',
value: value.rules[0].loader,
},
})
.forEach((p) => {
j(p.parent).remove();
});
}
}
// value => array
if (Array.isArray(value)) {
return ast
.find(j.Literal, {
value: value[0],
})
.forEach((p) => {
const configKey = safeTraverse(p, ['parent', 'parent', 'node', 'key', 'name']);
if (configKey === key) {
j(p).remove();
}
});
}
// value => literal string / boolean / nested object
let objKeyToRemove = null;
if (value === null) {
objKeyToRemove = key;
}
else if (typeof value === 'object') {
for (const innerKey in value) {
if (value[innerKey] === null) {
objKeyToRemove = innerKey;
}
}
}
return ast
.find(j.Property, {
key: {
name: objKeyToRemove,
type: 'Identifier',
},
})
.forEach((p) => {
j(p).remove();
});
}
exports.removeProperty = removeProperty;
/**
*
* Get an property named topScope from yeoman and inject it to the top scope of
* the config, outside module.exports
*
* @param j — jscodeshift API
* @param ast - jscodeshift API
* @param {any} value - transformation object to scaffold
* @param {String} action - action that indicates what to be done to the AST
* @returns ast - jscodeshift API
*/
// eslint-disable-next-line @typescript-eslint/no-unused-vars
function parseTopScope(j, ast, value, action) {
function createTopScopeProperty(p) {
value.forEach((n) => {
if (!p.value.body[0].declarations || n.indexOf(p.value.body[0].declarations[0].id.name) <= 0) {
p.value.body.splice(-1, 0, n);
}
});
return false; // TODO: debug later
}
if (value) {
return ast.find(j.Program).filter((p) => createTopScopeProperty(p));
}
else {
return ast;
}
}
exports.parseTopScope = parseTopScope;
/**
*
* Transform for merge. Finds the merge property from yeoman and creates a way
* for users to allow webpack-merge in their scaffold
*
* @param j — jscodeshift API
* @param ast - jscodeshift API
* @param {any} value - transformation object to scaffold
* @param {String} action - action that indicates what to be done to the AST
* @returns ast - jscodeshift API
*/
// eslint-disable-next-line @typescript-eslint/no-unused-vars
function parseMerge(j, ast, value, action) {
function createMergeProperty(p, configIdentifier) {
// FIXME Use j.callExp()
const exportsDecl = p.value.body.map((n) => {
if (n.expression) {
return n.expression.right;
}
});
const bodyLength = exportsDecl.length;
const newVal = {};
newVal.type = 'ExpressionStatement';
newVal.expression = {
left: {
computed: false,
object: j.identifier('module'),
property: j.identifier('exports'),
type: 'MemberExpression',
},
operator: '=',
right: j.callExpression(j.identifier('merge'), [j.identifier(configIdentifier), exportsDecl.pop()]),
type: 'AssignmentExpression',
};
p.value.body[bodyLength - 1] = newVal;
return false; // TODO: debug later
}
function addMergeImports(configIdentifier, configPath) {
if (typeof configIdentifier !== 'string' || typeof configPath !== 'string') {
throw new Error(`Both parameters should be strings. received ${typeof configIdentifier}, ${typeof configPath}`);
}
ast.find(j.Program).forEach((p) => {
if (!isImportPresent(j, ast, 'webpack-merge')) {
p.value.body.splice(-1, 0, "const merge = require('webpack-merge')");
}
if (!isImportPresent(j, ast, configPath)) {
p.value.body.splice(-1, 0, `const ${configIdentifier} = require('${configPath}')`);
}
});
}
if (value) {
const [configIdentifier, configPath] = value;
addMergeImports(configIdentifier, configPath);
return ast.find(j.Program).filter((p) => createMergeProperty(p, configIdentifier));
}
else {
return ast;
}
}
exports.parseMerge = parseMerge;