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.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}/src/index.html`);
expect(moveOutput).toContain(`CREATE ${newPath}/src/main.ts`);

View File

@ -127,8 +127,7 @@ describe('Angular Projects', () => {
// check e2e tests
if (runE2ETests('playwright')) {
const e2eResults = runCLI(`e2e ${app1}-e2e`);
expect(e2eResults).toContain('Successfully ran target e2e for project');
expect(() => runCLI(`e2e ${app1}-e2e`)).not.toThrow();
expect(await killPort(4200)).toBeTruthy();
}
@ -160,10 +159,7 @@ describe('Angular Projects', () => {
);
if (runE2ETests('playwright')) {
const e2eResults = runCLI(`e2e ${app}-e2e`);
expect(e2eResults).toContain(
`Successfully ran target e2e for project ${app}-e2e`
);
expect(() => runCLI(`e2e ${app}-e2e`)).not.toThrow();
expect(await killPort(4200)).toBeTruthy();
}
}, 1000000);
@ -495,7 +491,7 @@ describe('Angular Projects', () => {
updateFile(`${lib}/src/lib/${lib}.module.ts`, moduleContent);
// ACT
const buildOutput = runCLI(`build ${lib}`);
const buildOutput = runCLI(`build ${lib}`, { env: { CI: 'false' } });
// ASSERT
expect(buildOutput).toContain(`Building entry point '@${proj}/${lib}'`);
@ -516,14 +512,9 @@ describe('Angular Projects', () => {
// check files are generated with the layout directory ("apps/")
checkFilesExist(`apps/${appName}/src/app/app.module.ts`);
// check build works
expect(runCLI(`build ${appName}`)).toContain(
`Successfully ran target build for project ${appName}`
);
expect(() => runCLI(`build ${appName}`)).not.toThrow();
// check tests pass
const appTestResult = runCLI(`test ${appName}`);
expect(appTestResult).toContain(
`Successfully ran target test for project ${appName}`
);
expect(() => runCLI(`test ${appName}`)).not.toThrow();
runCLI(
`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`
);
// check build works
expect(runCLI(`build ${libName}`)).toContain(
`Successfully ran target build for project ${libName}`
);
expect(() => runCLI(`build ${libName}`)).not.toThrow();
// check tests pass
const libTestResult = runCLI(`test ${libName}`);
expect(libTestResult).toContain(
`Successfully ran target test for project ${libName}`
);
expect(() => runCLI(`test ${libName}`)).not.toThrow();
}, 500_000);
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`
);
// check build works
expect(runCLI(`build ${libName}`)).toContain(
`Successfully ran target build for project ${libName}`
);
expect(() => runCLI(`build ${libName}`)).not.toThrow();
// check tests pass
const libTestResult = runCLI(`test ${libName}`);
expect(libTestResult).toContain(
`Successfully ran target test for project ${libName}`
);
expect(() => runCLI(`test ${libName}`)).not.toThrow();
}, 500_000);
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(
readJson(`dist/libs/${parentLib}/package.json`).dependencies
).toEqual({
'jsonc-eslint-parser': expect.any(String),
// Don't care about the versions, just that they exist
rambda: expect.any(String),
lodash: expect.any(String),

View File

@ -14,14 +14,17 @@ import {
} from '@nx/e2e/utils';
describe('Linter (legacy)', () => {
describe('Integrated', () => {
describe('Integrated (eslintrc config)', () => {
let originalEslintUseFlatConfigVal: string | undefined;
const myapp = uniq('myapp');
const mylib = uniq('mylib');
let projScope;
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'],
});
runCLI(`generate @nx/react:app ${myapp} --tags=validtag`, {
@ -31,7 +34,10 @@ describe('Linter (legacy)', () => {
env: { NX_ADD_PLUGINS: 'false' },
});
});
afterAll(() => cleanupProject());
afterAll(() => {
process.env.ESLINT_USE_FLAT_CONFIG = originalEslintUseFlatConfigVal;
cleanupProject();
});
describe('linting errors', () => {
let defaultEslintrc;
@ -58,8 +64,7 @@ describe('Linter (legacy)', () => {
updateFile('.eslintrc.json', JSON.stringify(eslintrc, null, 2));
// 1. linting should error when rules are not followed
let out = runCLI(`lint ${myapp}`, { silenceError: true });
expect(out).toContain('Unexpected console statement');
expect(() => runCLI(`lint ${myapp}`)).toThrow();
// 2. linting should not error when rules are not followed and the force flag is specified
expect(() => runCLI(`lint ${myapp} --force`)).not.toThrow();
@ -72,8 +77,9 @@ describe('Linter (legacy)', () => {
updateFile('.eslintrc.json', JSON.stringify(eslintrc, null, 2));
// 3. linting should not error when all rules are followed
out = runCLI(`lint ${myapp}`, { silenceError: true });
expect(out).toContain('All files pass linting');
expect(() =>
runCLI(`lint ${myapp}`, { silenceError: true })
).not.toThrow();
}, 1000000);
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));
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,
});
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';
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(() => {
process.env.NX_ADD_PLUGINS = 'false';
});
@ -162,7 +179,9 @@ describe('Linter (legacy)', () => {
const outFlat = runCLI(`affected -t lint`, {
silenceError: true,
});
expect(outFlat).toContain('ran target lint');
expect(outFlat).toContain(`${myapp}:lint`);
expect(outFlat).toContain(`${mylib}:lint`);
expect(outFlat).toContain(`${mylib2}:lint`);
}, 1000000);
it('should convert standalone to flat config', () => {
@ -199,7 +218,8 @@ describe('Linter (legacy)', () => {
const outFlat = runCLI(`affected -t lint`, {
silenceError: true,
});
expect(outFlat).toContain('ran target lint');
expect(outFlat).toContain(`${myapp}:lint`);
expect(outFlat).toContain(`${mylib}:lint`);
}, 1000000);
});
});

View File

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

View File

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

View File

@ -16,10 +16,7 @@ describe('Jest root projects', () => {
});
it('should test root level app projects', async () => {
const rootProjectTestResults = await runCLIAsync(`test ${myapp}`);
expect(rootProjectTestResults.combinedOutput).toContain(
'Test Suites: 1 passed, 1 total'
);
expect(() => runCLI(`test ${myapp}`)).not.toThrow();
}, 300_000);
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`
);
const libProjectTestResults = await runCLIAsync(`test ${mylib}`);
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'
);
expect(() => runCLI(`test ${mylib}`)).not.toThrow();
expect(() => runCLI(`test ${myapp}`)).not.toThrow();
}, 300_000);
});
@ -53,11 +41,7 @@ describe('Jest root projects', () => {
});
it('should test root level app projects', async () => {
const rootProjectTestResults = await runCLIAsync(`test ${myapp}`);
expect(rootProjectTestResults.combinedOutput).toContain(
'Test Suites: 1 passed, 1 total'
);
expect(() => runCLI(`test ${myapp}`)).not.toThrow();
}, 300_000);
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`
);
const libProjectTestResults = await runCLIAsync(`test ${mylib}`);
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'
);
expect(() => runCLI(`test ${mylib}`)).not.toThrow();
expect(() => runCLI(`test ${myapp}`)).not.toThrow();
}, 300_000);
});
});

View File

