├── .editorconfig ├── .eslintrc.js ├── .gitattributes ├── .gitignore ├── .husky └── pre-push ├── .npmignore ├── .prettierrc ├── README.md ├── lib ├── index.js ├── rules │ └── file-extension-in-import-ts.js └── util │ ├── get-resolve-paths.js │ ├── import-target.js │ ├── mapping-extensions.js │ ├── strip-import-path-params.js │ └── visit-import.js ├── package-lock.json └── package.json /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_size = 2 5 | indent_style = space 6 | max_line_length = 120 7 | tab_width = 2 8 | end_of_line = lf 9 | charset = utf-8 10 | trim_trailing_whitespace = true 11 | insert_final_newline = true 12 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | const defaultEnv = 'production'; 2 | const supportedEnvs = ['development', 'production']; 3 | const currentEnv = supportedEnvs.includes(process.env.NODE_ENV) ? process.env.NODE_ENV : defaultEnv; 4 | const isDevelopment = currentEnv === 'development'; 5 | 6 | module.exports = { 7 | root: true, 8 | env: { 9 | node: true, 10 | jest: true, 11 | }, 12 | ignorePatterns: ['.eslintrc.js'], 13 | plugins: ['import', 'unicorn', 'sort-keys-fix'], 14 | extends: ['airbnb-base', 'plugin:import/recommended', 'eslint:recommended', 'plugin:prettier/recommended'], 15 | settings: { 16 | 'import/resolver': { 17 | node: { 18 | moduleDirectory: ['node_modules', 'lib'], 19 | extensions: ['.js', '.jsx'], 20 | }, 21 | }, 22 | }, 23 | overrides: [ 24 | { 25 | files: ['jest.config.ts'], 26 | rules: { 27 | 'import/no-default-export': 'off', 28 | }, 29 | }, 30 | { 31 | files: ['**/**/*.json'], 32 | rules: { 33 | 'no-unused-expressions': 'off', 34 | 'prettier/prettier': 'off', 35 | }, 36 | }, 37 | ], 38 | rules: { 39 | 'global-require': 'off', 40 | 'no-await-in-loop': 'off', 41 | 'newline-before-return': 'error', 42 | camelcase: ['error', { properties: 'always' }], 43 | 'no-param-reassign': 'off', 44 | 'class-methods-use-this': 'off', 45 | 'no-underscore-dangle': 'off', 46 | 'no-unused-vars': isDevelopment 47 | ? 'off' 48 | : [ 49 | 'error', 50 | { 51 | vars: 'all', 52 | args: 'after-used', 53 | ignoreRestSiblings: false, 54 | }, 55 | ], 56 | 'no-alert': isDevelopment ? 'off' : 'error', 57 | 'no-console': isDevelopment ? 'off' : 'error', 58 | 'no-debugger': isDevelopment ? 'off' : 'error', 59 | 60 | 'prettier/prettier': [ 61 | 'error', 62 | { 63 | singleQuote: true, 64 | useTabs: false, 65 | semi: true, 66 | trailingComma: 'all', 67 | bracketSpacing: true, 68 | printWidth: 120, 69 | endOfLine: 'lf', 70 | }, 71 | ], 72 | 73 | 'sort-keys-fix/sort-keys-fix': 'warn', 74 | 75 | 'import/order': [ 76 | 'error', 77 | { 78 | groups: ['builtin', 'external', 'internal', 'parent', 'sibling', 'index', 'object'], 79 | pathGroups: [ 80 | { 81 | pattern: '@', 82 | group: 'internal', 83 | position: 'after', 84 | }, 85 | ], 86 | 'newlines-between': 'always', 87 | alphabetize: { 88 | order: 'asc', 89 | }, 90 | }, 91 | ], 92 | 'import/prefer-default-export': 'off', 93 | 'import/no-default-export': 'error', 94 | 95 | 'unicorn/filename-case': [ 96 | 'error', 97 | { 98 | case: 'kebabCase', 99 | }, 100 | ], 101 | 'unicorn/throw-new-error': 'error', 102 | 'unicorn/no-instanceof-array': 'error', 103 | 'unicorn/prefer-node-protocol': 'error', 104 | 'unicorn/prefer-keyboard-event-key': 'error', 105 | 'unicorn/error-message': 'error', 106 | 'unicorn/empty-brace-spaces': 'error', 107 | 'unicorn/custom-error-definition': 'error', 108 | }, 109 | }; 110 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto eol=lf 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # GIT 2 | .git 3 | 4 | # Logs 5 | logs 6 | *.log 7 | npm-debug.log* 8 | yarn-debug.log* 9 | yarn-error.log* 10 | 11 | # ide 12 | .idea 13 | .vscode 14 | 15 | # Runtime data 16 | pids 17 | *.pid 18 | *.seed 19 | *.pid.lock 20 | 21 | # Directory for instrumented libs generated by jscoverage/JSCover 22 | lib-cov 23 | 24 | # Coverage directory used by tools like istanbul 25 | coverage 26 | 27 | # nyc test coverage 28 | .nyc_output 29 | 30 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 31 | .grunt 32 | 33 | # Bower dependency directory (https://bower.io/) 34 | bower_components 35 | 36 | # node-waf configuration 37 | .lock-wscript 38 | 39 | # Compiled binary addons (https://nodejs.org/api/addons.html) 40 | build/Release 41 | 42 | # Dependency directories 43 | node_modules/ 44 | jspm_packages/ 45 | 46 | # TypeScript v1 declaration files 47 | typings/ 48 | 49 | # Optional npm cache directory 50 | .npm 51 | 52 | # Optional eslint cache 53 | .eslintcache 54 | 55 | # Optional REPL history 56 | .node_repl_history 57 | 58 | # Output of 'npm pack' 59 | *.tgz 60 | 61 | # Yarn Integrity file 62 | .yarn-integrity 63 | 64 | # dotenv environment variables file 65 | *.env 66 | 67 | # next.js build output 68 | .next 69 | 70 | # vuepress build output 71 | .vuepress/dist 72 | 73 | # Dist 74 | dist 75 | build 76 | *.map 77 | 78 | # Caches 79 | .module-cache 80 | 81 | # Serverless directories 82 | .serverless 83 | 84 | # Extra 85 | NO_COMMIT 86 | 87 | # OS 88 | Thumbs.db 89 | .DS_Store 90 | 91 | # Test reports 92 | test-reports 93 | 94 | # Custom config for prettier and eslint 95 | .eslintrc.custom.js 96 | .prettierrc.custom.js 97 | 98 | # dotenv file ought to ignore 99 | .env 100 | 101 | # Custom rules 102 | .clinic 103 | 104 | # Database 105 | prisma/dev.db 106 | prisma/dev.db-journal 107 | -------------------------------------------------------------------------------- /.husky/pre-push: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | . "$(dirname -- "$0")/_/husky.sh" 3 | 4 | npm run format:package && npx git-is-ready-to-push && npm run lint 5 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | # GIT 2 | .git 3 | 4 | # GIT Hooks 5 | .husky 6 | 7 | # Logs 8 | logs 9 | *.log 10 | npm-debug.log* 11 | yarn-debug.log* 12 | yarn-error.log* 13 | 14 | # ide 15 | .idea 16 | .vscode 17 | 18 | # Runtime data 19 | pids 20 | *.pid 21 | *.seed 22 | *.pid.lock 23 | 24 | # Directory for instrumented libs generated by jscoverage/JSCover 25 | lib-cov 26 | 27 | # Coverage directory used by tools like istanbul 28 | coverage 29 | 30 | # nyc test coverage 31 | .nyc_output 32 | 33 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 34 | .grunt 35 | 36 | # Bower dependency directory (https://bower.io/) 37 | bower_components 38 | 39 | # node-waf configuration 40 | .lock-wscript 41 | 42 | # Compiled binary addons (https://nodejs.org/api/addons.html) 43 | build/Release 44 | 45 | # Dependency directories 46 | node_modules 47 | jspm_packages 48 | 49 | # Optional npm cache directory 50 | .npm 51 | 52 | # Optional eslint cache 53 | .eslintcache 54 | 55 | # Optional REPL history 56 | .node_repl_history 57 | 58 | # Output of 'npm pack' 59 | *.tgz 60 | 61 | # Yarn Integrity file 62 | .yarn-integrity 63 | 64 | # dotenv environment variables file 65 | *.env 66 | 67 | # next.js build output 68 | .next 69 | 70 | # vuepress build output 71 | .vuepress/dist 72 | 73 | # Dist 74 | dist 75 | build 76 | *.map 77 | 78 | # Caches 79 | .module-cache 80 | 81 | # Serverless directories 82 | .serverless 83 | 84 | # Extra 85 | NO_COMMIT 86 | 87 | # OS 88 | Thumbs.db 89 | .DS_Store 90 | 91 | # Test reports 92 | reports 93 | test-reports 94 | 95 | # Custom config for prettier and eslint 96 | .eslintrc.custom.js 97 | .prettierrc.custom.js 98 | 99 | # dotenv file ought to ignore 100 | .env 101 | 102 | # NPM rules 103 | documentation 104 | example 105 | examples 106 | 107 | # Custom rules 108 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "trailingComma": "all", 4 | "useTabs": false, 5 | "semi": true, 6 | "bracketSpacing": true, 7 | "printWidth": 120, 8 | "endOfLine": "lf" 9 | } 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # eslint-plugin-file-extension-in-import-ts 2 | 3 | ## Disclaimer 4 | 5 | This is small patch to the great rule: 6 | 7 | **file-extension-in-import** 8 | 9 | from [eslint-plugin-node](https://github.com/mysticatea/eslint-plugin-node/) module. 10 | 11 | Author of code: 12 | 13 | [mysticatea](https://github.com/mysticatea) 14 | 15 | I've just added mapping functionality. 16 | 17 | ## Motivation 18 | 19 | When we use type module we must add extension to the file 20 | 21 | ```js 22 | import foo from "./path/to/a/file" 23 | ``` 24 | It's a bad practice 25 | 26 | ```js 27 | import eslint from "eslint" 28 | import foo from "./path/to/a/file.js" 29 | ``` 30 | 31 | It's a good practice. 32 | 33 | **node/file-extension-in-import** rule raise error when we forgot to add extension to the import. But it doesn't work with Typescript. 34 | 35 | [Full description of rule here](https://github.com/mysticatea/eslint-plugin-node/blob/master/docs/rules/file-extension-in-import.md) 36 | 37 | ### Typescript usage 38 | 39 | When we use Typescript we can't add .ts extension to the file. It won't be process with Typescript compiler. In this case we need to add .js extension. 40 | 41 | In this case we need to replace .ts extension to .js. 42 | 43 | In this plugin I will add possibility to make mapping for extensions: 44 | 45 | ```js 46 | 'file-extension-in-import-ts/file-extension-in-import-ts': [ 47 | 'error', 48 | 'always', 49 | { extMapping: {'.ts': '.js' } } 50 | ] 51 | ``` 52 | 53 | It means, this rule will fix our ts files to .js extension in import statement. 54 | 55 | ```js 56 | import module from './my-module'; 57 | ``` 58 | 59 | will be changed to: 60 | 61 | ```js 62 | import module from './my-module/index.js'; 63 | ``` 64 | 65 | ## Installation: 66 | 67 | 1. Install the package: 68 | ```shell 69 | npm i -D eslint-plugin-file-extension-in-import-ts 70 | ``` 71 | 72 | 2. Add to .eslintrc.js: 73 | ```json 74 | { 75 | "plugins": [ 76 | "file-extension-in-import-ts" 77 | ], 78 | "rules": { 79 | "file-extension-in-import-ts/file-extension-in-import-ts": "error" 80 | } 81 | } 82 | ``` 83 | 84 | By default, the plugin will process mapping: 85 | 86 | - .ts -> .js 87 | - .tsx -> .js 88 | 89 | ## Troubleshooting 90 | 91 | This plugin has conflict with [eslint-import-plugin](https://www.npmjs.com/package/eslint-plugin-import), the rule: *import/no-unresolved* rule 92 | 93 | To quick fix it we can skip '.js' (in the folder with TS sources JS files are not existing). 94 | 95 | ```js 96 | 'import/no-unresolved': ['error', { ignore: [ '\\.js$' ] }], 97 | ``` 98 | 99 | ## The MIT License 100 | 101 | Copyright (c) 102 | - [mysticatea](https://github.com/mysticatea) 103 | - [alexsergey](https://github.com/AlexSergey) 104 | 105 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated 106 | documentation files (the “Software”), to deal in the Software without restriction, including without limitation the 107 | rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit 108 | persons to whom the Software is furnished to do so, subject to the following conditions: 109 | 110 | THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE 111 | WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 112 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR 113 | OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 114 | -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | rules: { 3 | 'file-extension-in-import-ts': require('./rules/file-extension-in-import-ts'), 4 | }, 5 | }; 6 | -------------------------------------------------------------------------------- /lib/rules/file-extension-in-import-ts.js: -------------------------------------------------------------------------------- 1 | const fs = require('node:fs'); 2 | const path = require('node:path'); 3 | 4 | const mappingExtensions = require('../util/mapping-extensions'); 5 | const visitImport = require('../util/visit-import'); 6 | 7 | const packageNamePattern = /^(?:@[^/\\]+[/\\])?[^@~.]+$/u; 8 | const corePackageOverridePattern = 9 | /^(?:assert|async_hooks|buffer|child_process|cluster|console|constants|crypto|dgram|dns|domain|events|fs|http|http2|https|inspector|module|net|os|path|perf_hooks|process|punycode|querystring|readline|repl|stream|string_decoder|sys|timers|tls|trace_events|tty|url|util|v8|vm|worker_threads|zlib)[/\\]$/u; 10 | 11 | /** 12 | * Get all file extensions of the files which have the same basename. 13 | * @param {string} filePath The path to the original file to check. 14 | * @returns {string[]} File extensions. 15 | */ 16 | function getExistingExtensions(filePath, extMappingList) { 17 | const basename = fs.existsSync(filePath) ? path.basename(filePath, path.extname(filePath)) : path.basename(filePath); 18 | 19 | try { 20 | const isDirectory = fs.existsSync(filePath) && fs.statSync(filePath).isDirectory(); 21 | 22 | if (isDirectory) { 23 | // eslint-disable-next-line no-plusplus 24 | for (let i = 0, l = extMappingList.length; i < l; i++) { 25 | const ext = extMappingList[i]; 26 | 27 | if (fs.existsSync(path.join(filePath, `/index${ext}`))) { 28 | return ['/index.js']; 29 | } 30 | } 31 | } 32 | 33 | return fs 34 | .readdirSync(path.dirname(filePath)) 35 | .filter((filename) => path.basename(filename, path.extname(filename)) === basename) 36 | .map((filename) => path.extname(filename)); 37 | } catch (_error) { 38 | return []; 39 | } 40 | } 41 | 42 | module.exports = { 43 | create(context) { 44 | if (context.getFilename().startsWith('<')) { 45 | return {}; 46 | } 47 | const defaultStyle = context.options[0] || 'always'; 48 | const overrideStyle = context.options[1] || {}; 49 | const extMapping = overrideStyle.extMapping || mappingExtensions.mappingDefault; 50 | const extMappingList = Object.keys(extMapping); 51 | 52 | function verify({ filePath, name, node }) { 53 | const isDirectory = fs.existsSync(filePath) && fs.statSync(filePath).isDirectory(); 54 | // Ignore if it's not resolved to a file or it's a bare module. 55 | if (!filePath || packageNamePattern.test(name) || corePackageOverridePattern.test(name)) { 56 | return; 57 | } 58 | 59 | // Get extension. 60 | const originalExt = path.extname(name); 61 | // eslint-disable-next-line no-nested-ternary 62 | const resolvedExt = isDirectory ? null : fs.existsSync(filePath) ? path.extname(filePath) : null; 63 | const existingExts = getExistingExtensions(filePath, extMappingList); 64 | if (!resolvedExt && existingExts.length !== 1) { 65 | // Ignore if the file extension could not be determined one. 66 | return; 67 | } 68 | const ext = mappingExtensions(resolvedExt || existingExts[0], extMapping); 69 | const style = overrideStyle[ext] || defaultStyle; 70 | // Verify. 71 | if (style === 'always' && ext !== originalExt) { 72 | context.report({ 73 | data: { ext }, 74 | fix(fixer) { 75 | if (existingExts.length !== 1) { 76 | return null; 77 | } 78 | const index = node.range[1] - 1; 79 | 80 | return fixer.insertTextBeforeRange([index, index], ext); 81 | }, 82 | messageId: 'requireExt', 83 | node, 84 | }); 85 | } else if (style === 'never' && ext === originalExt) { 86 | context.report({ 87 | data: { ext }, 88 | fix(fixer) { 89 | if (existingExts.length !== 1) { 90 | return null; 91 | } 92 | const index = name.lastIndexOf(ext); 93 | const start = node.range[0] + 1 + index; 94 | const end = start + ext.length; 95 | 96 | return fixer.removeRange([start, end]); 97 | }, 98 | messageId: 'forbidExt', 99 | node, 100 | }); 101 | } 102 | } 103 | 104 | return visitImport(context, { optionIndex: 1 }, (targets) => { 105 | targets.forEach(verify); 106 | }); 107 | }, 108 | meta: { 109 | docs: { 110 | category: 'Stylistic Issues', 111 | description: 'enforce the style of file extensions in `import` declarations', 112 | recommended: false, 113 | url: 'https://github.com/mysticatea/eslint-plugin-node/blob/v11.1.0/docs/rules/file-extension-in-import.md', 114 | }, 115 | fixable: 'code', 116 | messages: { 117 | forbidExt: "forbid file extension '{{ext}}'.", 118 | requireExt: "require file extension '{{ext}}'.", 119 | }, 120 | schema: [ 121 | { 122 | enum: ['always', 'never'], 123 | }, 124 | { 125 | additionalProperties: { 126 | enum: ['always', 'never'], 127 | }, 128 | properties: { 129 | extMapping: mappingExtensions.schema, 130 | }, 131 | type: 'object', 132 | }, 133 | ], 134 | type: 'suggestion', 135 | }, 136 | }; 137 | -------------------------------------------------------------------------------- /lib/util/get-resolve-paths.js: -------------------------------------------------------------------------------- 1 | const DEFAULT_VALUE = Object.freeze([]); 2 | 3 | /** 4 | * Gets `resolvePaths` property from a given option object. 5 | * 6 | * @param {object|undefined} option - An option object to get. 7 | * @returns {string[]|null} The `allowModules` value, or `null`. 8 | */ 9 | function get(option) { 10 | if (option && option.resolvePaths && Array.isArray(option.resolvePaths)) { 11 | return option.resolvePaths.map(String); 12 | } 13 | 14 | return null; 15 | } 16 | 17 | /** 18 | * Gets "resolvePaths" setting. 19 | * 20 | * 1. This checks `options` property, then returns it if exists. 21 | * 2. This checks `settings.node` property, then returns it if exists. 22 | * 3. This returns `[]`. 23 | * 24 | * @param {RuleContext} context - The rule context. 25 | * @returns {string[]} A list of extensions. 26 | */ 27 | module.exports = function getResolvePaths(context, optionIndex = 0) { 28 | return ( 29 | get(context.options && context.options[optionIndex]) || 30 | get(context.settings && context.settings.node) || 31 | DEFAULT_VALUE 32 | ); 33 | }; 34 | 35 | module.exports.schema = { 36 | items: { type: 'string' }, 37 | type: 'array', 38 | uniqueItems: true, 39 | }; 40 | -------------------------------------------------------------------------------- /lib/util/import-target.js: -------------------------------------------------------------------------------- 1 | const path = require('node:path'); 2 | 3 | const resolve = require('resolve'); 4 | 5 | /** 6 | * Resolve the given id to file paths. 7 | * @param {boolean} isModule The flag which indicates this id is a module. 8 | * @param {string} id The id to resolve. 9 | * @param {object} options The options of node-resolve module. 10 | * It requires `options.basedir`. 11 | * @returns {string|null} The resolved path. 12 | */ 13 | function getFilePath(isModule, id, options) { 14 | if (isModule) { 15 | return null; 16 | } 17 | try { 18 | return resolve.sync(id, options); 19 | } catch (_err) { 20 | return path.resolve(options.basedir, id); 21 | } 22 | } 23 | 24 | /** 25 | * Gets the module name of a given path. 26 | * 27 | * e.g. `eslint/lib/ast-utils` -> `eslint` 28 | * 29 | * @param {string} nameOrPath - A path to get. 30 | * @returns {string} The module name of the path. 31 | */ 32 | function getModuleName(nameOrPath) { 33 | let end = nameOrPath.indexOf('/'); 34 | if (end !== -1 && nameOrPath[0] === '@') { 35 | end = nameOrPath.indexOf('/', 1 + end); 36 | } 37 | 38 | return end === -1 ? nameOrPath : nameOrPath.slice(0, end); 39 | } 40 | 41 | /** 42 | * Information of an import target. 43 | */ 44 | module.exports = class ImportTarget { 45 | /** 46 | * Initialize this instance. 47 | * @param {ASTNode} node - The node of a `require()` or a module declaraiton. 48 | * @param {string} name - The name of an import target. 49 | * @param {object} options - The options of `node-resolve` module. 50 | */ 51 | constructor(node, name, options) { 52 | const isModule = !/^(?:[./\\]|\w+:)/u.test(name); 53 | 54 | /** 55 | * The node of a `require()` or a module declaraiton. 56 | * @type {ASTNode} 57 | */ 58 | this.node = node; 59 | 60 | /** 61 | * The name of this import target. 62 | * @type {string} 63 | */ 64 | this.name = name; 65 | 66 | /** 67 | * The full path of this import target. 68 | * If the target is a module and it does not exist then this is `null`. 69 | * @type {string|null} 70 | */ 71 | this.filePath = getFilePath(isModule, name, options); 72 | 73 | /** 74 | * The module name of this import target. 75 | * If the target is a relative path then this is `null`. 76 | * @type {string|null} 77 | */ 78 | this.moduleName = isModule ? getModuleName(name) : null; 79 | } 80 | }; 81 | -------------------------------------------------------------------------------- /lib/util/mapping-extensions.js: -------------------------------------------------------------------------------- 1 | const mappingExtensions = (ext, mapping) => { 2 | if (mapping[ext]) { 3 | return mapping[ext]; 4 | } 5 | 6 | return ext; 7 | }; 8 | 9 | module.exports = mappingExtensions; 10 | 11 | module.exports.schema = { type: 'object' }; 12 | 13 | module.exports.mappingDefault = { 14 | '.ts': '.js', 15 | '.tsx': '.js', 16 | }; 17 | -------------------------------------------------------------------------------- /lib/util/strip-import-path-params.js: -------------------------------------------------------------------------------- 1 | module.exports = function stripImportPathParams(path) { 2 | const i = path.indexOf('!'); 3 | 4 | return i === -1 ? path : path.slice(0, i); 5 | }; 6 | -------------------------------------------------------------------------------- /lib/util/visit-import.js: -------------------------------------------------------------------------------- 1 | const path = require('node:path'); 2 | 3 | const isCoreModule = require('is-core-module'); 4 | 5 | const getResolvePaths = require('./get-resolve-paths'); 6 | const ImportTarget = require('./import-target'); 7 | const stripImportPathParams = require('./strip-import-path-params'); 8 | 9 | /** 10 | * Gets a list of `import`/`export` declaration targets. 11 | * 12 | * Core modules of Node.js (e.g. `fs`, `http`) are excluded. 13 | * 14 | * @param {RuleContext} context - The rule context. 15 | * @param {Object} [options] - The flag to include core modules. 16 | * @param {boolean} [options.includeCore] - The flag to include core modules. 17 | * @param {number} [options.optionIndex] - The index of rule options. 18 | * @param {function(ImportTarget[]):void} callback The callback function to get result. 19 | * @returns {ImportTarget[]} A list of found target's information. 20 | */ 21 | // eslint-disable-next-line default-param-last 22 | module.exports = function visitImport(context, { includeCore = false, optionIndex = 0 } = {}, callback) { 23 | const targets = []; 24 | const basedir = path.dirname(path.resolve(context.getFilename())); 25 | const paths = getResolvePaths(context, optionIndex); 26 | const options = { basedir, paths }; 27 | 28 | return { 29 | [['ExportAllDeclaration', 'ExportNamedDeclaration', 'ImportDeclaration', 'ImportExpression']](node) { 30 | const sourceNode = node.source; 31 | 32 | // skip `import(foo)` 33 | if (node.type === 'ImportExpression' && sourceNode && sourceNode.type !== 'Literal') { 34 | return; 35 | } 36 | 37 | const name = sourceNode && stripImportPathParams(sourceNode.value); 38 | // Note: "999" arbitrary to check current/future Node.js version 39 | if ( 40 | name && 41 | node.importKind !== 'type' && 42 | node.exportKind !== 'type' && 43 | (includeCore || !isCoreModule(name, '999')) 44 | ) { 45 | targets.push(new ImportTarget(sourceNode, name, options)); 46 | } 47 | }, 48 | 49 | 'Program:exit': function exit() { 50 | callback(targets); 51 | }, 52 | }; 53 | }; 54 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "eslint-plugin-file-extension-in-import-ts", 3 | "version": "2.1.1", 4 | "description": "The plugin check and fix file extensions in Node.js application type module in Typescript", 5 | "keywords": [], 6 | "homepage": "https://github.com/AlexSergey/eslint-plugin-file-extension-in-import-ts#readme", 7 | "bugs": { 8 | "url": "https://github.com/AlexSergey/eslint-plugin-file-extension-in-import-ts/issues" 9 | }, 10 | "repository": { 11 | "type": "git", 12 | "url": "git+https://github.com/AlexSergey/eslint-plugin-file-extension-in-import-ts.git" 13 | }, 14 | "license": "ISC", 15 | "author": "", 16 | "main": "lib/index.js", 17 | "scripts": { 18 | "format": "npm run format:package && npm run format:prettier && npm run format:eslint", 19 | "format:eslint": "eslint ./lib/** --fix", 20 | "format:package": "sort-package-json", 21 | "format:prettier": "prettier --write \"lib/**/*.js\"", 22 | "lint": "npm run lint:code", 23 | "lint:code": "eslint ./lib/**", 24 | "prepare": "husky install" 25 | }, 26 | "dependencies": { 27 | "is-core-module": "^2.13.1", 28 | "resolve": "^1.22.8" 29 | }, 30 | "devDependencies": { 31 | "eslint": "^8.54.0", 32 | "eslint-config-airbnb": "^19.0.4", 33 | "eslint-config-prettier": "^8.10.0", 34 | "eslint-plugin-import": "^2.29.0", 35 | "eslint-plugin-prettier": "^5.0.1", 36 | "eslint-plugin-sort-keys-fix": "^1.1.2", 37 | "eslint-plugin-unicorn": "^49.0.0", 38 | "git-is-ready-to-push": "^1.0.0", 39 | "husky": "^8.0.3", 40 | "prettier": "^3.1.0", 41 | "sort-package-json": "^1.57.0" 42 | } 43 | } 44 | --------------------------------------------------------------------------------