123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448 |
- const validateIdentifier = require("./validate-identifier");
- function safeTraverse(obj, paths) {
- let val = obj;
- let idx = 0;
- while (idx < paths.length) {
- if (!val) {
- return null;
- }
- val = val[paths[idx]];
- idx++;
- }
- return val;
- }
- // Convert nested MemberExpressions to strings like webpack.optimize.DedupePlugin
- 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
- );
- });
- }
- /**
- * 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();
- }
- });
- }
- /**
- *
- * 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 } });
- }
- /**
- *
- * 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)
- );
- }
- /**
- *
- * 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
- if (val === "false") literalVal = false;
- // '1' => 1
- if (!isNaN(Number(val))) literalVal = Number(val);
- }
- return j.literal(literalVal);
- }
- /**
- *
- * 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 (typeof val === "string" || 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 (val.__paths) {
- let regExpVal = val.__paths[0].value.program.body[0].expression;
- return j.literal(regExpVal.value);
- } else {
- // Use identifier instead
- if (
- !validateIdentifier.isKeyword(literalVal) ||
- !validateIdentifier.isIdentifierStart(literalVal) ||
- !validateIdentifier.isIdentifierChar(literalVal)
- )
- return j.identifier(literalVal);
- }
- }
- return j.literal(literalVal);
- }
- /**
- *
- * 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(
- path => path.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)])
- )
- );
- }
- }
- /**
- *
- * 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;
- }
- /**
- *
- * 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
- let 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(path => opt.key.value === path.value.name);
- if (existingProps.size()) {
- // Replacing values for the same key
- existingProps.forEach(path => {
- j(path.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);
- }
- }
- /**
- *
- * 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;
- }
- /**
- *
- * Returns true if type is given type
- * @param {Node} path - pathNode
- * @param {String} type - node type
- * @returns {Boolean}
- */
- function isType(path, type) {
- return path.type === type;
- }
- function findObjWithOneOfKeys(p, keyNames) {
- return p.value.properties.reduce((predicate, prop) => {
- const name = prop.key.name;
- return keyNames.indexOf(name) > -1 || predicate;
- }, false);
- }
- /**
- *
- * 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)])
- )
- ]);
- }
- /**
- *
- * 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
- * @returns {Node} - the created ast
- */
- function addProperty(j, p, key, value) {
- let valForNode;
- if (!p) {
- return;
- }
- if (Array.isArray(value)) {
- const arr = j.arrayExpression([]);
- value.filter(val => val).forEach(val => {
- addProperty(j, arr, null, val);
- });
- valForNode = arr;
- } else if (
- typeof value === "object" &&
- !(value.__paths || value instanceof RegExp)
- ) {
- // object -> loop through it
- let objectExp = j.objectExpression([]);
- 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;
- }
- 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;
- }
- module.exports = {
- safeTraverse,
- createProperty,
- findPluginsByName,
- findRootNodesByName,
- addOrUpdateConfigObject,
- findAndRemovePluginByName,
- createOrUpdatePluginByName,
- findVariableToPlugin,
- findPluginsArrayAndRemoveIfEmpty,
- isType,
- createLiteral,
- createIdentifierOrLiteral,
- findObjWithOneOfKeys,
- getRequire,
- addProperty
- };
|