├── .eslintrc.json ├── .gitignore ├── .prettierignore ├── .prettierrc.json ├── LICENSE ├── README.md ├── assets ├── default-export-match-filename.png └── default-import-match-filename.png ├── docs └── rules │ ├── default-export-match-filename.md │ └── default-import-match-filename.md ├── lib ├── common │ ├── ModuleCache.js │ ├── getExportedName.js │ ├── hash.js │ ├── isIgnoredFilename.js │ ├── isIndexFile.js │ ├── isStaticRequire.js │ ├── parseFilename.js │ └── resolve.js ├── index.js └── rules │ ├── default-export-match-filename.js │ └── default-import-match-filename.js ├── package.json └── tests ├── files └── default-import-match-filename │ ├── ignored │ └── foo.js │ ├── main.js │ ├── package.json │ └── some-directory │ ├── a.js │ └── index.js ├── lib └── rules │ ├── default-export-match-filename.js │ └── default-import-match-filename.js └── setup.js /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "eslint:recommended", 4 | "plugin:import/recommended", 5 | "plugin:prettier/recommended" 6 | ], 7 | "env": { 8 | "node": true, 9 | "es6": true 10 | }, 11 | "rules": { 12 | "import/no-unresolved": [2, { "commonjs": true }] 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | 3 | # Only apps should have lockfiles 4 | yarn.lock 5 | package-lock.json 6 | npm-shrinkwrap.json -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | *.md -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "es5", 3 | "tabWidth": 4, 4 | "semi": true, 5 | "singleQuote": true 6 | } 7 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Minseok Suh 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # eslint-plugin-consistent-default-export-name 2 | 3 | Adds rules to help use consistent "default export" names throughout the project. 4 | 5 | This plugin is basically a repackaging of two rules, each from two separate plugins: 6 | 7 | 1. __default-export-match-filename__: checks when filename does not match its default export name 8 | ![default-export-match-filename.png](https://raw.githubusercontent.com/minseoksuh/eslint-plugin-consistent-default-export-name/main/assets/default-export-match-filename.png) 9 | 10 | 2. __default-import-match-filename__: checks when default import name does not match its source filename 11 | ![default-import-match-filename.png](https://raw.githubusercontent.com/minseoksuh/eslint-plugin-consistent-default-export-name/main/assets/default-import-match-filename.png) 12 | 13 | If both rules are activated, default names will be consistent overall. 14 | 15 | ## I DID NOT WRITE THE RULES 16 | 17 | - Thanks to @selaux who wrote the rule (filenames/match-exported) and made `eslint-plugin-filenames` 18 | - Thanks to @golopot who wrote the rule and made PR to `eslint-plugin-import` 19 | 20 | ## How To Use 21 | 22 | 1. either extend config which enables both rules 23 | 24 | ```json 25 | { 26 | "extends": ["plugin:consistent-default-export-name/fixed"] 27 | } 28 | ``` 29 | 30 | which, sets below 31 | 32 | ```json 33 | { 34 | "rules": { 35 | "consistent-default-export-name/default-export-match-filename": "error", 36 | "consistent-default-export-name/default-import-match-filename": "error" 37 | } 38 | } 39 | ``` 40 | 41 | 2. or set rules inidividually 42 | 43 | ```json 44 | { 45 | "rules": { 46 | "consistent-default-export-name/default-export-match-filename": "error", 47 | } 48 | } 49 | ``` 50 | 51 | ## Rule Option & Documentation 52 | 53 | - [default-export-match-filename](./docs/rules/default-export-match-filename.md) 54 | - [default-import-match-filename](./docs/rules/default-import-match-filename.md) 55 | 56 | ## Installation 57 | 58 | ```shell 59 | npm install eslint-plugin-consistent-default-export-name --save-dev 60 | ``` 61 | 62 | ```shell 63 | yarn add -D eslint-plugin-consistent-default-export-name 64 | ``` 65 | 66 | ## Supported Rules 67 | 68 | - default-export-match-filename 69 | - default-import-match-filename 70 | 71 | ## Github 72 | 73 | [https://github.com/minseoksuh/eslint-plugin-consistent-default-export-name/blob/main/README.md](https://github.com/minseoksuh/eslint-plugin-consistent-default-export-name/blob/main/README.md) 74 | -------------------------------------------------------------------------------- /assets/default-export-match-filename.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/minseoksuh/eslint-plugin-consistent-default-export-name/be15da14cc9d53a9bf6d3ab58b6d1183b94e2781/assets/default-export-match-filename.png -------------------------------------------------------------------------------- /assets/default-import-match-filename.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/minseoksuh/eslint-plugin-consistent-default-export-name/be15da14cc9d53a9bf6d3ab58b6d1183b94e2781/assets/default-import-match-filename.png -------------------------------------------------------------------------------- /docs/rules/default-export-match-filename.md: -------------------------------------------------------------------------------- 1 | # consistent-default-export-name/default-export-match-filename (filenames/match-exported) 2 | 3 | > @author Stefan Lau 4 | 5 | Match the file name against the default exported value in the module. Files that dont have a default export will 6 | be ignored. The exports of `index.js` are matched against their parent directory. 7 | 8 | ```js 9 | // Considered problem only if the file isn't named foo.js or foo/index.js 10 | export default function foo() {} 11 | 12 | // Considered problem only if the file isn't named Foo.js or Foo/index.js 13 | module.exports = class Foo() {} 14 | 15 | // Considered problem only if the file isn't named someVariable.js or someVariable/index.js 16 | module.exports = someVariable; 17 | 18 | // Never considered a problem 19 | export default { foo: "bar" }; 20 | ``` 21 | 22 | If your filename policy doesn't quite match with your variable naming policy, you can add one or multiple transforms: 23 | 24 | ```json 25 | "consistent-default-export-name/default-export-match-filename": [ 2, "kebab" ] 26 | ``` 27 | 28 | Now, in your code: 29 | 30 | ```js 31 | // Considered problem only if file isn't named variable-name.js or variable-name/index.js 32 | export default function variableName; 33 | ``` 34 | 35 | Available transforms: 36 | '[snake](https://www.npmjs.com/package/lodash.snakecase)', 37 | '[kebab](https://www.npmjs.com/package/lodash.kebabcase)', 38 | '[camel](https://www.npmjs.com/package/lodash.camelcase)', and 39 | 'pascal' (camel-cased with first letter in upper case). 40 | 41 | For multiple transforms simply specify an array like this (null in this case stands for no transform): 42 | 43 | ```json 44 | "consistent-default-export-name/default-export-match-filename": [2, [ null, "kebab", "snake" ] ] 45 | ``` 46 | 47 | If you prefer to use suffixes for your files (e.g. `Foo.react.js` for a React component file), 48 | you can use a second configuration parameter. It allows you to remove parts of a filename matching a regex pattern 49 | before transforming and matching against the export. 50 | 51 | ```json 52 | "consistent-default-export-name/default-export-match-filename": [ 2, null, "\\.react$" ] 53 | ``` 54 | 55 | Now, in your code: 56 | 57 | ```js 58 | // Considered problem only if file isn't named variableName.react.js, variableName.js or variableName/index.js 59 | export default function variableName; 60 | ``` 61 | 62 | If you also want to match exported function calls you can use the third option (a boolean flag). 63 | 64 | ```json 65 | "consistent-default-export-name/default-export-match-filename": [ 2, null, null, true ] 66 | ``` 67 | 68 | Now, in your code: 69 | 70 | ```js 71 | // Considered problem only if file isn't named functionName.js or functionName/index.js 72 | export default functionName(); 73 | ``` 74 | -------------------------------------------------------------------------------- /docs/rules/default-import-match-filename.md: -------------------------------------------------------------------------------- 1 | # consistent-default-export-name/default-import-match-filename 2 | 3 | > @author Chiawen Chen (github: @golopot) 4 | 5 | Enforces default import name to match filename. Name matching is case-insensitive, and characters `._-` are stripped. 6 | 7 | ## Rule Details 8 | 9 | ### Options 10 | 11 | #### `ignorePaths` 12 | 13 | This option accepts an array of glob patterns. An import statement will be ignored if the import source path matches some of the glob patterns, where the glob patterns are relative to the current working directory where the linter is launched. For example, with the option `{ignorePaths: ['**/foo.js']}`, the statement `import whatever from './foo.js'` will be ignored. 14 | 15 | ### Fail 16 | 17 | ```js 18 | import notFoo from './foo'; 19 | import utilsFoo from '../utils/foo'; 20 | import notFoo from '../foo/index.js'; 21 | import notMerge from 'lodash/merge'; 22 | import notPackageName from '..'; // When "../package.json" has name "package-name" 23 | import notDirectoryName from '..'; // When ".." is a directory named "directory-name" 24 | const bar = require('./foo'); 25 | const bar = require('../foo'); 26 | ``` 27 | 28 | ### Pass 29 | 30 | ```js 31 | import foo from './foo'; 32 | import foo from '../foo/index.js'; 33 | import merge from 'lodash/merge'; 34 | import packageName from '..'; // When "../package.json" has name "package-name" 35 | import directoryName from '..'; // When ".." is a directory named "directory-name" 36 | import anything from 'foo'; 37 | import foo_ from './foo'; 38 | import foo from './foo.js'; 39 | import fooBar from './foo-bar'; 40 | import FoObAr from './foo-bar'; 41 | import catModel from './cat.model.js'; 42 | const foo = require('./foo'); 43 | 44 | // Option `{ ignorePaths: ['**/models/*.js'] }` 45 | import whatever from '../models/foo.js'; 46 | // Option `{ ignorePaths: ['src/foo/bar.js'] }` 47 | // Linted file is "${PROJECT_ROOT}/src/a.js" 48 | // Current working directory is "${PROJECT_ROOT}" 49 | import whatever from './foo/bar.js' 50 | ``` 51 | -------------------------------------------------------------------------------- /lib/common/ModuleCache.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | exports.__esModule = true; 3 | 4 | const log = require('debug')('eslint-module-utils:ModuleCache'); 5 | 6 | class ModuleCache { 7 | constructor(map) { 8 | this.map = map || new Map(); 9 | } 10 | 11 | /** 12 | * returns value for returning inline 13 | * @param {[type]} cacheKey [description] 14 | * @param {[type]} result [description] 15 | */ 16 | set(cacheKey, result) { 17 | this.map.set(cacheKey, { result, lastSeen: process.hrtime() }); 18 | log('setting entry for', cacheKey); 19 | return result; 20 | } 21 | 22 | get(cacheKey, settings) { 23 | if (this.map.has(cacheKey)) { 24 | const f = this.map.get(cacheKey); 25 | // check freshness 26 | if (process.hrtime(f.lastSeen)[0] < settings.lifetime) 27 | return f.result; 28 | } else log('cache miss for', cacheKey); 29 | // cache miss 30 | return undefined; 31 | } 32 | } 33 | 34 | ModuleCache.getSettings = function (settings) { 35 | const cacheSettings = Object.assign( 36 | { 37 | lifetime: 30, // seconds 38 | }, 39 | settings['import/cache'] 40 | ); 41 | 42 | // parse infinity 43 | if ( 44 | cacheSettings.lifetime === '∞' || 45 | cacheSettings.lifetime === 'Infinity' 46 | ) { 47 | cacheSettings.lifetime = Infinity; 48 | } 49 | 50 | return cacheSettings; 51 | }; 52 | 53 | exports.default = ModuleCache; 54 | -------------------------------------------------------------------------------- /lib/common/getExportedName.js: -------------------------------------------------------------------------------- 1 | function getNodeName(node, options) { 2 | var op = options || []; 3 | 4 | if (node.type === 'Identifier') { 5 | return node.name; 6 | } 7 | 8 | if (node.id && node.id.type === 'Identifier') { 9 | return node.id.name; 10 | } 11 | 12 | if ( 13 | op[2] && 14 | node.type === 'CallExpression' && 15 | node.callee.type === 'Identifier' 16 | ) { 17 | return node.callee.name; 18 | } 19 | } 20 | 21 | module.exports = function getExportedName(programNode, options) { 22 | for (var i = 0; i < programNode.body.length; i += 1) { 23 | var node = programNode.body[i]; 24 | 25 | // export default ... 26 | if (node.type === 'ExportDefaultDeclaration') { 27 | return getNodeName(node.declaration, options); 28 | } 29 | 30 | // module.exports = ... 31 | if ( 32 | node.type === 'ExpressionStatement' && 33 | node.expression.type === 'AssignmentExpression' && 34 | node.expression.left.type === 'MemberExpression' && 35 | node.expression.left.object.type === 'Identifier' && 36 | node.expression.left.object.name === 'module' && 37 | node.expression.left.property.type === 'Identifier' && 38 | node.expression.left.property.name === 'exports' 39 | ) { 40 | return getNodeName(node.expression.right, options); 41 | } 42 | } 43 | }; 44 | -------------------------------------------------------------------------------- /lib/common/hash.js: -------------------------------------------------------------------------------- 1 | /** 2 | * utilities for hashing config objects. 3 | * basically iteratively updates hash with a JSON-like format 4 | */ 5 | 'use strict'; 6 | exports.__esModule = true; 7 | 8 | const createHash = require('crypto').createHash; 9 | 10 | const stringify = JSON.stringify; 11 | 12 | function hashify(value, hash) { 13 | if (!hash) hash = createHash('sha256'); 14 | 15 | if (Array.isArray(value)) { 16 | hashArray(value, hash); 17 | } else if (value instanceof Object) { 18 | hashObject(value, hash); 19 | } else { 20 | hash.update(stringify(value) || 'undefined'); 21 | } 22 | 23 | return hash; 24 | } 25 | exports.default = hashify; 26 | 27 | function hashArray(array, hash) { 28 | if (!hash) hash = createHash('sha256'); 29 | 30 | hash.update('['); 31 | for (let i = 0; i < array.length; i++) { 32 | hashify(array[i], hash); 33 | hash.update(','); 34 | } 35 | hash.update(']'); 36 | 37 | return hash; 38 | } 39 | hashify.array = hashArray; 40 | exports.hashArray = hashArray; 41 | 42 | function hashObject(object, hash) { 43 | if (!hash) hash = createHash('sha256'); 44 | 45 | hash.update('{'); 46 | Object.keys(object) 47 | .sort() 48 | .forEach((key) => { 49 | hash.update(stringify(key)); 50 | hash.update(':'); 51 | hashify(object[key], hash); 52 | hash.update(','); 53 | }); 54 | hash.update('}'); 55 | 56 | return hash; 57 | } 58 | hashify.object = hashObject; 59 | exports.hashObject = hashObject; 60 | -------------------------------------------------------------------------------- /lib/common/isIgnoredFilename.js: -------------------------------------------------------------------------------- 1 | var ignoredFilenames = ['', '']; 2 | 3 | module.exports = function isIgnoredFilename(filename) { 4 | return ignoredFilenames.indexOf(filename) !== -1; 5 | }; 6 | -------------------------------------------------------------------------------- /lib/common/isIndexFile.js: -------------------------------------------------------------------------------- 1 | module.exports = function isIndexFile(parsed) { 2 | return parsed.name === 'index'; 3 | }; 4 | -------------------------------------------------------------------------------- /lib/common/isStaticRequire.js: -------------------------------------------------------------------------------- 1 | module.exports = function isStaticRequire(node) { 2 | return ( 3 | node && 4 | node.callee && 5 | node.callee.type === 'Identifier' && 6 | node.callee.name === 'require' && 7 | node.arguments.length === 1 && 8 | node.arguments[0].type === 'Literal' && 9 | typeof node.arguments[0].value === 'string' 10 | ); 11 | }; 12 | -------------------------------------------------------------------------------- /lib/common/parseFilename.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | 3 | module.exports = function parseFilename(filename) { 4 | var ext = path.extname(filename); 5 | 6 | return { 7 | dir: path.dirname(filename), 8 | base: path.basename(filename), 9 | ext: ext, 10 | name: path.basename(filename, ext), 11 | }; 12 | }; 13 | -------------------------------------------------------------------------------- /lib/common/resolve.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | exports.__esModule = true; 3 | 4 | const pkgDir = require('pkg-dir'); 5 | 6 | const fs = require('fs'); 7 | const Module = require('module'); 8 | const path = require('path'); 9 | 10 | const hashObject = require('./hash').hashObject; 11 | const ModuleCache = require('./ModuleCache').default; 12 | 13 | const CASE_SENSITIVE_FS = !fs.existsSync( 14 | path.join(__dirname.toUpperCase(), 'reSOLVE.js') 15 | ); 16 | exports.CASE_SENSITIVE_FS = CASE_SENSITIVE_FS; 17 | 18 | const ERROR_NAME = 'EslintPluginImportResolveError'; 19 | 20 | const fileExistsCache = new ModuleCache(); 21 | 22 | // Polyfill Node's `Module.createRequireFromPath` if not present (added in Node v10.12.0) 23 | // Use `Module.createRequire` if available (added in Node v12.2.0) 24 | const createRequire = 25 | Module.createRequire || 26 | Module.createRequireFromPath || 27 | function (filename) { 28 | const mod = new Module(filename, null); 29 | mod.filename = filename; 30 | mod.paths = Module._nodeModulePaths(path.dirname(filename)); 31 | 32 | mod._compile(`module.exports = require;`, filename); 33 | 34 | return mod.exports; 35 | }; 36 | 37 | function tryRequire(target, sourceFile) { 38 | let resolved; 39 | try { 40 | // Check if the target exists 41 | if (sourceFile != null) { 42 | try { 43 | resolved = createRequire(path.resolve(sourceFile)).resolve( 44 | target 45 | ); 46 | } catch (e) { 47 | resolved = require.resolve(target); 48 | } 49 | } else { 50 | resolved = require.resolve(target); 51 | } 52 | } catch (e) { 53 | // If the target does not exist then just return undefined 54 | return undefined; 55 | } 56 | 57 | // If the target exists then return the loaded module 58 | return require(resolved); 59 | } 60 | 61 | // http://stackoverflow.com/a/27382838 62 | exports.fileExistsWithCaseSync = function fileExistsWithCaseSync( 63 | filepath, 64 | cacheSettings 65 | ) { 66 | // don't care if the FS is case-sensitive 67 | if (CASE_SENSITIVE_FS) return true; 68 | 69 | // null means it resolved to a builtin 70 | if (filepath === null) return true; 71 | if (filepath.toLowerCase() === process.cwd().toLowerCase()) return true; 72 | const parsedPath = path.parse(filepath); 73 | const dir = parsedPath.dir; 74 | 75 | let result = fileExistsCache.get(filepath, cacheSettings); 76 | if (result != null) return result; 77 | 78 | // base case 79 | if (dir === '' || parsedPath.root === filepath) { 80 | result = true; 81 | } else { 82 | const filenames = fs.readdirSync(dir); 83 | if (filenames.indexOf(parsedPath.base) === -1) { 84 | result = false; 85 | } else { 86 | result = fileExistsWithCaseSync(dir, cacheSettings); 87 | } 88 | } 89 | fileExistsCache.set(filepath, result); 90 | return result; 91 | }; 92 | 93 | function relative(modulePath, sourceFile, settings) { 94 | return fullResolve(modulePath, sourceFile, settings).path; 95 | } 96 | 97 | function fullResolve(modulePath, sourceFile, settings) { 98 | // check if this is a bonus core module 99 | const coreSet = new Set(settings['import/core-modules']); 100 | if (coreSet.has(modulePath)) return { found: true, path: null }; 101 | 102 | const sourceDir = path.dirname(sourceFile); 103 | const cacheKey = 104 | sourceDir + hashObject(settings).digest('hex') + modulePath; 105 | 106 | const cacheSettings = ModuleCache.getSettings(settings); 107 | 108 | const cachedPath = fileExistsCache.get(cacheKey, cacheSettings); 109 | if (cachedPath !== undefined) return { found: true, path: cachedPath }; 110 | 111 | function cache(resolvedPath) { 112 | fileExistsCache.set(cacheKey, resolvedPath); 113 | } 114 | 115 | function withResolver(resolver, config) { 116 | function v1() { 117 | try { 118 | const resolved = resolver.resolveImport( 119 | modulePath, 120 | sourceFile, 121 | config 122 | ); 123 | if (resolved === undefined) return { found: false }; 124 | return { found: true, path: resolved }; 125 | } catch (err) { 126 | return { found: false }; 127 | } 128 | } 129 | 130 | function v2() { 131 | return resolver.resolve(modulePath, sourceFile, config); 132 | } 133 | 134 | switch (resolver.interfaceVersion) { 135 | case 2: 136 | return v2(); 137 | 138 | default: 139 | case 1: 140 | return v1(); 141 | } 142 | } 143 | 144 | const configResolvers = settings['import/resolver'] || { 145 | node: settings['import/resolve'], 146 | }; // backward compatibility 147 | 148 | const resolvers = resolverReducer(configResolvers, new Map()); 149 | 150 | for (const pair of resolvers) { 151 | const name = pair[0]; 152 | const config = pair[1]; 153 | const resolver = requireResolver(name, sourceFile); 154 | const resolved = withResolver(resolver, config); 155 | 156 | if (!resolved.found) continue; 157 | 158 | // else, counts 159 | cache(resolved.path); 160 | return resolved; 161 | } 162 | 163 | // failed 164 | // cache(undefined) 165 | return { found: false }; 166 | } 167 | exports.relative = relative; 168 | 169 | function resolverReducer(resolvers, map) { 170 | if (Array.isArray(resolvers)) { 171 | resolvers.forEach((r) => resolverReducer(r, map)); 172 | return map; 173 | } 174 | 175 | if (typeof resolvers === 'string') { 176 | map.set(resolvers, null); 177 | return map; 178 | } 179 | 180 | if (typeof resolvers === 'object') { 181 | for (const key in resolvers) { 182 | map.set(key, resolvers[key]); 183 | } 184 | return map; 185 | } 186 | 187 | const err = new Error('invalid resolver config'); 188 | err.name = ERROR_NAME; 189 | throw err; 190 | } 191 | 192 | function getBaseDir(sourceFile) { 193 | return pkgDir.sync(sourceFile) || process.cwd(); 194 | } 195 | function requireResolver(name, sourceFile) { 196 | // Try to resolve package with conventional name 197 | const resolver = 198 | tryRequire(`eslint-import-resolver-${name}`, sourceFile) || 199 | tryRequire(name, sourceFile) || 200 | tryRequire(path.resolve(getBaseDir(sourceFile), name)); 201 | 202 | if (!resolver) { 203 | const err = new Error(`unable to load resolver "${name}".`); 204 | err.name = ERROR_NAME; 205 | throw err; 206 | } 207 | if (!isResolverValid(resolver)) { 208 | const err = new Error( 209 | `${name} with invalid interface loaded as resolver` 210 | ); 211 | err.name = ERROR_NAME; 212 | throw err; 213 | } 214 | 215 | return resolver; 216 | } 217 | 218 | function isResolverValid(resolver) { 219 | if (resolver.interfaceVersion === 2) { 220 | return resolver.resolve && typeof resolver.resolve === 'function'; 221 | } else { 222 | return ( 223 | resolver.resolveImport && 224 | typeof resolver.resolveImport === 'function' 225 | ); 226 | } 227 | } 228 | 229 | const erroredContexts = new Set(); 230 | 231 | /** 232 | * Given 233 | * @param {string} p - module path 234 | * @param {object} context - ESLint context 235 | * @return {string} - the full module filesystem path; 236 | * null if package is core; 237 | * undefined if not found 238 | */ 239 | function resolve(p, context) { 240 | try { 241 | return relative( 242 | p, 243 | context.getPhysicalFilename 244 | ? context.getPhysicalFilename() 245 | : context.getFilename(), 246 | context.settings 247 | ); 248 | } catch (err) { 249 | if (!erroredContexts.has(context)) { 250 | // The `err.stack` string starts with `err.name` followed by colon and `err.message`. 251 | // We're filtering out the default `err.name` because it adds little value to the message. 252 | let errMessage = err.message; 253 | if (err.name !== ERROR_NAME && err.stack) { 254 | errMessage = err.stack.replace(/^Error: /, ''); 255 | } 256 | context.report({ 257 | message: `Resolve error: ${errMessage}`, 258 | loc: { line: 1, column: 0 }, 259 | }); 260 | erroredContexts.add(context); 261 | } 262 | } 263 | } 264 | resolve.relative = relative; 265 | exports.default = resolve; 266 | -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview default-export-name = default-import-name = filename 3 | * @author minseoksuh 4 | */ 5 | 'use strict'; 6 | 7 | //------------------------------------------------------------------------------ 8 | // Requirements 9 | //------------------------------------------------------------------------------ 10 | 11 | const defaultExportMatchFilename = require('./rules/default-export-match-filename'); 12 | const defaultImportMatchFilename = require('./rules/default-import-match-filename'); 13 | 14 | module.exports = { 15 | configs: { 16 | fixed: { 17 | plugins: ['consistent-default-export-name'], 18 | env: { 19 | browser: true, 20 | es6: true, 21 | node: true, 22 | }, 23 | rules: { 24 | 'consistent-default-export-name/default-export-match-filename': 25 | 'error', 26 | 'consistent-default-export-name/default-import-match-filename': 27 | 'error', 28 | }, 29 | }, 30 | }, 31 | rules: { 32 | 'default-export-match-filename': defaultExportMatchFilename, 33 | 'default-import-match-filename': defaultImportMatchFilename, 34 | }, 35 | }; 36 | -------------------------------------------------------------------------------- /lib/rules/default-export-match-filename.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @author Stefan Lau 3 | */ 4 | 5 | const camelCase = require('lodash.camelcase'); 6 | const upperFirst = require('lodash.upperfirst'); 7 | const kebabCase = require('lodash.kebabcase'); 8 | const snakeCase = require('lodash.snakecase'); 9 | 10 | var path = require('path'), 11 | parseFilename = require('../common/parseFilename'), 12 | isIgnoredFilename = require('../common/isIgnoredFilename'), 13 | getExportedName = require('../common/getExportedName'), 14 | isIndexFile = require('../common/isIndexFile'), 15 | transforms = { 16 | kebab: kebabCase, 17 | snake: snakeCase, 18 | camel: camelCase, 19 | pascal: function (name) { 20 | return upperFirst(camelCase(name)); 21 | }, 22 | }, 23 | transformNames = Object.keys(transforms), 24 | transformSchema = { enum: transformNames.concat([null]) }; 25 | 26 | function getStringToCheckAgainstExport(parsed, replacePattern) { 27 | var dirArray = parsed.dir.split(path.sep); 28 | var lastDirectory = dirArray[dirArray.length - 1]; 29 | 30 | if (isIndexFile(parsed)) { 31 | return lastDirectory; 32 | } else { 33 | return replacePattern 34 | ? parsed.name.replace(replacePattern, '') 35 | : parsed.name; 36 | } 37 | } 38 | 39 | function getTransformsFromOptions(option) { 40 | var usedTransforms = option && option.push ? option : [option]; 41 | 42 | return usedTransforms.map(function (name) { 43 | return transforms[name]; 44 | }); 45 | } 46 | 47 | function transform(exportedName, transforms) { 48 | return transforms.map(function (t) { 49 | return t ? t(exportedName) : exportedName; 50 | }); 51 | } 52 | 53 | function anyMatch(expectedExport, transformedNames) { 54 | return transformedNames.some(function (name) { 55 | return name === expectedExport; 56 | }); 57 | } 58 | 59 | function getWhatToMatchMessage(transforms) { 60 | if (transforms.length === 1 && !transforms[0]) { 61 | return 'the exported name'; 62 | } 63 | if (transforms.length > 1) { 64 | return 'any of the exported and transformed names'; 65 | } 66 | return 'the exported and transformed name'; 67 | } 68 | 69 | module.exports = { 70 | meta: { 71 | type: 'suggestion', 72 | docs: { 73 | description: 74 | 'If node contains default export, the filename should match the name of the defualt export', 75 | category: 'Possible Errors', 76 | recommended: true, 77 | }, 78 | 79 | fixable: 'code', 80 | 81 | schema: [ 82 | { 83 | oneOf: [ 84 | transformSchema, 85 | { type: 'array', items: transformSchema, minItems: 1 }, 86 | ], 87 | }, 88 | { 89 | type: ['string', 'null'], 90 | }, 91 | { 92 | type: ['boolean', 'null'], 93 | }, 94 | ], // no options 95 | }, 96 | create: function (context) { 97 | return { 98 | Program: function (node) { 99 | var transforms = getTransformsFromOptions(context.options[0]), 100 | replacePattern = context.options[1] 101 | ? new RegExp(context.options[1]) 102 | : null, 103 | filename = context.getFilename(), 104 | absoluteFilename = path.resolve(filename), 105 | parsed = parseFilename(absoluteFilename), 106 | shouldIgnore = isIgnoredFilename(filename), 107 | exportedName = getExportedName(node, context.options), 108 | isExporting = Boolean(exportedName), 109 | expectedExport = getStringToCheckAgainstExport( 110 | parsed, 111 | replacePattern 112 | ), 113 | transformedNames = transform(exportedName, transforms), 114 | everythingIsIndex = 115 | exportedName === 'index' && parsed.name === 'index', 116 | matchesExported = 117 | anyMatch(expectedExport, transformedNames) || 118 | everythingIsIndex, 119 | reportIf = function ( 120 | condition, 121 | messageForNormalFile, 122 | messageForIndexFile 123 | ) { 124 | var message = 125 | !messageForIndexFile || !isIndexFile(parsed) 126 | ? messageForNormalFile 127 | : messageForIndexFile; 128 | 129 | if (condition) { 130 | context.report(node, message, { 131 | name: parsed.base, 132 | expectedExport: expectedExport, 133 | exportName: transformedNames.join("', '"), 134 | extension: parsed.ext, 135 | whatToMatch: getWhatToMatchMessage(transforms), 136 | }); 137 | } 138 | }; 139 | 140 | if (shouldIgnore) return; 141 | 142 | reportIf( 143 | isExporting && !matchesExported, 144 | "Filename '{{expectedExport}}' must match {{whatToMatch}} '{{exportName}}'.", 145 | "The directory '{{expectedExport}}' must be named '{{exportName}}', after the exported value of its index file." 146 | ); 147 | }, 148 | }; 149 | }, 150 | }; 151 | -------------------------------------------------------------------------------- /lib/rules/default-import-match-filename.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @author Chiawen Chen (github: @golopot) 3 | */ 4 | 5 | const isStaticRequire = require('../common/isStaticRequire'); 6 | const Path = require('path'); 7 | const minimatch = require('minimatch'); 8 | const resolve = require('../common/resolve').default; 9 | 10 | /** 11 | * @param {string} filename 12 | * @returns {string} 13 | */ 14 | function removeExtension(filename) { 15 | return Path.basename(filename, Path.extname(filename)); 16 | } 17 | 18 | /** 19 | * @param {string} filename 20 | * @returns {string} 21 | */ 22 | function normalizeFilename(filename) { 23 | return filename.replace(/[-_.]/g, '').toLowerCase(); 24 | } 25 | 26 | /** 27 | * Test if local name matches filename. 28 | * @param {string} localName 29 | * @param {string} filename 30 | * @returns {boolean} 31 | */ 32 | function isCompatible(localName, filename) { 33 | const normalizedLocalName = localName.replace(/_/g, '').toLowerCase(); 34 | 35 | return ( 36 | normalizedLocalName === normalizeFilename(filename) || 37 | normalizedLocalName === normalizeFilename(removeExtension(filename)) 38 | ); 39 | } 40 | 41 | /** 42 | * Match 'foo' and '@foo/bar' but not 'foo/bar.js', './foo', or '@foo/bar/a.js' 43 | * @param {string} path 44 | * @returns {boolean} 45 | */ 46 | function isBarePackageImport(path) { 47 | return ( 48 | (path !== '.' && 49 | path !== '..' && 50 | !path.includes('/') && 51 | !path.startsWith('@')) || 52 | /@[^/]+\/[^/]+$/.test(path) 53 | ); 54 | } 55 | 56 | /** 57 | * Match paths consisting of only '.' and '..', like '.', './', '..', '../..'. 58 | * @param {string} path 59 | * @returns {boolean} 60 | */ 61 | function isAncestorRelativePath(path) { 62 | return ( 63 | path.length > 0 && 64 | !path.startsWith('/') && 65 | path 66 | .split(/[/\\]/) 67 | .every( 68 | (segment) => 69 | segment === '..' || segment === '.' || segment === '' 70 | ) 71 | ); 72 | } 73 | 74 | /** 75 | * @param {string} packageJsonPath 76 | * @returns {string | undefined} 77 | */ 78 | function getPackageJsonName(packageJsonPath) { 79 | try { 80 | return require(packageJsonPath).name || undefined; 81 | } catch (_) { 82 | return undefined; 83 | } 84 | } 85 | 86 | function getNameFromPackageJsonOrDirname(path, context) { 87 | const directoryName = Path.join(context.getFilename(), path, '..'); 88 | const packageJsonPath = Path.join(directoryName, 'package.json'); 89 | const packageJsonName = getPackageJsonName(packageJsonPath); 90 | return packageJsonName || Path.basename(directoryName); 91 | } 92 | 93 | /** 94 | * Get filename from a path. 95 | * @param {string} path 96 | * @param {object} context 97 | * @returns {string | undefined} 98 | */ 99 | function getFilename(path, context) { 100 | // like require('lodash') 101 | if (isBarePackageImport(path)) { 102 | return undefined; 103 | } 104 | 105 | const basename = Path.basename(path); 106 | 107 | const isDir = /^index$|^index\./.test(basename); 108 | const processedPath = isDir ? Path.dirname(path) : path; 109 | 110 | // like require('.'), require('..'), require('../..') 111 | if (isAncestorRelativePath(processedPath)) { 112 | return getNameFromPackageJsonOrDirname(processedPath, context); 113 | } 114 | 115 | return Path.basename(processedPath) + (isDir ? '/' : ''); 116 | } 117 | 118 | /** 119 | * @param {object} context 120 | * @param {Set} ignorePaths 121 | * @param {string} path 122 | * @returns {boolean} 123 | */ 124 | function isIgnored(context, ignorePaths, path) { 125 | const resolvedPath = resolve(path, context); 126 | 127 | return ( 128 | resolvedPath != null && 129 | [...ignorePaths].some((pattern) => 130 | minimatch(Path.relative(process.cwd(), resolvedPath), pattern) 131 | ) 132 | ); 133 | } 134 | 135 | module.exports = { 136 | meta: { 137 | type: 'suggestion', 138 | schema: [ 139 | { 140 | type: 'object', 141 | additionalProperties: false, 142 | properties: { 143 | ignorePaths: { 144 | type: 'array', 145 | items: { 146 | type: 'string', 147 | }, 148 | }, 149 | }, 150 | }, 151 | ], 152 | }, 153 | 154 | create(context) { 155 | const ignorePaths = new Set( 156 | context.options[0] ? context.options[0].ignorePaths || [] : [] 157 | ); 158 | 159 | return { 160 | ImportDeclaration(node) { 161 | const defaultImportSpecifier = node.specifiers.find( 162 | ({ type }) => type === 'ImportDefaultSpecifier' 163 | ); 164 | 165 | const defaultImportName = 166 | defaultImportSpecifier && defaultImportSpecifier.local.name; 167 | 168 | if (!defaultImportName) { 169 | return; 170 | } 171 | 172 | const filename = getFilename(node.source.value, context); 173 | 174 | if (!filename) { 175 | return; 176 | } 177 | 178 | if ( 179 | !isCompatible(defaultImportName, filename) && 180 | !isIgnored(context, ignorePaths, node.source.value) 181 | ) { 182 | context.report({ 183 | node: defaultImportSpecifier, 184 | message: `Default import name does not match filename "${filename}".`, 185 | }); 186 | } 187 | }, 188 | 189 | CallExpression(node) { 190 | if ( 191 | !isStaticRequire(node) || 192 | node.parent.type !== 'VariableDeclarator' || 193 | node.parent.id.type !== 'Identifier' 194 | ) { 195 | return; 196 | } 197 | 198 | const localName = node.parent.id.name; 199 | 200 | const filename = getFilename(node.arguments[0].value, context); 201 | 202 | if (!filename) { 203 | return; 204 | } 205 | 206 | if ( 207 | !isCompatible(localName, filename) && 208 | !isIgnored(context, ignorePaths, node.arguments[0].value) 209 | ) { 210 | context.report({ 211 | node: node.parent.id, 212 | message: `Default import name does not match filename "${filename}".`, 213 | }); 214 | } 215 | }, 216 | }; 217 | }, 218 | }; 219 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "eslint-plugin-consistent-default-export-name", 3 | "version": "0.0.14", 4 | "homepage": "https://github.com/minseoksuh/eslint-plugin-consistent-default-export-name/blob/main/README.md", 5 | "description": "eslint plugin with rules to help use default export names consistently throughout the project", 6 | "keywords": [ 7 | "eslint", 8 | "eslintplugin", 9 | "eslint-plugin", 10 | "default export", 11 | "default import" 12 | ], 13 | "author": "minseoksuh", 14 | "main": "lib/index.js", 15 | "files": [ 16 | "lib", 17 | "docs", 18 | "LICENSE", 19 | "README.md" 20 | ], 21 | "scripts": { 22 | "test": "mocha tests --recursive" 23 | }, 24 | "dependencies": { 25 | "lodash.camelcase": "^4.3.0", 26 | "lodash.kebabcase": "^4.1.1", 27 | "lodash.snakecase": "^4.1.1", 28 | "lodash.upperfirst": "^4.3.1" 29 | }, 30 | "devDependencies": { 31 | "eslint": "^7.1.0", 32 | "eslint-config-prettier": "^8.3.0", 33 | "eslint-plugin-import": "^2.23.4", 34 | "eslint-plugin-prettier": "^3.4.0", 35 | "mocha": "^7.2.0", 36 | "prettier": "2.3.2", 37 | "sinon": "^11.1.1" 38 | }, 39 | "engines": { 40 | "node": ">=0.10.0" 41 | }, 42 | "license": "MIT" 43 | } 44 | -------------------------------------------------------------------------------- /tests/files/default-import-match-filename/ignored/foo.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/minseoksuh/eslint-plugin-consistent-default-export-name/be15da14cc9d53a9bf6d3ab58b6d1183b94e2781/tests/files/default-import-match-filename/ignored/foo.js -------------------------------------------------------------------------------- /tests/files/default-import-match-filename/main.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/minseoksuh/eslint-plugin-consistent-default-export-name/be15da14cc9d53a9bf6d3ab58b6d1183b94e2781/tests/files/default-import-match-filename/main.js -------------------------------------------------------------------------------- /tests/files/default-import-match-filename/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "package-name" 3 | } 4 | -------------------------------------------------------------------------------- /tests/files/default-import-match-filename/some-directory/a.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/minseoksuh/eslint-plugin-consistent-default-export-name/be15da14cc9d53a9bf6d3ab58b6d1183b94e2781/tests/files/default-import-match-filename/some-directory/a.js -------------------------------------------------------------------------------- /tests/files/default-import-match-filename/some-directory/index.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/minseoksuh/eslint-plugin-consistent-default-export-name/be15da14cc9d53a9bf6d3ab58b6d1183b94e2781/tests/files/default-import-match-filename/some-directory/index.js -------------------------------------------------------------------------------- /tests/lib/rules/default-export-match-filename.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @author Stefan Lau 3 | */ 4 | 5 | var exportedRule = require('../../../lib/rules/default-export-match-filename'), 6 | RuleTester = require('eslint').RuleTester; 7 | 8 | var testCode = "var foo = 'bar';", 9 | testCallCode = 'export default foo();', 10 | exportedVariableCode = 'module.exports = exported;', 11 | exportedJsxClassCode = 12 | 'module.exports = class Foo { render() { return Test Class; } };', 13 | exportedClassCode = 'module.exports = class Foo {};', 14 | exportedFunctionCode = 'module.exports = function foo() {};', 15 | exportUnnamedFunctionCode = 'module.exports = function() {};', 16 | exportedCalledFunctionCode = 'module.exports = foo();', 17 | exportedJsxFunctionCode = 18 | 'module.exports = function foo() { return Test Fn };', 19 | exportedEs6VariableCode = 'export default exported;', 20 | exportedEs6ClassCode = 'export default class Foo {};', 21 | exportedEs6JsxClassCode = 22 | 'export default class Foo { render() { return Test Class; } };', 23 | exportedEs6FunctionCode = 'export default function foo() {};', 24 | exportedEs6JsxFunctionCode = 25 | 'export default function foo() { return Test Fn };', 26 | exportedEs6Index = 'export default function index() {};', 27 | camelCaseCommonJS = 'module.exports = variableName;', 28 | snakeCaseCommonJS = 'module.exports = variable_name;', 29 | camelCaseEs6 = 'export default variableName;', 30 | snakeCaseEs6 = 'export default variable_name;', 31 | ruleTester = new RuleTester(); 32 | 33 | ruleTester.run('lib/rules/filename', exportedRule, { 34 | valid: [ 35 | { 36 | code: testCode, 37 | filename: '', 38 | }, 39 | { 40 | code: testCode, 41 | filename: '', 42 | }, 43 | { 44 | code: exportUnnamedFunctionCode, 45 | filename: 'testFile.js', 46 | }, 47 | { 48 | code: testCode, 49 | filename: '/some/dir/exported.js', 50 | }, 51 | { 52 | code: testCallCode, 53 | filename: '/some/dir/foo.js', 54 | parserOptions: { ecmaVersion: 6, sourceType: 'module' }, 55 | }, 56 | { 57 | code: exportedVariableCode, 58 | filename: '/some/dir/exported.js', 59 | }, 60 | { 61 | code: exportedClassCode, 62 | filename: '/some/dir/Foo.js', 63 | parserOptions: { ecmaVersion: 6 }, 64 | }, 65 | { 66 | code: exportedJsxClassCode, 67 | filename: '/some/dir/Foo.js', 68 | parserOptions: { ecmaVersion: 6, ecmaFeatures: { jsx: true } }, 69 | }, 70 | { 71 | code: exportedFunctionCode, 72 | filename: '/some/dir/foo.js', 73 | }, 74 | { 75 | code: exportedCalledFunctionCode, 76 | filename: '/some/dir/bar.js', 77 | }, 78 | { 79 | code: exportedJsxFunctionCode, 80 | filename: '/some/dir/foo.js', 81 | parserOptions: { ecmaFeatures: { jsx: true } }, 82 | }, 83 | { 84 | code: exportedEs6VariableCode, 85 | filename: '/some/dir/exported.js', 86 | parserOptions: { ecmaVersion: 6, sourceType: 'module' }, 87 | }, 88 | { 89 | code: exportedEs6ClassCode, 90 | filename: '/some/dir/Foo.js', 91 | parserOptions: { ecmaVersion: 6, sourceType: 'module' }, 92 | }, 93 | { 94 | code: exportedEs6JsxClassCode, 95 | filename: '/some/dir/Foo.js', 96 | parserOptions: { 97 | ecmaVersion: 6, 98 | sourceType: 'module', 99 | ecmaFeatures: { jsx: true }, 100 | }, 101 | }, 102 | { 103 | code: exportedEs6FunctionCode, 104 | filename: '/some/dir/foo.js', 105 | parserOptions: { ecmaVersion: 6, sourceType: 'module' }, 106 | }, 107 | { 108 | code: exportedEs6JsxFunctionCode, 109 | filename: '/some/dir/foo.js', 110 | parserOptions: { 111 | ecmaVersion: 6, 112 | sourceType: 'module', 113 | ecmaFeatures: { jsx: true }, 114 | }, 115 | }, 116 | { 117 | code: exportedEs6FunctionCode, 118 | filename: '/some/dir/foo/index.js', 119 | parserOptions: { ecmaVersion: 6, sourceType: 'module' }, 120 | }, 121 | { 122 | code: exportedEs6JsxFunctionCode, 123 | filename: '/some/dir/foo/index.js', 124 | parserOptions: { 125 | ecmaVersion: 6, 126 | sourceType: 'module', 127 | ecmaFeatures: { jsx: true }, 128 | }, 129 | }, 130 | // { 131 | // code: exportedEs6FunctionCode, 132 | // // /foo is used as cwd for test setup so full path will be /foo/index.js 133 | // filename: 'index.js', 134 | // parserOptions: { ecmaVersion: 6, sourceType: 'module' }, 135 | // }, 136 | { 137 | code: exportedEs6Index, 138 | // /foo is used as cwd for test setup so full path will be /foo/index.js 139 | filename: 'index.js', 140 | parserOptions: { ecmaVersion: 6, sourceType: 'module' }, 141 | }, 142 | ], 143 | 144 | invalid: [ 145 | { 146 | code: exportedVariableCode, 147 | filename: '/some/dir/fooBar.js', 148 | errors: [ 149 | { 150 | message: 151 | "Filename 'fooBar' must match the exported name 'exported'.", 152 | column: 1, 153 | line: 1, 154 | }, 155 | ], 156 | }, 157 | { 158 | code: exportedClassCode, 159 | filename: '/some/dir/foo.js', 160 | parserOptions: { ecmaVersion: 6 }, 161 | errors: [ 162 | { 163 | message: 164 | "Filename 'foo' must match the exported name 'Foo'.", 165 | column: 1, 166 | line: 1, 167 | }, 168 | ], 169 | }, 170 | { 171 | code: exportedJsxClassCode, 172 | filename: '/some/dir/foo.js', 173 | parserOptions: { ecmaVersion: 6, ecmaFeatures: { jsx: true } }, 174 | errors: [ 175 | { 176 | message: 177 | "Filename 'foo' must match the exported name 'Foo'.", 178 | column: 1, 179 | line: 1, 180 | }, 181 | ], 182 | }, 183 | { 184 | code: exportedFunctionCode, 185 | filename: '/some/dir/bar.js', 186 | errors: [ 187 | { 188 | message: 189 | "Filename 'bar' must match the exported name 'foo'.", 190 | column: 1, 191 | line: 1, 192 | }, 193 | ], 194 | }, 195 | { 196 | code: exportedJsxFunctionCode, 197 | filename: '/some/dir/bar.js', 198 | parserOptions: { ecmaFeatures: { jsx: true } }, 199 | errors: [ 200 | { 201 | message: 202 | "Filename 'bar' must match the exported name 'foo'.", 203 | column: 1, 204 | line: 1, 205 | }, 206 | ], 207 | }, 208 | { 209 | code: exportedEs6VariableCode, 210 | filename: '/some/dir/fooBar.js', 211 | parserOptions: { ecmaVersion: 6, sourceType: 'module' }, 212 | errors: [ 213 | { 214 | message: 215 | "Filename 'fooBar' must match the exported name 'exported'.", 216 | column: 1, 217 | line: 1, 218 | }, 219 | ], 220 | }, 221 | { 222 | code: exportedEs6ClassCode, 223 | filename: '/some/dir/bar.js', 224 | parserOptions: { ecmaVersion: 6, sourceType: 'module' }, 225 | errors: [ 226 | { 227 | message: 228 | "Filename 'bar' must match the exported name 'Foo'.", 229 | column: 1, 230 | line: 1, 231 | }, 232 | ], 233 | }, 234 | { 235 | code: exportedEs6JsxClassCode, 236 | filename: '/some/dir/bar.js', 237 | parserOptions: { 238 | ecmaVersion: 6, 239 | sourceType: 'module', 240 | ecmaFeatures: { jsx: true }, 241 | }, 242 | errors: [ 243 | { 244 | message: 245 | "Filename 'bar' must match the exported name 'Foo'.", 246 | column: 1, 247 | line: 1, 248 | }, 249 | ], 250 | }, 251 | { 252 | code: exportedEs6FunctionCode, 253 | filename: '/some/dir/fooBar/index.js', 254 | parserOptions: { ecmaVersion: 6, sourceType: 'module' }, 255 | errors: [ 256 | { 257 | message: 258 | "The directory 'fooBar' must be named 'foo', after the exported value of its index file.", 259 | column: 1, 260 | line: 1, 261 | }, 262 | ], 263 | }, 264 | { 265 | code: exportedEs6JsxFunctionCode, 266 | filename: '/some/dir/fooBar/index.js', 267 | parserOptions: { 268 | ecmaVersion: 6, 269 | sourceType: 'module', 270 | ecmaFeatures: { jsx: true }, 271 | }, 272 | errors: [ 273 | { 274 | message: 275 | "The directory 'fooBar' must be named 'foo', after the exported value of its index file.", 276 | column: 1, 277 | line: 1, 278 | }, 279 | ], 280 | }, 281 | // { 282 | // code: exportedVariableCode, 283 | // filename: 'index.js', 284 | // parserOptions: { ecmaVersion: 6, sourceType: 'module' }, 285 | // errors: [ 286 | // { 287 | // message: 288 | // "The directory 'foo' must be named 'exported', after the exported value of its index file.", 289 | // column: 1, 290 | // line: 1, 291 | // }, 292 | // ], 293 | // }, 294 | { 295 | code: exportedJsxClassCode, 296 | filename: '/some/dir/Foo.react.js', 297 | parserOptions: { ecmaVersion: 6, ecmaFeatures: { jsx: true } }, 298 | errors: [ 299 | { 300 | message: 301 | "Filename 'Foo.react' must match the exported name 'Foo'.", 302 | column: 1, 303 | line: 1, 304 | }, 305 | ], 306 | }, 307 | ], 308 | }); 309 | 310 | ruleTester.run('lib/rules/filename with configuration', exportedRule, { 311 | valid: [ 312 | { 313 | code: camelCaseCommonJS, 314 | filename: 'variable_name.js', 315 | options: ['snake'], 316 | }, 317 | { 318 | code: camelCaseCommonJS, 319 | filename: 'variable_name/index.js', 320 | options: ['snake'], 321 | }, 322 | { 323 | code: camelCaseCommonJS, 324 | filename: 'variable-name.js', 325 | options: ['kebab'], 326 | }, 327 | { 328 | code: snakeCaseCommonJS, 329 | filename: 'variableName.js', 330 | options: ['camel'], 331 | }, 332 | { 333 | code: camelCaseEs6, 334 | filename: 'variable_name.js', 335 | parserOptions: { ecmaVersion: 6, sourceType: 'module' }, 336 | options: ['snake'], 337 | }, 338 | { 339 | code: camelCaseEs6, 340 | filename: 'variable-name.js', 341 | parserOptions: { ecmaVersion: 6, sourceType: 'module' }, 342 | options: ['kebab'], 343 | }, 344 | { 345 | code: snakeCaseEs6, 346 | filename: 'variableName.js', 347 | parserOptions: { ecmaVersion: 6, sourceType: 'module' }, 348 | options: ['camel'], 349 | }, 350 | { 351 | code: snakeCaseEs6, 352 | filename: 'VariableName.js', 353 | parserOptions: { ecmaVersion: 6, sourceType: 'module' }, 354 | options: ['pascal'], 355 | }, 356 | { 357 | code: snakeCaseEs6, 358 | filename: 'variableName.js', 359 | parserOptions: { ecmaVersion: 6, sourceType: 'module' }, 360 | options: [['pascal', 'camel']], 361 | }, 362 | { 363 | code: snakeCaseEs6, 364 | filename: 'VariableName.js', 365 | parserOptions: { ecmaVersion: 6, sourceType: 'module' }, 366 | options: [['pascal', 'camel']], 367 | }, 368 | { 369 | code: exportedJsxClassCode, 370 | filename: '/some/dir/Foo.react.js', 371 | parserOptions: { ecmaVersion: 6, ecmaFeatures: { jsx: true } }, 372 | options: [null, '\\.react$'], 373 | }, 374 | { 375 | code: exportedEs6JsxClassCode, 376 | filename: '/some/dir/Foo.react.js', 377 | parserOptions: { 378 | ecmaVersion: 6, 379 | sourceType: 'module', 380 | ecmaFeatures: { jsx: true }, 381 | }, 382 | options: [null, '\\.react$'], 383 | }, 384 | { 385 | code: exportedCalledFunctionCode, 386 | filename: '/some/dir/foo.js', 387 | options: [null, null, true], 388 | }, 389 | ], 390 | 391 | invalid: [ 392 | { 393 | code: camelCaseCommonJS, 394 | filename: 'variableName.js', 395 | options: ['snake'], 396 | errors: [ 397 | { 398 | message: 399 | "Filename 'variableName' must match the exported and transformed name 'variable_name'.", 400 | column: 1, 401 | line: 1, 402 | }, 403 | ], 404 | }, 405 | { 406 | code: camelCaseEs6, 407 | filename: 'variableName.js', 408 | parserOptions: { ecmaVersion: 6, sourceType: 'module' }, 409 | options: ['kebab'], 410 | errors: [ 411 | { 412 | message: 413 | "Filename 'variableName' must match the exported and transformed name 'variable-name'.", 414 | column: 1, 415 | line: 1, 416 | }, 417 | ], 418 | }, 419 | { 420 | code: camelCaseEs6, 421 | filename: 'variableName.js', 422 | parserOptions: { ecmaVersion: 6, sourceType: 'module' }, 423 | options: ['pascal'], 424 | errors: [ 425 | { 426 | message: 427 | "Filename 'variableName' must match the exported and transformed name 'VariableName'.", 428 | column: 1, 429 | line: 1, 430 | }, 431 | ], 432 | }, 433 | { 434 | code: camelCaseEs6, 435 | filename: 'VariableName.js', 436 | parserOptions: { ecmaVersion: 6, sourceType: 'module' }, 437 | options: [[null]], 438 | errors: [ 439 | { 440 | message: 441 | "Filename 'VariableName' must match the exported name 'variableName'.", 442 | column: 1, 443 | line: 1, 444 | }, 445 | ], 446 | }, 447 | { 448 | code: camelCaseEs6, 449 | filename: 'variableName.js', 450 | parserOptions: { ecmaVersion: 6, sourceType: 'module' }, 451 | options: [['pascal', 'snake']], 452 | errors: [ 453 | { 454 | message: 455 | "Filename 'variableName' must match any of the exported and transformed names 'VariableName', 'variable_name'.", 456 | column: 1, 457 | line: 1, 458 | }, 459 | ], 460 | }, 461 | { 462 | code: exportedEs6JsxClassCode, 463 | filename: '/some/dir/Foo.bar.js', 464 | parserOptions: { 465 | ecmaVersion: 6, 466 | sourceType: 'module', 467 | ecmaFeatures: { jsx: true }, 468 | }, 469 | options: [null, '\\.react$'], 470 | errors: [ 471 | { 472 | message: 473 | "Filename 'Foo.bar' must match the exported name 'Foo'.", 474 | column: 1, 475 | line: 1, 476 | }, 477 | ], 478 | }, 479 | { 480 | code: exportedEs6JsxClassCode, 481 | filename: '/some/dir/Foo.react/index.js', 482 | parserOptions: { 483 | ecmaVersion: 6, 484 | sourceType: 'module', 485 | ecmaFeatures: { jsx: true }, 486 | }, 487 | options: [null, '\\.react$'], 488 | errors: [ 489 | { 490 | message: 491 | "The directory 'Foo.react' must be named 'Foo', after the exported value of its index file.", 492 | column: 1, 493 | line: 1, 494 | }, 495 | ], 496 | }, 497 | { 498 | code: exportedCalledFunctionCode, 499 | filename: '/some/dir/bar.js', 500 | errors: [ 501 | { 502 | message: 503 | "Filename 'bar' must match the exported name 'foo'.", 504 | column: 1, 505 | line: 1, 506 | }, 507 | ], 508 | options: [null, null, true], 509 | }, 510 | ], 511 | }); 512 | -------------------------------------------------------------------------------- /tests/lib/rules/default-import-match-filename.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @author Chiawen Chen (github: @golopot) 3 | */ 4 | 5 | const { RuleTester } = require('eslint'); 6 | 7 | const rule = require('../../../lib/rules/default-import-match-filename'); 8 | 9 | const path = require('path'); 10 | 11 | function testFilePath(relativePath) { 12 | return path.join(process.cwd(), './tests/files', relativePath); 13 | } 14 | 15 | const ruleTester = new RuleTester({ 16 | parserOptions: { 17 | sourceType: 'module', 18 | ecmaVersion: 6, 19 | }, 20 | }); 21 | 22 | function getMessage(filename) { 23 | return `Default import name does not match filename "${filename}".`; 24 | } 25 | 26 | /** 27 | * @param {string} code 28 | * @param {string} expectedFilename 29 | * @param {string} [filename] 30 | */ 31 | function fail(code, expectedFilename, filename) { 32 | return { 33 | code, 34 | errors: [ 35 | { 36 | message: getMessage(expectedFilename), 37 | }, 38 | ], 39 | filename, 40 | }; 41 | } 42 | 43 | const parserOptions = { 44 | ecmaVersion: 6, 45 | sourceType: 'module', 46 | }; 47 | 48 | ruleTester.run('default-import-name', rule, { 49 | valid: [ 50 | 'import Cat from "./cat"', 51 | 'import cat from "./cat"', 52 | 'import cat from "./Cat"', 53 | 'import Cat from "./Cat"', 54 | 'import cat from "./cat.js"', 55 | 'import cat from "./cat.ts"', 56 | 'import cat from "./cat.jpeg"', 57 | 'import cat from ".cat"', 58 | 'import cat_ from "./cat"', 59 | 'import cat from "./cat/index"', 60 | 'import cat from "./cat/index.js"', 61 | 'import cat from "./cat/index.css"', 62 | 'import cat from "../cat/index.js"', 63 | 'import merge from "lodash/merge"', 64 | 'import cat from "/cat.js"', // absolute path 65 | 'import cat from "C:\\cat.js"', 66 | 'import cat from "C:/cat.js"', 67 | 'import loudCat from "./loud-cat"', 68 | 'import LOUDCAT from "./loud-cat"', 69 | 'import loud_cat from "./loud-cat"', 70 | 'import loudcat from "./loud_cat"', 71 | 'import loud_cat from "./loud_cat"', 72 | 'import loudCat from "./loud_cat"', 73 | 'import catModel from "./cat.model"', 74 | 'import catModel from "./cat.model.js"', 75 | 'import doge from "cat"', 76 | 'import doge from "loud-cat"', 77 | 'import doge from ".cat"', 78 | 'import doge from ""', 79 | 'import whatever from "@foo/bar"', 80 | 'import {doge} from "./cat"', 81 | 'import cat, {doge} from "./cat"', 82 | 'const cat = require("./cat")', 83 | 'const cat = require("../cat")', 84 | 'const cat = require("./cat/index")', 85 | 'const cat = require("./cat/index.js")', 86 | 'const doge = require("cat")', 87 | 'const {f, g} = require("./cat")', 88 | { 89 | code: `import whatever from './ignored/foo.js'`, 90 | filename: testFilePath('default-import-match-filename/main.js'), 91 | options: [{ ignorePaths: ['**/ignored/*.js'] }], 92 | }, 93 | { 94 | code: `import whatever from '../ignored/foo.js'`, 95 | filename: testFilePath( 96 | 'default-import-match-filename/some-directory/a.js' 97 | ), 98 | options: [{ ignorePaths: ['**/ignored/*.js'] }], 99 | }, 100 | { 101 | code: `import whatever from './ignored/foo.js'`, 102 | filename: testFilePath('default-import-match-filename/main.js'), 103 | options: [{ ignorePaths: ['**/foo.js'] }], 104 | }, 105 | { 106 | code: `import whatever from './some-directory/a.js'`, 107 | filename: testFilePath('default-import-match-filename/main.js'), 108 | // This test should be ran with project root as process.cwd(). 109 | options: [ 110 | { 111 | ignorePaths: [ 112 | 'tests/files/default-import-match-filename/some-directory/a.js', 113 | ], 114 | }, 115 | ], 116 | }, 117 | { 118 | code: ` 119 | import someDirectory from "."; 120 | import someDirectory_ from "./"; 121 | const someDirectory__ = require('.'); 122 | `, 123 | filename: testFilePath( 124 | 'default-import-match-filename/some-directory/a.js' 125 | ), 126 | }, 127 | { 128 | code: ` 129 | import packageName from ".."; 130 | import packageName_ from "../"; 131 | import packageName__ from "./.."; 132 | import packageName___ from "./../"; 133 | const packageName____ = require('..'); 134 | `, 135 | filename: testFilePath( 136 | 'default-import-match-filename/some-directory/a.js' 137 | ), 138 | }, 139 | { 140 | code: 'import doge from "../index.js"', 141 | filename: 'doge/a/a.js', 142 | }, 143 | { 144 | code: 'import JordanHarband from "./JordanHarband";', 145 | parserOptions, 146 | }, 147 | { 148 | code: 'import JordanHarband from "./JordanHarband.js";', 149 | parserOptions, 150 | }, 151 | { 152 | code: 'import JordanHarband from "./JordanHarband.jsx";', 153 | parserOptions, 154 | }, 155 | { 156 | code: 'import JordanHarband from "/some/path/to/JordanHarband.js";', 157 | parserOptions, 158 | }, 159 | { 160 | code: 'import JordanHarband from "/another/path/to/JordanHarband.js";', 161 | parserOptions, 162 | }, 163 | { 164 | code: 'import JordanHarband from "/another/path/to/jordanHarband.js";', 165 | parserOptions, 166 | }, 167 | { 168 | code: 'import JordanHarband from "/another/path/to/jordanHarband.jsx";', 169 | parserOptions, 170 | }, 171 | { 172 | code: 'import JordanHarband from "./JordanHarband/index.js";', 173 | parserOptions, 174 | }, 175 | { 176 | code: 'import JordanHarband from "./JordanHarband/index.jsx";', 177 | parserOptions, 178 | }, 179 | { 180 | code: 'import TaeKim from "./TaeKim.ts";', 181 | parserOptions, 182 | settings: { 183 | 'import/extensions': ['.ts'], 184 | }, 185 | }, 186 | { 187 | code: 'import TaeKim from "./TaeKim.tsx";', 188 | parserOptions, 189 | settings: { 190 | 'import/extensions': ['.ts'], 191 | }, 192 | }, 193 | { 194 | code: 'import TaeKim from "./TaeKim.js";', 195 | parserOptions, 196 | settings: { 197 | 'import/extensions': ['.js', '.jsx', '.ts', '.tsx'], 198 | }, 199 | }, 200 | { 201 | code: 'import TaeKim from "./TaeKim.jsx";', 202 | parserOptions, 203 | settings: { 204 | 'import/extensions': ['.js', '.jsx', '.ts', '.tsx'], 205 | }, 206 | }, 207 | { 208 | code: 'import TaeKim from "./TaeKim.ts";', 209 | parserOptions, 210 | settings: { 211 | 'import/extensions': ['.js', '.jsx', '.ts', '.tsx'], 212 | }, 213 | }, 214 | { 215 | code: 'import TaeKim from "./TaeKim.tsx";', 216 | parserOptions, 217 | settings: { 218 | 'import/extensions': ['.js', '.jsx', '.ts', '.tsx'], 219 | }, 220 | }, 221 | { 222 | code: 'import JordanHarband from "path/to/something/JoRdAnHaRbAnD.jsx";', 223 | parserOptions, 224 | }, 225 | ], 226 | invalid: [ 227 | fail('import cat0 from "./cat"', 'cat'), 228 | fail('import catfish from "./cat"', 'cat'), 229 | fail('import catfish, {cat} from "./cat"', 'cat'), 230 | fail('import catModel from "./models/cat"', 'cat'), 231 | fail('import cat from "./cat.model.js"', 'cat.model.js'), 232 | fail('import doge from "./cat/index"', 'cat/'), 233 | fail('import doge from "./cat/index.js"', 'cat/'), 234 | fail('import doge from "../cat/index.js"', 'cat/'), 235 | fail('import doge from "../cat/index.css"', 'cat/'), 236 | fail('import doge from "lodash/merge"', 'merge'), 237 | fail('import doge from "lodash/a/b/c"', 'c'), 238 | fail('import doge from "@foo/bar/a"', 'a'), 239 | fail('import doge from "/cat"', 'cat'), 240 | fail('import cat7 from "./cat8"', 'cat8'), 241 | fail('const catfish = require("./cat")', 'cat'), 242 | fail('const doge = require("./cat/index")', 'cat/'), 243 | fail('const doge = require("./cat/index.js")', 'cat/'), 244 | fail('const doge = require("../models/cat")', 'cat'), 245 | fail( 246 | 'import nope from "."', 247 | 'some-directory', 248 | testFilePath('default-import-match-filename/some-directory/a.js') 249 | ), 250 | fail( 251 | 'import nope from ".."', 252 | 'package-name', 253 | testFilePath('default-import-match-filename/some-directory/a.js') 254 | ), 255 | fail( 256 | 'import nope from "../../index.js"', 257 | 'package-name', 258 | testFilePath( 259 | 'default-import-match-filename/some-directory/foo/a.js' 260 | ) 261 | ), 262 | { 263 | code: `import wrongName from './some-directory/a.js';`, 264 | output: `import wrongName from './some-directory/a.js';`, 265 | filename: testFilePath('default-import-match-filename/main.js'), 266 | options: [{ ignorePaths: ['**/b.js', 'a.js', './a.js'] }], 267 | errors: [{ message: getMessage('a.js') }], 268 | }, 269 | { 270 | code: 'import JordanHarband from "./NotJordanHarband.js";', 271 | output: 'import JordanHarband from "./NotJordanHarband.js";', 272 | parserOptions, 273 | errors: [ 274 | { 275 | message: getMessage('NotJordanHarband.js'), 276 | type: 'ImportDefaultSpecifier', 277 | }, 278 | ], 279 | }, 280 | { 281 | code: 'import JordanHarband from "./NotJordanHarband.jsx";', 282 | output: 'import JordanHarband from "./NotJordanHarband.jsx";', 283 | parserOptions, 284 | errors: [ 285 | { 286 | message: getMessage('NotJordanHarband.jsx'), 287 | type: 'ImportDefaultSpecifier', 288 | }, 289 | ], 290 | }, 291 | { 292 | code: 'import JordanHarband from "./NotJordanHarband/index.js";', 293 | output: 'import JordanHarband from "./NotJordanHarband/index.js";', 294 | parserOptions, 295 | errors: [ 296 | { 297 | message: getMessage('NotJordanHarband/'), 298 | type: 'ImportDefaultSpecifier', 299 | }, 300 | ], 301 | }, 302 | { 303 | code: 'import JordanHarband from "./JordanHarband/foobar.js";', 304 | output: 'import JordanHarband from "./JordanHarband/foobar.js";', 305 | parserOptions, 306 | errors: [ 307 | { 308 | message: getMessage('foobar.js'), 309 | type: 'ImportDefaultSpecifier', 310 | }, 311 | ], 312 | }, 313 | { 314 | code: 'import JordanHarband from "/path/to/JordanHarbandReducer.js";', 315 | output: 'import JordanHarband from "/path/to/JordanHarbandReducer.js";', 316 | parserOptions, 317 | errors: [ 318 | { 319 | message: getMessage('JordanHarbandReducer.js'), 320 | type: 'ImportDefaultSpecifier', 321 | }, 322 | ], 323 | }, 324 | ], 325 | }); 326 | -------------------------------------------------------------------------------- /tests/setup.js: -------------------------------------------------------------------------------- 1 | // const sinon = require('sinon'); 2 | // const { beforeEach, afterEach } = require('mocha'); 3 | 4 | // beforeEach(function () { 5 | // sinon.stub(process, 'cwd').returns('/foo'); 6 | // }); 7 | 8 | // afterEach(function () { 9 | // process.cwd.restore(); 10 | // }); 11 | --------------------------------------------------------------------------------