import { addDependenciesToPackageJson, addProjectConfiguration, readJson, readWorkspaceConfiguration, Tree, updateWorkspaceConfiguration, writeJson, } from '@nrwl/devkit'; import { createTreeWithEmptyWorkspace } from '@nrwl/devkit/testing'; import { ProjectConverter } from './project-converter'; /** * Don't run actual child_process implementation of installPackagesTask() */ jest.mock('child_process'); /** * Don't run the conversion util, it touches the file system and has its own tests */ jest.mock('./utils', () => { return { // Since upgrading to (ts-)jest 26 this usage of this mock has caused issues... // @ts-ignore ...jest.requireActual('./utils'), convertTSLintConfig: jest.fn(() => { return { convertedESLintConfig: { rules: { 'some-converted-rule': 'error', }, }, unconvertedTSLintRules: [], ensureESLintPlugins: [], }; }), }; }); describe('ProjectConverter', () => { let host: Tree; const projectName = 'foo'; const projectRoot = `apps/${projectName}`; beforeEach(async () => { // Clean up any previous dry run simulations process.argv = process.argv.filter( (a) => !['--dry-run', '--dryRun', '-d'].includes(a) ); host = createTreeWithEmptyWorkspace(); addProjectConfiguration(host, projectName, { root: projectRoot, projectType: 'application', targets: { /** * LINT TARGET CONFIG - BEFORE CONVERSION * * TSLint executor configured for the project */ lint: { executor: '@angular-devkit/build-angular:tslint', options: { exclude: ['**/node_modules/**', `!${projectRoot}/**/*`], tsConfig: [`${projectRoot}/tsconfig.app.json`], }, }, }, /** * PROJECT-LEVEL GENERATOR CONFIG - BEFORE CONVERSION * * Default set to tslint, using shorthand syntax */ generators: { '@nrwl/angular:library': { linter: 'tslint', }, '@nrwl/angular:application': { e2eTestRunner: 'cypress', linter: 'eslint', unitTestRunner: 'jest', }, }, }); }); it('should throw if --dry-run is set', () => { writeJson(host, 'tslint.json', {}); writeJson(host, `${projectRoot}/tslint.json`, {}); process.argv.push('--dry-run'); expect( () => new ProjectConverter({ host, projectName, ignoreExistingTslintConfig: false, eslintInitializer: () => undefined, }) ).toThrowErrorMatchingSnapshot(); }); it('should throw if --dryRun is set', () => { writeJson(host, 'tslint.json', {}); writeJson(host, `${projectRoot}/tslint.json`, {}); process.argv.push('--dryRun'); expect( () => new ProjectConverter({ host, projectName, ignoreExistingTslintConfig: false, eslintInitializer: () => undefined, }) ).toThrowErrorMatchingSnapshot(); }); it('should throw if -d is set', () => { writeJson(host, 'tslint.json', {}); writeJson(host, `${projectRoot}/tslint.json`, {}); process.argv.push('-d'); expect( () => new ProjectConverter({ host, projectName, ignoreExistingTslintConfig: false, eslintInitializer: () => undefined, }) ).toThrowErrorMatchingSnapshot(); }); it('should throw if no root tslint.json is found', () => { expect( () => new ProjectConverter({ host, projectName, ignoreExistingTslintConfig: false, eslintInitializer: () => undefined, }) ).toThrowErrorMatchingSnapshot(); }); it('should throw if no project tslint.json is found', () => { writeJson(host, 'tslint.json', {}); expect( () => new ProjectConverter({ host, projectName, ignoreExistingTslintConfig: false, eslintInitializer: () => undefined, }) ).toThrowErrorMatchingSnapshot(); }); it('should not throw when not in dry-run and config files successfully found', () => { writeJson(host, 'tslint.json', {}); writeJson(host, `${projectRoot}/tslint.json`, {}); expect( () => new ProjectConverter({ host, projectName, ignoreExistingTslintConfig: false, eslintInitializer: () => undefined, }) ).not.toThrow(); }); describe('ignoreExistingTslintConfig', () => { describe('ignoreExistingTslintConfig: false', () => { it('should use existing TSLint configs and merge their converted ESLint equivalents with recommended ESLint configs', async () => { const { convertTSLintConfig } = require('./utils'); (convertTSLintConfig as jest.Mock).mockClear(); writeJson(host, 'tslint.json', {}); writeJson(host, `${projectRoot}/tslint.json`, {}); const projectConverterDiscardFalse = new ProjectConverter({ host, projectName, ignoreExistingTslintConfig: false, eslintInitializer: () => { writeJson(host, '.eslintrc.json', { rules: { 'some-recommended-root-eslint-rule': 'error', }, }); writeJson(host, `${projectRoot}/.eslintrc.json`, { rules: { 'some-recommended-project-eslint-rule': 'error', }, }); return Promise.resolve(undefined); }, }); await projectConverterDiscardFalse.initESLint(); // Existing ESLint rules from init stage will be merged with converted rules await projectConverterDiscardFalse.convertRootTSLintConfig((j) => j); expect(readJson(host, '.eslintrc.json')).toMatchSnapshot(); // Existing ESLint rules from init stage will be merged with converted rules await projectConverterDiscardFalse.convertProjectConfig((j) => j); expect( readJson(host, `${projectRoot}/.eslintrc.json`) ).toMatchSnapshot(); expect(convertTSLintConfig).toHaveBeenCalledTimes(2); }); }); describe('ignoreExistingTslintConfig: true', () => { it('should ignore existing TSLint configs and just reset the project to use recommended ESLint configs', async () => { const { convertTSLintConfig } = require('./utils'); (convertTSLintConfig as jest.Mock).mockClear(); writeJson(host, 'tslint.json', {}); writeJson(host, `${projectRoot}/tslint.json`, {}); const projectConverterDiscardTrue = new ProjectConverter({ host, projectName, ignoreExistingTslintConfig: true, eslintInitializer: () => { writeJson(host, '.eslintrc.json', { rules: { 'some-recommended-root-eslint-rule': 'error', }, }); writeJson(host, `${projectRoot}/.eslintrc.json`, { rules: { 'some-recommended-project-eslint-rule': 'error', }, }); return Promise.resolve(undefined); }, }); await projectConverterDiscardTrue.initESLint(); // Should not contain any converted rules, just existing ESLint rules from init stage await projectConverterDiscardTrue.convertRootTSLintConfig((j) => j); expect(readJson(host, '.eslintrc.json')).toMatchSnapshot(); // Should not contain any converted rules, just existing ESLint rules from init stage await projectConverterDiscardTrue.convertProjectConfig((j) => j); expect( readJson(host, `${projectRoot}/.eslintrc.json`) ).toMatchSnapshot(); expect(convertTSLintConfig).not.toHaveBeenCalled(); }); }); }); describe('setDefaults()', () => { it('should save in workspace.json', async () => { writeJson(host, 'tslint.json', {}); writeJson(host, `${projectRoot}/tslint.json`, {}); const projectConverter = new ProjectConverter({ host, projectName, ignoreExistingTslintConfig: false, eslintInitializer: () => undefined, }); const workspace = readWorkspaceConfiguration(host); workspace.generators = { '@nrwl/angular': { application: { linter: 'tslint', }, library: { linter: 'tslint', }, }, }; updateWorkspaceConfiguration(host, workspace); // BEFORE - no entry for convert-tslint-to-eslint wthin @nrwl/angular generators expect(readJson(host, 'workspace.json')).toMatchSnapshot(); projectConverter.setDefaults('@nrwl/angular', { ignoreExistingTslintConfig: true, removeTSLintIfNoMoreTSLintTargets: true, }); // AFTER (1) - convert-tslint-to-eslint wthin @nrwl/angular generators has removeTSLintIfNoMoreTSLintTargets set to true expect(readJson(host, 'workspace.json')).toMatchSnapshot(); projectConverter.setDefaults('@nrwl/angular', { ignoreExistingTslintConfig: false, removeTSLintIfNoMoreTSLintTargets: false, }); // AFTER (2) - convert-tslint-to-eslint wthin @nrwl/angular generators has removeTSLintIfNoMoreTSLintTargets set to false expect(readJson(host, 'workspace.json')).toMatchSnapshot(); }); }); describe('removeTSLintFromWorkspace()', () => { it('should remove all relevant traces of TSLint from the workspace', async () => { writeJson(host, 'tslint.json', {}); writeJson(host, `${projectRoot}/tslint.json`, {}); const projectConverter = new ProjectConverter({ host, projectName, ignoreExistingTslintConfig: false, eslintInitializer: () => undefined, }); await addDependenciesToPackageJson( host, {}, { codelyzer: 'latest', tslint: 'latest', } )(); const workspace = readWorkspaceConfiguration(host); // Not using shorthand syntax this time workspace.generators = { '@nrwl/angular': { application: { linter: 'tslint', }, library: { linter: 'tslint', }, }, }; updateWorkspaceConfiguration(host, workspace); // BEFORE - tslint and codelyzer are present expect(readJson(host, 'package.json')).toMatchSnapshot(); // BEFORE - tslint set as both global and project-level default linter for @nrwl/angular generators expect(readJson(host, 'workspace.json')).toMatchSnapshot(); await projectConverter.removeTSLintFromWorkspace()(); // AFTER - it should remove tslint and codelyzer expect(readJson(host, 'package.json')).toMatchSnapshot(); // AFTER - generators config from global and project-level settings removed (because eslint is always default) expect(readJson(host, 'workspace.json')).toMatchSnapshot(); }); it('should remove the entry in generators for convert-tslint-to-eslint because it is no longer needed', async () => { writeJson(host, 'tslint.json', {}); writeJson(host, `${projectRoot}/tslint.json`, {}); const projectConverter = new ProjectConverter({ host, projectName, ignoreExistingTslintConfig: false, eslintInitializer: () => undefined, }); const workspace = readWorkspaceConfiguration(host); workspace.generators = { '@nrwl/angular': { 'convert-tslint-to-eslint': { removeTSLintIfNoMoreTSLintTargets: true, }, }, }; updateWorkspaceConfiguration(host, workspace); // BEFORE - convert-tslint-to-eslint wthin @nrwl/angular generators has a value for removeTSLintIfNoMoreTSLintTargets expect(readJson(host, 'workspace.json')).toMatchSnapshot(); await projectConverter.removeTSLintFromWorkspace()(); // AFTER - generators config no longer has a reference to convert-tslint-to-eslint expect(readJson(host, 'workspace.json')).toMatchSnapshot(); }); }); });