-
-
Notifications
You must be signed in to change notification settings - Fork 41
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
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 |
---|---|---|
|
@@ -41,6 +41,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 || {}, | ||
|
@@ -69,6 +71,8 @@ function getDependencies(context: RuleContext, packageDir?: string | string[]) { | |
|
||
try { | ||
let packageContent: PackageDeps = { | ||
name: undefined, | ||
exports: undefined, | ||
dependencies: {}, | ||
devDependencies: {}, | ||
optionalDependencies: {}, | ||
|
@@ -91,10 +95,30 @@ 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 | ||
} | ||
Object.assign( | ||
packageContent.dependencies, | ||
packageContent_.dependencies, | ||
) | ||
Object.assign( | ||
packageContent.devDependencies, | ||
packageContent_.devDependencies, | ||
) | ||
Object.assign( | ||
packageContent.optionalDependencies, | ||
packageContent_.optionalDependencies, | ||
) | ||
Object.assign( | ||
packageContent.peerDependencies, | ||
packageContent_.peerDependencies, | ||
) | ||
Object.assign( | ||
packageContent.bundledDependencies, | ||
packageContent_.bundledDependencies, | ||
) | ||
} | ||
} | ||
} else { | ||
|
@@ -111,13 +135,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 | ||
} | ||
|
@@ -297,6 +325,20 @@ function reportIfMissing( | |
return | ||
} | ||
|
||
if (importPackageName === deps.name) { | ||
if (!deps.exports) { | ||
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. I'm not sure to understand why 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. 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:
{
"name": "pkg",
"exports": "./index.js"
}
require('pkg')
console.log('Hello') If you run $ node index.js
Hello If you now remove the $ node index.js
node:internal/modules/cjs/loader:1228
throw err;
^
Error: Cannot find module 'pkg'
Require stack:
- /home/remco/Downloads/asd/index.js
at Function._resolveFilename (node:internal/modules/cjs/loader:1225:15)
at Function._load (node:internal/modules/cjs/loader:1055:27)
at TracingChannel.traceSync (node:diagnostics_channel:322:14)
at wrapModuleLoad (node:internal/modules/cjs/loader:220:24)
at Module.require (node:internal/modules/cjs/loader:1311:12)
at require (node:internal/modules/helpers:136:16)
at Object.<anonymous> (/home/remco/Downloads/asd/index.js:1:1)
at Module._compile (node:internal/modules/cjs/loader:1554:14)
at Object..js (node:internal/modules/cjs/loader:1706:10)
at Module.load (node:internal/modules/cjs/loader:1289:32) {
code: 'MODULE_NOT_FOUND',
requireStack: [ '/home/remco/Downloads/asd/index.js' ]
}
Node.js v22.14.0 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. 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 commentThe 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 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. 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. 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 commentThe reason will be displayed to describe this comment to others. Learn more.
Correct! 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. @remcohaszing Would you like to combine your proposed 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. 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 commentThe 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 commentThe 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 |
||
context.report({ | ||
node, | ||
messageId: 'selfImport', | ||
data: { | ||
packageName, | ||
}, | ||
}) | ||
} | ||
|
||
return | ||
} | ||
|
||
if (declarationStatus.isInDevDeps && !depsOptions.allowDevDeps) { | ||
context.report({ | ||
node, | ||
|
@@ -358,6 +400,7 @@ export type MessageId = | |
| 'devDep' | ||
| 'optDep' | ||
| 'missing' | ||
| 'selfImport' | ||
|
||
export default createRule<[Options?], MessageId>({ | ||
name: 'no-extraneous-dependencies', | ||
|
@@ -392,6 +435,8 @@ export default createRule<[Options?], MessageId>({ | |
"'{{packageName}}' should be listed in the project's dependencies, not optionalDependencies.", | ||
missing: | ||
"'{{packageName}}' should be listed in the project's dependencies. Run 'npm i -S {{packageName}}' to add it", | ||
selfImport: | ||
"'{{packageName}}' may only import itself if the exports field is defined in package.json", | ||
}, | ||
}, | ||
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.
Merging
bundledDependencies
usingObject.assign
may not properly concatenate arrays if multiplepackage.json
files provide them. Consider whether concatenating arrays (e.g. using array spread orconcat
) might be more appropriate here.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.
Personally I don't like current refactor, it's too verbose to me, only
name
andexports
are special.@remcohaszing Could you revert it back to previous style? So that I can merge this PR as-is.
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.
Friendly ping @remcohaszing
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.
What do you mean by reverting to the previous style? This is not a stylistic change.
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.
Aren't they? Only
name
andexports
are special, all other keys can key previous style assignment.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.
By the way, should we check whether the importee is truly exported in
exports
? Example:/subpath
needs to be checked whether available inexports
?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 really don’t understand what style you envision. Please elaborate. Do you mean the loop? That doesn’t work. Not all keys are objects that should be shallow merged.
No. That doesn’t change anything. If
fake-pkg
uses its own package exports, it uses the exports from its ownpackage.json
. It no longer checksnode_modules/fake-pkg/package.json
. So even if thefake-pkg/subpath
export doesn’t exist, it still doesn’t resolve to implicit dependencies.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.
Loop with check
name
andexports
specially only, all other keys should not be changed.I'm not sure to understand, I know
<rootDir>/node_modules/fake-pkg/package.json
won't be checked, I'm talking about<rootDir>/package.json
, and also, when the user installsfake-pkg
manually or in its dependency tree implicitly, what means<rootDir>/node_modules/fake-pkg/package.json
is present, what'll happen?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 think you're both right
@remcohaszing i think you may be better off having a set of known merged fields, like:
you're right we probably shouldn't blindly merge every key now that not all keys are objects (and that may change in future if we add even more non-object keys).
i think @JounQin is just suggesting you keep the existing "deep merge every key", but you'd have to have a condition in there to skip
name
andexports
. then it'd achieve the same as what you have herebut imo we should just be explicit about the fields we want to merge