diff --git a/example/arkham.js b/example/arkham.js new file mode 100644 index 0000000..e69de29 diff --git a/example/arkham/index.js b/example/arkham/index.js new file mode 100644 index 0000000..e69de29 diff --git a/example/batcave.ts b/example/batcave.ts new file mode 100644 index 0000000..e69de29 diff --git a/example/batcave/index.ts b/example/batcave/index.ts new file mode 100644 index 0000000..e69de29 diff --git a/index.js b/index.js index b17b28d..b355aa5 100644 --- a/index.js +++ b/index.js @@ -1,5 +1,5 @@ const { existsSync, lstatSync } = require('fs'); -const { dirname, resolve } = require('path'); +const { join, dirname, resolve, posix, extname, basename } = require('path'); module.exports = { configs: { @@ -13,7 +13,9 @@ module.exports = { }, rules: { 'require-extensions': rule((context, node, path) => { - if (!existsSync(path)) { + const fileExt = extname(context.getFilename()); + + if (!existsSync(path) || existsSync(`${path}${fileExt}`)) { let fix; if (!node.source.value.includes('?')) { fix = (fixer) => { @@ -29,12 +31,18 @@ module.exports = { } }), 'require-index': rule((context, node, path) => { - if (existsSync(path) && lstatSync(path).isDirectory()) { + const fileExt = extname(context.getFilename()); + const conflictingFile = join(dirname(path), `${basename(path)}${fileExt}`); + + if (existsSync(path) && lstatSync(path).isDirectory() && !existsSync(conflictingFile)) { context.report({ node, message: 'Directory paths must end with index.js', fix(fixer) { - return fixer.replaceText(node.source, `'${node.source.value}/index.js'`); + const { value: source } = node.source; + + const prefix = source.startsWith('./') || source === '.' ? './' : ''; + return fixer.replaceText(node.source, `'${prefix}${posix.join(source, 'index.js')}'`); }, }); } @@ -52,7 +60,9 @@ function rule(check) { const source = node.source; if (!source) return; const value = source.value.replace(/\?.*$/, ''); - if (!value || !value.startsWith('.') || value.endsWith('.js')) return; + + const validExtensions = ['.js', '.jsx', '.cjs', '.mjs']; + if (!value || !value.startsWith('.') || validExtensions.some(ext => value.endsWith(ext))) return; check(context, node, resolve(dirname(context.getFilename()), value)); } diff --git a/package-lock.json b/package-lock.json index eb621e3..8f28da3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "eslint-plugin-require-extensions", - "version": "0.1.2", + "version": "0.1.3", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "eslint-plugin-require-extensions", - "version": "0.1.2", + "version": "0.1.3", "license": "Apache-2.0", "devDependencies": { "eslint": "^8.23.0", diff --git a/package.json b/package.json index 20876a5..91b8ee2 100644 --- a/package.json +++ b/package.json @@ -5,9 +5,6 @@ "repository": "https://github.com/solana-labs/eslint-plugin-require-extensions", "license": "Apache-2.0", "type": "commonjs", - "scripts": { - "test": "eslint . --report-unused-disable-directives" - }, "engines": { "node": ">=16" }, @@ -22,7 +19,7 @@ ], "scripts": { "fmt": "prettier --write '{*,**/*}.{ts,tsx,js,jsx,json}'", - "test": "eslint . --report-unused-disable-directives" + "test": "node --test && eslint . --report-unused-disable-directives" }, "publishConfig": { "access": "public" diff --git a/tests/index.test.js b/tests/index.test.js new file mode 100644 index 0000000..759763b --- /dev/null +++ b/tests/index.test.js @@ -0,0 +1,150 @@ +const { describe, it } = require('node:test'); +const path = require('node:path'); +const { RuleTester } = require('eslint'); +const { rules } = require('../index'); + +const ruleTester = new RuleTester({ parserOptions: { ecmaVersion: 2020, sourceType: 'module' } }); +const filename = path.join(path.dirname(__dirname), 'example/index.js'); + +global.describe = describe; +global.it = it; + +describe('eslint-plugin-require-extensions', () => { + ruleTester.run('require-extensions', rules['require-extensions'], { + valid: [ + { + name: 'import with extension', + code: "import test from './dir/index.js'", + filename, + }, + { + name: 'package import', + code: "import batcave from '@wayne/foundation'", + filename, + }, + { + name: 'import jsx', + code: "import test from './joker.jsx'", + filename, + }, + { + name: 'import cjs', + code: "import test from './joker.cjs'", + filename, + }, + { + name: 'import mjs', + code: "import test from './joker.mjs'", + filename, + }, + ], + invalid: [ + { + name: 'import without extension', + code: "import test from './dir/index'", + output: "import test from './dir/index.js'", + errors: ['Relative imports and exports must end with .js'], + filename, + }, + { + name: 'file with sibling folder of same name', + code: "import arkham from './arkham'", + output: "import arkham from './arkham.js'", + errors: ['Relative imports and exports must end with .js'], + filename, + }, + { + name: 'typescript file with sibling folder of same name', + code: "import batcave from './batcave'", + output: "import batcave from './batcave.js'", + errors: ['Relative imports and exports must end with .js'], + filename: path.join(path.dirname(__dirname), 'example/index.ts'), + }, + ], + }); + + ruleTester.run('require-index', rules['require-index'], { + valid: [ + { + name: 'import from index.js', + code: "import test from './dir/index.js'", + filename, + }, + { + name: 'package import', + code: "import batcave from '@wayne/foundation'", + filename, + }, + { + name: 'bail on import from file with sibling folder of same name', + code: "import arkham from './arkham'", + filename, + }, + ], + invalid: [ + { + name: 'import without index', + code: "import './dir'", + output: "import './dir/index.js'", + errors: ['Directory paths must end with index.js'], + filename, + }, + { + name: 'export * without index', + code: "export * from './dir'", + output: "export * from './dir/index.js'", + errors: ['Directory paths must end with index.js'], + filename, + }, + { + name: 'export named without index', + code: "export { joker } from './dir'", + output: "export { joker } from './dir/index.js'", + errors: ['Directory paths must end with index.js'], + filename, + }, + { + name: "import from '../'", + code: "import plugin from '../'", + output: "import plugin from '../index.js'", + errors: ['Directory paths must end with index.js'], + filename, + }, + { + name: "import from '..'", + code: "import plugin from '..'", + output: "import plugin from '../index.js'", + errors: ['Directory paths must end with index.js'], + filename, + }, + { + name: "import from './'", + code: "import index from './'", + output: "import index from './index.js'", + errors: ['Directory paths must end with index.js'], + filename: path.join(path.dirname(__dirname), 'example/other.js'), + }, + { + name: "import from '.'", + code: "import index from '.'", + output: "import index from './index.js'", + errors: ['Directory paths must end with index.js'], + filename: path.join(path.dirname(__dirname), 'example/other.js'), + }, + { + name: 'named import without index', + code: "import { batmobile } from './dir'", + output: "import { batmobile } from './dir/index.js'", + errors: ['Directory paths must end with index.js'], + filename, + }, + { + name: 'default import without index', + code: "import batmobile from './dir'", + output: "import batmobile from './dir/index.js'", + errors: ['Directory paths must end with index.js'], + filename, + }, + ], + }); +});