Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ This plugin intends to support linting of ES2015+ (ES6+) import/export syntax, a
| [consistent-type-specifier-style](docs/rules/consistent-type-specifier-style.md) | Enforce or ban the use of inline type-only markers for named imports. | | | | 🔧 | | |
| [dynamic-import-chunkname](docs/rules/dynamic-import-chunkname.md) | Enforce a leading comment with the webpackChunkName for dynamic imports. | | | | | 💡 | |
| [exports-last](docs/rules/exports-last.md) | Ensure all exports appear after other statements. | | | | | | |
| [extensions](docs/rules/extensions.md) | Ensure consistent use of file extension within the import path. | | | | | | |
| [extensions](docs/rules/extensions.md) | Ensure consistent use of file extension within the import path. | | | | 🔧 | | |
| [first](docs/rules/first.md) | Ensure all imports appear before other statements. | | | | 🔧 | | |
| [group-exports](docs/rules/group-exports.md) | Prefer named exports to be grouped together in a single export declaration | | | | | | |
| [imports-first](docs/rules/imports-first.md) | Replaced by `import/first`. | | | | 🔧 | | ❌ |
Expand Down
2 changes: 2 additions & 0 deletions docs/rules/extensions.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# import/extensions

🔧 This rule is automatically fixable by the [`--fix` CLI option](https://eslint.org/docs/latest/user-guide/command-line-interface#--fix).

<!-- end auto-generated rule header -->

Some file resolve algorithms allow you to omit the file extension within the import source path. For example the `node` resolver (which does not yet support ESM/`import`) can resolve `./foo/bar` to the absolute path `/User/someone/foo/bar.js` because the `.js` extension is resolved automatically by default in CJS. Depending on the resolver you can configure more extensions to get resolved automatically.
Expand Down
78 changes: 76 additions & 2 deletions src/rules/extensions.js
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ function buildProperties(context) {
defaultConfig: 'never',
pattern: {},
ignorePackages: false,
fix: false,
};

context.options.forEach((obj) => {
Expand All @@ -72,6 +73,11 @@ function buildProperties(context) {
result.ignorePackages = obj.ignorePackages;
}

// If fix is provided, transfer it to result
if (obj.fix !== undefined) {
result.fix = obj.fix;
}

if (obj.checkTypeImports !== undefined) {
result.checkTypeImports = obj.checkTypeImports;
}
Expand All @@ -97,7 +103,7 @@ module.exports = {
description: 'Ensure consistent use of file extension within the import path.',
url: docsUrl('extensions'),
},

fixable: 'code',
schema: {
anyOf: [
{
Expand Down Expand Up @@ -177,6 +183,33 @@ module.exports = {
}
}

function replaceImportPath(source, importPath) {
return source.replace(
/^(['"])(.+)\1$/,
(_, quote) => `${quote}${importPath}${quote}`,
)
}

const parsePath = (path) => {
const hashIndex = path.indexOf('#')
const queryIndex = path.indexOf('?')
const hasHash = hashIndex !== -1
const hash = hasHash ? path.slice(hashIndex) : ''
const hasQuery = queryIndex !== -1 && (!hasHash || queryIndex < hashIndex)
const query = hasQuery
? path.slice(queryIndex, hasHash ? hashIndex : undefined)
: ''
const pathname = hasQuery
? path.slice(0, queryIndex)
: hasHash
? path.slice(0, hashIndex)
: path
return { pathname, query, hash }
}

const stringifyPath = ({ pathname, query, hash }) =>
pathname + query + hash

function checkFileExtension(source, node) {
// bail if the declaration doesn't have a source, e.g. "export { foo };", or if it's only partially typed like in an editor
if (!source || !source.value) { return; }
Expand All @@ -196,7 +229,11 @@ module.exports = {
// don't enforce anything on builtins
if (!overrideAction && isBuiltIn(importPathWithQueryString, context.settings)) { return; }

const importPath = importPathWithQueryString.replace(/\?(.*)$/, '');
const {
pathname: importPath,
query,
hash,
} = parsePath(importPathWithQueryString)

// don't enforce in root external packages as they may have names with `.js`.
// Like `import Decimal from decimal.js`)
Expand All @@ -221,17 +258,54 @@ module.exports = {
const extensionRequired = isUseOfExtensionRequired(extension, !overrideAction && isPackage);
const extensionForbidden = isUseOfExtensionForbidden(extension);
if (extensionRequired && !extensionForbidden) {
const fixedImportPath = stringifyPath({
pathname: `${
/([\\/]|[\\/]?\.?\.)$/.test(importPath)
? `${
importPath.endsWith('/')
? importPath.slice(0, -1)
: importPath
}/index.${extension}`
: `${importPath}.${extension}`
}`,
query,
hash,
})
context.report({
node: source,
message:
`Missing file extension ${extension ? `"${extension}" ` : ''}for "${importPathWithQueryString}"`,
...props.fix && extension ? {
fix(fixer) {
return fixer.replaceText(
source,
replaceImportPath(source.raw, fixedImportPath),
);
},
} : {},
});
}
} else if (extension) {
if (isUseOfExtensionForbidden(extension) && isResolvableWithoutExtension(importPath)) {
context.report({
node: source,
message: `Unexpected use of file extension "${extension}" for "${importPathWithQueryString}"`,
...props.fix
? {
fix(fixer) {
return fixer.replaceText(
source,
replaceImportPath(
source.raw,
stringifyPath({
pathname: importPath.slice(0, -(extension.length + 1)),
query,
hash,
}),
),
);
},
} : {},
});
}
}
Expand Down
17 changes: 17 additions & 0 deletions tests/src/rules/extensions.js
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,16 @@ ruleTester.run('extensions', rule, {
].join('\n'),
options: ['always'],
}),

test({
code: "import foo from './foo';",
options: [{ fix: true }],
}),

test({
code: "import foo from './foo.js';",
options: [{ fix: true, pattern: { js: 'always' } }],
}),
],

invalid: [
Expand Down Expand Up @@ -652,6 +662,13 @@ ruleTester.run('extensions', rule, {
},
],
}),

test({
code: 'import foo from "./foo.js";',
options: ['always', { pattern: { js: 'never' }, fix: true }],
errors: [{ message: 'Unexpected use of file extension "js" for "./foo.js"' }],
output: 'import foo from "./foo";',
}),
],
});

Expand Down
Loading