diff --git a/src/index.ts b/src/index.ts index 03d119a89..dfba92f46 100644 --- a/src/index.ts +++ b/src/index.ts @@ -26,7 +26,7 @@ const importDefault = (moduleName: string) => interopRequireDefault(require(moduleName)).default; const rulesDir = join(__dirname, 'rules'); -const excludedFiles = ['__tests__', 'utils']; +const excludedFiles = ['__tests__', 'detectJestVersion', 'utils']; const rules = readdirSync(rulesDir) .map(rule => parse(rule).name) diff --git a/src/rules/__tests__/detectJestVersion.test.ts b/src/rules/__tests__/detectJestVersion.test.ts new file mode 100644 index 000000000..deebb1e56 --- /dev/null +++ b/src/rules/__tests__/detectJestVersion.test.ts @@ -0,0 +1,229 @@ +import { spawnSync } from 'child_process'; +import * as fs from 'fs'; +import * as os from 'os'; +import * as path from 'path'; +import { JSONSchemaForNPMPackageJsonFiles } from '@schemastore/package'; +import { create } from 'ts-node'; +import { detectJestVersion } from '../detectJestVersion'; + +const compileFnCode = (pathToFn: string) => { + const fnContents = fs.readFileSync(pathToFn, 'utf-8'); + + return create({ + transpileOnly: true, + compilerOptions: { sourceMap: false }, + }).compile(fnContents, pathToFn); +}; +const compiledFn = compileFnCode(require.resolve('../detectJestVersion.ts')); +const relativePathToFn = 'eslint-plugin-jest/lib/rules/detectJestVersion.js'; + +const runNodeScript = (cwd: string, script: string) => { + return spawnSync('node', ['-e', script], { cwd, encoding: 'utf-8' }); +}; + +const runDetectJestVersion = (cwd: string) => { + return runNodeScript( + cwd, + ` + try { + console.log(require('${relativePathToFn}').detectJestVersion()); + } catch (error) { + console.error(error.message); + } + `, + ); +}; + +/** + * Makes a new temp directory, prefixed with `eslint-plugin-jest-` + * + * @return {Promise} + */ +const makeTempDir = () => + fs.mkdtempSync(path.join(os.tmpdir(), 'eslint-plugin-jest-')); + +interface ProjectStructure { + [key: `${string}/package.json`]: JSONSchemaForNPMPackageJsonFiles; + [key: `${string}/${typeof relativePathToFn}`]: string; + [key: `${string}/`]: null; + 'package.json'?: JSONSchemaForNPMPackageJsonFiles; +} + +const setupFakeProject = (structure: ProjectStructure): string => { + const tempDir = makeTempDir(); + + for (const [filePath, contents] of Object.entries(structure)) { + if (contents === null) { + fs.mkdirSync(path.join(tempDir, filePath), { recursive: true }); + + continue; + } + + const folderPath = path.dirname(filePath); + + // make the directory (recursively) + fs.mkdirSync(path.join(tempDir, folderPath), { recursive: true }); + + const finalContents = + typeof contents === 'string' ? contents : JSON.stringify(contents); + + fs.writeFileSync(path.join(tempDir, filePath), finalContents); + } + + return tempDir; +}; + +// pin the original cwd so that we can restore it after each test +// const projectDir = process.cwd(); + +// afterEach(() => process.chdir(projectDir)); + +describe('detectJestVersion', () => { + describe('basic tests', () => { + const packageJsonFactory = jest.fn(); + + beforeEach(() => { + jest.resetModules(); + jest.doMock(require.resolve('jest/package.json'), packageJsonFactory); + }); + + describe('when the package.json is missing the version property', () => { + it('throws an error', () => { + packageJsonFactory.mockReturnValue({}); + + expect(() => detectJestVersion()).toThrow( + /Unable to detect Jest version/iu, + ); + }); + }); + + it('caches versions', () => { + packageJsonFactory.mockReturnValue({ version: '1.2.3' }); + + const version = detectJestVersion(); + + jest.resetModules(); + + expect(detectJestVersion).not.toThrow(); + expect(detectJestVersion()).toBe(version); + }); + }); + + describe('when in a simple project', () => { + it('finds the correct version', () => { + const projectDir = setupFakeProject({ + 'package.json': { name: 'simple-project' }, + [`node_modules/${relativePathToFn}` as const]: compiledFn, + 'node_modules/jest/package.json': { + name: 'jest', + version: '21.0.0', + }, + }); + + const { stdout, stderr } = runDetectJestVersion(projectDir); + + expect(stdout.trim()).toBe('21'); + expect(stderr.trim()).toBe(''); + }); + }); + + describe('when in a hoisted mono-repo', () => { + it('finds the correct version', () => { + const projectDir = setupFakeProject({ + 'package.json': { name: 'mono-repo' }, + [`node_modules/${relativePathToFn}` as const]: compiledFn, + 'node_modules/jest/package.json': { + name: 'jest', + version: '19.0.0', + }, + 'packages/a/package.json': { name: 'package-a' }, + 'packages/b/package.json': { name: 'package-b' }, + }); + + const { stdout, stderr } = runDetectJestVersion(projectDir); + + expect(stdout.trim()).toBe('19'); + expect(stderr.trim()).toBe(''); + }); + }); + + describe('when in a subproject', () => { + it('finds the correct versions', () => { + const projectDir = setupFakeProject({ + 'backend/package.json': { name: 'package-a' }, + [`backend/node_modules/${relativePathToFn}` as const]: compiledFn, + 'backend/node_modules/jest/package.json': { + name: 'jest', + version: '24.0.0', + }, + 'frontend/package.json': { name: 'package-b' }, + [`frontend/node_modules/${relativePathToFn}` as const]: compiledFn, + 'frontend/node_modules/jest/package.json': { + name: 'jest', + version: '15.0.0', + }, + }); + + const { stdout: stdoutBackend, stderr: stderrBackend } = + runDetectJestVersion(path.join(projectDir, 'backend')); + + expect(stdoutBackend.trim()).toBe('24'); + expect(stderrBackend.trim()).toBe(''); + + const { stdout: stdoutFrontend, stderr: stderrFrontend } = + runDetectJestVersion(path.join(projectDir, 'frontend')); + + expect(stdoutFrontend.trim()).toBe('15'); + expect(stderrFrontend.trim()).toBe(''); + }); + }); + + describe('when jest is not installed', () => { + it('throws an error', () => { + const projectDir = setupFakeProject({ + 'package.json': { name: 'no-jest' }, + [`node_modules/${relativePathToFn}` as const]: compiledFn, + 'node_modules/pack/package.json': { name: 'pack' }, + }); + + const { stdout, stderr } = runDetectJestVersion(projectDir); + + expect(stdout.trim()).toBe(''); + expect(stderr.trim()).toContain('Unable to detect Jest version'); + }); + }); + + describe('when jest is changed on disk', () => { + it('uses the cached version', () => { + const projectDir = setupFakeProject({ + 'package.json': { name: 'no-jest' }, + [`node_modules/${relativePathToFn}` as const]: compiledFn, + 'node_modules/jest/package.json': { name: 'jest', version: '26.0.0' }, + }); + + const { stdout, stderr } = runNodeScript( + projectDir, + ` + const { detectJestVersion } = require('${relativePathToFn}'); + const fs = require('fs'); + + console.log(detectJestVersion()); + fs.writeFileSync( + 'node_modules/jest/package.json', + JSON.stringify({ + name: 'jest', + version: '25.0.0', + }), + ); + console.log(detectJestVersion()); + `, + ); + + const [firstCall, secondCall] = stdout.split('\n'); + + expect(firstCall).toBe('26'); + expect(secondCall).toBe('26'); + expect(stderr.trim()).toBe(''); + }); + }); +}); diff --git a/src/rules/__tests__/no-deprecated-functions.test.ts b/src/rules/__tests__/no-deprecated-functions.test.ts index 3c753def8..a596ec36f 100644 --- a/src/rules/__tests__/no-deprecated-functions.test.ts +++ b/src/rules/__tests__/no-deprecated-functions.test.ts @@ -1,58 +1,14 @@ -import * as fs from 'fs'; -import * as os from 'os'; -import * as path from 'path'; -import { JSONSchemaForNPMPackageJsonFiles } from '@schemastore/package'; import { TSESLint } from '@typescript-eslint/experimental-utils'; -import rule, { - JestVersion, - _clearCachedJestVersion, -} from '../no-deprecated-functions'; +import { JestVersion, detectJestVersion } from '../detectJestVersion'; +import rule from '../no-deprecated-functions'; -const ruleTester = new TSESLint.RuleTester(); - -// pin the original cwd so that we can restore it after each test -const projectDir = process.cwd(); - -afterEach(() => process.chdir(projectDir)); - -/** - * Makes a new temp directory, prefixed with `eslint-plugin-jest-` - * - * @return {Promise} - */ -const makeTempDir = async () => - fs.mkdtempSync(path.join(os.tmpdir(), 'eslint-plugin-jest-')); +jest.mock('../detectJestVersion'); -/** - * Sets up a fake project with a `package.json` file located in - * `node_modules/jest` whose version is set to the given `jestVersion`. - * - * @param {JestVersion} jestVersion - * - * @return {Promise} - */ -const setupFakeProjectDirectory = async ( - jestVersion: JestVersion, -): Promise => { - const jestPackageJson: JSONSchemaForNPMPackageJsonFiles = { - name: 'jest', - version: `${jestVersion}.0.0`, - }; +const detectJestVersionMock = detectJestVersion as jest.MockedFunction< + typeof detectJestVersion +>; - const tempDir = await makeTempDir(); - const jestPackagePath = path.join(tempDir, 'node_modules', 'jest'); - - // todo: remove in node@10 & replace with { recursive: true } - fs.mkdirSync(path.join(tempDir, 'node_modules')); - - fs.mkdirSync(jestPackagePath); - await fs.writeFileSync( - path.join(jestPackagePath, 'package.json'), - JSON.stringify(jestPackageJson), - ); - - return tempDir; -}; +const ruleTester = new TSESLint.RuleTester(); const generateValidCases = ( jestVersion: JestVersion | undefined, @@ -97,37 +53,8 @@ const generateInvalidCases = ( ]; }; -describe('the jest version cache', () => { - beforeEach(async () => process.chdir(await setupFakeProjectDirectory(17))); - - // change the jest version *after* each test case - afterEach(async () => { - const jestPackageJson: JSONSchemaForNPMPackageJsonFiles = { - name: 'jest', - version: '24.0.0', - }; - - const tempDir = process.cwd(); - - await fs.writeFileSync( - path.join(tempDir, 'node_modules', 'jest', 'package.json'), - JSON.stringify(jestPackageJson), - ); - }); - - ruleTester.run('no-deprecated-functions', rule, { - valid: [ - 'require("fs")', // this will cause jest version to be read & cached - 'jest.requireActual()', // deprecated after jest 17 - ], - invalid: [], - }); -}); - // contains the cache-clearing beforeEach so we can test the cache too describe('the rule', () => { - beforeEach(() => _clearCachedJestVersion()); - // a few sanity checks before doing our massive loop ruleTester.run('no-deprecated-functions', rule, { valid: [ @@ -155,9 +82,9 @@ describe('the rule', () => { describe.each([ 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, ])('when using jest version %i', jestVersion => { - beforeEach(async () => - process.chdir(await setupFakeProjectDirectory(jestVersion)), - ); + beforeEach(async () => { + detectJestVersionMock.mockReturnValue(jestVersion); + }); const allowedFunctions: string[] = []; const deprecations = ( @@ -210,50 +137,23 @@ describe('the rule', () => { }); }); - describe('when no jest version is provided', () => { - describe('when the jest package.json is missing the version property', () => { - beforeEach(async () => { - const tempDir = await setupFakeProjectDirectory(1); - - await fs.writeFileSync( - path.join(tempDir, 'node_modules', 'jest', 'package.json'), - JSON.stringify({}), - ); - - process.chdir(tempDir); - }); - - it('requires the version to be set explicitly', () => { - expect(() => { - const linter = new TSESLint.Linter(); - - linter.defineRule('no-deprecated-functions', rule); - - linter.verify('jest.resetModuleRegistry()', { - rules: { 'no-deprecated-functions': 'error' }, - }); - }).toThrow( - 'Unable to detect Jest version - please ensure jest package is installed, or otherwise set version explicitly', - ); + describe('when there is an error in detecting the jest version', () => { + beforeEach(() => { + detectJestVersionMock.mockImplementation(() => { + throw new Error('oh noes!'); }); }); - describe('when the jest package.json is not found', () => { - beforeEach(async () => process.chdir(await makeTempDir())); - - it('requires the version to be set explicitly', () => { - expect(() => { - const linter = new TSESLint.Linter(); + it('bubbles the error up', () => { + expect(() => { + const linter = new TSESLint.Linter(); - linter.defineRule('no-deprecated-functions', rule); + linter.defineRule('no-deprecated-functions', rule); - linter.verify('jest.resetModuleRegistry()', { - rules: { 'no-deprecated-functions': 'error' }, - }); - }).toThrow( - 'Unable to detect Jest version - please ensure jest package is installed, or otherwise set version explicitly', - ); - }); + linter.verify('jest.resetModuleRegistry()', { + rules: { 'no-deprecated-functions': 'error' }, + }); + }).toThrow('oh noes!'); }); }); }); diff --git a/src/rules/detectJestVersion.ts b/src/rules/detectJestVersion.ts new file mode 100644 index 000000000..0e5ba5bea --- /dev/null +++ b/src/rules/detectJestVersion.ts @@ -0,0 +1,44 @@ +import { JSONSchemaForNPMPackageJsonFiles } from '@schemastore/package'; + +export type JestVersion = + | 14 + | 15 + | 16 + | 17 + | 18 + | 19 + | 20 + | 21 + | 22 + | 23 + | 24 + | 25 + | 26 + | 27 + | number; + +let cachedJestVersion: JestVersion | null = null; + +export const detectJestVersion = (): JestVersion => { + if (cachedJestVersion) { + return cachedJestVersion; + } + + try { + const jestPath = require.resolve('jest/package.json'); + + const jestPackageJson = + // eslint-disable-next-line @typescript-eslint/no-require-imports + require(jestPath) as JSONSchemaForNPMPackageJsonFiles; + + if (jestPackageJson.version) { + const [majorVersion] = jestPackageJson.version.split('.'); + + return (cachedJestVersion = parseInt(majorVersion, 10)); + } + } catch {} + + throw new Error( + 'Unable to detect Jest version - please ensure jest package is installed, or otherwise set version explicitly', + ); +}; diff --git a/src/rules/no-deprecated-functions.ts b/src/rules/no-deprecated-functions.ts index 189a0aa65..4454b7f8b 100644 --- a/src/rules/no-deprecated-functions.ts +++ b/src/rules/no-deprecated-functions.ts @@ -1,64 +1,18 @@ -import { JSONSchemaForNPMPackageJsonFiles } from '@schemastore/package'; import { AST_NODE_TYPES, TSESTree, } from '@typescript-eslint/experimental-utils'; +import { JestVersion, detectJestVersion } from './detectJestVersion'; import { createRule, getNodeName } from './utils'; interface ContextSettings { jest?: EslintPluginJestSettings; } -export type JestVersion = - | 14 - | 15 - | 16 - | 17 - | 18 - | 19 - | 20 - | 21 - | 22 - | 23 - | 24 - | 25 - | 26 - | 27 - | number; - interface EslintPluginJestSettings { version: JestVersion; } -let cachedJestVersion: JestVersion | null = null; - -/** @internal */ -export const _clearCachedJestVersion = () => (cachedJestVersion = null); - -const detectJestVersion = (): JestVersion => { - if (cachedJestVersion) { - return cachedJestVersion; - } - - try { - const jestPath = require.resolve('jest/package.json'); - - const jestPackageJson = - // eslint-disable-next-line @typescript-eslint/no-require-imports - require(jestPath) as JSONSchemaForNPMPackageJsonFiles; - - if (jestPackageJson.version) { - const [majorVersion] = jestPackageJson.version.split('.'); - - return (cachedJestVersion = parseInt(majorVersion, 10)); - } - } catch {} - - throw new Error( - 'Unable to detect Jest version - please ensure jest package is installed, or otherwise set version explicitly', - ); -}; - export default createRule({ name: __filename, meta: {