feat(linter): create new workspaces with ESLint v9 and typescript-eslint v8 (#27404)

Closes #27451

---------

Co-authored-by: Leosvel Pérez Espinosa <leosvel.perez.espinosa@gmail.com>
Co-authored-by: Jack Hsu <jack.hsu@gmail.com>
This commit is contained in:
James Henry 2024-09-13 00:02:27 +04:00 committed by GitHub
parent 2e0f374964
commit 68eeb2eeed
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
112 changed files with 3970 additions and 1582 deletions

View File

@ -41,7 +41,7 @@ describe('Move Angular Project', () => {
expect(moveOutput).toContain(`CREATE ${newPath}/tsconfig.app.json`); expect(moveOutput).toContain(`CREATE ${newPath}/tsconfig.app.json`);
expect(moveOutput).toContain(`CREATE ${newPath}/tsconfig.json`); expect(moveOutput).toContain(`CREATE ${newPath}/tsconfig.json`);
expect(moveOutput).toContain(`CREATE ${newPath}/tsconfig.spec.json`); expect(moveOutput).toContain(`CREATE ${newPath}/tsconfig.spec.json`);
expect(moveOutput).toContain(`CREATE ${newPath}/.eslintrc.json`); expect(moveOutput).toContain(`CREATE ${newPath}/eslint.config.js`);
expect(moveOutput).toContain(`CREATE ${newPath}/public/favicon.ico`); expect(moveOutput).toContain(`CREATE ${newPath}/public/favicon.ico`);
expect(moveOutput).toContain(`CREATE ${newPath}/src/index.html`); expect(moveOutput).toContain(`CREATE ${newPath}/src/index.html`);
expect(moveOutput).toContain(`CREATE ${newPath}/src/main.ts`); expect(moveOutput).toContain(`CREATE ${newPath}/src/main.ts`);

View File

@ -127,8 +127,7 @@ describe('Angular Projects', () => {
// check e2e tests // check e2e tests
if (runE2ETests('playwright')) { if (runE2ETests('playwright')) {
const e2eResults = runCLI(`e2e ${app1}-e2e`); expect(() => runCLI(`e2e ${app1}-e2e`)).not.toThrow();
expect(e2eResults).toContain('Successfully ran target e2e for project');
expect(await killPort(4200)).toBeTruthy(); expect(await killPort(4200)).toBeTruthy();
} }
@ -160,10 +159,7 @@ describe('Angular Projects', () => {
); );
if (runE2ETests('playwright')) { if (runE2ETests('playwright')) {
const e2eResults = runCLI(`e2e ${app}-e2e`); expect(() => runCLI(`e2e ${app}-e2e`)).not.toThrow();
expect(e2eResults).toContain(
`Successfully ran target e2e for project ${app}-e2e`
);
expect(await killPort(4200)).toBeTruthy(); expect(await killPort(4200)).toBeTruthy();
} }
}, 1000000); }, 1000000);
@ -495,7 +491,7 @@ describe('Angular Projects', () => {
updateFile(`${lib}/src/lib/${lib}.module.ts`, moduleContent); updateFile(`${lib}/src/lib/${lib}.module.ts`, moduleContent);
// ACT // ACT
const buildOutput = runCLI(`build ${lib}`); const buildOutput = runCLI(`build ${lib}`, { env: { CI: 'false' } });
// ASSERT // ASSERT
expect(buildOutput).toContain(`Building entry point '@${proj}/${lib}'`); expect(buildOutput).toContain(`Building entry point '@${proj}/${lib}'`);
@ -516,14 +512,9 @@ describe('Angular Projects', () => {
// check files are generated with the layout directory ("apps/") // check files are generated with the layout directory ("apps/")
checkFilesExist(`apps/${appName}/src/app/app.module.ts`); checkFilesExist(`apps/${appName}/src/app/app.module.ts`);
// check build works // check build works
expect(runCLI(`build ${appName}`)).toContain( expect(() => runCLI(`build ${appName}`)).not.toThrow();
`Successfully ran target build for project ${appName}`
);
// check tests pass // check tests pass
const appTestResult = runCLI(`test ${appName}`); expect(() => runCLI(`test ${appName}`)).not.toThrow();
expect(appTestResult).toContain(
`Successfully ran target test for project ${appName}`
);
runCLI( runCLI(
`generate @nx/angular:lib ${libName} --standalone --buildable --project-name-and-root-format=derived` `generate @nx/angular:lib ${libName} --standalone --buildable --project-name-and-root-format=derived`
@ -535,14 +526,9 @@ describe('Angular Projects', () => {
`libs/${libName}/src/lib/${libName}/${libName}.component.ts` `libs/${libName}/src/lib/${libName}/${libName}.component.ts`
); );
// check build works // check build works
expect(runCLI(`build ${libName}`)).toContain( expect(() => runCLI(`build ${libName}`)).not.toThrow();
`Successfully ran target build for project ${libName}`
);
// check tests pass // check tests pass
const libTestResult = runCLI(`test ${libName}`); expect(() => runCLI(`test ${libName}`)).not.toThrow();
expect(libTestResult).toContain(
`Successfully ran target test for project ${libName}`
);
}, 500_000); }, 500_000);
it('should support generating libraries with a scoped name when --project-name-and-root-format=as-provided', () => { it('should support generating libraries with a scoped name when --project-name-and-root-format=as-provided', () => {
@ -568,14 +554,9 @@ describe('Angular Projects', () => {
}.component.ts` }.component.ts`
); );
// check build works // check build works
expect(runCLI(`build ${libName}`)).toContain( expect(() => runCLI(`build ${libName}`)).not.toThrow();
`Successfully ran target build for project ${libName}`
);
// check tests pass // check tests pass
const libTestResult = runCLI(`test ${libName}`); expect(() => runCLI(`test ${libName}`)).not.toThrow();
expect(libTestResult).toContain(
`Successfully ran target test for project ${libName}`
);
}, 500_000); }, 500_000);
it('should support generating applications with SSR and converting targets with webpack-based executors to use the application executor', async () => { it('should support generating applications with SSR and converting targets with webpack-based executors to use the application executor', async () => {

View File

@ -162,6 +162,7 @@ describe('EsBuild Plugin', () => {
expect( expect(
readJson(`dist/libs/${parentLib}/package.json`).dependencies readJson(`dist/libs/${parentLib}/package.json`).dependencies
).toEqual({ ).toEqual({
'jsonc-eslint-parser': expect.any(String),
// Don't care about the versions, just that they exist // Don't care about the versions, just that they exist
rambda: expect.any(String), rambda: expect.any(String),
lodash: expect.any(String), lodash: expect.any(String),

View File

@ -14,14 +14,17 @@ import {
} from '@nx/e2e/utils'; } from '@nx/e2e/utils';
describe('Linter (legacy)', () => { describe('Linter (legacy)', () => {
describe('Integrated', () => { describe('Integrated (eslintrc config)', () => {
let originalEslintUseFlatConfigVal: string | undefined;
const myapp = uniq('myapp'); const myapp = uniq('myapp');
const mylib = uniq('mylib'); const mylib = uniq('mylib');
let projScope;
beforeAll(() => { beforeAll(() => {
projScope = newProject({ // Opt into legacy .eslintrc config format for these tests
originalEslintUseFlatConfigVal = process.env.ESLINT_USE_FLAT_CONFIG;
process.env.ESLINT_USE_FLAT_CONFIG = 'false';
newProject({
packages: ['@nx/react', '@nx/js', '@nx/eslint'], packages: ['@nx/react', '@nx/js', '@nx/eslint'],
}); });
runCLI(`generate @nx/react:app ${myapp} --tags=validtag`, { runCLI(`generate @nx/react:app ${myapp} --tags=validtag`, {
@ -31,7 +34,10 @@ describe('Linter (legacy)', () => {
env: { NX_ADD_PLUGINS: 'false' }, env: { NX_ADD_PLUGINS: 'false' },
}); });
}); });
afterAll(() => cleanupProject()); afterAll(() => {
process.env.ESLINT_USE_FLAT_CONFIG = originalEslintUseFlatConfigVal;
cleanupProject();
});
describe('linting errors', () => { describe('linting errors', () => {
let defaultEslintrc; let defaultEslintrc;
@ -58,8 +64,7 @@ describe('Linter (legacy)', () => {
updateFile('.eslintrc.json', JSON.stringify(eslintrc, null, 2)); updateFile('.eslintrc.json', JSON.stringify(eslintrc, null, 2));
// 1. linting should error when rules are not followed // 1. linting should error when rules are not followed
let out = runCLI(`lint ${myapp}`, { silenceError: true }); expect(() => runCLI(`lint ${myapp}`)).toThrow();
expect(out).toContain('Unexpected console statement');
// 2. linting should not error when rules are not followed and the force flag is specified // 2. linting should not error when rules are not followed and the force flag is specified
expect(() => runCLI(`lint ${myapp} --force`)).not.toThrow(); expect(() => runCLI(`lint ${myapp} --force`)).not.toThrow();
@ -72,8 +77,9 @@ describe('Linter (legacy)', () => {
updateFile('.eslintrc.json', JSON.stringify(eslintrc, null, 2)); updateFile('.eslintrc.json', JSON.stringify(eslintrc, null, 2));
// 3. linting should not error when all rules are followed // 3. linting should not error when all rules are followed
out = runCLI(`lint ${myapp}`, { silenceError: true }); expect(() =>
expect(out).toContain('All files pass linting'); runCLI(`lint ${myapp}`, { silenceError: true })
).not.toThrow();
}, 1000000); }, 1000000);
it('should print the effective configuration for a file specified using --print-config', () => { it('should print the effective configuration for a file specified using --print-config', () => {
@ -86,6 +92,7 @@ describe('Linter (legacy)', () => {
}); });
updateFile('.eslintrc.json', JSON.stringify(eslint, null, 2)); updateFile('.eslintrc.json', JSON.stringify(eslint, null, 2));
const out = runCLI(`lint ${myapp} --print-config src/index.ts`, { const out = runCLI(`lint ${myapp} --print-config src/index.ts`, {
env: { CI: 'false' }, // We don't want to show the summary table from cloud runner
silenceError: true, silenceError: true,
}); });
expect(out).toContain('"specific-rule": ['); expect(out).toContain('"specific-rule": [');
@ -93,9 +100,19 @@ describe('Linter (legacy)', () => {
}); });
}); });
describe('Flat config', () => { describe('eslintrc convert to flat config', () => {
let originalEslintUseFlatConfigVal: string | undefined;
const packageManager = getSelectedPackageManager() || 'pnpm'; const packageManager = getSelectedPackageManager() || 'pnpm';
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;
});
beforeEach(() => { beforeEach(() => {
process.env.NX_ADD_PLUGINS = 'false'; process.env.NX_ADD_PLUGINS = 'false';
}); });
@ -162,7 +179,9 @@ describe('Linter (legacy)', () => {
const outFlat = runCLI(`affected -t lint`, { const outFlat = runCLI(`affected -t lint`, {
silenceError: true, silenceError: true,
}); });
expect(outFlat).toContain('ran target lint'); expect(outFlat).toContain(`${myapp}:lint`);
expect(outFlat).toContain(`${mylib}:lint`);
expect(outFlat).toContain(`${mylib2}:lint`);
}, 1000000); }, 1000000);
it('should convert standalone to flat config', () => { it('should convert standalone to flat config', () => {
@ -199,7 +218,8 @@ describe('Linter (legacy)', () => {
const outFlat = runCLI(`affected -t lint`, { const outFlat = runCLI(`affected -t lint`, {
silenceError: true, silenceError: true,
}); });
expect(outFlat).toContain('ran target lint'); expect(outFlat).toContain(`${myapp}:lint`);
expect(outFlat).toContain(`${mylib}:lint`);
}, 1000000); }, 1000000);
}); });
}); });

View File

@ -15,6 +15,16 @@ import {
import * as ts from 'typescript'; import * as ts from 'typescript';
describe('Linter', () => { 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', () => { describe('Integrated', () => {
const myapp = uniq('myapp'); const myapp = uniq('myapp');
const mylib = uniq('mylib'); const mylib = uniq('mylib');
@ -54,7 +64,10 @@ describe('Linter', () => {
}); });
updateFile('.eslintrc.json', JSON.stringify(eslintrc, null, 2)); updateFile('.eslintrc.json', JSON.stringify(eslintrc, null, 2));
let out = runCLI(`lint ${myapp}`, { silenceError: true }); let out = runCLI(`lint ${myapp}`, {
silenceError: true,
env: { CI: 'false' },
});
expect(out).toContain('Unexpected console statement'); expect(out).toContain('Unexpected console statement');
eslintrc.overrides.forEach((override) => { eslintrc.overrides.forEach((override) => {
@ -65,7 +78,10 @@ describe('Linter', () => {
updateFile('.eslintrc.json', JSON.stringify(eslintrc, null, 2)); updateFile('.eslintrc.json', JSON.stringify(eslintrc, null, 2));
// 3. linting should not error when all rules are followed // 3. linting should not error when all rules are followed
out = runCLI(`lint ${myapp}`, { silenceError: true }); out = runCLI(`lint ${myapp}`, {
silenceError: true,
env: { CI: 'false' },
});
expect(out).toContain('Successfully ran target lint'); expect(out).toContain('Successfully ran target lint');
}, 1000000); }, 1000000);
@ -80,7 +96,10 @@ describe('Linter', () => {
// should generate a default cache file // should generate a default cache file
let cachePath = path.join('apps', myapp, '.eslintcache'); let cachePath = path.join('apps', myapp, '.eslintcache');
expect(() => checkFilesExist(cachePath)).toThrow(); expect(() => checkFilesExist(cachePath)).toThrow();
runCLI(`lint ${myapp} --cache`, { silenceError: true }); runCLI(`lint ${myapp} --cache`, {
silenceError: true,
env: { CI: 'false' },
});
expect(() => checkFilesExist(cachePath)).not.toThrow(); expect(() => checkFilesExist(cachePath)).not.toThrow();
expect(readCacheFile(cachePath)).toContain( expect(readCacheFile(cachePath)).toContain(
path.normalize(`${myapp}/src/app/app.spec.tsx`) path.normalize(`${myapp}/src/app/app.spec.tsx`)
@ -91,6 +110,7 @@ describe('Linter', () => {
expect(() => checkFilesExist(cachePath)).toThrow(); expect(() => checkFilesExist(cachePath)).toThrow();
runCLI(`lint ${myapp} --cache --cache-location="my-cache"`, { runCLI(`lint ${myapp} --cache --cache-location="my-cache"`, {
silenceError: true, silenceError: true,
env: { CI: 'false' },
}); });
expect(() => checkFilesExist(cachePath)).not.toThrow(); expect(() => checkFilesExist(cachePath)).not.toThrow();
expect(readCacheFile(cachePath)).toContain( expect(readCacheFile(cachePath)).toContain(
@ -116,6 +136,7 @@ describe('Linter', () => {
`lint ${myapp} --output-file="${outputFile}" --format=json`, `lint ${myapp} --output-file="${outputFile}" --format=json`,
{ {
silenceError: true, silenceError: true,
env: { CI: 'false' },
} }
); );
expect(stdout).not.toContain('Unexpected console statement'); expect(stdout).not.toContain('Unexpected console statement');
@ -147,8 +168,7 @@ describe('Linter', () => {
runCLI(`generate @nx/eslint:workspace-rule ${newRuleName}`); runCLI(`generate @nx/eslint:workspace-rule ${newRuleName}`);
// Ensure that the unit tests for the new rule are runnable // Ensure that the unit tests for the new rule are runnable
const unitTestsOutput = runCLI(`test eslint-rules`); expect(() => runCLI(`test eslint-rules`)).not.toThrow();
expect(unitTestsOutput).toContain('Successfully ran target test');
// Update the rule for the e2e test so that we can assert that it produces the expected lint failure when used // 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 knownLintErrorMessage = 'e2e test known error message';
@ -177,6 +197,7 @@ describe('Linter', () => {
const lintOutput = runCLI(`lint ${myapp} --verbose`, { const lintOutput = runCLI(`lint ${myapp} --verbose`, {
silenceError: true, silenceError: true,
env: { CI: 'false' },
}); });
expect(lintOutput).toContain(newRuleNameForUsage); expect(lintOutput).toContain(newRuleNameForUsage);
expect(lintOutput).toContain(knownLintErrorMessage); expect(lintOutput).toContain(knownLintErrorMessage);
@ -232,7 +253,10 @@ describe('Linter', () => {
` `
); );
const out = runCLI(`lint ${myapp}`, { silenceError: true }); const out = runCLI(`lint ${myapp}`, {
silenceError: true,
env: { CI: 'false' },
});
expect(out).toContain( expect(out).toContain(
'Projects cannot be imported by a relative or absolute path, and must begin with a npm scope' 'Projects cannot be imported by a relative or absolute path, and must begin with a npm scope'
); );
@ -379,6 +403,7 @@ describe('Linter', () => {
it('should fix noSelfCircularDependencies', () => { it('should fix noSelfCircularDependencies', () => {
const stdout = runCLI(`lint ${libC}`, { const stdout = runCLI(`lint ${libC}`, {
silenceError: true, silenceError: true,
env: { CI: 'false' },
}); });
expect(stdout).toContain( expect(stdout).toContain(
'Projects should use relative imports to import from other files within the same project' 'Projects should use relative imports to import from other files within the same project'
@ -387,6 +412,7 @@ describe('Linter', () => {
// fix them // fix them
const fixedStout = runCLI(`lint ${libC} --fix`, { const fixedStout = runCLI(`lint ${libC} --fix`, {
silenceError: true, silenceError: true,
env: { CI: 'false' },
}); });
expect(fixedStout).toContain( expect(fixedStout).toContain(
`Successfully ran target lint for project ${libC}` `Successfully ran target lint for project ${libC}`
@ -407,6 +433,7 @@ describe('Linter', () => {
it('should fix noRelativeOrAbsoluteImportsAcrossLibraries', () => { it('should fix noRelativeOrAbsoluteImportsAcrossLibraries', () => {
const stdout = runCLI(`lint ${libB}`, { const stdout = runCLI(`lint ${libB}`, {
silenceError: true, silenceError: true,
env: { CI: 'false' },
}); });
expect(stdout).toContain( expect(stdout).toContain(
'Projects cannot be imported by a relative or absolute path, and must begin with a npm scope' 'Projects cannot be imported by a relative or absolute path, and must begin with a npm scope'
@ -415,6 +442,7 @@ describe('Linter', () => {
// fix them // fix them
const fixedStout = runCLI(`lint ${libB} --fix`, { const fixedStout = runCLI(`lint ${libB} --fix`, {
silenceError: true, silenceError: true,
env: { CI: 'false' },
}); });
expect(fixedStout).toContain( expect(fixedStout).toContain(
`Successfully ran target lint for project ${libB}` `Successfully ran target lint for project ${libB}`
@ -468,7 +496,10 @@ describe('Linter', () => {
const nxVersion = rootPackageJson.devDependencies.nx; const nxVersion = rootPackageJson.devDependencies.nx;
const tslibVersion = rootPackageJson.dependencies['tslib']; const tslibVersion = rootPackageJson.dependencies['tslib'];
let out = runCLI(`lint ${mylib}`, { silenceError: true }); let out = runCLI(`lint ${mylib}`, {
silenceError: true,
env: { CI: 'false' },
});
expect(out).toContain('Successfully ran target lint'); expect(out).toContain('Successfully ran target lint');
// make an explict dependency to nx // make an explict dependency to nx
@ -485,7 +516,10 @@ describe('Linter', () => {
}); });
// output should now report missing dependency and obsolete dependency // output should now report missing dependency and obsolete dependency
out = runCLI(`lint ${mylib}`, { silenceError: true }); out = runCLI(`lint ${mylib}`, {
silenceError: true,
env: { CI: 'false' },
});
expect(out).toContain('they are missing'); expect(out).toContain('they are missing');
expect(out).toContain('@nx/devkit'); expect(out).toContain('@nx/devkit');
expect(out).toContain( expect(out).toContain(
@ -493,7 +527,10 @@ describe('Linter', () => {
); );
// should fix the missing and obsolete dependency issues // should fix the missing and obsolete dependency issues
out = runCLI(`lint ${mylib} --fix`, { silenceError: true }); out = runCLI(`lint ${mylib} --fix`, {
silenceError: true,
env: { CI: 'false' },
});
expect(out).toContain( expect(out).toContain(
`Successfully ran target lint for project ${mylib}` `Successfully ran target lint for project ${mylib}`
); );
@ -518,13 +555,19 @@ describe('Linter', () => {
json.dependencies['@nx/devkit'] = '100.0.0'; json.dependencies['@nx/devkit'] = '100.0.0';
return json; return json;
}); });
out = runCLI(`lint ${mylib}`, { silenceError: true }); out = runCLI(`lint ${mylib}`, {
silenceError: true,
env: { CI: 'false' },
});
expect(out).toContain( expect(out).toContain(
'version specifier does not contain the installed version of "@nx/devkit"' 'version specifier does not contain the installed version of "@nx/devkit"'
); );
// should fix the version mismatch issue // should fix the version mismatch issue
out = runCLI(`lint ${mylib} --fix`, { silenceError: true }); out = runCLI(`lint ${mylib} --fix`, {
silenceError: true,
env: { CI: 'false' },
});
expect(out).toContain( expect(out).toContain(
`Successfully ran target lint for project ${mylib}` `Successfully ran target lint for project ${mylib}`
); );
@ -532,8 +575,15 @@ describe('Linter', () => {
}); });
describe('flat config', () => { describe('flat config', () => {
let envVar: string | undefined;
beforeAll(() => { beforeAll(() => {
runCLI(`generate @nx/eslint:convert-to-flat-config`); 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', () => { it('should generate new projects using flat config', () => {
@ -557,14 +607,8 @@ describe('Linter', () => {
); );
// validate that the new projects are linted successfully // validate that the new projects are linted successfully
let output = runCLI(`lint ${reactLib}`); expect(() => runCLI(`lint ${reactLib}`)).not.toThrow();
expect(output).toContain( expect(() => runCLI(`lint ${jsLib}`)).not.toThrow();
`Successfully ran target lint for project ${reactLib}`
);
output = runCLI(`lint ${jsLib}`);
expect(output).toContain(
`Successfully ran target lint for project ${jsLib}`
);
}); });
}); });
}); });
@ -578,12 +622,12 @@ describe('Linter', () => {
afterEach(() => cleanupProject()); afterEach(() => cleanupProject());
function verifySuccessfulStandaloneSetup(myapp: string) { function verifySuccessfulStandaloneSetup(myapp: string) {
expect(runCLI(`lint ${myapp}`, { silenceError: true })).toContain( expect(
'Successfully ran target lint' runCLI(`lint ${myapp}`, { silenceError: true, env: { CI: 'false' } })
); ).toContain('Successfully ran target lint');
expect(runCLI(`lint e2e`, { silenceError: true })).toContain( expect(
'Successfully ran target lint' runCLI(`lint e2e`, { silenceError: true, env: { CI: 'false' } })
); ).toContain('Successfully ran target lint');
expect(() => checkFilesExist(`.eslintrc.base.json`)).toThrow(); expect(() => checkFilesExist(`.eslintrc.base.json`)).toThrow();
const rootEslint = readJson('.eslintrc.json'); const rootEslint = readJson('.eslintrc.json');
@ -595,15 +639,15 @@ describe('Linter', () => {
} }
function verifySuccessfulMigratedSetup(myapp: string, mylib: string) { function verifySuccessfulMigratedSetup(myapp: string, mylib: string) {
expect(runCLI(`lint ${myapp}`, { silenceError: true })).toContain( expect(
'Successfully ran target lint' runCLI(`lint ${myapp}`, { silenceError: true, env: { CI: 'false' } })
); ).toContain('Successfully ran target lint');
expect(runCLI(`lint e2e`, { silenceError: true })).toContain( expect(
'Successfully ran target lint' runCLI(`lint e2e`, { silenceError: true, env: { CI: 'false' } })
); ).toContain('Successfully ran target lint');
expect(runCLI(`lint ${mylib}`, { silenceError: true })).toContain( expect(
'Successfully ran target lint' runCLI(`lint ${mylib}`, { silenceError: true, env: { CI: 'false' } })
); ).toContain('Successfully ran target lint');
expect(() => checkFilesExist(`.eslintrc.base.json`)).not.toThrow(); expect(() => checkFilesExist(`.eslintrc.base.json`)).not.toThrow();
const rootEslint = readJson('.eslintrc.base.json'); const rootEslint = readJson('.eslintrc.base.json');

View File

@ -1,7 +1,6 @@
import { import {
checkFilesExist, checkFilesExist,
cleanupProject, cleanupProject,
expectTestsPass,
getPackageManagerCommand, getPackageManagerCommand,
killPorts, killPorts,
newProject, newProject,
@ -64,8 +63,8 @@ describe('@nx/expo (legacy)', () => {
return updated; return updated;
}); });
expectTestsPass(await runCLIAsync(`test ${appName}`)); expect(() => runCLI(`test ${appName}`)).not.toThrow();
expectTestsPass(await runCLIAsync(`test ${libName}`)); expect(() => runCLI(`test ${libName}`)).not.toThrow();
const appLintResults = await runCLIAsync(`lint ${appName}`); const appLintResults = await runCLIAsync(`lint ${appName}`);
expect(appLintResults.combinedOutput).toContain( expect(appLintResults.combinedOutput).toContain(

View File

@ -16,10 +16,7 @@ describe('Jest root projects', () => {
}); });
it('should test root level app projects', async () => { it('should test root level app projects', async () => {
const rootProjectTestResults = await runCLIAsync(`test ${myapp}`); expect(() => runCLI(`test ${myapp}`)).not.toThrow();
expect(rootProjectTestResults.combinedOutput).toContain(
'Test Suites: 1 passed, 1 total'
);
}, 300_000); }, 300_000);
it('should add lib project and tests should still work', async () => { it('should add lib project and tests should still work', async () => {
@ -27,17 +24,8 @@ describe('Jest root projects', () => {
`generate @nx/angular:lib ${mylib} --projectNameAndRootFormat as-provided --no-interactive` `generate @nx/angular:lib ${mylib} --projectNameAndRootFormat as-provided --no-interactive`
); );
const libProjectTestResults = await runCLIAsync(`test ${mylib}`); expect(() => runCLI(`test ${mylib}`)).not.toThrow();
expect(() => runCLI(`test ${myapp}`)).not.toThrow();
expect(libProjectTestResults.combinedOutput).toContain(
'Test Suites: 1 passed, 1 total'
);
const rootProjectTestResults = await runCLIAsync(`test ${myapp}`);
expect(rootProjectTestResults.combinedOutput).toContain(
'Test Suites: 1 passed, 1 total'
);
}, 300_000); }, 300_000);
}); });
@ -53,11 +41,7 @@ describe('Jest root projects', () => {
}); });
it('should test root level app projects', async () => { it('should test root level app projects', async () => {
const rootProjectTestResults = await runCLIAsync(`test ${myapp}`); expect(() => runCLI(`test ${myapp}`)).not.toThrow();
expect(rootProjectTestResults.combinedOutput).toContain(
'Test Suites: 1 passed, 1 total'
);
}, 300_000); }, 300_000);
it('should add lib project and tests should still work', async () => { it('should add lib project and tests should still work', async () => {
@ -65,17 +49,8 @@ describe('Jest root projects', () => {
`generate @nx/react:lib ${mylib} --unitTestRunner=jest --projectNameAndRootFormat as-provided` `generate @nx/react:lib ${mylib} --unitTestRunner=jest --projectNameAndRootFormat as-provided`
); );
const libProjectTestResults = await runCLIAsync(`test ${mylib}`); expect(() => runCLI(`test ${mylib}`)).not.toThrow();
expect(() => runCLI(`test ${myapp}`)).not.toThrow();
expect(libProjectTestResults.combinedOutput).toContain(
'Test Suites: 1 passed, 1 total'
);
const rootProjectTestResults = await runCLIAsync(`test ${myapp}`);
expect(rootProjectTestResults.combinedOutput).toContain(
'Test Suites: 1 passed, 1 total'
);
}, 300_000); }, 300_000);
}); });
}); });

View File

@ -10,6 +10,7 @@ import {
getPackageManagerCommand, getPackageManagerCommand,
readJson, readJson,
updateFile, updateFile,
renameFile,
} from '@nx/e2e/utils'; } from '@nx/e2e/utils';
import { join } from 'path'; import { join } from 'path';
@ -169,6 +170,16 @@ describe('packaging libs', () => {
`libs/${swcEsmLib}/src/index.ts`, `libs/${swcEsmLib}/src/index.ts`,
`export * from './lib/${swcEsmLib}.js';` `export * from './lib/${swcEsmLib}.js';`
); );
// We also need to update the eslint config file extensions to be explicitly commonjs
// TODO: re-evaluate this once we support ESM eslint configs
renameFile(
`libs/${tscEsmLib}/eslint.config.js`,
`libs/${tscEsmLib}/eslint.config.cjs`
);
renameFile(
`libs/${swcEsmLib}/eslint.config.js`,
`libs/${swcEsmLib}/eslint.config.cjs`
);
// Add additional entry points for `exports` field // Add additional entry points for `exports` field
updateJson(join('libs', tscLib, 'project.json'), (json) => { updateJson(join('libs', tscLib, 'project.json'), (json) => {

View File

@ -30,22 +30,17 @@ describe('Nuxt Plugin', () => {
}); });
it('should build application', async () => { it('should build application', async () => {
const result = runCLI(`build ${app}`); expect(() => runCLI(`build ${app}`)).not.toThrow();
expect(result).toContain(
`Successfully ran target build for project ${app}`
);
checkFilesExist(`${app}/.nuxt/nuxt.d.ts`); checkFilesExist(`${app}/.nuxt/nuxt.d.ts`);
checkFilesExist(`${app}/.output/nitro.json`); checkFilesExist(`${app}/.output/nitro.json`);
}); });
it('should test application', async () => { it('should test application', async () => {
const result = runCLI(`test ${app}`); expect(() => runCLI(`test ${app}`)).not.toThrow();
expect(result).toContain(`Successfully ran target test for project ${app}`);
}, 150_000); }, 150_000);
it('should lint application', async () => { it('should lint application', async () => {
const result = runCLI(`lint ${app}`); expect(() => runCLI(`lint ${app}`)).not.toThrow();
expect(result).toContain(`Successfully ran target lint for project ${app}`);
}); });
it('should build storybook for app', () => { it('should build storybook for app', () => {

View File

@ -120,28 +120,6 @@ describe('nx init (for React - legacy)', () => {
process.env.SELECTED_PM = originalPM; process.env.SELECTED_PM = originalPM;
}); });
it('should convert to a standalone workspace with craco (webpack)', () => {
const appName = 'my-app';
createReactApp(appName);
const craToNxOutput = runCommand(
`${
pmc.runUninstalledPackage
} nx@${getPublishedVersion()} init --no-interactive --vite=false`
);
expect(craToNxOutput).toContain('🎉 Done!');
runCLI(`build ${appName}`, {
env: {
// since craco 7.1.0 the NODE_ENV is used, since the tests set it
// to "test" is causes an issue with React Refresh Babel
NODE_ENV: undefined,
},
});
checkFilesExist(`dist/${appName}/index.html`);
});
it('should convert to an standalone workspace with Vite', () => { it('should convert to an standalone workspace with Vite', () => {
const appName = 'my-app'; const appName = 'my-app';
createReactApp(appName); createReactApp(appName);

View File

@ -25,8 +25,8 @@ exports[`Extra Nx Misc Tests task graph inputs should correctly expand dependent
"nx.json", "nx.json",
], ],
"lib-base-123": [ "lib-base-123": [
"libs/lib-base-123/.eslintrc.json",
"libs/lib-base-123/README.md", "libs/lib-base-123/README.md",
"libs/lib-base-123/eslint.config.js",
"libs/lib-base-123/jest.config.ts", "libs/lib-base-123/jest.config.ts",
"libs/lib-base-123/package.json", "libs/lib-base-123/package.json",
"libs/lib-base-123/project.json", "libs/lib-base-123/project.json",
@ -38,8 +38,8 @@ exports[`Extra Nx Misc Tests task graph inputs should correctly expand dependent
"libs/lib-base-123/tsconfig.spec.json", "libs/lib-base-123/tsconfig.spec.json",
], ],
"lib-dependent-123": [ "lib-dependent-123": [
"libs/lib-dependent-123/.eslintrc.json",
"libs/lib-dependent-123/README.md", "libs/lib-dependent-123/README.md",
"libs/lib-dependent-123/eslint.config.js",
"libs/lib-dependent-123/jest.config.ts", "libs/lib-dependent-123/jest.config.ts",
"libs/lib-dependent-123/package.json", "libs/lib-dependent-123/package.json",
"libs/lib-dependent-123/project.json", "libs/lib-dependent-123/project.json",

View File

@ -60,9 +60,14 @@ describe('Nx Import', () => {
execSync(`git commit -am "initial commit"`, { execSync(`git commit -am "initial commit"`, {
cwd: tempViteProjectPath, cwd: tempViteProjectPath,
}); });
execSync(`git checkout -b main`, {
cwd: tempViteProjectPath, try {
}); execSync(`git checkout -b main`, {
cwd: tempViteProjectPath,
});
} catch {
// This fails if git is already configured to have `main` branch, but that's OK
}
const remote = tempViteProjectPath; const remote = tempViteProjectPath;
const ref = 'main'; const ref = 'main';

View File

@ -1,7 +1,6 @@
import { import {
checkFilesExist, checkFilesExist,
cleanupProject, cleanupProject,
expectTestsPass,
getPackageManagerCommand, getPackageManagerCommand,
isOSX, isOSX,
killProcessAndPorts, killProcessAndPorts,
@ -52,8 +51,7 @@ describe('@nx/react-native (legacy)', () => {
}); });
it('should build for web', async () => { it('should build for web', async () => {
const results = runCLI(`build ${appName}`); expect(() => runCLI(`build ${appName}`)).not.toThrow();
expect(results).toContain('Successfully ran target build');
}); });
it('should test and lint', async () => { it('should test and lint', async () => {
@ -67,37 +65,28 @@ describe('@nx/react-native (legacy)', () => {
return updated; return updated;
}); });
expectTestsPass(await runCLIAsync(`test ${appName}`)); expect(() => runCLI(`test ${appName}`)).not.toThrow();
expectTestsPass(await runCLIAsync(`test ${libName}`)); expect(() => runCLI(`test ${libName}`)).not.toThrow();
expect(() => runCLI(`lint ${appName}`)).not.toThrow();
const appLintResults = await runCLIAsync(`lint ${appName}`); expect(() => runCLI(`lint ${libName}`)).not.toThrow();
expect(appLintResults.combinedOutput).toContain(
'Successfully ran target lint'
);
const libLintResults = await runCLIAsync(`lint ${libName}`);
expect(libLintResults.combinedOutput).toContain(
'Successfully ran target lint'
);
}); });
it('should run e2e for cypress', async () => { it('should run e2e for cypress', async () => {
if (runE2ETests()) { if (runE2ETests()) {
let results = runCLI(`e2e ${appName}-e2e`); expect(() => runCLI(`e2e ${appName}-e2e`)).not.toThrow();
expect(results).toContain('Successfully ran target e2e');
results = runCLI(`e2e ${appName}-e2e --configuration=ci`); expect(() =>
expect(results).toContain('Successfully ran target e2e'); runCLI(`e2e ${appName}-e2e --configuration=ci`)
).not.toThrow();
} }
}); });
it('should bundle-ios', async () => { it('should bundle-ios', async () => {
const iosBundleResult = await runCLIAsync( expect(() =>
`bundle-ios ${appName} --sourcemapOutput=../../dist/apps/${appName}/ios/main.map` runCLI(
); `bundle-ios ${appName} --sourcemapOutput=../../dist/apps/${appName}/ios/main.map`
expect(iosBundleResult.combinedOutput).toContain( )
'Done writing bundle output' ).not.toThrow();
);
expect(() => { expect(() => {
checkFilesExist(`dist/apps/${appName}/ios/main.jsbundle`); checkFilesExist(`dist/apps/${appName}/ios/main.jsbundle`);
checkFilesExist(`dist/apps/${appName}/ios/main.map`); checkFilesExist(`dist/apps/${appName}/ios/main.map`);
@ -105,12 +94,12 @@ describe('@nx/react-native (legacy)', () => {
}); });
it('should bundle-android', async () => { it('should bundle-android', async () => {
const androidBundleResult = await runCLIAsync( expect(() =>
`bundle-android ${appName} --sourcemapOutput=../../dist/apps/${appName}/android/main.map` runCLI(
); `bundle-android ${appName} --sourcemapOutput=../../dist/apps/${appName}/android/main.map`
expect(androidBundleResult.combinedOutput).toContain( )
'Done writing bundle output' ).not.toThrow();
);
expect(() => { expect(() => {
checkFilesExist(`dist/apps/${appName}/android/main.jsbundle`); checkFilesExist(`dist/apps/${appName}/android/main.jsbundle`);
checkFilesExist(`dist/apps/${appName}/android/main.map`); checkFilesExist(`dist/apps/${appName}/android/main.map`);
@ -283,10 +272,7 @@ describe('@nx/react-native (legacy)', () => {
// using the project name as the directory when no directory is provided // using the project name as the directory when no directory is provided
checkFilesExist(`${appName}/src/app/App.tsx`); checkFilesExist(`${appName}/src/app/App.tsx`);
// check tests pass // check tests pass
const appTestResult = runCLI(`test ${appName}`); expect(() => runCLI(`test ${appName}`)).not.toThrow();
expect(appTestResult).toContain(
`Successfully ran target test for project ${appName}`
);
// assert scoped project names are not supported when --project-name-and-root-format=derived // assert scoped project names are not supported when --project-name-and-root-format=derived
expect(() => expect(() =>
@ -303,10 +289,7 @@ describe('@nx/react-native (legacy)', () => {
// using the project name as the directory when no directory is provided // using the project name as the directory when no directory is provided
checkFilesExist(`${libName}/src/index.ts`); checkFilesExist(`${libName}/src/index.ts`);
// check tests pass // check tests pass
const libTestResult = runCLI(`test ${libName}`); expect(() => runCLI(`test ${libName}`)).not.toThrow();
expect(libTestResult).toContain(
`Successfully ran target test for project ${libName}`
);
}); });
it('should run build with vite bundler and e2e with playwright', async () => { it('should run build with vite bundler and e2e with playwright', async () => {
@ -314,11 +297,9 @@ describe('@nx/react-native (legacy)', () => {
runCLI( runCLI(
`generate @nx/react-native:application ${appName2} --bundler=vite --e2eTestRunner=playwright --install=false --no-interactive` `generate @nx/react-native:application ${appName2} --bundler=vite --e2eTestRunner=playwright --install=false --no-interactive`
); );
const buildResults = runCLI(`build ${appName2}`); expect(() => runCLI(`build ${appName2}`)).not.toThrow();
expect(buildResults).toContain('Successfully ran target build');
if (runE2ETests()) { if (runE2ETests()) {
const e2eResults = runCLI(`e2e ${appName2}-e2e`); expect(() => runCLI(`e2e ${appName2}-e2e`)).not.toThrow();
expect(e2eResults).toContain('Successfully ran target e2e');
} }
runCLI( runCLI(

View File

@ -25,14 +25,12 @@ describe('@nx/react-native', () => {
afterAll(() => cleanupProject()); afterAll(() => cleanupProject());
it('should bundle the app', async () => { it('should bundle the app', async () => {
const result = runCLI( expect(() =>
`bundle ${appName} --platform=ios --bundle-output=dist.js --entry-file=src/main.tsx` runCLI(
); `bundle ${appName} --platform=ios --bundle-output=dist.js --entry-file=src/main.tsx`
)
).not.toThrow();
fileExists(` ${appName}/dist.js`); fileExists(` ${appName}/dist.js`);
expect(result).toContain(
`Successfully ran target bundle for project ${appName}`
);
}, 200_000); }, 200_000);
it('should start the app', async () => { it('should start the app', async () => {
@ -87,11 +85,11 @@ describe('@nx/react-native', () => {
it('should run e2e for cypress', async () => { it('should run e2e for cypress', async () => {
if (runE2ETests()) { if (runE2ETests()) {
let results = runCLI(`e2e ${appName}-e2e`); expect(() => runCLI(`e2e ${appName}-e2e`)).not.toThrow();
expect(results).toContain('Successfully ran target e2e');
results = runCLI(`e2e ${appName}-e2e --configuration=ci`); expect(() =>
expect(results).toContain('Successfully ran target e2e'); runCLI(`e2e ${appName}-e2e --configuration=ci`)
).not.toThrow();
// port and process cleanup // port and process cleanup
try { try {
@ -119,11 +117,9 @@ describe('@nx/react-native', () => {
runCLI( runCLI(
`generate @nx/react-native:application ${appName2} --bundler=vite --e2eTestRunner=playwright --install=false --no-interactive` `generate @nx/react-native:application ${appName2} --bundler=vite --e2eTestRunner=playwright --install=false --no-interactive`
); );
const buildResults = runCLI(`build ${appName2}`); expect(() => runCLI(`build ${appName2}`)).not.toThrow();
expect(buildResults).toContain('Successfully ran target build');
if (runE2ETests()) { if (runE2ETests()) {
const e2eResults = runCLI(`e2e ${appName2}-e2e`); expect(() => runCLI(`e2e ${appName2}-e2e`)).not.toThrow();
expect(e2eResults).toContain('Successfully ran target e2e');
// port and process cleanup // port and process cleanup
try { try {
if (process && process.pid) { if (process && process.pid) {

View File

@ -232,13 +232,25 @@ export function runCommandAsync(
}, },
(err, stdout, stderr) => { (err, stdout, stderr) => {
if (!opts.silenceError && err) { if (!opts.silenceError && err) {
logError(`Original command: ${command}`, `${stdout}\n\n${stderr}`);
reject(err); reject(err);
} }
resolve({
const outputs = {
stdout: stripConsoleColors(stdout), stdout: stripConsoleColors(stdout),
stderr: stripConsoleColors(stderr), stderr: stripConsoleColors(stderr),
combinedOutput: stripConsoleColors(`${stdout}${stderr}`), combinedOutput: stripConsoleColors(`${stdout}${stderr}`),
}); };
if (opts.verbose ?? isVerboseE2ERun()) {
output.log({
title: `Original command: ${command}`,
bodyLines: [outputs.combinedOutput],
color: 'green',
});
}
resolve(outputs);
} }
); );
}); });
@ -302,10 +314,11 @@ export function runCLIAsync(
} }
): Promise<{ stdout: string; stderr: string; combinedOutput: string }> { ): Promise<{ stdout: string; stderr: string; combinedOutput: string }> {
const pm = getPackageManagerCommand(); const pm = getPackageManagerCommand();
return runCommandAsync( const commandToRun = `${opts.silent ? pm.runNxSilent : pm.runNx} ${command} ${
`${opts.silent ? pm.runNxSilent : pm.runNx} ${command}`, opts.verbose ?? isVerboseE2ERun() ? ' --verbose' : ''
opts }${opts.redirectStderr ? ' 2>&1' : ''}`;
);
return runCommandAsync(commandToRun, opts);
} }
export function runNgAdd( export function runNgAdd(

View File

@ -45,24 +45,20 @@ describe('@nx/vite/plugin', () => {
describe('build and test React app', () => { describe('build and test React app', () => {
it('should build application', () => { it('should build application', () => {
const result = runCLI(`build ${myApp}`); expect(() => runCLI(`build ${myApp}`)).not.toThrow();
expect(result).toContain('Successfully ran target build');
}, 200_000); }, 200_000);
it('should test application', () => { it('should test application', () => {
const result = runCLI(`test ${myApp} --watch=false`); expect(() => runCLI(`test ${myApp} --watch=false`)).not.toThrow();
expect(result).toContain('Successfully ran target test');
}, 200_000); }, 200_000);
}); });
describe('build and test Vue app', () => { describe('build and test Vue app', () => {
it('should build application', () => { it('should build application', () => {
const result = runCLI(`build ${myVueApp}`); expect(() => runCLI(`build ${myVueApp}`)).not.toThrow();
expect(result).toContain('Successfully ran target build');
}, 200_000); }, 200_000);
it('should test application', () => { it('should test application', () => {
const result = runCLI(`test ${myVueApp} --watch=false`); expect(() => runCLI(`test ${myVueApp} --watch=false`)).not.toThrow();
expect(result).toContain('Successfully ran target test');
}, 200_000); }, 200_000);
}); });
@ -129,13 +125,7 @@ describe('@nx/vite/plugin', () => {
});` });`
); );
const result = runCLI(`build ${myApp}`); expect(() => runCLI(`build ${myApp}`)).not.toThrow();
expect(result).toContain(
`Running target build for project ${myApp} and 1 task it depends on`
);
expect(result).toContain(
`Successfully ran target build for project ${myApp} and 1 task it depends on`
);
}); });
}); });

View File

@ -1,4 +1,4 @@
import { Page, test, expect } from '@playwright/test'; import { expect, test } from '@playwright/test';
/** /**
* Assert a text is present on the visited page. * Assert a text is present on the visited page.
* @param page * @param page
@ -11,11 +11,12 @@ export function assertTextOnPage(
title: string, title: string,
selector: string = 'h1' selector: string = 'h1'
): void { ): void {
test.describe(path, () => // eslint-disable-next-line playwright/valid-title
test.describe(path, () => {
test(`should display "${title}"`, async ({ page }) => { test(`should display "${title}"`, async ({ page }) => {
await page.goto(path); await page.goto(path);
const locator = page.locator(selector); const locator = page.locator(selector);
await expect(locator).toContainText(title); await expect(locator).toContainText(title);
}) });
); });
} }

View File

@ -30,9 +30,9 @@
"@angular-devkit/build-angular": "~18.2.0", "@angular-devkit/build-angular": "~18.2.0",
"@angular-devkit/core": "~18.2.0", "@angular-devkit/core": "~18.2.0",
"@angular-devkit/schematics": "~18.2.0", "@angular-devkit/schematics": "~18.2.0",
"@angular-eslint/eslint-plugin": "^18.0.1", "@angular-eslint/eslint-plugin": "^18.3.0",
"@angular-eslint/eslint-plugin-template": "^18.0.1", "@angular-eslint/eslint-plugin-template": "^18.3.0",
"@angular-eslint/template-parser": "^18.0.1", "@angular-eslint/template-parser": "^18.3.0",
"@angular/cli": "~18.2.0", "@angular/cli": "~18.2.0",
"@angular/common": "~18.2.0", "@angular/common": "~18.2.0",
"@angular/compiler": "~18.2.0", "@angular/compiler": "~18.2.0",
@ -45,6 +45,7 @@
"@babel/preset-react": "^7.22.5", "@babel/preset-react": "^7.22.5",
"@babel/preset-typescript": "^7.22.5", "@babel/preset-typescript": "^7.22.5",
"@babel/runtime": "^7.22.6", "@babel/runtime": "^7.22.6",
"@eslint/compat": "^1.1.1",
"@eslint/eslintrc": "^2.1.1", "@eslint/eslintrc": "^2.1.1",
"@eslint/js": "^8.48.0", "@eslint/js": "^8.48.0",
"@floating-ui/react": "0.26.6", "@floating-ui/react": "0.26.6",
@ -116,6 +117,7 @@
"@types/detect-port": "^1.3.2", "@types/detect-port": "^1.3.2",
"@types/ejs": "3.1.2", "@types/ejs": "3.1.2",
"@types/eslint": "~8.56.10", "@types/eslint": "~8.56.10",
"@types/eslint__js": "^8.42.3",
"@types/express": "4.17.14", "@types/express": "4.17.14",
"@types/flat": "^5.0.1", "@types/flat": "^5.0.1",
"@types/fs-extra": "^11.0.0", "@types/fs-extra": "^11.0.0",
@ -134,16 +136,16 @@
"@types/tmp": "^0.2.0", "@types/tmp": "^0.2.0",
"@types/yargs": "17.0.10", "@types/yargs": "17.0.10",
"@types/yarnpkg__lockfile": "^1.1.5", "@types/yarnpkg__lockfile": "^1.1.5",
"@typescript-eslint/eslint-plugin": "7.16.0", "@typescript-eslint/rule-tester": "^8.0.0",
"@typescript-eslint/parser": "7.16.0", "@typescript-eslint/type-utils": "^8.0.0",
"@typescript-eslint/type-utils": "^7.16.0", "@typescript-eslint/utils": "^8.0.0",
"@typescript-eslint/utils": "7.16.0",
"@xstate/immer": "0.3.1", "@xstate/immer": "0.3.1",
"@xstate/inspect": "0.7.0", "@xstate/inspect": "0.7.0",
"@xstate/react": "3.0.1", "@xstate/react": "3.0.1",
"@zkochan/js-yaml": "0.0.7", "@zkochan/js-yaml": "0.0.7",
"ai": "^2.2.10", "ai": "^2.2.10",
"ajv": "^8.12.0", "ajv": "^8.12.0",
"angular-eslint": "^18.3.0",
"autoprefixer": "10.4.13", "autoprefixer": "10.4.13",
"babel-jest": "29.7.0", "babel-jest": "29.7.0",
"babel-loader": "^9.1.2", "babel-loader": "^9.1.2",
@ -175,10 +177,10 @@
"eslint-config-next": "14.2.3", "eslint-config-next": "14.2.3",
"eslint-config-prettier": "9.1.0", "eslint-config-prettier": "9.1.0",
"eslint-plugin-cypress": "2.14.0", "eslint-plugin-cypress": "2.14.0",
"eslint-plugin-import": "2.27.5", "eslint-plugin-import": "2.30.0",
"eslint-plugin-jsx-a11y": "6.7.1", "eslint-plugin-jsx-a11y": "6.7.1",
"eslint-plugin-playwright": "^0.15.3", "eslint-plugin-playwright": "^1.6.2",
"eslint-plugin-react": "7.32.2", "eslint-plugin-react": "7.35.0",
"eslint-plugin-react-hooks": "4.6.0", "eslint-plugin-react-hooks": "4.6.0",
"eslint-plugin-storybook": "^0.8.0", "eslint-plugin-storybook": "^0.8.0",
"express": "^4.19.2", "express": "^4.19.2",
@ -190,6 +192,7 @@
"fork-ts-checker-webpack-plugin": "7.2.13", "fork-ts-checker-webpack-plugin": "7.2.13",
"fs-extra": "^11.1.0", "fs-extra": "^11.1.0",
"github-slugger": "^2.0.0", "github-slugger": "^2.0.0",
"globals": "^15.9.0",
"gpt3-tokenizer": "^1.1.5", "gpt3-tokenizer": "^1.1.5",
"handlebars": "4.7.7", "handlebars": "4.7.7",
"html-webpack-plugin": "5.5.0", "html-webpack-plugin": "5.5.0",
@ -287,6 +290,7 @@
"typedoc": "0.25.12", "typedoc": "0.25.12",
"typedoc-plugin-markdown": "3.17.1", "typedoc-plugin-markdown": "3.17.1",
"typescript": "~5.5.2", "typescript": "~5.5.2",
"typescript-eslint": "^8.0.0",
"unist-builder": "^4.0.0", "unist-builder": "^4.0.0",
"unzipper": "^0.10.11", "unzipper": "^0.10.11",
"url-loader": "^4.1.1", "url-loader": "^4.1.1",

View File

@ -48,7 +48,7 @@
}, },
"dependencies": { "dependencies": {
"@phenomnomnominal/tsquery": "~5.0.1", "@phenomnomnominal/tsquery": "~5.0.1",
"@typescript-eslint/type-utils": "^7.16.0", "@typescript-eslint/type-utils": "^8.0.0",
"chalk": "^4.1.0", "chalk": "^4.1.0",
"find-cache-dir": "^3.3.2", "find-cache-dir": "^3.3.2",
"magic-string": "~0.30.2", "magic-string": "~0.30.2",

View File

@ -7,11 +7,14 @@ import {
} from '@nx/devkit'; } from '@nx/devkit';
import { camelize, dasherize } from '@nx/devkit/src/utils/string-utils'; import { camelize, dasherize } from '@nx/devkit/src/utils/string-utils';
import { Linter, lintProjectGenerator } from '@nx/eslint'; import { Linter, lintProjectGenerator } from '@nx/eslint';
import type * as eslint from 'eslint';
import { import {
javaScriptOverride, javaScriptOverride,
typeScriptOverride, typeScriptOverride,
} from '@nx/eslint/src/generators/init/global-eslint-config'; } from '@nx/eslint/src/generators/init/global-eslint-config';
import { import {
addOverrideToLintConfig,
addPredefinedConfigToFlatLintConfig,
findEslintFile, findEslintFile,
isEslintConfigSupported, isEslintConfigSupported,
replaceOverridesInLintConfig, replaceOverridesInLintConfig,
@ -19,6 +22,7 @@ import {
import { addAngularEsLintDependencies } from './lib/add-angular-eslint-dependencies'; import { addAngularEsLintDependencies } from './lib/add-angular-eslint-dependencies';
import { isBuildableLibraryProject } from './lib/buildable-project'; import { isBuildableLibraryProject } from './lib/buildable-project';
import type { AddLintingGeneratorSchema } from './schema'; import type { AddLintingGeneratorSchema } from './schema';
import { useFlatConfig } from '@nx/eslint/src/utils/flat-config';
export async function addLintingGenerator( export async function addLintingGenerator(
tree: Tree, tree: Tree,
@ -49,21 +53,19 @@ export async function addLintingGenerator(
.read(joinPathFragments(options.projectRoot, eslintFile), 'utf8') .read(joinPathFragments(options.projectRoot, eslintFile), 'utf8')
.includes(`${options.projectRoot}/tsconfig.*?.json`); .includes(`${options.projectRoot}/tsconfig.*?.json`);
replaceOverridesInLintConfig(tree, options.projectRoot, [ if (useFlatConfig(tree)) {
...(rootProject ? [typeScriptOverride, javaScriptOverride] : []), addPredefinedConfigToFlatLintConfig(
{ tree,
options.projectRoot,
'flat/angular'
);
addPredefinedConfigToFlatLintConfig(
tree,
options.projectRoot,
'flat/angular-template'
);
addOverrideToLintConfig(tree, options.projectRoot, {
files: ['*.ts'], files: ['*.ts'],
...(hasParserOptions
? {
parserOptions: {
project: [`${options.projectRoot}/tsconfig.*?.json`],
},
}
: {}),
extends: [
'plugin:@nx/angular',
'plugin:@angular-eslint/template/process-inline-templates',
],
rules: { rules: {
'@angular-eslint/directive-selector': [ '@angular-eslint/directive-selector': [
'error', 'error',
@ -82,28 +84,92 @@ export async function addLintingGenerator(
}, },
], ],
}, },
}, });
{ addOverrideToLintConfig(tree, options.projectRoot, {
files: ['*.html'], files: ['*.html'],
extends: ['plugin:@nx/angular-template'],
/**
* Having an empty rules object present makes it more obvious to the user where they would
* extend things from if they needed to
*/
rules: {}, rules: {},
}, });
...(isBuildableLibraryProject(tree, options.projectName)
? [ if (isBuildableLibraryProject(tree, options.projectName)) {
{ addOverrideToLintConfig(tree, '', {
files: ['*.json'], files: ['*.json'],
parser: 'jsonc-eslint-parser', parser: 'jsonc-eslint-parser',
rules: { rules: {
'@nx/dependency-checks': 'error', '@nx/dependency-checks': [
'error',
{
// With flat configs, we don't want to include imports in the eslint js/cjs/mjs files to be checked
ignoredFiles: ['{projectRoot}/eslint.config.{js,cjs,mjs}'],
},
],
},
});
}
} else {
replaceOverridesInLintConfig(tree, options.projectRoot, [
...(rootProject ? [typeScriptOverride, javaScriptOverride] : []),
{
files: ['*.ts'],
...(hasParserOptions
? {
parserOptions: {
project: [`${options.projectRoot}/tsconfig.*?.json`],
},
}
: {}),
extends: [
'plugin:@nx/angular',
'plugin:@angular-eslint/template/process-inline-templates',
],
rules: {
'@angular-eslint/directive-selector': [
'error',
{
type: 'attribute',
prefix: camelize(options.prefix),
style: 'camelCase',
},
],
'@angular-eslint/component-selector': [
'error',
{
type: 'element',
prefix: dasherize(options.prefix),
style: 'kebab-case',
},
],
},
},
{
files: ['*.html'],
extends: ['plugin:@nx/angular-template'],
/**
* Having an empty rules object present makes it more obvious to the user where they would
* extend things from if they needed to
*/
rules: {},
},
...(isBuildableLibraryProject(tree, options.projectName)
? [
{
files: ['*.json'],
parser: 'jsonc-eslint-parser',
rules: {
'@nx/dependency-checks': [
'error',
{
// With flat configs, we don't want to include imports in the eslint js/cjs/mjs files to be checked
ignoredFiles: [
'{projectRoot}/eslint.config.{js,cjs,mjs}',
],
},
],
},
} as any, } as any,
}, ]
] : []),
: []), ]);
]); }
} }
if (!options.skipPackageJson) { if (!options.skipPackageJson) {

View File

@ -5,6 +5,7 @@ import {
} from '@nx/devkit'; } from '@nx/devkit';
import { versions } from '../../utils/version-utils'; import { versions } from '../../utils/version-utils';
import { isBuildableLibraryProject } from './buildable-project'; import { isBuildableLibraryProject } from './buildable-project';
import { useFlatConfig } from '@nx/eslint/src/utils/flat-config';
export function addAngularEsLintDependencies( export function addAngularEsLintDependencies(
tree: Tree, tree: Tree,
@ -12,11 +13,15 @@ export function addAngularEsLintDependencies(
): GeneratorCallback { ): GeneratorCallback {
const compatVersions = versions(tree); const compatVersions = versions(tree);
const angularEslintVersionToInstall = compatVersions.angularEslintVersion; const angularEslintVersionToInstall = compatVersions.angularEslintVersion;
const devDependencies = { const devDependencies = useFlatConfig(tree)
'@angular-eslint/eslint-plugin': angularEslintVersionToInstall, ? {
'@angular-eslint/eslint-plugin-template': angularEslintVersionToInstall, 'angular-eslint': angularEslintVersionToInstall,
'@angular-eslint/template-parser': angularEslintVersionToInstall, }
}; : {
'@angular-eslint/eslint-plugin': angularEslintVersionToInstall,
'@angular-eslint/eslint-plugin-template': angularEslintVersionToInstall,
'@angular-eslint/template-parser': angularEslintVersionToInstall,
};
if ('typescriptEslintVersion' in compatVersions) { if ('typescriptEslintVersion' in compatVersions) {
devDependencies['@typescript-eslint/utils'] = devDependencies['@typescript-eslint/utils'] =

View File

@ -701,7 +701,14 @@ describe('lib', () => {
], ],
"parser": "jsonc-eslint-parser", "parser": "jsonc-eslint-parser",
"rules": { "rules": {
"@nx/dependency-checks": "error" "@nx/dependency-checks": [
"error",
{
"ignoredFiles": [
"{projectRoot}/eslint.config.{js,cjs,mjs}"
]
}
]
} }
} }
] ]
@ -1193,7 +1200,52 @@ describe('lib', () => {
describe('--linter', () => { describe('--linter', () => {
describe('eslint', () => { describe('eslint', () => {
it('should add valid eslint JSON configuration which extends from Nx presets', async () => { it('should add valid eslint JSON configuration which extends from Nx presets (flat config)', async () => {
tree.write('eslint.config.js', '');
await runLibraryGeneratorWithOpts({ linter: Linter.EsLint });
const eslintConfig = tree.read('my-lib/eslint.config.js', 'utf-8');
expect(eslintConfig).toMatchInlineSnapshot(`
"const nx = require("@nx/eslint-plugin");
const baseConfig = require("../eslint.config.js");
module.exports = [
...baseConfig,
...nx.configs["flat/angular"],
...nx.configs["flat/angular-template"],
{
files: ["**/*.ts"],
rules: {
"@angular-eslint/directive-selector": [
"error",
{
type: "attribute",
prefix: "lib",
style: "camelCase"
}
],
"@angular-eslint/component-selector": [
"error",
{
type: "element",
prefix: "lib",
style: "kebab-case"
}
]
}
},
{
files: ["**/*.html"],
// Override or add rules here
rules: {}
}
];
"
`);
});
it('should add valid eslint JSON configuration which extends from Nx presets (eslintrc)', async () => {
// ACT // ACT
await runLibraryGeneratorWithOpts({ linter: Linter.EsLint }); await runLibraryGeneratorWithOpts({ linter: Linter.EsLint });
@ -1311,7 +1363,14 @@ describe('lib', () => {
], ],
"parser": "jsonc-eslint-parser", "parser": "jsonc-eslint-parser",
"rules": { "rules": {
"@nx/dependency-checks": "error", "@nx/dependency-checks": [
"error",
{
"ignoredFiles": [
"{projectRoot}/eslint.config.{js,cjs,mjs}",
],
},
],
}, },
}, },
], ],

View File

@ -17,7 +17,7 @@ export const browserSyncVersion = '^3.0.0';
export const moduleFederationNodeVersion = '~2.5.0'; export const moduleFederationNodeVersion = '~2.5.0';
export const moduleFederationEnhancedVersion = '~0.6.0'; export const moduleFederationEnhancedVersion = '~0.6.0';
export const angularEslintVersion = '^18.0.1'; export const angularEslintVersion = '^18.3.0';
export const typescriptEslintVersion = '^7.16.0'; export const typescriptEslintVersion = '^7.16.0';
export const tailwindVersion = '^3.0.2'; export const tailwindVersion = '^3.0.2';
export const postcssVersion = '^8.4.5'; export const postcssVersion = '^8.4.5';

View File

@ -11,7 +11,13 @@ export function spawnAndWait(command: string, args: string[], cwd: string) {
const childProcess = spawn(command, args, { const childProcess = spawn(command, args, {
cwd, cwd,
stdio: 'inherit', stdio: 'inherit',
env: { ...process.env, NX_DAEMON: 'false' }, env: {
...process.env,
NX_DAEMON: 'false',
// This is the same environment variable that ESLint uses to determine if it should use a flat config.
// Default to true for all new workspaces.
ESLINT_USE_FLAT_CONFIG: process.env.ESLINT_USE_FLAT_CONFIG ?? 'true',
},
shell: true, shell: true,
windowsHide: true, windowsHide: true,
}); });

View File

@ -13,6 +13,7 @@ import {
addExtendsToLintConfig, addExtendsToLintConfig,
addOverrideToLintConfig, addOverrideToLintConfig,
addPluginsToLintConfig, addPluginsToLintConfig,
addPredefinedConfigToFlatLintConfig,
findEslintFile, findEslintFile,
isEslintConfigSupported, isEslintConfigSupported,
replaceOverridesInLintConfig, replaceOverridesInLintConfig,
@ -21,6 +22,7 @@ import {
javaScriptOverride, javaScriptOverride,
typeScriptOverride, typeScriptOverride,
} from '@nx/eslint/src/generators/init/global-eslint-config'; } from '@nx/eslint/src/generators/init/global-eslint-config';
import { useFlatConfig } from '@nx/eslint/src/utils/flat-config';
export interface CyLinterOptions { export interface CyLinterOptions {
project: string; project: string;
@ -90,16 +92,33 @@ export async function addLinterToCyProject(
isEslintConfigSupported(tree) isEslintConfigSupported(tree)
) { ) {
const overrides = []; const overrides = [];
if (options.rootProject) { if (useFlatConfig(tree)) {
addPluginsToLintConfig(tree, projectConfig.root, '@nx'); addPredefinedConfigToFlatLintConfig(
overrides.push(typeScriptOverride); tree,
overrides.push(javaScriptOverride); projectConfig.root,
'recommended',
'cypress',
'eslint-plugin-cypress/flat',
false,
false
);
addOverrideToLintConfig(tree, projectConfig.root, {
files: ['*.ts', '*.js'],
rules: {},
});
} else {
if (options.rootProject) {
addPluginsToLintConfig(tree, projectConfig.root, '@nx');
overrides.push(typeScriptOverride);
overrides.push(javaScriptOverride);
}
const addExtendsTask = addExtendsToLintConfig(
tree,
projectConfig.root,
'plugin:cypress/recommended'
);
tasks.push(addExtendsTask);
} }
addExtendsToLintConfig(
tree,
projectConfig.root,
'plugin:cypress/recommended'
);
const cyVersion = installedCypressVersion(); const cyVersion = installedCypressVersion();
/** /**
* We need this override because we enabled allowJS in the tsconfig to allow for JS based Cypress tests. * We need this override because we enabled allowJS in the tsconfig to allow for JS based Cypress tests.
@ -116,7 +135,10 @@ export async function addLinterToCyProject(
if (options.overwriteExisting) { if (options.overwriteExisting) {
overrides.unshift({ overrides.unshift({
files: ['*.ts', '*.tsx', '*.js', '*.jsx'], files: useFlatConfig(tree)
? // For flat configs we don't need to specify the files
undefined
: ['*.ts', '*.tsx', '*.js', '*.jsx'],
parserOptions: !options.setParserOptionsProject parserOptions: !options.setParserOptionsProject
? undefined ? undefined
: { : {
@ -130,10 +152,13 @@ export async function addLinterToCyProject(
replaceOverridesInLintConfig(tree, projectConfig.root, overrides); replaceOverridesInLintConfig(tree, projectConfig.root, overrides);
} else { } else {
overrides.unshift({ overrides.unshift({
files: [ files: useFlatConfig(tree)
'*.cy.{ts,js,tsx,jsx}', ? // For flat configs we don't need to specify the files
`${options.cypressDir}/**/*.{ts,js,tsx,jsx}`, undefined
], : [
'*.cy.{ts,js,tsx,jsx}',
`${options.cypressDir}/**/*.{ts,js,tsx,jsx}`,
],
parserOptions: !options.setParserOptionsProject parserOptions: !options.setParserOptionsProject
? undefined ? undefined
: { : {

View File

@ -1,5 +1,5 @@
export const nxVersion = require('../../package.json').version; export const nxVersion = require('../../package.json').version;
export const eslintPluginCypressVersion = '^2.13.4'; export const eslintPluginCypressVersion = '^3.5.0';
export const typesNodeVersion = '18.16.9'; export const typesNodeVersion = '18.16.9';
export const cypressViteDevServerVersion = '^2.2.1'; export const cypressViteDevServerVersion = '^2.2.1';
export const cypressVersion = '^13.13.0'; export const cypressVersion = '^13.13.0';

View File

@ -1,6 +1,7 @@
import { Linter, lintProjectGenerator } from '@nx/eslint'; import { Linter, lintProjectGenerator } from '@nx/eslint';
import { import {
addDependenciesToPackageJson, addDependenciesToPackageJson,
GeneratorCallback,
joinPathFragments, joinPathFragments,
runTasksInSerial, runTasksInSerial,
Tree, Tree,
@ -9,14 +10,18 @@ import { extraEslintDependencies } from '@nx/react';
import { NormalizedSchema } from './normalize-options'; import { NormalizedSchema } from './normalize-options';
import { import {
addExtendsToLintConfig, addExtendsToLintConfig,
addOverrideToLintConfig,
addPredefinedConfigToFlatLintConfig,
isEslintConfigSupported, isEslintConfigSupported,
} from '@nx/eslint/src/generators/utils/eslint-file'; } from '@nx/eslint/src/generators/utils/eslint-file';
import { useFlatConfig } from '@nx/eslint/src/utils/flat-config';
export async function addLinting(host: Tree, options: NormalizedSchema) { export async function addLinting(host: Tree, options: NormalizedSchema) {
if (options.linter === Linter.None) { if (options.linter === Linter.None) {
return () => {}; return () => {};
} }
const tasks: GeneratorCallback[] = [];
const lintTask = await lintProjectGenerator(host, { const lintTask = await lintProjectGenerator(host, {
linter: options.linter, linter: options.linter,
project: options.e2eProjectName, project: options.e2eProjectName,
@ -26,9 +31,28 @@ export async function addLinting(host: Tree, options: NormalizedSchema) {
skipFormat: true, skipFormat: true,
addPlugin: options.addPlugin, addPlugin: options.addPlugin,
}); });
tasks.push(lintTask);
if (isEslintConfigSupported(host)) { if (isEslintConfigSupported(host)) {
addExtendsToLintConfig(host, options.e2eProjectRoot, 'plugin:@nx/react'); if (useFlatConfig(host)) {
addPredefinedConfigToFlatLintConfig(
host,
options.e2eProjectRoot,
'flat/react'
);
// Add an empty rules object to users know how to add/override rules
addOverrideToLintConfig(host, options.e2eProjectRoot, {
files: ['*.ts', '*.tsx', '*.js', '*.jsx'],
rules: {},
});
} else {
const addExtendsTask = addExtendsToLintConfig(
host,
options.e2eProjectRoot,
{ name: 'plugin:@nx/react', needCompatFixup: true }
);
tasks.push(addExtendsTask);
}
} }
const installTask = addDependenciesToPackageJson( const installTask = addDependenciesToPackageJson(
@ -36,6 +60,7 @@ export async function addLinting(host: Tree, options: NormalizedSchema) {
extraEslintDependencies.dependencies, extraEslintDependencies.dependencies,
extraEslintDependencies.devDependencies extraEslintDependencies.devDependencies
); );
tasks.push(installTask);
return runTasksInSerial(lintTask, installTask); return runTasksInSerial(...tasks);
} }

View File

@ -37,7 +37,14 @@
// Installed to workspace by plugins // Installed to workspace by plugins
"@typescript-eslint/parser", "@typescript-eslint/parser",
"eslint-config-prettier", "eslint-config-prettier",
"@angular-eslint/eslint-plugin" "@angular-eslint/eslint-plugin",
"angular-eslint",
"typescript-eslint",
"@eslint/js",
"eslint-plugin-import",
"eslint-plugin-jsx-a11y",
"eslint-plugin-react",
"eslint-plugin-react-hooks"
] ]
} }
] ]

View File

@ -0,0 +1,16 @@
import angular from './src/flat-configs/angular';
import angularTemplate from './src/flat-configs/angular-template';
const plugin = {
configs: {
angular,
'angular-template': angularTemplate,
},
rules: {},
};
// ESM
export default plugin;
// CommonJS
module.exports = plugin;

View File

@ -0,0 +1,27 @@
import { workspaceRules } from './src/resolve-workspace-rules';
import dependencyChecks, {
RULE_NAME as dependencyChecksRuleName,
} from './src/rules/dependency-checks';
import enforceModuleBoundaries, {
RULE_NAME as enforceModuleBoundariesRuleName,
} from './src/rules/enforce-module-boundaries';
import nxPluginChecksRule, {
RULE_NAME as nxPluginChecksRuleName,
} from './src/rules/nx-plugin-checks';
const plugin = {
configs: {},
rules: {
[enforceModuleBoundariesRuleName]: enforceModuleBoundaries,
[nxPluginChecksRuleName]: nxPluginChecksRule,
[dependencyChecksRuleName]: dependencyChecks,
// Resolve any custom rules that might exist in the current workspace
...workspaceRules,
},
};
// ESM
export default plugin;
// CommonJS
module.exports = plugin;

View File

@ -25,7 +25,7 @@
}, },
"homepage": "https://nx.dev", "homepage": "https://nx.dev",
"peerDependencies": { "peerDependencies": {
"@typescript-eslint/parser": "^6.13.2 || ^7.0.0", "@typescript-eslint/parser": "^6.13.2 || ^7.0.0 || ^8.0.0",
"eslint-config-prettier": "^9.0.0" "eslint-config-prettier": "^9.0.0"
}, },
"peerDependenciesMeta": { "peerDependenciesMeta": {
@ -34,12 +34,14 @@
} }
}, },
"dependencies": { "dependencies": {
"@eslint/compat": "^1.1.1",
"@nx/devkit": "file:../devkit", "@nx/devkit": "file:../devkit",
"@nx/js": "file:../js", "@nx/js": "file:../js",
"@typescript-eslint/type-utils": "^7.16.0", "@typescript-eslint/type-utils": "^8.0.0",
"@typescript-eslint/utils": "^7.16.0", "@typescript-eslint/utils": "^8.0.0",
"chalk": "^4.1.0", "chalk": "^4.1.0",
"confusing-browser-globals": "^1.0.9", "confusing-browser-globals": "^1.0.9",
"globals": "^15.9.0",
"jsonc-eslint-parser": "^2.1.0", "jsonc-eslint-parser": "^2.1.0",
"semver": "^7.5.3", "semver": "^7.5.3",
"tslib": "^2.3.0" "tslib": "^2.3.0"

View File

@ -0,0 +1,20 @@
import reactBase from './src/flat-configs/react-base';
import reactJsx from './src/flat-configs/react-jsx';
import reactTmp from './src/flat-configs/react-tmp';
import reactTypescript from './src/flat-configs/react-typescript';
const plugin = {
configs: {
react: reactTmp,
'react-base': reactBase,
'react-typescript': reactTypescript,
'react-jsx': reactJsx,
},
rules: {},
};
// ESM
export default plugin;
// CommonJS
module.exports = plugin;

View File

@ -66,5 +66,12 @@ export default {
'@typescript-eslint/no-unused-vars': 'warn', '@typescript-eslint/no-unused-vars': 'warn',
'@typescript-eslint/no-empty-interface': 'error', '@typescript-eslint/no-empty-interface': 'error',
'@typescript-eslint/no-explicit-any': 'warn', '@typescript-eslint/no-explicit-any': 'warn',
/**
* During the migration to use ESLint v9 and typescript-eslint v8 for new workspaces,
* this rule would have created a lot of noise, so we are disabling it by default for now.
*
* TODO(v20): we should make this part of what we re-evaluate in v20
*/
'@typescript-eslint/no-require-imports': 'off',
}, },
}; };

View File

@ -49,5 +49,12 @@ export default {
'@typescript-eslint/no-unused-vars': 'warn', '@typescript-eslint/no-unused-vars': 'warn',
'@typescript-eslint/no-empty-interface': 'error', '@typescript-eslint/no-empty-interface': 'error',
'@typescript-eslint/no-explicit-any': 'warn', '@typescript-eslint/no-explicit-any': 'warn',
/**
* During the migration to use ESLint v9 and typescript-eslint v8 for new workspaces,
* this rule would have created a lot of noise, so we are disabling it by default for now.
*
* TODO(v20): we should make this part of what we re-evaluate in v20
*/
'@typescript-eslint/no-require-imports': 'off',
}, },
}; };

View File

@ -0,0 +1,25 @@
import angular from 'angular-eslint';
import tseslint from 'typescript-eslint';
/**
* This configuration is intended to be applied to ALL .html files in Angular
* projects within an Nx workspace, as well as extracted inline templates from
* .component.ts files (or similar).
*
* It should therefore NOT contain any rules or plugins which are related to
* Angular source code.
*
* NOTE: The processor to extract the inline templates is applied in users'
* configs by the relevant schematic.
*
* This configuration is intended to be combined with other configs from this
* package.
*/
export default tseslint.config({
files: ['**/*.html'],
extends: [
...angular.configs.templateRecommended,
...angular.configs.templateAccessibility,
],
rules: {},
});

View File

@ -0,0 +1,26 @@
import angularEslint from 'angular-eslint';
import globals from 'globals';
import tseslint from 'typescript-eslint';
/**
* This configuration is intended to be applied to ALL .ts files in Angular
* projects within an Nx workspace.
*
* It should therefore NOT contain any rules or plugins which are related to
* Angular Templates, or more cross-cutting concerns which are not specific
* to Angular.
*
* This configuration is intended to be combined with other configs from this
* package.
*/
export default tseslint.config(...angularEslint.configs.tsRecommended, {
languageOptions: {
globals: {
...globals.browser,
...globals.es2015,
...globals.node,
},
},
processor: angularEslint.processInlineTemplates,
plugins: { '@angular-eslint': angularEslint.tsPlugin },
});

View File

@ -0,0 +1,10 @@
export default [
{
plugins: {
get ['@nx']() {
return require('../index');
},
},
ignores: ['.nx'],
},
];

View File

@ -0,0 +1,82 @@
import eslint from '@eslint/js';
import globals from 'globals';
import tseslint from 'typescript-eslint';
import { packageExists } from '../utils/config-utils';
const isPrettierAvailable =
packageExists('prettier') && packageExists('eslint-config-prettier');
/**
* This configuration is intended to be applied to ALL .js and .jsx files
* within an Nx workspace.
*
* It should therefore NOT contain any rules or plugins which are specific
* to one ecosystem, such as React, Angular, Node etc.
*
* We use @typescript-eslint/parser rather than the built in JS parser
* because that is what Nx ESLint configs have always done and we don't
* want to change too much all at once.
*
* TODO: Evaluate switching to the built-in JS parser (espree) in Nx v11,
* it should yield a performance improvement but could introduce subtle
* breaking changes - we should also look to replace all the @typescript-eslint
* related plugins and rules below.
*/
export default tseslint.config(
eslint.configs.recommended,
...tseslint.configs.recommended,
...(isPrettierAvailable ? [require('eslint-config-prettier')] : []),
{
languageOptions: {
parser: tseslint.parser,
ecmaVersion: 2020,
sourceType: 'module',
globals: {
...globals.browser,
...globals.node,
},
},
plugins: { '@typescript-eslint': tseslint.plugin },
},
{
files: ['**/*.js', '**/*.jsx'],
rules: {
'@typescript-eslint/explicit-member-accessibility': 'off',
'@typescript-eslint/explicit-module-boundary-types': 'off',
'@typescript-eslint/explicit-function-return-type': 'off',
'@typescript-eslint/no-parameter-properties': 'off',
/**
* Until ESM usage in Node matures, using require in e.g. JS config files
* is by far the more common thing to do, so disabling this to avoid users
* having to frequently use "eslint-disable-next-line" in their configs.
*/
'@typescript-eslint/no-var-requires': 'off',
/**
* From https://typescript-eslint.io/blog/announcing-typescript-eslint-v6/#updated-configuration-rules
*
* The following rules were added to preserve the linting rules that were
* previously defined v5 of `@typescript-eslint`. v6 of `@typescript-eslint`
* changed how configurations are defined.
*
* TODO(v20): re-evalute these deviations from @typescript-eslint/recommended in v20 of Nx
*/
'@typescript-eslint/no-non-null-assertion': 'warn',
'@typescript-eslint/adjacent-overload-signatures': 'error',
'@typescript-eslint/prefer-namespace-keyword': 'error',
'no-empty-function': 'off',
'@typescript-eslint/no-empty-function': 'error',
'@typescript-eslint/no-inferrable-types': 'error',
'@typescript-eslint/no-unused-vars': 'warn',
'@typescript-eslint/no-empty-interface': 'error',
'@typescript-eslint/no-explicit-any': 'warn',
/**
* During the migration to use ESLint v9 and typescript-eslint v8 for new workspaces,
* this rule would have created a lot of noise, so we are disabling it by default for now.
*
* TODO(v20): we should make this part of what we re-evaluate in v20
*/
'@typescript-eslint/no-require-imports': 'off',
},
}
);

View File

@ -0,0 +1,148 @@
import { fixupPluginRules } from '@eslint/compat';
import * as importPlugin from 'eslint-plugin-import';
import globals from 'globals';
import tseslint from 'typescript-eslint';
/**
* This configuration is intended to be applied to ALL files within a React
* project in an Nx workspace.
*
* It should therefore NOT contain any rules or plugins which are specific
* to one particular variant, e.g. TypeScript vs JavaScript, .js vs .jsx etc
*
* This configuration is intended to be combined with other configs from this
* package.
*/
/**
* Rule set originally adapted from:
* https://github.com/facebook/create-react-app/blob/567f36c9235f1e1fd4a76dc6d1ae00be754ca047/packages/eslint-config-react-app/index.js
*/
export default tseslint.config({
plugins: { import: fixupPluginRules(importPlugin) },
languageOptions: {
globals: {
...globals.browser,
...globals.commonjs,
...globals.es2015,
...globals.jest,
...globals.node,
},
},
rules: {
/**
* Standard ESLint rule configurations
* https://eslint.org/docs/rules
*/
'array-callback-return': 'warn',
'dot-location': ['warn', 'property'],
eqeqeq: ['warn', 'smart'],
'new-parens': 'warn',
'no-caller': 'warn',
'no-cond-assign': ['warn', 'except-parens'],
'no-const-assign': 'warn',
'no-control-regex': 'warn',
'no-delete-var': 'warn',
'no-dupe-args': 'warn',
'no-dupe-keys': 'warn',
'no-duplicate-case': 'warn',
'no-empty-character-class': 'warn',
'no-empty-pattern': 'warn',
'no-eval': 'warn',
'no-ex-assign': 'warn',
'no-extend-native': 'warn',
'no-extra-bind': 'warn',
'no-extra-label': 'warn',
'no-fallthrough': 'warn',
'no-func-assign': 'warn',
'no-implied-eval': 'warn',
'no-invalid-regexp': 'warn',
'no-iterator': 'warn',
'no-label-var': 'warn',
'no-labels': ['warn', { allowLoop: true, allowSwitch: false }],
'no-lone-blocks': 'warn',
'no-loop-func': 'warn',
'no-mixed-operators': [
'warn',
{
groups: [
['&', '|', '^', '~', '<<', '>>', '>>>'],
['==', '!=', '===', '!==', '>', '>=', '<', '<='],
['&&', '||'],
['in', 'instanceof'],
],
allowSamePrecedence: false,
},
],
'no-multi-str': 'warn',
'no-native-reassign': 'warn',
'no-negated-in-lhs': 'warn',
'no-new-func': 'warn',
'no-new-object': 'warn',
'no-new-symbol': 'warn',
'no-new-wrappers': 'warn',
'no-obj-calls': 'warn',
'no-octal': 'warn',
'no-octal-escape': 'warn',
'no-redeclare': 'warn',
'no-regex-spaces': 'warn',
'no-restricted-syntax': ['warn', 'WithStatement'],
'no-script-url': 'warn',
'no-self-assign': 'warn',
'no-self-compare': 'warn',
'no-sequences': 'warn',
'no-shadow-restricted-names': 'warn',
'no-sparse-arrays': 'warn',
'no-template-curly-in-string': 'warn',
'no-this-before-super': 'warn',
'no-throw-literal': 'warn',
'no-restricted-globals': ['error', ...require('confusing-browser-globals')],
'no-unexpected-multiline': 'warn',
'no-unreachable': 'warn',
'no-unused-expressions': 'off',
'no-unused-labels': 'warn',
'no-useless-computed-key': 'warn',
'no-useless-concat': 'warn',
'no-useless-escape': 'warn',
'no-useless-rename': [
'warn',
{
ignoreDestructuring: false,
ignoreImport: false,
ignoreExport: false,
},
],
'no-with': 'warn',
'no-whitespace-before-property': 'warn',
'require-yield': 'warn',
'rest-spread-spacing': ['warn', 'never'],
strict: ['warn', 'never'],
'unicode-bom': ['warn', 'never'],
'use-isnan': 'warn',
'valid-typeof': 'warn',
'no-restricted-properties': [
'error',
{
object: 'require',
property: 'ensure',
message:
'Please use import() instead. More info: https://facebook.github.io/create-react-app/docs/code-splitting',
},
{
object: 'System',
property: 'import',
message:
'Please use import() instead. More info: https://facebook.github.io/create-react-app/docs/code-splitting',
},
],
'getter-return': 'warn',
/**
* Import rule configurations
* https://github.com/benmosher/eslint-plugin-import
*/
'import/first': 'error',
'import/no-amd': 'error',
'import/no-webpack-loader-syntax': 'error',
},
});

View File

@ -0,0 +1,78 @@
import jsxA11yPlugin from 'eslint-plugin-jsx-a11y';
import reactPlugin from 'eslint-plugin-react';
import reactHooksPlugin from 'eslint-plugin-react-hooks';
import tseslint from 'typescript-eslint';
/**
* This configuration is intended to be applied to ONLY files which contain JSX/TSX
* code.
*
* It should therefore NOT contain any rules or plugins which are generic
* to all file types within variants of React projects.
*
* This configuration is intended to be combined with other configs from this
* package.
*/
export default tseslint.config(
{
plugins: {
'react-hooks': reactHooksPlugin,
},
rules: reactHooksPlugin.configs.recommended.rules,
},
{
settings: { react: { version: 'detect' } },
plugins: {
'jsx-a11y': jsxA11yPlugin,
react: reactPlugin,
},
rules: {
/**
* React-specific rule configurations
* https://github.com/yannickcr/eslint-plugin-react
*/
'react/forbid-foreign-prop-types': ['warn', { allowInPropTypes: true }],
'react/jsx-no-comment-textnodes': 'warn',
'react/jsx-no-duplicate-props': 'warn',
'react/jsx-no-target-blank': 'warn',
'react/jsx-no-undef': 'error',
'react/jsx-pascal-case': ['warn', { allowAllCaps: true, ignore: [] }],
'react/jsx-uses-vars': 'warn',
'react/no-danger-with-children': 'warn',
'react/no-direct-mutation-state': 'warn',
'react/no-is-mounted': 'warn',
'react/no-typos': 'error',
'react/jsx-uses-react': 'off',
'react/react-in-jsx-scope': 'off',
'react/require-render-return': 'error',
'react/style-prop-object': 'warn',
'react/jsx-no-useless-fragment': 'warn',
/**
* JSX Accessibility rule configurations
* https://github.com/evcohen/eslint-plugin-jsx-a11y
*/
'jsx-a11y/accessible-emoji': 'warn',
'jsx-a11y/alt-text': 'warn',
'jsx-a11y/anchor-has-content': 'warn',
'jsx-a11y/anchor-is-valid': [
'warn',
{ aspects: ['noHref', 'invalidHref'] },
],
'jsx-a11y/aria-activedescendant-has-tabindex': 'warn',
'jsx-a11y/aria-props': 'warn',
'jsx-a11y/aria-proptypes': 'warn',
'jsx-a11y/aria-role': 'warn',
'jsx-a11y/aria-unsupported-elements': 'warn',
'jsx-a11y/heading-has-content': 'warn',
'jsx-a11y/iframe-has-title': 'warn',
'jsx-a11y/img-redundant-alt': 'warn',
'jsx-a11y/no-access-key': 'warn',
'jsx-a11y/no-distracting-elements': 'warn',
'jsx-a11y/no-redundant-roles': 'warn',
'jsx-a11y/role-has-required-aria-props': 'warn',
'jsx-a11y/role-supports-aria-props': 'warn',
'jsx-a11y/scope': 'warn',
},
}
);

View File

@ -0,0 +1,14 @@
import tseslint from 'typescript-eslint';
import reactBase from './react-base';
import reactTypescript from './react-typescript';
import reactJsx from './react-jsx';
/**
* THIS IS A TEMPORARY CONFIG WHICH MATCHES THE CURRENT BEHAVIOR
* of including all the rules for all file types within the ESLint
* config for React projects.
*
* It will be refactored in a follow up PR to correctly apply rules
* to the right file types via overrides.
*/
export default tseslint.config(...reactBase, ...reactTypescript, ...reactJsx);

View File

@ -0,0 +1,55 @@
import tseslint from 'typescript-eslint';
/**
* This configuration is intended to be applied to ONLY .ts and .tsx files within a
* React project in an Nx workspace.
*
* It should therefore NOT contain any rules or plugins which are generic
* to all variants of React projects, e.g. TypeScript vs JavaScript, .js vs .jsx etc
*
* This configuration is intended to be combined with other configs from this
* package.
*/
export default tseslint.config({
rules: {
// TypeScript"s `noFallthroughCasesInSwitch` option is more robust (#6906)
'default-case': 'off',
// "tsc" already handles this (https://github.com/typescript-eslint/typescript-eslint/issues/291)
'no-dupe-class-members': 'off',
// "tsc" already handles this (https://github.com/typescript-eslint/typescript-eslint/issues/477)
'no-undef': 'off',
// Add TypeScript specific rules (and turn off ESLint equivalents)
'no-array-constructor': 'off',
'@typescript-eslint/no-array-constructor': 'warn',
'@typescript-eslint/no-namespace': 'error',
'no-use-before-define': 'off',
'@typescript-eslint/no-use-before-define': [
'warn',
{
functions: false,
classes: false,
variables: false,
typedefs: false,
},
],
'no-unused-vars': 'off',
'@typescript-eslint/no-unused-vars': [
'warn',
{
args: 'none',
ignoreRestSiblings: true,
},
],
'no-useless-constructor': 'off',
'@typescript-eslint/no-useless-constructor': 'warn',
'@typescript-eslint/no-unused-expressions': [
'error',
{
allowShortCircuit: true,
allowTernary: true,
allowTaggedTemplates: true,
},
],
},
});

View File

@ -0,0 +1,66 @@
import eslint from '@eslint/js';
import { workspaceRoot } from '@nx/devkit';
import tseslint from 'typescript-eslint';
import { packageExists } from '../utils/config-utils';
const isPrettierAvailable =
packageExists('prettier') && packageExists('eslint-config-prettier');
/**
* This configuration is intended to be applied to ALL .ts and .tsx files
* within an Nx workspace.
*
* It should therefore NOT contain any rules or plugins which are specific
* to one ecosystem, such as React, Angular, Node etc.
*/
export default tseslint.config(
eslint.configs.recommended,
...tseslint.configs.recommended,
...(isPrettierAvailable ? [require('eslint-config-prettier')] : []),
{
plugins: { '@typescript-eslint': tseslint.plugin },
languageOptions: {
parser: tseslint.parser,
ecmaVersion: 2020,
sourceType: 'module',
parserOptions: {
tsconfigRootDir: workspaceRoot,
},
},
},
{
files: ['**/*.ts', '**/*.tsx'],
rules: {
'@typescript-eslint/explicit-member-accessibility': 'off',
'@typescript-eslint/explicit-module-boundary-types': 'off',
'@typescript-eslint/explicit-function-return-type': 'off',
'@typescript-eslint/no-parameter-properties': 'off',
/**
* From https://typescript-eslint.io/blog/announcing-typescript-eslint-v6/#updated-configuration-rules
*
* The following rules were added to preserve the linting rules that were
* previously defined v5 of `@typescript-eslint`. v6 of `@typescript-eslint`
* changed how configurations are defined.
*
* TODO(v20): re-evalute these deviations from @typescript-eslint/recommended in v20 of Nx
*/
'@typescript-eslint/no-non-null-assertion': 'warn',
'@typescript-eslint/adjacent-overload-signatures': 'error',
'@typescript-eslint/prefer-namespace-keyword': 'error',
'no-empty-function': 'off',
'@typescript-eslint/no-empty-function': 'error',
'@typescript-eslint/no-inferrable-types': 'error',
'@typescript-eslint/no-unused-vars': 'warn',
'@typescript-eslint/no-empty-interface': 'error',
'@typescript-eslint/no-explicit-any': 'warn',
/**
* During the migration to use ESLint v9 and typescript-eslint v8 for new workspaces,
* this rule would have created a lot of noise, so we are disabling it by default for now.
*
* TODO(v20): we should make this part of what we re-evaluate in v20
*/
'@typescript-eslint/no-require-imports': 'off',
},
}
);

View File

@ -7,6 +7,8 @@ import reactTypescript from './configs/react-typescript';
import angularCode from './configs/angular'; import angularCode from './configs/angular';
import angularTemplate from './configs/angular-template'; import angularTemplate from './configs/angular-template';
import flatBase from './flat-configs/base';
import enforceModuleBoundaries, { import enforceModuleBoundaries, {
RULE_NAME as enforceModuleBoundariesRuleName, RULE_NAME as enforceModuleBoundariesRuleName,
} from './rules/enforce-module-boundaries'; } from './rules/enforce-module-boundaries';
@ -24,6 +26,7 @@ import { workspaceRules } from './resolve-workspace-rules';
module.exports = { module.exports = {
configs: { configs: {
// eslintrc configs
typescript, typescript,
javascript, javascript,
react: reactTmp, react: reactTmp,
@ -32,6 +35,34 @@ module.exports = {
'react-jsx': reactJsx, 'react-jsx': reactJsx,
angular: angularCode, angular: angularCode,
'angular-template': angularTemplate, 'angular-template': angularTemplate,
// flat configs
// Note: Using getters here to avoid importing packages `angular-eslint` statically, which can lead to errors if not installed.
'flat/base': flatBase,
get ['flat/typescript']() {
return require('./flat-configs/typescript').default;
},
get ['flat/javascript']() {
return require('./flat-configs/javascript').default;
},
get ['flat/react']() {
return require('./flat-configs/react-tmp').default;
},
get ['flat/react-base']() {
return require('./flat-configs/react-base').default;
},
get ['flat/react-typescript']() {
return require('./flat-configs/react-typescript').default;
},
get ['flat/react-jsx']() {
return require('./flat-configs/react-jsx').default;
},
get ['flat/angular']() {
return require('./flat-configs/angular').default;
},
get ['flat/angular-template']() {
return require('./flat-configs/angular-template').default;
},
}, },
rules: { rules: {
[enforceModuleBoundariesRuleName]: enforceModuleBoundaries, [enforceModuleBoundariesRuleName]: enforceModuleBoundaries,

View File

@ -47,7 +47,6 @@ export default ESLintUtils.RuleCreator(
type: 'suggestion', type: 'suggestion',
docs: { docs: {
description: `Checks dependencies in project's package.json for version mismatches`, description: `Checks dependencies in project's package.json for version mismatches`,
recommended: 'recommended',
}, },
fixable: 'code', fixable: 'code',
schema: [ schema: [

View File

@ -91,7 +91,6 @@ export default ESLintUtils.RuleCreator(
type: 'suggestion', type: 'suggestion',
docs: { docs: {
description: `Ensure that module boundaries are respected within the monorepo`, description: `Ensure that module boundaries are respected within the monorepo`,
recommended: 'recommended',
}, },
fixable: 'code', fixable: 'code',
schema: [ schema: [

View File

@ -57,7 +57,6 @@ export default ESLintUtils.RuleCreator(() => ``)<Options, MessageIds>({
meta: { meta: {
docs: { docs: {
description: 'Checks common nx-plugin configuration files for validity', description: 'Checks common nx-plugin configuration files for validity',
recommended: 'recommended',
}, },
schema: [ schema: [
{ {

View File

@ -0,0 +1,16 @@
import javascript from './src/flat-configs/javascript';
import typescript from './src/flat-configs/typescript';
const plugin = {
configs: {
javascript,
typescript,
},
rules: {},
};
// ESM
export default plugin;
// CommonJS
module.exports = plugin;

View File

@ -1,8 +1,8 @@
jest.mock('eslint', () => ({ jest.mock('eslint/use-at-your-own-risk', () => ({
ESLint: jest.fn(), LegacyESLint: jest.fn(),
})); }));
import { ESLint } from 'eslint'; const { LegacyESLint } = require('eslint/use-at-your-own-risk');
import { resolveAndInstantiateESLint } from './eslint-utils'; import { resolveAndInstantiateESLint } from './eslint-utils';
describe('eslint-utils', () => { describe('eslint-utils', () => {
@ -18,7 +18,7 @@ describe('eslint-utils', () => {
cacheStrategy: 'content', cacheStrategy: 'content',
}).catch(() => {}); }).catch(() => {});
expect(ESLint).toHaveBeenCalledWith({ expect(LegacyESLint).toHaveBeenCalledWith({
overrideConfigFile: './.eslintrc.json', overrideConfigFile: './.eslintrc.json',
fix: true, fix: true,
cache: true, cache: true,
@ -40,7 +40,7 @@ describe('eslint-utils', () => {
cacheStrategy: 'content', cacheStrategy: 'content',
}).catch(() => {}); }).catch(() => {});
expect(ESLint).toHaveBeenCalledWith({ expect(LegacyESLint).toHaveBeenCalledWith({
overrideConfigFile: undefined, overrideConfigFile: undefined,
fix: true, fix: true,
cache: true, cache: true,
@ -63,7 +63,7 @@ describe('eslint-utils', () => {
noEslintrc: true, noEslintrc: true,
}).catch(() => {}); }).catch(() => {});
expect(ESLint).toHaveBeenCalledWith({ expect(LegacyESLint).toHaveBeenCalledWith({
overrideConfigFile: undefined, overrideConfigFile: undefined,
fix: true, fix: true,
cache: true, cache: true,
@ -89,7 +89,7 @@ describe('eslint-utils', () => {
rulesdir: extraRuleDirectories, rulesdir: extraRuleDirectories,
} as any).catch(() => {}); } as any).catch(() => {});
expect(ESLint).toHaveBeenCalledWith({ expect(LegacyESLint).toHaveBeenCalledWith({
overrideConfigFile: undefined, overrideConfigFile: undefined,
fix: true, fix: true,
cache: true, cache: true,
@ -114,7 +114,7 @@ describe('eslint-utils', () => {
resolvePluginsRelativeTo: './some-path', resolvePluginsRelativeTo: './some-path',
} as any).catch(() => {}); } as any).catch(() => {});
expect(ESLint).toHaveBeenCalledWith({ expect(LegacyESLint).toHaveBeenCalledWith({
overrideConfigFile: undefined, overrideConfigFile: undefined,
fix: true, fix: true,
cache: true, cache: true,
@ -135,7 +135,7 @@ describe('eslint-utils', () => {
reportUnusedDisableDirectives: 'error', reportUnusedDisableDirectives: 'error',
} as any).catch(() => {}); } as any).catch(() => {});
expect(ESLint).toHaveBeenCalledWith({ expect(LegacyESLint).toHaveBeenCalledWith({
cache: false, cache: false,
cacheLocation: undefined, cacheLocation: undefined,
cacheStrategy: undefined, cacheStrategy: undefined,
@ -153,7 +153,7 @@ describe('eslint-utils', () => {
it('should create a ESLint instance with no "reportUnusedDisableDirectives" if it is undefined', async () => { it('should create a ESLint instance with no "reportUnusedDisableDirectives" if it is undefined', async () => {
await resolveAndInstantiateESLint(undefined, {} as any); await resolveAndInstantiateESLint(undefined, {} as any);
expect(ESLint).toHaveBeenCalledWith( expect(LegacyESLint).toHaveBeenCalledWith(
expect.objectContaining({ expect.objectContaining({
reportUnusedDisableDirectives: undefined, reportUnusedDisableDirectives: undefined,
}) })

View File

@ -14,7 +14,9 @@ export async function resolveAndInstantiateESLint(
'When using the new Flat Config with ESLint, all configs must be named eslint.config.js or eslint.config.cjs and .eslintrc files may not be used. See https://eslint.org/docs/latest/use/configure/configuration-files' 'When using the new Flat Config with ESLint, all configs must be named eslint.config.js or eslint.config.cjs and .eslintrc files may not be used. See https://eslint.org/docs/latest/use/configure/configuration-files'
); );
} }
const ESLint = await resolveESLintClass(useFlatConfig); const ESLint = await resolveESLintClass({
useFlatConfigOverrideVal: useFlatConfig,
});
const eslintOptions: ESLint.Options = { const eslintOptions: ESLint.Options = {
overrideConfigFile: eslintConfigPath, overrideConfigFile: eslintConfigPath,

View File

@ -2,9 +2,9 @@
exports[`convert-to-flat-config generator should add env configuration 1`] = ` exports[`convert-to-flat-config generator should add env configuration 1`] = `
"const { FlatCompat } = require('@eslint/eslintrc'); "const { FlatCompat } = require('@eslint/eslintrc');
const js = require('@eslint/js');
const nxEslintPlugin = require('@nx/eslint-plugin'); const nxEslintPlugin = require('@nx/eslint-plugin');
const globals = require('globals'); const globals = require('globals');
const js = require('@eslint/js');
const compat = new FlatCompat({ const compat = new FlatCompat({
baseDirectory: __dirname, baseDirectory: __dirname,
@ -52,9 +52,9 @@ module.exports = [
exports[`convert-to-flat-config generator should add global and env configuration 1`] = ` exports[`convert-to-flat-config generator should add global and env configuration 1`] = `
"const { FlatCompat } = require('@eslint/eslintrc'); "const { FlatCompat } = require('@eslint/eslintrc');
const js = require('@eslint/js');
const nxEslintPlugin = require('@nx/eslint-plugin'); const nxEslintPlugin = require('@nx/eslint-plugin');
const globals = require('globals'); const globals = require('globals');
const js = require('@eslint/js');
const compat = new FlatCompat({ const compat = new FlatCompat({
baseDirectory: __dirname, baseDirectory: __dirname,
@ -106,8 +106,8 @@ module.exports = [
exports[`convert-to-flat-config generator should add global configuration 1`] = ` exports[`convert-to-flat-config generator should add global configuration 1`] = `
"const { FlatCompat } = require('@eslint/eslintrc'); "const { FlatCompat } = require('@eslint/eslintrc');
const nxEslintPlugin = require('@nx/eslint-plugin');
const js = require('@eslint/js'); const js = require('@eslint/js');
const nxEslintPlugin = require('@nx/eslint-plugin');
const compat = new FlatCompat({ const compat = new FlatCompat({
baseDirectory: __dirname, baseDirectory: __dirname,
@ -155,8 +155,8 @@ module.exports = [
exports[`convert-to-flat-config generator should add global eslintignores 1`] = ` exports[`convert-to-flat-config generator should add global eslintignores 1`] = `
"const { FlatCompat } = require('@eslint/eslintrc'); "const { FlatCompat } = require('@eslint/eslintrc');
const nxEslintPlugin = require('@nx/eslint-plugin');
const js = require('@eslint/js'); const js = require('@eslint/js');
const nxEslintPlugin = require('@nx/eslint-plugin');
const compat = new FlatCompat({ const compat = new FlatCompat({
baseDirectory: __dirname, baseDirectory: __dirname,
@ -204,9 +204,9 @@ module.exports = [
exports[`convert-to-flat-config generator should add parser 1`] = ` exports[`convert-to-flat-config generator should add parser 1`] = `
"const { FlatCompat } = require('@eslint/eslintrc'); "const { FlatCompat } = require('@eslint/eslintrc');
const js = require('@eslint/js');
const nxEslintPlugin = require('@nx/eslint-plugin'); const nxEslintPlugin = require('@nx/eslint-plugin');
const typescriptEslintParser = require('@typescript-eslint/parser'); const typescriptEslintParser = require('@typescript-eslint/parser');
const js = require('@eslint/js');
const compat = new FlatCompat({ const compat = new FlatCompat({
baseDirectory: __dirname, baseDirectory: __dirname,
@ -254,11 +254,11 @@ module.exports = [
exports[`convert-to-flat-config generator should add plugins 1`] = ` exports[`convert-to-flat-config generator should add plugins 1`] = `
"const { FlatCompat } = require('@eslint/eslintrc'); "const { FlatCompat } = require('@eslint/eslintrc');
const js = require('@eslint/js');
const eslintPluginImport = require('eslint-plugin-import'); const eslintPluginImport = require('eslint-plugin-import');
const eslintPluginSingleName = require('eslint-plugin-single-name'); const eslintPluginSingleName = require('eslint-plugin-single-name');
const scopeEslintPluginWithName = require('@scope/eslint-plugin-with-name'); const scopeEslintPluginWithName = require('@scope/eslint-plugin-with-name');
const justScopeEslintPlugin = require('@just-scope/eslint-plugin'); const justScopeEslintPlugin = require('@just-scope/eslint-plugin');
const js = require('@eslint/js');
const compat = new FlatCompat({ const compat = new FlatCompat({
baseDirectory: __dirname, baseDirectory: __dirname,
@ -312,8 +312,8 @@ module.exports = [
exports[`convert-to-flat-config generator should add settings 1`] = ` exports[`convert-to-flat-config generator should add settings 1`] = `
"const { FlatCompat } = require('@eslint/eslintrc'); "const { FlatCompat } = require('@eslint/eslintrc');
const nxEslintPlugin = require('@nx/eslint-plugin');
const js = require('@eslint/js'); const js = require('@eslint/js');
const nxEslintPlugin = require('@nx/eslint-plugin');
const compat = new FlatCompat({ const compat = new FlatCompat({
baseDirectory: __dirname, baseDirectory: __dirname,
@ -361,8 +361,8 @@ module.exports = [
exports[`convert-to-flat-config generator should convert json successfully 1`] = ` exports[`convert-to-flat-config generator should convert json successfully 1`] = `
"const { FlatCompat } = require('@eslint/eslintrc'); "const { FlatCompat } = require('@eslint/eslintrc');
const nxEslintPlugin = require('@nx/eslint-plugin');
const js = require('@eslint/js'); const js = require('@eslint/js');
const nxEslintPlugin = require('@nx/eslint-plugin');
const compat = new FlatCompat({ const compat = new FlatCompat({
baseDirectory: __dirname, baseDirectory: __dirname,
@ -414,14 +414,17 @@ module.exports = [
...baseConfig, ...baseConfig,
{ {
files: ['**/*.ts', '**/*.tsx', '**/*.js', '**/*.jsx'], files: ['**/*.ts', '**/*.tsx', '**/*.js', '**/*.jsx'],
// Override or add rules here
rules: {}, rules: {},
}, },
{ {
files: ['**/*.ts', '**/*.tsx'], files: ['**/*.ts', '**/*.tsx'],
// Override or add rules here
rules: {}, rules: {},
}, },
{ {
files: ['**/*.js', '**/*.jsx'], files: ['**/*.js', '**/*.jsx'],
// Override or add rules here
rules: {}, rules: {},
}, },
]; ];
@ -430,8 +433,8 @@ module.exports = [
exports[`convert-to-flat-config generator should convert yaml successfully 1`] = ` exports[`convert-to-flat-config generator should convert yaml successfully 1`] = `
"const { FlatCompat } = require('@eslint/eslintrc'); "const { FlatCompat } = require('@eslint/eslintrc');
const nxEslintPlugin = require('@nx/eslint-plugin');
const js = require('@eslint/js'); const js = require('@eslint/js');
const nxEslintPlugin = require('@nx/eslint-plugin');
const compat = new FlatCompat({ const compat = new FlatCompat({
baseDirectory: __dirname, baseDirectory: __dirname,
@ -483,14 +486,17 @@ module.exports = [
...baseConfig, ...baseConfig,
{ {
files: ['**/*.ts', '**/*.tsx', '**/*.js', '**/*.jsx'], files: ['**/*.ts', '**/*.tsx', '**/*.js', '**/*.jsx'],
// Override or add rules here
rules: {}, rules: {},
}, },
{ {
files: ['**/*.ts', '**/*.tsx'], files: ['**/*.ts', '**/*.tsx'],
// Override or add rules here
rules: {}, rules: {},
}, },
{ {
files: ['**/*.js', '**/*.jsx'], files: ['**/*.js', '**/*.jsx'],
// Override or add rules here
rules: {}, rules: {},
}, },
]; ];
@ -499,8 +505,8 @@ module.exports = [
exports[`convert-to-flat-config generator should convert yml successfully 1`] = ` exports[`convert-to-flat-config generator should convert yml successfully 1`] = `
"const { FlatCompat } = require('@eslint/eslintrc'); "const { FlatCompat } = require('@eslint/eslintrc');
const nxEslintPlugin = require('@nx/eslint-plugin');
const js = require('@eslint/js'); const js = require('@eslint/js');
const nxEslintPlugin = require('@nx/eslint-plugin');
const compat = new FlatCompat({ const compat = new FlatCompat({
baseDirectory: __dirname, baseDirectory: __dirname,
@ -552,14 +558,17 @@ module.exports = [
...baseConfig, ...baseConfig,
{ {
files: ['**/*.ts', '**/*.tsx', '**/*.js', '**/*.jsx'], files: ['**/*.ts', '**/*.tsx', '**/*.js', '**/*.jsx'],
// Override or add rules here
rules: {}, rules: {},
}, },
{ {
files: ['**/*.ts', '**/*.tsx'], files: ['**/*.ts', '**/*.tsx'],
// Override or add rules here
rules: {}, rules: {},
}, },
{ {
files: ['**/*.js', '**/*.jsx'], files: ['**/*.js', '**/*.jsx'],
// Override or add rules here
rules: {}, rules: {},
}, },
]; ];
@ -573,14 +582,17 @@ module.exports = [
...baseConfig, ...baseConfig,
{ {
files: ['**/*.ts', '**/*.tsx', '**/*.js', '**/*.jsx'], files: ['**/*.ts', '**/*.tsx', '**/*.js', '**/*.jsx'],
// Override or add rules here
rules: {}, rules: {},
}, },
{ {
files: ['**/*.ts', '**/*.tsx'], files: ['**/*.ts', '**/*.tsx'],
// Override or add rules here
rules: {}, rules: {},
}, },
{ {
files: ['**/*.js', '**/*.jsx'], files: ['**/*.js', '**/*.jsx'],
// Override or add rules here
rules: {}, rules: {},
}, },
{ ignores: ['ignore/me'] }, { ignores: ['ignore/me'] },

View File

@ -67,12 +67,12 @@ describe('convertEslintJsonToFlatConfig', () => {
expect(content).toMatchInlineSnapshot(` expect(content).toMatchInlineSnapshot(`
"const { FlatCompat } = require("@eslint/eslintrc"); "const { FlatCompat } = require("@eslint/eslintrc");
const nxEslintPlugin = require("@nx/eslint-plugin");
const js = require("@eslint/js"); const js = require("@eslint/js");
const nxEslintPlugin = require("@nx/eslint-plugin");
const compat = new FlatCompat({ const compat = new FlatCompat({
baseDirectory: __dirname, baseDirectory: __dirname,
recommendedConfig: js.configs.recommended, recommendedConfig: js.configs.recommended,
}); });
module.exports = [ module.exports = [
@ -182,13 +182,13 @@ describe('convertEslintJsonToFlatConfig', () => {
expect(content).toMatchInlineSnapshot(` expect(content).toMatchInlineSnapshot(`
"const { FlatCompat } = require("@eslint/eslintrc"); "const { FlatCompat } = require("@eslint/eslintrc");
const js = require("@eslint/js");
const baseConfig = require("../../eslint.config.js"); const baseConfig = require("../../eslint.config.js");
const globals = require("globals"); const globals = require("globals");
const js = require("@eslint/js");
const compat = new FlatCompat({ const compat = new FlatCompat({
baseDirectory: __dirname, baseDirectory: __dirname,
recommendedConfig: js.configs.recommended, recommendedConfig: js.configs.recommended,
}); });
module.exports = [ module.exports = [
@ -213,6 +213,7 @@ describe('convertEslintJsonToFlatConfig', () => {
"**/*.ts", "**/*.ts",
"**/*.tsx" "**/*.tsx"
], ],
// Override or add rules here
rules: {} rules: {}
}, },
{ {
@ -220,16 +221,14 @@ describe('convertEslintJsonToFlatConfig', () => {
"**/*.js", "**/*.js",
"**/*.jsx" "**/*.jsx"
], ],
// Override or add rules here
rules: {} rules: {}
}, },
...compat.config({ parser: "jsonc-eslint-parser" }).map(config => ({ {
...config,
files: ["**/*.json"], files: ["**/*.json"],
rules: { rules: { "@nx/dependency-checks": "error" },
...config.rules, languageOptions: { parser: require("jsonc-eslint-parser") }
"@nx/dependency-checks": "error" },
}
})),
{ ignores: [".next/**/*"] }, { ignores: [".next/**/*"] },
{ ignores: ["something/else"] } { ignores: ["something/else"] }
]; ];

View File

@ -2,6 +2,7 @@ import { Tree, names } from '@nx/devkit';
import { ESLint } from 'eslint'; import { ESLint } from 'eslint';
import * as ts from 'typescript'; import * as ts from 'typescript';
import { import {
addFlatCompatToFlatConfig,
createNodeList, createNodeList,
generateAst, generateAst,
generateFlatOverride, generateFlatOverride,
@ -149,6 +150,21 @@ export function convertEslintJsonToFlatConfig(
isFlatCompatNeeded = true; isFlatCompatNeeded = true;
} }
exportElements.push(generateFlatOverride(override)); exportElements.push(generateFlatOverride(override));
// eslint-plugin-import cannot be used with ESLint v9 yet
// TODO(jack): Once v9 support is released, remove this block.
// See: https://github.com/import-js/eslint-plugin-import/pull/2996
if (override.extends === 'plugin:@nx/react') {
exportElements.push(
generateFlatOverride({
rules: {
'import/first': 'off',
'import/no-amd': 'off',
'import/no-webpack-loader-syntax': 'off',
},
})
);
}
}); });
} }
@ -181,14 +197,14 @@ export function convertEslintJsonToFlatConfig(
} }
// create the node list and print it to new file // create the node list and print it to new file
const nodeList = createNodeList( const nodeList = createNodeList(importsMap, exportElements);
importsMap, let content = stringifyNodeList(nodeList);
exportElements, if (isFlatCompatNeeded) {
isFlatCompatNeeded content = addFlatCompatToFlatConfig(content);
); }
return { return {
content: stringifyNodeList(nodeList), content,
addESLintRC: isFlatCompatNeeded, addESLintRC: isFlatCompatNeeded,
addESLintJS: isESLintJSNeeded, addESLintJS: isESLintJSNeeded,
}; };

View File

@ -147,8 +147,8 @@ describe('convert-to-flat-config generator', () => {
expect(tree.read('eslint.config.js', 'utf-8')).toMatchInlineSnapshot(` expect(tree.read('eslint.config.js', 'utf-8')).toMatchInlineSnapshot(`
"const { FlatCompat } = require('@eslint/eslintrc'); "const { FlatCompat } = require('@eslint/eslintrc');
const nxEslintPlugin = require('@nx/eslint-plugin');
const js = require('@eslint/js'); const js = require('@eslint/js');
const nxEslintPlugin = require('@nx/eslint-plugin');
const compat = new FlatCompat({ const compat = new FlatCompat({
baseDirectory: __dirname, baseDirectory: __dirname,
@ -201,14 +201,17 @@ describe('convert-to-flat-config generator', () => {
...baseConfig, ...baseConfig,
{ {
files: ['**/*.ts', '**/*.tsx', '**/*.js', '**/*.jsx'], files: ['**/*.ts', '**/*.tsx', '**/*.js', '**/*.jsx'],
// Override or add rules here
rules: {}, rules: {},
}, },
{ {
files: ['**/*.ts', '**/*.tsx'], files: ['**/*.ts', '**/*.tsx'],
// Override or add rules here
rules: {}, rules: {},
}, },
{ {
files: ['**/*.js', '**/*.jsx'], files: ['**/*.js', '**/*.jsx'],
// Override or add rules here
rules: {}, rules: {},
}, },
]; ];
@ -392,8 +395,8 @@ describe('convert-to-flat-config generator', () => {
expect(tree.read('eslint.config.js', 'utf-8')).toMatchInlineSnapshot(` expect(tree.read('eslint.config.js', 'utf-8')).toMatchInlineSnapshot(`
"const { FlatCompat } = require('@eslint/eslintrc'); "const { FlatCompat } = require('@eslint/eslintrc');
const nxEslintPlugin = require('@nx/eslint-plugin');
const js = require('@eslint/js'); const js = require('@eslint/js');
const nxEslintPlugin = require('@nx/eslint-plugin');
const compat = new FlatCompat({ const compat = new FlatCompat({
baseDirectory: __dirname, baseDirectory: __dirname,
@ -554,6 +557,7 @@ describe('convert-to-flat-config generator', () => {
...baseConfig, ...baseConfig,
{ {
files: ['**/*.ts', '**/*.tsx', '**/*.js', '**/*.jsx'], files: ['**/*.ts', '**/*.tsx', '**/*.js', '**/*.jsx'],
// Override or add rules here
rules: {}, rules: {},
languageOptions: { languageOptions: {
parserOptions: { project: ['apps/dx-assets-ui/tsconfig.*?.json'] }, parserOptions: { project: ['apps/dx-assets-ui/tsconfig.*?.json'] },
@ -561,10 +565,12 @@ describe('convert-to-flat-config generator', () => {
}, },
{ {
files: ['**/*.ts', '**/*.tsx'], files: ['**/*.ts', '**/*.tsx'],
// Override or add rules here
rules: {}, rules: {},
}, },
{ {
files: ['**/*.js', '**/*.jsx'], files: ['**/*.js', '**/*.jsx'],
// Override or add rules here
rules: {}, rules: {},
}, },
{ ignores: ['__fixtures__/**/*'] }, { ignores: ['__fixtures__/**/*'] },

View File

@ -15,12 +15,16 @@ import {
import { ConvertToFlatConfigGeneratorSchema } from './schema'; import { ConvertToFlatConfigGeneratorSchema } from './schema';
import { findEslintFile } from '../utils/eslint-file'; import { findEslintFile } from '../utils/eslint-file';
import { join } from 'path'; import { join } from 'path';
import { eslintrcVersion, eslintVersion } from '../../utils/versions'; import {
eslint9__eslintVersion,
eslint9__typescriptESLintVersion,
eslintConfigPrettierVersion,
eslintrcVersion,
eslintVersion,
} from '../../utils/versions';
import { ESLint } from 'eslint'; import { ESLint } from 'eslint';
import { convertEslintJsonToFlatConfig } from './converters/json-converter'; import { convertEslintJsonToFlatConfig } from './converters/json-converter';
let shouldInstallDeps = false;
export async function convertToFlatConfigGenerator( export async function convertToFlatConfigGenerator(
tree: Tree, tree: Tree,
options: ConvertToFlatConfigGeneratorSchema options: ConvertToFlatConfigGeneratorSchema
@ -65,9 +69,7 @@ export async function convertToFlatConfigGenerator(
await formatFiles(tree); await formatFiles(tree);
} }
if (shouldInstallDeps) { return () => installPackagesTask(tree);
return () => installPackagesTask(tree);
}
} }
export default convertToFlatConfigGenerator; export default convertToFlatConfigGenerator;
@ -221,25 +223,21 @@ function processConvertedConfig(
// save new // save new
tree.write(join(root, target), content); tree.write(join(root, target), content);
// These dependencies are required for flat configs that are generated by subsequent app/lib generators.
const devDependencies: Record<string, string> = {
eslint: eslint9__eslintVersion,
'eslint-config-prettier': eslintConfigPrettierVersion,
'typescript-eslint': eslint9__typescriptESLintVersion,
};
// add missing packages // add missing packages
if (addESLintRC) { if (addESLintRC) {
shouldInstallDeps = true; devDependencies['@eslint/eslintrc'] = eslintrcVersion;
addDependenciesToPackageJson(
tree,
{},
{
'@eslint/eslintrc': eslintrcVersion,
}
);
} }
if (addESLintJS) { if (addESLintJS) {
shouldInstallDeps = true; devDependencies['@eslint/js'] = eslintVersion;
addDependenciesToPackageJson(
tree,
{},
{
'@eslint/js': eslintVersion,
}
);
} }
addDependenciesToPackageJson(tree, {}, devDependencies);
} }

View File

@ -2,10 +2,9 @@ import { Linter } from 'eslint';
import { import {
addBlockToFlatConfigExport, addBlockToFlatConfigExport,
addImportToFlatConfig, addImportToFlatConfig,
addPluginsToExportsBlock,
createNodeList, createNodeList,
generateAst,
generateFlatOverride, generateFlatOverride,
generateFlatPredefinedConfig,
stringifyNodeList, stringifyNodeList,
} from '../utils/flat-config/ast-utils'; } from '../utils/flat-config/ast-utils';
@ -93,40 +92,56 @@ export const getGlobalEsLintConfiguration = (
}; };
export const getGlobalFlatEslintConfiguration = ( export const getGlobalFlatEslintConfiguration = (
unitTestRunner?: string,
rootProject?: boolean rootProject?: boolean
): string => { ): string => {
const nodeList = createNodeList(new Map(), [], true); const nodeList = createNodeList(new Map(), []);
let content = stringifyNodeList(nodeList); let content = stringifyNodeList(nodeList);
content = addImportToFlatConfig(content, 'nxPlugin', '@nx/eslint-plugin'); content = addImportToFlatConfig(content, 'nx', '@nx/eslint-plugin');
content = addPluginsToExportsBlock(content, [
{ name: '@nx', varName: 'nxPlugin', imp: '@nx/eslint-plugin' }, content = addBlockToFlatConfigExport(
]); content,
generateFlatPredefinedConfig('flat/base'),
{ insertAtTheEnd: false }
);
content = addBlockToFlatConfigExport(
content,
generateFlatPredefinedConfig('flat/typescript')
);
content = addBlockToFlatConfigExport(
content,
generateFlatPredefinedConfig('flat/javascript')
);
if (!rootProject) { if (!rootProject) {
content = addBlockToFlatConfigExport( content = addBlockToFlatConfigExport(
content, content,
generateFlatOverride(moduleBoundariesOverride) generateFlatOverride({
files: ['*.ts', '*.tsx', '*.js', '*.jsx'],
rules: {
'@nx/enforce-module-boundaries': [
'error',
{
enforceBuildableLibDependency: true,
allow: [
// This allows a root project to be present without causing lint errors
// since all projects will depend on this base file.
'^.*/eslint(\\.base)?\\.config\\.[cm]?js$',
],
depConstraints: [
{ sourceTag: '*', onlyDependOnLibsWithTags: ['*'] },
],
},
],
} as Linter.RulesRecord,
})
); );
} }
content = addBlockToFlatConfigExport( content = addBlockToFlatConfigExport(
content, content,
generateFlatOverride(typeScriptOverride) generateFlatOverride({
); files: ['**/*.ts', '**/*.tsx', '**/*.js', '**/*.jsx'],
content = addBlockToFlatConfigExport( rules: {},
content,
generateFlatOverride(javaScriptOverride)
);
if (unitTestRunner === 'jest') {
content = addBlockToFlatConfigExport(
content,
generateFlatOverride(jestOverride)
);
}
// add ignore for .nx folder
content = addBlockToFlatConfigExport(
content,
generateAst({
ignores: ['.nx'],
}) })
); );

View File

@ -23,6 +23,7 @@ import {
generateSpreadElement, generateSpreadElement,
removeCompatExtends, removeCompatExtends,
removePlugin, removePlugin,
removePredefinedConfigs,
} from '../utils/flat-config/ast-utils'; } from '../utils/flat-config/ast-utils';
import { hasEslintPlugin } from '../utils/plugin'; import { hasEslintPlugin } from '../utils/plugin';
import { ESLINT_CONFIG_FILENAMES } from '../../utils/config-file'; import { ESLINT_CONFIG_FILENAMES } from '../../utils/config-file';
@ -59,7 +60,7 @@ export function migrateConfigToMonorepoStyle(
tree.exists('eslint.config.js') tree.exists('eslint.config.js')
? 'eslint.base.config.js' ? 'eslint.base.config.js'
: 'eslint.config.js', : 'eslint.config.js',
getGlobalFlatEslintConfiguration(unitTestRunner) getGlobalFlatEslintConfiguration()
); );
} else { } else {
const eslintFile = findEslintFile(tree, '.'); const eslintFile = findEslintFile(tree, '.');
@ -152,6 +153,11 @@ function migrateEslintFile(projectEslintPath: string, tree: Tree) {
'plugin:@nrwl/typescript', 'plugin:@nrwl/typescript',
'plugin:@nrwl/javascript', 'plugin:@nrwl/javascript',
]); ]);
config = removePredefinedConfigs(config, '@nx/eslint-plugin', 'nx', [
'flat/base',
'flat/typescript',
'flat/javascript',
]);
tree.write(projectEslintPath, config); tree.write(projectEslintPath, config);
} else { } else {
updateJson(tree, projectEslintPath, (json) => { updateJson(tree, projectEslintPath, (json) => {

View File

@ -42,7 +42,53 @@ describe('@nx/eslint:lint-project', () => {
}); });
}); });
it('should generate a eslint config and configure the target in project configuration', async () => { it('should generate a flat eslint base config', async () => {
const originalEslintUseFlatConfigVal = process.env.ESLINT_USE_FLAT_CONFIG;
process.env.ESLINT_USE_FLAT_CONFIG = 'true';
await lintProjectGenerator(tree, {
...defaultOptions,
linter: Linter.EsLint,
project: 'test-lib',
setParserOptionsProject: false,
});
expect(tree.read('eslint.config.js', 'utf-8')).toMatchInlineSnapshot(`
"const nx = require('@nx/eslint-plugin');
module.exports = [
...nx.configs['flat/base'],
...nx.configs['flat/typescript'],
...nx.configs['flat/javascript'],
{
files: ['**/*.ts', '**/*.tsx', '**/*.js', '**/*.jsx'],
rules: {
'@nx/enforce-module-boundaries': [
'error',
{
enforceBuildableLibDependency: true,
allow: ['^.*/eslint(\\\\.base)?\\\\.config\\\\.[cm]?js$'],
depConstraints: [
{
sourceTag: '*',
onlyDependOnLibsWithTags: ['*'],
},
],
},
],
},
},
{
files: ['**/*.ts', '**/*.tsx', '**/*.js', '**/*.jsx'],
// Override or add rules here
rules: {},
},
];
"
`);
process.env.ESLINT_USE_FLAT_CONFIG = originalEslintUseFlatConfigVal;
});
it('should generate a eslint config (legacy)', async () => {
await lintProjectGenerator(tree, { await lintProjectGenerator(tree, {
...defaultOptions, ...defaultOptions,
linter: Linter.EsLint, linter: Linter.EsLint,
@ -121,7 +167,12 @@ describe('@nx/eslint:lint-project', () => {
"files": ["*.json"], "files": ["*.json"],
"parser": "jsonc-eslint-parser", "parser": "jsonc-eslint-parser",
"rules": { "rules": {
"@nx/dependency-checks": "error" "@nx/dependency-checks": [
"error",
{
"ignoredFiles": ["{projectRoot}/eslint.config.{js,cjs,mjs}"]
}
]
} }
} }
] ]

View File

@ -197,60 +197,69 @@ function createEsLintConfiguration(
const pathToRootConfig = extendedRootConfig const pathToRootConfig = extendedRootConfig
? `${offsetFromRoot(projectConfig.root)}${extendedRootConfig}` ? `${offsetFromRoot(projectConfig.root)}${extendedRootConfig}`
: undefined; : undefined;
const addDependencyChecks = isBuildableLibraryProject(projectConfig); const addDependencyChecks =
const overrides: Linter.ConfigOverride<Linter.RulesRecord>[] = [
{
files: ['*.ts', '*.tsx', '*.js', '*.jsx'],
/**
* NOTE: We no longer set parserOptions.project by default when creating new projects.
*
* We have observed that users rarely add rules requiring type-checking to their Nx workspaces, and therefore
* do not actually need the capabilites which parserOptions.project provides. When specifying parserOptions.project,
* typescript-eslint needs to create full TypeScript Programs for you. When omitting it, it can perform a simple
* parse (and AST tranformation) of the source files it encounters during a lint run, which is much faster and much
* less memory intensive.
*
* In the rare case that users attempt to add rules requiring type-checking to their setup later on (and haven't set
* parserOptions.project), the executor will attempt to look for the particular error typescript-eslint gives you
* and provide feedback to the user.
*/
parserOptions: !setParserOptionsProject
? undefined
: {
project: [`${projectConfig.root}/tsconfig.*?.json`],
},
/**
* Having an empty rules object present makes it more obvious to the user where they would
* extend things from if they needed to
*/
rules: {},
},
{
files: ['*.ts', '*.tsx'],
rules: {},
},
{
files: ['*.js', '*.jsx'],
rules: {},
},
];
if (
options.addPackageJsonDependencyChecks || options.addPackageJsonDependencyChecks ||
isBuildableLibraryProject(projectConfig) isBuildableLibraryProject(projectConfig);
) {
const overrides: Linter.ConfigOverride<Linter.RulesRecord>[] = useFlatConfig(
tree
)
? // For flat configs, we don't need to generate different overrides for each file. Users should add their own overrides as needed.
[]
: [
{
files: ['*.ts', '*.tsx', '*.js', '*.jsx'],
/**
* NOTE: We no longer set parserOptions.project by default when creating new projects.
*
* We have observed that users rarely add rules requiring type-checking to their Nx workspaces, and therefore
* do not actually need the capabilites which parserOptions.project provides. When specifying parserOptions.project,
* typescript-eslint needs to create full TypeScript Programs for you. When omitting it, it can perform a simple
* parse (and AST tranformation) of the source files it encounters during a lint run, which is much faster and much
* less memory intensive.
*
* In the rare case that users attempt to add rules requiring type-checking to their setup later on (and haven't set
* parserOptions.project), the executor will attempt to look for the particular error typescript-eslint gives you
* and provide feedback to the user.
*/
parserOptions: !setParserOptionsProject
? undefined
: {
project: [`${projectConfig.root}/tsconfig.*?.json`],
},
/**
* Having an empty rules object present makes it more obvious to the user where they would
* extend things from if they needed to
*/
rules: {},
},
{
files: ['*.ts', '*.tsx'],
rules: {},
},
{
files: ['*.js', '*.jsx'],
rules: {},
},
];
if (addDependencyChecks) {
overrides.push({ overrides.push({
files: ['*.json'], files: ['*.json'],
parser: 'jsonc-eslint-parser', parser: 'jsonc-eslint-parser',
rules: { rules: {
'@nx/dependency-checks': 'error', '@nx/dependency-checks': [
'error',
{
// With flat configs, we don't want to include imports in the eslint js/cjs/mjs files to be checked
ignoredFiles: ['{projectRoot}/eslint.config.{js,cjs,mjs}'],
},
],
}, },
}); });
} }
if (useFlatConfig(tree)) { if (useFlatConfig(tree)) {
const isCompatNeeded = addDependencyChecks;
const nodes = []; const nodes = [];
const importMap = new Map(); const importMap = new Map();
if (extendedRootConfig) { if (extendedRootConfig) {
@ -260,7 +269,7 @@ function createEsLintConfiguration(
overrides.forEach((override) => { overrides.forEach((override) => {
nodes.push(generateFlatOverride(override)); nodes.push(generateFlatOverride(override));
}); });
const nodeList = createNodeList(importMap, nodes, isCompatNeeded); const nodeList = createNodeList(importMap, nodes);
const content = stringifyNodeList(nodeList); const content = stringifyNodeList(nodeList);
tree.write(join(projectConfig.root, 'eslint.config.js'), content); tree.write(join(projectConfig.root, 'eslint.config.js'), content);
} else { } else {

View File

@ -4,12 +4,18 @@ import {
type GeneratorCallback, type GeneratorCallback,
type Tree, type Tree,
} from '@nx/devkit'; } from '@nx/devkit';
import { useFlatConfig } from '../../utils/flat-config';
import { import {
eslint9__eslintVersion,
eslint9__typescriptESLintVersion,
eslintConfigPrettierVersion, eslintConfigPrettierVersion,
nxVersion, nxVersion,
typescriptESLintVersion, typescriptESLintVersion,
} from '../../utils/versions'; } from '../../utils/versions';
import { getGlobalEsLintConfiguration } from '../init/global-eslint-config'; import {
getGlobalEsLintConfiguration,
getGlobalFlatEslintConfiguration,
} from '../init/global-eslint-config';
import { findEslintFile } from '../utils/eslint-file'; import { findEslintFile } from '../utils/eslint-file';
export type SetupRootEsLintOptions = { export type SetupRootEsLintOptions = {
@ -26,7 +32,13 @@ export function setupRootEsLint(
if (rootEslintFile) { if (rootEslintFile) {
return () => {}; return () => {};
} }
if (!useFlatConfig(tree)) {
return setUpLegacyRootEslintRc(tree, options);
}
return setUpRootFlatConfig(tree, options);
}
function setUpLegacyRootEslintRc(tree: Tree, options: SetupRootEsLintOptions) {
writeJson( writeJson(
tree, tree,
'.eslintrc.json', '.eslintrc.json',
@ -56,3 +68,24 @@ export function setupRootEsLint(
) )
: () => {}; : () => {};
} }
function setUpRootFlatConfig(tree: Tree, options: SetupRootEsLintOptions) {
tree.write(
'eslint.config.js',
getGlobalFlatEslintConfiguration(options.rootProject)
);
return !options.skipPackageJson
? addDependenciesToPackageJson(
tree,
{},
{
'@eslint/js': eslint9__eslintVersion,
'@nx/eslint-plugin': nxVersion,
eslint: eslint9__eslintVersion,
'eslint-config-prettier': eslintConfigPrettierVersion,
'typescript-eslint': eslint9__typescriptESLintVersion,
}
)
: () => {};
}

View File

@ -1,3 +1,10 @@
import { readJson, type Tree } from '@nx/devkit';
import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing';
import * as devkitInternals from 'nx/src/devkit-internals';
import {
ESLINT_CONFIG_FILENAMES,
baseEsLintConfigFile,
} from '../../utils/config-file';
import { import {
addExtendsToLintConfig, addExtendsToLintConfig,
findEslintFile, findEslintFile,
@ -5,13 +12,6 @@ import {
replaceOverridesInLintConfig, replaceOverridesInLintConfig,
} from './eslint-file'; } from './eslint-file';
import { Tree, readJson } from '@nx/devkit';
import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing';
import {
ESLINT_CONFIG_FILENAMES,
baseEsLintConfigFile,
} from '../../utils/config-file';
describe('@nx/eslint:lint-file', () => { describe('@nx/eslint:lint-file', () => {
let tree: Tree; let tree: Tree;
@ -120,6 +120,236 @@ describe('@nx/eslint:lint-file', () => {
'../../.eslintrc', '../../.eslintrc',
]); ]);
}); });
it('should add extends to flat config', () => {
tree.write('eslint.config.js', 'module.exports = {};');
tree.write(
'apps/demo/eslint.config.js',
`const baseConfig = require("../../eslint.config.js");
module.exports = [
...baseConfig,
{
files: [
"**/*.ts",
"**/*.tsx",
"**/*.js",
"**/*.jsx"
],
rules: {}
},
];`
);
addExtendsToLintConfig(tree, 'apps/demo', 'plugin:playwright/recommend');
expect(tree.read('apps/demo/eslint.config.js', 'utf-8'))
.toMatchInlineSnapshot(`
"const { FlatCompat } = require("@eslint/eslintrc");
const js = require("@eslint/js");
const baseConfig = require("../../eslint.config.js");
const compat = new FlatCompat({
baseDirectory: __dirname,
recommendedConfig: js.configs.recommended,
});
module.exports = [
...compat.extends("plugin:playwright/recommend"),
...baseConfig,
{
files: [
"**/*.ts",
"**/*.tsx",
"**/*.js",
"**/*.jsx"
],
rules: {}
},
];"
`);
});
it('should add wrapped plugin for compat in extends when using eslint v9', () => {
// mock eslint version
jest.spyOn(devkitInternals, 'readModulePackageJson').mockReturnValue({
packageJson: { name: 'eslint', version: '9.0.0' },
path: '',
});
tree.write('eslint.config.js', 'module.exports = {};');
tree.write(
'apps/demo/eslint.config.js',
`const baseConfig = require("../../eslint.config.js");
module.exports = [
...baseConfig,
{
files: [
"**/*.ts",
"**/*.tsx",
"**/*.js",
"**/*.jsx"
],
rules: {}
},
];`
);
addExtendsToLintConfig(tree, 'apps/demo', {
name: 'plugin:playwright/recommend',
needCompatFixup: true,
});
expect(tree.read('apps/demo/eslint.config.js', 'utf-8'))
.toMatchInlineSnapshot(`
"const { FlatCompat } = require("@eslint/eslintrc");
const js = require("@eslint/js");
const { fixupConfigRules } = require("@eslint/compat");
const baseConfig = require("../../eslint.config.js");
const compat = new FlatCompat({
baseDirectory: __dirname,
recommendedConfig: js.configs.recommended,
});
module.exports = [
...fixupConfigRules(compat.extends("plugin:playwright/recommend")),
...baseConfig,
{
files: [
"**/*.ts",
"**/*.tsx",
"**/*.js",
"**/*.jsx"
],
rules: {}
},
];"
`);
});
it('should handle mixed multiple incompatible and compatible plugins and add them to extends in the specified order when using eslint v9', () => {
// mock eslint version
jest.spyOn(devkitInternals, 'readModulePackageJson').mockReturnValue({
packageJson: { name: 'eslint', version: '9.0.0' },
path: '',
});
tree.write('eslint.config.js', 'module.exports = {};');
tree.write(
'apps/demo/eslint.config.js',
`const baseConfig = require("../../eslint.config.js");
module.exports = [
...baseConfig,
{
files: [
"**/*.ts",
"**/*.tsx",
"**/*.js",
"**/*.jsx"
],
rules: {}
},
];`
);
addExtendsToLintConfig(tree, 'apps/demo', [
'plugin:some-plugin1',
'plugin:some-plugin2',
{ name: 'incompatible-plugin1', needCompatFixup: true },
{ name: 'incompatible-plugin2', needCompatFixup: true },
'plugin:some-plugin3',
{ name: 'incompatible-plugin3', needCompatFixup: true },
]);
expect(tree.read('apps/demo/eslint.config.js', 'utf-8'))
.toMatchInlineSnapshot(`
"const { FlatCompat } = require("@eslint/eslintrc");
const js = require("@eslint/js");
const { fixupConfigRules } = require("@eslint/compat");
const baseConfig = require("../../eslint.config.js");
const compat = new FlatCompat({
baseDirectory: __dirname,
recommendedConfig: js.configs.recommended,
});
module.exports = [
...compat.extends("plugin:some-plugin1", "plugin:some-plugin2"),
...fixupConfigRules(compat.extends("incompatible-plugin1")),
...fixupConfigRules(compat.extends("incompatible-plugin2")),
...compat.extends("plugin:some-plugin3"),
...fixupConfigRules(compat.extends("incompatible-plugin3")),
...baseConfig,
{
files: [
"**/*.ts",
"**/*.tsx",
"**/*.js",
"**/*.jsx"
],
rules: {}
},
];"
`);
});
it('should not add wrapped plugin for compat in extends when not using eslint v9', () => {
// mock eslint version
jest.spyOn(devkitInternals, 'readModulePackageJson').mockReturnValue({
packageJson: { name: 'eslint', version: '8.0.0' },
path: '',
});
tree.write('eslint.config.js', 'module.exports = {};');
tree.write(
'apps/demo/eslint.config.js',
`const baseConfig = require("../../eslint.config.js");
module.exports = [
...baseConfig,
{
files: [
"**/*.ts",
"**/*.tsx",
"**/*.js",
"**/*.jsx"
],
rules: {}
},
];`
);
addExtendsToLintConfig(tree, 'apps/demo', {
name: 'plugin:playwright/recommend',
needCompatFixup: true,
});
expect(tree.read('apps/demo/eslint.config.js', 'utf-8'))
.toMatchInlineSnapshot(`
"const { FlatCompat } = require("@eslint/eslintrc");
const js = require("@eslint/js");
const baseConfig = require("../../eslint.config.js");
const compat = new FlatCompat({
baseDirectory: __dirname,
recommendedConfig: js.configs.recommended,
});
module.exports = [
...compat.extends("plugin:playwright/recommend"),
...baseConfig,
{
files: [
"**/*.ts",
"**/*.tsx",
"**/*.js",
"**/*.jsx"
],
rules: {}
},
];"
`);
});
}); });
describe('replaceOverridesInLintConfig', () => { describe('replaceOverridesInLintConfig', () => {
@ -197,10 +427,9 @@ module.exports = [
const baseConfig = require("../../eslint.config.js"); const baseConfig = require("../../eslint.config.js");
const compat = new FlatCompat({ const compat = new FlatCompat({
baseDirectory: __dirname, baseDirectory: __dirname,
recommendedConfig: js.configs.recommended, recommendedConfig: js.configs.recommended,
}); });
module.exports = [ module.exports = [
...baseConfig, ...baseConfig,

View File

@ -1,35 +1,43 @@
import { import {
addDependenciesToPackageJson,
type GeneratorCallback,
joinPathFragments, joinPathFragments,
names, names,
offsetFromRoot, offsetFromRoot,
readJson, readJson,
type Tree,
updateJson, updateJson,
} from '@nx/devkit'; } from '@nx/devkit';
import type { Tree } from '@nx/devkit';
import type { Linter } from 'eslint'; import type { Linter } from 'eslint';
import { import { gte } from 'semver';
flatConfigEslintFilename,
useFlatConfig,
} from '../../utils/flat-config';
import {
addBlockToFlatConfigExport,
addCompatToFlatConfig,
addImportToFlatConfig,
addPluginsToExportsBlock,
generateAst,
generateFlatOverride,
generatePluginExtendsElement,
hasOverride,
removeOverridesFromLintConfig,
replaceOverride,
} from './flat-config/ast-utils';
import ts = require('typescript');
import { mapFilePath } from './flat-config/path-utils';
import { import {
baseEsLintConfigFile, baseEsLintConfigFile,
baseEsLintFlatConfigFile, baseEsLintFlatConfigFile,
ESLINT_CONFIG_FILENAMES, ESLINT_CONFIG_FILENAMES,
} from '../../utils/config-file'; } from '../../utils/config-file';
import {
getRootESLintFlatConfigFilename,
useFlatConfig,
} from '../../utils/flat-config';
import { getInstalledEslintVersion } from '../../utils/version-utils';
import { eslint9__eslintVersion, eslintCompat } from '../../utils/versions';
import {
addBlockToFlatConfigExport,
addFlatCompatToFlatConfig,
addImportToFlatConfig,
addPluginsToExportsBlock,
generateAst,
generateFlatOverride,
generateFlatPredefinedConfig,
generatePluginExtendsElement,
generatePluginExtendsElementWithCompatFixup,
hasOverride,
overrideNeedsCompat,
removeOverridesFromLintConfig,
replaceOverride,
} from './flat-config/ast-utils';
import { mapFilePath } from './flat-config/path-utils';
import ts = require('typescript');
export function findEslintFile( export function findEslintFile(
tree: Tree, tree: Tree,
@ -167,7 +175,7 @@ function offsetFilePath(
export function addOverrideToLintConfig( export function addOverrideToLintConfig(
tree: Tree, tree: Tree,
root: string, root: string,
override: Linter.ConfigOverride<Linter.RulesRecord>, override: Partial<Linter.ConfigOverride<Linter.RulesRecord>>,
options: { insertAtTheEnd?: boolean; checkBaseConfig?: boolean } = { options: { insertAtTheEnd?: boolean; checkBaseConfig?: boolean } = {
insertAtTheEnd: true, insertAtTheEnd: true,
} }
@ -177,13 +185,13 @@ export function addOverrideToLintConfig(
if (useFlatConfig(tree)) { if (useFlatConfig(tree)) {
const fileName = joinPathFragments( const fileName = joinPathFragments(
root, root,
isBase ? baseEsLintFlatConfigFile : flatConfigEslintFilename(tree) isBase ? baseEsLintFlatConfigFile : getRootESLintFlatConfigFilename(tree)
); );
const flatOverride = generateFlatOverride(override); const flatOverride = generateFlatOverride(override);
let content = tree.read(fileName, 'utf8'); let content = tree.read(fileName, 'utf8');
// we will be using compat here so we need to make sure it's added // Check if the provided override using legacy eslintrc properties or plugins, if so we need to add compat
if (overrideNeedsCompat(override)) { if (overrideNeedsCompat(override)) {
content = addCompatToFlatConfig(content); content = addFlatCompatToFlatConfig(content);
} }
tree.write( tree.write(
fileName, fileName,
@ -206,14 +214,6 @@ export function addOverrideToLintConfig(
} }
} }
function overrideNeedsCompat(
override: Linter.ConfigOverride<Linter.RulesRecord>
) {
return (
override.env || override.extends || override.plugins || override.parser
);
}
export function updateOverrideInLintConfig( export function updateOverrideInLintConfig(
tree: Tree, tree: Tree,
root: string, root: string,
@ -223,7 +223,10 @@ export function updateOverrideInLintConfig(
) => Linter.ConfigOverride<Linter.RulesRecord> ) => Linter.ConfigOverride<Linter.RulesRecord>
) { ) {
if (useFlatConfig(tree)) { if (useFlatConfig(tree)) {
const fileName = joinPathFragments(root, flatConfigEslintFilename(tree)); const fileName = joinPathFragments(
root,
getRootESLintFlatConfigFilename(tree)
);
let content = tree.read(fileName, 'utf8'); let content = tree.read(fileName, 'utf8');
content = replaceOverride(content, root, lookup, update); content = replaceOverride(content, root, lookup, update);
tree.write(fileName, content); tree.write(fileName, content);
@ -265,7 +268,7 @@ export function lintConfigHasOverride(
if (useFlatConfig(tree)) { if (useFlatConfig(tree)) {
const fileName = joinPathFragments( const fileName = joinPathFragments(
root, root,
isBase ? baseEsLintFlatConfigFile : flatConfigEslintFilename(tree) isBase ? baseEsLintFlatConfigFile : getRootESLintFlatConfigFilename(tree)
); );
const content = tree.read(fileName, 'utf8'); const content = tree.read(fileName, 'utf8');
return hasOverride(content, lookup); return hasOverride(content, lookup);
@ -285,11 +288,14 @@ export function replaceOverridesInLintConfig(
overrides: Linter.ConfigOverride<Linter.RulesRecord>[] overrides: Linter.ConfigOverride<Linter.RulesRecord>[]
) { ) {
if (useFlatConfig(tree)) { if (useFlatConfig(tree)) {
const fileName = joinPathFragments(root, flatConfigEslintFilename(tree)); const fileName = joinPathFragments(
root,
getRootESLintFlatConfigFilename(tree)
);
let content = tree.read(fileName, 'utf8'); let content = tree.read(fileName, 'utf8');
// we will be using compat here so we need to make sure it's added // Check if any of the provided overrides using legacy eslintrc properties or plugins, if so we need to add compat
if (overrides.some(overrideNeedsCompat)) { if (overrides.some(overrideNeedsCompat)) {
content = addCompatToFlatConfig(content); content = addFlatCompatToFlatConfig(content);
} }
content = removeOverridesFromLintConfig(content); content = removeOverridesFromLintConfig(content);
overrides.forEach((override) => { overrides.forEach((override) => {
@ -310,21 +316,92 @@ export function replaceOverridesInLintConfig(
export function addExtendsToLintConfig( export function addExtendsToLintConfig(
tree: Tree, tree: Tree,
root: string, root: string,
plugin: string | string[] plugin:
) { | string
const plugins = Array.isArray(plugin) ? plugin : [plugin]; | { name: string; needCompatFixup: boolean }
| Array<string | { name: string; needCompatFixup: boolean }>,
insertAtTheEnd = false
): GeneratorCallback {
if (useFlatConfig(tree)) { if (useFlatConfig(tree)) {
const fileName = joinPathFragments(root, flatConfigEslintFilename(tree)); const pluginExtends: ts.SpreadElement[] = [];
const pluginExtends = generatePluginExtendsElement(plugins); const fileName = joinPathFragments(
let content = tree.read(fileName, 'utf8'); root,
content = addCompatToFlatConfig(content); getRootESLintFlatConfigFilename(tree)
tree.write(
fileName,
addBlockToFlatConfigExport(content, pluginExtends, {
insertAtTheEnd: false,
})
); );
let shouldImportEslintCompat = false;
// assume eslint version is 9 if not found, as it's what we'd be generating by default
const eslintVersion =
getInstalledEslintVersion(tree) ?? eslint9__eslintVersion;
if (gte(eslintVersion, '9.0.0')) {
// eslint v9 requires the incompatible plugins to be wrapped with a helper from @eslint/compat
const plugins = (Array.isArray(plugin) ? plugin : [plugin]).map((p) =>
typeof p === 'string' ? { name: p, needCompatFixup: false } : p
);
let compatiblePluginsBatch: string[] = [];
plugins.forEach(({ name, needCompatFixup }) => {
if (needCompatFixup) {
if (compatiblePluginsBatch.length > 0) {
// flush the current batch of compatible plugins and reset it
pluginExtends.push(
generatePluginExtendsElement(compatiblePluginsBatch)
);
compatiblePluginsBatch = [];
}
// generate the extends for the incompatible plugin
pluginExtends.push(generatePluginExtendsElementWithCompatFixup(name));
shouldImportEslintCompat = true;
} else {
// add the compatible plugin to the current batch
compatiblePluginsBatch.push(name);
}
});
if (compatiblePluginsBatch.length > 0) {
// flush the batch of compatible plugins
pluginExtends.push(
generatePluginExtendsElement(compatiblePluginsBatch)
);
}
} else {
const plugins = (Array.isArray(plugin) ? plugin : [plugin]).map((p) =>
typeof p === 'string' ? p : p.name
);
pluginExtends.push(generatePluginExtendsElement(plugins));
}
let content = tree.read(fileName, 'utf8');
if (shouldImportEslintCompat) {
content = addImportToFlatConfig(
content,
['fixupConfigRules'],
'@eslint/compat'
);
}
content = addFlatCompatToFlatConfig(content);
// reverse the order to ensure they are added in the correct order at the
// start of the `extends` array
for (const pluginExtend of pluginExtends.reverse()) {
content = addBlockToFlatConfigExport(content, pluginExtend, {
insertAtTheEnd,
});
}
tree.write(fileName, content);
if (shouldImportEslintCompat) {
return addDependenciesToPackageJson(
tree,
{},
{ '@eslint/compat': eslintCompat },
undefined,
true
);
}
return () => {};
} else { } else {
const plugins = (Array.isArray(plugin) ? plugin : [plugin]).map((p) =>
typeof p === 'string' ? p : p.name
);
const fileName = joinPathFragments(root, '.eslintrc.json'); const fileName = joinPathFragments(root, '.eslintrc.json');
updateJson(tree, fileName, (json) => { updateJson(tree, fileName, (json) => {
json.extends ??= []; json.extends ??= [];
@ -334,9 +411,39 @@ export function addExtendsToLintConfig(
]; ];
return json; return json;
}); });
return () => {};
} }
} }
export function addPredefinedConfigToFlatLintConfig(
tree: Tree,
root: string,
predefinedConfigName: string,
moduleName = 'nx',
moduleImportPath = '@nx/eslint-plugin',
spread = true,
insertAtTheEnd = true
): void {
if (!useFlatConfig(tree))
throw new Error('Predefined configs can only be used with flat configs');
const fileName = joinPathFragments(
root,
getRootESLintFlatConfigFilename(tree)
);
let content = tree.read(fileName, 'utf8');
content = addImportToFlatConfig(content, moduleName, moduleImportPath);
content = addBlockToFlatConfigExport(
content,
generateFlatPredefinedConfig(predefinedConfigName, moduleName, spread),
{ insertAtTheEnd }
);
tree.write(fileName, content);
}
export function addPluginsToLintConfig( export function addPluginsToLintConfig(
tree: Tree, tree: Tree,
root: string, root: string,
@ -344,7 +451,10 @@ export function addPluginsToLintConfig(
) { ) {
const plugins = Array.isArray(plugin) ? plugin : [plugin]; const plugins = Array.isArray(plugin) ? plugin : [plugin];
if (useFlatConfig(tree)) { if (useFlatConfig(tree)) {
const fileName = joinPathFragments(root, flatConfigEslintFilename(tree)); const fileName = joinPathFragments(
root,
getRootESLintFlatConfigFilename(tree)
);
let content = tree.read(fileName, 'utf8'); let content = tree.read(fileName, 'utf8');
const mappedPlugins: { name: string; varName: string; imp: string }[] = []; const mappedPlugins: { name: string; varName: string; imp: string }[] = [];
plugins.forEach((name) => { plugins.forEach((name) => {
@ -372,7 +482,10 @@ export function addIgnoresToLintConfig(
ignorePatterns: string[] ignorePatterns: string[]
) { ) {
if (useFlatConfig(tree)) { if (useFlatConfig(tree)) {
const fileName = joinPathFragments(root, flatConfigEslintFilename(tree)); const fileName = joinPathFragments(
root,
getRootESLintFlatConfigFilename(tree)
);
const block = generateAst<ts.ObjectLiteralExpression>({ const block = generateAst<ts.ObjectLiteralExpression>({
ignores: ignorePatterns.map((path) => mapFilePath(path)), ignores: ignorePatterns.map((path) => mapFilePath(path)),
}); });

View File

@ -1,16 +1,153 @@
import ts = require('typescript'); import ts = require('typescript');
import { import {
addBlockToFlatConfigExport, addBlockToFlatConfigExport,
generateAst, addFlatCompatToFlatConfig,
addImportToFlatConfig, addImportToFlatConfig,
addCompatToFlatConfig, generateAst,
removeOverridesFromLintConfig, generateFlatOverride,
replaceOverride, generatePluginExtendsElementWithCompatFixup,
removePlugin,
removeCompatExtends, removeCompatExtends,
removeImportFromFlatConfig,
removeOverridesFromLintConfig,
removePlugin,
removePredefinedConfigs,
replaceOverride,
} from './ast-utils'; } from './ast-utils';
import { stripIndents } from '@nx/devkit';
describe('ast-utils', () => { describe('ast-utils', () => {
const printer = ts.createPrinter();
function printTsNode(node: ts.Node) {
return printer.printNode(
ts.EmitHint.Unspecified,
node,
ts.createSourceFile('test.ts', '', ts.ScriptTarget.Latest)
);
}
describe('generateFlatOverride', () => {
it('should create appropriate ASTs for a flat config entries based on the provided legacy eslintrc JSON override data', () => {
// It's easier to review the stringified result of the AST than the AST itself
const getOutput = (input: any) => {
const ast = generateFlatOverride(input);
return printTsNode(ast);
};
expect(getOutput({})).toMatchInlineSnapshot(`"{}"`);
// It should apply rules directly
expect(
getOutput({
rules: {
a: 'error',
b: 'off',
c: [
'error',
{
some: {
rich: ['config', 'options'],
},
},
],
},
})
).toMatchInlineSnapshot(`
"{
rules: {
a: "error",
b: "off",
c: [
"error",
{ some: { rich: [
"config",
"options"
] } }
]
}
}"
`);
// It should normalize and apply files as an array
expect(
getOutput({
files: '*.ts', // old single * syntax should be replaced by **/*
})
).toMatchInlineSnapshot(`"{ files: ["**/*.ts"] }"`);
expect(
getOutput({
// It should not only nest the parser in languageOptions, but also wrap it in a require call because parsers are passed by reference in flat config
parser: 'jsonc-eslint-parser',
})
).toMatchInlineSnapshot(`
"{
languageOptions: { parser: require("jsonc-eslint-parser") }
}"
`);
expect(
getOutput({
// It should nest parserOptions in languageOptions
parserOptions: {
foo: 'bar',
},
})
).toMatchInlineSnapshot(`
"{
languageOptions: { parserOptions: { foo: "bar" } }
}"
`);
// It should add the compat tooling for extends, and spread the rules object to allow for easier editing by users
expect(getOutput({ extends: ['plugin:@nx/typescript'] }))
.toMatchInlineSnapshot(`
"...compat.config({ extends: ["plugin:@nx/typescript"] }).map(config => ({
...config,
rules: {
...config.rules
}
}))"
`);
// It should add the compat tooling for plugins, and spread the rules object to allow for easier editing by users
expect(getOutput({ plugins: ['@nx/eslint-plugin'] }))
.toMatchInlineSnapshot(`
"...compat.config({ plugins: ["@nx/eslint-plugin"] }).map(config => ({
...config,
rules: {
...config.rules
}
}))"
`);
// It should add the compat tooling for env, and spread the rules object to allow for easier editing by users
expect(getOutput({ env: { jest: true } })).toMatchInlineSnapshot(`
"...compat.config({ env: { jest: true } }).map(config => ({
...config,
rules: {
...config.rules
}
}))"
`);
// Files for the compat tooling should be added appropriately
expect(getOutput({ env: { jest: true }, files: ['*.ts', '*.tsx'] }))
.toMatchInlineSnapshot(`
"...compat.config({ env: { jest: true } }).map(config => ({
...config,
files: [
"**/*.ts",
"**/*.tsx"
],
rules: {
...config.rules
}
}))"
`);
});
});
describe('addBlockToFlatConfigExport', () => { describe('addBlockToFlatConfigExport', () => {
it('should inject block to the end of the file', () => { it('should inject block to the end of the file', () => {
const content = `const baseConfig = require("../../eslint.config.js"); const content = `const baseConfig = require("../../eslint.config.js");
@ -207,6 +344,32 @@ describe('ast-utils', () => {
}); });
}); });
describe('removeImportFromFlatConfig', () => {
it('should remove existing import from config if the var name matches', () => {
const content = stripIndents`
const nx = require("@nx/eslint-plugin");
const thisShouldRemain = require("@nx/eslint-plugin");
const playwright = require('eslint-plugin-playwright');
module.exports = [
playwright.configs['flat/recommended'],
];
`;
const result = removeImportFromFlatConfig(
content,
'nx',
'@nx/eslint-plugin'
);
expect(result).toMatchInlineSnapshot(`
"
const thisShouldRemain = require("@nx/eslint-plugin");
const playwright = require('eslint-plugin-playwright');
module.exports = [
playwright.configs['flat/recommended'],
];"
`);
});
});
describe('addCompatToFlatConfig', () => { describe('addCompatToFlatConfig', () => {
it('should add compat to config', () => { it('should add compat to config', () => {
const content = `const baseConfig = require("../../eslint.config.js"); const content = `const baseConfig = require("../../eslint.config.js");
@ -221,17 +384,16 @@ describe('ast-utils', () => {
}, },
{ ignores: ["my-lib/.cache/**/*"] }, { ignores: ["my-lib/.cache/**/*"] },
];`; ];`;
const result = addCompatToFlatConfig(content); const result = addFlatCompatToFlatConfig(content);
expect(result).toMatchInlineSnapshot(` expect(result).toMatchInlineSnapshot(`
"const { FlatCompat } = require("@eslint/eslintrc"); "const { FlatCompat } = require("@eslint/eslintrc");
const js = require("@eslint/js"); const js = require("@eslint/js");
const baseConfig = require("../../eslint.config.js"); const baseConfig = require("../../eslint.config.js");
const compat = new FlatCompat({ const compat = new FlatCompat({
baseDirectory: __dirname, baseDirectory: __dirname,
recommendedConfig: js.configs.recommended, recommendedConfig: js.configs.recommended,
}); });
module.exports = [ module.exports = [
...baseConfig, ...baseConfig,
{ {
@ -260,17 +422,16 @@ describe('ast-utils', () => {
}, },
{ ignores: ["my-lib/.cache/**/*"] }, { ignores: ["my-lib/.cache/**/*"] },
];`; ];`;
const result = addCompatToFlatConfig(content); const result = addFlatCompatToFlatConfig(content);
expect(result).toMatchInlineSnapshot(` expect(result).toMatchInlineSnapshot(`
"const { FlatCompat } = require("@eslint/eslintrc"); "const { FlatCompat } = require("@eslint/eslintrc");
const baseConfig = require("../../eslint.config.js"); const baseConfig = require("../../eslint.config.js");
const js = require("@eslint/js"); const js = require("@eslint/js");
const compat = new FlatCompat({ const compat = new FlatCompat({
baseDirectory: __dirname, baseDirectory: __dirname,
recommendedConfig: js.configs.recommended, recommendedConfig: js.configs.recommended,
}); });
module.exports = [ module.exports = [
...baseConfig, ...baseConfig,
{ {
@ -306,7 +467,7 @@ describe('ast-utils', () => {
}, },
{ ignores: ["my-lib/.cache/**/*"] }, { ignores: ["my-lib/.cache/**/*"] },
];`; ];`;
const result = addCompatToFlatConfig(content); const result = addFlatCompatToFlatConfig(content);
expect(result).toEqual(content); expect(result).toEqual(content);
}); });
}); });
@ -833,4 +994,74 @@ describe('ast-utils', () => {
`); `);
}); });
}); });
describe('removePredefinedConfigs', () => {
it('should remove config objects and import', () => {
const content = stripIndents`
const nx = require("@nx/eslint-plugin");
const playwright = require('eslint-plugin-playwright');
module.exports = [
...nx.config['flat/base'],
...nx.config['flat/typescript'],
...nx.config['flat/javascript'],
playwright.configs['flat/recommended'],
];
`;
const result = removePredefinedConfigs(
content,
'@nx/eslint-plugin',
'nx',
['flat/base', 'flat/typescript', 'flat/javascript']
);
expect(result).toMatchInlineSnapshot(`
"
const playwright = require('eslint-plugin-playwright');
module.exports = [
playwright.configs['flat/recommended'],
];"
`);
});
it('should keep configs that are not in the list', () => {
const content = stripIndents`
const nx = require("@nx/eslint-plugin");
const playwright = require('eslint-plugin-playwright');
module.exports = [
...nx.config['flat/base'],
...nx.config['flat/typescript'],
...nx.config['flat/javascript'],
...nx.config['flat/react'],
playwright.configs['flat/recommended'],
];
`;
const result = removePredefinedConfigs(
content,
'@nx/eslint-plugin',
'nx',
['flat/base', 'flat/typescript', 'flat/javascript']
);
expect(result).toMatchInlineSnapshot(`
"const nx = require("@nx/eslint-plugin");
const playwright = require('eslint-plugin-playwright');
module.exports = [
...nx.config['flat/react'],
playwright.configs['flat/recommended'],
];"
`);
});
});
describe('generatePluginExtendsElementWithCompatFixup', () => {
it('should return spread element with fixupConfigRules call wrapping the extended plugin', () => {
const result = generatePluginExtendsElementWithCompatFixup('my-plugin');
expect(printTsNode(result)).toMatchInlineSnapshot(
`"...fixupConfigRules(compat.extends("my-plugin"))"`
);
});
});
}); });

View File

@ -1,8 +1,8 @@
import { import {
ChangeType,
StringChange,
applyChangesToString, applyChangesToString,
ChangeType,
parseJson, parseJson,
StringChange,
} from '@nx/devkit'; } from '@nx/devkit';
import { Linter } from 'eslint'; import { Linter } from 'eslint';
import * as ts from 'typescript'; import * as ts from 'typescript';
@ -101,12 +101,7 @@ export function hasOverride(
// strip any spread elements // strip any spread elements
objSource = fullNodeText.replace(SPREAD_ELEMENTS_REGEXP, ''); objSource = fullNodeText.replace(SPREAD_ELEMENTS_REGEXP, '');
} }
const data = parseJson( const data = parseTextToJson(objSource);
objSource
// ensure property names have double quotes so that JSON.parse works
.replace(/'/g, '"')
.replace(/\s([a-zA-Z0-9_]+)\s*:/g, ' "$1": ')
);
if (lookup(data)) { if (lookup(data)) {
return true; return true;
} }
@ -121,6 +116,8 @@ function parseTextToJson(text: string): any {
// ensure property names have double quotes so that JSON.parse works // ensure property names have double quotes so that JSON.parse works
.replace(/'/g, '"') .replace(/'/g, '"')
.replace(/\s([a-zA-Z0-9_]+)\s*:/g, ' "$1": ') .replace(/\s([a-zA-Z0-9_]+)\s*:/g, ' "$1": ')
// stringify any require calls to avoid JSON parsing errors, turn them into just the string value being required
.replace(/require\(['"]([^'"]+)['"]\)/g, '"$1"')
); );
} }
@ -132,8 +129,8 @@ export function replaceOverride(
root: string, root: string,
lookup: (override: Linter.ConfigOverride<Linter.RulesRecord>) => boolean, lookup: (override: Linter.ConfigOverride<Linter.RulesRecord>) => boolean,
update?: ( update?: (
override: Linter.ConfigOverride<Linter.RulesRecord> override: Partial<Linter.ConfigOverride<Linter.RulesRecord>>
) => Linter.ConfigOverride<Linter.RulesRecord> ) => Partial<Linter.ConfigOverride<Linter.RulesRecord>>
): string { ): string {
const source = ts.createSourceFile( const source = ts.createSourceFile(
'', '',
@ -172,13 +169,18 @@ export function replaceOverride(
start, start,
length: end - start, length: end - start,
}); });
const updatedData = update(data); let updatedData = update(data);
if (updatedData) { if (updatedData) {
mapFilePaths(updatedData); updatedData = mapFilePaths(updatedData);
changes.push({ changes.push({
type: ChangeType.Insert, type: ChangeType.Insert,
index: start, index: start,
text: JSON.stringify(updatedData, null, 2).slice(2, -2), // remove curly braces and start/end line breaks since we are injecting just properties text: JSON.stringify(updatedData, null, 2)
// restore any parser require calls that were stripped during JSON parsing
.replace(/"parser": "([^"]+)"/g, (_, parser) => {
return `"parser": require('${parser}')`;
})
.slice(2, -2), // remove curly braces and start/end line breaks since we are injecting just properties
}); });
} }
} }
@ -313,6 +315,50 @@ export function addImportToFlatConfig(
]); ]);
} }
/**
* Remove an import from flat config
*/
export function removeImportFromFlatConfig(
content: string,
variable: string,
imp: string
): string {
const source = ts.createSourceFile(
'',
content,
ts.ScriptTarget.Latest,
true,
ts.ScriptKind.JS
);
const changes: StringChange[] = [];
ts.forEachChild(source, (node) => {
// we can only combine object binding patterns
if (
ts.isVariableStatement(node) &&
ts.isVariableDeclaration(node.declarationList.declarations[0]) &&
ts.isIdentifier(node.declarationList.declarations[0].name) &&
node.declarationList.declarations[0].name.getText() === variable &&
ts.isCallExpression(node.declarationList.declarations[0].initializer) &&
node.declarationList.declarations[0].initializer.expression.getText() ===
'require' &&
ts.isStringLiteral(
node.declarationList.declarations[0].initializer.arguments[0]
) &&
node.declarationList.declarations[0].initializer.arguments[0].text === imp
) {
changes.push({
type: ChangeType.Delete,
start: node.pos,
length: node.end - node.pos,
});
}
});
return applyChangesToString(content, changes);
}
/** /**
* Injects new ts.expression to the end of the module.exports array. * Injects new ts.expression to the end of the module.exports array.
*/ */
@ -342,6 +388,12 @@ export function addBlockToFlatConfigExport(
return node.expression.right.elements; return node.expression.right.elements;
} }
}); });
// The config is not in the format that we generate with, skip update.
// This could happen during `init-migration` when extracting config from the base, but
// base config was not generated by Nx.
if (!exportsArray) return content;
const insert = printer.printNode(ts.EmitHint.Expression, config, source); const insert = printer.printNode(ts.EmitHint.Expression, config, source);
if (options.insertAtTheEnd) { if (options.insertAtTheEnd) {
const index = const index =
@ -520,7 +572,7 @@ export function removeCompatExtends(
ts.ScriptKind.JS ts.ScriptKind.JS
); );
const changes: StringChange[] = []; const changes: StringChange[] = [];
findAllBlocks(source).forEach((node) => { findAllBlocks(source)?.forEach((node) => {
if ( if (
ts.isSpreadElement(node) && ts.isSpreadElement(node) &&
ts.isCallExpression(node.expression) && ts.isCallExpression(node.expression) &&
@ -554,7 +606,10 @@ export function removeCompatExtends(
text: text:
'\n' + '\n' +
body.replace( body.replace(
new RegExp('[ \t]s*...' + paramName + '[ \t]*,?\\s*', 'g'), new RegExp(
'[ \t]s*...' + paramName + '(\\.rules)?[ \t]*,?\\s*',
'g'
),
'' ''
), ),
}); });
@ -565,6 +620,52 @@ export function removeCompatExtends(
return applyChangesToString(content, changes); return applyChangesToString(content, changes);
} }
export function removePredefinedConfigs(
content: string,
moduleImport: string,
moduleVariable: string,
configs: string[]
): string {
const source = ts.createSourceFile(
'',
content,
ts.ScriptTarget.Latest,
true,
ts.ScriptKind.JS
);
const changes: StringChange[] = [];
let removeImport = true;
findAllBlocks(source)?.forEach((node) => {
if (
ts.isSpreadElement(node) &&
ts.isElementAccessExpression(node.expression) &&
ts.isPropertyAccessExpression(node.expression.expression) &&
ts.isIdentifier(node.expression.expression.expression) &&
node.expression.expression.expression.getText() === moduleVariable &&
ts.isStringLiteral(node.expression.argumentExpression)
) {
const config = node.expression.argumentExpression.getText();
// Check the text without quotes
if (configs.includes(config.substring(1, config.length - 1))) {
changes.push({
type: ChangeType.Delete,
start: node.pos,
length: node.end - node.pos + 1, // trailing comma
});
} else {
// If there is still a config used, do not remove import
removeImport = false;
}
}
});
let updated = applyChangesToString(content, changes);
if (removeImport) {
updated = removeImportFromFlatConfig(updated, moduleVariable, moduleImport);
}
return updated;
}
/** /**
* Add plugins block to the top of the export blocks * Add plugins block to the top of the export blocks
*/ */
@ -596,7 +697,7 @@ export function addPluginsToExportsBlock(
/** /**
* Adds compat if missing to flat config * Adds compat if missing to flat config
*/ */
export function addCompatToFlatConfig(content: string) { export function addFlatCompatToFlatConfig(content: string) {
let result = content; let result = content;
result = addImportToFlatConfig(result, 'js', '@eslint/js'); result = addImportToFlatConfig(result, 'js', '@eslint/js');
if (result.includes('const compat = new FlatCompat')) { if (result.includes('const compat = new FlatCompat')) {
@ -608,42 +709,27 @@ export function addCompatToFlatConfig(content: string) {
{ {
type: ChangeType.Insert, type: ChangeType.Insert,
index: index - 1, index: index - 1,
text: `${DEFAULT_FLAT_CONFIG}\n`, text: `
const compat = new FlatCompat({
baseDirectory: __dirname,
recommendedConfig: js.configs.recommended,
});
`,
}, },
]); ]);
} }
const DEFAULT_FLAT_CONFIG = `
const compat = new FlatCompat({
baseDirectory: __dirname,
recommendedConfig: js.configs.recommended,
});
`;
/** /**
* Generate node list representing the imports and the exports blocks * Generate node list representing the imports and the exports blocks
* Optionally add flat compat initialization * Optionally add flat compat initialization
*/ */
export function createNodeList( export function createNodeList(
importsMap: Map<string, string>, importsMap: Map<string, string>,
exportElements: ts.Expression[], exportElements: ts.Expression[]
isFlatCompatNeeded: boolean
): ts.NodeArray< ): ts.NodeArray<
ts.VariableStatement | ts.Identifier | ts.ExpressionStatement | ts.SourceFile ts.VariableStatement | ts.Identifier | ts.ExpressionStatement | ts.SourceFile
> { > {
const importsList = []; const importsList = [];
if (isFlatCompatNeeded) {
importsMap.set('@eslint/js', 'js');
importsList.push(
generateRequire(
ts.factory.createObjectBindingPattern([
ts.factory.createBindingElement(undefined, undefined, 'FlatCompat'),
]),
'@eslint/eslintrc'
)
);
}
// generateRequire(varName, imp, ts.factory); // generateRequire(varName, imp, ts.factory);
Array.from(importsMap.entries()).forEach(([imp, varName]) => { Array.from(importsMap.entries()).forEach(([imp, varName]) => {
@ -655,7 +741,7 @@ export function createNodeList(
...importsList, ...importsList,
ts.createSourceFile( ts.createSourceFile(
'', '',
isFlatCompatNeeded ? DEFAULT_FLAT_CONFIG : '', '',
ts.ScriptTarget.Latest, ts.ScriptTarget.Latest,
false, false,
ts.ScriptKind.JS ts.ScriptKind.JS
@ -694,6 +780,27 @@ export function generatePluginExtendsElement(
); );
} }
export function generatePluginExtendsElementWithCompatFixup(
plugin: string
): ts.SpreadElement {
return ts.factory.createSpreadElement(
ts.factory.createCallExpression(
ts.factory.createIdentifier('fixupConfigRules'),
undefined,
[
ts.factory.createCallExpression(
ts.factory.createPropertyAccessExpression(
ts.factory.createIdentifier('compat'),
ts.factory.createIdentifier('extends')
),
undefined,
[ts.factory.createStringLiteral(plugin)]
),
]
)
);
}
/** /**
* Stringifies TS nodes to file content string * Stringifies TS nodes to file content string
*/ */
@ -754,25 +861,132 @@ export function generateRequire(
} }
/** /**
* Generates AST object or spread element based on JSON override object * FROM: https://github.com/eslint/rewrite/blob/e2a7ec809db20e638abbad250d105ddbde88a8d5/packages/migrate-config/src/migrate-config.js#L222
*
* Converts a glob pattern to a format that can be used in a flat config.
* @param {string} pattern The glob pattern to convert.
* @returns {string} The converted glob pattern.
*/
function convertGlobPattern(pattern: string): string {
const isNegated = pattern.startsWith('!');
const patternToTest = isNegated ? pattern.slice(1) : pattern;
// if the pattern is already in the correct format, return it
if (patternToTest === '**' || patternToTest.includes('/')) {
return pattern;
}
return `${isNegated ? '!' : ''}**/${patternToTest}`;
}
// FROM: https://github.com/eslint/rewrite/blob/e2a7ec809db20e638abbad250d105ddbde88a8d5/packages/migrate-config/src/migrate-config.js#L38
const keysToCopy = ['settings', 'rules', 'processor'];
export function overrideNeedsCompat(
override: Partial<Linter.ConfigOverride<Linter.RulesRecord>>
) {
return override.env || override.extends || override.plugins;
}
/**
* Generates an AST object or spread element representing a modern flat config entry,
* based on a given legacy eslintrc JSON override object
*/ */
export function generateFlatOverride( export function generateFlatOverride(
override: Linter.ConfigOverride<Linter.RulesRecord> _override: Partial<Linter.ConfigOverride<Linter.RulesRecord>>
): ts.ObjectLiteralExpression | ts.SpreadElement { ): ts.ObjectLiteralExpression | ts.SpreadElement {
mapFilePaths(override); const override = mapFilePaths(_override);
if (
!override.env && // We do not need the compat tooling for this override
!override.extends && if (!overrideNeedsCompat(override)) {
!override.plugins && // Ensure files is an array
!override.parser let files = override.files;
) { if (typeof files === 'string') {
if (override.parserOptions) { files = [files];
const { parserOptions, ...rest } = override;
return generateAst({ ...rest, languageOptions: { parserOptions } });
} }
return generateAst(override);
const flatConfigOverride: Linter.FlatConfig = {
files,
};
if (override.rules) {
flatConfigOverride.rules = override.rules;
}
// Copy over everything that stays the same
keysToCopy.forEach((key) => {
if (override[key]) {
flatConfigOverride[key] = override[key];
}
});
if (override.parser || override.parserOptions) {
const languageOptions = {};
if (override.parser) {
languageOptions['parser'] = override.parser;
}
if (override.parserOptions) {
languageOptions['parserOptions'] = override.parserOptions;
}
if (Object.keys(languageOptions).length) {
flatConfigOverride.languageOptions = languageOptions;
}
}
if (override['languageOptions']) {
flatConfigOverride.languageOptions = override['languageOptions'];
}
if (override.excludedFiles) {
flatConfigOverride.ignores = (
Array.isArray(override.excludedFiles)
? override.excludedFiles
: [override.excludedFiles]
).map((p) => convertGlobPattern(p));
}
return generateAst(flatConfigOverride, {
keyToMatch: /^(parser|rules)$/,
replacer: (propertyAssignment, propertyName) => {
if (propertyName === 'rules') {
// Add comment that user can override rules if there are no overrides.
if (
ts.isObjectLiteralExpression(propertyAssignment.initializer) &&
propertyAssignment.initializer.properties.length === 0
) {
return ts.addSyntheticLeadingComment(
ts.factory.createPropertyAssignment(
propertyAssignment.name,
ts.factory.createObjectLiteralExpression([])
),
ts.SyntaxKind.SingleLineCommentTrivia,
' Override or add rules here'
);
}
return propertyAssignment;
} else {
// Change parser to require statement.
return ts.factory.createPropertyAssignment(
'parser',
ts.factory.createCallExpression(
ts.factory.createIdentifier('require'),
undefined,
[
ts.factory.createStringLiteral(
override['languageOptions']?.['parserOptions']?.parser ??
override['languageOptions']?.parser ??
override.parser
),
]
)
);
}
},
});
} }
const { files, excludedFiles, rules, parserOptions, ...rest } = override;
// At this point we are applying the flat config compat tooling to the override
const { excludedFiles, parser, parserOptions, rules, files, ...rest } =
override;
const objectLiteralElements: ts.ObjectLiteralElementLike[] = [ const objectLiteralElements: ts.ObjectLiteralElementLike[] = [
ts.factory.createSpreadAssignment(ts.factory.createIdentifier('config')), ts.factory.createSpreadAssignment(ts.factory.createIdentifier('config')),
@ -844,9 +1058,28 @@ export function generateFlatOverride(
); );
} }
export function generateFlatPredefinedConfig(
predefinedConfigName: string,
moduleName = 'nx',
spread = true
): ts.ObjectLiteralExpression | ts.SpreadElement | ts.ElementAccessExpression {
const node = ts.factory.createElementAccessExpression(
ts.factory.createPropertyAccessExpression(
ts.factory.createIdentifier(moduleName),
ts.factory.createIdentifier('configs')
),
ts.factory.createStringLiteral(predefinedConfigName)
);
return spread ? ts.factory.createSpreadElement(node) : node;
}
export function mapFilePaths( export function mapFilePaths(
override: Linter.ConfigOverride<Linter.RulesRecord> _override: Partial<Linter.ConfigOverride<Linter.RulesRecord>>
) { ) {
const override: Partial<Linter.ConfigOverride<Linter.RulesRecord>> = {
..._override,
};
if (override.files) { if (override.files) {
override.files = Array.isArray(override.files) override.files = Array.isArray(override.files)
? override.files ? override.files
@ -861,6 +1094,7 @@ export function mapFilePaths(
mapFilePath(file) mapFilePath(file)
); );
} }
return override;
} }
function addTSObjectProperty( function addTSObjectProperty(
@ -876,10 +1110,21 @@ function addTSObjectProperty(
/** /**
* Generates an AST from a JSON-type input * Generates an AST from a JSON-type input
*/ */
export function generateAst<T>(input: unknown): T { export function generateAst<T>(
input: unknown,
propertyAssignmentReplacer?: {
keyToMatch: RegExp | string;
replacer: (
propertyAssignment: ts.PropertyAssignment,
propertyName: string
) => ts.PropertyAssignment;
}
): T {
if (Array.isArray(input)) { if (Array.isArray(input)) {
return ts.factory.createArrayLiteralExpression( return ts.factory.createArrayLiteralExpression(
input.map((item) => generateAst<ts.Expression>(item)), input.map((item) =>
generateAst<ts.Expression>(item, propertyAssignmentReplacer)
),
input.length > 1 // multiline only if more than one item input.length > 1 // multiline only if more than one item
) as T; ) as T;
} }
@ -888,14 +1133,10 @@ export function generateAst<T>(input: unknown): T {
} }
if (typeof input === 'object') { if (typeof input === 'object') {
return ts.factory.createObjectLiteralExpression( return ts.factory.createObjectLiteralExpression(
Object.entries(input) generatePropertyAssignmentsFromObjectEntries(
.filter(([_, value]) => value !== undefined) input,
.map(([key, value]) => propertyAssignmentReplacer
ts.factory.createPropertyAssignment( ),
isValidKey(key) ? key : ts.factory.createStringLiteral(key),
generateAst<ts.Expression>(value)
)
),
Object.keys(input).length > 1 // multiline only if more than one property Object.keys(input).length > 1 // multiline only if more than one property
) as T; ) as T;
} }
@ -912,6 +1153,35 @@ export function generateAst<T>(input: unknown): T {
throw new Error(`Unknown type: ${typeof input} `); throw new Error(`Unknown type: ${typeof input} `);
} }
function generatePropertyAssignmentsFromObjectEntries(
input: object,
propertyAssignmentReplacer?: {
keyToMatch: RegExp | string;
replacer: (
propertyAssignment: ts.PropertyAssignment,
propertyName: string
) => ts.PropertyAssignment;
}
): ts.PropertyAssignment[] {
return Object.entries(input)
.filter(([_, value]) => value !== undefined)
.map(([key, value]) => {
const original = ts.factory.createPropertyAssignment(
isValidKey(key) ? key : ts.factory.createStringLiteral(key),
generateAst<ts.Expression>(value, propertyAssignmentReplacer)
);
if (
propertyAssignmentReplacer &&
(typeof propertyAssignmentReplacer.keyToMatch === 'string'
? key === propertyAssignmentReplacer.keyToMatch
: propertyAssignmentReplacer.keyToMatch.test(key))
) {
return propertyAssignmentReplacer.replacer(original, key);
}
return original;
});
}
function isValidKey(key: string): boolean { function isValidKey(key: string): boolean {
return /^[a-zA-Z0-9_]+$/.test(key); return /^[a-zA-Z0-9_]+$/.test(key);
} }

View File

@ -78,8 +78,7 @@ exports[`@nx/eslint:workspace-rules-project should generate the required files 4
`; `;
exports[`@nx/eslint:workspace-rules-project should generate the required files 5`] = ` exports[`@nx/eslint:workspace-rules-project should generate the required files 5`] = `
"/* eslint-disable */ "export default {
export default {
displayName: 'eslint-rules', displayName: 'eslint-rules',
preset: '../../jest.preset.js', preset: '../../jest.preset.js',
transform: { transform: {

View File

@ -96,7 +96,9 @@ const internalCreateNodes = async (
).sort((a, b) => (a !== b && isSubDir(a, b) ? -1 : 1)); ).sort((a, b) => (a !== b && isSubDir(a, b) ? -1 : 1));
const excludePatterns = dedupedProjectRoots.map((root) => `${root}/**/*`); const excludePatterns = dedupedProjectRoots.map((root) => `${root}/**/*`);
const ESLint = await resolveESLintClass(isFlatConfig(configFilePath)); const ESLint = await resolveESLintClass({
useFlatConfigOverrideVal: isFlatConfig(configFilePath),
});
const eslintVersion = ESLint.version; const eslintVersion = ESLint.version;
const projects: CreateNodesResult['projects'] = {}; const projects: CreateNodesResult['projects'] = {};
@ -180,7 +182,9 @@ const internalCreateNodesV2 = async (
): Promise<CreateNodesResult> => { ): Promise<CreateNodesResult> => {
const configDir = dirname(configFilePath); const configDir = dirname(configFilePath);
const ESLint = await resolveESLintClass(isFlatConfig(configFilePath)); const ESLint = await resolveESLintClass({
useFlatConfigOverrideVal: isFlatConfig(configFilePath),
});
const eslintVersion = ESLint.version; const eslintVersion = ESLint.version;
const projects: CreateNodesResult['projects'] = {}; const projects: CreateNodesResult['projects'] = {};

View File

@ -1,4 +1,5 @@
import { Tree } from '@nx/devkit'; import { Tree } from '@nx/devkit';
import { gte } from 'semver';
// todo: add support for eslint.config.mjs, // todo: add support for eslint.config.mjs,
export const eslintFlatConfigFilenames = [ export const eslintFlatConfigFilenames = [
@ -6,19 +7,42 @@ export const eslintFlatConfigFilenames = [
'eslint.config.cjs', 'eslint.config.cjs',
]; ];
export function flatConfigEslintFilename(tree: Tree): string { export function getRootESLintFlatConfigFilename(tree: Tree): string {
for (const file of eslintFlatConfigFilenames) { for (const file of eslintFlatConfigFilenames) {
if (tree.exists(file)) { if (tree.exists(file)) {
return file; return file;
} }
} }
throw new Error('Could not find flat config file'); throw new Error('Could not find root flat config file');
} }
export function useFlatConfig(tree: Tree): boolean { export function useFlatConfig(tree?: Tree): boolean {
try { // Prioritize taking ESLint's own environment variable into account when determining if we should use flat config
return !!flatConfigEslintFilename(tree); // If it is not defined, then default to true.
} catch { if (process.env.ESLINT_USE_FLAT_CONFIG === 'true') {
return true;
} else if (process.env.ESLINT_USE_FLAT_CONFIG === 'false') {
return false; return false;
} }
// If we find an existing flat config file in the root of the provided tree, we should use flat config
if (tree) {
const hasRootFlatConfig = eslintFlatConfigFilenames.some((filename) =>
tree.exists(filename)
);
if (hasRootFlatConfig) {
return true;
}
}
// Otherwise fallback to checking the installed eslint version
try {
const { ESLint } = require('eslint');
// Default to any v8 version to compare against in this case as it implies a much older version of ESLint was found (and gte() requires a valid version)
const eslintVersion = ESLint.version || '8.0.0';
return gte(eslintVersion, '9.0.0');
} catch {
// Default to assuming flat config in case ESLint is not yet installed
return true;
}
} }

View File

@ -1,22 +1,26 @@
import type { ESLint } from 'eslint'; import type { ESLint } from 'eslint';
import { useFlatConfig } from '../utils/flat-config';
export async function resolveESLintClass( export async function resolveESLintClass(opts?: {
useFlatConfig = false useFlatConfigOverrideVal: boolean;
): Promise<typeof ESLint> { }): Promise<typeof ESLint> {
try { try {
// In eslint 8.57.0 (the final v8 version), a dedicated API was added for resolving the correct ESLint class. // Explicitly use the FlatESLint and LegacyESLint classes here because the ESLint class points at a different one based on ESLint v8 vs ESLint v9
const eslint = await import('eslint'); // But the decision on which one to use is not just based on the major version of ESLint.
if (typeof (eslint as any).loadESLint === 'function') { // @ts-expect-error The may be wrong based on our installed eslint version
return await (eslint as any).loadESLint({ useFlatConfig }); const { LegacyESLint, FlatESLint } = await import(
} 'eslint/use-at-your-own-risk'
// If that API is not available (an older version of v8), we need to use the old way of resolving the ESLint class. );
if (!useFlatConfig) {
return eslint.ESLint; const shouldESLintUseFlatConfig =
} typeof opts?.useFlatConfigOverrideVal === 'boolean'
// eslint-disable-next-line @typescript-eslint/no-var-requires ? opts.useFlatConfigOverrideVal
const { FlatESLint } = require('eslint/use-at-your-own-risk'); : useFlatConfig();
return FlatESLint;
return shouldESLintUseFlatConfig ? FlatESLint : LegacyESLint;
} catch { } catch {
throw new Error('Unable to find ESLint. Ensure ESLint is installed.'); throw new Error(
'Unable to find `eslint`. Ensure a valid `eslint` version is installed.'
);
} }
} }

View File

@ -0,0 +1,32 @@
import { readJson, readJsonFile, type Tree } from '@nx/devkit';
import { checkAndCleanWithSemver } from '@nx/devkit/src/utils/semver';
import { readModulePackageJson } from 'nx/src/devkit-internals';
export function getInstalledEslintVersion(tree?: Tree): string | null {
try {
const eslintPackageJson = readModulePackageJson('eslint').packageJson;
return eslintPackageJson.version;
} catch {}
// eslint is not installed on disk, it could be in the package.json
// but waiting to be installed
const rootPackageJson = tree
? readJson(tree, 'package.json')
: readJsonFile('package.json');
const eslintVersionInRootPackageJson =
rootPackageJson.devDependencies?.['eslint'] ??
rootPackageJson.dependencies?.['eslint'];
if (!eslintVersionInRootPackageJson) {
// eslint is not installed
return null;
}
try {
// try to parse and return the version
return checkAndCleanWithSemver('eslint', eslintVersionInRootPackageJson);
} catch {}
// we could not resolve the version
return null;
}

View File

@ -4,3 +4,8 @@ export const eslintVersion = '~8.57.0';
export const eslintrcVersion = '^2.1.1'; export const eslintrcVersion = '^2.1.1';
export const eslintConfigPrettierVersion = '^9.0.0'; export const eslintConfigPrettierVersion = '^9.0.0';
export const typescriptESLintVersion = '^7.16.0'; export const typescriptESLintVersion = '^7.16.0';
// Updated linting stack for ESLint v9, typescript-eslint v8
export const eslint9__typescriptESLintVersion = '^8.0.0';
export const eslint9__eslintVersion = '^9.8.0';
export const eslintCompat = '^1.1.1';

View File

@ -35,7 +35,15 @@ export async function installAndUpdatePackageJson(
context: ExecutorContext, context: ExecutorContext,
options: ExpoInstallOptions options: ExpoInstallOptions
) { ) {
await installAsync(context.root, options); const { installAsync } = require('@expo/cli/build/src/install/installAsync');
const packages =
typeof options.packages === 'string'
? options.packages.split(',')
: options.packages ?? [];
// Use force in case there are any unmet peer dependencies.
await installAsync(packages, createInstallOptions(options), ['--force']);
const projectRoot = const projectRoot =
context.projectsConfigurations.projects[context.projectName].root; context.projectsConfigurations.projects[context.projectName].root;
@ -48,10 +56,6 @@ export async function installAndUpdatePackageJson(
const workspacePackageJson = readJsonFile(workspacePackageJsonPath); const workspacePackageJson = readJsonFile(workspacePackageJsonPath);
const projectPackageJson = readJsonFile(projectPackageJsonPath); const projectPackageJson = readJsonFile(projectPackageJsonPath);
const packages =
typeof options.packages === 'string'
? options.packages.split(',')
: options.packages;
displayNewlyAddedDepsMessage( displayNewlyAddedDepsMessage(
context.projectName, context.projectName,
await syncDeps( await syncDeps(
@ -65,51 +69,19 @@ export async function installAndUpdatePackageJson(
); );
} }
export function installAsync(
workspaceRoot: string,
options: ExpoInstallOptions
): Promise<number> {
return new Promise((resolve, reject) => {
childProcess = fork(
require.resolve('@expo/cli/build/bin/cli'),
['install', ...createInstallOptions(options)],
{ cwd: workspaceRoot, env: process.env }
);
// Ensure the child process is killed when the parent exits
process.on('exit', () => childProcess.kill());
process.on('SIGTERM', () => childProcess.kill());
childProcess.on('error', (err) => {
reject(err);
});
childProcess.on('exit', (code) => {
if (code === 0) {
resolve(code);
} else {
reject(code);
}
});
});
}
// options from https://github.com/expo/expo/blob/main/packages/%40expo/cli/src/install/index.ts // options from https://github.com/expo/expo/blob/main/packages/%40expo/cli/src/install/index.ts
function createInstallOptions(options: ExpoInstallOptions) { function createInstallOptions(options: ExpoInstallOptions) {
return Object.keys(options).reduce((acc, k) => { return Object.keys(options).reduce((acc, k) => {
const v = options[k]; const v = options[k];
if (k === 'packages') { if (typeof v === 'boolean') {
const packages = typeof v === 'string' ? v.split(',') : v; if (v === true) {
acc.push(...packages); // when true, does not need to pass the value true, just need to pass the flag in kebob case
} else { acc.push(`--${names(k).fileName}`);
if (typeof v === 'boolean') {
if (v === true) {
// when true, does not need to pass the value true, just need to pass the flag in kebob case
acc.push(`--${names(k).fileName}`);
}
} else {
acc.push(`--${names(k).fileName}`, v);
} }
} else {
acc.push(`--${names(k).fileName}`, v);
} }
return acc; return acc;
}, []); }, []);
} }

View File

@ -3,7 +3,6 @@ import { ChildProcess, fork } from 'child_process';
import { join } from 'path'; import { join } from 'path';
import { podInstall } from '../../utils/pod-install-task'; import { podInstall } from '../../utils/pod-install-task';
import { installAsync } from '../install/install.impl';
import { ExpoPrebuildOptions } from './schema'; import { ExpoPrebuildOptions } from './schema';
export interface ExpoPrebuildOutput { export interface ExpoPrebuildOutput {
@ -23,7 +22,10 @@ export default async function* prebuildExecutor(
await prebuildAsync(context.root, projectRoot, options); await prebuildAsync(context.root, projectRoot, options);
if (options.install) { if (options.install) {
await installAsync(workspaceRoot, {}); const {
installAsync,
} = require('@expo/cli/build/src/install/installAsync');
await installAsync([], {});
if (options.platform === 'ios') { if (options.platform === 'ios') {
podInstall(join(context.root, projectRoot, 'ios')); podInstall(join(context.root, projectRoot, 'ios'));
} }

View File

@ -7,7 +7,6 @@ import { existsSync } from 'fs-extra';
import { ExpoRunOptions } from './schema'; import { ExpoRunOptions } from './schema';
import { prebuildAsync } from '../prebuild/prebuild.impl'; import { prebuildAsync } from '../prebuild/prebuild.impl';
import { podInstall } from '../../utils/pod-install-task'; import { podInstall } from '../../utils/pod-install-task';
import { installAsync } from '../install/install.impl';
export interface ExpoRunOutput { export interface ExpoRunOutput {
success: boolean; success: boolean;
@ -34,7 +33,10 @@ export default async function* runExecutor(
} }
if (options.install) { if (options.install) {
await installAsync(context.root, {}); const {
installAsync,
} = require('@expo/cli/build/src/install/installAsync');
await installAsync([], {});
if (options.platform === 'ios') { if (options.platform === 'ios') {
podInstall(join(context.root, projectRoot, 'ios')); podInstall(join(context.root, projectRoot, 'ios'));
} }

View File

@ -9,8 +9,11 @@ import { extraEslintDependencies } from '@nx/react/src/utils/lint';
import { import {
addExtendsToLintConfig, addExtendsToLintConfig,
addIgnoresToLintConfig, addIgnoresToLintConfig,
addOverrideToLintConfig,
addPredefinedConfigToFlatLintConfig,
isEslintConfigSupported, isEslintConfigSupported,
} from '@nx/eslint/src/generators/utils/eslint-file'; } from '@nx/eslint/src/generators/utils/eslint-file';
import { useFlatConfig } from '@nx/eslint/src/utils/flat-config';
interface NormalizedSchema { interface NormalizedSchema {
linter?: Linter | LinterType; linter?: Linter | LinterType;
@ -40,7 +43,24 @@ export async function addLinting(host: Tree, options: NormalizedSchema) {
tasks.push(lintTask); tasks.push(lintTask);
if (isEslintConfigSupported(host)) { if (isEslintConfigSupported(host)) {
addExtendsToLintConfig(host, options.projectRoot, 'plugin:@nx/react'); if (useFlatConfig(host)) {
addPredefinedConfigToFlatLintConfig(
host,
options.projectRoot,
'flat/react'
);
// Add an empty rules object to users know how to add/override rules
addOverrideToLintConfig(host, options.projectRoot, {
files: ['*.ts', '*.tsx', '*.js', '*.jsx'],
rules: {},
});
} else {
const addExtendsTask = addExtendsToLintConfig(host, options.projectRoot, {
name: 'plugin:@nx/react',
needCompatFixup: true,
});
tasks.push(addExtendsTask);
}
addIgnoresToLintConfig(host, options.projectRoot, [ addIgnoresToLintConfig(host, options.projectRoot, [
'.expo', '.expo',
'web-build', 'web-build',

View File

@ -1,8 +1,7 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP // Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`jestProject --babelJest should generate proper jest.transform when --compiler=swc and supportTsx is true 1`] = ` exports[`jestProject --babelJest should generate proper jest.transform when --compiler=swc and supportTsx is true 1`] = `
"/* eslint-disable */ "export default {
export default {
displayName: 'lib1', displayName: 'lib1',
preset: '../../jest.preset.js', preset: '../../jest.preset.js',
transform: { transform: {
@ -23,8 +22,7 @@ export default {
`; `;
exports[`jestProject --babelJest should generate proper jest.transform when babelJest and supportTsx is true 1`] = ` exports[`jestProject --babelJest should generate proper jest.transform when babelJest and supportTsx is true 1`] = `
"/* eslint-disable */ "export default {
export default {
displayName: 'lib1', displayName: 'lib1',
preset: '../../jest.preset.js', preset: '../../jest.preset.js',
transform: { transform: {
@ -37,8 +35,7 @@ export default {
`; `;
exports[`jestProject --babelJest should generate proper jest.transform when babelJest is true 1`] = ` exports[`jestProject --babelJest should generate proper jest.transform when babelJest is true 1`] = `
"/* eslint-disable */ "export default {
export default {
displayName: 'lib1', displayName: 'lib1',
preset: '../../jest.preset.js', preset: '../../jest.preset.js',
transform: { transform: {
@ -51,8 +48,7 @@ export default {
`; `;
exports[`jestProject --setup-file should have setupFilesAfterEnv and globals.ts-jest in the jest.config when generated for angular 1`] = ` exports[`jestProject --setup-file should have setupFilesAfterEnv and globals.ts-jest in the jest.config when generated for angular 1`] = `
"/* eslint-disable */ "export default {
export default {
displayName: 'lib1', displayName: 'lib1',
preset: '../../jest.preset.js', preset: '../../jest.preset.js',
setupFilesAfterEnv: ['<rootDir>/src/test-setup.ts'], setupFilesAfterEnv: ['<rootDir>/src/test-setup.ts'],
@ -77,8 +73,7 @@ export default {
`; `;
exports[`jestProject should create a jest.config.ts 1`] = ` exports[`jestProject should create a jest.config.ts 1`] = `
"/* eslint-disable */ "export default {
export default {
displayName: 'lib1', displayName: 'lib1',
preset: '../../jest.preset.js', preset: '../../jest.preset.js',
coverageDirectory: '../../coverage/libs/lib1', coverageDirectory: '../../coverage/libs/lib1',
@ -87,8 +82,7 @@ export default {
`; `;
exports[`jestProject should generate files 2`] = ` exports[`jestProject should generate files 2`] = `
"/* eslint-disable */ "export default {
export default {
displayName: 'lib1', displayName: 'lib1',
preset: '../../jest.preset.js', preset: '../../jest.preset.js',
setupFilesAfterEnv: ['<rootDir>/src/test-setup.ts'], setupFilesAfterEnv: ['<rootDir>/src/test-setup.ts'],

View File

@ -357,8 +357,7 @@ describe('jestProject', () => {
project: 'my-project', project: 'my-project',
}); });
expect(tree.read('jest.config.ts', 'utf-8')).toMatchInlineSnapshot(` expect(tree.read('jest.config.ts', 'utf-8')).toMatchInlineSnapshot(`
"/* eslint-disable */ "export default {
export default {
displayName: 'my-project', displayName: 'my-project',
preset: './jest.preset.js', preset: './jest.preset.js',
coverageDirectory: './coverage/my-project', coverageDirectory: './coverage/my-project',
@ -389,8 +388,7 @@ describe('jestProject', () => {
js: true, js: true,
}); });
expect(tree.read('jest.config.js', 'utf-8')).toMatchInlineSnapshot(` expect(tree.read('jest.config.js', 'utf-8')).toMatchInlineSnapshot(`
"/* eslint-disable */ "module.exports = {
module.exports = {
displayName: 'my-project', displayName: 'my-project',
preset: './jest.preset.js', preset: './jest.preset.js',
coverageDirectory: './coverage/my-project', coverageDirectory: './coverage/my-project',
@ -424,8 +422,7 @@ describe('jestProject', () => {
// ASSERT // ASSERT
expect(tree.read('libs/lib1/jest.config.ts', 'utf-8')) expect(tree.read('libs/lib1/jest.config.ts', 'utf-8'))
.toMatchInlineSnapshot(` .toMatchInlineSnapshot(`
"/* eslint-disable */ "export default {
export default {
displayName: 'lib1', displayName: 'lib1',
preset: '../../jest.preset.cjs', preset: '../../jest.preset.cjs',
coverageDirectory: '../../coverage/libs/lib1', coverageDirectory: '../../coverage/libs/lib1',
@ -451,8 +448,7 @@ describe('jestProject', () => {
expect(tree.exists('jest.preset.cjs')).toBeTruthy(); expect(tree.exists('jest.preset.cjs')).toBeTruthy();
expect(tree.read('libs/lib1/jest.config.ts', 'utf-8')) expect(tree.read('libs/lib1/jest.config.ts', 'utf-8'))
.toMatchInlineSnapshot(` .toMatchInlineSnapshot(`
"/* eslint-disable */ "export default {
export default {
displayName: 'lib1', displayName: 'lib1',
preset: '../../jest.preset.cjs', preset: '../../jest.preset.cjs',
coverageDirectory: '../../coverage/libs/lib1', coverageDirectory: '../../coverage/libs/lib1',

View File

@ -1,4 +1,3 @@
/* eslint-disable */
<% if(js){ %>module.exports =<% } else{ %>export default<% } %> { <% if(js){ %>module.exports =<% } else{ %>export default<% } %> {
displayName: '<%= project %>', displayName: '<%= project %>',
preset: '<%= offsetFromRoot %>jest.preset.<%= presetExt %>', preset: '<%= offsetFromRoot %>jest.preset.<%= presetExt %>',

View File

@ -1,4 +1,3 @@
/* eslint-disable */
<% if(js){ %>module.exports =<% } else{ %>export default<% } %> { <% if(js){ %>module.exports =<% } else{ %>export default<% } %> {
displayName: '<%= project %>', displayName: '<%= project %>',
preset: '<%= offsetFromRoot %>jest.preset.<%= presetExt %>',<% if(setupFile !== 'none') { %> preset: '<%= offsetFromRoot %>jest.preset.<%= presetExt %>',<% if(setupFile !== 'none') { %>

View File

@ -537,7 +537,14 @@ describe('lib', () => {
], ],
"parser": "jsonc-eslint-parser", "parser": "jsonc-eslint-parser",
"rules": { "rules": {
"@nx/dependency-checks": "error", "@nx/dependency-checks": [
"error",
{
"ignoredFiles": [
"{projectRoot}/eslint.config.{js,cjs,mjs}",
],
},
],
}, },
}, },
], ],
@ -594,7 +601,14 @@ describe('lib', () => {
], ],
"parser": "jsonc-eslint-parser", "parser": "jsonc-eslint-parser",
"rules": { "rules": {
"@nx/dependency-checks": "error", "@nx/dependency-checks": [
"error",
{
"ignoredFiles": [
"{projectRoot}/eslint.config.{js,cjs,mjs}",
],
},
],
}, },
}, },
], ],
@ -719,7 +733,14 @@ describe('lib', () => {
], ],
"parser": "jsonc-eslint-parser", "parser": "jsonc-eslint-parser",
"rules": { "rules": {
"@nx/dependency-checks": "error", "@nx/dependency-checks": [
"error",
{
"ignoredFiles": [
"{projectRoot}/eslint.config.{js,cjs,mjs}",
],
},
],
}, },
}, },
], ],
@ -745,8 +766,7 @@ describe('lib', () => {
expect(tree.exists(`my-lib/jest.config.ts`)).toBeTruthy(); expect(tree.exists(`my-lib/jest.config.ts`)).toBeTruthy();
expect(tree.read(`my-lib/jest.config.ts`, 'utf-8')) expect(tree.read(`my-lib/jest.config.ts`, 'utf-8'))
.toMatchInlineSnapshot(` .toMatchInlineSnapshot(`
"/* eslint-disable */ "export default {
export default {
displayName: 'my-lib', displayName: 'my-lib',
preset: '../jest.preset.js', preset: '../jest.preset.js',
transform: { transform: {
@ -1483,7 +1503,10 @@ describe('lib', () => {
'@nx/dependency-checks': [ '@nx/dependency-checks': [
'error', 'error',
{ {
ignoredFiles: ['{projectRoot}/esbuild.config.{js,ts,mjs,mts}'], ignoredFiles: [
'{projectRoot}/eslint.config.{js,cjs,mjs}',
'{projectRoot}/esbuild.config.{js,ts,mjs,mts}',
],
}, },
], ],
}, },
@ -1508,7 +1531,10 @@ describe('lib', () => {
'@nx/dependency-checks': [ '@nx/dependency-checks': [
'error', 'error',
{ {
ignoredFiles: ['{projectRoot}/rollup.config.{js,ts,mjs,mts}'], ignoredFiles: [
'{projectRoot}/eslint.config.{js,cjs,mjs}',
'{projectRoot}/rollup.config.{js,ts,mjs,mts}',
],
}, },
], ],
}, },

View File

@ -347,7 +347,13 @@ export async function addLint(
files: ['*.json'], files: ['*.json'],
parser: 'jsonc-eslint-parser', parser: 'jsonc-eslint-parser',
rules: { rules: {
'@nx/dependency-checks': 'error', '@nx/dependency-checks': [
'error',
{
// With flat configs, we don't want to include imports in the eslint js/cjs/mjs files to be checked
ignoredFiles: ['{projectRoot}/eslint.config.{js,cjs,mjs}'],
},
],
}, },
}); });
} }
@ -382,19 +388,22 @@ export async function addLint(
ruleOptions = {}; ruleOptions = {};
} }
if (options.bundler === 'vite' || options.unitTestRunner === 'vitest') { if (options.bundler === 'vite' || options.unitTestRunner === 'vitest') {
ruleOptions.ignoredFiles = [ ruleOptions.ignoredFiles ??= [];
'{projectRoot}/vite.config.{js,ts,mjs,mts}', ruleOptions.ignoredFiles.push(
]; '{projectRoot}/vite.config.{js,ts,mjs,mts}'
);
o.rules['@nx/dependency-checks'] = [ruleSeverity, ruleOptions]; o.rules['@nx/dependency-checks'] = [ruleSeverity, ruleOptions];
} else if (options.bundler === 'rollup') { } else if (options.bundler === 'rollup') {
ruleOptions.ignoredFiles = [ ruleOptions.ignoredFiles ??= [];
'{projectRoot}/rollup.config.{js,ts,mjs,mts}', ruleOptions.ignoredFiles.push(
]; '{projectRoot}/rollup.config.{js,ts,mjs,mts}'
);
o.rules['@nx/dependency-checks'] = [ruleSeverity, ruleOptions]; o.rules['@nx/dependency-checks'] = [ruleSeverity, ruleOptions];
} else if (options.bundler === 'esbuild') { } else if (options.bundler === 'esbuild') {
ruleOptions.ignoredFiles = [ ruleOptions.ignoredFiles ??= [];
'{projectRoot}/esbuild.config.{js,ts,mjs,mts}', ruleOptions.ignoredFiles.push(
]; '{projectRoot}/esbuild.config.{js,ts,mjs,mts}'
);
o.rules['@nx/dependency-checks'] = [ruleSeverity, ruleOptions]; o.rules['@nx/dependency-checks'] = [ruleSeverity, ruleOptions];
} }
return o; return o;

View File

@ -1,8 +1,7 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP // Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`lib --testEnvironment should set target jest testEnvironment to jsdom 1`] = ` exports[`lib --testEnvironment should set target jest testEnvironment to jsdom 1`] = `
"/* eslint-disable */ "export default {
export default {
displayName: 'my-lib', displayName: 'my-lib',
preset: '../jest.preset.js', preset: '../jest.preset.js',
transform: { transform: {
@ -15,8 +14,7 @@ export default {
`; `;
exports[`lib --testEnvironment should set target jest testEnvironment to node by default 1`] = ` exports[`lib --testEnvironment should set target jest testEnvironment to node by default 1`] = `
"/* eslint-disable */ "export default {
export default {
displayName: 'my-lib', displayName: 'my-lib',
preset: '../jest.preset.js', preset: '../jest.preset.js',
testEnvironment: 'node', testEnvironment: 'node',

View File

@ -601,6 +601,38 @@ describe('app', () => {
describe('--linter', () => { describe('--linter', () => {
describe('default (eslint)', () => { describe('default (eslint)', () => {
it('should add flat config as needed', async () => {
tree.write('eslint.config.js', '');
const name = uniq();
await applicationGenerator(tree, {
name,
style: 'css',
projectNameAndRootFormat: 'as-provided',
});
expect(tree.read(`${name}/eslint.config.js`, 'utf-8'))
.toMatchInlineSnapshot(`
"const { FlatCompat } = require('@eslint/eslintrc');
const js = require('@eslint/js');
const nx = require('@nx/eslint-plugin');
const baseConfig = require('../eslint.config.js');
const compat = new FlatCompat({
baseDirectory: __dirname,
recommendedConfig: js.configs.recommended,
});
module.exports = [
...compat.extends('next', 'next/core-web-vitals'),
...baseConfig,
...nx.configs['flat/react-typescript'],
{ ignores: ['.next/**/*'] },
];
"
`);
});
it('should add .eslintrc.json and dependencies', async () => { it('should add .eslintrc.json and dependencies', async () => {
const name = uniq(); const name = uniq();
@ -660,17 +692,6 @@ describe('app', () => {
], ],
"rules": {}, "rules": {},
}, },
{
"env": {
"jest": true,
},
"files": [
"*.spec.ts",
"*.spec.tsx",
"*.spec.js",
"*.spec.jsx",
],
},
], ],
} }
`); `);

View File

@ -90,17 +90,6 @@ describe('updateEslint', () => {
], ],
"rules": {}, "rules": {},
}, },
{
"env": {
"jest": true,
},
"files": [
"*.spec.ts",
"*.spec.tsx",
"*.spec.js",
"*.spec.jsx",
],
},
], ],
} }
`); `);
@ -115,57 +104,18 @@ describe('updateEslint', () => {
.toMatchInlineSnapshot(` .toMatchInlineSnapshot(`
"const { FlatCompat } = require("@eslint/eslintrc"); "const { FlatCompat } = require("@eslint/eslintrc");
const js = require("@eslint/js"); const js = require("@eslint/js");
const nx = require("@nx/eslint-plugin");
const baseConfig = require("../eslint.config.js"); const baseConfig = require("../eslint.config.js");
const compat = new FlatCompat({ const compat = new FlatCompat({
baseDirectory: __dirname, baseDirectory: __dirname,
recommendedConfig: js.configs.recommended, recommendedConfig: js.configs.recommended,
}); });
module.exports = [ module.exports = [
...compat.extends("plugin:@nx/react-typescript", "next", "next/core-web-vitals"), ...compat.extends("next", "next/core-web-vitals"),
...baseConfig, ...baseConfig,
{ ...nx.configs["flat/react-typescript"],
"files": [
"**/*.ts",
"**/*.tsx",
"**/*.js",
"**/*.jsx"
],
"rules": {
"@next/next/no-html-link-for-pages": [
"error",
"my-app/pages"
]
}
},
{
files: [
"**/*.ts",
"**/*.tsx"
],
rules: {}
},
{
files: [
"**/*.js",
"**/*.jsx"
],
rules: {}
},
...compat.config({ env: { jest: true } }).map(config => ({
...config,
files: [
"**/*.spec.ts",
"**/*.spec.tsx",
"**/*.spec.js",
"**/*.spec.jsx"
],
rules: {
...config.rules
}
})),
{ ignores: [".next/**/*"] } { ignores: [".next/**/*"] }
]; ];
" "

View File

@ -11,11 +11,12 @@ import { NormalizedSchema } from './normalize-options';
import { import {
addExtendsToLintConfig, addExtendsToLintConfig,
addIgnoresToLintConfig, addIgnoresToLintConfig,
addOverrideToLintConfig, addPredefinedConfigToFlatLintConfig,
isEslintConfigSupported, isEslintConfigSupported,
updateOverrideInLintConfig, updateOverrideInLintConfig,
} from '@nx/eslint/src/generators/utils/eslint-file'; } from '@nx/eslint/src/generators/utils/eslint-file';
import { eslintConfigNextVersion } from '../../../utils/versions'; import { eslintConfigNextVersion } from '../../../utils/versions';
import { useFlatConfig } from '@nx/eslint/src/utils/flat-config';
export async function addLinting( export async function addLinting(
host: Tree, host: Tree,
@ -39,11 +40,34 @@ export async function addLinting(
); );
if (options.linter === Linter.EsLint && isEslintConfigSupported(host)) { if (options.linter === Linter.EsLint && isEslintConfigSupported(host)) {
addExtendsToLintConfig(host, options.appProjectRoot, [ if (useFlatConfig(host)) {
'plugin:@nx/react-typescript', addPredefinedConfigToFlatLintConfig(
'next', host,
'next/core-web-vitals', options.appProjectRoot,
]); 'flat/react-typescript'
);
// Since Next.js does not support flat configs yet, we need to use compat fixup.
const addExtendsTask = addExtendsToLintConfig(
host,
options.appProjectRoot,
[
{ name: 'next', needCompatFixup: true },
{ name: 'next/core-web-vitals', needCompatFixup: true },
]
);
tasks.push(addExtendsTask);
} else {
const addExtendsTask = addExtendsToLintConfig(
host,
options.appProjectRoot,
[
'plugin:@nx/react-typescript',
{ name: 'next', needCompatFixup: true },
{ name: 'next/core-web-vitals', needCompatFixup: true },
]
);
tasks.push(addExtendsTask);
}
updateOverrideInLintConfig( updateOverrideInLintConfig(
host, host,
@ -65,15 +89,6 @@ export async function addLinting(
}, },
}) })
); );
// add jest specific config
if (options.unitTestRunner === 'jest') {
addOverrideToLintConfig(host, options.appProjectRoot, {
files: ['*.spec.ts', '*.spec.tsx', '*.spec.js', '*.spec.jsx'],
env: {
jest: true,
},
});
}
addIgnoresToLintConfig(host, options.appProjectRoot, ['.next/**/*']); addIgnoresToLintConfig(host, options.appProjectRoot, ['.next/**/*']);
} }

View File

@ -433,8 +433,7 @@ describe('app', () => {
expect(tree.read(`my-node-app/jest.config.ts`, 'utf-8')) expect(tree.read(`my-node-app/jest.config.ts`, 'utf-8'))
.toMatchInlineSnapshot(` .toMatchInlineSnapshot(`
"/* eslint-disable */ "export default {
export default {
displayName: 'my-node-app', displayName: 'my-node-app',
preset: '../jest.preset.js', preset: '../jest.preset.js',
testEnvironment: 'node', testEnvironment: 'node',
@ -460,8 +459,7 @@ describe('app', () => {
expect(tree.read(`my-node-app/jest.config.ts`, 'utf-8')) expect(tree.read(`my-node-app/jest.config.ts`, 'utf-8'))
.toMatchInlineSnapshot(` .toMatchInlineSnapshot(`
"/* eslint-disable */ "export default {
export default {
displayName: 'my-node-app', displayName: 'my-node-app',
preset: '../jest.preset.js', preset: '../jest.preset.js',
testEnvironment: 'node', testEnvironment: 'node',

View File

@ -1,4 +1,3 @@
/* eslint-disable */
export default { export default {
displayName: '<%= e2eProjectName %>', displayName: '<%= e2eProjectName %>',
preset: '<%= offsetFromRoot %><%= jestPreset %>', preset: '<%= offsetFromRoot %><%= jestPreset %>',

View File

@ -1,4 +1,3 @@
/* eslint-disable */
export default { export default {
displayName: '<%= e2eProjectName %>', displayName: '<%= e2eProjectName %>',
preset: '<%= offsetFromRoot %><%= jestPreset %>', preset: '<%= offsetFromRoot %><%= jestPreset %>',

View File

@ -1,8 +1,7 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP // Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`lib not nested should update configuration 1`] = ` exports[`lib not nested should update configuration 1`] = `
"/* eslint-disable */ "export default {
export default {
displayName: 'my-lib', displayName: 'my-lib',
preset: '../jest.preset.js', preset: '../jest.preset.js',
testEnvironment: 'node', testEnvironment: 'node',

View File

@ -440,8 +440,7 @@ describe('lib', () => {
expect(tree.read(`my-lib/jest.config.ts`, 'utf-8')) expect(tree.read(`my-lib/jest.config.ts`, 'utf-8'))
.toMatchInlineSnapshot(` .toMatchInlineSnapshot(`
"/* eslint-disable */ "export default {
export default {
displayName: 'my-lib', displayName: 'my-lib',
preset: '../jest.preset.js', preset: '../jest.preset.js',
testEnvironment: 'node', testEnvironment: 'node',

View File

@ -31,6 +31,7 @@
"buildTargets": ["build-base"], "buildTargets": ["build-base"],
"ignoredDependencies": [ "ignoredDependencies": [
"nx", "nx",
"eslint",
"typescript", "typescript",
"@nx/cypress", "@nx/cypress",
"@nx/playwright", "@nx/playwright",

View File

@ -18,22 +18,49 @@ exports[`app generated files content - as-provided - my-app general application
} }
`; `;
exports[`app generated files content - as-provided - my-app general application should configure eslint correctly 1`] = ` exports[`app generated files content - as-provided - my-app general application should configure eslint correctly (eslintrc) 1`] = `
"{ "{
"extends": ["@nuxt/eslint-config", "../.eslintrc.json"], "extends": ["@nuxt/eslint-config", "../.eslintrc.json"],
"ignorePatterns": ["!**/*", ".nuxt/**", ".output/**", "node_modules"], "ignorePatterns": ["!**/*", ".nuxt/**", ".output/**", "node_modules"],
"overrides": [ "overrides": [
{ {
"files": ["*.ts", "*.tsx", "*.js", "*.jsx", "*.vue"], "files": ["*.ts", "*.tsx", "*.js", "*.jsx", "*.vue"],
"rules": { "rules": {}
"vue/multi-word-component-names": "off"
}
} }
] ]
} }
" "
`; `;
exports[`app generated files content - as-provided - my-app general application should configure eslint correctly (flat config) 1`] = `
"const { FlatCompat } = require('@eslint/eslintrc');
const js = require('@eslint/js');
const baseConfig = require('../eslint.config.js');
const compat = new FlatCompat({
baseDirectory: __dirname,
recommendedConfig: js.configs.recommended,
});
module.exports = [
...baseConfig,
{
files: ['**/*.ts', '**/*.tsx', '**/*.js', '**/*.jsx', '**/*.vue'],
// Override or add rules here
rules: {},
},
...compat.extends('@nuxt/eslint-config'),
{
files: ['**/*.vue'],
languageOptions: {
parserOptions: { parser: require('@typescript-eslint/parser') },
},
},
{ ignores: ['.nuxt/**', '.output/**', 'node_modules'] },
];
"
`;
exports[`app generated files content - as-provided - my-app general application should configure nuxt correctly 1`] = ` exports[`app generated files content - as-provided - my-app general application should configure nuxt correctly 1`] = `
"import { nxViteTsPaths } from '@nx/vite/plugins/nx-tsconfig-paths.plugin'; "import { nxViteTsPaths } from '@nx/vite/plugins/nx-tsconfig-paths.plugin';
import { defineNuxtConfig } from 'nuxt/config'; import { defineNuxtConfig } from 'nuxt/config';
@ -358,22 +385,49 @@ exports[`app generated files content - as-provided - myApp general application s
} }
`; `;
exports[`app generated files content - as-provided - myApp general application should configure eslint correctly 1`] = ` exports[`app generated files content - as-provided - myApp general application should configure eslint correctly (eslintrc) 1`] = `
"{ "{
"extends": ["@nuxt/eslint-config", "../.eslintrc.json"], "extends": ["@nuxt/eslint-config", "../.eslintrc.json"],
"ignorePatterns": ["!**/*", ".nuxt/**", ".output/**", "node_modules"], "ignorePatterns": ["!**/*", ".nuxt/**", ".output/**", "node_modules"],
"overrides": [ "overrides": [
{ {
"files": ["*.ts", "*.tsx", "*.js", "*.jsx", "*.vue"], "files": ["*.ts", "*.tsx", "*.js", "*.jsx", "*.vue"],
"rules": { "rules": {}
"vue/multi-word-component-names": "off"
}
} }
] ]
} }
" "
`; `;
exports[`app generated files content - as-provided - myApp general application should configure eslint correctly (flat config) 1`] = `
"const { FlatCompat } = require('@eslint/eslintrc');
const js = require('@eslint/js');
const baseConfig = require('../eslint.config.js');
const compat = new FlatCompat({
baseDirectory: __dirname,
recommendedConfig: js.configs.recommended,
});
module.exports = [
...baseConfig,
{
files: ['**/*.ts', '**/*.tsx', '**/*.js', '**/*.jsx', '**/*.vue'],
// Override or add rules here
rules: {},
},
...compat.extends('@nuxt/eslint-config'),
{
files: ['**/*.vue'],
languageOptions: {
parserOptions: { parser: require('@typescript-eslint/parser') },
},
},
{ ignores: ['.nuxt/**', '.output/**', 'node_modules'] },
];
"
`;
exports[`app generated files content - as-provided - myApp general application should configure nuxt correctly 1`] = ` exports[`app generated files content - as-provided - myApp general application should configure nuxt correctly 1`] = `
"import { nxViteTsPaths } from '@nx/vite/plugins/nx-tsconfig-paths.plugin'; "import { nxViteTsPaths } from '@nx/vite/plugins/nx-tsconfig-paths.plugin';
import { defineNuxtConfig } from 'nuxt/config'; import { defineNuxtConfig } from 'nuxt/config';

View File

@ -13,14 +13,15 @@ describe('app', () => {
describe('general application', () => { describe('general application', () => {
beforeEach(async () => { beforeEach(async () => {
tree = createTreeWithEmptyWorkspace(); tree = createTreeWithEmptyWorkspace();
});
it('should not add targets', async () => {
await applicationGenerator(tree, { await applicationGenerator(tree, {
name, name,
projectNameAndRootFormat: 'as-provided', projectNameAndRootFormat: 'as-provided',
unitTestRunner: 'vitest', unitTestRunner: 'vitest',
}); });
});
it('should not add targets', async () => {
const projectConfig = readProjectConfiguration(tree, name); const projectConfig = readProjectConfiguration(tree, name);
expect(projectConfig.targets.build).toBeUndefined(); expect(projectConfig.targets.build).toBeUndefined();
expect(projectConfig.targets.serve).toBeUndefined(); expect(projectConfig.targets.serve).toBeUndefined();
@ -30,27 +31,71 @@ describe('app', () => {
}); });
it('should create all new files in the correct location', async () => { it('should create all new files in the correct location', async () => {
await applicationGenerator(tree, {
name,
projectNameAndRootFormat: 'as-provided',
unitTestRunner: 'vitest',
});
const newFiles = tree.listChanges().map((change) => change.path); const newFiles = tree.listChanges().map((change) => change.path);
expect(newFiles).toMatchSnapshot(); expect(newFiles).toMatchSnapshot();
}); });
it('should add nuxt entries in .gitignore', () => { it('should add nuxt entries in .gitignore', async () => {
await applicationGenerator(tree, {
name,
projectNameAndRootFormat: 'as-provided',
unitTestRunner: 'vitest',
});
expect(tree.read('.gitignore', 'utf-8')).toMatchSnapshot(); expect(tree.read('.gitignore', 'utf-8')).toMatchSnapshot();
}); });
it('should configure nuxt correctly', () => { it('should configure nuxt correctly', async () => {
await applicationGenerator(tree, {
name,
projectNameAndRootFormat: 'as-provided',
unitTestRunner: 'vitest',
});
expect( expect(
tree.read(`${name}/nuxt.config.ts`, 'utf-8') tree.read(`${name}/nuxt.config.ts`, 'utf-8')
).toMatchSnapshot(); ).toMatchSnapshot();
}); });
it('should configure eslint correctly', () => { it('should configure eslint correctly (flat config)', async () => {
tree.write('eslint.config.js', '');
await applicationGenerator(tree, {
name,
projectNameAndRootFormat: 'as-provided',
unitTestRunner: 'vitest',
});
expect(
tree.read(`${name}/eslint.config.js`, 'utf-8')
).toMatchSnapshot();
});
it('should configure eslint correctly (eslintrc)', async () => {
await applicationGenerator(tree, {
name,
projectNameAndRootFormat: 'as-provided',
unitTestRunner: 'vitest',
});
expect( expect(
tree.read(`${name}/.eslintrc.json`, 'utf-8') tree.read(`${name}/.eslintrc.json`, 'utf-8')
).toMatchSnapshot(); ).toMatchSnapshot();
}); });
it('should configure vitest correctly', () => { it('should configure vitest correctly', async () => {
await applicationGenerator(tree, {
name,
projectNameAndRootFormat: 'as-provided',
unitTestRunner: 'vitest',
});
expect( expect(
tree.read(`${name}/vitest.config.ts`, 'utf-8') tree.read(`${name}/vitest.config.ts`, 'utf-8')
).toMatchSnapshot(); ).toMatchSnapshot();
@ -62,12 +107,24 @@ describe('app', () => {
expect(packageJson.devDependencies['vitest']).toEqual('^1.3.1'); expect(packageJson.devDependencies['vitest']).toEqual('^1.3.1');
}); });
it('should configure tsconfig and project.json correctly', () => { it('should configure tsconfig and project.json correctly', async () => {
await applicationGenerator(tree, {
name,
projectNameAndRootFormat: 'as-provided',
unitTestRunner: 'vitest',
});
expect(tree.read(`${name}/project.json`, 'utf-8')).toMatchSnapshot(); expect(tree.read(`${name}/project.json`, 'utf-8')).toMatchSnapshot();
expect(tree.read(`${name}/tsconfig.json`, 'utf-8')).toMatchSnapshot(); expect(tree.read(`${name}/tsconfig.json`, 'utf-8')).toMatchSnapshot();
}); });
it('should add the nuxt and vitest plugins', () => { it('should add the nuxt and vitest plugins', async () => {
await applicationGenerator(tree, {
name,
projectNameAndRootFormat: 'as-provided',
unitTestRunner: 'vitest',
});
const nxJson = readJson(tree, 'nx.json'); const nxJson = readJson(tree, 'nx.json');
expect(nxJson.plugins).toMatchObject([ expect(nxJson.plugins).toMatchObject([
{ {

View File

@ -1,15 +1,25 @@
import { Tree } from 'nx/src/generators/tree'; import { Tree } from 'nx/src/generators/tree';
import { lintProjectGenerator, Linter, LinterType } from '@nx/eslint'; import type { Linter as EsLintLinter } from 'eslint';
import { Linter, LinterType, lintProjectGenerator } from '@nx/eslint';
import { joinPathFragments } from 'nx/src/utils/path'; import { joinPathFragments } from 'nx/src/utils/path';
import { import {
GeneratorCallback,
addDependenciesToPackageJson, addDependenciesToPackageJson,
GeneratorCallback,
runTasksInSerial, runTasksInSerial,
updateJson,
} from '@nx/devkit'; } from '@nx/devkit';
import { editEslintConfigFiles } from '@nx/vue'; import {
addExtendsToLintConfig,
addIgnoresToLintConfig,
addOverrideToLintConfig,
isEslintConfigSupported,
lintConfigHasOverride,
replaceOverridesInLintConfig,
updateOverrideInLintConfig,
} from '@nx/eslint/src/generators/utils/eslint-file';
import { nuxtEslintConfigVersion } from './versions'; import { nuxtEslintConfigVersion } from './versions';
import { useFlatConfig } from '@nx/eslint/src/utils/flat-config';
// TODO(colum): Look into the recommended set up using `withNuxt` inside eslint.config.mjs. https://eslint.nuxt.com/packages/config
export async function addLinting( export async function addLinting(
host: Tree, host: Tree,
options: { options: {
@ -33,30 +43,36 @@ export async function addLinting(
}); });
tasks.push(lintTask); tasks.push(lintTask);
editEslintConfigFiles(host, options.projectRoot); if (isEslintConfigSupported(host, options.projectRoot)) {
editEslintConfigFiles(host, options.projectRoot);
updateJson( const addExtendsTask = addExtendsToLintConfig(
host, host,
joinPathFragments(options.projectRoot, '.eslintrc.json'), options.projectRoot,
(json) => { ['@nuxt/eslint-config'],
const { true
extends: pluginExtends, );
ignorePatterns: pluginIgnorePatters, tasks.push(addExtendsTask);
...config
} = json;
return { if (useFlatConfig(host)) {
extends: ['@nuxt/eslint-config', ...(pluginExtends || [])], addOverrideToLintConfig(
ignorePatterns: [ host,
...(pluginIgnorePatters || []), options.projectRoot,
'.nuxt/**', {
'.output/**', files: ['**/*.vue'],
'node_modules', languageOptions: {
], parserOptions: { parser: '@typescript-eslint/parser' },
...config, },
}; } as unknown // languageOptions is not in eslintrc format but for flat config
);
} }
);
addIgnoresToLintConfig(host, options.projectRoot, [
'.nuxt/**',
'.output/**',
'node_modules',
]);
}
const installTask = addDependenciesToPackageJson( const installTask = addDependenciesToPackageJson(
host, host,
@ -69,3 +85,68 @@ export async function addLinting(
} }
return runTasksInSerial(...tasks); return runTasksInSerial(...tasks);
} }
function editEslintConfigFiles(tree: Tree, projectRoot: string) {
const hasVueFiles = (
o: EsLintLinter.ConfigOverride<EsLintLinter.RulesRecord>
) =>
o.files &&
(Array.isArray(o.files)
? o.files.some((f) => f.endsWith('*.vue'))
: o.files.endsWith('*.vue'));
const addVueFiles = (
o: EsLintLinter.ConfigOverride<EsLintLinter.RulesRecord>
) => {
if (!o.files) {
o.files = ['*.vue'];
} else if (Array.isArray(o.files)) {
o.files.push('*.vue');
} else {
o.files = [o.files, '*.vue'];
}
};
if (
lintConfigHasOverride(
tree,
projectRoot,
(o) => o.parserOptions && !hasVueFiles(o),
true
)
) {
updateOverrideInLintConfig(
tree,
projectRoot,
(o) => !!o.parserOptions,
(o) => {
addVueFiles(o);
return o;
}
);
} else {
replaceOverridesInLintConfig(tree, projectRoot, [
{
files: ['*.ts', '*.tsx', '*.js', '*.jsx', '*.vue'],
rules: {},
},
]);
}
if (
lintConfigHasOverride(
tree,
'',
(o) => o.rules?.['@nx/enforce-module-boundaries'] && !hasVueFiles(o),
true
)
) {
updateOverrideInLintConfig(
tree,
'',
(o) => !!o.rules?.['@nx/enforce-module-boundaries'],
(o) => {
addVueFiles(o);
return o;
}
);
}
}

View File

@ -7,4 +7,4 @@ export const nuxtDevtoolsVersion = '1.0.0';
export const nuxtUiTemplatesVersion = '^1.3.1'; export const nuxtUiTemplatesVersion = '^1.3.1';
// linting deps // linting deps
export const nuxtEslintConfigVersion = '~0.3.6'; export const nuxtEslintConfigVersion = '~0.5.6';

View File

@ -13,9 +13,11 @@ import {
addExtendsToLintConfig, addExtendsToLintConfig,
addOverrideToLintConfig, addOverrideToLintConfig,
addPluginsToLintConfig, addPluginsToLintConfig,
addPredefinedConfigToFlatLintConfig,
findEslintFile, findEslintFile,
isEslintConfigSupported, isEslintConfigSupported,
} from '@nx/eslint/src/generators/utils/eslint-file'; } from '@nx/eslint/src/generators/utils/eslint-file';
import { useFlatConfig } from '@nx/eslint/src/utils/flat-config';
export interface PlaywrightLinterOptions { export interface PlaywrightLinterOptions {
project: string; project: string;
@ -76,24 +78,42 @@ export async function addLinterToPlaywrightProject(
isEslintConfigSupported(tree, projectConfig.root) || isEslintConfigSupported(tree, projectConfig.root) ||
isEslintConfigSupported(tree) isEslintConfigSupported(tree)
) { ) {
addExtendsToLintConfig( if (useFlatConfig(tree)) {
tree, addPredefinedConfigToFlatLintConfig(
projectConfig.root, tree,
'plugin:playwright/recommended' projectConfig.root,
); 'flat/recommended',
if (options.rootProject) { 'playwright',
addPluginsToLintConfig(tree, projectConfig.root, '@nx'); 'eslint-plugin-playwright',
addOverrideToLintConfig(tree, projectConfig.root, javaScriptOverride); false,
false
);
addOverrideToLintConfig(tree, projectConfig.root, {
files: ['*.ts', '*.js'],
rules: {},
});
} else {
const addExtendsTask = addExtendsToLintConfig(
tree,
projectConfig.root,
'plugin:playwright/recommended'
);
tasks.push(addExtendsTask);
if (options.rootProject) {
addPluginsToLintConfig(tree, projectConfig.root, '@nx');
addOverrideToLintConfig(tree, projectConfig.root, javaScriptOverride);
}
addOverrideToLintConfig(tree, projectConfig.root, {
files: [`${options.directory}/**/*.{ts,js,tsx,jsx}`],
parserOptions: !options.setParserOptionsProject
? undefined
: {
project: `${projectConfig.root}/tsconfig.*?.json`,
},
rules: {},
});
} }
addOverrideToLintConfig(tree, projectConfig.root, {
files: [`${options.directory}/**/*.{ts,js,tsx,jsx}`],
parserOptions: !options.setParserOptionsProject
? undefined
: {
project: `${projectConfig.root}/tsconfig.*?.json`,
},
rules: {},
});
} }
return runTasksInSerial(...tasks); return runTasksInSerial(...tasks);

View File

@ -1,3 +1,3 @@
export const nxVersion = require('../../package.json').version; export const nxVersion = require('../../package.json').version;
export const playwrightVersion = '^1.36.0'; export const playwrightVersion = '^1.36.0';
export const eslintPluginPlaywrightVersion = '^0.15.3'; export const eslintPluginPlaywrightVersion = '^1.6.2';

View File

@ -156,7 +156,14 @@ describe('lint-checks generator', () => {
], ],
"parser": "jsonc-eslint-parser", "parser": "jsonc-eslint-parser",
"rules": { "rules": {
"@nx/dependency-checks": "error", "@nx/dependency-checks": [
"error",
{
"ignoredFiles": [
"{projectRoot}/eslint.config.{js,cjs,mjs}",
],
},
],
}, },
}, },
{ {

View File

@ -8,6 +8,7 @@ import {
readProjectConfiguration, readProjectConfiguration,
TargetConfiguration, TargetConfiguration,
Tree, Tree,
updateJson,
updateProjectConfiguration, updateProjectConfiguration,
writeJson, writeJson,
} from '@nx/devkit'; } from '@nx/devkit';
@ -113,12 +114,23 @@ export function addMigrationJsonChecks(
fileSet.add(relativeMigrationsJsonPath); fileSet.add(relativeMigrationsJsonPath);
return { return {
...o, ...o,
files: Array.from(fileSet), files: formatFilesEntries(host, Array.from(fileSet)),
}; };
} }
); );
} }
function formatFilesEntries(tree: Tree, files: string[]): string[] {
if (!useFlatConfig(tree)) {
return files;
}
const filesAfter = files.map((f) => {
const after = f.startsWith('./') ? f.replace('./', '**/') : f;
return after;
});
return filesAfter;
}
function updateProjectTarget( function updateProjectTarget(
host: Tree, host: Tree,
options: PluginLintChecksGeneratorSchema, options: PluginLintChecksGeneratorSchema,
@ -199,12 +211,12 @@ function updateProjectEslintConfig(
// update it // update it
updateOverrideInLintConfig(host, options.root, lookup, (o) => ({ updateOverrideInLintConfig(host, options.root, lookup, (o) => ({
...o, ...o,
files: [ files: formatFilesEntries(host, [
...new Set([ ...new Set([
...(Array.isArray(o.files) ? o.files : [o.files]), ...(Array.isArray(o.files) ? o.files : [o.files]),
...files, ...files,
]), ]),
], ]),
...parser, ...parser,
rules: { rules: {
...o.rules, ...o.rules,
@ -214,7 +226,7 @@ function updateProjectEslintConfig(
} else { } else {
// add it // add it
addOverrideToLintConfig(host, options.root, { addOverrideToLintConfig(host, options.root, {
files, files: formatFilesEntries(host, files),
...parser, ...parser,
rules: { rules: {
'@nx/nx-plugin-checks': 'error', '@nx/nx-plugin-checks': 'error',

View File

@ -9,8 +9,11 @@ import { extraEslintDependencies } from '@nx/react/src/utils/lint';
import { import {
addExtendsToLintConfig, addExtendsToLintConfig,
addIgnoresToLintConfig, addIgnoresToLintConfig,
addOverrideToLintConfig,
addPredefinedConfigToFlatLintConfig,
isEslintConfigSupported, isEslintConfigSupported,
} from '@nx/eslint/src/generators/utils/eslint-file'; } from '@nx/eslint/src/generators/utils/eslint-file';
import { useFlatConfig } from '@nx/eslint/src/utils/flat-config';
interface NormalizedSchema { interface NormalizedSchema {
linter?: Linter | LinterType; linter?: Linter | LinterType;
@ -40,7 +43,24 @@ export async function addLinting(host: Tree, options: NormalizedSchema) {
tasks.push(lintTask); tasks.push(lintTask);
if (isEslintConfigSupported(host)) { if (isEslintConfigSupported(host)) {
addExtendsToLintConfig(host, options.projectRoot, 'plugin:@nx/react'); if (useFlatConfig(host)) {
addPredefinedConfigToFlatLintConfig(
host,
options.projectRoot,
'flat/react'
);
// Add an empty rules object to users know how to add/override rules
addOverrideToLintConfig(host, options.projectRoot, {
files: ['*.ts', '*.tsx', '*.js', '*.jsx'],
rules: {},
});
} else {
const addExtendsTask = addExtendsToLintConfig(host, options.projectRoot, {
name: 'plugin:@nx/react',
needCompatFixup: true,
});
tasks.push(addExtendsTask);
}
addIgnoresToLintConfig(host, options.projectRoot, [ addIgnoresToLintConfig(host, options.projectRoot, [
'public', 'public',
'.cache', '.cache',

View File

@ -38,11 +38,14 @@ import { showPossibleWarnings } from './lib/show-possible-warnings';
import { addE2e } from './lib/add-e2e'; import { addE2e } from './lib/add-e2e';
import { import {
addExtendsToLintConfig, addExtendsToLintConfig,
addOverrideToLintConfig,
addPredefinedConfigToFlatLintConfig,
isEslintConfigSupported, isEslintConfigSupported,
} from '@nx/eslint/src/generators/utils/eslint-file'; } from '@nx/eslint/src/generators/utils/eslint-file';
import { initGenerator as jsInitGenerator } from '@nx/js'; import { initGenerator as jsInitGenerator } from '@nx/js';
import { logShowProjectCommand } from '@nx/devkit/src/utils/log-show-project-command'; import { logShowProjectCommand } from '@nx/devkit/src/utils/log-show-project-command';
import { setupTailwindGenerator } from '../setup-tailwind/setup-tailwind'; import { setupTailwindGenerator } from '../setup-tailwind/setup-tailwind';
import { useFlatConfig } from '@nx/eslint/src/utils/flat-config';
async function addLinting(host: Tree, options: NormalizedSchema) { async function addLinting(host: Tree, options: NormalizedSchema) {
const tasks: GeneratorCallback[] = []; const tasks: GeneratorCallback[] = [];
@ -62,7 +65,25 @@ async function addLinting(host: Tree, options: NormalizedSchema) {
tasks.push(lintTask); tasks.push(lintTask);
if (isEslintConfigSupported(host)) { if (isEslintConfigSupported(host)) {
addExtendsToLintConfig(host, options.appProjectRoot, 'plugin:@nx/react'); if (useFlatConfig(host)) {
addPredefinedConfigToFlatLintConfig(
host,
options.appProjectRoot,
'flat/react'
);
// Add an empty rules object to users know how to add/override rules
addOverrideToLintConfig(host, options.appProjectRoot, {
files: ['*.ts', '*.tsx', '*.js', '*.jsx'],
rules: {},
});
} else {
const addExtendsTask = addExtendsToLintConfig(
host,
options.appProjectRoot,
{ name: 'plugin:@nx/react', needCompatFixup: true }
);
tasks.push(addExtendsTask);
}
} }
if (!options.skipPackageJson) { if (!options.skipPackageJson) {

Some files were not shown because too many files have changed in this diff Show More