diff --git a/packages/eslint-plugin/docs/rules/no-require-imports.md b/packages/eslint-plugin/docs/rules/no-require-imports.md index 746ca6dff7ed..d67d639b9cb8 100644 --- a/packages/eslint-plugin/docs/rules/no-require-imports.md +++ b/packages/eslint-plugin/docs/rules/no-require-imports.md @@ -28,6 +28,28 @@ import { lib2 } from 'lib2'; import * as lib3 from 'lib3'; ``` +## Options + +### `allow` + +A array of strings. These strings will be compiled into regular expressions with the `u` flag and be used to test against the imported path. A common use case is to allow importing `package.json`. This is because `package.json` commonly lives outside of the TS root directory, so statically importing it would lead to root directory conflicts, especially with `resolveJsonModule` enabled. You can also use it to allow importing any JSON if your environment doesn't support JSON modules, or use it for other cases where `import` statements cannot work. + +With `{allow: ['/package\\.json$']}`: + + + +### ❌ Incorrect + +```ts +console.log(require('../data.json').version); +``` + +### ✅ Correct + +```ts +console.log(require('../package.json').version); +``` + ## When Not To Use It If your project frequently uses older CommonJS `require`s, then this rule might not be applicable to you. diff --git a/packages/eslint-plugin/docs/rules/no-var-requires.md b/packages/eslint-plugin/docs/rules/no-var-requires.md index f4605ac335bd..0bd0b2b564b9 100644 --- a/packages/eslint-plugin/docs/rules/no-var-requires.md +++ b/packages/eslint-plugin/docs/rules/no-var-requires.md @@ -28,6 +28,28 @@ require('foo'); import foo from 'foo'; ``` +## Options + +### `allow` + +A array of strings. These strings will be compiled into regular expressions with the `u` flag and be used to test against the imported path. A common use case is to allow importing `package.json`. This is because `package.json` commonly lives outside of the TS root directory, so statically importing it would lead to root directory conflicts, especially with `resolveJsonModule` enabled. You can also use it to allow importing any JSON if your environment doesn't support JSON modules, or use it for other cases where `import` statements cannot work. + +With `{allow: ['/package\\.json$']}`: + + + +### ❌ Incorrect + +```ts +const foo = require('../data.json'); +``` + +### ✅ Correct + +```ts +const foo = require('../package.json'); +``` + ## When Not To Use It If your project frequently uses older CommonJS `require`s, then this rule might not be applicable to you. diff --git a/packages/eslint-plugin/src/rules/no-require-imports.ts b/packages/eslint-plugin/src/rules/no-require-imports.ts index fbf4bc785717..c4f20e6c68f7 100644 --- a/packages/eslint-plugin/src/rules/no-require-imports.ts +++ b/packages/eslint-plugin/src/rules/no-require-imports.ts @@ -1,27 +1,59 @@ import type { TSESTree } from '@typescript-eslint/utils'; -import { ASTUtils } from '@typescript-eslint/utils'; +import { AST_NODE_TYPES, ASTUtils } from '@typescript-eslint/utils'; import { getScope } from '@typescript-eslint/utils/eslint-utils'; -import { createRule } from '../util'; +import * as util from '../util'; -export default createRule({ +type Options = [ + { + allow: string[]; + }, +]; +type MessageIds = 'noRequireImports'; + +export default util.createRule({ name: 'no-require-imports', meta: { type: 'problem', docs: { description: 'Disallow invocation of `require()`', }, - schema: [], + schema: [ + { + type: 'object', + properties: { + allow: { + type: 'array', + items: { type: 'string' }, + description: 'Patterns of import paths to allow requiring from.', + }, + }, + additionalProperties: false, + }, + ], messages: { noRequireImports: 'A `require()` style import is forbidden.', }, }, - defaultOptions: [], - create(context) { + defaultOptions: [{ allow: [] }], + create(context, options) { + const allowPatterns = options[0].allow.map( + pattern => new RegExp(pattern, 'u'), + ); + function isImportPathAllowed(importPath: string): boolean { + return allowPatterns.some(pattern => importPath.match(pattern)); + } return { 'CallExpression[callee.name="require"]'( node: TSESTree.CallExpression, ): void { + if ( + node.arguments[0]?.type === AST_NODE_TYPES.Literal && + typeof node.arguments[0].value === 'string' && + isImportPathAllowed(node.arguments[0].value) + ) { + return; + } const variable = ASTUtils.findVariable(getScope(context), 'require'); // ignore non-global require usage as it's something user-land custom instead @@ -34,6 +66,13 @@ export default createRule({ } }, TSExternalModuleReference(node): void { + if ( + node.expression.type === AST_NODE_TYPES.Literal && + typeof node.expression.value === 'string' && + isImportPathAllowed(node.expression.value) + ) { + return; + } context.report({ node, messageId: 'noRequireImports', diff --git a/packages/eslint-plugin/src/rules/no-var-requires.ts b/packages/eslint-plugin/src/rules/no-var-requires.ts index 7ebf45250f05..3605904e655b 100644 --- a/packages/eslint-plugin/src/rules/no-var-requires.ts +++ b/packages/eslint-plugin/src/rules/no-var-requires.ts @@ -4,7 +4,11 @@ import { getScope } from '@typescript-eslint/utils/eslint-utils'; import { createRule } from '../util'; -type Options = []; +type Options = [ + { + allow: string[]; + }, +]; type MessageIds = 'noVarReqs'; export default createRule({ @@ -18,14 +22,39 @@ export default createRule({ messages: { noVarReqs: 'Require statement not part of import statement.', }, - schema: [], + schema: [ + { + type: 'object', + properties: { + allow: { + type: 'array', + items: { type: 'string' }, + description: 'Patterns of import paths to allow requiring from.', + }, + }, + additionalProperties: false, + }, + ], }, - defaultOptions: [], - create(context) { + defaultOptions: [{ allow: [] }], + create(context, options) { + const allowPatterns = options[0].allow.map( + pattern => new RegExp(pattern, 'u'), + ); + function isImportPathAllowed(importPath: string): boolean { + return allowPatterns.some(pattern => importPath.match(pattern)); + } return { 'CallExpression[callee.name="require"]'( node: TSESTree.CallExpression, ): void { + if ( + node.arguments[0]?.type === AST_NODE_TYPES.Literal && + typeof node.arguments[0].value === 'string' && + isImportPathAllowed(node.arguments[0].value) + ) { + return; + } const parent = node.parent.type === AST_NODE_TYPES.ChainExpression ? node.parent.parent diff --git a/packages/eslint-plugin/tests/rules/no-require-imports.test.ts b/packages/eslint-plugin/tests/rules/no-require-imports.test.ts index ff6cbd2a6032..42374ccffbf8 100644 --- a/packages/eslint-plugin/tests/rules/no-require-imports.test.ts +++ b/packages/eslint-plugin/tests/rules/no-require-imports.test.ts @@ -23,6 +23,30 @@ import { createRequire } from 'module'; const require = createRequire(); require('remark-preset-prettier'); `, + { + code: "const pkg = require('./package.json');", + options: [{ allow: ['/package\\.json$'] }], + }, + { + code: "const pkg = require('../package.json');", + options: [{ allow: ['/package\\.json$'] }], + }, + { + code: "const pkg = require('../packages/package.json');", + options: [{ allow: ['/package\\.json$'] }], + }, + { + code: "import pkg = require('../packages/package.json');", + options: [{ allow: ['/package\\.json$'] }], + }, + { + code: "import pkg = require('data.json');", + options: [{ allow: ['\\.json$'] }], + }, + { + code: "import pkg = require('some-package');", + options: [{ allow: ['^some-package$'] }], + }, ], invalid: [ { @@ -111,5 +135,58 @@ var lib5 = require?.('lib5'), }, ], }, + { + code: "const pkg = require('./package.json');", + errors: [ + { + line: 1, + column: 13, + messageId: 'noRequireImports', + }, + ], + }, + { + code: "const pkg = require('./package.jsonc');", + options: [{ allow: ['/package\\.json$'] }], + errors: [ + { + line: 1, + column: 13, + messageId: 'noRequireImports', + }, + ], + }, + { + code: "import pkg = require('./package.json');", + errors: [ + { + line: 1, + column: 14, + messageId: 'noRequireImports', + }, + ], + }, + { + code: "import pkg = require('./package.jsonc');", + options: [{ allow: ['/package\\.json$'] }], + errors: [ + { + line: 1, + column: 14, + messageId: 'noRequireImports', + }, + ], + }, + { + code: "import pkg = require('./package.json');", + options: [{ allow: ['^some-package$'] }], + errors: [ + { + line: 1, + column: 14, + messageId: 'noRequireImports', + }, + ], + }, ], }); diff --git a/packages/eslint-plugin/tests/rules/no-var-requires.test.ts b/packages/eslint-plugin/tests/rules/no-var-requires.test.ts index 14ce51109941..718bcc46db31 100644 --- a/packages/eslint-plugin/tests/rules/no-var-requires.test.ts +++ b/packages/eslint-plugin/tests/rules/no-var-requires.test.ts @@ -16,6 +16,26 @@ import { createRequire } from 'module'; const require = createRequire('foo'); const json = require('./some.json'); `, + { + code: "const pkg = require('./package.json');", + options: [{ allow: ['/package\\.json$'] }], + }, + { + code: "const pkg = require('../package.json');", + options: [{ allow: ['/package\\.json$'] }], + }, + { + code: "const pkg = require('../packages/package.json');", + options: [{ allow: ['/package\\.json$'] }], + }, + { + code: "const pkg = require('data.json');", + options: [{ allow: ['\\.json$'] }], + }, + { + code: "const pkg = require('some-package');", + options: [{ allow: ['^some-package$'] }], + }, ], invalid: [ { @@ -157,5 +177,37 @@ configValidator.addSchema(require('./a.json')); }, ], }, + { + code: "const pkg = require('./package.json');", + errors: [ + { + line: 1, + column: 13, + messageId: 'noVarReqs', + }, + ], + }, + { + code: "const pkg = require('./package.jsonc');", + options: [{ allow: ['/package\\.json$'] }], + errors: [ + { + line: 1, + column: 13, + messageId: 'noVarReqs', + }, + ], + }, + { + code: "const pkg = require('./package.json');", + options: [{ allow: ['^some-package$'] }], + errors: [ + { + line: 1, + column: 13, + messageId: 'noVarReqs', + }, + ], + }, ], }); diff --git a/packages/eslint-plugin/tests/schema-snapshots/no-require-imports.shot b/packages/eslint-plugin/tests/schema-snapshots/no-require-imports.shot index bdb7cb325381..7561bcbc4b3c 100644 --- a/packages/eslint-plugin/tests/schema-snapshots/no-require-imports.shot +++ b/packages/eslint-plugin/tests/schema-snapshots/no-require-imports.shot @@ -4,11 +4,30 @@ exports[`Rule schemas should be convertible to TS types for documentation purpos " # SCHEMA: -[] +[ + { + "additionalProperties": false, + "properties": { + "allow": { + "description": "Patterns of import paths to allow requiring from.", + "items": { + "type": "string" + }, + "type": "array" + } + }, + "type": "object" + } +] # TYPES: -/** No options declared */ -type Options = [];" +type Options = [ + { + /** Patterns of import paths to allow requiring from. */ + allow?: string[]; + }, +]; +" `; diff --git a/packages/eslint-plugin/tests/schema-snapshots/no-var-requires.shot b/packages/eslint-plugin/tests/schema-snapshots/no-var-requires.shot index 992833a20ae3..5271679f9bbb 100644 --- a/packages/eslint-plugin/tests/schema-snapshots/no-var-requires.shot +++ b/packages/eslint-plugin/tests/schema-snapshots/no-var-requires.shot @@ -4,11 +4,30 @@ exports[`Rule schemas should be convertible to TS types for documentation purpos " # SCHEMA: -[] +[ + { + "additionalProperties": false, + "properties": { + "allow": { + "description": "Patterns of import paths to allow requiring from.", + "items": { + "type": "string" + }, + "type": "array" + } + }, + "type": "object" + } +] # TYPES: -/** No options declared */ -type Options = [];" +type Options = [ + { + /** Patterns of import paths to allow requiring from. */ + allow?: string[]; + }, +]; +" `;