@ -10,6 +10,7 @@ import {
getPackageManagerCommand,
readJson,
updateFile,
renameFile,
} from '@nx/e2e/utils';
import { join } from 'path';
@ -169,6 +170,16 @@ describe('packaging libs', () => {
`libs/${swcEsmLib}/src/index.ts`,
`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
updateJson(join('libs', tscLib, 'project.json'), (json) => {

View File

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

View File

@ -120,28 +120,6 @@ describe('nx init (for React - legacy)', () => {
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', () => {
const appName = 'my-app';
createReactApp(appName);

View File

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

View File

@ -60,9 +60,14 @@ describe('Nx Import', () => {
execSync(`git commit -am "initial commit"`, {
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 ref = 'main';

View File

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

View File

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

View File

@ -232,14 +232,26 @@ export function runCommandAsync(
},
(err, stdout, stderr) => {
if (!opts.silenceError && err) {
logError(`Original command: ${command}`, `${stdout}\n\n${stderr}`);
reject(err);
}
resolve({
const outputs = {
stdout: stripConsoleColors(stdout),
stderr: stripConsoleColors(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 }> {
const pm = getPackageManagerCommand();
return runCommandAsync(
`${opts.silent ? pm.runNxSilent : pm.runNx} ${command}`,
opts
);
const commandToRun = `${opts.silent ? pm.runNxSilent : pm.runNx} ${command} ${
opts.verbose ?? isVerboseE2ERun() ? ' --verbose' : ''
}${opts.redirectStderr ? ' 2>&1' : ''}`;
return runCommandAsync(commandToRun, opts);
}
export function runNgAdd(

View File

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

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.
* @param page
@ -11,11 +11,12 @@ export function assertTextOnPage(
title: string,
selector: string = 'h1'
): void {
test.describe(path, () =>
// eslint-disable-next-line playwright/valid-title
test.describe(path, () => {
test(`should display "${title}"`, async ({ page }) => {
await page.goto(path);
const locator = page.locator(selector);
await expect(locator).toContainText(title);
})
);
});
});
}

View File

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

View File

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

View File

@ -7,11 +7,14 @@ import {
} from '@nx/devkit';
import { camelize, dasherize } from '@nx/devkit/src/utils/string-utils';
import { Linter, lintProjectGenerator } from '@nx/eslint';
import type * as eslint from 'eslint';
import {
javaScriptOverride,
typeScriptOverride,
} from '@nx/eslint/src/generators/init/global-eslint-config';
import {
addOverrideToLintConfig,
addPredefinedConfigToFlatLintConfig,
findEslintFile,
isEslintConfigSupported,
replaceOverridesInLintConfig,
@ -19,6 +22,7 @@ import {
import { addAngularEsLintDependencies } from './lib/add-angular-eslint-dependencies';
import { isBuildableLibraryProject } from './lib/buildable-project';
import type { AddLintingGeneratorSchema } from './schema';
import { useFlatConfig } from '@nx/eslint/src/utils/flat-config';
export async function addLintingGenerator(
tree: Tree,
@ -49,6 +53,59 @@ export async function addLintingGenerator(
.read(joinPathFragments(options.projectRoot, eslintFile), 'utf8')
.includes(`${options.projectRoot}/tsconfig.*?.json`);
if (useFlatConfig(tree)) {
addPredefinedConfigToFlatLintConfig(
tree,
options.projectRoot,
'flat/angular'
);
addPredefinedConfigToFlatLintConfig(
tree,
options.projectRoot,
'flat/angular-template'
);
addOverrideToLintConfig(tree, options.projectRoot, {
files: ['*.ts'],
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',
},
],
},
});
addOverrideToLintConfig(tree, options.projectRoot, {
files: ['*.html'],
rules: {},
});
if (isBuildableLibraryProject(tree, options.projectName)) {
addOverrideToLintConfig(tree, '', {
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}'],
},
],
},
});
}
} else {
replaceOverridesInLintConfig(tree, options.projectRoot, [
...(rootProject ? [typeScriptOverride, javaScriptOverride] : []),
{
@ -98,13 +155,22 @@ export async function addLintingGenerator(
files: ['*.json'],
parser: 'jsonc-eslint-parser',
rules: {
'@nx/dependency-checks': 'error',
} as any,
'@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,
]
: []),
]);
}
}
if (!options.skipPackageJson) {
const installTask = addAngularEsLintDependencies(tree, options.projectName);

View File

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

View File

@ -701,7 +701,14 @@ describe('lib', () => {
],
"parser": "jsonc-eslint-parser",
"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('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
await runLibraryGeneratorWithOpts({ linter: Linter.EsLint });
@ -1311,7 +1363,14 @@ describe('lib', () => {
],
"parser": "jsonc-eslint-parser",
"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 moduleFederationEnhancedVersion = '~0.6.0';
export const angularEslintVersion = '^18.0.1';
export const angularEslintVersion = '^18.3.0';
export const typescriptEslintVersion = '^7.16.0';
export const tailwindVersion = '^3.0.2';
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, {
cwd,
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,
windowsHide: true,
});

View File

@ -13,6 +13,7 @@ import {
addExtendsToLintConfig,
addOverrideToLintConfig,
addPluginsToLintConfig,
addPredefinedConfigToFlatLintConfig,
findEslintFile,
isEslintConfigSupported,
replaceOverridesInLintConfig,
@ -21,6 +22,7 @@ import {
javaScriptOverride,
typeScriptOverride,
} from '@nx/eslint/src/generators/init/global-eslint-config';
import { useFlatConfig } from '@nx/eslint/src/utils/flat-config';
export interface CyLinterOptions {
project: string;
@ -90,16 +92,33 @@ export async function addLinterToCyProject(
isEslintConfigSupported(tree)
) {
const overrides = [];
if (useFlatConfig(tree)) {
addPredefinedConfigToFlatLintConfig(
tree,
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);
}
addExtendsToLintConfig(
const addExtendsTask = addExtendsToLintConfig(
tree,
projectConfig.root,
'plugin:cypress/recommended'
);
tasks.push(addExtendsTask);
}
const cyVersion = installedCypressVersion();
/**
* 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) {
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
? undefined
: {
@ -130,7 +152,10 @@ export async function addLinterToCyProject(
replaceOverridesInLintConfig(tree, projectConfig.root, overrides);
} else {
overrides.unshift({
files: [
files: useFlatConfig(tree)
? // For flat configs we don't need to specify the files
undefined
: [
'*.cy.{ts,js,tsx,jsx}',
`${options.cypressDir}/**/*.{ts,js,tsx,jsx}`,
],

View File

@ -1,5 +1,5 @@
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 cypressViteDevServerVersion = '^2.2.1';
export const cypressVersion = '^13.13.0';

View File

@ -1,6 +1,7 @@
import { Linter, lintProjectGenerator } from '@nx/eslint';
import {
addDependenciesToPackageJson,
GeneratorCallback,
joinPathFragments,
runTasksInSerial,
Tree,
@ -9,14 +10,18 @@ import { extraEslintDependencies } from '@nx/react';
import { NormalizedSchema } from './normalize-options';
import {
addExtendsToLintConfig,
addOverrideToLintConfig,
addPredefinedConfigToFlatLintConfig,
isEslintConfigSupported,
} 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) {
if (options.linter === Linter.None) {
return () => {};
}
const tasks: GeneratorCallback[] = [];
const lintTask = await lintProjectGenerator(host, {
linter: options.linter,
project: options.e2eProjectName,
@ -26,9 +31,28 @@ export async function addLinting(host: Tree, options: NormalizedSchema) {
skipFormat: true,
addPlugin: options.addPlugin,
});
tasks.push(lintTask);
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(
@ -36,6 +60,7 @@ export async function addLinting(host: Tree, options: NormalizedSchema) {
extraEslintDependencies.dependencies,
extraEslintDependencies.devDependencies
);
tasks.push(installTask);
return runTasksInSerial(lintTask, installTask);
return runTasksInSerial(...tasks);
}

View File

@ -37,7 +37,14 @@
// Installed to workspace by plugins
"@typescript-eslint/parser",
"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",
"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"
},
"peerDependenciesMeta": {
@ -34,12 +34,14 @@
}
},
"dependencies": {
"@eslint/compat": "^1.1.1",
"@nx/devkit": "file:../devkit",
"@nx/js": "file:../js",
"@typescript-eslint/type-utils": "^7.16.0",
"@typescript-eslint/utils": "^7.16.0",
"@typescript-eslint/type-utils": "^8.0.0",
"@typescript-eslint/utils": "^8.0.0",
"chalk": "^4.1.0",
"confusing-browser-globals": "^1.0.9",
"globals": "^15.9.0",
"jsonc-eslint-parser": "^2.1.0",
"semver": "^7.5.3",
"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-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

@ -49,5 +49,12 @@ export default {
'@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,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 angularTemplate from './configs/angular-template';
import flatBase from './flat-configs/base';
import enforceModuleBoundaries, {
RULE_NAME as enforceModuleBoundariesRuleName,
} from './rules/enforce-module-boundaries';
@ -24,6 +26,7 @@ import { workspaceRules } from './resolve-workspace-rules';
module.exports = {
configs: {
// eslintrc configs
typescript,
javascript,
react: reactTmp,
@ -32,6 +35,34 @@ module.exports = {
'react-jsx': reactJsx,
angular: angularCode,
'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: {
[enforceModuleBoundariesRuleName]: enforceModuleBoundaries,

View File

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

View File

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

View File

@ -57,7 +57,6 @@ export default ESLintUtils.RuleCreator(() => ``)<Options, MessageIds>({
meta: {
docs: {
description: 'Checks common nx-plugin configuration files for validity',
recommended: 'recommended',
},
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', () => ({
ESLint: jest.fn(),
jest.mock('eslint/use-at-your-own-risk', () => ({
LegacyESLint: jest.fn(),
}));
import { ESLint } from 'eslint';
const { LegacyESLint } = require('eslint/use-at-your-own-risk');
import { resolveAndInstantiateESLint } from './eslint-utils';
describe('eslint-utils', () => {
@ -18,7 +18,7 @@ describe('eslint-utils', () => {
cacheStrategy: 'content',
}).catch(() => {});
expect(ESLint).toHaveBeenCalledWith({
expect(LegacyESLint).toHaveBeenCalledWith({
overrideConfigFile: './.eslintrc.json',
fix: true,
cache: true,
@ -40,7 +40,7 @@ describe('eslint-utils', () => {
cacheStrategy: 'content',
}).catch(() => {});
expect(ESLint).toHaveBeenCalledWith({
expect(LegacyESLint).toHaveBeenCalledWith({
overrideConfigFile: undefined,
fix: true,
cache: true,
@ -63,7 +63,7 @@ describe('eslint-utils', () => {
noEslintrc: true,
}).catch(() => {});
expect(ESLint).toHaveBeenCalledWith({
expect(LegacyESLint).toHaveBeenCalledWith({
overrideConfigFile: undefined,
fix: true,
cache: true,
@ -89,7 +89,7 @@ describe('eslint-utils', () => {
rulesdir: extraRuleDirectories,
} as any).catch(() => {});
expect(ESLint).toHaveBeenCalledWith({
expect(LegacyESLint).toHaveBeenCalledWith({
overrideConfigFile: undefined,
fix: true,
cache: true,
@ -114,7 +114,7 @@ describe('eslint-utils', () => {
resolvePluginsRelativeTo: './some-path',
} as any).catch(() => {});
expect(ESLint).toHaveBeenCalledWith({
expect(LegacyESLint).toHaveBeenCalledWith({
overrideConfigFile: undefined,
fix: true,
cache: true,
@ -135,7 +135,7 @@ describe('eslint-utils', () => {
reportUnusedDisableDirectives: 'error',
} as any).catch(() => {});
expect(ESLint).toHaveBeenCalledWith({
expect(LegacyESLint).toHaveBeenCalledWith({
cache: false,
cacheLocation: undefined,
cacheStrategy: undefined,
@ -153,7 +153,7 @@ describe('eslint-utils', () => {
it('should create a ESLint instance with no "reportUnusedDisableDirectives" if it is undefined', async () => {
await resolveAndInstantiateESLint(undefined, {} as any);
expect(ESLint).toHaveBeenCalledWith(
expect(LegacyESLint).toHaveBeenCalledWith(
expect.objectContaining({
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'
);
}
const ESLint = await resolveESLintClass(useFlatConfig);
const ESLint = await resolveESLintClass({
useFlatConfigOverrideVal: useFlatConfig,
});
const eslintOptions: ESLint.Options = {
overrideConfigFile: eslintConfigPath,

View File

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

View File

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

View File

@ -2,6 +2,7 @@ import { Tree, names } from '@nx/devkit';
import { ESLint } from 'eslint';
import * as ts from 'typescript';
import {
addFlatCompatToFlatConfig,
createNodeList,
generateAst,
generateFlatOverride,
@ -149,6 +150,21 @@ export function convertEslintJsonToFlatConfig(
isFlatCompatNeeded = true;
}
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
const nodeList = createNodeList(
importsMap,
exportElements,
isFlatCompatNeeded
);
const nodeList = createNodeList(importsMap, exportElements);
let content = stringifyNodeList(nodeList);
if (isFlatCompatNeeded) {
content = addFlatCompatToFlatConfig(content);
}
return {
content: stringifyNodeList(nodeList),
content,
addESLintRC: isFlatCompatNeeded,
addESLintJS: isESLintJSNeeded,
};

View File

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

View File

@ -15,12 +15,16 @@ import {
import { ConvertToFlatConfigGeneratorSchema } from './schema';
import { findEslintFile } from '../utils/eslint-file';
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 { convertEslintJsonToFlatConfig } from './converters/json-converter';
let shouldInstallDeps = false;
export async function convertToFlatConfigGenerator(
tree: Tree,
options: ConvertToFlatConfigGeneratorSchema
@ -65,9 +69,7 @@ export async function convertToFlatConfigGenerator(
await formatFiles(tree);
}
if (shouldInstallDeps) {
return () => installPackagesTask(tree);
}
}
export default convertToFlatConfigGenerator;
@ -221,25 +223,21 @@ function processConvertedConfig(
// save new
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
if (addESLintRC) {
shouldInstallDeps = true;
addDependenciesToPackageJson(
tree,
{},
{
'@eslint/eslintrc': eslintrcVersion,
}
);
devDependencies['@eslint/eslintrc'] = eslintrcVersion;
}
if (addESLintJS) {
shouldInstallDeps = true;
addDependenciesToPackageJson(
tree,
{},
{
'@eslint/js': eslintVersion,
}
);
devDependencies['@eslint/js'] = eslintVersion;
}
addDependenciesToPackageJson(tree, {}, devDependencies);
}

View File

@ -2,10 +2,9 @@ import { Linter } from 'eslint';
import {
addBlockToFlatConfigExport,
addImportToFlatConfig,
addPluginsToExportsBlock,
createNodeList,
generateAst,
generateFlatOverride,
generateFlatPredefinedConfig,
stringifyNodeList,
} from '../utils/flat-config/ast-utils';
@ -93,40 +92,56 @@ export const getGlobalEsLintConfiguration = (
};
export const getGlobalFlatEslintConfiguration = (
unitTestRunner?: string,
rootProject?: boolean
): string => {
const nodeList = createNodeList(new Map(), [], true);
const nodeList = createNodeList(new Map(), []);
let content = stringifyNodeList(nodeList);
content = addImportToFlatConfig(content, 'nxPlugin', '@nx/eslint-plugin');
content = addPluginsToExportsBlock(content, [
{ name: '@nx', varName: 'nxPlugin', imp: '@nx/eslint-plugin' },
]);
content = addImportToFlatConfig(content, 'nx', '@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) {
content = addBlockToFlatConfigExport(
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,
generateFlatOverride(typeScriptOverride)
);
content = addBlockToFlatConfigExport(
content,
generateFlatOverride(javaScriptOverride)
);
if (unitTestRunner === 'jest') {
content = addBlockToFlatConfigExport(
content,
generateFlatOverride(jestOverride)
);
}
// add ignore for .nx folder
content = addBlockToFlatConfigExport(
content,
generateAst({
ignores: ['.nx'],
generateFlatOverride({
files: ['**/*.ts', '**/*.tsx', '**/*.js', '**/*.jsx'],
rules: {},
})
);

View File

@ -23,6 +23,7 @@ import {
generateSpreadElement,
removeCompatExtends,
removePlugin,
removePredefinedConfigs,
} from '../utils/flat-config/ast-utils';
import { hasEslintPlugin } from '../utils/plugin';
import { ESLINT_CONFIG_FILENAMES } from '../../utils/config-file';
@ -59,7 +60,7 @@ export function migrateConfigToMonorepoStyle(
tree.exists('eslint.config.js')
? 'eslint.base.config.js'
: 'eslint.config.js',
getGlobalFlatEslintConfiguration(unitTestRunner)
getGlobalFlatEslintConfiguration()
);
} else {
const eslintFile = findEslintFile(tree, '.');
@ -152,6 +153,11 @@ function migrateEslintFile(projectEslintPath: string, tree: Tree) {
'plugin:@nrwl/typescript',
'plugin:@nrwl/javascript',
]);
config = removePredefinedConfigs(config, '@nx/eslint-plugin', 'nx', [
'flat/base',
'flat/typescript',
'flat/javascript',
]);
tree.write(projectEslintPath, config);
} else {
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, {
...defaultOptions,
linter: Linter.EsLint,
@ -121,7 +167,12 @@ describe('@nx/eslint:lint-project', () => {
"files": ["*.json"],
"parser": "jsonc-eslint-parser",
"rules": {
"@nx/dependency-checks": "error"
"@nx/dependency-checks": [
"error",
{
"ignoredFiles": ["{projectRoot}/eslint.config.{js,cjs,mjs}"]
}
]
}
}
]

View File

@ -197,9 +197,16 @@ function createEsLintConfiguration(
const pathToRootConfig = extendedRootConfig
? `${offsetFromRoot(projectConfig.root)}${extendedRootConfig}`
: undefined;
const addDependencyChecks = isBuildableLibraryProject(projectConfig);
const addDependencyChecks =
options.addPackageJsonDependencyChecks ||
isBuildableLibraryProject(projectConfig);
const overrides: Linter.ConfigOverride<Linter.RulesRecord>[] = [
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'],
/**
@ -236,21 +243,23 @@ function createEsLintConfiguration(
},
];
if (
options.addPackageJsonDependencyChecks ||
isBuildableLibraryProject(projectConfig)
) {
if (addDependencyChecks) {
overrides.push({
files: ['*.json'],
parser: 'jsonc-eslint-parser',
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)) {
const isCompatNeeded = addDependencyChecks;
const nodes = [];
const importMap = new Map();
if (extendedRootConfig) {
@ -260,7 +269,7 @@ function createEsLintConfiguration(
overrides.forEach((override) => {
nodes.push(generateFlatOverride(override));
});
const nodeList = createNodeList(importMap, nodes, isCompatNeeded);
const nodeList = createNodeList(importMap, nodes);
const content = stringifyNodeList(nodeList);
tree.write(join(projectConfig.root, 'eslint.config.js'), content);
} else {

View File

@ -4,12 +4,18 @@ import {
type GeneratorCallback,
type Tree,
} from '@nx/devkit';
import { useFlatConfig } from '../../utils/flat-config';
import {
eslint9__eslintVersion,
eslint9__typescriptESLintVersion,
eslintConfigPrettierVersion,
nxVersion,
typescriptESLintVersion,
} from '../../utils/versions';
import { getGlobalEsLintConfiguration } from '../init/global-eslint-config';
import {
getGlobalEsLintConfiguration,
getGlobalFlatEslintConfiguration,
} from '../init/global-eslint-config';
import { findEslintFile } from '../utils/eslint-file';
export type SetupRootEsLintOptions = {
@ -26,7 +32,13 @@ export function setupRootEsLint(
if (rootEslintFile) {
return () => {};
}
if (!useFlatConfig(tree)) {
return setUpLegacyRootEslintRc(tree, options);
}
return setUpRootFlatConfig(tree, options);
}
function setUpLegacyRootEslintRc(tree: Tree, options: SetupRootEsLintOptions) {
writeJson(
tree,
'.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 {
addExtendsToLintConfig,
findEslintFile,
@ -5,13 +12,6 @@ import {
replaceOverridesInLintConfig,
} 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', () => {
let tree: Tree;
@ -120,6 +120,236 @@ describe('@nx/eslint:lint-file', () => {
'../../.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', () => {
@ -201,7 +431,6 @@ module.exports = [
recommendedConfig: js.configs.recommended,
});
module.exports = [
...baseConfig,
...compat.config({ extends: [

View File

@ -1,35 +1,43 @@
import {
addDependenciesToPackageJson,
type GeneratorCallback,
joinPathFragments,
names,
offsetFromRoot,
readJson,
type Tree,
updateJson,
} from '@nx/devkit';
import type { Tree } from '@nx/devkit';
import type { Linter } from 'eslint';
import {
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 { gte } from 'semver';
import {
baseEsLintConfigFile,
baseEsLintFlatConfigFile,
ESLINT_CONFIG_FILENAMES,
} 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(
tree: Tree,
@ -167,7 +175,7 @@ function offsetFilePath(
export function addOverrideToLintConfig(
tree: Tree,
root: string,
override: Linter.ConfigOverride<Linter.RulesRecord>,
override: Partial<Linter.ConfigOverride<Linter.RulesRecord>>,
options: { insertAtTheEnd?: boolean; checkBaseConfig?: boolean } = {
insertAtTheEnd: true,
}
@ -177,13 +185,13 @@ export function addOverrideToLintConfig(
if (useFlatConfig(tree)) {
const fileName = joinPathFragments(
root,
isBase ? baseEsLintFlatConfigFile : flatConfigEslintFilename(tree)
isBase ? baseEsLintFlatConfigFile : getRootESLintFlatConfigFilename(tree)
);
const flatOverride = generateFlatOverride(override);
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)) {
content = addCompatToFlatConfig(content);
content = addFlatCompatToFlatConfig(content);
}
tree.write(
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(
tree: Tree,
root: string,
@ -223,7 +223,10 @@ export function updateOverrideInLintConfig(
) => Linter.ConfigOverride<Linter.RulesRecord>
) {
if (useFlatConfig(tree)) {
const fileName = joinPathFragments(root, flatConfigEslintFilename(tree));
const fileName = joinPathFragments(
root,
getRootESLintFlatConfigFilename(tree)
);
let content = tree.read(fileName, 'utf8');
content = replaceOverride(content, root, lookup, update);
tree.write(fileName, content);
@ -265,7 +268,7 @@ export function lintConfigHasOverride(
if (useFlatConfig(tree)) {
const fileName = joinPathFragments(
root,
isBase ? baseEsLintFlatConfigFile : flatConfigEslintFilename(tree)
isBase ? baseEsLintFlatConfigFile : getRootESLintFlatConfigFilename(tree)
);
const content = tree.read(fileName, 'utf8');
return hasOverride(content, lookup);
@ -285,11 +288,14 @@ export function replaceOverridesInLintConfig(
overrides: Linter.ConfigOverride<Linter.RulesRecord>[]
) {
if (useFlatConfig(tree)) {
const fileName = joinPathFragments(root, flatConfigEslintFilename(tree));
const fileName = joinPathFragments(
root,
getRootESLintFlatConfigFilename(tree)
);
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)) {
content = addCompatToFlatConfig(content);
content = addFlatCompatToFlatConfig(content);
}
content = removeOverridesFromLintConfig(content);
overrides.forEach((override) => {
@ -310,21 +316,92 @@ export function replaceOverridesInLintConfig(
export function addExtendsToLintConfig(
tree: Tree,
root: string,
plugin: string | string[]
) {
const plugins = Array.isArray(plugin) ? plugin : [plugin];
plugin:
| string
| { name: string; needCompatFixup: boolean }
| Array<string | { name: string; needCompatFixup: boolean }>,
insertAtTheEnd = false
): GeneratorCallback {
if (useFlatConfig(tree)) {
const fileName = joinPathFragments(root, flatConfigEslintFilename(tree));
const pluginExtends = generatePluginExtendsElement(plugins);
let content = tree.read(fileName, 'utf8');
content = addCompatToFlatConfig(content);
tree.write(
fileName,
addBlockToFlatConfigExport(content, pluginExtends, {
insertAtTheEnd: false,
})
const pluginExtends: ts.SpreadElement[] = [];
const fileName = joinPathFragments(
root,
getRootESLintFlatConfigFilename(tree)
);
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 {
const plugins = (Array.isArray(plugin) ? plugin : [plugin]).map((p) =>
typeof p === 'string' ? p : p.name
);
const fileName = joinPathFragments(root, '.eslintrc.json');
updateJson(tree, fileName, (json) => {
json.extends ??= [];
@ -334,9 +411,39 @@ export function addExtendsToLintConfig(
];
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(
tree: Tree,
root: string,
@ -344,7 +451,10 @@ export function addPluginsToLintConfig(
) {
const plugins = Array.isArray(plugin) ? plugin : [plugin];
if (useFlatConfig(tree)) {
const fileName = joinPathFragments(root, flatConfigEslintFilename(tree));
const fileName = joinPathFragments(
root,
getRootESLintFlatConfigFilename(tree)
);
let content = tree.read(fileName, 'utf8');
const mappedPlugins: { name: string; varName: string; imp: string }[] = [];
plugins.forEach((name) => {
@ -372,7 +482,10 @@ export function addIgnoresToLintConfig(
ignorePatterns: string[]
) {
if (useFlatConfig(tree)) {
const fileName = joinPathFragments(root, flatConfigEslintFilename(tree));
const fileName = joinPathFragments(
root,
getRootESLintFlatConfigFilename(tree)
);
const block = generateAst<ts.ObjectLiteralExpression>({
ignores: ignorePatterns.map((path) => mapFilePath(path)),
});

View File

@ -1,16 +1,153 @@
import ts = require('typescript');
import {
addBlockToFlatConfigExport,
generateAst,
addFlatCompatToFlatConfig,
addImportToFlatConfig,
addCompatToFlatConfig,
removeOverridesFromLintConfig,
replaceOverride,
removePlugin,
generateAst,
generateFlatOverride,
generatePluginExtendsElementWithCompatFixup,
removeCompatExtends,
removeImportFromFlatConfig,
removeOverridesFromLintConfig,
removePlugin,
removePredefinedConfigs,
replaceOverride,
} from './ast-utils';
import { stripIndents } from '@nx/devkit';
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', () => {
it('should inject block to the end of the file', () => {
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', () => {
it('should add compat to config', () => {
const content = `const baseConfig = require("../../eslint.config.js");
@ -221,7 +384,7 @@ describe('ast-utils', () => {
},
{ ignores: ["my-lib/.cache/**/*"] },
];`;
const result = addCompatToFlatConfig(content);
const result = addFlatCompatToFlatConfig(content);
expect(result).toMatchInlineSnapshot(`
"const { FlatCompat } = require("@eslint/eslintrc");
const js = require("@eslint/js");
@ -231,7 +394,6 @@ describe('ast-utils', () => {
baseDirectory: __dirname,
recommendedConfig: js.configs.recommended,
});
module.exports = [
...baseConfig,
{
@ -260,7 +422,7 @@ describe('ast-utils', () => {
},
{ ignores: ["my-lib/.cache/**/*"] },
];`;
const result = addCompatToFlatConfig(content);
const result = addFlatCompatToFlatConfig(content);
expect(result).toMatchInlineSnapshot(`
"const { FlatCompat } = require("@eslint/eslintrc");
const baseConfig = require("../../eslint.config.js");
@ -270,7 +432,6 @@ describe('ast-utils', () => {
baseDirectory: __dirname,
recommendedConfig: js.configs.recommended,
});
module.exports = [
...baseConfig,
{
@ -306,7 +467,7 @@ describe('ast-utils', () => {
},
{ ignores: ["my-lib/.cache/**/*"] },
];`;
const result = addCompatToFlatConfig(content);
const result = addFlatCompatToFlatConfig(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 {
ChangeType,
StringChange,
applyChangesToString,
ChangeType,
parseJson,
StringChange,
} from '@nx/devkit';
import { Linter } from 'eslint';
import * as ts from 'typescript';
@ -101,12 +101,7 @@ export function hasOverride(
// strip any spread elements
objSource = fullNodeText.replace(SPREAD_ELEMENTS_REGEXP, '');
}
const data = parseJson(
objSource
// ensure property names have double quotes so that JSON.parse works
.replace(/'/g, '"')
.replace(/\s([a-zA-Z0-9_]+)\s*:/g, ' "$1": ')
);
const data = parseTextToJson(objSource);
if (lookup(data)) {
return true;
}
@ -121,6 +116,8 @@ function parseTextToJson(text: string): any {
// ensure property names have double quotes so that JSON.parse works
.replace(/'/g, '"')
.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,
lookup: (override: Linter.ConfigOverride<Linter.RulesRecord>) => boolean,
update?: (
override: Linter.ConfigOverride<Linter.RulesRecord>
) => Linter.ConfigOverride<Linter.RulesRecord>
override: Partial<Linter.ConfigOverride<Linter.RulesRecord>>
) => Partial<Linter.ConfigOverride<Linter.RulesRecord>>
): string {
const source = ts.createSourceFile(
'',
@ -172,13 +169,18 @@ export function replaceOverride(
start,
length: end - start,
});
const updatedData = update(data);
let updatedData = update(data);
if (updatedData) {
mapFilePaths(updatedData);
updatedData = mapFilePaths(updatedData);
changes.push({
type: ChangeType.Insert,
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.
*/
@ -342,6 +388,12 @@ export function addBlockToFlatConfigExport(
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);
if (options.insertAtTheEnd) {
const index =
@ -520,7 +572,7 @@ export function removeCompatExtends(
ts.ScriptKind.JS
);
const changes: StringChange[] = [];
findAllBlocks(source).forEach((node) => {
findAllBlocks(source)?.forEach((node) => {
if (
ts.isSpreadElement(node) &&
ts.isCallExpression(node.expression) &&
@ -554,7 +606,10 @@ export function removeCompatExtends(
text:
'\n' +
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);
}
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
*/
@ -596,7 +697,7 @@ export function addPluginsToExportsBlock(
/**
* Adds compat if missing to flat config
*/
export function addCompatToFlatConfig(content: string) {
export function addFlatCompatToFlatConfig(content: string) {
let result = content;
result = addImportToFlatConfig(result, 'js', '@eslint/js');
if (result.includes('const compat = new FlatCompat')) {
@ -608,17 +709,15 @@ export function addCompatToFlatConfig(content: string) {
{
type: ChangeType.Insert,
index: index - 1,
text: `${DEFAULT_FLAT_CONFIG}\n`,
},
]);
}
const DEFAULT_FLAT_CONFIG = `
text: `
const compat = new FlatCompat({
baseDirectory: __dirname,
recommendedConfig: js.configs.recommended,
});
`;
});
`,
},
]);
}
/**
* Generate node list representing the imports and the exports blocks
@ -626,24 +725,11 @@ const compat = new FlatCompat({
*/
export function createNodeList(
importsMap: Map<string, string>,
exportElements: ts.Expression[],
isFlatCompatNeeded: boolean
exportElements: ts.Expression[]
): ts.NodeArray<
ts.VariableStatement | ts.Identifier | ts.ExpressionStatement | ts.SourceFile
> {
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);
Array.from(importsMap.entries()).forEach(([imp, varName]) => {
@ -655,7 +741,7 @@ export function createNodeList(
...importsList,
ts.createSourceFile(
'',
isFlatCompatNeeded ? DEFAULT_FLAT_CONFIG : '',
'',
ts.ScriptTarget.Latest,
false,
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
*/
@ -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(
override: Linter.ConfigOverride<Linter.RulesRecord>
_override: Partial<Linter.ConfigOverride<Linter.RulesRecord>>
): ts.ObjectLiteralExpression | ts.SpreadElement {
mapFilePaths(override);
if (
!override.env &&
!override.extends &&
!override.plugins &&
!override.parser
) {
const override = mapFilePaths(_override);
// We do not need the compat tooling for this override
if (!overrideNeedsCompat(override)) {
// Ensure files is an array
let files = override.files;
if (typeof files === 'string') {
files = [files];
}
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) {
const { parserOptions, ...rest } = override;
return generateAst({ ...rest, languageOptions: { parserOptions } });
languageOptions['parserOptions'] = override.parserOptions;
}
return generateAst(override);
if (Object.keys(languageOptions).length) {
flatConfigOverride.languageOptions = languageOptions;
}
const { files, excludedFiles, rules, parserOptions, ...rest } = override;
}
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
),
]
)
);
}
},
});
}
// 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[] = [
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(
override: Linter.ConfigOverride<Linter.RulesRecord>
_override: Partial<Linter.ConfigOverride<Linter.RulesRecord>>
) {
const override: Partial<Linter.ConfigOverride<Linter.RulesRecord>> = {
..._override,
};
if (override.files) {
override.files = Array.isArray(override.files)
? override.files
@ -861,6 +1094,7 @@ export function mapFilePaths(
mapFilePath(file)
);
}
return override;
}
function addTSObjectProperty(
@ -876,10 +1110,21 @@ function addTSObjectProperty(
/**
* 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)) {
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
) as T;
}
@ -888,13 +1133,9 @@ export function generateAst<T>(input: unknown): T {
}
if (typeof input === 'object') {
return ts.factory.createObjectLiteralExpression(
Object.entries(input)
.filter(([_, value]) => value !== undefined)
.map(([key, value]) =>
ts.factory.createPropertyAssignment(
isValidKey(key) ? key : ts.factory.createStringLiteral(key),
generateAst<ts.Expression>(value)
)
generatePropertyAssignmentsFromObjectEntries(
input,
propertyAssignmentReplacer
),
Object.keys(input).length > 1 // multiline only if more than one property
) as T;
@ -912,6 +1153,35 @@ export function generateAst<T>(input: unknown): T {
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 {
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`] = `
"/* eslint-disable */
export default {
"export default {
displayName: 'eslint-rules',
preset: '../../jest.preset.js',
transform: {

View File

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

View File

@ -1,4 +1,5 @@
import { Tree } from '@nx/devkit';
import { gte } from 'semver';
// todo: add support for eslint.config.mjs,
export const eslintFlatConfigFilenames = [
@ -6,19 +7,42 @@ export const eslintFlatConfigFilenames = [
'eslint.config.cjs',
];
export function flatConfigEslintFilename(tree: Tree): string {
export function getRootESLintFlatConfigFilename(tree: Tree): string {
for (const file of eslintFlatConfigFilenames) {
if (tree.exists(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 {
try {
return !!flatConfigEslintFilename(tree);
} catch {
export function useFlatConfig(tree?: Tree): boolean {
// Prioritize taking ESLint's own environment variable into account when determining if we should use flat config
// If it is not defined, then default to true.
if (process.env.ESLINT_USE_FLAT_CONFIG === 'true') {
return true;
} else if (process.env.ESLINT_USE_FLAT_CONFIG === '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 { useFlatConfig } from '../utils/flat-config';
export async function resolveESLintClass(
useFlatConfig = false
): Promise<typeof ESLint> {
export async function resolveESLintClass(opts?: {
useFlatConfigOverrideVal: boolean;
}): Promise<typeof ESLint> {
try {
// In eslint 8.57.0 (the final v8 version), a dedicated API was added for resolving the correct ESLint class.
const eslint = await import('eslint');
if (typeof (eslint as any).loadESLint === 'function') {
return await (eslint as any).loadESLint({ useFlatConfig });
}
// 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;
}
// eslint-disable-next-line @typescript-eslint/no-var-requires
const { FlatESLint } = require('eslint/use-at-your-own-risk');
return FlatESLint;
// Explicitly use the FlatESLint and LegacyESLint classes here because the ESLint class points at a different one based on ESLint v8 vs ESLint v9
// But the decision on which one to use is not just based on the major version of ESLint.
// @ts-expect-error The may be wrong based on our installed eslint version
const { LegacyESLint, FlatESLint } = await import(
'eslint/use-at-your-own-risk'
);
const shouldESLintUseFlatConfig =
typeof opts?.useFlatConfigOverrideVal === 'boolean'
? opts.useFlatConfigOverrideVal
: useFlatConfig();
return shouldESLintUseFlatConfig ? FlatESLint : LegacyESLint;
} 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 eslintConfigPrettierVersion = '^9.0.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,
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 =
context.projectsConfigurations.projects[context.projectName].root;
@ -48,10 +56,6 @@ export async function installAndUpdatePackageJson(
const workspacePackageJson = readJsonFile(workspacePackageJsonPath);
const projectPackageJson = readJsonFile(projectPackageJsonPath);
const packages =
typeof options.packages === 'string'
? options.packages.split(',')
: options.packages;
displayNewlyAddedDepsMessage(
context.projectName,
await syncDeps(
@ -65,42 +69,10 @@ 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
function createInstallOptions(options: ExpoInstallOptions) {
return Object.keys(options).reduce((acc, k) => {
const v = options[k];
if (k === 'packages') {
const packages = typeof v === 'string' ? v.split(',') : v;
acc.push(...packages);
} else {
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
@ -109,7 +81,7 @@ function createInstallOptions(options: ExpoInstallOptions) {
} else {
acc.push(`--${names(k).fileName}`, v);
}
}
return acc;
}, []);
}

View File

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

View File

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

View File

@ -9,8 +9,11 @@ import { extraEslintDependencies } from '@nx/react/src/utils/lint';
import {
addExtendsToLintConfig,
addIgnoresToLintConfig,
addOverrideToLintConfig,
addPredefinedConfigToFlatLintConfig,
isEslintConfigSupported,
} from '@nx/eslint/src/generators/utils/eslint-file';
import { useFlatConfig } from '@nx/eslint/src/utils/flat-config';
interface NormalizedSchema {
linter?: Linter | LinterType;
@ -40,7 +43,24 @@ export async function addLinting(host: Tree, options: NormalizedSchema) {
tasks.push(lintTask);
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, [
'.expo',
'web-build',

View File

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

View File

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

View File

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

View File

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

View File

@ -537,7 +537,14 @@ describe('lib', () => {
],
"parser": "jsonc-eslint-parser",
"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",
"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",
"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.read(`my-lib/jest.config.ts`, 'utf-8'))
.toMatchInlineSnapshot(`
"/* eslint-disable */
export default {
"export default {
displayName: 'my-lib',
preset: '../jest.preset.js',
transform: {
@ -1483,7 +1503,10 @@ describe('lib', () => {
'@nx/dependency-checks': [
'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': [
'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'],
parser: 'jsonc-eslint-parser',
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 = {};
}
if (options.bundler === 'vite' || options.unitTestRunner === 'vitest') {
ruleOptions.ignoredFiles = [
'{projectRoot}/vite.config.{js,ts,mjs,mts}',
];
ruleOptions.ignoredFiles ??= [];
ruleOptions.ignoredFiles.push(
'{projectRoot}/vite.config.{js,ts,mjs,mts}'
);
o.rules['@nx/dependency-checks'] = [ruleSeverity, ruleOptions];
} else if (options.bundler === 'rollup') {
ruleOptions.ignoredFiles = [
'{projectRoot}/rollup.config.{js,ts,mjs,mts}',
];
ruleOptions.ignoredFiles ??= [];
ruleOptions.ignoredFiles.push(
'{projectRoot}/rollup.config.{js,ts,mjs,mts}'
);
o.rules['@nx/dependency-checks'] = [ruleSeverity, ruleOptions];
} else if (options.bundler === 'esbuild') {
ruleOptions.ignoredFiles = [
'{projectRoot}/esbuild.config.{js,ts,mjs,mts}',
];
ruleOptions.ignoredFiles ??= [];
ruleOptions.ignoredFiles.push(
'{projectRoot}/esbuild.config.{js,ts,mjs,mts}'
);
o.rules['@nx/dependency-checks'] = [ruleSeverity, ruleOptions];
}
return o;

View File

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

View File

@ -601,6 +601,38 @@ describe('app', () => {
describe('--linter', () => {
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 () => {
const name = uniq();
@ -660,17 +692,6 @@ describe('app', () => {
],
"rules": {},
},
{
"env": {
"jest": true,
},
"files": [
"*.spec.ts",
"*.spec.tsx",
"*.spec.js",
"*.spec.jsx",
],
},
],
}
`);

View File

@ -90,17 +90,6 @@ describe('updateEslint', () => {
],
"rules": {},
},
{
"env": {
"jest": true,
},
"files": [
"*.spec.ts",
"*.spec.tsx",
"*.spec.js",
"*.spec.jsx",
],
},
],
}
`);
@ -115,6 +104,7 @@ describe('updateEslint', () => {
.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({
@ -122,50 +112,10 @@ describe('updateEslint', () => {
recommendedConfig: js.configs.recommended,
});
module.exports = [
...compat.extends("plugin:@nx/react-typescript", "next", "next/core-web-vitals"),
...compat.extends("next", "next/core-web-vitals"),
...baseConfig,
{
"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
}
})),
...nx.configs["flat/react-typescript"],
{ ignores: [".next/**/*"] }
];
"

View File

@ -11,11 +11,12 @@ import { NormalizedSchema } from './normalize-options';
import {
addExtendsToLintConfig,
addIgnoresToLintConfig,
addOverrideToLintConfig,
addPredefinedConfigToFlatLintConfig,
isEslintConfigSupported,
updateOverrideInLintConfig,
} from '@nx/eslint/src/generators/utils/eslint-file';
import { eslintConfigNextVersion } from '../../../utils/versions';
import { useFlatConfig } from '@nx/eslint/src/utils/flat-config';
export async function addLinting(
host: Tree,
@ -39,11 +40,34 @@ export async function addLinting(
);
if (options.linter === Linter.EsLint && isEslintConfigSupported(host)) {
addExtendsToLintConfig(host, options.appProjectRoot, [
if (useFlatConfig(host)) {
addPredefinedConfigToFlatLintConfig(
host,
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',
'next',
'next/core-web-vitals',
]);
{ name: 'next', needCompatFixup: true },
{ name: 'next/core-web-vitals', needCompatFixup: true },
]
);
tasks.push(addExtendsTask);
}
updateOverrideInLintConfig(
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/**/*']);
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -31,6 +31,7 @@
"buildTargets": ["build-base"],
"ignoredDependencies": [
"nx",
"eslint",
"typescript",
"@nx/cypress",
"@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"],
"ignorePatterns": ["!**/*", ".nuxt/**", ".output/**", "node_modules"],
"overrides": [
{
"files": ["*.ts", "*.tsx", "*.js", "*.jsx", "*.vue"],
"rules": {
"vue/multi-word-component-names": "off"
}
"rules": {}
}
]
}
"
`;
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`] = `
"import { nxViteTsPaths } from '@nx/vite/plugins/nx-tsconfig-paths.plugin';
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"],
"ignorePatterns": ["!**/*", ".nuxt/**", ".output/**", "node_modules"],
"overrides": [
{
"files": ["*.ts", "*.tsx", "*.js", "*.jsx", "*.vue"],
"rules": {
"vue/multi-word-component-names": "off"
}
"rules": {}
}
]
}
"
`;
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`] = `
"import { nxViteTsPaths } from '@nx/vite/plugins/nx-tsconfig-paths.plugin';
import { defineNuxtConfig } from 'nuxt/config';

View File

@ -13,14 +13,15 @@ describe('app', () => {
describe('general application', () => {
beforeEach(async () => {
tree = createTreeWithEmptyWorkspace();
});
it('should not add targets', async () => {
await applicationGenerator(tree, {
name,
projectNameAndRootFormat: 'as-provided',
unitTestRunner: 'vitest',
});
});
it('should not add targets', async () => {
const projectConfig = readProjectConfiguration(tree, name);
expect(projectConfig.targets.build).toBeUndefined();
expect(projectConfig.targets.serve).toBeUndefined();
@ -30,27 +31,71 @@ describe('app', () => {
});
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);
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();
});
it('should configure nuxt correctly', () => {
it('should configure nuxt correctly', async () => {
await applicationGenerator(tree, {
name,
projectNameAndRootFormat: 'as-provided',
unitTestRunner: 'vitest',
});
expect(
tree.read(`${name}/nuxt.config.ts`, 'utf-8')
).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(
tree.read(`${name}/.eslintrc.json`, 'utf-8')
).toMatchSnapshot();
});
it('should configure vitest correctly', () => {
it('should configure vitest correctly', async () => {
await applicationGenerator(tree, {
name,
projectNameAndRootFormat: 'as-provided',
unitTestRunner: 'vitest',
});
expect(
tree.read(`${name}/vitest.config.ts`, 'utf-8')
).toMatchSnapshot();
@ -62,12 +107,24 @@ describe('app', () => {
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}/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');
expect(nxJson.plugins).toMatchObject([
{

View File

@ -1,15 +1,25 @@
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 {
GeneratorCallback,
addDependenciesToPackageJson,
GeneratorCallback,
runTasksInSerial,
updateJson,
} 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 { 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(
host: Tree,
options: {
@ -33,30 +43,36 @@ export async function addLinting(
});
tasks.push(lintTask);
if (isEslintConfigSupported(host, options.projectRoot)) {
editEslintConfigFiles(host, options.projectRoot);
updateJson(
const addExtendsTask = addExtendsToLintConfig(
host,
joinPathFragments(options.projectRoot, '.eslintrc.json'),
(json) => {
const {
extends: pluginExtends,
ignorePatterns: pluginIgnorePatters,
...config
} = json;
options.projectRoot,
['@nuxt/eslint-config'],
true
);
tasks.push(addExtendsTask);
return {
extends: ['@nuxt/eslint-config', ...(pluginExtends || [])],
ignorePatterns: [
...(pluginIgnorePatters || []),
if (useFlatConfig(host)) {
addOverrideToLintConfig(
host,
options.projectRoot,
{
files: ['**/*.vue'],
languageOptions: {
parserOptions: { parser: '@typescript-eslint/parser' },
},
} as unknown // languageOptions is not in eslintrc format but for flat config
);
}
addIgnoresToLintConfig(host, options.projectRoot, [
'.nuxt/**',
'.output/**',
'node_modules',
],
...config,
};
]);
}
);
const installTask = addDependenciesToPackageJson(
host,
@ -69,3 +85,68 @@ export async function addLinting(
}
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';
// linting deps
export const nuxtEslintConfigVersion = '~0.3.6';
export const nuxtEslintConfigVersion = '~0.5.6';

View File

@ -13,9 +13,11 @@ import {
addExtendsToLintConfig,
addOverrideToLintConfig,
addPluginsToLintConfig,
addPredefinedConfigToFlatLintConfig,
findEslintFile,
isEslintConfigSupported,
} from '@nx/eslint/src/generators/utils/eslint-file';
import { useFlatConfig } from '@nx/eslint/src/utils/flat-config';
export interface PlaywrightLinterOptions {
project: string;
@ -76,11 +78,28 @@ export async function addLinterToPlaywrightProject(
isEslintConfigSupported(tree, projectConfig.root) ||
isEslintConfigSupported(tree)
) {
addExtendsToLintConfig(
if (useFlatConfig(tree)) {
addPredefinedConfigToFlatLintConfig(
tree,
projectConfig.root,
'flat/recommended',
'playwright',
'eslint-plugin-playwright',
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);
@ -95,6 +114,7 @@ export async function addLinterToPlaywrightProject(
rules: {},
});
}
}
return runTasksInSerial(...tasks);
}

View File

@ -1,3 +1,3 @@
export const nxVersion = require('../../package.json').version;
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",
"rules": {
"@nx/dependency-checks": "error",
"@nx/dependency-checks": [
"error",
{
"ignoredFiles": [
"{projectRoot}/eslint.config.{js,cjs,mjs}",
],
},
],
},
},
{

View File

@ -8,6 +8,7 @@ import {
readProjectConfiguration,
TargetConfiguration,
Tree,
updateJson,
updateProjectConfiguration,
writeJson,
} from '@nx/devkit';
@ -113,12 +114,23 @@ export function addMigrationJsonChecks(
fileSet.add(relativeMigrationsJsonPath);
return {
...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(
host: Tree,
options: PluginLintChecksGeneratorSchema,
@ -199,12 +211,12 @@ function updateProjectEslintConfig(
// update it
updateOverrideInLintConfig(host, options.root, lookup, (o) => ({
...o,
files: [
files: formatFilesEntries(host, [
...new Set([
...(Array.isArray(o.files) ? o.files : [o.files]),
...files,
]),
],
]),
...parser,
rules: {
...o.rules,
@ -214,7 +226,7 @@ function updateProjectEslintConfig(
} else {
// add it
addOverrideToLintConfig(host, options.root, {
files,
files: formatFilesEntries(host, files),
...parser,
rules: {
'@nx/nx-plugin-checks': 'error',

View File

@ -9,8 +9,11 @@ import { extraEslintDependencies } from '@nx/react/src/utils/lint';
import {
addExtendsToLintConfig,
addIgnoresToLintConfig,
addOverrideToLintConfig,
addPredefinedConfigToFlatLintConfig,
isEslintConfigSupported,
} from '@nx/eslint/src/generators/utils/eslint-file';
import { useFlatConfig } from '@nx/eslint/src/utils/flat-config';
interface NormalizedSchema {
linter?: Linter | LinterType;
@ -40,7 +43,24 @@ export async function addLinting(host: Tree, options: NormalizedSchema) {
tasks.push(lintTask);
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, [
'public',
'.cache',

View File

@ -38,11 +38,14 @@ import { showPossibleWarnings } from './lib/show-possible-warnings';
import { addE2e } from './lib/add-e2e';
import {
addExtendsToLintConfig,
addOverrideToLintConfig,
addPredefinedConfigToFlatLintConfig,
isEslintConfigSupported,
} from '@nx/eslint/src/generators/utils/eslint-file';
import { initGenerator as jsInitGenerator } from '@nx/js';
import { logShowProjectCommand } from '@nx/devkit/src/utils/log-show-project-command';
import { setupTailwindGenerator } from '../setup-tailwind/setup-tailwind';
import { useFlatConfig } from '@nx/eslint/src/utils/flat-config';
async function addLinting(host: Tree, options: NormalizedSchema) {
const tasks: GeneratorCallback[] = [];
@ -62,7 +65,25 @@ async function addLinting(host: Tree, options: NormalizedSchema) {
tasks.push(lintTask);
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) {

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