diff --git a/packages/knip/fixtures/re-exports-cjs/1-entry.js b/packages/knip/fixtures/re-exports-cjs/1-entry.js new file mode 100644 index 000000000..95dc2fd0d --- /dev/null +++ b/packages/knip/fixtures/re-exports-cjs/1-entry.js @@ -0,0 +1,6 @@ +const { something } = require('./2-re-export-star'); +something; + +module.exports.somethingToIgnore = require('./2-re-export-star').somethingToIgnore; + +module.exports.somethingNotToIgnore = require('./2-re-export-star').somethingNotToIgnore; diff --git a/packages/knip/fixtures/re-exports-cjs/2-re-export-star.js b/packages/knip/fixtures/re-exports-cjs/2-re-export-star.js new file mode 100644 index 000000000..047432801 --- /dev/null +++ b/packages/knip/fixtures/re-exports-cjs/2-re-export-star.js @@ -0,0 +1 @@ +module.exports = require('./3-my-module'); diff --git a/packages/knip/fixtures/re-exports-cjs/3-my-module.js b/packages/knip/fixtures/re-exports-cjs/3-my-module.js new file mode 100644 index 000000000..a29c8385f --- /dev/null +++ b/packages/knip/fixtures/re-exports-cjs/3-my-module.js @@ -0,0 +1,3 @@ +module.exports.something = {}; +module.exports.somethingToIgnore = {}; +module.exports.somethingNotToIgnore = {}; diff --git a/packages/knip/fixtures/re-exports-cjs/package.json b/packages/knip/fixtures/re-exports-cjs/package.json new file mode 100644 index 000000000..1f70d55ab --- /dev/null +++ b/packages/knip/fixtures/re-exports-cjs/package.json @@ -0,0 +1,7 @@ +{ + "name": "@fixtures/re-exports-cjs", + "knip": { + "entry": ["1-entry.js"], + "project": ["*.js"] + } +} diff --git a/packages/knip/src/typescript/ast-helpers.ts b/packages/knip/src/typescript/ast-helpers.ts index f9896e7ba..c9f474a95 100644 --- a/packages/knip/src/typescript/ast-helpers.ts +++ b/packages/knip/src/typescript/ast-helpers.ts @@ -218,3 +218,11 @@ export const getExportKeywordNode = (node: ts.Node) => export const getDefaultKeywordNode = (node: ts.Node) => // @ts-expect-error Property 'modifiers' does not exist on type 'Node'. (node.modifiers as ts.Modifier[])?.find(mod => mod.kind === ts.SyntaxKind.DefaultKeyword); + +export const hasRequireCall = (node: ts.Node): boolean => { + if (ts.isCallExpression(node) && ts.isIdentifier(node.expression) && node.expression.text === 'require') return true; + return node.getChildren().some(child => hasRequireCall(child)); +}; + +export const isModuleExportsAccess = (node: ts.PropertyAccessExpression) => + ts.isIdentifier(node.expression) && node.expression.escapedText === 'module' && node.name.escapedText === 'exports'; diff --git a/packages/knip/src/typescript/get-imports-and-exports.ts b/packages/knip/src/typescript/get-imports-and-exports.ts index 759ad7a74..9106e9aea 100644 --- a/packages/knip/src/typescript/get-imports-and-exports.ts +++ b/packages/knip/src/typescript/get-imports-and-exports.ts @@ -120,6 +120,7 @@ const getImportsAndExports = ( // Patterns: // export { id } from 'specifier'; // export * from 'specifier'; + // module.exports = require('specifier'); addValue(imports.reExported, identifier, sourceFile.fileName); } } else { diff --git a/packages/knip/src/typescript/visitors/dynamic-imports/requireCall.ts b/packages/knip/src/typescript/visitors/dynamic-imports/requireCall.ts index 52ccc6e0e..fc6cd70c9 100644 --- a/packages/knip/src/typescript/visitors/dynamic-imports/requireCall.ts +++ b/packages/knip/src/typescript/visitors/dynamic-imports/requireCall.ts @@ -1,5 +1,6 @@ import ts from 'typescript'; -import { findAncestor, findDescendants, isRequireCall, isTopLevel } from '../../ast-helpers.js'; +import { IMPORT_STAR } from '../../../constants.js'; +import { findAncestor, findDescendants, isModuleExportsAccess, isRequireCall, isTopLevel } from '../../ast-helpers.js'; import { isNotJS } from '../helpers.js'; import { importVisitor as visit } from '../index.js'; @@ -56,6 +57,16 @@ export default visit( // Pattern: require('specifier') return { identifier: 'default', specifier, pos: node.arguments[0].pos, resolve }; } + + if ( + ts.isBinaryExpression(node.parent) && + ts.isPropertyAccessExpression(node.parent.left) && + isModuleExportsAccess(node.parent.left) + ) { + // Pattern: module.exports = require('specifier') + return { identifier: IMPORT_STAR, specifier, isReExport: true, pos: node.arguments[0].pos }; + } + // Pattern: require('side-effects') return { identifier: 'default', specifier, pos: node.arguments[0].pos, resolve }; } diff --git a/packages/knip/src/typescript/visitors/exports/moduleExportsAccessExpression.ts b/packages/knip/src/typescript/visitors/exports/moduleExportsAccessExpression.ts index 383e4f973..edd59db81 100644 --- a/packages/knip/src/typescript/visitors/exports/moduleExportsAccessExpression.ts +++ b/packages/knip/src/typescript/visitors/exports/moduleExportsAccessExpression.ts @@ -1,13 +1,10 @@ import ts from 'typescript'; import type { Fix } from '../../../types/exports.js'; import { SymbolType } from '../../../types/issues.js'; -import { stripQuotes } from '../../ast-helpers.js'; +import { hasRequireCall, isModuleExportsAccess, stripQuotes } from '../../ast-helpers.js'; import { isJS } from '../helpers.js'; import { exportVisitor as visit } from '../index.js'; -const isModuleExportsAccess = (node: ts.PropertyAccessExpression) => - ts.isIdentifier(node.expression) && node.expression.escapedText === 'module' && node.name.escapedText === 'exports'; - export default visit(isJS, (node, { isFixExports }) => { if (ts.isExpressionStatement(node)) { if (ts.isBinaryExpression(node.expression)) { @@ -37,6 +34,12 @@ export default visit(isJS, (node, { isFixExports }) => { return { node, identifier: node.getText(), type: SymbolType.UNKNOWN, pos: node.getStart(), fix }; }); } + + if (ts.isCallExpression(node.expression.right) && hasRequireCall(node.expression.right)) { + // Pattern: module.exports = require('specifier'); + return; + } + // Pattern: module.exports = any return { node, identifier: 'default', type: SymbolType.UNKNOWN, pos: expr.pos + 1, fix: undefined }; } diff --git a/packages/knip/test/re-exports-cjs.test.ts b/packages/knip/test/re-exports-cjs.test.ts new file mode 100644 index 000000000..e8d793bf0 --- /dev/null +++ b/packages/knip/test/re-exports-cjs.test.ts @@ -0,0 +1,21 @@ +import { test } from 'bun:test'; +import assert from 'node:assert/strict'; +import { main } from '../src/index.js'; +import { resolve } from '../src/util/path.js'; +import baseArguments from './helpers/baseArguments.js'; +import baseCounters from './helpers/baseCounters.js'; + +const cwd = resolve('fixtures/re-exports-cjs'); + +test('Ignore re-exports from entry files (CommonJS', async () => { + const { counters } = await main({ + ...baseArguments, + cwd, + }); + + assert.deepEqual(counters, { + ...baseCounters, + processed: 3, + total: 3, + }); +});