diff --git a/readme.md b/readme.md index e69de29..31cac7f 100644 --- a/readme.md +++ b/readme.md @@ -0,0 +1,85 @@ +ESLint-Plugin-Alternate-Import +=============================== + +# Installation + +Install [ESLint](https://www.github.com/eslint/eslint) either locally or globally. (Note that locally, per project, is strongly preferred) + +```sh +$ npm install eslint --save-dev +``` + +Install Plugin + +```sh +$ npm install eslint-plugin-alternate-import --save-dev +``` + +# Configuration + +Following is a sample configuration for how to restrict a package and suggest alternatives. It support both ES6 `import` and ES5 `require()` syntax. + +`.eslintrc` + +```json5 +{ + "settings": { + "alternate-import": { + "alternatePackagesMap": [ + { + "original": "restricted-package-name", + "alternate": "alternate-package-name" // use alternate instead of original + }, + { + "original": "restricted-package-name", + "alternate": "/old-package-imports.js" // import from file instead of original package + }, + { + "original": "restricted-package-name" // Restrict package but do not suggest any alternative + } + ], + "customFileImportRootPrefix": "@" // Prepend static path to alternate custom file(s) + }, + } +} +``` + + +## Why this plugin? + +As an owner of the project, you need to be sure that your project does not include any of the known possible bad or unnecessary npm package(s). With this, you can restrict the use of those kinds of npm packages in your project and can suggest a better alternative. + +Eg: Instead of using moment.js for some basic time manipulation you can use some lightweight date library or you can use your custom utility. But, now you also want to restrict everyone contributing to your project to follow this particular rule. Here you can use this plugin and it will help you in the following cases: + +1. Restrict use of Deprecated package(s). +2. Restrict the use of package(s) with known Security Vulnerabilities. +3. Restrict the use of package(s) for which we know some better alternatives. +4. Restrict use of package(s) which may have compatibility issues with current dependencies and environment. + +`.eslintrc` + +```json5 +{ + "settings": { + "alternate-import": { + "alternatePackagesMap": [ + { + "original": "package-which-is-deprecated-and-not-secure-and-compatible", //restrict + "alternate": "much-better-package" // better suggestion + }, + { + "original": "package-with-es5-require", // automatic support for both ES6 Import and ES5 require() syntax + "alternate": "much-better-package" // better suggestion + }, + { + "original": "package-with-lots-of-unwanted-code", + "alternate": "/old-package-imports.js" // use import from file instead of original package - less code + } + + ] + }, + } +} +``` + +## License diff --git a/rules/restricted-direct-import.js b/rules/restricted-direct-import.js index 14e12d1..34c7874 100644 --- a/rules/restricted-direct-import.js +++ b/rules/restricted-direct-import.js @@ -24,7 +24,7 @@ function reportErrorAndFix( fix: function(fixer) { return fixer.replaceText( node, - `import { ${importedVariableNames.join(', ')} } from '${ + `import ${importedVariableNames} from '${ !isAlternateCustomPackage ? `${matchedRestrictedPackage.alternate}` : `${customFileImportRootPrefix}${matchedRestrictedPackage.alternate.replace( @@ -79,12 +79,21 @@ function checkCustomFileImport( return traverse(alternatePackageAST, { ExportNamedDeclaration: function(path) { - const exportedVariableNames = path.node.specifiers.map(({ exported: { name } }) => name); - - const missingExportInAlternatePackage = importedVariableNames.filter( + const exportedVariableNames = path.node.specifiers.map(({ exported: { name } }) => name); + const missingExportInAlternatePackage = (typeof importedVariableNames == 'string')? + ([importedVariableNames]) : (importedVariableNames.map((importedVariableName) => { + if(importedVariableName.indexOf('as') != -1) { + // remove local name and use orignal export name + // to verify its exported from the module by removing its + // alias name + let arr = importedVariableName.split('as') + return arr[0] && arr[0].trim(); + } else { + return importedVariableName + } + }).filter( importedVariableName => exportedVariableNames.indexOf(importedVariableName) === -1 - ); - + )); return reportErrorAndFix( context, node, @@ -114,6 +123,44 @@ module.exports = { ]; return { + VariableDeclaration(node) { + node.declarations.map(obj => { + if (obj.init.type === "CallExpression") { + // we just need package name and we know it will be always first + // argument that's how require works. + // const package = require('package-name') + const packageName = obj.init.arguments[0].value + + const matchedRestrictedPackage = alternatePackagesMap.find( + obj => obj.original === packageName + ); + if (!matchedRestrictedPackage) return; + + const isAlternateCustomPackage = !!( + matchedRestrictedPackage.alternate && matchedRestrictedPackage.alternate.match(/.js$/) + ); + + // No Alternate Import Provided + if (!matchedRestrictedPackage.alternate) { + return context.report({ + node, + message: `Direct import restricted for "${node.source.value}" package.` + }); + } + + // Alternate Node Package Import + if (!isAlternateCustomPackage) { + } else { + return context.report({ + node, + message: `Require restricted for "${packageName}" package, Please use ES6 "import" syntax and use "import from '${ + matchedRestrictedPackage.alternate + }'"` + }); + } + } + }); + }, ImportDeclaration(node) { const matchedRestrictedPackage = alternatePackagesMap.find( obj => obj.original === node.source.value @@ -121,12 +168,28 @@ module.exports = { if (!matchedRestrictedPackage) return; - const importedVariableNames = node.specifiers.map(({ imported: { name } }) => name); - const isAlternateCustomPackage = !!( matchedRestrictedPackage.alternate && matchedRestrictedPackage.alternate.match(/.js$/) ); + let importedVariableNames = []; + let checkItem = node.specifiers[0]; + if(checkItem.type == 'ImportSpecifier') { + importedVariableNames = node.specifiers.reduce((specifierList, item) => { + if(item.imported.name != item.local.name) { + specifierList.push(`${item.imported.name} as ${item.local.name}`) + } else { + specifierList.push(item.imported.name) + } + return specifierList; + }, []); + importedVariableNames = `{ ${importedVariableNames.join(', ')} }` + } else if(checkItem.type == 'ImportNamespaceSpecifier') { + importedVariableNames = `* as ${node.specifiers.shift().local.name}` + } else { + // for default import specifier + importedVariableNames = node.specifiers.shift().local.name + } // No Alternate Import Provided if (!matchedRestrictedPackage.alternate) { return context.report({ @@ -134,16 +197,25 @@ module.exports = { message: `Direct import restricted for "${node.source.value}" package.` }); } - // Alternate Node Package Import if (!isAlternateCustomPackage) { - return reportErrorAndFix( - context, + return context.report({ node, - matchedRestrictedPackage, - importedVariableNames, - isAlternateCustomPackage - ); + message: `Direct import restricted for "${node.source.value}" package.`, + fix: function(fixer) { + return fixer.replaceText( + node, + `import ${importedVariableNames} from '${ + !isAlternateCustomPackage + ? `${matchedRestrictedPackage.alternate}` + : `${customFileImportRootPrefix}${matchedRestrictedPackage.alternate.replace( + /.js$/, + '' + )}` + }';` + ); + } + }); } // Alternate Custom File Import