nx/packages/angular/src/generators/convert-tslint-to-eslint/convert-tslint-to-eslint.spec.ts

369 lines
11 KiB
TypeScript

import {
addProjectConfiguration,
joinPathFragments,
ProjectConfiguration,
readJson,
readProjectConfiguration,
Tree,
writeJson,
} from '@nx/devkit';
import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing';
import { exampleRootTslintJson } from '@nx/linter';
import { conversionGenerator } from './convert-tslint-to-eslint';
/**
* Don't run actual child_process implementation of installPackagesTask()
*/
jest.mock('child_process', () => {
return {
...jest.requireActual<any>('child_process'),
execSync: jest.fn((command: string) => {
if (command.includes('pnpm --version')) {
return '8.2.0';
}
return;
}),
};
});
const appProjectName = 'angular-app-1';
const appProjectRoot = `apps/${appProjectName}`;
const appProjectTSLintJsonPath = joinPathFragments(
appProjectRoot,
'tslint.json'
);
const projectPrefix = 'angular-app';
const libProjectName = 'angular-lib-1';
const libProjectRoot = `libs/${libProjectName}`;
const libProjectTSLintJsonPath = joinPathFragments(
libProjectRoot,
'tslint.json'
);
// Used to configure the test Tree and stub the response from tslint-to-eslint-config util findReportedConfiguration()
const projectTslintJsonData = {
raw: {
extends: '../../tslint.json',
rules: {
// Standard Nx/Angular CLI generated rules
'directive-selector': [true, 'attribute', projectPrefix, 'camelCase'],
'component-selector': [true, 'element', projectPrefix, 'kebab-case'],
// User custom TS rule
'no-empty-interface': true,
// User custom template/HTML rule
'template-banana-in-box': true,
// User custom rule with no known automated converter
'some-super-custom-rule-with-no-converter': true,
},
linterOptions: {
exclude: ['!**/*'],
},
},
tslintPrintConfigResult: {
rules: {
'directive-selector': {
ruleArguments: ['attribute', projectPrefix, 'camelCase'],
ruleSeverity: 'error',
},
'component-selector': {
ruleArguments: ['element', projectPrefix, 'kebab-case'],
ruleSeverity: 'error',
},
'no-empty-interface': {
ruleArguments: [],
ruleSeverity: 'error',
},
'template-banana-in-box': {
ruleArguments: [],
ruleSeverity: 'error',
},
'some-super-custom-rule-with-no-converter': {
ruleArguments: [],
ruleSeverity: 'error',
},
},
},
};
function mockFindReportedConfiguration(_, pathToTslintJson) {
switch (pathToTslintJson) {
case 'tslint.json':
return exampleRootTslintJson.tslintPrintConfigResult;
case appProjectTSLintJsonPath:
return {
/**
* Add in an example of rule which requires type-checking so we can test
* that parserOptions.project is appropriately preserved in the final
* config in this case.
*/
rules: {
...projectTslintJsonData.tslintPrintConfigResult.rules,
'await-promise': {
ruleArguments: [],
ruleSeverity: 'error',
},
},
};
case libProjectTSLintJsonPath:
return projectTslintJsonData.tslintPrintConfigResult;
default:
throw new Error(
`mockFindReportedConfiguration - Did not recognize path ${pathToTslintJson}`
);
}
}
/**
* See ./mock-tslint-to-eslint-config.ts for why this is needed
*/
jest.mock('tslint-to-eslint-config', () => {
return {
// Since upgrading to (ts-)jest 26 this usage of this mock has caused issues...
// @ts-ignore
...jest.requireActual<any>('tslint-to-eslint-config'),
findReportedConfiguration: jest.fn(mockFindReportedConfiguration),
};
});
/**
* Mock the the mutating fs utilities used within the conversion logic, they are not
* needed because of our stubbed response for findReportedConfiguration() above, and
* they would cause noise in the git data of the actual Nx repo when the tests run.
*/
jest.mock('fs', () => {
return {
// Since upgrading to (ts-)jest 26 this usage of this mock has caused issues...
// @ts-ignore
...jest.requireActual<any>('fs'),
writeFileSync: jest.fn(),
mkdirSync: jest.fn(),
};
});
describe('convert-tslint-to-eslint', () => {
let host: Tree;
beforeEach(async () => {
host = createTreeWithEmptyWorkspace({ layout: 'apps-libs' });
writeJson(host, 'tslint.json', exampleRootTslintJson.raw);
addProjectConfiguration(host, appProjectName, {
root: appProjectRoot,
prefix: projectPrefix,
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/**', '!apps/angular-app-1/**/*'],
tsConfig: ['apps/angular-app-1/tsconfig.app.json'],
},
},
},
} as ProjectConfiguration);
addProjectConfiguration(host, libProjectName, {
root: libProjectRoot,
prefix: projectPrefix,
projectType: 'library',
targets: {
/**
* LINT TARGET CONFIG - BEFORE CONVERSION
*
* TSLint executor configured for the project
*/
lint: {
executor: '@angular-devkit/build-angular:tslint',
options: {
exclude: ['**/node_modules/**', '!libs/angular-lib-1/**/*'],
tsConfig: ['libs/angular-lib-1/tsconfig.app.json'],
},
},
},
} as ProjectConfiguration);
/**
* Existing tslint.json file for the app project
*/
writeJson(host, 'apps/angular-app-1/tslint.json', {
...projectTslintJsonData.raw,
rules: {
...projectTslintJsonData.raw.rules,
/**
* Add in an example of rule which requires type-checking so we can test
* that parserOptions.project is appropriately preserved in the final
* config in this case.
*/
'await-promise': true,
},
});
/**
* Existing tslint.json file for the lib project
*/
writeJson(
host,
'libs/angular-lib-1/tslint.json',
projectTslintJsonData.raw
);
});
it('should work for Angular applications', async () => {
await conversionGenerator(host, {
project: appProjectName,
ignoreExistingTslintConfig: false,
removeTSLintIfNoMoreTSLintTargets: false,
});
/**
* It should ensure the required Nx packages are installed and available
*
* NOTE: tslint-to-eslint-config should NOT be present
*/
expect(readJson(host, 'package.json')).toMatchSnapshot();
/**
* LINT TARGET CONFIG - AFTER CONVERSION
*
* It should replace the TSLint executor with the ESLint one
*/
expect(readProjectConfiguration(host, appProjectName)).toMatchSnapshot();
/**
* The root level .eslintrc.json should now have been generated
*/
const eslintJson = readJson(host, '.eslintrc.json');
expect(eslintJson.overrides[3].rules['no-console'][1].allow).toContain(
'log'
);
// Remove no-console config because it is not deterministic across node versions
delete eslintJson.overrides[3].rules['no-console'][1].allow;
expect(eslintJson).toMatchSnapshot();
/**
* The project level .eslintrc.json should now have been generated
* and extend from the root, as well as applying any customizations
* which are specific to this projectType.
*/
expect(
readJson(host, joinPathFragments(appProjectRoot, '.eslintrc.json'))
).toMatchSnapshot();
/**
* The project's TSLint file should have been deleted
*/
expect(host.exists(appProjectTSLintJsonPath)).toEqual(false);
});
it('should work for Angular libraries', async () => {
await conversionGenerator(host, {
project: libProjectName,
ignoreExistingTslintConfig: false,
removeTSLintIfNoMoreTSLintTargets: false,
});
/**
* It should ensure the required Nx packages are installed and available
*
* NOTE: tslint-to-eslint-config should NOT be present
*/
expect(readJson(host, 'package.json')).toMatchSnapshot();
/**
* LINT TARGET CONFIG - AFTER CONVERSION
*
* It should replace the TSLint executor with the ESLint one
*/
expect(readProjectConfiguration(host, libProjectName)).toMatchSnapshot();
/**
* The root level .eslintrc.json should now have been generated
*/
const eslintJson = readJson(host, '.eslintrc.json');
expect(eslintJson.overrides[3].rules['no-console'][1].allow).toContain(
'log'
);
// Remove no-console config because it is not deterministic across node versions
delete eslintJson.overrides[3].rules['no-console'][1].allow;
expect(eslintJson).toMatchSnapshot();
/**
* The project level .eslintrc.json should now have been generated
* and extend from the root, as well as applying any customizations
* which are specific to this projectType.
*/
expect(
readJson(host, joinPathFragments(libProjectRoot, '.eslintrc.json'))
).toMatchSnapshot();
/**
* The project's TSLint file should have been deleted
*/
expect(host.exists(libProjectTSLintJsonPath)).toEqual(false);
});
it('should not override .eslint config if migration in progress', async () => {
/**
* First we convert app
*/
await conversionGenerator(host, {
project: appProjectName,
ignoreExistingTslintConfig: false,
removeTSLintIfNoMoreTSLintTargets: true,
});
/**
* The root level .eslintrc.json should now have been generated
*/
const eslintContent = readJson(host, '.eslintrc.json');
expect(eslintContent.overrides[3].rules['no-console'][1].allow).toContain(
'log'
);
// Remove no-console config because it is not deterministic across node versions
delete eslintContent.overrides[3].rules['no-console'][1].allow;
expect(eslintContent).toMatchSnapshot();
/**
* We will make a change to the eslint config before the next step
*/
eslintContent.overrides[0].rules[
'@nx/enforce-module-boundaries'
][1].enforceBuildableLibDependency = false;
writeJson(host, '.eslintrc.json', eslintContent);
/**
* Convert the lib
*/
await conversionGenerator(host, {
project: libProjectName,
ignoreExistingTslintConfig: false,
removeTSLintIfNoMoreTSLintTargets: true,
});
/**
* The project level .eslintrc.json should now have been generated
* and extend from the root, as well as applying any customizations
* which are specific to this projectType.
*/
expect(
readJson(host, joinPathFragments(libProjectRoot, '.eslintrc.json'))
).toMatchSnapshot();
/**
* The root level .eslintrc.json should not be re-created
* if it already existed
*/
const eslintJson2 = readJson(host, '.eslintrc.json');
expect(eslintJson2).toMatchSnapshot();
/**
* The root TSLint file should have been deleted
*/
expect(host.exists('tslint.json')).toEqual(false);
});
});