nx/e2e/eslint/src/linter.test.ts
Leosvel Pérez Espinosa dd9b09f917
fix(js): generate js libs with exports in package.json and ensure esm output when using rollup bundler (#29565)
- Ensure libs are generated with `exports` in `package.json`
- Generate `types` instead of `typings` in package.json
- Update js lib with rollup to only output esm
- Update `tsconfig.spec.json` for js libraries with rollup to set
`module: esnext` and `moduleResolution: bundler` (they use `@swc/jest`)
- Fix `@nx/js/typescript` issue with absolute paths when normalizing
inputs/outputs
- Fix `@nx/js/typescript` issue identifying buildable libs
- Fix express app generator not installing `@types/express`

<!-- Please make sure you have read the submission guidelines before
posting an PR -->
<!--
https://github.com/nrwl/nx/blob/master/CONTRIBUTING.md#-submitting-a-pr
-->

<!-- Please make sure that your commit message follows our format -->
<!-- Example: `fix(nx): must begin with lowercase` -->

<!-- If this is a particularly complex change or feature addition, you
can request a dedicated Nx release for this pull request branch. Mention
someone from the Nx team or the `@nrwl/nx-pipelines-reviewers` and they
will confirm if the PR warrants its own release for testing purposes,
and generate it for you if appropriate. -->

## Current Behavior
<!-- This is the behavior we have today -->

## Expected Behavior
<!-- This is the behavior we should expect with the changes in this PR
-->

## Related Issue(s)
<!-- Please link the issue being fixed so it gets closed when this is
merged. -->

Fixes #

---------

Co-authored-by: Jack Hsu <jack.hsu@gmail.com>
2025-01-10 08:29:09 -05:00

995 lines
33 KiB
TypeScript

import * as path from 'path';
import {
checkFilesDoNotExist,
checkFilesExist,
cleanupProject,
createFile,
newProject,
readFile,
readJson,
runCLI,
uniq,
updateFile,
updateJson,
} from '@nx/e2e/utils';
import * as ts from 'typescript';
describe('Linter', () => {
let originalEslintUseFlatConfigVal: string | undefined;
beforeAll(() => {
// Opt into legacy .eslintrc config format for these tests
originalEslintUseFlatConfigVal = process.env.ESLINT_USE_FLAT_CONFIG;
process.env.ESLINT_USE_FLAT_CONFIG = 'false';
});
afterAll(() => {
process.env.ESLINT_USE_FLAT_CONFIG = originalEslintUseFlatConfigVal;
});
describe('Integrated', () => {
const myapp = uniq('myapp');
const mylib = uniq('mylib');
let projScope;
beforeAll(() => {
projScope = newProject({
packages: ['@nx/react', '@nx/js', '@nx/eslint'],
});
runCLI(
`generate @nx/react:app apps/${myapp} --tags=validtag --linter eslint --unitTestRunner vitest`
);
runCLI(`generate @nx/js:lib libs/${mylib} --linter eslint`);
});
afterAll(() => cleanupProject());
describe('linting errors', () => {
let defaultEslintrc;
beforeAll(() => {
updateFile(`apps/${myapp}/src/main.ts`, `console.log("should fail");`);
defaultEslintrc = readJson('.eslintrc.json');
});
afterEach(() => {
updateFile('.eslintrc.json', JSON.stringify(defaultEslintrc, null, 2));
});
it('should check for linting errors', () => {
// create faulty file
updateFile(`apps/${myapp}/src/main.ts`, `console.log("should fail");`);
const eslintrc = readJson('.eslintrc.json');
// set the eslint rules to error
eslintrc.overrides.forEach((override) => {
if (override.files.includes('*.ts')) {
override.rules['no-console'] = 'error';
}
});
updateFile('.eslintrc.json', JSON.stringify(eslintrc, null, 2));
let out = runCLI(`lint ${myapp}`, {
silenceError: true,
env: { CI: 'false' },
});
expect(out).toContain('Unexpected console statement');
eslintrc.overrides.forEach((override) => {
if (override.files.includes('*.ts')) {
override.rules['no-console'] = undefined;
}
});
updateFile('.eslintrc.json', JSON.stringify(eslintrc, null, 2));
// 3. linting should not error when all rules are followed
out = runCLI(`lint ${myapp}`, {
silenceError: true,
env: { CI: 'false' },
});
expect(out).toContain('Successfully ran target lint');
}, 1000000);
it('should cache eslint with --cache', () => {
function readCacheFile(cacheFile) {
const cacheInfo = readFile(cacheFile);
return process.platform === 'win32'
? cacheInfo.replace(/\\\\/g, '\\')
: cacheInfo;
}
// should generate a default cache file
let cachePath = path.join('apps', myapp, '.eslintcache');
expect(() => checkFilesExist(cachePath)).toThrow();
runCLI(`lint ${myapp} --cache`, {
silenceError: true,
env: { CI: 'false' },
});
expect(() => checkFilesExist(cachePath)).not.toThrow();
expect(readCacheFile(cachePath)).toContain(
path.normalize(`${myapp}/src/app/app.spec.tsx`)
);
// should let you specify a cache file location
cachePath = path.join('apps', myapp, 'my-cache');
expect(() => checkFilesExist(cachePath)).toThrow();
runCLI(`lint ${myapp} --cache --cache-location="my-cache"`, {
silenceError: true,
env: { CI: 'false' },
});
expect(() => checkFilesExist(cachePath)).not.toThrow();
expect(readCacheFile(cachePath)).toContain(
path.normalize(`${myapp}/src/app/app.spec.tsx`)
);
});
it('linting should generate an output file with a specific format', () => {
const eslintrc = readJson('.eslintrc.json');
eslintrc.overrides.forEach((override) => {
if (override.files.includes('*.ts')) {
override.rules['no-console'] = 'error';
}
});
updateFile('.eslintrc.json', JSON.stringify(eslintrc, null, 2));
const outputFile = 'a/b/c/lint-output.json';
const outputFilePath = path.join('apps', myapp, outputFile);
expect(() => {
checkFilesExist(outputFilePath);
}).toThrow();
const stdout = runCLI(
`lint ${myapp} --output-file="${outputFile}" --format=json`,
{
silenceError: true,
env: { CI: 'false' },
}
);
expect(stdout).not.toContain('Unexpected console statement');
expect(() => checkFilesExist(outputFilePath)).not.toThrow();
const outputContents = JSON.parse(readFile(outputFilePath));
const outputForApp: any = Object.values(outputContents).filter(
(result: any) =>
result.filePath.includes(path.normalize(`${myapp}/src/main.ts`))
)[0];
expect(outputForApp.errorCount).toBe(1);
expect(outputForApp.messages[0].ruleId).toBe('no-console');
expect(outputForApp.messages[0].message).toBe(
'Unexpected console statement.'
);
}, 1000000);
it('should support creating, testing and using workspace lint rules', () => {
const messageId = 'e2eMessageId';
const libMethodName = 'getMessageId';
// add custom function
updateFile(
`libs/${mylib}/src/lib/${mylib}.ts`,
`export const ${libMethodName} = (): '${messageId}' => '${messageId}';`
);
// Generate a new rule (should also scaffold the required workspace project and tests)
const newRuleName = 'e2e-test-rule-name';
runCLI(`generate @nx/eslint:workspace-rule ${newRuleName}`);
// TODO(@AgentEnder): This reset gets rid of a lockfile changed error... we should fix this in another way
runCLI(`reset`, {
env: { CI: 'false' },
});
// Ensure that the unit tests for the new rule are runnable
expect(() =>
runCLI(`test eslint-rules`, {
env: { CI: 'false' },
})
).not.toThrow();
// Update the rule for the e2e test so that we can assert that it produces the expected lint failure when used
const knownLintErrorMessage = 'e2e test known error message';
const newRulePath = `tools/eslint-rules/rules/${newRuleName}.ts`;
const newRuleGeneratedContents = readFile(newRulePath);
const updatedRuleContents = updateGeneratedRuleImplementation(
newRulePath,
newRuleGeneratedContents,
knownLintErrorMessage,
messageId,
libMethodName,
`@${projScope}/${mylib}`
);
updateFile(newRulePath, updatedRuleContents);
const newRuleNameForUsage = `@nx/workspace-${newRuleName}`;
// Add the new workspace rule to the lint config and run linting
const eslintrc = readJson('.eslintrc.json');
eslintrc.overrides.forEach((override) => {
if (override.files.includes('*.ts')) {
override.rules[newRuleNameForUsage] = 'error';
}
});
updateFile('.eslintrc.json', JSON.stringify(eslintrc, null, 2));
const lintOutput = runCLI(`lint ${myapp} --verbose`, {
silenceError: true,
env: { CI: 'false' },
});
expect(lintOutput).toContain(newRuleNameForUsage);
expect(lintOutput).toContain(knownLintErrorMessage);
}, 1000000);
it('lint plugin should ensure module boundaries', () => {
const myapp2 = uniq('myapp2');
const lazylib = uniq('lazylib');
const invalidtaglib = uniq('invalidtaglib');
const validtaglib = uniq('validtaglib');
runCLI(`generate @nx/react:app apps/${myapp2} --linter eslint`);
runCLI(`generate @nx/react:lib libs/${lazylib} --linter eslint`);
runCLI(
`generate @nx/js:lib libs/${invalidtaglib} --linter eslint --tags=invalidtag`
);
runCLI(
`generate @nx/js:lib libs/${validtaglib} --linter eslint --tags=validtag`
);
const eslint = readJson('.eslintrc.json');
eslint.overrides[0].rules[
'@nx/enforce-module-boundaries'
][1].depConstraints = [
{ sourceTag: 'validtag', onlyDependOnLibsWithTags: ['validtag'] },
...eslint.overrides[0].rules['@nx/enforce-module-boundaries'][1]
.depConstraints,
];
updateFile('.eslintrc.json', JSON.stringify(eslint, null, 2));
const tsConfig = readJson('tsconfig.base.json');
/**
* apps do not add themselves to the tsconfig file.
*
* Let's add it so that we can trigger the lint failure
*/
tsConfig.compilerOptions.paths[`@${projScope}/${myapp2}`] = [
`apps/${myapp2}/src/main.ts`,
];
tsConfig.compilerOptions.paths[`@secondScope/${lazylib}`] =
tsConfig.compilerOptions.paths[`@${projScope}/${lazylib}`];
delete tsConfig.compilerOptions.paths[`@${projScope}/${lazylib}`];
updateFile('tsconfig.base.json', JSON.stringify(tsConfig, null, 2));
updateFile(
`apps/${myapp}/src/main.ts`,
`
import '../../../libs/${mylib}';
import '@secondScope/${lazylib}';
import '@${projScope}/${myapp2}';
import '@${projScope}/${invalidtaglib}';
import '@${projScope}/${validtaglib}';
const s = {loadChildren: '@secondScope/${lazylib}'};
`
);
const out = runCLI(`lint ${myapp}`, {
silenceError: true,
env: { CI: 'false' },
});
expect(out).toContain(
'Projects cannot be imported by a relative or absolute path, and must begin with a npm scope'
);
expect(out).toContain('Imports of apps are forbidden');
expect(out).toContain(
'A project tagged with "validtag" can only depend on libs tagged with "validtag"'
);
}, 1000000);
});
describe('workspace boundary rules', () => {
const libA = uniq('tslib-a');
const libB = uniq('tslib-b');
const libC = uniq('tslib-c');
beforeAll(() => {
// make these libs non-buildable to avoid dep-checks triggering lint errors
runCLI(
`generate @nx/js:lib libs/${libA} --bundler=none --linter eslint`
);
runCLI(
`generate @nx/js:lib libs/${libB} --bundler=none --linter eslint`
);
runCLI(
`generate @nx/js:lib libs/${libC} --bundler=none --linter eslint`
);
/**
* create tslib-a structure
*/
createFile(
`libs/${libA}/src/lib/tslib-a.ts`,
`
export function libASayHi(): string {
return 'hi there';
}
export function libASayHello(): string {
return 'Hi from tslib-a';
}
`
);
createFile(
`libs/${libA}/src/lib/some-non-exported-function.ts`,
`
export function someNonPublicLibFunction() {
return 'this function is exported, but not via the libs barrel file';
}
export function someSelectivelyExportedFn() {
return 'this fn is exported selectively in the barrel file';
}
`
);
createFile(
`libs/${libA}/src/index.ts`,
`
export * from './lib/tslib-a';
export { someSelectivelyExportedFn } from './lib/some-non-exported-function';
`
);
/**
* create tslib-b structure
*/
createFile(
`libs/${libB}/src/index.ts`,
`
export * from './lib/tslib-b';
`
);
createFile(
`libs/${libB}/src/lib/tslib-b.ts`,
`
import { libASayHi } from 'libs/${libA}/src/lib/tslib-a';
import { libASayHello } from '../../../${libA}/src/lib/tslib-a';
// import { someNonPublicLibFunction } from '../../../${libA}/src/lib/some-non-exported-function';
import { someSelectivelyExportedFn } from '../../../${libA}/src/lib/some-non-exported-function';
export function tslibB(): string {
// someNonPublicLibFunction();
someSelectivelyExportedFn();
libASayHi();
libASayHello();
return 'hi there';
}
`
);
/**
* create tslib-c structure
*/
createFile(
`libs/${libC}/src/index.ts`,
`
export * from './lib/tslib-c';
export * from './lib/constant';
`
);
createFile(
`libs/${libC}/src/lib/constant.ts`,
`
export const SOME_CONSTANT = 'some constant value';
export const someFunc1 = () => 'hi';
export function someFunc2() {
return 'hi2';
}
`
);
createFile(
`libs/${libC}/src/lib/tslib-c-another.ts`,
`
import { tslibC, SOME_CONSTANT, someFunc1, someFunc2 } from '@${projScope}/${libC}';
export function someStuff() {
someFunc1();
someFunc2();
tslibC();
console.log(SOME_CONSTANT);
return 'hi';
}
`
);
createFile(
`libs/${libC}/src/lib/tslib-c.ts`,
`
import { someFunc1, someFunc2, SOME_CONSTANT } from '@${projScope}/${libC}';
export function tslibC(): string {
someFunc1();
someFunc2();
console.log(SOME_CONSTANT);
return 'tslib-c';
}
`
);
});
it('should fix noSelfCircularDependencies', () => {
const stdout = runCLI(`lint ${libC}`, {
silenceError: true,
env: { CI: 'false' },
});
expect(stdout).toContain(
'Projects should use relative imports to import from other files within the same project'
);
// fix them
const fixedStout = runCLI(`lint ${libC} --fix`, {
silenceError: true,
env: { CI: 'false' },
});
expect(fixedStout).toContain(
`Successfully ran target lint for project ${libC}`
);
const fileContent = readFile(`libs/${libC}/src/lib/tslib-c-another.ts`);
expect(fileContent).toContain(`import { tslibC } from './tslib-c';`);
expect(fileContent).toContain(
`import { SOME_CONSTANT, someFunc1, someFunc2 } from './constant';`
);
const fileContentTslibC = readFile(`libs/${libC}/src/lib/tslib-c.ts`);
expect(fileContentTslibC).toContain(
`import { someFunc1, someFunc2, SOME_CONSTANT } from './constant';`
);
});
it('should fix noRelativeOrAbsoluteImportsAcrossLibraries', () => {
const stdout = runCLI(`lint ${libB}`, {
silenceError: true,
env: { CI: 'false' },
});
expect(stdout).toContain(
'Projects cannot be imported by a relative or absolute path, and must begin with a npm scope'
);
// fix them
const fixedStout = runCLI(`lint ${libB} --fix`, {
silenceError: true,
env: { CI: 'false' },
});
expect(fixedStout).toContain(
`Successfully ran target lint for project ${libB}`
);
const fileContent = readFile(`libs/${libB}/src/lib/tslib-b.ts`);
expect(fileContent).toContain(
`import { libASayHello } from '@${projScope}/${libA}';`
);
expect(fileContent).toContain(
`import { libASayHi } from '@${projScope}/${libA}';`
);
expect(fileContent).toContain(
`import { someSelectivelyExportedFn } from '@${projScope}/${libA}';`
);
});
});
describe('dependency checks', () => {
beforeAll(() => {
updateJson(`libs/${mylib}/.eslintrc.json`, (json) => {
if (!json.overrides.some((o) => o.rules?.['@nx/dependency-checks'])) {
json.overrides = [
...json.overrides,
{
files: ['*.json'],
parser: 'jsonc-eslint-parser',
rules: {
'@nx/dependency-checks': 'error',
},
},
];
}
return json;
});
});
afterAll(() => {
// ensure the rule for dependency checks is removed
// so that it does not affect other tests
updateJson(`libs/${mylib}/.eslintrc.json`, (json) => {
json.overrides = json.overrides.filter(
(o) => !o.rules?.['@nx/dependency-checks']
);
return json;
});
});
it('should report dependency check issues', () => {
const rootPackageJson = readJson('package.json');
const nxVersion = rootPackageJson.devDependencies.nx;
const tslibVersion =
rootPackageJson.dependencies['tslib'] ||
rootPackageJson.devDependencies['tslib'];
let out = runCLI(`lint ${mylib}`, {
silenceError: true,
env: { CI: 'false' },
});
expect(out).toContain('Successfully ran target lint');
// make an explict dependency to nx
updateFile(
`libs/${mylib}/src/lib/${mylib}.ts`,
(content) =>
`import { names } from '@nx/devkit';\n\n` +
content.replace(/=> .*;/, `=> names('${mylib}').className;`)
);
// intentionally set an obsolete dependency
updateJson(`libs/${mylib}/package.json`, (json) => {
json.dependencies['@nx/js'] = nxVersion;
return json;
});
// output should now report missing dependency and obsolete dependency
out = runCLI(`lint ${mylib}`, {
silenceError: true,
env: { CI: 'false' },
});
expect(out).toContain('they are missing');
expect(out).toContain('@nx/devkit');
expect(out).toContain(
`The "@nx/js" package is not used by "${mylib}" project`
);
// should fix the missing and obsolete dependency issues
out = runCLI(`lint ${mylib} --fix`, {
silenceError: true,
env: { CI: 'false' },
});
expect(out).toContain(
`Successfully ran target lint for project ${mylib}`
);
const packageJson = readJson(`libs/${mylib}/package.json`);
expect(packageJson).toMatchObject({
dependencies: {
'@nx/devkit': nxVersion,
tslib: tslibVersion,
},
main: './src/index.js',
name: `@proj/${mylib}`,
private: true,
type: 'commonjs',
types: './src/index.d.ts',
version: '0.0.1',
});
// intentionally set the invalid version
updateJson(`libs/${mylib}/package.json`, (json) => {
json.dependencies['@nx/devkit'] = '100.0.0';
return json;
});
out = runCLI(`lint ${mylib}`, {
silenceError: true,
env: { CI: 'false' },
});
expect(out).toContain(
'version specifier does not contain the installed version of "@nx/devkit"'
);
// should fix the version mismatch issue
out = runCLI(`lint ${mylib} --fix`, {
silenceError: true,
env: { CI: 'false' },
});
expect(out).toContain(
`Successfully ran target lint for project ${mylib}`
);
});
});
describe('flat config', () => {
let envVar: string | undefined;
beforeAll(() => {
runCLI(`generate @nx/eslint:convert-to-flat-config`);
envVar = process.env.ESLINT_USE_FLAT_CONFIG;
// Now that we have converted the existing configs to flat config we need to clear the explicitly set env var to allow it to infer things from the root config file type
delete process.env.ESLINT_USE_FLAT_CONFIG;
});
afterAll(() => {
process.env.ESLINT_USE_FLAT_CONFIG = envVar;
});
it('should generate new projects using flat config', () => {
const reactLib = uniq('react-lib');
const jsLib = uniq('js-lib');
runCLI(`generate @nx/react:lib ${reactLib} --linter eslint`);
runCLI(`generate @nx/js:lib ${jsLib} --linter eslint`);
checkFilesExist(
`${reactLib}/eslint.config.cjs`,
`${jsLib}/eslint.config.cjs`
);
checkFilesDoNotExist(
`${reactLib}/.eslintrc.json`,
`${jsLib}/.eslintrc.json`
);
// validate that the new projects are linted successfully
expect(() =>
runCLI(`lint ${reactLib}`, {
env: { CI: 'false' },
})
).not.toThrow();
expect(() =>
runCLI(`lint ${jsLib}`, {
env: { CI: 'false' },
})
).not.toThrow();
});
});
});
describe('Root projects migration', () => {
beforeEach(() =>
newProject({
packages: ['@nx/react', '@nx/js', '@nx/angular', '@nx/node'],
})
);
afterEach(() => cleanupProject());
function verifySuccessfulStandaloneSetup(myapp: string) {
expect(
runCLI(`lint ${myapp}`, { silenceError: true, env: { CI: 'false' } })
).toContain('Successfully ran target lint');
expect(
runCLI(`lint e2e`, { silenceError: true, env: { CI: 'false' } })
).toContain('Successfully ran target lint');
expect(() => checkFilesExist(`.eslintrc.base.json`)).toThrow();
const rootEslint = readJson('.eslintrc.json');
const e2eEslint = readJson('e2e/.eslintrc.json');
// should directly refer to nx plugin
expect(rootEslint.plugins).toEqual(['@nx']);
expect(e2eEslint.plugins).toEqual(['@nx']);
}
function verifySuccessfulMigratedSetup(myapp: string, mylib: string) {
expect(
runCLI(`lint ${myapp}`, { silenceError: true, env: { CI: 'false' } })
).toContain('Successfully ran target lint');
expect(
runCLI(`lint e2e`, { silenceError: true, env: { CI: 'false' } })
).toContain('Successfully ran target lint');
expect(
runCLI(`lint ${mylib}`, { silenceError: true, env: { CI: 'false' } })
).toContain('Successfully ran target lint');
expect(() => checkFilesExist(`.eslintrc.base.json`)).not.toThrow();
const rootEslint = readJson('.eslintrc.base.json');
const appEslint = readJson('.eslintrc.json');
const e2eEslint = readJson('e2e/.eslintrc.json');
const libEslint = readJson(`libs/${mylib}/.eslintrc.json`);
// should directly refer to nx plugin
expect(rootEslint.plugins).toEqual(['@nx']);
expect(appEslint.plugins).toBeUndefined();
expect(e2eEslint.plugins).toBeUndefined();
expect(libEslint.plugins).toBeUndefined();
// should extend base
expect(appEslint.extends.slice(-1)).toEqual(['./.eslintrc.base.json']);
expect(e2eEslint.extends.slice(-1)).toEqual(['../.eslintrc.base.json']);
expect(libEslint.extends.slice(-1)).toEqual([
'../../.eslintrc.base.json',
]);
}
it('(React standalone) should set root project config to app and e2e app and migrate when another lib is added', () => {
const myapp = uniq('myapp');
const mylib = uniq('mylib');
runCLI(
`generate @nx/react:app --name=${myapp} --unitTestRunner=jest --linter eslint --directory="."`
);
runCLI('reset', { env: { CI: 'false' } });
verifySuccessfulStandaloneSetup(myapp);
let appEslint = readJson('.eslintrc.json');
let e2eEslint = readJson('e2e/.eslintrc.json');
// should have plugin extends
let appOverrides = JSON.stringify(appEslint.overrides);
expect(appOverrides).toContain('plugin:@nx/javascript');
expect(appOverrides).toContain('plugin:@nx/typescript');
let e2eOverrides = JSON.stringify(e2eEslint.overrides);
expect(e2eOverrides).toContain('plugin:@nx/javascript');
runCLI(
`generate @nx/js:lib libs/${mylib} --unitTestRunner=jest --linter eslint`
);
runCLI('reset', { env: { CI: 'false' } });
verifySuccessfulMigratedSetup(myapp, mylib);
appEslint = readJson(`.eslintrc.json`);
e2eEslint = readJson('e2e/.eslintrc.json');
// should have no plugin extends
appOverrides = JSON.stringify(appEslint.overrides);
expect(appOverrides).not.toContain('plugin:@nx/javascript');
expect(appOverrides).not.toContain('plugin:@nx/typescript');
e2eOverrides = JSON.stringify(e2eEslint.overrides);
expect(e2eOverrides).not.toContain('plugin:@nx/javascript');
expect(e2eOverrides).not.toContain('plugin:@nx/typescript');
});
it('(Angular standalone) should set root project config to app and e2e app and migrate when another lib is added', () => {
const myapp = uniq('myapp');
const mylib = uniq('mylib');
runCLI(
`generate @nx/angular:app --name=${myapp} --directory="." --linter eslint --no-interactive`
);
runCLI('reset', { env: { CI: 'false' } });
verifySuccessfulStandaloneSetup(myapp);
let appEslint = readJson('.eslintrc.json');
let e2eEslint = readJson('e2e/.eslintrc.json');
// should have plugin extends
let appOverrides = JSON.stringify(appEslint.overrides);
expect(appOverrides).toContain('plugin:@nx/typescript');
let e2eOverrides = JSON.stringify(e2eEslint.overrides);
expect(e2eOverrides).toContain('plugin:@nx/javascript');
runCLI(
`generate @nx/js:lib libs/${mylib} --linter eslint --no-interactive`
);
runCLI('reset', { env: { CI: 'false' } });
verifySuccessfulMigratedSetup(myapp, mylib);
appEslint = readJson(`.eslintrc.json`);
e2eEslint = readJson('e2e/.eslintrc.json');
// should have no plugin extends
appOverrides = JSON.stringify(appEslint.overrides);
expect(appOverrides).not.toContain('plugin:@nx/typescript');
e2eOverrides = JSON.stringify(e2eEslint.overrides);
expect(e2eOverrides).not.toContain('plugin:@nx/typescript');
});
it('(Node standalone) should set root project config to app and e2e app and migrate when another lib is added', async () => {
const myapp = uniq('myapp');
const mylib = uniq('mylib');
runCLI(
`generate @nx/node:app --name=${myapp} --linter=eslint --directory="." --unitTestRunner=jest --e2eTestRunner=jest --no-interactive`
);
runCLI('reset', { env: { CI: 'false' } });
verifySuccessfulStandaloneSetup(myapp);
let appEslint = readJson('.eslintrc.json');
let e2eEslint = readJson('e2e/.eslintrc.json');
// should have plugin extends
let appOverrides = JSON.stringify(appEslint.overrides);
expect(appOverrides).toContain('plugin:@nx/javascript');
expect(appOverrides).toContain('plugin:@nx/typescript');
let e2eOverrides = JSON.stringify(e2eEslint.overrides);
expect(e2eOverrides).toContain('plugin:@nx/javascript');
expect(e2eOverrides).toContain('plugin:@nx/typescript');
runCLI(
`generate @nx/js:lib libs/${mylib} --linter eslint --no-interactive`
);
runCLI('reset', { env: { CI: 'false' } });
verifySuccessfulMigratedSetup(myapp, mylib);
appEslint = readJson(`.eslintrc.json`);
e2eEslint = readJson('e2e/.eslintrc.json');
// should have no plugin extends
// should have no plugin extends
appOverrides = JSON.stringify(appEslint.overrides);
expect(appOverrides).not.toContain('plugin:@nx/javascript');
expect(appOverrides).not.toContain('plugin:@nx/typescript');
e2eOverrides = JSON.stringify(e2eEslint.overrides);
expect(e2eOverrides).not.toContain('plugin:@nx/javascript');
expect(e2eOverrides).not.toContain('plugin:@nx/typescript');
});
});
});
/**
* Update the generated rule implementation to produce a known lint error from all files.
*
* It is important that we do this surgically via AST transformations, otherwise we will
* drift further and further away from the original generated code and therefore make our
* e2e test less accurate and less valuable.
*/
function updateGeneratedRuleImplementation(
newRulePath: string,
newRuleGeneratedContents: string,
knownLintErrorMessage: string,
messageId,
libMethodName: string,
libPath: string
): string {
const printer = ts.createPrinter({ newLine: ts.NewLineKind.LineFeed });
const newRuleSourceFile = ts.createSourceFile(
newRulePath,
newRuleGeneratedContents,
ts.ScriptTarget.Latest,
true
);
const transformer = <T extends ts.SourceFile>(
context: ts.TransformationContext
) =>
((rootNode: T) => {
function visit(node: ts.Node): ts.Node {
/**
* Add an ESLint messageId which will show the knownLintErrorMessage
*
* i.e.
*
* messages: {
* e2eMessageId: knownLintErrorMessage
* }
*/
if (
ts.isPropertyAssignment(node) &&
ts.isIdentifier(node.name) &&
node.name.escapedText === 'messages'
) {
return ts.factory.updatePropertyAssignment(
node,
node.name,
ts.factory.createObjectLiteralExpression([
ts.factory.createPropertyAssignment(
messageId,
ts.factory.createStringLiteral(knownLintErrorMessage)
),
])
);
}
/**
* Update the rule implementation to report the knownLintErrorMessage on every Program node
*
* During the debugging of the switch from ts-node to swc-node we found out
* that regular rules would work even without explicit path mapping registration,
* but rules that import runtime functionality from within the workspace
* would break the rule registration.
*
* Instead of having a static literal messageId we retreieved it via imported getMessageId method.
*
* i.e.
*
* create(context) {
* return {
* Program(node) {
* context.report({
* messageId: getMessageId(),
* node,
* });
* }
* }
* }
*/
if (
ts.isMethodDeclaration(node) &&
ts.isIdentifier(node.name) &&
node.name.escapedText === 'create'
) {
return ts.factory.updateMethodDeclaration(
node,
node.modifiers,
node.asteriskToken,
node.name,
node.questionToken,
node.typeParameters,
node.parameters,
node.type,
ts.factory.createBlock([
ts.factory.createReturnStatement(
ts.factory.createObjectLiteralExpression([
ts.factory.createMethodDeclaration(
[],
undefined,
'Program',
undefined,
[],
[
ts.factory.createParameterDeclaration(
[],
undefined,
'node',
undefined,
undefined,
undefined
),
],
undefined,
ts.factory.createBlock([
ts.factory.createExpressionStatement(
ts.factory.createCallExpression(
ts.factory.createPropertyAccessExpression(
ts.factory.createIdentifier('context'),
'report'
),
[],
[
ts.factory.createObjectLiteralExpression([
ts.factory.createPropertyAssignment(
'messageId',
ts.factory.createCallExpression(
ts.factory.createIdentifier(libMethodName),
[],
[]
)
),
ts.factory.createShorthandPropertyAssignment(
'node'
),
]),
]
)
),
])
),
])
),
])
);
}
return ts.visitEachChild(node, visit, context);
}
/**
* Add lib import as a first line of the rule file.
* Needed for the access of getMessageId in the context report above.
*
* i.e.
*
* import { getMessageId } from "@myproj/mylib";
*
*/
const importAdded = ts.factory.updateSourceFile(rootNode, [
ts.factory.createImportDeclaration(
undefined,
ts.factory.createImportClause(
false,
undefined,
ts.factory.createNamedImports([
ts.factory.createImportSpecifier(
false,
undefined,
ts.factory.createIdentifier(libMethodName)
),
])
),
ts.factory.createStringLiteral(libPath)
),
...rootNode.statements,
]);
return ts.visitNode(importAdded, visit);
}) as ts.Transformer<T>;
const result: ts.TransformationResult<ts.SourceFile> =
ts.transform<ts.SourceFile>(newRuleSourceFile, [transformer]);
const updatedSourceFile: ts.SourceFile = result.transformed[0];
return printer.printFile(updatedSourceFile);
}