From 4e0dc4f3866e27ee9e79752d35d72d7c6f351e19 Mon Sep 17 00:00:00 2001 From: Remco Haszing Date: Fri, 18 Apr 2025 15:45:44 +0200 Subject: [PATCH] feat(no-extraneous-dependencies): allow package to import itself Allow packages to import themselves via the `package.json` exports field. If a package tries this, but the exports field is not defined, a new message is reported. Closes #305 --- .changeset/cruel-cooks-notice.md | 5 ++ src/rules/no-extraneous-dependencies.ts | 65 ++++++++++++++++--- test/fixtures/package-named-exports/index.js | 1 + .../package-named-exports/index.js | 1 + .../package-named-exports/package.json | 5 ++ .../package-named-exports/package.json | 5 ++ .../node_modules/package-named/index.js | 1 + .../node_modules/package-named/package.json | 5 ++ test/rules/no-extraneous-dependencies.spec.ts | 13 ++++ 9 files changed, 91 insertions(+), 10 deletions(-) create mode 100644 .changeset/cruel-cooks-notice.md create mode 100644 test/fixtures/package-named-exports/index.js create mode 100644 test/fixtures/package-named-exports/node_modules/package-named-exports/index.js create mode 100644 test/fixtures/package-named-exports/node_modules/package-named-exports/package.json create mode 100644 test/fixtures/package-named-exports/package.json create mode 100644 test/fixtures/package-named/node_modules/package-named/index.js create mode 100644 test/fixtures/package-named/node_modules/package-named/package.json diff --git a/.changeset/cruel-cooks-notice.md b/.changeset/cruel-cooks-notice.md new file mode 100644 index 00000000..568e1e35 --- /dev/null +++ b/.changeset/cruel-cooks-notice.md @@ -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` diff --git a/src/rules/no-extraneous-dependencies.ts b/src/rules/no-extraneous-dependencies.ts index b3d27443..c275ecde 100644 --- a/src/rules/no-extraneous-dependencies.ts +++ b/src/rules/no-extraneous-dependencies.ts @@ -41,6 +41,8 @@ function readJSON(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) { + 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: [], diff --git a/test/fixtures/package-named-exports/index.js b/test/fixtures/package-named-exports/index.js new file mode 100644 index 00000000..ea9b101e --- /dev/null +++ b/test/fixtures/package-named-exports/index.js @@ -0,0 +1 @@ +export default function () {} diff --git a/test/fixtures/package-named-exports/node_modules/package-named-exports/index.js b/test/fixtures/package-named-exports/node_modules/package-named-exports/index.js new file mode 100644 index 00000000..ea9b101e --- /dev/null +++ b/test/fixtures/package-named-exports/node_modules/package-named-exports/index.js @@ -0,0 +1 @@ +export default function () {} diff --git a/test/fixtures/package-named-exports/node_modules/package-named-exports/package.json b/test/fixtures/package-named-exports/node_modules/package-named-exports/package.json new file mode 100644 index 00000000..efb3abb2 --- /dev/null +++ b/test/fixtures/package-named-exports/node_modules/package-named-exports/package.json @@ -0,0 +1,5 @@ +{ + "name": "package-named-exports", + "description": "Standard, named package with exports", + "exports": "./index.js" +} diff --git a/test/fixtures/package-named-exports/package.json b/test/fixtures/package-named-exports/package.json new file mode 100644 index 00000000..efb3abb2 --- /dev/null +++ b/test/fixtures/package-named-exports/package.json @@ -0,0 +1,5 @@ +{ + "name": "package-named-exports", + "description": "Standard, named package with exports", + "exports": "./index.js" +} diff --git a/test/fixtures/package-named/node_modules/package-named/index.js b/test/fixtures/package-named/node_modules/package-named/index.js new file mode 100644 index 00000000..ea9b101e --- /dev/null +++ b/test/fixtures/package-named/node_modules/package-named/index.js @@ -0,0 +1 @@ +export default function () {} diff --git a/test/fixtures/package-named/node_modules/package-named/package.json b/test/fixtures/package-named/node_modules/package-named/package.json new file mode 100644 index 00000000..69ff5910 --- /dev/null +++ b/test/fixtures/package-named/node_modules/package-named/package.json @@ -0,0 +1,5 @@ +{ + "name": "package-named", + "description": "Standard, named package", + "main": "index.js" +} diff --git a/test/rules/no-extraneous-dependencies.spec.ts b/test/rules/no-extraneous-dependencies.spec.ts index a1ee05b5..f50be5ff 100644 --- a/test/rules/no-extraneous-dependencies.spec.ts +++ b/test/rules/no-extraneous-dependencies.spec.ts @@ -238,6 +238,11 @@ ruleTester.run('no-extraneous-dependencies', rule, { { packageDir: packageDirMonoRepoRoot, whitelist: ['not-a-dependency'] }, ], }), + tValid({ + code: 'import "package-named-exports"', + filename: testFilePath('package-named-exports/index.js'), + options: [{ packageDir: testFilePath('package-named-exports') }], + }), ], invalid: [ tInvalid({ @@ -446,6 +451,14 @@ ruleTester.run('no-extraneous-dependencies', rule, { { messageId: 'missing', data: { packageName: 'not-a-dependency' } }, ], }), + tInvalid({ + code: 'import "package-named"', + filename: testFilePath('package-named/index.js'), + options: [{ packageDir: testFilePath('package-named') }], + errors: [ + { messageId: 'selfImport', data: { packageName: 'package-named' } }, + ], + }), ], })