-
-
Notifications
You must be signed in to change notification settings - Fork 53
feat(no-extraneous-dependencies): allow package to import itself #309
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
4e0dc4f
cac809c
39db681
d878aac
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
--- | ||
"eslint-plugin-import-x": patch | ||
--- | ||
|
||
Allow packages to import themselves in `import-x/no-extraneous-imports` if the `exports` field is defined in `package.json` |
Original file line number | Diff line number | Diff line change | ||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
@@ -42,6 +42,8 @@ function readJSON<T>(jsonPath: string, throwException: boolean) { | |||||||||||||
|
||||||||||||||
function extractDepFields(pkg: PackageJson) { | ||||||||||||||
return { | ||||||||||||||
name: pkg.name, | ||||||||||||||
exports: pkg.exports, | ||||||||||||||
dependencies: pkg.dependencies || {}, | ||||||||||||||
devDependencies: pkg.devDependencies || {}, | ||||||||||||||
optionalDependencies: pkg.optionalDependencies || {}, | ||||||||||||||
|
@@ -70,6 +72,8 @@ function getDependencies(context: RuleContext, packageDir?: string | string[]) { | |||||||||||||
|
||||||||||||||
try { | ||||||||||||||
let packageContent: PackageDeps = { | ||||||||||||||
name: undefined, | ||||||||||||||
exports: undefined, | ||||||||||||||
dependencies: {}, | ||||||||||||||
devDependencies: {}, | ||||||||||||||
optionalDependencies: {}, | ||||||||||||||
|
@@ -92,9 +96,19 @@ function getDependencies(context: RuleContext, packageDir?: string | string[]) { | |||||||||||||
paths.length === 1, | ||||||||||||||
) | ||||||||||||||
if (packageContent_) { | ||||||||||||||
for (const depsKey of Object.keys(packageContent)) { | ||||||||||||||
const key = depsKey as keyof PackageDeps | ||||||||||||||
Object.assign(packageContent[key], packageContent_[key]) | ||||||||||||||
if (!packageContent.name) { | ||||||||||||||
packageContent.name = packageContent_.name | ||||||||||||||
packageContent.exports = packageContent_.exports | ||||||||||||||
} | ||||||||||||||
const fieldsToMerge = [ | ||||||||||||||
'dependencies', | ||||||||||||||
'devDependencies', | ||||||||||||||
'optionalDependencies', | ||||||||||||||
'peerDependencies', | ||||||||||||||
'bundledDependencies', | ||||||||||||||
] as const | ||||||||||||||
for (const field of fieldsToMerge) { | ||||||||||||||
Object.assign(packageContent[field], packageContent_[field]) | ||||||||||||||
} | ||||||||||||||
} | ||||||||||||||
} | ||||||||||||||
|
@@ -112,13 +126,17 @@ function getDependencies(context: RuleContext, packageDir?: string | string[]) { | |||||||||||||
} | ||||||||||||||
|
||||||||||||||
if ( | ||||||||||||||
![ | ||||||||||||||
packageContent.dependencies, | ||||||||||||||
packageContent.devDependencies, | ||||||||||||||
packageContent.optionalDependencies, | ||||||||||||||
packageContent.peerDependencies, | ||||||||||||||
packageContent.bundledDependencies, | ||||||||||||||
].some(hasKeys) | ||||||||||||||
!( | ||||||||||||||
packageContent.name || | ||||||||||||||
packageContent.exports || | ||||||||||||||
[ | ||||||||||||||
packageContent.dependencies, | ||||||||||||||
packageContent.devDependencies, | ||||||||||||||
packageContent.optionalDependencies, | ||||||||||||||
packageContent.peerDependencies, | ||||||||||||||
packageContent.bundledDependencies, | ||||||||||||||
].some(hasKeys) | ||||||||||||||
) | ||||||||||||||
) { | ||||||||||||||
return | ||||||||||||||
} | ||||||||||||||
|
@@ -298,6 +316,20 @@ function reportIfMissing( | |||||||||||||
return | ||||||||||||||
} | ||||||||||||||
|
||||||||||||||
if (importPackageName === deps.name) { | ||||||||||||||
if (!deps.exports) { | ||||||||||||||
context.report({ | ||||||||||||||
node, | ||||||||||||||
messageId: 'selfImport', | ||||||||||||||
data: { | ||||||||||||||
packageName, | ||||||||||||||
}, | ||||||||||||||
}) | ||||||||||||||
} | ||||||||||||||
|
||||||||||||||
return | ||||||||||||||
} | ||||||||||||||
Comment on lines
+319
to
+331
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion Harden self-import detection using the resolved package name Currently, self-import is detected via importPackageName === deps.name. If a resolver aliases the bare specifier to a different package/path, this could incorrectly treat a non-self import as self and skip reporting. Prefer using realPackageName when available. - if (importPackageName === deps.name) {
+ const isSelfImport = (realPackageName ?? importPackageName) === deps.name
+ if (isSelfImport) {
if (!deps.exports) {
context.report({
node,
messageId: 'selfImport',
data: {
packageName,
},
})
}
return
} Note: This preserves existing behavior but avoids false positives when aliasing is involved.
🤖 Prompt for AI Agents
|
||||||||||||||
|
||||||||||||||
if (declarationStatus.isInDevDeps && !depsOptions.allowDevDeps) { | ||||||||||||||
context.report({ | ||||||||||||||
node, | ||||||||||||||
|
@@ -366,6 +398,7 @@ export type MessageId = | |||||||||||||
| 'devDep' | ||||||||||||||
| 'optDep' | ||||||||||||||
| 'missing' | ||||||||||||||
| 'selfImport' | ||||||||||||||
|
||||||||||||||
export default createRule<[Options?], MessageId>({ | ||||||||||||||
name: 'no-extraneous-dependencies', | ||||||||||||||
|
@@ -399,6 +432,8 @@ export default createRule<[Options?], MessageId>({ | |||||||||||||
optDep: | ||||||||||||||
"'{{packageName}}' should be listed in the project's dependencies, not optionalDependencies.", | ||||||||||||||
missing: `'{{packageName}}' should be listed in the project's dependencies. Run '${getNpmInstallCommand('{{packageName}}')}' to add it`, | ||||||||||||||
selfImport: | ||||||||||||||
"'{{packageName}}' may only import itself if the exports field is defined in package.json", | ||||||||||||||
}, | ||||||||||||||
Comment on lines
+435
to
437
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 💡 Verification agent 🧩 Analysis chainNew message text added; consider minor polish and docs/tests sync
To verify coverage quickly: 🏁 Script executed: #!/bin/bash
# Check tests reference the selfImport messageId and that docs mention it.
rg -n "selfImport" -S
rg -n "no-extraneous-dependencies" docs -S || true Length of output: 2274 Polish The
📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents
|
||||||||||||||
}, | ||||||||||||||
defaultOptions: [], | ||||||||||||||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
export default function () {} |
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
{ | ||
"name": "package-named-exports", | ||
"description": "Standard, named package with exports", | ||
"exports": "./index.js" | ||
} |
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure to understand why
exports
field is required, and is this prevents self-importing issues in source files?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don’t know the rationale behind that decision, but that is how Node.js behaves. You can try it by creating a directory with two files:
package.json
:index.js
:If you run
index.js
, this logs:If you now remove the
exports
field, or change it tomain
, this logs:There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Even if it works for self-importing, but I personally still believe it's not a good practice to do like this as circular dependency itself. It should only be allowed in non-source files IMO.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This example is indeed circular, which is a bit silly. This was to keep it minimal. Modules can resolve package-local modules using
package.json
exports. Tests are one useful example, but I can think of other use cases.The goal of this PR however is not to discuss whether or not people should do this or when. People can do it. The concept of what a source file is also varies per project. The goal of this PR is to fix a false positives for detecting imports of extraneous modules.
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
People can do anything they want without enabling this rule, the ESLint rules are for good practice, not how codes can be used in runtime. Otherwise, running the codes itself already helps you confirming it's working.
But I'd like to hear more voices from @thepassle @SukkaW @Shinigami92 @43081j
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Correct!
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@remcohaszing Would you like to combine your proposed
import-x/no-own-exports
rule into this PR together? Or maybe wait for other reviewers.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That rule seems unrelated to these changes TBH. Also I don’t have personal interest in that rule.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Well, that rule should be added together with this behavior change IMO.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That makes sense to me
We can probably merge this one first @JounQin and one of us can work on the new rule separately.
It looks like this won't change existing behaviour other than making this edge case work. So should be an easy one to land