feat(linter): add flat config support to generators (#18534)
This commit is contained in:
parent
1c058ded80
commit
e34219ab96
@ -105,10 +105,9 @@
|
||||
"@swc/core": "^1.3.51",
|
||||
"@swc/jest": "^0.2.20",
|
||||
"@testing-library/react": "13.4.0",
|
||||
"@types/css-minimizer-webpack-plugin": "^3.2.1",
|
||||
"@types/cytoscape": "^3.18.2",
|
||||
"@types/detect-port": "^1.3.2",
|
||||
"@types/eslint": "~8.4.1",
|
||||
"@types/eslint": "~8.44.2",
|
||||
"@types/express": "4.17.14",
|
||||
"@types/flat": "^5.0.1",
|
||||
"@types/fs-extra": "^11.0.0",
|
||||
|
||||
@ -4,13 +4,17 @@ import {
|
||||
joinPathFragments,
|
||||
runTasksInSerial,
|
||||
Tree,
|
||||
updateJson,
|
||||
} from '@nx/devkit';
|
||||
import { Linter, lintProjectGenerator } from '@nx/linter';
|
||||
import { mapLintPattern } from '@nx/linter/src/generators/lint-project/lint-project';
|
||||
import { addAngularEsLintDependencies } from './lib/add-angular-eslint-dependencies';
|
||||
import { extendAngularEslintJson } from './lib/create-eslint-configuration';
|
||||
import type { AddLintingGeneratorSchema } from './schema';
|
||||
import {
|
||||
findEslintFile,
|
||||
isEslintConfigSupported,
|
||||
replaceOverridesInLintConfig,
|
||||
} from '@nx/linter/src/generators/utils/eslint-file';
|
||||
import { camelize, dasherize } from '@nx/devkit/src/utils/string-utils';
|
||||
|
||||
export async function addLintingGenerator(
|
||||
tree: Tree,
|
||||
@ -35,11 +39,57 @@ export async function addLintingGenerator(
|
||||
});
|
||||
tasks.push(lintTask);
|
||||
|
||||
updateJson(
|
||||
tree,
|
||||
joinPathFragments(options.projectRoot, '.eslintrc.json'),
|
||||
(json) => extendAngularEslintJson(json, options)
|
||||
);
|
||||
if (isEslintConfigSupported(tree)) {
|
||||
const eslintFile = findEslintFile(tree, options.projectRoot);
|
||||
// keep parser options if they exist
|
||||
const hasParserOptions = tree
|
||||
.read(joinPathFragments(options.projectRoot, eslintFile), 'utf8')
|
||||
.includes(`${options.projectRoot}/tsconfig.*?.json`);
|
||||
|
||||
replaceOverridesInLintConfig(tree, options.projectRoot, [
|
||||
{
|
||||
files: ['*.ts'],
|
||||
...(hasParserOptions
|
||||
? {
|
||||
parserOptions: {
|
||||
project: [`${options.projectRoot}/tsconfig.*?.json`],
|
||||
},
|
||||
}
|
||||
: {}),
|
||||
extends: [
|
||||
'plugin:@nx/angular',
|
||||
'plugin:@angular-eslint/template/process-inline-templates',
|
||||
],
|
||||
rules: {
|
||||
'@angular-eslint/directive-selector': [
|
||||
'error',
|
||||
{
|
||||
type: 'attribute',
|
||||
prefix: camelize(options.prefix),
|
||||
style: 'camelCase',
|
||||
},
|
||||
],
|
||||
'@angular-eslint/component-selector': [
|
||||
'error',
|
||||
{
|
||||
type: 'element',
|
||||
prefix: dasherize(options.prefix),
|
||||
style: 'kebab-case',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ['*.html'],
|
||||
extends: ['plugin:@nx/angular-template'],
|
||||
/**
|
||||
* Having an empty rules object present makes it more obvious to the user where they would
|
||||
* extend things from if they needed to
|
||||
*/
|
||||
rules: {},
|
||||
},
|
||||
]);
|
||||
}
|
||||
|
||||
if (!options.skipPackageJson) {
|
||||
const installTask = addAngularEsLintDependencies(tree);
|
||||
|
||||
@ -8,6 +8,9 @@ type EslintExtensionSchema = {
|
||||
prefix: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* @deprecated Use tools from `@nx/linter/src/generators/utils/eslint-file` instead
|
||||
*/
|
||||
export const extendAngularEslintJson = (
|
||||
json: Linter.Config,
|
||||
options: EslintExtensionSchema
|
||||
|
||||
@ -21,6 +21,7 @@ describe('Cypress e2e configuration', () => {
|
||||
|
||||
beforeEach(() => {
|
||||
tree = createTreeWithEmptyWorkspace({ layout: 'apps-libs' });
|
||||
tree.write('.eslintrc.json', '{}'); // we are explicitly checking for existance of config type
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
|
||||
@ -5,12 +5,19 @@ import {
|
||||
readProjectConfiguration,
|
||||
runTasksInSerial,
|
||||
Tree,
|
||||
updateJson,
|
||||
} from '@nx/devkit';
|
||||
import { Linter, lintProjectGenerator } from '@nx/linter';
|
||||
import { globalJavaScriptOverrides } from '@nx/linter/src/generators/init/global-eslint-config';
|
||||
import { installedCypressVersion } from './cypress-version';
|
||||
import { eslintPluginCypressVersion } from './versions';
|
||||
import {
|
||||
addExtendsToLintConfig,
|
||||
addOverrideToLintConfig,
|
||||
addPluginsToLintConfig,
|
||||
findEslintFile,
|
||||
isEslintConfigSupported,
|
||||
replaceOverridesInLintConfig,
|
||||
} from '@nx/linter/src/generators/utils/eslint-file';
|
||||
import { javaScriptOverride } from '@nx/linter/src/generators/init/global-eslint-config';
|
||||
|
||||
export interface CyLinterOptions {
|
||||
project: string;
|
||||
@ -42,7 +49,8 @@ export async function addLinterToCyProject(
|
||||
const tasks: GeneratorCallback[] = [];
|
||||
const projectConfig = readProjectConfiguration(tree, options.project);
|
||||
|
||||
if (!tree.exists(joinPathFragments(projectConfig.root, '.eslintrc.json'))) {
|
||||
const eslintFile = findEslintFile(tree, projectConfig.root);
|
||||
if (!eslintFile) {
|
||||
tasks.push(
|
||||
await lintProjectGenerator(tree, {
|
||||
project: options.project,
|
||||
@ -73,19 +81,33 @@ export async function addLinterToCyProject(
|
||||
: () => {}
|
||||
);
|
||||
|
||||
updateJson(
|
||||
tree,
|
||||
joinPathFragments(projectConfig.root, '.eslintrc.json'),
|
||||
(json) => {
|
||||
if (options.rootProject) {
|
||||
json.plugins = ['@nx'];
|
||||
json.extends = ['plugin:cypress/recommended'];
|
||||
} else {
|
||||
json.extends = ['plugin:cypress/recommended', ...json.extends];
|
||||
}
|
||||
json.overrides ??= [];
|
||||
const globals = options.rootProject ? [globalJavaScriptOverrides] : [];
|
||||
const override = {
|
||||
if (isEslintConfigSupported(tree)) {
|
||||
const overrides = [];
|
||||
if (options.rootProject) {
|
||||
addPluginsToLintConfig(tree, projectConfig.root, '@nx');
|
||||
overrides.push(javaScriptOverride);
|
||||
}
|
||||
addExtendsToLintConfig(
|
||||
tree,
|
||||
projectConfig.root,
|
||||
'plugin:cypress/recommended'
|
||||
);
|
||||
const cyVersion = installedCypressVersion();
|
||||
/**
|
||||
* We need this override because we enabled allowJS in the tsconfig to allow for JS based Cypress tests.
|
||||
* That however leads to issues with the CommonJS Cypress plugin file.
|
||||
*/
|
||||
const cy6Override = {
|
||||
files: [`${options.cypressDir}/plugins/index.js`],
|
||||
rules: {
|
||||
'@typescript-eslint/no-var-requires': 'off',
|
||||
'no-undef': 'off',
|
||||
},
|
||||
};
|
||||
const addCy6Override = cyVersion && cyVersion < 7;
|
||||
|
||||
if (options.overwriteExisting) {
|
||||
overrides.push({
|
||||
files: ['*.ts', '*.tsx', '*.js', '*.jsx'],
|
||||
parserOptions: !options.setParserOptionsProject
|
||||
? undefined
|
||||
@ -93,40 +115,32 @@ export async function addLinterToCyProject(
|
||||
project: `${projectConfig.root}/tsconfig.*?.json`,
|
||||
},
|
||||
rules: {},
|
||||
};
|
||||
const cyFiles = [
|
||||
{
|
||||
...override,
|
||||
files: [
|
||||
'*.cy.{ts,js,tsx,jsx}',
|
||||
`${options.cypressDir}/**/*.{ts,js,tsx,jsx}`,
|
||||
],
|
||||
},
|
||||
];
|
||||
if (options.overwriteExisting) {
|
||||
json.overrides = [...globals, override];
|
||||
} else {
|
||||
json.overrides.push(...globals);
|
||||
json.overrides.push(...cyFiles);
|
||||
});
|
||||
if (addCy6Override) {
|
||||
overrides.push(cy6Override);
|
||||
}
|
||||
|
||||
const cyVersion = installedCypressVersion();
|
||||
if (cyVersion && cyVersion < 7) {
|
||||
/**
|
||||
* We need this override because we enabled allowJS in the tsconfig to allow for JS based Cypress tests.
|
||||
* That however leads to issues with the CommonJS Cypress plugin file.
|
||||
*/
|
||||
json.overrides.push({
|
||||
files: [`${options.cypressDir}/plugins/index.js`],
|
||||
rules: {
|
||||
'@typescript-eslint/no-var-requires': 'off',
|
||||
'no-undef': 'off',
|
||||
},
|
||||
});
|
||||
replaceOverridesInLintConfig(tree, projectConfig.root, overrides);
|
||||
} else {
|
||||
overrides.unshift({
|
||||
files: [
|
||||
'*.cy.{ts,js,tsx,jsx}',
|
||||
`${options.cypressDir}/**/*.{ts,js,tsx,jsx}`,
|
||||
],
|
||||
parserOptions: !options.setParserOptionsProject
|
||||
? undefined
|
||||
: {
|
||||
project: `${projectConfig.root}/tsconfig.*?.json`,
|
||||
},
|
||||
rules: {},
|
||||
});
|
||||
if (addCy6Override) {
|
||||
overrides.push(cy6Override);
|
||||
}
|
||||
return json;
|
||||
overrides.forEach((override) =>
|
||||
addOverrideToLintConfig(tree, projectConfig.root, override)
|
||||
);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
return runTasksInSerial(...tasks);
|
||||
}
|
||||
|
||||
@ -4,10 +4,13 @@ import {
|
||||
joinPathFragments,
|
||||
runTasksInSerial,
|
||||
Tree,
|
||||
updateJson,
|
||||
} from '@nx/devkit';
|
||||
import { extendReactEslintJson, extraEslintDependencies } from '@nx/react';
|
||||
import { extraEslintDependencies } from '@nx/react';
|
||||
import { NormalizedSchema } from './normalize-options';
|
||||
import {
|
||||
addExtendsToLintConfig,
|
||||
isEslintConfigSupported,
|
||||
} from '@nx/linter/src/generators/utils/eslint-file';
|
||||
|
||||
export async function addLinting(host: Tree, options: NormalizedSchema) {
|
||||
if (options.linter === Linter.None) {
|
||||
@ -24,11 +27,9 @@ export async function addLinting(host: Tree, options: NormalizedSchema) {
|
||||
skipFormat: true,
|
||||
});
|
||||
|
||||
updateJson(
|
||||
host,
|
||||
joinPathFragments(options.e2eProjectRoot, '.eslintrc.json'),
|
||||
extendReactEslintJson
|
||||
);
|
||||
if (isEslintConfigSupported(host)) {
|
||||
addExtendsToLintConfig(host, options.e2eProjectRoot, 'plugin:@nx/react');
|
||||
}
|
||||
|
||||
const installTask = addDependenciesToPackageJson(
|
||||
host,
|
||||
|
||||
@ -2,16 +2,15 @@ import { Linter, lintProjectGenerator } from '@nx/linter';
|
||||
import {
|
||||
addDependenciesToPackageJson,
|
||||
GeneratorCallback,
|
||||
joinPathFragments,
|
||||
runTasksInSerial,
|
||||
Tree,
|
||||
updateJson,
|
||||
} from '@nx/devkit';
|
||||
import { extraEslintDependencies } from '@nx/react/src/utils/lint';
|
||||
import {
|
||||
extendReactEslintJson,
|
||||
extraEslintDependencies,
|
||||
} from '@nx/react/src/utils/lint';
|
||||
import type { Linter as ESLintLinter } from 'eslint';
|
||||
addExtendsToLintConfig,
|
||||
addIgnoresToLintConfig,
|
||||
isEslintConfigSupported,
|
||||
} from '@nx/linter/src/generators/utils/eslint-file';
|
||||
|
||||
interface NormalizedSchema {
|
||||
linter?: Linter;
|
||||
@ -39,24 +38,15 @@ export async function addLinting(host: Tree, options: NormalizedSchema) {
|
||||
|
||||
tasks.push(lintTask);
|
||||
|
||||
updateJson(
|
||||
host,
|
||||
joinPathFragments(options.projectRoot, '.eslintrc.json'),
|
||||
(json: ESLintLinter.Config) => {
|
||||
json = extendReactEslintJson(json);
|
||||
|
||||
json.ignorePatterns = [
|
||||
...json.ignorePatterns,
|
||||
'.expo',
|
||||
'node_modules',
|
||||
'web-build',
|
||||
'cache',
|
||||
'dist',
|
||||
];
|
||||
|
||||
return json;
|
||||
}
|
||||
);
|
||||
if (isEslintConfigSupported(host)) {
|
||||
addExtendsToLintConfig(host, options.projectRoot, 'plugin:@nx/react');
|
||||
addIgnoresToLintConfig(host, options.projectRoot, [
|
||||
'.expo',
|
||||
'web-build',
|
||||
'cache',
|
||||
'dist',
|
||||
]);
|
||||
}
|
||||
|
||||
if (!options.skipPackageJson) {
|
||||
const installTask = await addDependenciesToPackageJson(
|
||||
|
||||
@ -256,20 +256,24 @@ export async function addLint(
|
||||
setParserOptionsProject: options.setParserOptionsProject,
|
||||
rootProject: options.rootProject,
|
||||
});
|
||||
// Also update the root .eslintrc.json lintProjectGenerator will not generate it for root projects.
|
||||
// Also update the root ESLint config. The lintProjectGenerator will not generate it for root projects.
|
||||
// But we need to set the package.json checks.
|
||||
if (options.rootProject) {
|
||||
updateJson(tree, '.eslintrc.json', (json) => {
|
||||
json.overrides ??= [];
|
||||
json.overrides.push({
|
||||
const {
|
||||
addOverrideToLintConfig,
|
||||
isEslintConfigSupported,
|
||||
// nx-ignore-next-line
|
||||
} = require('@nx/linter/src/generators/utils/eslint-file');
|
||||
|
||||
if (isEslintConfigSupported(tree)) {
|
||||
addOverrideToLintConfig(tree, '', {
|
||||
files: ['*.json'],
|
||||
parser: 'jsonc-eslint-parser',
|
||||
rules: {
|
||||
'@nx/dependency-checks': 'error',
|
||||
},
|
||||
});
|
||||
return json;
|
||||
});
|
||||
}
|
||||
}
|
||||
return task;
|
||||
}
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { ExecutorContext, joinPathFragments } from '@nx/devkit';
|
||||
import { ExecutorContext, joinPathFragments, workspaceRoot } from '@nx/devkit';
|
||||
import { ESLint } from 'eslint';
|
||||
import { existsSync, mkdirSync, writeFileSync } from 'fs';
|
||||
import { dirname, resolve } from 'path';
|
||||
@ -46,11 +46,11 @@ export default async function run(
|
||||
* we only want to support it if the user has explicitly opted into it by converting
|
||||
* their root ESLint config to use eslint.config.js
|
||||
*/
|
||||
const useFlatConfig = existsSync(
|
||||
joinPathFragments(systemRoot, 'eslint.config.js')
|
||||
const hasFlatConfig = existsSync(
|
||||
joinPathFragments(workspaceRoot, 'eslint.config.js')
|
||||
);
|
||||
|
||||
if (!eslintConfigPath && useFlatConfig) {
|
||||
if (!eslintConfigPath && hasFlatConfig) {
|
||||
const projectRoot =
|
||||
context.projectsConfigurations.projects[context.projectName].root;
|
||||
eslintConfigPath = joinPathFragments(projectRoot, 'eslint.config.js');
|
||||
@ -59,7 +59,7 @@ export default async function run(
|
||||
const { eslint, ESLint } = await resolveAndInstantiateESLint(
|
||||
eslintConfigPath,
|
||||
normalizedOptions,
|
||||
useFlatConfig
|
||||
hasFlatConfig
|
||||
);
|
||||
|
||||
const version = ESLint.version?.split('.');
|
||||
@ -130,7 +130,7 @@ Please see https://nx.dev/guides/eslint for full guidance on how to resolve this
|
||||
.filter((pattern) => !!pattern)
|
||||
.map((pattern) => `- '${pattern}'`);
|
||||
if (ignoredPatterns.length) {
|
||||
const ignoreSection = useFlatConfig
|
||||
const ignoreSection = hasFlatConfig
|
||||
? `'ignores' configuration`
|
||||
: `'.eslintignore' file`;
|
||||
throw new Error(
|
||||
|
||||
@ -19,7 +19,7 @@ export interface Schema extends JsonObject {
|
||||
cacheStrategy: 'content' | 'metadata' | null;
|
||||
rulesdir: string[];
|
||||
resolvePluginsRelativeTo: string | null;
|
||||
reportUnusedDisableDirectives: Linter.RuleLevel | null;
|
||||
reportUnusedDisableDirectives: Linter.StringSeverity | null;
|
||||
printConfig?: string | null;
|
||||
}
|
||||
|
||||
|
||||
@ -1,68 +0,0 @@
|
||||
import * as ts from 'typescript';
|
||||
|
||||
/**
|
||||
* Generates an AST from a JSON-type input
|
||||
*/
|
||||
export function generateAst<T>(input: unknown): T {
|
||||
if (Array.isArray(input)) {
|
||||
return ts.factory.createArrayLiteralExpression(
|
||||
input.map((item) => generateAst<ts.Expression>(item)),
|
||||
input.length > 1 // multiline only if more than one item
|
||||
) as T;
|
||||
}
|
||||
if (input === null) {
|
||||
return ts.factory.createNull() as 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)
|
||||
)
|
||||
),
|
||||
Object.keys(input).length > 1 // multiline only if more than one property
|
||||
) as T;
|
||||
}
|
||||
if (typeof input === 'string') {
|
||||
return ts.factory.createStringLiteral(input) as T;
|
||||
}
|
||||
if (typeof input === 'number') {
|
||||
return ts.factory.createNumericLiteral(input) as T;
|
||||
}
|
||||
if (typeof input === 'boolean') {
|
||||
return (input ? ts.factory.createTrue() : ts.factory.createFalse()) as T;
|
||||
}
|
||||
// since we are parsing JSON, this should never happen
|
||||
throw new Error(`Unknown type: ${typeof input}`);
|
||||
}
|
||||
|
||||
export function generateRequire(
|
||||
variableName: string | ts.ObjectBindingPattern,
|
||||
imp: string
|
||||
): ts.VariableStatement {
|
||||
return ts.factory.createVariableStatement(
|
||||
undefined,
|
||||
ts.factory.createVariableDeclarationList(
|
||||
[
|
||||
ts.factory.createVariableDeclaration(
|
||||
variableName,
|
||||
undefined,
|
||||
undefined,
|
||||
ts.factory.createCallExpression(
|
||||
ts.factory.createIdentifier('require'),
|
||||
undefined,
|
||||
[ts.factory.createStringLiteral(imp)]
|
||||
)
|
||||
),
|
||||
],
|
||||
ts.NodeFlags.Const
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function isValidKey(key: string): boolean {
|
||||
return /^[a-zA-Z0-9_]+$/.test(key);
|
||||
}
|
||||
@ -5,10 +5,19 @@ import {
|
||||
readJson,
|
||||
} from '@nx/devkit';
|
||||
import { join } from 'path';
|
||||
import { ESLint, Linter } from 'eslint';
|
||||
import { ESLint } from 'eslint';
|
||||
import * as ts from 'typescript';
|
||||
import { generateAst, generateRequire } from './generate-ast';
|
||||
import { eslintrcVersion } from '../../../utils/versions';
|
||||
import {
|
||||
createNodeList,
|
||||
generateAst,
|
||||
generateFlatOverride,
|
||||
generatePluginExtendsElement,
|
||||
generateSpreadElement,
|
||||
mapFilePath,
|
||||
stringifyNodeList,
|
||||
} from '../../utils/flat-config/ast-utils';
|
||||
import { getPluginImport } from '../../utils/eslint-file';
|
||||
|
||||
/**
|
||||
* Converts an ESLint JSON config to a flat config.
|
||||
@ -38,7 +47,16 @@ export function convertEslintJsonToFlatConfig(
|
||||
}
|
||||
|
||||
if (config.parser) {
|
||||
languageOptions.push(addParser(importsMap, config));
|
||||
const imp = config.parser;
|
||||
const parserName = names(imp).propertyName;
|
||||
importsMap.set(imp, parserName);
|
||||
|
||||
languageOptions.push(
|
||||
ts.factory.createPropertyAssignment(
|
||||
'parser',
|
||||
ts.factory.createIdentifier(parserName)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
if (config.parserOptions) {
|
||||
@ -129,7 +147,6 @@ export function convertEslintJsonToFlatConfig(
|
||||
|
||||
if (config.overrides) {
|
||||
config.overrides.forEach((override) => {
|
||||
updateFiles(override, root);
|
||||
if (
|
||||
override.env ||
|
||||
override.extends ||
|
||||
@ -137,10 +154,8 @@ export function convertEslintJsonToFlatConfig(
|
||||
override.parser
|
||||
) {
|
||||
isFlatCompatNeeded = true;
|
||||
addFlattenedOverride(override, exportElements);
|
||||
} else {
|
||||
exportElements.push(generateAst(override));
|
||||
}
|
||||
exportElements.push(generateFlatOverride(override, root));
|
||||
});
|
||||
}
|
||||
|
||||
@ -179,20 +194,8 @@ export function convertEslintJsonToFlatConfig(
|
||||
exportElements,
|
||||
isFlatCompatNeeded
|
||||
);
|
||||
const printer = ts.createPrinter({ newLine: ts.NewLineKind.LineFeed });
|
||||
const resultFile = ts.createSourceFile(
|
||||
join(root, destinationFile),
|
||||
'',
|
||||
ts.ScriptTarget.Latest,
|
||||
true,
|
||||
ts.ScriptKind.JS
|
||||
);
|
||||
const result = printer.printList(
|
||||
ts.ListFormat.MultiLine,
|
||||
nodeList,
|
||||
resultFile
|
||||
);
|
||||
tree.write(join(root, destinationFile), result);
|
||||
const content = stringifyNodeList(nodeList, root, destinationFile);
|
||||
tree.write(join(root, destinationFile), content);
|
||||
|
||||
if (isFlatCompatNeeded) {
|
||||
addDependenciesToPackageJson(
|
||||
@ -205,35 +208,6 @@ export function convertEslintJsonToFlatConfig(
|
||||
}
|
||||
}
|
||||
|
||||
function updateFiles(
|
||||
override: Linter.ConfigOverride<Linter.RulesRecord>,
|
||||
root: string
|
||||
) {
|
||||
if (override.files) {
|
||||
override.files = Array.isArray(override.files)
|
||||
? override.files
|
||||
: [override.files];
|
||||
override.files = override.files.map((file) => mapFilePath(file, root));
|
||||
}
|
||||
return override;
|
||||
}
|
||||
|
||||
function mapFilePath(filePath: string, root: string) {
|
||||
if (filePath.startsWith('!')) {
|
||||
const fileWithoutBang = filePath.slice(1);
|
||||
if (fileWithoutBang.startsWith('*.')) {
|
||||
return `!${join(root, '**', fileWithoutBang)}`;
|
||||
} else {
|
||||
return `!${join(root, fileWithoutBang)}`;
|
||||
}
|
||||
}
|
||||
if (filePath.startsWith('*.')) {
|
||||
return join(root, '**', filePath);
|
||||
} else {
|
||||
return join(root, filePath);
|
||||
}
|
||||
}
|
||||
|
||||
// add parsed extends to export blocks and add import statements
|
||||
function addExtends(
|
||||
importsMap: Map<string, string | string[]>,
|
||||
@ -254,9 +228,7 @@ function addExtends(
|
||||
.forEach((imp, index) => {
|
||||
if (imp.match(/\.eslintrc(.base)?\.json$/)) {
|
||||
const localName = index ? `baseConfig${index}` : 'baseConfig';
|
||||
configBlocks.push(
|
||||
ts.factory.createSpreadElement(ts.factory.createIdentifier(localName))
|
||||
);
|
||||
configBlocks.push(generateSpreadElement(localName));
|
||||
const newImport = imp.replace(
|
||||
/^(.*)\.eslintrc(.base)?\.json$/,
|
||||
'$1eslint$2.config.js'
|
||||
@ -311,36 +283,12 @@ function addExtends(
|
||||
}
|
||||
);
|
||||
|
||||
const pluginExtendsSpread = ts.factory.createSpreadElement(
|
||||
ts.factory.createCallExpression(
|
||||
ts.factory.createPropertyAccessExpression(
|
||||
ts.factory.createIdentifier('compat'),
|
||||
ts.factory.createIdentifier('extends')
|
||||
),
|
||||
undefined,
|
||||
eslintrcConfigs.map((plugin) => ts.factory.createStringLiteral(plugin))
|
||||
)
|
||||
);
|
||||
configBlocks.push(pluginExtendsSpread);
|
||||
configBlocks.push(generatePluginExtendsElement(eslintrcConfigs));
|
||||
}
|
||||
|
||||
return isFlatCompatNeeded;
|
||||
}
|
||||
|
||||
function getPluginImport(pluginName: string): string {
|
||||
if (pluginName.includes('eslint-plugin-')) {
|
||||
return pluginName;
|
||||
}
|
||||
if (!pluginName.startsWith('@')) {
|
||||
return `eslint-plugin-${pluginName}`;
|
||||
}
|
||||
if (!pluginName.includes('/')) {
|
||||
return `${pluginName}/eslint-plugin`;
|
||||
}
|
||||
const [scope, name] = pluginName.split('/');
|
||||
return `${scope}/eslint-plugin-${name}`;
|
||||
}
|
||||
|
||||
function addPlugins(
|
||||
importsMap: Map<string, string | string[]>,
|
||||
configBlocks: ts.Expression[],
|
||||
@ -382,143 +330,3 @@ function addPlugins(
|
||||
);
|
||||
configBlocks.push(pluginsAst);
|
||||
}
|
||||
|
||||
function addParser(
|
||||
importsMap: Map<string, string>,
|
||||
config: ESLint.ConfigData
|
||||
): ts.PropertyAssignment {
|
||||
const imp = config.parser;
|
||||
const parserName = names(imp).propertyName;
|
||||
importsMap.set(imp, parserName);
|
||||
|
||||
return ts.factory.createPropertyAssignment(
|
||||
'parser',
|
||||
ts.factory.createIdentifier(parserName)
|
||||
);
|
||||
}
|
||||
|
||||
function addFlattenedOverride(
|
||||
override: Linter.ConfigOverride<Linter.RulesRecord>,
|
||||
configBlocks: ts.Expression[]
|
||||
) {
|
||||
const { files, excludedFiles, rules, ...rest } = override;
|
||||
|
||||
const objectLiteralElements: ts.ObjectLiteralElementLike[] = [
|
||||
ts.factory.createSpreadAssignment(ts.factory.createIdentifier('config')),
|
||||
];
|
||||
if (files) {
|
||||
objectLiteralElements.push(
|
||||
ts.factory.createPropertyAssignment('files', generateAst(files))
|
||||
);
|
||||
}
|
||||
if (excludedFiles) {
|
||||
objectLiteralElements.push(
|
||||
ts.factory.createPropertyAssignment(
|
||||
'excludedFiles',
|
||||
generateAst(excludedFiles)
|
||||
)
|
||||
);
|
||||
}
|
||||
if (rules) {
|
||||
objectLiteralElements.push(
|
||||
ts.factory.createPropertyAssignment('rules', generateAst(rules))
|
||||
);
|
||||
}
|
||||
|
||||
const overrideSpread = ts.factory.createSpreadElement(
|
||||
ts.factory.createCallExpression(
|
||||
ts.factory.createPropertyAccessExpression(
|
||||
ts.factory.createCallExpression(
|
||||
ts.factory.createPropertyAccessExpression(
|
||||
ts.factory.createIdentifier('compat'),
|
||||
ts.factory.createIdentifier('config')
|
||||
),
|
||||
undefined,
|
||||
[generateAst(rest)]
|
||||
),
|
||||
ts.factory.createIdentifier('map')
|
||||
),
|
||||
undefined,
|
||||
[
|
||||
ts.factory.createArrowFunction(
|
||||
undefined,
|
||||
undefined,
|
||||
[
|
||||
ts.factory.createParameterDeclaration(
|
||||
undefined,
|
||||
undefined,
|
||||
'config'
|
||||
),
|
||||
],
|
||||
undefined,
|
||||
ts.factory.createToken(ts.SyntaxKind.EqualsGreaterThanToken),
|
||||
ts.factory.createParenthesizedExpression(
|
||||
ts.factory.createObjectLiteralExpression(
|
||||
objectLiteralElements,
|
||||
true
|
||||
)
|
||||
)
|
||||
),
|
||||
]
|
||||
)
|
||||
);
|
||||
configBlocks.push(overrideSpread);
|
||||
}
|
||||
|
||||
const DEFAULT_FLAT_CONFIG = `
|
||||
const compat = new FlatCompat({
|
||||
baseDirectory: __dirname,
|
||||
recommendedConfig: js.configs.recommended,
|
||||
});
|
||||
`;
|
||||
|
||||
function createNodeList(
|
||||
importsMap: Map<string, string>,
|
||||
exportElements: ts.Expression[],
|
||||
isFlatCompatNeeded: boolean
|
||||
): 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]) => {
|
||||
importsList.push(generateRequire(varName, imp));
|
||||
});
|
||||
|
||||
return ts.factory.createNodeArray([
|
||||
// add plugin imports
|
||||
...importsList,
|
||||
ts.createSourceFile(
|
||||
'',
|
||||
isFlatCompatNeeded ? DEFAULT_FLAT_CONFIG : '',
|
||||
ts.ScriptTarget.Latest,
|
||||
false,
|
||||
ts.ScriptKind.JS
|
||||
),
|
||||
// creates:
|
||||
// module.exports = [ ... ];
|
||||
ts.factory.createExpressionStatement(
|
||||
ts.factory.createBinaryExpression(
|
||||
ts.factory.createPropertyAccessExpression(
|
||||
ts.factory.createIdentifier('module'),
|
||||
ts.factory.createIdentifier('exports')
|
||||
),
|
||||
ts.factory.createToken(ts.SyntaxKind.EqualsToken),
|
||||
ts.factory.createArrayLiteralExpression(exportElements, true)
|
||||
)
|
||||
),
|
||||
]);
|
||||
}
|
||||
|
||||
@ -1,10 +1,19 @@
|
||||
import { Linter as LinterType } from 'eslint';
|
||||
import { Linter } from 'eslint';
|
||||
import {
|
||||
addBlockToFlatConfigExport,
|
||||
addImportToFlatConfig,
|
||||
addPluginsToExportsBlock,
|
||||
createNodeList,
|
||||
generateFlatOverride,
|
||||
stringifyNodeList,
|
||||
} from '../utils/flat-config/ast-utils';
|
||||
import { addPluginsToLintConfig } from '../utils/eslint-file';
|
||||
|
||||
/**
|
||||
* This configuration is intended to apply to all TypeScript source files.
|
||||
* See the eslint-plugin package for what is in the referenced shareable config.
|
||||
*/
|
||||
export const globalTypeScriptOverrides = {
|
||||
export const typeScriptOverride = {
|
||||
files: ['*.ts', '*.tsx'],
|
||||
extends: ['plugin:@nx/typescript'],
|
||||
/**
|
||||
@ -18,7 +27,7 @@ export const globalTypeScriptOverrides = {
|
||||
* This configuration is intended to apply to all JavaScript source files.
|
||||
* See the eslint-plugin package for what is in the referenced shareable config.
|
||||
*/
|
||||
export const globalJavaScriptOverrides = {
|
||||
export const javaScriptOverride = {
|
||||
files: ['*.js', '*.jsx'],
|
||||
extends: ['plugin:@nx/javascript'],
|
||||
/**
|
||||
@ -28,25 +37,11 @@ export const globalJavaScriptOverrides = {
|
||||
rules: {},
|
||||
};
|
||||
|
||||
/**
|
||||
* This configuration is intended to apply to all JSON source files.
|
||||
* See the eslint-plugin package for what is in the referenced shareable config.
|
||||
*/
|
||||
export const globalJsonOverrides = {
|
||||
files: ['*.json'],
|
||||
parser: 'jsonc-eslint-parser',
|
||||
/**
|
||||
* Having an empty rules object present makes it more obvious to the user where they would
|
||||
* extend things from if they needed to
|
||||
*/
|
||||
rules: {},
|
||||
};
|
||||
|
||||
/**
|
||||
* This configuration is intended to apply to all "source code" (but not
|
||||
* markup like HTML, or other custom file types like GraphQL)
|
||||
*/
|
||||
export const moduleBoundariesOverride = {
|
||||
const moduleBoundariesOverride = {
|
||||
files: ['*.ts', '*.tsx', '*.js', '*.jsx'],
|
||||
rules: {
|
||||
'@nx/enforce-module-boundaries': [
|
||||
@ -57,14 +52,26 @@ export const moduleBoundariesOverride = {
|
||||
depConstraints: [{ sourceTag: '*', onlyDependOnLibsWithTags: ['*'] }],
|
||||
},
|
||||
],
|
||||
} as LinterType.RulesRecord,
|
||||
} as Linter.RulesRecord,
|
||||
};
|
||||
|
||||
/**
|
||||
* This configuration is intended to apply to all "source code" (but not
|
||||
* markup like HTML, or other custom file types like GraphQL)
|
||||
*/
|
||||
const jestOverride = {
|
||||
files: ['*.spec.ts', '*.spec.tsx', '*.spec.js', '*.spec.jsx'],
|
||||
env: {
|
||||
jest: true,
|
||||
},
|
||||
rules: {},
|
||||
};
|
||||
|
||||
export const getGlobalEsLintConfiguration = (
|
||||
unitTestRunner?: string,
|
||||
rootProject?: boolean
|
||||
) => {
|
||||
const config: LinterType.Config = {
|
||||
): Linter.Config => {
|
||||
const config: Linter.Config = {
|
||||
root: true,
|
||||
ignorePatterns: rootProject ? ['!**/*'] : ['**/*'],
|
||||
plugins: ['@nx'],
|
||||
@ -77,18 +84,44 @@ export const getGlobalEsLintConfiguration = (
|
||||
*/
|
||||
overrides: [
|
||||
...(rootProject ? [] : [moduleBoundariesOverride]),
|
||||
globalTypeScriptOverrides,
|
||||
globalJavaScriptOverrides,
|
||||
typeScriptOverride,
|
||||
javaScriptOverride,
|
||||
...(unitTestRunner === 'jest' ? [jestOverride] : []),
|
||||
],
|
||||
};
|
||||
if (unitTestRunner === 'jest') {
|
||||
config.overrides.push({
|
||||
files: ['*.spec.ts', '*.spec.tsx', '*.spec.js', '*.spec.jsx'],
|
||||
env: {
|
||||
jest: true,
|
||||
},
|
||||
rules: {},
|
||||
});
|
||||
}
|
||||
return config;
|
||||
};
|
||||
|
||||
export const getGlobalFlatEslintConfiguration = (
|
||||
unitTestRunner?: string,
|
||||
rootProject?: boolean
|
||||
): string => {
|
||||
const nodeList = createNodeList(new Map(), [], true);
|
||||
let content = stringifyNodeList(nodeList, '', 'eslint.config.js');
|
||||
content = addImportToFlatConfig(content, 'nxPlugin', '@nx/eslint-plugin');
|
||||
content = addPluginsToExportsBlock(content, [
|
||||
{ name: '@nx', varName: 'nxPlugin', imp: '@nx/eslint-plugin' },
|
||||
]);
|
||||
if (!rootProject) {
|
||||
content = addBlockToFlatConfigExport(
|
||||
content,
|
||||
generateFlatOverride(moduleBoundariesOverride, '')
|
||||
);
|
||||
}
|
||||
content = addBlockToFlatConfigExport(
|
||||
content,
|
||||
generateFlatOverride(typeScriptOverride, '')
|
||||
);
|
||||
content = addBlockToFlatConfigExport(
|
||||
content,
|
||||
generateFlatOverride(javaScriptOverride, '')
|
||||
);
|
||||
if (unitTestRunner === 'jest') {
|
||||
content = addBlockToFlatConfigExport(
|
||||
content,
|
||||
generateFlatOverride(jestOverride, '')
|
||||
);
|
||||
}
|
||||
|
||||
return content;
|
||||
};
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import {
|
||||
addDependenciesToPackageJson,
|
||||
joinPathFragments,
|
||||
offsetFromRoot,
|
||||
ProjectConfiguration,
|
||||
@ -8,19 +9,46 @@ import {
|
||||
writeJson,
|
||||
} from '@nx/devkit';
|
||||
import { dirname } from 'path';
|
||||
import { findEslintFile } from '../utils/eslint-file';
|
||||
import { getGlobalEsLintConfiguration } from './global-eslint-config';
|
||||
import { findEslintFile, isEslintConfigSupported } from '../utils/eslint-file';
|
||||
import {
|
||||
getGlobalEsLintConfiguration,
|
||||
getGlobalFlatEslintConfiguration,
|
||||
} from './global-eslint-config';
|
||||
import { useFlatConfig } from '../../utils/flat-config';
|
||||
import { eslintrcVersion } from '../../utils/versions';
|
||||
import {
|
||||
addBlockToFlatConfigExport,
|
||||
addImportToFlatConfig,
|
||||
generateSpreadElement,
|
||||
removeCompatExtends,
|
||||
removePlugin,
|
||||
} from '../utils/flat-config/ast-utils';
|
||||
|
||||
export function migrateConfigToMonorepoStyle(
|
||||
projects: ProjectConfiguration[],
|
||||
tree: Tree,
|
||||
unitTestRunner: string
|
||||
): void {
|
||||
writeJson(
|
||||
tree,
|
||||
'.eslintrc.base.json',
|
||||
getGlobalEsLintConfiguration(unitTestRunner)
|
||||
);
|
||||
if (useFlatConfig(tree)) {
|
||||
// we need this for the compat
|
||||
addDependenciesToPackageJson(
|
||||
tree,
|
||||
{},
|
||||
{
|
||||
'@eslint/js': eslintrcVersion,
|
||||
}
|
||||
);
|
||||
tree.write(
|
||||
'eslint.base.config.js',
|
||||
getGlobalFlatEslintConfiguration(unitTestRunner)
|
||||
);
|
||||
} else {
|
||||
writeJson(
|
||||
tree,
|
||||
'.eslintrc.base.json',
|
||||
getGlobalEsLintConfiguration(unitTestRunner)
|
||||
);
|
||||
}
|
||||
|
||||
// update extens in all projects' eslint configs
|
||||
projects.forEach((project) => {
|
||||
@ -47,49 +75,72 @@ export function findLintTarget(
|
||||
}
|
||||
|
||||
function migrateEslintFile(projectEslintPath: string, tree: Tree) {
|
||||
if (
|
||||
projectEslintPath.endsWith('.json') ||
|
||||
projectEslintPath.endsWith('.eslintrc')
|
||||
) {
|
||||
updateJson(tree, projectEslintPath, (json) => {
|
||||
// we have a new root now
|
||||
delete json.root;
|
||||
// remove nrwl/nx plugins
|
||||
if (json.plugins) {
|
||||
json.plugins = json.plugins.filter(
|
||||
(p) => p !== '@nx' && p !== '@nrwl/nx'
|
||||
);
|
||||
if (json.plugins.length === 0) {
|
||||
delete json.plugins;
|
||||
}
|
||||
}
|
||||
// add extends
|
||||
json.extends = json.extends || [];
|
||||
const pathToRootConfig = `${offsetFromRoot(
|
||||
dirname(projectEslintPath)
|
||||
)}.eslintrc.base.json`;
|
||||
if (json.extends.indexOf(pathToRootConfig) === -1) {
|
||||
json.extends.push(pathToRootConfig);
|
||||
}
|
||||
// cleanup overrides
|
||||
if (json.overrides) {
|
||||
json.overrides.forEach((override) => {
|
||||
if (override.extends) {
|
||||
override.extends = override.extends.filter(
|
||||
(ext) =>
|
||||
ext !== 'plugin:@nx/typescript' &&
|
||||
ext !== 'plugin:@nrwl/nx/typescript' &&
|
||||
ext !== 'plugin:@nx/javascript' &&
|
||||
ext !== 'plugin:@nrwl/nx/javascript'
|
||||
);
|
||||
if (override.extends.length === 0) {
|
||||
delete override.extends;
|
||||
}
|
||||
if (isEslintConfigSupported(tree)) {
|
||||
if (useFlatConfig(tree)) {
|
||||
let config = tree.read(projectEslintPath, 'utf-8');
|
||||
// remove @nx plugin
|
||||
config = removePlugin(config, '@nx', '@nx/eslint-plugin-nx');
|
||||
// extend eslint.base.config.js
|
||||
config = addImportToFlatConfig(
|
||||
config,
|
||||
'baseConfig',
|
||||
`${offsetFromRoot(dirname(projectEslintPath))}eslint.base.config.js`
|
||||
);
|
||||
config = addBlockToFlatConfigExport(
|
||||
config,
|
||||
generateSpreadElement('baseConfig'),
|
||||
{ insertAtTheEnd: false }
|
||||
);
|
||||
// cleanup file extends
|
||||
config = removeCompatExtends(config, [
|
||||
'plugin:@nx/typescript',
|
||||
'plugin:@nx/javascript',
|
||||
'plugin:@nrwl/typescript',
|
||||
'plugin:@nrwl/javascript',
|
||||
]);
|
||||
console.warn('Flat eslint config is not supported yet for migration');
|
||||
tree.write(projectEslintPath, config);
|
||||
} else {
|
||||
updateJson(tree, projectEslintPath, (json) => {
|
||||
// we have a new root now
|
||||
delete json.root;
|
||||
// remove nrwl/nx plugins
|
||||
if (json.plugins) {
|
||||
json.plugins = json.plugins.filter(
|
||||
(p) => p !== '@nx' && p !== '@nrwl/nx'
|
||||
);
|
||||
if (json.plugins.length === 0) {
|
||||
delete json.plugins;
|
||||
}
|
||||
});
|
||||
}
|
||||
return json;
|
||||
});
|
||||
}
|
||||
// add extends
|
||||
json.extends = json.extends || [];
|
||||
const pathToRootConfig = `${offsetFromRoot(
|
||||
dirname(projectEslintPath)
|
||||
)}.eslintrc.base.json`;
|
||||
if (json.extends.indexOf(pathToRootConfig) === -1) {
|
||||
json.extends.push(pathToRootConfig);
|
||||
}
|
||||
// cleanup overrides
|
||||
if (json.overrides) {
|
||||
json.overrides.forEach((override) => {
|
||||
if (override.extends) {
|
||||
override.extends = override.extends.filter(
|
||||
(ext) =>
|
||||
ext !== 'plugin:@nx/typescript' &&
|
||||
ext !== 'plugin:@nrwl/nx/typescript' &&
|
||||
ext !== 'plugin:@nx/javascript' &&
|
||||
ext !== 'plugin:@nrwl/nx/javascript'
|
||||
);
|
||||
if (override.extends.length === 0) {
|
||||
delete override.extends;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
return json;
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (
|
||||
@ -99,6 +150,6 @@ function migrateEslintFile(projectEslintPath: string, tree: Tree) {
|
||||
console.warn('YAML eslint config is not supported yet for migration');
|
||||
}
|
||||
if (projectEslintPath.endsWith('.js') || projectEslintPath.endsWith('.cjs')) {
|
||||
console.warn('YAML eslint config is not supported yet for migration');
|
||||
console.warn('JS eslint config is not supported yet for migration');
|
||||
}
|
||||
}
|
||||
|
||||
@ -7,19 +7,31 @@ import {
|
||||
writeJson,
|
||||
} from '@nx/devkit';
|
||||
|
||||
import { Linter } from '../utils/linter';
|
||||
import { findEslintFile } from '../utils/eslint-file';
|
||||
import { Linter as LinterEnum } from '../utils/linter';
|
||||
import {
|
||||
baseEsLintConfigFile,
|
||||
baseEsLintFlatConfigFile,
|
||||
findEslintFile,
|
||||
} from '../utils/eslint-file';
|
||||
import { join } from 'path';
|
||||
import { lintInitGenerator } from '../init/init';
|
||||
import type { Linter } from 'eslint';
|
||||
import {
|
||||
findLintTarget,
|
||||
migrateConfigToMonorepoStyle,
|
||||
} from '../init/init-migration';
|
||||
import { getProjects } from 'nx/src/generators/utils/project-configuration';
|
||||
import { useFlatConfig } from '../../utils/flat-config';
|
||||
import {
|
||||
createNodeList,
|
||||
generateFlatOverride,
|
||||
generateSpreadElement,
|
||||
stringifyNodeList,
|
||||
} from '../utils/flat-config/ast-utils';
|
||||
|
||||
interface LintProjectOptions {
|
||||
project: string;
|
||||
linter?: Linter;
|
||||
linter?: LinterEnum;
|
||||
eslintFilePatterns?: string[];
|
||||
tsConfigPaths?: string[];
|
||||
skipFormat: boolean;
|
||||
@ -111,60 +123,85 @@ function createEsLintConfiguration(
|
||||
setParserOptionsProject: boolean
|
||||
) {
|
||||
const eslintConfig = findEslintFile(tree);
|
||||
writeJson(tree, join(projectConfig.root, `.eslintrc.json`), {
|
||||
extends: eslintConfig
|
||||
? [`${offsetFromRoot(projectConfig.root)}${eslintConfig}`]
|
||||
: undefined,
|
||||
// Include project files to be linted since the global one excludes all files.
|
||||
ignorePatterns: ['!**/*'],
|
||||
overrides: [
|
||||
{
|
||||
files: ['*.ts', '*.tsx', '*.js', '*.jsx'],
|
||||
/**
|
||||
* NOTE: We no longer set parserOptions.project by default when creating new projects.
|
||||
*
|
||||
* We have observed that users rarely add rules requiring type-checking to their Nx workspaces, and therefore
|
||||
* do not actually need the capabilites which parserOptions.project provides. When specifying parserOptions.project,
|
||||
* typescript-eslint needs to create full TypeScript Programs for you. When omitting it, it can perform a simple
|
||||
* parse (and AST tranformation) of the source files it encounters during a lint run, which is much faster and much
|
||||
* less memory intensive.
|
||||
*
|
||||
* In the rare case that users attempt to add rules requiring type-checking to their setup later on (and haven't set
|
||||
* parserOptions.project), the executor will attempt to look for the particular error typescript-eslint gives you
|
||||
* and provide feedback to the user.
|
||||
*/
|
||||
parserOptions: !setParserOptionsProject
|
||||
? undefined
|
||||
: {
|
||||
project: [`${projectConfig.root}/tsconfig.*?.json`],
|
||||
},
|
||||
/**
|
||||
* Having an empty rules object present makes it more obvious to the user where they would
|
||||
* extend things from if they needed to
|
||||
*/
|
||||
rules: {},
|
||||
},
|
||||
{
|
||||
files: ['*.ts', '*.tsx'],
|
||||
rules: {},
|
||||
},
|
||||
{
|
||||
files: ['*.js', '*.jsx'],
|
||||
rules: {},
|
||||
},
|
||||
...(isBuildableLibraryProject(projectConfig)
|
||||
? [
|
||||
{
|
||||
files: ['*.json'],
|
||||
parser: 'jsonc-eslint-parser',
|
||||
rules: {
|
||||
'@nx/dependency-checks': 'error',
|
||||
},
|
||||
},
|
||||
]
|
||||
: []),
|
||||
],
|
||||
});
|
||||
const pathToRootConfig = eslintConfig
|
||||
? `${offsetFromRoot(projectConfig.root)}${eslintConfig}`
|
||||
: undefined;
|
||||
const addDependencyChecks = isBuildableLibraryProject(projectConfig);
|
||||
|
||||
const overrides: Linter.ConfigOverride<Linter.RulesRecord>[] = [
|
||||
{
|
||||
files: ['*.ts', '*.tsx', '*.js', '*.jsx'],
|
||||
/**
|
||||
* NOTE: We no longer set parserOptions.project by default when creating new projects.
|
||||
*
|
||||
* We have observed that users rarely add rules requiring type-checking to their Nx workspaces, and therefore
|
||||
* do not actually need the capabilites which parserOptions.project provides. When specifying parserOptions.project,
|
||||
* typescript-eslint needs to create full TypeScript Programs for you. When omitting it, it can perform a simple
|
||||
* parse (and AST tranformation) of the source files it encounters during a lint run, which is much faster and much
|
||||
* less memory intensive.
|
||||
*
|
||||
* In the rare case that users attempt to add rules requiring type-checking to their setup later on (and haven't set
|
||||
* parserOptions.project), the executor will attempt to look for the particular error typescript-eslint gives you
|
||||
* and provide feedback to the user.
|
||||
*/
|
||||
parserOptions: !setParserOptionsProject
|
||||
? undefined
|
||||
: {
|
||||
project: [`${projectConfig.root}/tsconfig.*?.json`],
|
||||
},
|
||||
/**
|
||||
* Having an empty rules object present makes it more obvious to the user where they would
|
||||
* extend things from if they needed to
|
||||
*/
|
||||
rules: {},
|
||||
},
|
||||
{
|
||||
files: ['*.ts', '*.tsx'],
|
||||
rules: {},
|
||||
},
|
||||
{
|
||||
files: ['*.js', '*.jsx'],
|
||||
rules: {},
|
||||
},
|
||||
...(isBuildableLibraryProject(projectConfig)
|
||||
? [
|
||||
{
|
||||
files: ['*.json'],
|
||||
parser: 'jsonc-eslint-parser',
|
||||
rules: {
|
||||
'@nx/dependency-checks': 'error',
|
||||
} as Linter.RulesRecord,
|
||||
},
|
||||
]
|
||||
: []),
|
||||
];
|
||||
|
||||
if (useFlatConfig(tree)) {
|
||||
const isCompatNeeded = addDependencyChecks;
|
||||
const nodes = [];
|
||||
const importMap = new Map();
|
||||
if (eslintConfig) {
|
||||
importMap.set(pathToRootConfig, 'baseConfig');
|
||||
nodes.push(generateSpreadElement('baseConfig'));
|
||||
}
|
||||
overrides.forEach((override) => {
|
||||
nodes.push(generateFlatOverride(override, projectConfig.root));
|
||||
});
|
||||
const nodeList = createNodeList(importMap, nodes, isCompatNeeded);
|
||||
const content = stringifyNodeList(
|
||||
nodeList,
|
||||
projectConfig.root,
|
||||
'eslint.config.js'
|
||||
);
|
||||
tree.write(join(projectConfig.root, 'eslint.config.js'), content);
|
||||
} else {
|
||||
writeJson(tree, join(projectConfig.root, `.eslintrc.json`), {
|
||||
extends: eslintConfig ? [pathToRootConfig] : undefined,
|
||||
// Include project files to be linted since the global one excludes all files.
|
||||
ignorePatterns: ['!**/*'],
|
||||
overrides,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function isBuildableLibraryProject(
|
||||
@ -186,7 +223,10 @@ function isMigrationToMonorepoNeeded(
|
||||
tree: Tree
|
||||
): boolean {
|
||||
// the base config is already created, migration has been done
|
||||
if (tree.exists('.eslintrc.base.json')) {
|
||||
if (
|
||||
tree.exists(baseEsLintConfigFile) ||
|
||||
tree.exists(baseEsLintFlatConfigFile)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
@ -1,4 +1,27 @@
|
||||
import { joinPathFragments, Tree } from '@nx/devkit';
|
||||
import {
|
||||
joinPathFragments,
|
||||
names,
|
||||
offsetFromRoot,
|
||||
readJson,
|
||||
Tree,
|
||||
updateJson,
|
||||
} from '@nx/devkit';
|
||||
import { Linter } from 'eslint';
|
||||
import { useFlatConfig } from '../../utils/flat-config';
|
||||
import {
|
||||
addBlockToFlatConfigExport,
|
||||
addCompatToFlatConfig,
|
||||
addImportToFlatConfig,
|
||||
addPluginsToExportsBlock,
|
||||
generateAst,
|
||||
generateFlatOverride,
|
||||
generatePluginExtendsElement,
|
||||
hasOverride,
|
||||
mapFilePath,
|
||||
removeOverridesFromLintConfig,
|
||||
replaceOverride,
|
||||
} from './flat-config/ast-utils';
|
||||
import ts = require('typescript');
|
||||
|
||||
export const eslintConfigFileWhitelist = [
|
||||
'.eslintrc',
|
||||
@ -7,15 +30,19 @@ export const eslintConfigFileWhitelist = [
|
||||
'.eslintrc.yaml',
|
||||
'.eslintrc.yml',
|
||||
'.eslintrc.json',
|
||||
'eslint.config.js', // new format that requires `ESLINT_USE_FLAT_CONFIG=true`
|
||||
'eslint.config.js',
|
||||
];
|
||||
|
||||
export const baseEsLintConfigFile = '.eslintrc.base.json';
|
||||
export const baseEsLintFlatConfigFile = 'eslint.base.config.js';
|
||||
|
||||
export function findEslintFile(tree: Tree, projectRoot = ''): string | null {
|
||||
if (projectRoot === '' && tree.exists(baseEsLintConfigFile)) {
|
||||
return baseEsLintConfigFile;
|
||||
}
|
||||
if (projectRoot === '' && tree.exists(baseEsLintFlatConfigFile)) {
|
||||
return baseEsLintFlatConfigFile;
|
||||
}
|
||||
for (const file of eslintConfigFileWhitelist) {
|
||||
if (tree.exists(joinPathFragments(projectRoot, file))) {
|
||||
return file;
|
||||
@ -24,3 +51,322 @@ export function findEslintFile(tree: Tree, projectRoot = ''): string | null {
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
export function isEslintConfigSupported(tree: Tree, projectRoot = ''): boolean {
|
||||
const eslintFile = findEslintFile(tree, projectRoot);
|
||||
if (!eslintFile) {
|
||||
return;
|
||||
}
|
||||
return eslintFile.endsWith('.json') || eslintFile.endsWith('.config.js');
|
||||
}
|
||||
|
||||
export function updateRelativePathsInConfig(
|
||||
tree: Tree,
|
||||
sourcePath: string,
|
||||
destinationPath: string
|
||||
) {
|
||||
if (
|
||||
sourcePath === destinationPath ||
|
||||
!isEslintConfigSupported(tree, destinationPath)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const configPath = joinPathFragments(
|
||||
destinationPath,
|
||||
findEslintFile(tree, destinationPath)
|
||||
);
|
||||
const offset = offsetFromRoot(destinationPath);
|
||||
|
||||
if (useFlatConfig(tree)) {
|
||||
const config = tree.read(configPath, 'utf-8');
|
||||
tree.write(
|
||||
configPath,
|
||||
replaceFlatConfigPaths(config, sourcePath, offset, destinationPath)
|
||||
);
|
||||
} else {
|
||||
updateJson(tree, configPath, (json) => {
|
||||
if (typeof json.extends === 'string') {
|
||||
json.extends = offsetFilePath(sourcePath, json.extends, offset);
|
||||
} else if (json.extends) {
|
||||
json.extends = json.extends.map((extend: string) =>
|
||||
offsetFilePath(sourcePath, extend, offset)
|
||||
);
|
||||
}
|
||||
|
||||
json.overrides?.forEach(
|
||||
(o: { parserOptions?: { project?: string | string[] } }) => {
|
||||
if (o.parserOptions?.project) {
|
||||
o.parserOptions.project = Array.isArray(o.parserOptions.project)
|
||||
? o.parserOptions.project.map((p) =>
|
||||
p.replace(sourcePath, destinationPath)
|
||||
)
|
||||
: o.parserOptions.project.replace(sourcePath, destinationPath);
|
||||
}
|
||||
}
|
||||
);
|
||||
return json;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function replaceFlatConfigPaths(
|
||||
config: string,
|
||||
sourceRoot: string,
|
||||
offset: string,
|
||||
destinationRoot: string
|
||||
): string {
|
||||
let match;
|
||||
let newConfig = config;
|
||||
|
||||
// replace requires
|
||||
const requireRegex = RegExp(/require\(['"](.*)['"]\)/g);
|
||||
while ((match = requireRegex.exec(newConfig)) !== null) {
|
||||
const newPath = offsetFilePath(sourceRoot, match[1], offset);
|
||||
newConfig =
|
||||
newConfig.slice(0, match.index) +
|
||||
`require('${newPath}')` +
|
||||
newConfig.slice(match.index + match[0].length);
|
||||
}
|
||||
// replace projects
|
||||
const projectRegex = RegExp(/project:\s?\[?['"](.*)['"]\]?/g);
|
||||
while ((match = projectRegex.exec(newConfig)) !== null) {
|
||||
const newProjectDef = match[0].replaceAll(sourceRoot, destinationRoot);
|
||||
newConfig =
|
||||
newConfig.slice(0, match.index) +
|
||||
newProjectDef +
|
||||
newConfig.slice(match.index + match[0].length);
|
||||
}
|
||||
return newConfig;
|
||||
}
|
||||
|
||||
function offsetFilePath(
|
||||
projectRoot: string,
|
||||
pathToFile: string,
|
||||
offset: string
|
||||
): string {
|
||||
if (!pathToFile.startsWith('..')) {
|
||||
// not a relative path
|
||||
return pathToFile;
|
||||
}
|
||||
return joinPathFragments(offset, projectRoot, pathToFile);
|
||||
}
|
||||
|
||||
export function addOverrideToLintConfig(
|
||||
tree: Tree,
|
||||
root: string,
|
||||
override: Linter.ConfigOverride<Linter.RulesRecord>,
|
||||
options: { insertAtTheEnd?: boolean; checkBaseConfig?: boolean } = {
|
||||
insertAtTheEnd: true,
|
||||
}
|
||||
) {
|
||||
const isBase =
|
||||
options.checkBaseConfig && findEslintFile(tree, root).includes('.base');
|
||||
if (useFlatConfig(tree)) {
|
||||
const fileName = joinPathFragments(
|
||||
root,
|
||||
isBase ? baseEsLintFlatConfigFile : 'eslint.config.js'
|
||||
);
|
||||
const flatOverride = generateFlatOverride(override, root);
|
||||
let content = tree.read(fileName, 'utf8');
|
||||
// we will be using compat here so we need to make sure it's added
|
||||
if (overrideNeedsCompat(override)) {
|
||||
content = addCompatToFlatConfig(content);
|
||||
}
|
||||
tree.write(
|
||||
fileName,
|
||||
addBlockToFlatConfigExport(content, flatOverride, options)
|
||||
);
|
||||
} else {
|
||||
const fileName = joinPathFragments(
|
||||
root,
|
||||
isBase ? baseEsLintConfigFile : '.eslintrc.json'
|
||||
);
|
||||
updateJson(tree, fileName, (json) => {
|
||||
json.overrides ?? [];
|
||||
if (options.insertAtTheEnd) {
|
||||
json.overrides.push(override);
|
||||
} else {
|
||||
json.overrides.unshift(override);
|
||||
}
|
||||
return json;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function overrideNeedsCompat(
|
||||
override: Linter.ConfigOverride<Linter.RulesRecord>
|
||||
) {
|
||||
return (
|
||||
!override.env && !override.extends && !override.plugins && !override.parser
|
||||
);
|
||||
}
|
||||
|
||||
export function updateOverrideInLintConfig(
|
||||
tree: Tree,
|
||||
root: string,
|
||||
lookup: (override: Linter.ConfigOverride<Linter.RulesRecord>) => boolean,
|
||||
update: (
|
||||
override: Linter.ConfigOverride<Linter.RulesRecord>
|
||||
) => Linter.ConfigOverride<Linter.RulesRecord>
|
||||
) {
|
||||
if (useFlatConfig(tree)) {
|
||||
const fileName = joinPathFragments(root, 'eslint.config.js');
|
||||
let content = tree.read(fileName, 'utf8');
|
||||
content = replaceOverride(content, root, lookup, update);
|
||||
tree.write(fileName, content);
|
||||
} else {
|
||||
const fileName = joinPathFragments(root, '.eslintrc.json');
|
||||
updateJson(tree, fileName, (json: Linter.Config) => {
|
||||
const index = json.overrides.findIndex(lookup);
|
||||
if (index !== -1) {
|
||||
json.overrides[index] = update(json.overrides[index]);
|
||||
}
|
||||
return json;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export function lintConfigHasOverride(
|
||||
tree: Tree,
|
||||
root: string,
|
||||
lookup: (override: Linter.ConfigOverride<Linter.RulesRecord>) => boolean,
|
||||
checkBaseConfig = false
|
||||
): boolean {
|
||||
const isBase =
|
||||
checkBaseConfig && findEslintFile(tree, root).includes('.base');
|
||||
if (useFlatConfig(tree)) {
|
||||
const fileName = joinPathFragments(
|
||||
root,
|
||||
isBase ? baseEsLintFlatConfigFile : 'eslint.config.js'
|
||||
);
|
||||
const content = tree.read(fileName, 'utf8');
|
||||
return hasOverride(content, lookup);
|
||||
} else {
|
||||
const fileName = joinPathFragments(
|
||||
root,
|
||||
isBase ? baseEsLintConfigFile : '.eslintrc.json'
|
||||
);
|
||||
return readJson(tree, fileName).overrides?.some(lookup) || false;
|
||||
}
|
||||
}
|
||||
|
||||
export function replaceOverridesInLintConfig(
|
||||
tree: Tree,
|
||||
root: string,
|
||||
overrides: Linter.ConfigOverride<Linter.RulesRecord>[]
|
||||
) {
|
||||
if (useFlatConfig(tree)) {
|
||||
const fileName = joinPathFragments(root, 'eslint.config.js');
|
||||
let content = tree.read(fileName, 'utf8');
|
||||
// we will be using compat here so we need to make sure it's added
|
||||
if (overrides.some(overrideNeedsCompat)) {
|
||||
content = addCompatToFlatConfig(content);
|
||||
}
|
||||
content = removeOverridesFromLintConfig(content);
|
||||
overrides.forEach((override) => {
|
||||
const flatOverride = generateFlatOverride(override, root);
|
||||
addBlockToFlatConfigExport(content, flatOverride);
|
||||
});
|
||||
|
||||
tree.write(fileName, content);
|
||||
} else {
|
||||
const fileName = joinPathFragments(root, '.eslintrc.json');
|
||||
updateJson(tree, fileName, (json) => {
|
||||
json.overrides = overrides;
|
||||
return json;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export function addExtendsToLintConfig(
|
||||
tree: Tree,
|
||||
root: string,
|
||||
plugin: string | string[]
|
||||
) {
|
||||
const plugins = Array.isArray(plugin) ? plugin : [plugin];
|
||||
if (useFlatConfig(tree)) {
|
||||
const fileName = joinPathFragments(root, 'eslint.config.js');
|
||||
const pluginExtends = generatePluginExtendsElement(plugins);
|
||||
tree.write(
|
||||
fileName,
|
||||
addBlockToFlatConfigExport(tree.read(fileName, 'utf8'), pluginExtends)
|
||||
);
|
||||
} else {
|
||||
const fileName = joinPathFragments(root, '.eslintrc.json');
|
||||
updateJson(tree, fileName, (json) => {
|
||||
json.extends = [...plugins, ...(json.extends ?? [])];
|
||||
return json;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export function addPluginsToLintConfig(
|
||||
tree: Tree,
|
||||
root: string,
|
||||
plugin: string | string[]
|
||||
) {
|
||||
const plugins = Array.isArray(plugin) ? plugin : [plugin];
|
||||
if (useFlatConfig(tree)) {
|
||||
const fileName = joinPathFragments(root, 'eslint.config.js');
|
||||
let content = tree.read(fileName, 'utf8');
|
||||
const mappedPlugins: { name: string; varName: string; imp: string }[] = [];
|
||||
plugins.forEach((name) => {
|
||||
const imp = getPluginImport(name);
|
||||
const varName = names(imp).propertyName;
|
||||
mappedPlugins.push({ name, varName, imp });
|
||||
});
|
||||
mappedPlugins.forEach(({ varName, imp }) => {
|
||||
content = addImportToFlatConfig(content, varName, imp);
|
||||
});
|
||||
content = addPluginsToExportsBlock(content, mappedPlugins);
|
||||
tree.write(fileName, content);
|
||||
} else {
|
||||
const fileName = joinPathFragments(root, '.eslintrc.json');
|
||||
updateJson(tree, fileName, (json) => {
|
||||
json.plugins = [...plugins, ...(json.plugins ?? [])];
|
||||
return json;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export function addIgnoresToLintConfig(
|
||||
tree: Tree,
|
||||
root: string,
|
||||
ignorePatterns: string[]
|
||||
) {
|
||||
if (useFlatConfig(tree)) {
|
||||
const fileName = joinPathFragments(root, 'eslint.config.js');
|
||||
const block = generateAst<ts.ObjectLiteralExpression>({
|
||||
ignores: ignorePatterns.map((path) => mapFilePath(path, root)),
|
||||
});
|
||||
tree.write(
|
||||
fileName,
|
||||
addBlockToFlatConfigExport(tree.read(fileName, 'utf8'), block)
|
||||
);
|
||||
} else {
|
||||
const fileName = joinPathFragments(root, '.eslintrc.json');
|
||||
updateJson(tree, fileName, (json) => {
|
||||
const ignoreSet = new Set([
|
||||
...(json.ignorePatterns ?? []),
|
||||
...ignorePatterns,
|
||||
]);
|
||||
json.ignorePatterns = Array.from(ignoreSet);
|
||||
return json;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export function getPluginImport(pluginName: string): string {
|
||||
if (pluginName.includes('eslint-plugin-')) {
|
||||
return pluginName;
|
||||
}
|
||||
if (!pluginName.startsWith('@')) {
|
||||
return `eslint-plugin-${pluginName}`;
|
||||
}
|
||||
if (!pluginName.includes('/')) {
|
||||
return `${pluginName}/eslint-plugin`;
|
||||
}
|
||||
const [scope, name] = pluginName.split('/');
|
||||
return `${scope}/eslint-plugin-${name}`;
|
||||
}
|
||||
|
||||
@ -0,0 +1,836 @@
|
||||
import ts = require('typescript');
|
||||
import {
|
||||
addBlockToFlatConfigExport,
|
||||
generateAst,
|
||||
addImportToFlatConfig,
|
||||
addCompatToFlatConfig,
|
||||
removeOverridesFromLintConfig,
|
||||
replaceOverride,
|
||||
removePlugin,
|
||||
removeCompatExtends,
|
||||
} from './ast-utils';
|
||||
|
||||
describe('ast-utils', () => {
|
||||
describe('addBlockToFlatConfigExport', () => {
|
||||
it('should inject block to the end of the file', () => {
|
||||
const content = `const baseConfig = require("../../eslint.config.js");
|
||||
module.exports = [
|
||||
...baseConfig,
|
||||
{
|
||||
files: [
|
||||
"my-lib/**/*.ts",
|
||||
"my-lib/**/*.tsx"
|
||||
],
|
||||
rules: {}
|
||||
},
|
||||
{ ignores: ["my-lib/.cache/**/*"] },
|
||||
];`;
|
||||
const result = addBlockToFlatConfigExport(
|
||||
content,
|
||||
generateAst({
|
||||
files: ['**/*.svg'],
|
||||
rules: {
|
||||
'@nx/do-something-with-svg': 'error',
|
||||
},
|
||||
})
|
||||
);
|
||||
expect(result).toMatchInlineSnapshot(`
|
||||
"const baseConfig = require("../../eslint.config.js");
|
||||
module.exports = [
|
||||
...baseConfig,
|
||||
{
|
||||
files: [
|
||||
"my-lib/**/*.ts",
|
||||
"my-lib/**/*.tsx"
|
||||
],
|
||||
rules: {}
|
||||
},
|
||||
{ ignores: ["my-lib/.cache/**/*"] },
|
||||
{
|
||||
files: ["**/*.svg"],
|
||||
rules: { "@nx/do-something-with-svg": "error" }
|
||||
},
|
||||
];"
|
||||
`);
|
||||
});
|
||||
|
||||
it('should inject spread to the beginning of the file', () => {
|
||||
const content = `const baseConfig = require("../../eslint.config.js");
|
||||
module.exports = [
|
||||
...baseConfig,
|
||||
{
|
||||
files: [
|
||||
"my-lib/**/*.ts",
|
||||
"my-lib/**/*.tsx"
|
||||
],
|
||||
rules: {}
|
||||
},
|
||||
{ ignores: ["my-lib/.cache/**/*"] },
|
||||
];`;
|
||||
const result = addBlockToFlatConfigExport(
|
||||
content,
|
||||
ts.factory.createSpreadElement(ts.factory.createIdentifier('config')),
|
||||
{ insertAtTheEnd: false }
|
||||
);
|
||||
expect(result).toMatchInlineSnapshot(`
|
||||
"const baseConfig = require("../../eslint.config.js");
|
||||
module.exports = [
|
||||
...config,
|
||||
...baseConfig,
|
||||
{
|
||||
files: [
|
||||
"my-lib/**/*.ts",
|
||||
"my-lib/**/*.tsx"
|
||||
],
|
||||
rules: {}
|
||||
},
|
||||
{ ignores: ["my-lib/.cache/**/*"] },
|
||||
];"
|
||||
`);
|
||||
});
|
||||
});
|
||||
|
||||
describe('addImportToFlatConfig', () => {
|
||||
it('should inject import if not found', () => {
|
||||
const content = `const baseConfig = require("../../eslint.config.js");
|
||||
module.exports = [
|
||||
...baseConfig,
|
||||
{
|
||||
files: [
|
||||
"my-lib/**/*.ts",
|
||||
"my-lib/**/*.tsx"
|
||||
],
|
||||
rules: {}
|
||||
},
|
||||
{ ignores: ["my-lib/.cache/**/*"] },
|
||||
];`;
|
||||
const result = addImportToFlatConfig(
|
||||
content,
|
||||
'varName',
|
||||
'@myorg/awesome-config'
|
||||
);
|
||||
expect(result).toMatchInlineSnapshot(`
|
||||
"const varName = require("@myorg/awesome-config");
|
||||
const baseConfig = require("../../eslint.config.js");
|
||||
module.exports = [
|
||||
...baseConfig,
|
||||
{
|
||||
files: [
|
||||
"my-lib/**/*.ts",
|
||||
"my-lib/**/*.tsx"
|
||||
],
|
||||
rules: {}
|
||||
},
|
||||
{ ignores: ["my-lib/.cache/**/*"] },
|
||||
];"
|
||||
`);
|
||||
});
|
||||
|
||||
it('should update import if already found', () => {
|
||||
const content = `const { varName } = require("@myorg/awesome-config");
|
||||
const baseConfig = require("../../eslint.config.js");
|
||||
module.exports = [
|
||||
...baseConfig,
|
||||
{
|
||||
files: [
|
||||
"my-lib/**/*.ts",
|
||||
"my-lib/**/*.tsx"
|
||||
],
|
||||
rules: {}
|
||||
},
|
||||
{ ignores: ["my-lib/.cache/**/*"] },
|
||||
];`;
|
||||
const result = addImportToFlatConfig(
|
||||
content,
|
||||
['otherName', 'someName'],
|
||||
'@myorg/awesome-config'
|
||||
);
|
||||
expect(result).toMatchInlineSnapshot(`
|
||||
"const { varName, otherName, someName } = require("@myorg/awesome-config");
|
||||
const baseConfig = require("../../eslint.config.js");
|
||||
module.exports = [
|
||||
...baseConfig,
|
||||
{
|
||||
files: [
|
||||
"my-lib/**/*.ts",
|
||||
"my-lib/**/*.tsx"
|
||||
],
|
||||
rules: {}
|
||||
},
|
||||
{ ignores: ["my-lib/.cache/**/*"] },
|
||||
];"
|
||||
`);
|
||||
});
|
||||
|
||||
it('should not inject import if already exists', () => {
|
||||
const content = `const { varName, otherName } = require("@myorg/awesome-config");
|
||||
const baseConfig = require("../../eslint.config.js");
|
||||
module.exports = [
|
||||
...baseConfig,
|
||||
{
|
||||
files: [
|
||||
"my-lib/**/*.ts",
|
||||
"my-lib/**/*.tsx"
|
||||
],
|
||||
rules: {}
|
||||
},
|
||||
{ ignores: ["my-lib/.cache/**/*"] },
|
||||
];`;
|
||||
const result = addImportToFlatConfig(
|
||||
content,
|
||||
['otherName'],
|
||||
'@myorg/awesome-config'
|
||||
);
|
||||
expect(result).toEqual(content);
|
||||
});
|
||||
|
||||
it('should not update import if already exists', () => {
|
||||
const content = `const varName = require("@myorg/awesome-config");
|
||||
const baseConfig = require("../../eslint.config.js");
|
||||
module.exports = [
|
||||
...baseConfig,
|
||||
{
|
||||
files: [
|
||||
"my-lib/**/*.ts",
|
||||
"my-lib/**/*.tsx"
|
||||
],
|
||||
rules: {}
|
||||
},
|
||||
{ ignores: ["my-lib/.cache/**/*"] },
|
||||
];`;
|
||||
const result = addImportToFlatConfig(
|
||||
content,
|
||||
'varName',
|
||||
'@myorg/awesome-config'
|
||||
);
|
||||
expect(result).toEqual(content);
|
||||
});
|
||||
});
|
||||
|
||||
describe('addCompatToFlatConfig', () => {
|
||||
it('should add compat to config', () => {
|
||||
const content = `const baseConfig = require("../../eslint.config.js");
|
||||
module.exports = [
|
||||
...baseConfig,
|
||||
{
|
||||
files: [
|
||||
"my-lib/**/*.ts",
|
||||
"my-lib/**/*.tsx"
|
||||
],
|
||||
rules: {}
|
||||
},
|
||||
{ ignores: ["my-lib/.cache/**/*"] },
|
||||
];`;
|
||||
const result = addCompatToFlatConfig(content);
|
||||
expect(result).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 = [
|
||||
...baseConfig,
|
||||
{
|
||||
files: [
|
||||
"my-lib/**/*.ts",
|
||||
"my-lib/**/*.tsx"
|
||||
],
|
||||
rules: {}
|
||||
},
|
||||
{ ignores: ["my-lib/.cache/**/*"] },
|
||||
];"
|
||||
`);
|
||||
});
|
||||
|
||||
it('should add only partially compat to config if parts exist', () => {
|
||||
const content = `const baseConfig = require("../../eslint.config.js");
|
||||
const js = require("@eslint/js");
|
||||
module.exports = [
|
||||
...baseConfig,
|
||||
{
|
||||
files: [
|
||||
"my-lib/**/*.ts",
|
||||
"my-lib/**/*.tsx"
|
||||
],
|
||||
rules: {}
|
||||
},
|
||||
{ ignores: ["my-lib/.cache/**/*"] },
|
||||
];`;
|
||||
const result = addCompatToFlatConfig(content);
|
||||
expect(result).toMatchInlineSnapshot(`
|
||||
"const FlatCompat = require("@eslint/eslintrc");
|
||||
const baseConfig = require("../../eslint.config.js");
|
||||
const js = require("@eslint/js");
|
||||
|
||||
const compat = new FlatCompat({
|
||||
baseDirectory: __dirname,
|
||||
recommendedConfig: js.configs.recommended,
|
||||
});
|
||||
|
||||
module.exports = [
|
||||
...baseConfig,
|
||||
{
|
||||
files: [
|
||||
"my-lib/**/*.ts",
|
||||
"my-lib/**/*.tsx"
|
||||
],
|
||||
rules: {}
|
||||
},
|
||||
{ ignores: ["my-lib/.cache/**/*"] },
|
||||
];"
|
||||
`);
|
||||
});
|
||||
|
||||
it('should not add compat to config if exist', () => {
|
||||
const content = `const FlatCompat = require("@eslint/eslintrc");
|
||||
const baseConfig = require("../../eslint.config.js");
|
||||
const js = require("@eslint/js");
|
||||
|
||||
const compat = new FlatCompat({
|
||||
baseDirectory: __dirname,
|
||||
recommendedConfig: js.configs.recommended,
|
||||
});
|
||||
|
||||
module.exports = [
|
||||
...baseConfig,
|
||||
{
|
||||
files: [
|
||||
"my-lib/**/*.ts",
|
||||
"my-lib/**/*.tsx"
|
||||
],
|
||||
rules: {}
|
||||
},
|
||||
{ ignores: ["my-lib/.cache/**/*"] },
|
||||
];`;
|
||||
const result = addCompatToFlatConfig(content);
|
||||
expect(result).toEqual(content);
|
||||
});
|
||||
});
|
||||
|
||||
describe('removeOverridesFromLintConfig', () => {
|
||||
it('should remove all rules from config', () => {
|
||||
const content = `const FlatCompat = require("@eslint/eslintrc");
|
||||
const baseConfig = require("../../eslint.config.js");
|
||||
const js = require("@eslint/js");
|
||||
|
||||
const compat = new FlatCompat({
|
||||
baseDirectory: __dirname,
|
||||
recommendedConfig: js.configs.recommended,
|
||||
});
|
||||
|
||||
module.exports = [
|
||||
...baseConfig,
|
||||
{
|
||||
files: [
|
||||
"my-lib/**/*.ts",
|
||||
"my-lib/**/*.tsx"
|
||||
],
|
||||
rules: {}
|
||||
},
|
||||
...compat.config({ extends: ["plugin:@nx/typescript"] }).map(config => ({
|
||||
...config,
|
||||
files: [
|
||||
"**/*.ts",
|
||||
"**/*.tsx"
|
||||
],
|
||||
rules: {}
|
||||
})),
|
||||
...compat.config({ env: { jest: true } }).map(config => ({
|
||||
...config,
|
||||
files: [
|
||||
"**/*.spec.ts",
|
||||
"**/*.spec.tsx",
|
||||
"**/*.spec.js",
|
||||
"**/*.spec.jsx"
|
||||
],
|
||||
rules: {}
|
||||
})),
|
||||
{ ignores: ["my-lib/.cache/**/*"] },
|
||||
];`;
|
||||
const result = removeOverridesFromLintConfig(content);
|
||||
expect(result).toMatchInlineSnapshot(`
|
||||
"const FlatCompat = require("@eslint/eslintrc");
|
||||
const baseConfig = require("../../eslint.config.js");
|
||||
const js = require("@eslint/js");
|
||||
|
||||
const compat = new FlatCompat({
|
||||
baseDirectory: __dirname,
|
||||
recommendedConfig: js.configs.recommended,
|
||||
});
|
||||
|
||||
module.exports = [
|
||||
...baseConfig,
|
||||
{ ignores: ["my-lib/.cache/**/*"] },
|
||||
];"
|
||||
`);
|
||||
});
|
||||
|
||||
it('should remove all rules from starting with first', () => {
|
||||
const content = `const baseConfig = require("../../eslint.config.js");
|
||||
|
||||
module.exports = [
|
||||
{
|
||||
files: [
|
||||
"my-lib/**/*.ts",
|
||||
"my-lib/**/*.tsx"
|
||||
],
|
||||
rules: {}
|
||||
},
|
||||
...compat.config({ extends: ["plugin:@nx/typescript"] }).map(config => ({
|
||||
...config,
|
||||
files: [
|
||||
"**/*.ts",
|
||||
"**/*.tsx"
|
||||
],
|
||||
rules: {}
|
||||
})),
|
||||
...compat.config({ env: { jest: true } }).map(config => ({
|
||||
...config,
|
||||
files: [
|
||||
"**/*.spec.ts",
|
||||
"**/*.spec.tsx",
|
||||
"**/*.spec.js",
|
||||
"**/*.spec.jsx"
|
||||
],
|
||||
rules: {}
|
||||
}))
|
||||
];`;
|
||||
const result = removeOverridesFromLintConfig(content);
|
||||
expect(result).toMatchInlineSnapshot(`
|
||||
"const baseConfig = require("../../eslint.config.js");
|
||||
|
||||
module.exports = [
|
||||
];"
|
||||
`);
|
||||
});
|
||||
});
|
||||
|
||||
describe('replaceOverride', () => {
|
||||
it('should find and replace rules in override', () => {
|
||||
const content = `const baseConfig = require("../../eslint.config.js");
|
||||
|
||||
module.exports = [
|
||||
{
|
||||
files: [
|
||||
"my-lib/**/*.ts",
|
||||
"my-lib/**/*.tsx"
|
||||
],
|
||||
rules: {
|
||||
'my-ts-rule': 'error'
|
||||
}
|
||||
},
|
||||
{
|
||||
files: [
|
||||
"my-lib/**/*.ts",
|
||||
"my-lib/**/*.js"
|
||||
],
|
||||
rules: {}
|
||||
},
|
||||
{
|
||||
files: [
|
||||
"my-lib/**/*.js",
|
||||
"my-lib/**/*.jsx"
|
||||
],
|
||||
rules: {
|
||||
'my-js-rule': 'error'
|
||||
}
|
||||
},
|
||||
];`;
|
||||
|
||||
const result = replaceOverride(
|
||||
content,
|
||||
'my-lib',
|
||||
(o) => o.files.includes('my-lib/**/*.ts'),
|
||||
(o) => ({
|
||||
...o,
|
||||
rules: {
|
||||
'my-rule': 'error',
|
||||
},
|
||||
})
|
||||
);
|
||||
expect(result).toMatchInlineSnapshot(`
|
||||
"const baseConfig = require("../../eslint.config.js");
|
||||
|
||||
module.exports = [
|
||||
{
|
||||
"files": [
|
||||
"my-lib/**/*.ts",
|
||||
"my-lib/**/*.tsx"
|
||||
],
|
||||
"rules": {
|
||||
"my-rule": "error"
|
||||
}
|
||||
},
|
||||
{
|
||||
"files": [
|
||||
"my-lib/**/*.ts",
|
||||
"my-lib/**/*.js"
|
||||
],
|
||||
"rules": {
|
||||
"my-rule": "error"
|
||||
}
|
||||
},
|
||||
{
|
||||
files: [
|
||||
"my-lib/**/*.js",
|
||||
"my-lib/**/*.jsx"
|
||||
],
|
||||
rules: {
|
||||
'my-js-rule': 'error'
|
||||
}
|
||||
},
|
||||
];"
|
||||
`);
|
||||
});
|
||||
|
||||
it('should append rules in override', () => {
|
||||
const content = `const baseConfig = require("../../eslint.config.js");
|
||||
|
||||
module.exports = [
|
||||
{
|
||||
files: [
|
||||
"my-lib/**/*.ts",
|
||||
"my-lib/**/*.tsx"
|
||||
],
|
||||
rules: {
|
||||
'my-ts-rule': 'error'
|
||||
}
|
||||
},
|
||||
{
|
||||
files: [
|
||||
"my-lib/**/*.js",
|
||||
"my-lib/**/*.jsx"
|
||||
],
|
||||
rules: {
|
||||
'my-js-rule': 'error'
|
||||
}
|
||||
},
|
||||
];`;
|
||||
|
||||
const result = replaceOverride(
|
||||
content,
|
||||
'my-lib',
|
||||
(o) => o.files.includes('my-lib/**/*.ts'),
|
||||
(o) => ({
|
||||
...o,
|
||||
rules: {
|
||||
...o.rules,
|
||||
'my-new-rule': 'error',
|
||||
},
|
||||
})
|
||||
);
|
||||
expect(result).toMatchInlineSnapshot(`
|
||||
"const baseConfig = require("../../eslint.config.js");
|
||||
|
||||
module.exports = [
|
||||
{
|
||||
"files": [
|
||||
"my-lib/**/*.ts",
|
||||
"my-lib/**/*.tsx"
|
||||
],
|
||||
"rules": {
|
||||
"my-ts-rule": "error",
|
||||
"my-new-rule": "error"
|
||||
}
|
||||
},
|
||||
{
|
||||
files: [
|
||||
"my-lib/**/*.js",
|
||||
"my-lib/**/*.jsx"
|
||||
],
|
||||
rules: {
|
||||
'my-js-rule': 'error'
|
||||
}
|
||||
},
|
||||
];"
|
||||
`);
|
||||
});
|
||||
|
||||
it('should work for compat overrides', () => {
|
||||
const content = `const baseConfig = require("../../eslint.config.js");
|
||||
|
||||
module.exports = [
|
||||
...compat.config({ extends: ["plugin:@nx/typescript"] }).map(config => ({
|
||||
...config,
|
||||
files: [
|
||||
"my-lib/**/*.ts",
|
||||
"my-lib/**/*.tsx"
|
||||
],
|
||||
rules: {
|
||||
'my-ts-rule': 'error'
|
||||
}
|
||||
}),
|
||||
];`;
|
||||
|
||||
const result = replaceOverride(
|
||||
content,
|
||||
'my-lib',
|
||||
(o) => o.files.includes('my-lib/**/*.ts'),
|
||||
(o) => ({
|
||||
...o,
|
||||
rules: {
|
||||
...o.rules,
|
||||
'my-new-rule': 'error',
|
||||
},
|
||||
})
|
||||
);
|
||||
expect(result).toMatchInlineSnapshot(`
|
||||
"const baseConfig = require("../../eslint.config.js");
|
||||
|
||||
module.exports = [
|
||||
...compat.config({ extends: ["plugin:@nx/typescript"] }).map(config => ({
|
||||
...config,
|
||||
"files": [
|
||||
"my-lib/**/*.ts",
|
||||
"my-lib/**/*.tsx"
|
||||
],
|
||||
"rules": {
|
||||
"my-ts-rule": "error",
|
||||
"my-new-rule": "error"
|
||||
}
|
||||
}),
|
||||
];"
|
||||
`);
|
||||
});
|
||||
});
|
||||
|
||||
describe('removePlugin', () => {
|
||||
it('should remove plugins from config', () => {
|
||||
const content = `const { FlatCompat } = require("@eslint/eslintrc");
|
||||
const nxEslintPlugin = require("@nx/eslint-plugin");
|
||||
const js = require("@eslint/js");
|
||||
const compat = new FlatCompat({
|
||||
baseDirectory: __dirname,
|
||||
recommendedConfig: js.configs.recommended,
|
||||
});
|
||||
module.exports = [
|
||||
{ plugins: { "@nx": nxEslintPlugin } },
|
||||
{ ignores: ["src/ignore/to/keep.ts"] },
|
||||
{ ignores: ["something/else"] }
|
||||
];`;
|
||||
|
||||
const result = removePlugin(content, '@nx', '@nx/eslint-plugin');
|
||||
expect(result).toMatchInlineSnapshot(`
|
||||
"const { FlatCompat } = require("@eslint/eslintrc");
|
||||
const js = require("@eslint/js");
|
||||
const compat = new FlatCompat({
|
||||
baseDirectory: __dirname,
|
||||
recommendedConfig: js.configs.recommended,
|
||||
});
|
||||
module.exports = [
|
||||
{ ignores: ["src/ignore/to/keep.ts"] },
|
||||
{ ignores: ["something/else"] }
|
||||
];"
|
||||
`);
|
||||
});
|
||||
|
||||
it('should remove single plugin from config', () => {
|
||||
const content = `const { FlatCompat } = require("@eslint/eslintrc");
|
||||
const nxEslintPlugin = require("@nx/eslint-plugin");
|
||||
const otherPlugin = require("other/eslint-plugin");
|
||||
const js = require("@eslint/js");
|
||||
const compat = new FlatCompat({
|
||||
baseDirectory: __dirname,
|
||||
recommendedConfig: js.configs.recommended,
|
||||
});
|
||||
module.exports = [
|
||||
{ plugins: { "@nx": nxEslintPlugin, "@other": otherPlugin } },
|
||||
{ ignores: ["src/ignore/to/keep.ts"] },
|
||||
{ ignores: ["something/else"] }
|
||||
];`;
|
||||
|
||||
const result = removePlugin(content, '@nx', '@nx/eslint-plugin');
|
||||
expect(result).toMatchInlineSnapshot(`
|
||||
"const { FlatCompat } = require("@eslint/eslintrc");
|
||||
const otherPlugin = require("other/eslint-plugin");
|
||||
const js = require("@eslint/js");
|
||||
const compat = new FlatCompat({
|
||||
baseDirectory: __dirname,
|
||||
recommendedConfig: js.configs.recommended,
|
||||
});
|
||||
module.exports = [
|
||||
{ plugins: { "@other": otherPlugin } },
|
||||
{ ignores: ["src/ignore/to/keep.ts"] },
|
||||
{ ignores: ["something/else"] }
|
||||
];"
|
||||
`);
|
||||
});
|
||||
|
||||
it('should leave other properties in config', () => {
|
||||
const content = `const { FlatCompat } = require("@eslint/eslintrc");
|
||||
const nxEslintPlugin = require("@nx/eslint-plugin");
|
||||
const js = require("@eslint/js");
|
||||
const compat = new FlatCompat({
|
||||
baseDirectory: __dirname,
|
||||
recommendedConfig: js.configs.recommended,
|
||||
});
|
||||
module.exports = [
|
||||
{ plugins: { "@nx": nxEslintPlugin }, rules: {} },
|
||||
{ ignores: ["src/ignore/to/keep.ts"] },
|
||||
{ ignores: ["something/else"] }
|
||||
];`;
|
||||
|
||||
const result = removePlugin(content, '@nx', '@nx/eslint-plugin');
|
||||
expect(result).toMatchInlineSnapshot(`
|
||||
"const { FlatCompat } = require("@eslint/eslintrc");
|
||||
const js = require("@eslint/js");
|
||||
const compat = new FlatCompat({
|
||||
baseDirectory: __dirname,
|
||||
recommendedConfig: js.configs.recommended,
|
||||
});
|
||||
module.exports = [
|
||||
{ rules: {} },
|
||||
{ ignores: ["src/ignore/to/keep.ts"] },
|
||||
{ ignores: ["something/else"] }
|
||||
];"
|
||||
`);
|
||||
});
|
||||
|
||||
it('should remove single plugin from config array', () => {
|
||||
const content = `const { FlatCompat } = require("@eslint/eslintrc");
|
||||
const nxEslintPlugin = require("@nx/eslint-plugin");
|
||||
const js = require("@eslint/js");
|
||||
const compat = new FlatCompat({
|
||||
baseDirectory: __dirname,
|
||||
recommendedConfig: js.configs.recommended,
|
||||
});
|
||||
module.exports = [
|
||||
{ plugins: ["@nx", "something-else"] },
|
||||
{ ignores: ["src/ignore/to/keep.ts"] },
|
||||
{ ignores: ["something/else"] }
|
||||
];`;
|
||||
|
||||
const result = removePlugin(content, '@nx', '@nx/eslint-plugin');
|
||||
expect(result).toMatchInlineSnapshot(`
|
||||
"const { FlatCompat } = require("@eslint/eslintrc");
|
||||
const js = require("@eslint/js");
|
||||
const compat = new FlatCompat({
|
||||
baseDirectory: __dirname,
|
||||
recommendedConfig: js.configs.recommended,
|
||||
});
|
||||
module.exports = [
|
||||
{ plugins:["something-else"] },
|
||||
{ ignores: ["src/ignore/to/keep.ts"] },
|
||||
{ ignores: ["something/else"] }
|
||||
];"
|
||||
`);
|
||||
});
|
||||
|
||||
it('should leave other fields in the object', () => {
|
||||
const content = `const { FlatCompat } = require("@eslint/eslintrc");
|
||||
const nxEslintPlugin = require("@nx/eslint-plugin");
|
||||
const js = require("@eslint/js");
|
||||
const compat = new FlatCompat({
|
||||
baseDirectory: __dirname,
|
||||
recommendedConfig: js.configs.recommended,
|
||||
});
|
||||
module.exports = [
|
||||
{ plugins: ["@nx"], rules: { } },
|
||||
{ ignores: ["src/ignore/to/keep.ts"] },
|
||||
{ ignores: ["something/else"] }
|
||||
];`;
|
||||
|
||||
const result = removePlugin(content, '@nx', '@nx/eslint-plugin');
|
||||
expect(result).toMatchInlineSnapshot(`
|
||||
"const { FlatCompat } = require("@eslint/eslintrc");
|
||||
const js = require("@eslint/js");
|
||||
const compat = new FlatCompat({
|
||||
baseDirectory: __dirname,
|
||||
recommendedConfig: js.configs.recommended,
|
||||
});
|
||||
module.exports = [
|
||||
{ rules: { } },
|
||||
{ ignores: ["src/ignore/to/keep.ts"] },
|
||||
{ ignores: ["something/else"] }
|
||||
];"
|
||||
`);
|
||||
});
|
||||
|
||||
it('should remove entire plugin when array with single element', () => {
|
||||
const content = `const { FlatCompat } = require("@eslint/eslintrc");
|
||||
const nxEslintPlugin = require("@nx/eslint-plugin");
|
||||
const js = require("@eslint/js");
|
||||
const compat = new FlatCompat({
|
||||
baseDirectory: __dirname,
|
||||
recommendedConfig: js.configs.recommended,
|
||||
});
|
||||
module.exports = [
|
||||
{ plugins: ["@nx"] },
|
||||
{ ignores: ["src/ignore/to/keep.ts"] },
|
||||
{ ignores: ["something/else"] }
|
||||
];`;
|
||||
|
||||
const result = removePlugin(content, '@nx', '@nx/eslint-plugin');
|
||||
expect(result).toMatchInlineSnapshot(`
|
||||
"const { FlatCompat } = require("@eslint/eslintrc");
|
||||
const js = require("@eslint/js");
|
||||
const compat = new FlatCompat({
|
||||
baseDirectory: __dirname,
|
||||
recommendedConfig: js.configs.recommended,
|
||||
});
|
||||
module.exports = [
|
||||
{ ignores: ["src/ignore/to/keep.ts"] },
|
||||
{ ignores: ["something/else"] }
|
||||
];"
|
||||
`);
|
||||
});
|
||||
});
|
||||
|
||||
describe('removeCompatExtends', () => {
|
||||
it('should remove compat extends from config', () => {
|
||||
const content = `const { FlatCompat } = require("@eslint/eslintrc");
|
||||
const nxEslintPlugin = require("@nx/eslint-plugin");
|
||||
const js = require("@eslint/js");
|
||||
const compat = new FlatCompat({
|
||||
baseDirectory: __dirname,
|
||||
recommendedConfig: js.configs.recommended,
|
||||
});
|
||||
module.exports = [
|
||||
{ plugins: { "@nx": nxEslintPlugin } },
|
||||
...compat.config({ extends: ["plugin:@nx/typescript"] }).map(config => ({
|
||||
...config,
|
||||
files: ['*.ts', '*.tsx', '*.js', '*.jsx'],
|
||||
rules: {}
|
||||
})),
|
||||
{ ignores: ["src/ignore/to/keep.ts"] },
|
||||
...compat.config({ extends: ["plugin:@nrwl/javascript"] }).map(config => ({
|
||||
files: ['*.js', '*.jsx'],
|
||||
...config,
|
||||
rules: {}
|
||||
}))
|
||||
];`;
|
||||
|
||||
const result = removeCompatExtends(content, [
|
||||
'plugin:@nx/typescript',
|
||||
'plugin:@nx/javascript',
|
||||
'plugin:@nrwl/typescript',
|
||||
'plugin:@nrwl/javascript',
|
||||
]);
|
||||
expect(result).toMatchInlineSnapshot(`
|
||||
"const { FlatCompat } = require("@eslint/eslintrc");
|
||||
const nxEslintPlugin = require("@nx/eslint-plugin");
|
||||
const js = require("@eslint/js");
|
||||
const compat = new FlatCompat({
|
||||
baseDirectory: __dirname,
|
||||
recommendedConfig: js.configs.recommended,
|
||||
});
|
||||
module.exports = [
|
||||
{ plugins: { "@nx": nxEslintPlugin } },
|
||||
{
|
||||
files: ['*.ts', '*.tsx', '*.js', '*.jsx'],
|
||||
rules: {}
|
||||
},
|
||||
{ ignores: ["src/ignore/to/keep.ts"] },
|
||||
{
|
||||
files: ['*.js', '*.jsx'],
|
||||
rules: {}
|
||||
}
|
||||
];"
|
||||
`);
|
||||
});
|
||||
});
|
||||
});
|
||||
886
packages/linter/src/generators/utils/flat-config/ast-utils.ts
Normal file
886
packages/linter/src/generators/utils/flat-config/ast-utils.ts
Normal file
@ -0,0 +1,886 @@
|
||||
import {
|
||||
ChangeType,
|
||||
StringChange,
|
||||
applyChangesToString,
|
||||
joinPathFragments,
|
||||
} from '@nx/devkit';
|
||||
import { Linter } from 'eslint';
|
||||
import * as ts from 'typescript';
|
||||
|
||||
/**
|
||||
* Remove all overrides from the config file
|
||||
*/
|
||||
export function removeOverridesFromLintConfig(content: string): string {
|
||||
const source = ts.createSourceFile(
|
||||
'',
|
||||
content,
|
||||
ts.ScriptTarget.Latest,
|
||||
true,
|
||||
ts.ScriptKind.JS
|
||||
);
|
||||
|
||||
const exportsArray = findAllBlocks(source);
|
||||
if (!exportsArray) {
|
||||
return content;
|
||||
}
|
||||
|
||||
const changes: StringChange[] = [];
|
||||
exportsArray.forEach((node, i) => {
|
||||
if (isOverride(node)) {
|
||||
const commaOffset =
|
||||
i < exportsArray.length - 1 || exportsArray.hasTrailingComma ? 1 : 0;
|
||||
changes.push({
|
||||
type: ChangeType.Delete,
|
||||
start: node.pos,
|
||||
length: node.end - node.pos + commaOffset,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return applyChangesToString(content, changes);
|
||||
}
|
||||
|
||||
function findAllBlocks(source: ts.SourceFile): ts.NodeArray<ts.Node> {
|
||||
return ts.forEachChild(source, function analyze(node) {
|
||||
if (
|
||||
ts.isExpressionStatement(node) &&
|
||||
ts.isBinaryExpression(node.expression) &&
|
||||
node.expression.left.getText() === 'module.exports' &&
|
||||
ts.isArrayLiteralExpression(node.expression.right)
|
||||
) {
|
||||
return node.expression.right.elements;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function isOverride(node: ts.Node): boolean {
|
||||
return (
|
||||
(ts.isObjectLiteralExpression(node) &&
|
||||
node.properties.some((p) => p.name.getText() === 'files')) ||
|
||||
// detect ...compat.config(...).map(...)
|
||||
(ts.isSpreadElement(node) &&
|
||||
ts.isCallExpression(node.expression) &&
|
||||
ts.isPropertyAccessExpression(node.expression.expression) &&
|
||||
ts.isArrowFunction(node.expression.arguments[0]) &&
|
||||
ts.isParenthesizedExpression(node.expression.arguments[0].body))
|
||||
);
|
||||
}
|
||||
|
||||
export function hasOverride(
|
||||
content: string,
|
||||
lookup: (override: Linter.ConfigOverride<Linter.RulesRecord>) => boolean
|
||||
): boolean {
|
||||
const source = ts.createSourceFile(
|
||||
'',
|
||||
content,
|
||||
ts.ScriptTarget.Latest,
|
||||
true,
|
||||
ts.ScriptKind.JS
|
||||
);
|
||||
const exportsArray = findAllBlocks(source);
|
||||
if (!exportsArray) {
|
||||
return false;
|
||||
}
|
||||
for (const node of exportsArray) {
|
||||
if (isOverride(node)) {
|
||||
let objSource;
|
||||
if (ts.isObjectLiteralExpression(node)) {
|
||||
objSource = node.getFullText();
|
||||
} else {
|
||||
const fullNodeText =
|
||||
node['expression'].arguments[0].body.expression.getFullText();
|
||||
// strip any spread elements
|
||||
objSource = fullNodeText.replace(/\s*\.\.\.[a-zA-Z0-9_]+,?\n?/, '');
|
||||
}
|
||||
const data = JSON.parse(
|
||||
objSource
|
||||
// ensure property names have double quotes so that JSON.parse works
|
||||
.replace(/'/g, '"')
|
||||
.replace(/\s([a-zA-Z0-9_]+)\s*:/g, ' "$1": ')
|
||||
);
|
||||
if (lookup(data)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
const STRIP_SPREAD_ELEMENTS = /\s*\.\.\.[a-zA-Z0-9_]+,?\n?/g;
|
||||
|
||||
function parseTextToJson(text: string): any {
|
||||
return JSON.parse(
|
||||
text
|
||||
// ensure property names have double quotes so that JSON.parse works
|
||||
.replace(/'/g, '"')
|
||||
.replace(/\s([a-zA-Z0-9_]+)\s*:/g, ' "$1": ')
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds an override matching the lookup function and applies the update function to it
|
||||
*/
|
||||
export function replaceOverride(
|
||||
content: string,
|
||||
root: string,
|
||||
lookup: (override: Linter.ConfigOverride<Linter.RulesRecord>) => boolean,
|
||||
update: (
|
||||
override: Linter.ConfigOverride<Linter.RulesRecord>
|
||||
) => Linter.ConfigOverride<Linter.RulesRecord>
|
||||
): string {
|
||||
const source = ts.createSourceFile(
|
||||
'',
|
||||
content,
|
||||
ts.ScriptTarget.Latest,
|
||||
true,
|
||||
ts.ScriptKind.JS
|
||||
);
|
||||
const exportsArray = findAllBlocks(source);
|
||||
if (!exportsArray) {
|
||||
return content;
|
||||
}
|
||||
const changes: StringChange[] = [];
|
||||
exportsArray.forEach((node) => {
|
||||
if (isOverride(node)) {
|
||||
let objSource;
|
||||
let start, end;
|
||||
if (ts.isObjectLiteralExpression(node)) {
|
||||
objSource = node.getFullText();
|
||||
start = node.properties.pos + 1; // keep leading line break
|
||||
end = node.properties.end;
|
||||
} else {
|
||||
const fullNodeText =
|
||||
node['expression'].arguments[0].body.expression.getFullText();
|
||||
// strip any spread elements
|
||||
objSource = fullNodeText.replace(STRIP_SPREAD_ELEMENTS, '');
|
||||
start =
|
||||
node['expression'].arguments[0].body.expression.properties.pos +
|
||||
(fullNodeText.length - objSource.length);
|
||||
end = node['expression'].arguments[0].body.expression.properties.end;
|
||||
}
|
||||
const data = parseTextToJson(objSource);
|
||||
if (lookup(data)) {
|
||||
changes.push({
|
||||
type: ChangeType.Delete,
|
||||
start,
|
||||
length: end - start,
|
||||
});
|
||||
const updatedData = update(data);
|
||||
mapFilePaths(updatedData, root);
|
||||
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
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return applyChangesToString(content, changes);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adding require statement to the top of the file
|
||||
*/
|
||||
export function addImportToFlatConfig(
|
||||
content: string,
|
||||
variable: string | string[],
|
||||
imp: string
|
||||
): string {
|
||||
const printer = ts.createPrinter({ newLine: ts.NewLineKind.LineFeed });
|
||||
const source = ts.createSourceFile(
|
||||
'',
|
||||
content,
|
||||
ts.ScriptTarget.Latest,
|
||||
true,
|
||||
ts.ScriptKind.JS
|
||||
);
|
||||
|
||||
const foundBindingVars: ts.NodeArray<ts.BindingElement> = ts.forEachChild(
|
||||
source,
|
||||
function analyze(node) {
|
||||
// we can only combine object binding patterns
|
||||
if (!Array.isArray(variable)) {
|
||||
return;
|
||||
}
|
||||
if (
|
||||
ts.isVariableStatement(node) &&
|
||||
ts.isVariableDeclaration(node.declarationList.declarations[0]) &&
|
||||
ts.isObjectBindingPattern(node.declarationList.declarations[0].name) &&
|
||||
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
|
||||
) {
|
||||
return node.declarationList.declarations[0].name.elements;
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
if (foundBindingVars && Array.isArray(variable)) {
|
||||
const newVariables = variable.filter(
|
||||
(v) => !foundBindingVars.some((fv) => v === fv.name.getText())
|
||||
);
|
||||
if (newVariables.length === 0) {
|
||||
return content;
|
||||
}
|
||||
const isMultiLine = foundBindingVars.hasTrailingComma;
|
||||
const pos = foundBindingVars.end;
|
||||
const nodes = ts.factory.createNodeArray(
|
||||
newVariables.map((v) =>
|
||||
ts.factory.createBindingElement(undefined, undefined, v)
|
||||
)
|
||||
);
|
||||
const insert = printer.printList(
|
||||
ts.ListFormat.ObjectBindingPatternElements,
|
||||
nodes,
|
||||
source
|
||||
);
|
||||
return applyChangesToString(content, [
|
||||
{
|
||||
type: ChangeType.Insert,
|
||||
index: pos,
|
||||
text: isMultiLine ? `,\n${insert}` : `,${insert}`,
|
||||
},
|
||||
]);
|
||||
}
|
||||
|
||||
const hasSameIdentifierVar: boolean = ts.forEachChild(
|
||||
source,
|
||||
function analyze(node) {
|
||||
// we are searching for a single variable
|
||||
if (Array.isArray(variable)) {
|
||||
return;
|
||||
}
|
||||
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
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
if (hasSameIdentifierVar) {
|
||||
return content;
|
||||
}
|
||||
|
||||
// the import was not found, create a new one
|
||||
const requireStatement = generateRequire(
|
||||
typeof variable === 'string'
|
||||
? variable
|
||||
: ts.factory.createObjectBindingPattern(
|
||||
variable.map((v) =>
|
||||
ts.factory.createBindingElement(undefined, undefined, v)
|
||||
)
|
||||
),
|
||||
imp
|
||||
);
|
||||
const insert = printer.printNode(
|
||||
ts.EmitHint.Unspecified,
|
||||
requireStatement,
|
||||
source
|
||||
);
|
||||
return applyChangesToString(content, [
|
||||
{
|
||||
type: ChangeType.Insert,
|
||||
index: 0,
|
||||
text: `${insert}\n`,
|
||||
},
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Injects new ts.expression to the end of the module.exports array.
|
||||
*/
|
||||
export function addBlockToFlatConfigExport(
|
||||
content: string,
|
||||
config: ts.Expression | ts.SpreadElement,
|
||||
options: { insertAtTheEnd?: boolean; checkBaseConfig?: boolean } = {
|
||||
insertAtTheEnd: true,
|
||||
}
|
||||
): string {
|
||||
const printer = ts.createPrinter({ newLine: ts.NewLineKind.LineFeed });
|
||||
const source = ts.createSourceFile(
|
||||
'',
|
||||
content,
|
||||
ts.ScriptTarget.Latest,
|
||||
true,
|
||||
ts.ScriptKind.JS
|
||||
);
|
||||
|
||||
const exportsArray = ts.forEachChild(source, function analyze(node) {
|
||||
if (
|
||||
ts.isExpressionStatement(node) &&
|
||||
ts.isBinaryExpression(node.expression) &&
|
||||
node.expression.left.getText() === 'module.exports' &&
|
||||
ts.isArrayLiteralExpression(node.expression.right)
|
||||
) {
|
||||
return node.expression.right.elements;
|
||||
}
|
||||
});
|
||||
const insert = printer.printNode(ts.EmitHint.Expression, config, source);
|
||||
if (options.insertAtTheEnd) {
|
||||
return applyChangesToString(content, [
|
||||
{
|
||||
type: ChangeType.Insert,
|
||||
index: exportsArray[exportsArray.length - 1].end,
|
||||
text: `,\n${insert}`,
|
||||
},
|
||||
]);
|
||||
} else {
|
||||
return applyChangesToString(content, [
|
||||
{
|
||||
type: ChangeType.Insert,
|
||||
index: exportsArray[0].pos,
|
||||
text: `\n${insert},`,
|
||||
},
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
export function removePlugin(
|
||||
content: string,
|
||||
pluginName: string,
|
||||
pluginImport: string
|
||||
) {
|
||||
const source = ts.createSourceFile(
|
||||
'',
|
||||
content,
|
||||
ts.ScriptTarget.Latest,
|
||||
true,
|
||||
ts.ScriptKind.JS
|
||||
);
|
||||
const changes: StringChange[] = [];
|
||||
ts.forEachChild(source, function analyze(node) {
|
||||
if (
|
||||
ts.isVariableStatement(node) &&
|
||||
ts.isVariableDeclaration(node.declarationList.declarations[0]) &&
|
||||
ts.isCallExpression(node.declarationList.declarations[0].initializer) &&
|
||||
node.declarationList.declarations[0].initializer.arguments.length &&
|
||||
ts.isStringLiteral(
|
||||
node.declarationList.declarations[0].initializer.arguments[0]
|
||||
) &&
|
||||
node.declarationList.declarations[0].initializer.arguments[0].text ===
|
||||
pluginImport
|
||||
) {
|
||||
changes.push({
|
||||
type: ChangeType.Delete,
|
||||
start: node.pos,
|
||||
length: node.end - node.pos,
|
||||
});
|
||||
}
|
||||
});
|
||||
ts.forEachChild(source, function analyze(node) {
|
||||
if (
|
||||
ts.isExpressionStatement(node) &&
|
||||
ts.isBinaryExpression(node.expression) &&
|
||||
node.expression.left.getText() === 'module.exports' &&
|
||||
ts.isArrayLiteralExpression(node.expression.right)
|
||||
) {
|
||||
const blockElements = node.expression.right.elements;
|
||||
blockElements.forEach((element) => {
|
||||
if (ts.isObjectLiteralExpression(element)) {
|
||||
const pluginsElem = element.properties.find(
|
||||
(prop) => prop.name?.getText() === 'plugins'
|
||||
) as ts.PropertyAssignment;
|
||||
if (!pluginsElem) {
|
||||
return;
|
||||
}
|
||||
if (ts.isArrayLiteralExpression(pluginsElem.initializer)) {
|
||||
const pluginsArray = pluginsElem.initializer;
|
||||
const plugins = parseTextToJson(
|
||||
pluginsElem.initializer
|
||||
.getText()
|
||||
.replace(STRIP_SPREAD_ELEMENTS, '')
|
||||
);
|
||||
|
||||
if (plugins.length > 1) {
|
||||
changes.push({
|
||||
type: ChangeType.Delete,
|
||||
start: pluginsArray.pos,
|
||||
length: pluginsArray.end - pluginsArray.pos,
|
||||
});
|
||||
changes.push({
|
||||
type: ChangeType.Insert,
|
||||
index: pluginsArray.pos,
|
||||
text: JSON.stringify(plugins.filter((p) => p !== pluginName)),
|
||||
});
|
||||
} else {
|
||||
const keys = element.properties.map((prop) =>
|
||||
prop.name?.getText()
|
||||
);
|
||||
if (keys.length > 1) {
|
||||
const removeComma =
|
||||
keys.indexOf('plugins') < keys.length - 1 ||
|
||||
element.properties.hasTrailingComma;
|
||||
changes.push({
|
||||
type: ChangeType.Delete,
|
||||
start: pluginsElem.pos + (removeComma ? 1 : 0),
|
||||
length:
|
||||
pluginsElem.end - pluginsElem.pos + (removeComma ? 1 : 0),
|
||||
});
|
||||
} else {
|
||||
const removeComma =
|
||||
blockElements.indexOf(element) < blockElements.length - 1 ||
|
||||
blockElements.hasTrailingComma;
|
||||
changes.push({
|
||||
type: ChangeType.Delete,
|
||||
start: element.pos + (removeComma ? 1 : 0),
|
||||
length: element.end - element.pos + (removeComma ? 1 : 0),
|
||||
});
|
||||
}
|
||||
}
|
||||
} else if (ts.isObjectLiteralExpression(pluginsElem.initializer)) {
|
||||
const pluginsObj = pluginsElem.initializer;
|
||||
if (pluginsElem.initializer.properties.length > 1) {
|
||||
const plugin = pluginsObj.properties.find(
|
||||
(prop) => prop.name?.['text'] === pluginName
|
||||
) as ts.PropertyAssignment;
|
||||
const removeComma =
|
||||
pluginsObj.properties.indexOf(plugin) <
|
||||
pluginsObj.properties.length - 1 ||
|
||||
pluginsObj.properties.hasTrailingComma;
|
||||
changes.push({
|
||||
type: ChangeType.Delete,
|
||||
start: plugin.pos + (removeComma ? 1 : 0),
|
||||
length: plugin.end - plugin.pos + (removeComma ? 1 : 0),
|
||||
});
|
||||
} else {
|
||||
const keys = element.properties.map((prop) =>
|
||||
prop.name?.getText()
|
||||
);
|
||||
if (keys.length > 1) {
|
||||
const removeComma =
|
||||
keys.indexOf('plugins') < keys.length - 1 ||
|
||||
element.properties.hasTrailingComma;
|
||||
changes.push({
|
||||
type: ChangeType.Delete,
|
||||
start: pluginsElem.pos + (removeComma ? 1 : 0),
|
||||
length:
|
||||
pluginsElem.end - pluginsElem.pos + (removeComma ? 1 : 0),
|
||||
});
|
||||
} else {
|
||||
const removeComma =
|
||||
blockElements.indexOf(element) < blockElements.length - 1 ||
|
||||
blockElements.hasTrailingComma;
|
||||
changes.push({
|
||||
type: ChangeType.Delete,
|
||||
start: element.pos + (removeComma ? 1 : 0),
|
||||
length: element.end - element.pos + (removeComma ? 1 : 0),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
return applyChangesToString(content, changes);
|
||||
}
|
||||
|
||||
export function removeCompatExtends(
|
||||
content: string,
|
||||
compatExtends: string[]
|
||||
): string {
|
||||
const source = ts.createSourceFile(
|
||||
'',
|
||||
content,
|
||||
ts.ScriptTarget.Latest,
|
||||
true,
|
||||
ts.ScriptKind.JS
|
||||
);
|
||||
const changes: StringChange[] = [];
|
||||
findAllBlocks(source).forEach((node) => {
|
||||
if (
|
||||
ts.isSpreadElement(node) &&
|
||||
ts.isCallExpression(node.expression) &&
|
||||
ts.isArrowFunction(node.expression.arguments[0]) &&
|
||||
ts.isParenthesizedExpression(node.expression.arguments[0].body) &&
|
||||
ts.isPropertyAccessExpression(node.expression.expression) &&
|
||||
ts.isCallExpression(node.expression.expression.expression)
|
||||
) {
|
||||
const callExp = node.expression.expression.expression;
|
||||
if (
|
||||
((callExp.expression.getText() === 'compat.config' &&
|
||||
callExp.arguments[0].getText().includes('extends')) ||
|
||||
callExp.expression.getText() === 'compat.extends') &&
|
||||
compatExtends.some((ext) =>
|
||||
callExp.arguments[0].getText().includes(ext)
|
||||
)
|
||||
) {
|
||||
// remove the whole node
|
||||
changes.push({
|
||||
type: ChangeType.Delete,
|
||||
start: node.pos,
|
||||
length: node.end - node.pos,
|
||||
});
|
||||
// and replace it with new one
|
||||
const paramName =
|
||||
node.expression.arguments[0].parameters[0].name.getText();
|
||||
const body = node.expression.arguments[0].body.expression.getFullText();
|
||||
changes.push({
|
||||
type: ChangeType.Insert,
|
||||
index: node.pos,
|
||||
text:
|
||||
'\n' +
|
||||
body.replace(
|
||||
new RegExp('[ \t]s*...' + paramName + '[ \t]*,?\\s*', 'g'),
|
||||
''
|
||||
),
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return applyChangesToString(content, changes);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add plugins block to the top of the export blocks
|
||||
*/
|
||||
export function addPluginsToExportsBlock(
|
||||
content: string,
|
||||
plugins: { name: string; varName: string; imp: string }[]
|
||||
): string {
|
||||
const pluginsBlock = ts.factory.createObjectLiteralExpression(
|
||||
[
|
||||
ts.factory.createPropertyAssignment(
|
||||
'plugins',
|
||||
ts.factory.createObjectLiteralExpression(
|
||||
plugins.map(({ name, varName }) => {
|
||||
return ts.factory.createPropertyAssignment(
|
||||
ts.factory.createStringLiteral(name),
|
||||
ts.factory.createIdentifier(varName)
|
||||
);
|
||||
})
|
||||
)
|
||||
),
|
||||
],
|
||||
false
|
||||
);
|
||||
return addBlockToFlatConfigExport(content, pluginsBlock, {
|
||||
insertAtTheEnd: false,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds compat if missing to flat config
|
||||
*/
|
||||
export function addCompatToFlatConfig(content: string) {
|
||||
let result = content;
|
||||
result = addImportToFlatConfig(result, 'js', '@eslint/js');
|
||||
if (result.includes('const compat = new FlatCompat')) {
|
||||
return result;
|
||||
}
|
||||
result = addImportToFlatConfig(result, 'FlatCompat', '@eslint/eslintrc');
|
||||
const index = result.indexOf('module.exports');
|
||||
return applyChangesToString(result, [
|
||||
{
|
||||
type: ChangeType.Insert,
|
||||
index: index - 1,
|
||||
text: `${DEFAULT_FLAT_CONFIG}\n`,
|
||||
},
|
||||
]);
|
||||
}
|
||||
|
||||
const DEFAULT_FLAT_CONFIG = `
|
||||
const compat = new FlatCompat({
|
||||
baseDirectory: __dirname,
|
||||
recommendedConfig: js.configs.recommended,
|
||||
});
|
||||
`;
|
||||
|
||||
/**
|
||||
* Generate node list representing the imports and the exports blocks
|
||||
* Optionally add flat compat initialization
|
||||
*/
|
||||
export function createNodeList(
|
||||
importsMap: Map<string, string>,
|
||||
exportElements: ts.Expression[],
|
||||
isFlatCompatNeeded: boolean
|
||||
): 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]) => {
|
||||
importsList.push(generateRequire(varName, imp));
|
||||
});
|
||||
|
||||
return ts.factory.createNodeArray([
|
||||
// add plugin imports
|
||||
...importsList,
|
||||
ts.createSourceFile(
|
||||
'',
|
||||
isFlatCompatNeeded ? DEFAULT_FLAT_CONFIG : '',
|
||||
ts.ScriptTarget.Latest,
|
||||
false,
|
||||
ts.ScriptKind.JS
|
||||
),
|
||||
// creates:
|
||||
// module.exports = [ ... ];
|
||||
ts.factory.createExpressionStatement(
|
||||
ts.factory.createBinaryExpression(
|
||||
ts.factory.createPropertyAccessExpression(
|
||||
ts.factory.createIdentifier('module'),
|
||||
ts.factory.createIdentifier('exports')
|
||||
),
|
||||
ts.factory.createToken(ts.SyntaxKind.EqualsToken),
|
||||
ts.factory.createArrayLiteralExpression(exportElements, true)
|
||||
)
|
||||
),
|
||||
]);
|
||||
}
|
||||
|
||||
export function generateSpreadElement(name: string): ts.SpreadElement {
|
||||
return ts.factory.createSpreadElement(ts.factory.createIdentifier(name));
|
||||
}
|
||||
|
||||
export function generatePluginExtendsElement(
|
||||
plugins: string[]
|
||||
): ts.SpreadElement {
|
||||
return ts.factory.createSpreadElement(
|
||||
ts.factory.createCallExpression(
|
||||
ts.factory.createPropertyAccessExpression(
|
||||
ts.factory.createIdentifier('compat'),
|
||||
ts.factory.createIdentifier('extends')
|
||||
),
|
||||
undefined,
|
||||
plugins.map((plugin) => ts.factory.createStringLiteral(plugin))
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Stringifies TS nodes to file content string
|
||||
*/
|
||||
export function stringifyNodeList(
|
||||
nodes: ts.NodeArray<
|
||||
| ts.VariableStatement
|
||||
| ts.Identifier
|
||||
| ts.ExpressionStatement
|
||||
| ts.SourceFile
|
||||
>,
|
||||
root: string,
|
||||
fileName: string
|
||||
): string {
|
||||
const printer = ts.createPrinter({ newLine: ts.NewLineKind.LineFeed });
|
||||
const resultFile = ts.createSourceFile(
|
||||
joinPathFragments(root, fileName),
|
||||
'',
|
||||
ts.ScriptTarget.Latest,
|
||||
true,
|
||||
ts.ScriptKind.JS
|
||||
);
|
||||
return printer.printList(ts.ListFormat.MultiLine, nodes, resultFile);
|
||||
}
|
||||
|
||||
/**
|
||||
* generates AST require statement
|
||||
*/
|
||||
export function generateRequire(
|
||||
variableName: string | ts.ObjectBindingPattern,
|
||||
imp: string
|
||||
): ts.VariableStatement {
|
||||
return ts.factory.createVariableStatement(
|
||||
undefined,
|
||||
ts.factory.createVariableDeclarationList(
|
||||
[
|
||||
ts.factory.createVariableDeclaration(
|
||||
variableName,
|
||||
undefined,
|
||||
undefined,
|
||||
ts.factory.createCallExpression(
|
||||
ts.factory.createIdentifier('require'),
|
||||
undefined,
|
||||
[ts.factory.createStringLiteral(imp)]
|
||||
)
|
||||
),
|
||||
],
|
||||
ts.NodeFlags.Const
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates AST object or spread element based on JSON override object
|
||||
*/
|
||||
export function generateFlatOverride(
|
||||
override: Linter.ConfigOverride<Linter.RulesRecord>,
|
||||
root: string
|
||||
): ts.ObjectLiteralExpression | ts.SpreadElement {
|
||||
mapFilePaths(override, root);
|
||||
if (
|
||||
!override.env &&
|
||||
!override.extends &&
|
||||
!override.plugins &&
|
||||
!override.parser
|
||||
) {
|
||||
return generateAst(override);
|
||||
}
|
||||
const { files, excludedFiles, rules, ...rest } = override;
|
||||
|
||||
const objectLiteralElements: ts.ObjectLiteralElementLike[] = [
|
||||
ts.factory.createSpreadAssignment(ts.factory.createIdentifier('config')),
|
||||
];
|
||||
addTSObjectProperty(objectLiteralElements, 'files', files);
|
||||
addTSObjectProperty(objectLiteralElements, 'excludedFiles', excludedFiles);
|
||||
addTSObjectProperty(objectLiteralElements, 'rules', rules);
|
||||
|
||||
return ts.factory.createSpreadElement(
|
||||
ts.factory.createCallExpression(
|
||||
ts.factory.createPropertyAccessExpression(
|
||||
ts.factory.createCallExpression(
|
||||
ts.factory.createPropertyAccessExpression(
|
||||
ts.factory.createIdentifier('compat'),
|
||||
ts.factory.createIdentifier('config')
|
||||
),
|
||||
undefined,
|
||||
[generateAst(rest)]
|
||||
),
|
||||
ts.factory.createIdentifier('map')
|
||||
),
|
||||
undefined,
|
||||
[
|
||||
ts.factory.createArrowFunction(
|
||||
undefined,
|
||||
undefined,
|
||||
[
|
||||
ts.factory.createParameterDeclaration(
|
||||
undefined,
|
||||
undefined,
|
||||
'config'
|
||||
),
|
||||
],
|
||||
undefined,
|
||||
ts.factory.createToken(ts.SyntaxKind.EqualsGreaterThanToken),
|
||||
ts.factory.createParenthesizedExpression(
|
||||
ts.factory.createObjectLiteralExpression(
|
||||
objectLiteralElements,
|
||||
true
|
||||
)
|
||||
)
|
||||
),
|
||||
]
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export function mapFilePaths(
|
||||
override: Linter.ConfigOverride<Linter.RulesRecord>,
|
||||
root: string
|
||||
) {
|
||||
if (override.files) {
|
||||
override.files = Array.isArray(override.files)
|
||||
? override.files
|
||||
: [override.files];
|
||||
override.files = override.files.map((file) => mapFilePath(file, root));
|
||||
}
|
||||
if (override.excludedFiles) {
|
||||
override.excludedFiles = Array.isArray(override.excludedFiles)
|
||||
? override.excludedFiles
|
||||
: [override.excludedFiles];
|
||||
override.excludedFiles = override.excludedFiles.map((file) =>
|
||||
mapFilePath(file, root)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export function mapFilePath(filePath: string, root: string) {
|
||||
if (filePath.startsWith('!')) {
|
||||
const fileWithoutBang = filePath.slice(1);
|
||||
if (fileWithoutBang.startsWith('*.')) {
|
||||
return `!${joinPathFragments(root, '**', fileWithoutBang)}`;
|
||||
} else if (!fileWithoutBang.startsWith(root)) {
|
||||
return `!${joinPathFragments(root, fileWithoutBang)}`;
|
||||
}
|
||||
return filePath;
|
||||
}
|
||||
if (filePath.startsWith('*.')) {
|
||||
return joinPathFragments(root, '**', filePath);
|
||||
} else if (!filePath.startsWith(root)) {
|
||||
return joinPathFragments(root, filePath);
|
||||
}
|
||||
return filePath;
|
||||
}
|
||||
|
||||
function addTSObjectProperty(
|
||||
elements: ts.ObjectLiteralElementLike[],
|
||||
key: string,
|
||||
value: unknown
|
||||
) {
|
||||
if (value) {
|
||||
elements.push(ts.factory.createPropertyAssignment(key, generateAst(value)));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates an AST from a JSON-type input
|
||||
*/
|
||||
export function generateAst<T>(input: unknown): T {
|
||||
if (Array.isArray(input)) {
|
||||
return ts.factory.createArrayLiteralExpression(
|
||||
input.map((item) => generateAst<ts.Expression>(item)),
|
||||
input.length > 1 // multiline only if more than one item
|
||||
) as T;
|
||||
}
|
||||
if (input === null) {
|
||||
return ts.factory.createNull() as 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)
|
||||
)
|
||||
),
|
||||
Object.keys(input).length > 1 // multiline only if more than one property
|
||||
) as T;
|
||||
}
|
||||
if (typeof input === 'string') {
|
||||
return ts.factory.createStringLiteral(input) as T;
|
||||
}
|
||||
if (typeof input === 'number') {
|
||||
return ts.factory.createNumericLiteral(input) as T;
|
||||
}
|
||||
if (typeof input === 'boolean') {
|
||||
return (input ? ts.factory.createTrue() : ts.factory.createFalse()) as T;
|
||||
}
|
||||
// since we are parsing JSON, this should never happen
|
||||
throw new Error(`Unknown type: ${typeof input} `);
|
||||
}
|
||||
|
||||
function isValidKey(key: string): boolean {
|
||||
return /^[a-zA-Z0-9_]+$/.test(key);
|
||||
}
|
||||
@ -0,0 +1,31 @@
|
||||
import { joinPathFragments } from '@nx/devkit';
|
||||
import type { Linter } from 'eslint';
|
||||
|
||||
export function updateFiles(
|
||||
override: Linter.ConfigOverride<Linter.RulesRecord>,
|
||||
root: string
|
||||
) {
|
||||
if (override.files) {
|
||||
override.files = Array.isArray(override.files)
|
||||
? override.files
|
||||
: [override.files];
|
||||
override.files = override.files.map((file) => mapFilePath(file, root));
|
||||
}
|
||||
return override;
|
||||
}
|
||||
|
||||
function mapFilePath(filePath: string, root: string) {
|
||||
if (filePath.startsWith('!')) {
|
||||
const fileWithoutBang = filePath.slice(1);
|
||||
if (fileWithoutBang.startsWith('*.')) {
|
||||
return `!${joinPathFragments(root, '**', fileWithoutBang)}`;
|
||||
} else {
|
||||
return `!${joinPathFragments(root, fileWithoutBang)}`;
|
||||
}
|
||||
}
|
||||
if (filePath.startsWith('*.')) {
|
||||
return joinPathFragments(root, '**', filePath);
|
||||
} else {
|
||||
return joinPathFragments(root, filePath);
|
||||
}
|
||||
}
|
||||
@ -44,7 +44,7 @@ export async function lintWorkspaceRuleGenerator(
|
||||
|
||||
/**
|
||||
* Import the new rule into the workspace plugin index.ts and
|
||||
* register it ready for use in .eslintrc.json configs.
|
||||
* register it ready for use in lint configs.
|
||||
*/
|
||||
const pluginIndexPath = joinPathFragments(workspaceLintPluginDir, 'index.ts');
|
||||
const existingPluginIndexContents = tree.read(pluginIndexPath, 'utf-8');
|
||||
@ -106,7 +106,7 @@ export async function lintWorkspaceRuleGenerator(
|
||||
|
||||
await formatFiles(tree);
|
||||
|
||||
logger.info(`NX Reminder: Once you have finished writing your rule logic, you need to actually enable the rule within an appropriate .eslintrc.json in your workspace, for example:
|
||||
logger.info(`NX Reminder: Once you have finished writing your rule logic, you need to actually enable the rule within an appropriate ESLint config in your workspace, for example:
|
||||
|
||||
"rules": {
|
||||
"@nx/workspace/${options.name}": "error"
|
||||
|
||||
5
packages/linter/src/utils/flat-config.ts
Normal file
5
packages/linter/src/utils/flat-config.ts
Normal file
@ -0,0 +1,5 @@
|
||||
import { Tree } from '@nx/devkit';
|
||||
|
||||
export function useFlatConfig(tree: Tree): boolean {
|
||||
return tree.exists('eslint.config.js');
|
||||
}
|
||||
@ -573,9 +573,6 @@ describe('app', () => {
|
||||
const eslintJson = readJson(tree, '/apps/my-app/.eslintrc.json');
|
||||
expect(eslintJson).toMatchInlineSnapshot(`
|
||||
{
|
||||
"env": {
|
||||
"jest": true,
|
||||
},
|
||||
"extends": [
|
||||
"plugin:@nx/react-typescript",
|
||||
"next",
|
||||
@ -587,6 +584,14 @@ describe('app', () => {
|
||||
".next/**/*",
|
||||
],
|
||||
"overrides": [
|
||||
{
|
||||
"files": [
|
||||
"*.*",
|
||||
],
|
||||
"rules": {
|
||||
"@next/next/no-html-link-for-pages": "off",
|
||||
},
|
||||
},
|
||||
{
|
||||
"files": [
|
||||
"*.ts",
|
||||
@ -615,10 +620,18 @@ describe('app', () => {
|
||||
],
|
||||
"rules": {},
|
||||
},
|
||||
{
|
||||
"env": {
|
||||
"jest": true,
|
||||
},
|
||||
"files": [
|
||||
"*.spec.ts",
|
||||
"*.spec.tsx",
|
||||
"*.spec.js",
|
||||
"*.spec.jsx",
|
||||
],
|
||||
},
|
||||
],
|
||||
"rules": {
|
||||
"@next/next/no-html-link-for-pages": "off",
|
||||
},
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
181
packages/next/src/generators/application/lib/add-linting.spec.ts
Normal file
181
packages/next/src/generators/application/lib/add-linting.spec.ts
Normal file
@ -0,0 +1,181 @@
|
||||
import {
|
||||
ProjectConfiguration,
|
||||
Tree,
|
||||
addProjectConfiguration,
|
||||
readJson,
|
||||
} from '@nx/devkit';
|
||||
import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing';
|
||||
import { addLinting } from './add-linting';
|
||||
import { Linter } from '@nx/linter';
|
||||
import { NormalizedSchema } from './normalize-options';
|
||||
|
||||
describe('updateEslint', () => {
|
||||
let tree: Tree;
|
||||
let schema: NormalizedSchema;
|
||||
|
||||
beforeEach(async () => {
|
||||
schema = {
|
||||
projectName: 'my-app',
|
||||
appProjectRoot: 'apps/my-app',
|
||||
linter: Linter.EsLint,
|
||||
unitTestRunner: 'jest',
|
||||
e2eProjectName: 'my-app-e2e',
|
||||
e2eProjectRoot: 'apps/my-app-e2e',
|
||||
outputPath: 'dist/apps/my-app',
|
||||
name: 'my-app',
|
||||
parsedTags: [],
|
||||
fileName: 'index',
|
||||
e2eTestRunner: 'cypress',
|
||||
styledModule: null,
|
||||
};
|
||||
tree = createTreeWithEmptyWorkspace({ layout: 'apps-libs' });
|
||||
const project: ProjectConfiguration = {
|
||||
root: schema.appProjectRoot,
|
||||
sourceRoot: schema.appProjectRoot,
|
||||
projectType: 'application',
|
||||
targets: {},
|
||||
tags: schema.parsedTags,
|
||||
};
|
||||
|
||||
addProjectConfiguration(tree, schema.projectName, {
|
||||
...project,
|
||||
});
|
||||
});
|
||||
|
||||
it('should update the eslintrc config', async () => {
|
||||
tree.write('.eslintrc.json', JSON.stringify({ extends: ['some-config'] }));
|
||||
|
||||
await addLinting(tree, schema);
|
||||
|
||||
expect(readJson(tree, `${schema.appProjectRoot}/.eslintrc.json`))
|
||||
.toMatchInlineSnapshot(`
|
||||
{
|
||||
"extends": [
|
||||
"plugin:@nx/react-typescript",
|
||||
"next",
|
||||
"next/core-web-vitals",
|
||||
"../../.eslintrc.json",
|
||||
],
|
||||
"ignorePatterns": [
|
||||
"!**/*",
|
||||
".next/**/*",
|
||||
],
|
||||
"overrides": [
|
||||
{
|
||||
"files": [
|
||||
"*.*",
|
||||
],
|
||||
"rules": {
|
||||
"@next/next/no-html-link-for-pages": "off",
|
||||
},
|
||||
},
|
||||
{
|
||||
"files": [
|
||||
"*.ts",
|
||||
"*.tsx",
|
||||
"*.js",
|
||||
"*.jsx",
|
||||
],
|
||||
"rules": {
|
||||
"@next/next/no-html-link-for-pages": [
|
||||
"error",
|
||||
"apps/my-app/pages",
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
"files": [
|
||||
"*.ts",
|
||||
"*.tsx",
|
||||
],
|
||||
"rules": {},
|
||||
},
|
||||
{
|
||||
"files": [
|
||||
"*.js",
|
||||
"*.jsx",
|
||||
],
|
||||
"rules": {},
|
||||
},
|
||||
{
|
||||
"env": {
|
||||
"jest": true,
|
||||
},
|
||||
"files": [
|
||||
"*.spec.ts",
|
||||
"*.spec.tsx",
|
||||
"*.spec.js",
|
||||
"*.spec.jsx",
|
||||
],
|
||||
},
|
||||
],
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
it('should update the flat config', async () => {
|
||||
tree.write('eslint.config.js', `module.exports = []`);
|
||||
|
||||
await addLinting(tree, schema);
|
||||
|
||||
expect(tree.read(`${schema.appProjectRoot}/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 = [
|
||||
{
|
||||
files: ["apps/my-app/**/*.*"],
|
||||
rules: { "@next/next/no-html-link-for-pages": "off" }
|
||||
},
|
||||
...baseConfig,
|
||||
{
|
||||
"files": [
|
||||
"apps/my-app/**/*.ts",
|
||||
"apps/my-app/**/*.tsx",
|
||||
"apps/my-app/**/*.js",
|
||||
"apps/my-app/**/*.jsx"
|
||||
],
|
||||
"rules": {
|
||||
"@next/next/no-html-link-for-pages": [
|
||||
"error",
|
||||
"apps/my-app/pages"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
files: [
|
||||
"apps/my-app/**/*.ts",
|
||||
"apps/my-app/**/*.tsx"
|
||||
],
|
||||
rules: {}
|
||||
},
|
||||
{
|
||||
files: [
|
||||
"apps/my-app/**/*.js",
|
||||
"apps/my-app/**/*.jsx"
|
||||
],
|
||||
rules: {}
|
||||
},
|
||||
...compat.extends("plugin:@nx/react-typescript", "next", "next/core-web-vitals"),
|
||||
...compat.config({ env: { jest: true } }).map(config => ({
|
||||
...config,
|
||||
files: [
|
||||
"apps/my-app/**/*.spec.ts",
|
||||
"apps/my-app/**/*.spec.tsx",
|
||||
"apps/my-app/**/*.spec.js",
|
||||
"apps/my-app/**/*.spec.jsx"
|
||||
]
|
||||
})),
|
||||
{ ignores: ["apps/my-app/.next/**/*"] }
|
||||
];
|
||||
"
|
||||
`);
|
||||
});
|
||||
});
|
||||
@ -5,13 +5,16 @@ import {
|
||||
joinPathFragments,
|
||||
runTasksInSerial,
|
||||
Tree,
|
||||
updateJson,
|
||||
} from '@nx/devkit';
|
||||
import {
|
||||
extendReactEslintJson,
|
||||
extraEslintDependencies,
|
||||
} from '@nx/react/src/utils/lint';
|
||||
import { extraEslintDependencies } from '@nx/react/src/utils/lint';
|
||||
import { NormalizedSchema } from './normalize-options';
|
||||
import {
|
||||
addExtendsToLintConfig,
|
||||
addIgnoresToLintConfig,
|
||||
addOverrideToLintConfig,
|
||||
isEslintConfigSupported,
|
||||
updateOverrideInLintConfig,
|
||||
} from '@nx/linter/src/generators/utils/eslint-file';
|
||||
|
||||
export async function addLinting(
|
||||
host: Tree,
|
||||
@ -28,70 +31,56 @@ export async function addLinting(
|
||||
skipFormat: true,
|
||||
rootProject: options.rootProject,
|
||||
});
|
||||
if (options.linter === Linter.EsLint && isEslintConfigSupported(host)) {
|
||||
addExtendsToLintConfig(host, options.appProjectRoot, [
|
||||
'plugin:@nx/react-typescript',
|
||||
'next',
|
||||
'next/core-web-vitals',
|
||||
]);
|
||||
|
||||
if (options.linter === Linter.EsLint) {
|
||||
updateJson(
|
||||
// Turn off @next/next/no-html-link-for-pages since there is an issue with nextjs throwing linting errors
|
||||
// TODO(nicholas): remove after Vercel updates nextjs linter to only lint ["*.ts", "*.tsx", "*.js", "*.jsx"]
|
||||
addOverrideToLintConfig(
|
||||
host,
|
||||
joinPathFragments(options.appProjectRoot, '.eslintrc.json'),
|
||||
(json) => {
|
||||
json = extendReactEslintJson(json);
|
||||
|
||||
// Turn off @next/next/no-html-link-for-pages since there is an issue with nextjs throwing linting errors
|
||||
// TODO(nicholas): remove after Vercel updates nextjs linter to only lint ["*.ts", "*.tsx", "*.js", "*.jsx"]
|
||||
|
||||
json.ignorePatterns = [...json.ignorePatterns, '.next/**/*'];
|
||||
|
||||
json.rules = {
|
||||
options.appProjectRoot,
|
||||
{
|
||||
files: ['*.*'],
|
||||
rules: {
|
||||
'@next/next/no-html-link-for-pages': 'off',
|
||||
...json.rules,
|
||||
};
|
||||
|
||||
// Find the override that handles both TS and JS files.
|
||||
const commonOverride = json.overrides?.find((o) =>
|
||||
['*.ts', '*.tsx', '*.js', '*.jsx'].every((ext) =>
|
||||
o.files.includes(ext)
|
||||
)
|
||||
);
|
||||
if (commonOverride) {
|
||||
// Only set parserOptions.project if it already exists (defined by options.setParserOptionsProject)
|
||||
if (commonOverride.parserOptions?.project) {
|
||||
commonOverride.parserOptions.project = [
|
||||
`${options.appProjectRoot}/tsconfig(.*)?.json`,
|
||||
];
|
||||
}
|
||||
// Configure custom pages directory for next rule
|
||||
if (commonOverride.rules) {
|
||||
commonOverride.rules = {
|
||||
...commonOverride.rules,
|
||||
'@next/next/no-html-link-for-pages': [
|
||||
'error',
|
||||
`${options.appProjectRoot}/pages`,
|
||||
],
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
json.extends ??= [];
|
||||
if (typeof json.extends === 'string') {
|
||||
json.extends = [json.extends];
|
||||
}
|
||||
// add next.js configuration
|
||||
json.extends.unshift(...['next', 'next/core-web-vitals']);
|
||||
// remove nx/react plugin, as it conflicts with the next.js one
|
||||
json.extends = json.extends.filter(
|
||||
(name) =>
|
||||
name !== 'plugin:@nx/react' && name !== 'plugin:@nrwl/nx/react'
|
||||
);
|
||||
|
||||
json.extends.unshift('plugin:@nx/react-typescript');
|
||||
if (!json.env) {
|
||||
json.env = {};
|
||||
}
|
||||
json.env.jest = true;
|
||||
|
||||
return json;
|
||||
}
|
||||
},
|
||||
},
|
||||
{ insertAtTheEnd: false }
|
||||
);
|
||||
updateOverrideInLintConfig(
|
||||
host,
|
||||
options.appProjectRoot,
|
||||
(o) =>
|
||||
Array.isArray(o.files) &&
|
||||
o.files.some((f) => f.match(/\*\.ts$/)) &&
|
||||
o.files.some((f) => f.match(/\*\.tsx$/)) &&
|
||||
o.files.some((f) => f.match(/\*\.js$/)) &&
|
||||
o.files.some((f) => f.match(/\*\.jsx$/)),
|
||||
(o) => ({
|
||||
...o,
|
||||
rules: {
|
||||
...o.rules,
|
||||
'@next/next/no-html-link-for-pages': [
|
||||
'error',
|
||||
`${options.appProjectRoot}/pages`,
|
||||
],
|
||||
},
|
||||
})
|
||||
);
|
||||
// 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/**/*']);
|
||||
}
|
||||
|
||||
const installTask = addDependenciesToPackageJson(
|
||||
|
||||
@ -11,18 +11,21 @@ import {
|
||||
readProjectConfiguration,
|
||||
runTasksInSerial,
|
||||
Tree,
|
||||
updateJson,
|
||||
} from '@nx/devkit';
|
||||
import { determineProjectNameAndRootOptions } from '@nx/devkit/src/generators/project-name-and-root-utils';
|
||||
import { Linter, lintProjectGenerator } from '@nx/linter';
|
||||
import {
|
||||
globalJavaScriptOverrides,
|
||||
globalTypeScriptOverrides,
|
||||
javaScriptOverride,
|
||||
typeScriptOverride,
|
||||
} from '@nx/linter/src/generators/init/global-eslint-config';
|
||||
import * as path from 'path';
|
||||
import { join } from 'path';
|
||||
import { axiosVersion } from '../../utils/versions';
|
||||
import { Schema } from './schema';
|
||||
import {
|
||||
addPluginsToLintConfig,
|
||||
isEslintConfigSupported,
|
||||
replaceOverridesInLintConfig,
|
||||
} from '@nx/linter/src/generators/utils/eslint-file';
|
||||
|
||||
export async function e2eProjectGenerator(host: Tree, options: Schema) {
|
||||
return await e2eProjectGeneratorInternal(host, {
|
||||
@ -119,32 +122,13 @@ export async function e2eProjectGeneratorInternal(
|
||||
});
|
||||
tasks.push(linterTask);
|
||||
|
||||
updateJson(host, join(options.e2eProjectRoot, '.eslintrc.json'), (json) => {
|
||||
if (options.rootProject) {
|
||||
json.plugins = ['@nx'];
|
||||
json.extends = [];
|
||||
}
|
||||
json.overrides = [
|
||||
...(options.rootProject
|
||||
? [globalTypeScriptOverrides, globalJavaScriptOverrides]
|
||||
: []),
|
||||
/**
|
||||
* In order to ensure maximum efficiency when typescript-eslint generates TypeScript Programs
|
||||
* behind the scenes during lint runs, we need to make sure the project is configured to use its
|
||||
* own specific tsconfigs, and not fall back to the ones in the root of the workspace.
|
||||
*/
|
||||
{
|
||||
files: ['*.ts', '*.tsx', '*.js', '*.jsx'],
|
||||
/**
|
||||
* Having an empty rules object present makes it more obvious to the user where they would
|
||||
* extend things from if they needed to
|
||||
*/
|
||||
rules: {},
|
||||
},
|
||||
];
|
||||
|
||||
return json;
|
||||
});
|
||||
if (options.rootProject && isEslintConfigSupported(host)) {
|
||||
addPluginsToLintConfig(host, options.e2eProjectRoot, '@nx');
|
||||
replaceOverridesInLintConfig(host, options.e2eProjectRoot, [
|
||||
typeScriptOverride,
|
||||
javaScriptOverride,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
if (!options.skipFormat) {
|
||||
|
||||
@ -217,7 +217,10 @@ function projectHasKarmaConfig(
|
||||
function projectHasEslintConfig(
|
||||
project: AngularJsonProjectConfiguration
|
||||
): boolean {
|
||||
return fileExists(join(project.root, '.eslintrc.json'));
|
||||
return (
|
||||
fileExists(join(project.root, '.eslintrc.json')) ||
|
||||
fileExists(join(project.root, 'eslint.config.js'))
|
||||
);
|
||||
}
|
||||
|
||||
function replaceNgWithNxInPackageJsonScripts(repoRoot: string): void {
|
||||
|
||||
@ -5,11 +5,17 @@ import {
|
||||
readProjectConfiguration,
|
||||
runTasksInSerial,
|
||||
Tree,
|
||||
updateJson,
|
||||
} from '@nx/devkit';
|
||||
import { Linter, lintProjectGenerator } from '@nx/linter';
|
||||
import { globalJavaScriptOverrides } from '@nx/linter/src/generators/init/global-eslint-config';
|
||||
import { javaScriptOverride } from '@nx/linter/src/generators/init/global-eslint-config';
|
||||
import { eslintPluginPlaywrightVersion } from './versions';
|
||||
import {
|
||||
addExtendsToLintConfig,
|
||||
addOverrideToLintConfig,
|
||||
addPluginsToLintConfig,
|
||||
findEslintFile,
|
||||
isEslintConfigSupported,
|
||||
} from '@nx/linter/src/generators/utils/eslint-file';
|
||||
|
||||
export interface PlaywrightLinterOptions {
|
||||
project: string;
|
||||
@ -35,7 +41,8 @@ export async function addLinterToPlaywrightProject(
|
||||
const tasks: GeneratorCallback[] = [];
|
||||
const projectConfig = readProjectConfiguration(tree, options.project);
|
||||
|
||||
if (!tree.exists(joinPathFragments(projectConfig.root, '.eslintrc.json'))) {
|
||||
const eslintFile = findEslintFile(tree, projectConfig.root);
|
||||
if (!eslintFile) {
|
||||
tasks.push(
|
||||
await lintProjectGenerator(tree, {
|
||||
project: options.project,
|
||||
@ -66,38 +73,26 @@ export async function addLinterToPlaywrightProject(
|
||||
: () => {}
|
||||
);
|
||||
|
||||
updateJson(
|
||||
tree,
|
||||
joinPathFragments(projectConfig.root, '.eslintrc.json'),
|
||||
(json) => {
|
||||
if (options.rootProject) {
|
||||
json.plugins = ['@nx'];
|
||||
json.extends = ['plugin:playwright/recommended'];
|
||||
} else {
|
||||
json.extends = ['plugin:playwright/recommended', ...json.extends];
|
||||
}
|
||||
json.overrides ??= [];
|
||||
const globals = options.rootProject ? [globalJavaScriptOverrides] : [];
|
||||
const override = {
|
||||
files: ['*.ts', '*.tsx', '*.js', '*.jsx'],
|
||||
parserOptions: !options.setParserOptionsProject
|
||||
? undefined
|
||||
: {
|
||||
project: `${projectConfig.root}/tsconfig.*?.json`,
|
||||
},
|
||||
rules: {},
|
||||
};
|
||||
const palywrightFiles = [
|
||||
{
|
||||
...override,
|
||||
files: [`${options.directory}/**/*.{ts,js,tsx,jsx}`],
|
||||
},
|
||||
];
|
||||
json.overrides.push(...globals);
|
||||
json.overrides.push(...palywrightFiles);
|
||||
return json;
|
||||
if (isEslintConfigSupported(tree)) {
|
||||
addExtendsToLintConfig(
|
||||
tree,
|
||||
projectConfig.root,
|
||||
'plugin:playwright/recommended'
|
||||
);
|
||||
if (options.rootProject) {
|
||||
addPluginsToLintConfig(tree, projectConfig.root, '@nx');
|
||||
addOverrideToLintConfig(tree, projectConfig.root, javaScriptOverride);
|
||||
}
|
||||
);
|
||||
addOverrideToLintConfig(tree, projectConfig.root, {
|
||||
files: [`${options.directory}/**/*.{ts,js,tsx,jsx}`],
|
||||
parserOptions: !options.setParserOptionsProject
|
||||
? undefined
|
||||
: {
|
||||
project: `${projectConfig.root}/tsconfig.*?.json`,
|
||||
},
|
||||
rules: {},
|
||||
});
|
||||
}
|
||||
|
||||
return runTasksInSerial(...tasks);
|
||||
}
|
||||
|
||||
@ -8,18 +8,22 @@ import {
|
||||
readProjectConfiguration,
|
||||
TargetConfiguration,
|
||||
Tree,
|
||||
updateJson,
|
||||
updateProjectConfiguration,
|
||||
writeJson,
|
||||
} from '@nx/devkit';
|
||||
|
||||
import type { Linter as ESLint } from 'eslint';
|
||||
|
||||
import type { Schema as EsLintExecutorOptions } from '@nx/linter/src/executors/eslint/schema';
|
||||
|
||||
import { PluginLintChecksGeneratorSchema } from './schema';
|
||||
import { NX_PREFIX } from 'nx/src/utils/logger';
|
||||
import { PackageJson, readNxMigrateConfig } from 'nx/src/utils/package-json';
|
||||
import {
|
||||
addOverrideToLintConfig,
|
||||
isEslintConfigSupported,
|
||||
lintConfigHasOverride,
|
||||
updateOverrideInLintConfig,
|
||||
} from '@nx/linter/src/generators/utils/eslint-file';
|
||||
import { useFlatConfig } from '@nx/linter/src/utils/flat-config';
|
||||
|
||||
export default async function pluginLintCheckGenerator(
|
||||
host: Tree,
|
||||
@ -101,22 +105,19 @@ export function addMigrationJsonChecks(
|
||||
updateProjectConfiguration(host, options.projectName, projectConfiguration);
|
||||
|
||||
// Update project level eslintrc
|
||||
updateJson<ESLint.Config>(
|
||||
updateOverrideInLintConfig(
|
||||
host,
|
||||
`${projectConfiguration.root}/.eslintrc.json`,
|
||||
(c) => {
|
||||
const override = c.overrides.find(
|
||||
(o) =>
|
||||
Object.keys(o.rules ?? {})?.includes('@nx/nx-plugin-checks') ||
|
||||
Object.keys(o.rules ?? {})?.includes('@nrwl/nx/nx-plugin-checks')
|
||||
);
|
||||
if (
|
||||
Array.isArray(override?.files) &&
|
||||
!override.files.includes(relativeMigrationsJsonPath)
|
||||
) {
|
||||
override.files.push(relativeMigrationsJsonPath);
|
||||
}
|
||||
return c;
|
||||
projectConfiguration.root,
|
||||
(o) =>
|
||||
Object.keys(o.rules ?? {})?.includes('@nx/nx-plugin-checks') ||
|
||||
Object.keys(o.rules ?? {})?.includes('@nrwl/nx/nx-plugin-checks'),
|
||||
(o) => {
|
||||
const fileSet = new Set(Array.isArray(o.files) ? o.files : [o.files]);
|
||||
fileSet.add(relativeMigrationsJsonPath);
|
||||
return {
|
||||
...o,
|
||||
files: Array.from(fileSet),
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
@ -179,42 +180,49 @@ function updateProjectEslintConfig(
|
||||
options: ProjectConfiguration,
|
||||
packageJson: PackageJson
|
||||
) {
|
||||
// Update the project level lint configuration to specify
|
||||
// the plugin schema rule for generated files
|
||||
const eslintPath = `${options.root}/.eslintrc.json`;
|
||||
if (host.exists(eslintPath)) {
|
||||
const eslintConfig = readJson<ESLint.Config>(host, eslintPath);
|
||||
eslintConfig.overrides ??= [];
|
||||
let entry: ESLint.ConfigOverride<ESLint.RulesRecord> =
|
||||
eslintConfig.overrides.find(
|
||||
(x) =>
|
||||
Object.keys(x.rules ?? {}).includes('@nx/nx-plugin-checks') ||
|
||||
Object.keys(x.rules ?? {}).includes('@nrwl/nx/nx-plugin-checks')
|
||||
);
|
||||
const newentry = !entry;
|
||||
entry ??= { files: [] };
|
||||
entry.files = [
|
||||
...new Set([
|
||||
...(entry.files ?? []),
|
||||
...[
|
||||
'./package.json',
|
||||
packageJson.generators,
|
||||
packageJson.executors,
|
||||
packageJson.schematics,
|
||||
packageJson.builders,
|
||||
].filter((f) => !!f),
|
||||
]),
|
||||
];
|
||||
entry.parser = 'jsonc-eslint-parser';
|
||||
entry.rules ??= {
|
||||
'@nx/nx-plugin-checks': 'error',
|
||||
};
|
||||
if (isEslintConfigSupported(host, options.root)) {
|
||||
const lookup = (o) =>
|
||||
Object.keys(o.rules ?? {}).includes('@nx/nx-plugin-checks') ||
|
||||
Object.keys(o.rules ?? {}).includes('@nrwl/nx/nx-plugin-checks');
|
||||
|
||||
if (newentry) {
|
||||
eslintConfig.overrides.push(entry);
|
||||
const files = [
|
||||
'./package.json',
|
||||
packageJson.generators,
|
||||
packageJson.executors,
|
||||
packageJson.schematics,
|
||||
packageJson.builders,
|
||||
].filter((f) => !!f);
|
||||
|
||||
const parser = useFlatConfig(host)
|
||||
? { languageOptions: { parser: 'jsonc-eslint-parser' } }
|
||||
: { parser: 'jsonc-eslint-parser' };
|
||||
|
||||
if (lintConfigHasOverride(host, options.root, lookup)) {
|
||||
// update it
|
||||
updateOverrideInLintConfig(host, options.root, lookup, (o) => ({
|
||||
...o,
|
||||
files: [
|
||||
...new Set([
|
||||
...(Array.isArray(o.files) ? o.files : [o.files]),
|
||||
...files,
|
||||
]),
|
||||
],
|
||||
...parser,
|
||||
rules: {
|
||||
...o.rules,
|
||||
'@nx/nx-plugin-checks': 'error',
|
||||
},
|
||||
}));
|
||||
} else {
|
||||
// add it
|
||||
addOverrideToLintConfig(host, options.root, {
|
||||
files,
|
||||
...parser,
|
||||
rules: {
|
||||
'@nx/nx-plugin-checks': 'error',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
writeJson(host, eslintPath, eslintConfig);
|
||||
}
|
||||
}
|
||||
|
||||
@ -222,22 +230,34 @@ function updateProjectEslintConfig(
|
||||
// This is required, otherwise every json file that is not overriden
|
||||
// will display false errors in the IDE
|
||||
function updateRootEslintConfig(host: Tree) {
|
||||
if (host.exists('.eslintrc.json')) {
|
||||
const rootESLint = readJson<ESLint.Config>(host, '.eslintrc.json');
|
||||
rootESLint.overrides ??= [];
|
||||
if (!eslintConfigContainsJsonOverride(rootESLint)) {
|
||||
rootESLint.overrides.push({
|
||||
files: '*.json',
|
||||
parser: 'jsonc-eslint-parser',
|
||||
rules: {},
|
||||
});
|
||||
writeJson(host, '.eslintrc.json', rootESLint);
|
||||
if (isEslintConfigSupported(host)) {
|
||||
if (
|
||||
!lintConfigHasOverride(
|
||||
host,
|
||||
'',
|
||||
(o) =>
|
||||
Array.isArray(o.files)
|
||||
? o.files.some((f) => f.match(/\.json$/))
|
||||
: !!o.files?.match(/\.json$/),
|
||||
true
|
||||
)
|
||||
) {
|
||||
addOverrideToLintConfig(
|
||||
host,
|
||||
'',
|
||||
{
|
||||
files: '*.json',
|
||||
parser: 'jsonc-eslint-parser',
|
||||
rules: {},
|
||||
},
|
||||
{ checkBaseConfig: true }
|
||||
);
|
||||
}
|
||||
} else {
|
||||
output.note({
|
||||
title: 'Unable to update root eslint config.',
|
||||
bodyLines: [
|
||||
'We only automatically update the root eslint config if it is json.',
|
||||
'We only automatically update the root eslint config if it is json or flat config.',
|
||||
'If you are using a different format, you will need to update it manually.',
|
||||
'You need to set the parser to jsonc-eslint-parser for json files.',
|
||||
],
|
||||
@ -263,15 +283,6 @@ function setupVsCodeLintingForJsonFiles(host: Tree) {
|
||||
writeJson(host, '.vscode/settings.json', existing);
|
||||
}
|
||||
|
||||
function eslintConfigContainsJsonOverride(eslintConfig: ESLint.Config) {
|
||||
return eslintConfig.overrides.some((x) => {
|
||||
if (typeof x.files === 'string' && x.files.includes('.json')) {
|
||||
return true;
|
||||
}
|
||||
return Array.isArray(x.files) && x.files.some((f) => f.includes('.json'));
|
||||
});
|
||||
}
|
||||
|
||||
function projectIsEsLintEnabled(project: ProjectConfiguration) {
|
||||
return !!getEsLintOptions(project);
|
||||
}
|
||||
|
||||
@ -2,16 +2,15 @@ import { Linter, lintProjectGenerator } from '@nx/linter';
|
||||
import {
|
||||
addDependenciesToPackageJson,
|
||||
GeneratorCallback,
|
||||
joinPathFragments,
|
||||
runTasksInSerial,
|
||||
Tree,
|
||||
updateJson,
|
||||
} from '@nx/devkit';
|
||||
import { extraEslintDependencies } from '@nx/react/src/utils/lint';
|
||||
import {
|
||||
extendReactEslintJson,
|
||||
extraEslintDependencies,
|
||||
} from '@nx/react/src/utils/lint';
|
||||
import type { Linter as ESLintLinter } from 'eslint';
|
||||
addExtendsToLintConfig,
|
||||
addIgnoresToLintConfig,
|
||||
isEslintConfigSupported,
|
||||
} from '@nx/linter/src/generators/utils/eslint-file';
|
||||
|
||||
interface NormalizedSchema {
|
||||
linter?: Linter;
|
||||
@ -39,22 +38,14 @@ export async function addLinting(host: Tree, options: NormalizedSchema) {
|
||||
|
||||
tasks.push(lintTask);
|
||||
|
||||
updateJson(
|
||||
host,
|
||||
joinPathFragments(options.projectRoot, '.eslintrc.json'),
|
||||
(json: ESLintLinter.Config) => {
|
||||
json = extendReactEslintJson(json);
|
||||
|
||||
json.ignorePatterns = [
|
||||
...json.ignorePatterns,
|
||||
'public',
|
||||
'.cache',
|
||||
'node_modules',
|
||||
];
|
||||
|
||||
return json;
|
||||
}
|
||||
);
|
||||
if (isEslintConfigSupported(host)) {
|
||||
addExtendsToLintConfig(host, options.projectRoot, 'plugin:@nx/react');
|
||||
addIgnoresToLintConfig(host, options.projectRoot, [
|
||||
'public',
|
||||
'.cache',
|
||||
'node_modules',
|
||||
]);
|
||||
}
|
||||
|
||||
if (!options.skipPackageJson) {
|
||||
const installTask = await addDependenciesToPackageJson(
|
||||
|
||||
@ -1,7 +1,4 @@
|
||||
import {
|
||||
extendReactEslintJson,
|
||||
extraEslintDependencies,
|
||||
} from '../../utils/lint';
|
||||
import { extraEslintDependencies } from '../../utils/lint';
|
||||
import { NormalizedSchema, Schema } from './schema';
|
||||
import { createApplicationFiles } from './lib/create-application-files';
|
||||
import { updateSpecConfig } from './lib/update-jest-config';
|
||||
@ -22,7 +19,6 @@ import {
|
||||
runTasksInSerial,
|
||||
stripIndents,
|
||||
Tree,
|
||||
updateJson,
|
||||
} from '@nx/devkit';
|
||||
|
||||
import reactInitGenerator from '../init/init';
|
||||
@ -39,6 +35,10 @@ import { addSwcDependencies } from '@nx/js/src/utils/swc/add-swc-dependencies';
|
||||
import * as chalk from 'chalk';
|
||||
import { showPossibleWarnings } from './lib/show-possible-warnings';
|
||||
import { addE2e } from './lib/add-e2e';
|
||||
import {
|
||||
addExtendsToLintConfig,
|
||||
isEslintConfigSupported,
|
||||
} from '@nx/linter/src/generators/utils/eslint-file';
|
||||
|
||||
async function addLinting(host: Tree, options: NormalizedSchema) {
|
||||
const tasks: GeneratorCallback[] = [];
|
||||
@ -63,11 +63,9 @@ async function addLinting(host: Tree, options: NormalizedSchema) {
|
||||
});
|
||||
tasks.push(lintTask);
|
||||
|
||||
updateJson(
|
||||
host,
|
||||
joinPathFragments(options.appProjectRoot, '.eslintrc.json'),
|
||||
extendReactEslintJson
|
||||
);
|
||||
if (isEslintConfigSupported(host)) {
|
||||
addExtendsToLintConfig(host, options.appProjectRoot, 'plugin:@nx/react');
|
||||
}
|
||||
|
||||
if (!options.skipPackageJson) {
|
||||
const installTask = addDependenciesToPackageJson(
|
||||
|
||||
@ -4,7 +4,6 @@ import {
|
||||
ensurePackage,
|
||||
getPackageManagerCommand,
|
||||
joinPathFragments,
|
||||
readProjectConfiguration,
|
||||
} from '@nx/devkit';
|
||||
import { webStaticServeGenerator } from '@nx/web';
|
||||
|
||||
|
||||
@ -1,14 +1,14 @@
|
||||
import { Tree } from 'nx/src/generators/tree';
|
||||
import { Linter, lintProjectGenerator } from '@nx/linter';
|
||||
import { joinPathFragments } from 'nx/src/utils/path';
|
||||
import { updateJson } from 'nx/src/generators/utils/json';
|
||||
import { addDependenciesToPackageJson, runTasksInSerial } from '@nx/devkit';
|
||||
|
||||
import { NormalizedSchema } from '../schema';
|
||||
import { extraEslintDependencies } from '../../../utils/lint';
|
||||
import {
|
||||
extendReactEslintJson,
|
||||
extraEslintDependencies,
|
||||
} from '../../../utils/lint';
|
||||
addExtendsToLintConfig,
|
||||
isEslintConfigSupported,
|
||||
} from '@nx/linter/src/generators/utils/eslint-file';
|
||||
|
||||
export async function addLinting(host: Tree, options: NormalizedSchema) {
|
||||
if (options.linter === Linter.EsLint) {
|
||||
@ -25,11 +25,9 @@ export async function addLinting(host: Tree, options: NormalizedSchema) {
|
||||
setParserOptionsProject: options.setParserOptionsProject,
|
||||
});
|
||||
|
||||
updateJson(
|
||||
host,
|
||||
joinPathFragments(options.projectRoot, '.eslintrc.json'),
|
||||
extendReactEslintJson
|
||||
);
|
||||
if (isEslintConfigSupported(host)) {
|
||||
addExtendsToLintConfig(host, options.projectRoot, 'plugin:@nx/react');
|
||||
}
|
||||
|
||||
let installTask = () => {};
|
||||
if (!options.skipPackageJson) {
|
||||
|
||||
@ -1,5 +1,4 @@
|
||||
import { offsetFromRoot } from '@nx/devkit';
|
||||
import type { Linter } from 'eslint';
|
||||
import { Linter } from 'eslint';
|
||||
import {
|
||||
eslintPluginImportVersion,
|
||||
eslintPluginReactVersion,
|
||||
@ -17,6 +16,9 @@ export const extraEslintDependencies = {
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* @deprecated Use `addExtendsToLintConfig` from `@nx/linter` instead.
|
||||
*/
|
||||
export const extendReactEslintJson = (json: Linter.Config) => {
|
||||
const { extends: pluginExtends, ...config } = json;
|
||||
|
||||
|
||||
@ -28,7 +28,8 @@ import {
|
||||
import { StorybookConfigureSchema } from '../schema';
|
||||
import { UiFramework7 } from '../../../utils/models';
|
||||
import { nxVersion } from '../../../utils/versions';
|
||||
import ts = require('typescript');
|
||||
import { findEslintFile } from '@nx/linter/src/generators/utils/eslint-file';
|
||||
import { useFlatConfig } from '@nx/linter/src/utils/flat-config';
|
||||
|
||||
const DEFAULT_PORT = 4400;
|
||||
|
||||
@ -173,7 +174,7 @@ export function createStorybookTsconfigFile(
|
||||
if (tree.exists(oldStorybookTsConfigPath)) {
|
||||
logger.warn(`.storybook/tsconfig.json already exists for this project`);
|
||||
logger.warn(
|
||||
`It will be renamed and moved to tsconfig.storybook.json.
|
||||
`It will be renamed and moved to tsconfig.storybook.json.
|
||||
Please make sure all settings look correct after this change.
|
||||
Also, please make sure to use "nx migrate" to move from one version of Nx to another.
|
||||
`
|
||||
@ -365,7 +366,7 @@ export function configureTsSolutionConfig(
|
||||
* which includes *.stories files.
|
||||
*
|
||||
* For TSLint this is done via the builder config, for ESLint this is
|
||||
* done within the .eslintrc.json file.
|
||||
* done within the eslint config file.
|
||||
*/
|
||||
export function updateLintConfig(tree: Tree, schema: StorybookConfigureSchema) {
|
||||
const { name: projectName } = schema;
|
||||
@ -382,18 +383,46 @@ export function updateLintConfig(tree: Tree, schema: StorybookConfigureSchema) {
|
||||
]);
|
||||
});
|
||||
|
||||
if (tree.exists(join(root, '.eslintrc.json'))) {
|
||||
updateJson(tree, join(root, '.eslintrc.json'), (json) => {
|
||||
const eslintFile = findEslintFile(tree, root);
|
||||
if (!eslintFile) {
|
||||
return;
|
||||
}
|
||||
|
||||
const parserConfigPath = join(
|
||||
root,
|
||||
schema.uiFramework === '@storybook/angular'
|
||||
? '.storybook/tsconfig.json'
|
||||
: 'tsconfig.storybook.json'
|
||||
);
|
||||
|
||||
if (useFlatConfig(tree)) {
|
||||
let config = tree.read(eslintFile, 'utf-8');
|
||||
const projectRegex = RegExp(/project:\s?\[?['"](.*)['"]\]?/g);
|
||||
let match;
|
||||
while ((match = projectRegex.exec(config)) !== null) {
|
||||
const matchSet = new Set(
|
||||
match[1].split(',').map((p) => p.trim().replace(/['"]/g, ''))
|
||||
);
|
||||
matchSet.add(parserConfigPath);
|
||||
const insert = `project: [${Array.from(matchSet)
|
||||
.map((p) => `'${p}'`)
|
||||
.join(', ')}]`;
|
||||
config =
|
||||
config.slice(0, match.index) +
|
||||
insert +
|
||||
config.slice(match.index + match[0].length);
|
||||
}
|
||||
tree.write(eslintFile, config);
|
||||
} else {
|
||||
updateJson(tree, join(root, eslintFile), (json) => {
|
||||
if (typeof json.parserOptions?.project === 'string') {
|
||||
json.parserOptions.project = [json.parserOptions.project];
|
||||
}
|
||||
|
||||
if (Array.isArray(json.parserOptions?.project)) {
|
||||
if (json.parserOptions?.project) {
|
||||
json.parserOptions.project = dedupe([
|
||||
...json.parserOptions.project,
|
||||
schema.uiFramework === '@storybook/angular'
|
||||
? join(root, '.storybook/tsconfig.json')
|
||||
: join(root, 'tsconfig.storybook.json'),
|
||||
parserConfigPath,
|
||||
]);
|
||||
}
|
||||
|
||||
@ -402,12 +431,10 @@ export function updateLintConfig(tree: Tree, schema: StorybookConfigureSchema) {
|
||||
if (typeof o.parserOptions?.project === 'string') {
|
||||
o.parserOptions.project = [o.parserOptions.project];
|
||||
}
|
||||
if (Array.isArray(o.parserOptions?.project)) {
|
||||
if (o.parserOptions?.project) {
|
||||
o.parserOptions.project = dedupe([
|
||||
...o.parserOptions.project,
|
||||
schema.uiFramework === '@storybook/angular'
|
||||
? join(root, '.storybook/tsconfig.json')
|
||||
: join(root, 'tsconfig.storybook.json'),
|
||||
parserConfigPath,
|
||||
]);
|
||||
}
|
||||
}
|
||||
@ -629,15 +656,15 @@ export function rootFileIsTs(
|
||||
): boolean {
|
||||
if (tree.exists(`.storybook/${rootFileName}.ts`) && !tsConfiguration) {
|
||||
logger.info(
|
||||
`The root Storybook configuration is in TypeScript,
|
||||
so Nx will generate TypeScript Storybook configuration files
|
||||
`The root Storybook configuration is in TypeScript,
|
||||
so Nx will generate TypeScript Storybook configuration files
|
||||
in this project's .storybook folder as well.`
|
||||
);
|
||||
return true;
|
||||
} else if (tree.exists(`.storybook/${rootFileName}.js`) && tsConfiguration) {
|
||||
logger.info(
|
||||
`The root Storybook configuration is in JavaScript,
|
||||
so Nx will generate JavaScript Storybook configuration files
|
||||
`The root Storybook configuration is in JavaScript,
|
||||
so Nx will generate JavaScript Storybook configuration files
|
||||
in this project's .storybook folder as well.`
|
||||
);
|
||||
return false;
|
||||
@ -751,17 +778,13 @@ export function renameAndMoveOldTsConfig(
|
||||
});
|
||||
}
|
||||
|
||||
const projectEsLintFile = joinPathFragments(projectRoot, '.eslintrc.json');
|
||||
|
||||
if (tree.exists(projectEsLintFile)) {
|
||||
updateJson(tree, projectEsLintFile, (json) => {
|
||||
const jsonString = JSON.stringify(json);
|
||||
const newJsonString = jsonString.replace(
|
||||
/\.storybook\/tsconfig\.json/g,
|
||||
'tsconfig.storybook.json'
|
||||
);
|
||||
json = JSON.parse(newJsonString);
|
||||
return json;
|
||||
});
|
||||
const eslintFile = findEslintFile(tree, projectRoot);
|
||||
if (eslintFile) {
|
||||
const fileName = joinPathFragments(projectRoot, eslintFile);
|
||||
const config = tree.read(fileName, 'utf-8');
|
||||
tree.write(
|
||||
fileName,
|
||||
config.replace(/\.storybook\/tsconfig\.json/g, 'tsconfig.storybook.json')
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -24,6 +24,7 @@ export function moveProjectFiles(
|
||||
'tsconfig.spec.json',
|
||||
'.babelrc',
|
||||
'.eslintrc.json',
|
||||
'eslint.config.js',
|
||||
/^jest\.config\.(app|lib)\.[jt]s$/,
|
||||
'vite.config.ts',
|
||||
/^webpack.*\.js$/,
|
||||
|
||||
@ -0,0 +1,455 @@
|
||||
import {
|
||||
joinPathFragments,
|
||||
offsetFromRoot,
|
||||
readJson,
|
||||
readProjectConfiguration,
|
||||
Tree,
|
||||
updateJson,
|
||||
} from '@nx/devkit';
|
||||
import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing';
|
||||
import { Linter } from '../../../utils/lint';
|
||||
import { NormalizedSchema } from '../schema';
|
||||
import { updateEslintConfig } from './update-eslint-config';
|
||||
|
||||
// nx-ignore-next-line
|
||||
const { libraryGenerator } = require('@nx/js');
|
||||
|
||||
describe('updateEslint', () => {
|
||||
let tree: Tree;
|
||||
let schema: NormalizedSchema;
|
||||
|
||||
beforeEach(async () => {
|
||||
schema = {
|
||||
projectName: 'my-lib',
|
||||
destination: 'shared/my-destination',
|
||||
importPath: '@proj/shared-my-destination',
|
||||
updateImportPath: true,
|
||||
newProjectName: 'shared-my-destination',
|
||||
relativeToRootDestination: 'libs/shared/my-destination',
|
||||
};
|
||||
|
||||
tree = createTreeWithEmptyWorkspace({ layout: 'apps-libs' });
|
||||
});
|
||||
|
||||
it('should handle .eslintrc.json not existing', async () => {
|
||||
await libraryGenerator(tree, {
|
||||
name: 'my-lib',
|
||||
linter: Linter.None,
|
||||
});
|
||||
|
||||
const projectConfig = readProjectConfiguration(tree, 'my-lib');
|
||||
|
||||
expect(() => {
|
||||
updateEslintConfig(tree, schema, projectConfig);
|
||||
}).not.toThrow();
|
||||
});
|
||||
|
||||
it('should update .eslintrc.json extends path when project is moved to subdirectory', async () => {
|
||||
await libraryGenerator(tree, {
|
||||
name: 'my-lib',
|
||||
linter: Linter.EsLint,
|
||||
});
|
||||
// This step is usually handled elsewhere
|
||||
tree.rename(
|
||||
'libs/my-lib/.eslintrc.json',
|
||||
'libs/shared/my-destination/.eslintrc.json'
|
||||
);
|
||||
const projectConfig = readProjectConfiguration(tree, 'my-lib');
|
||||
|
||||
updateEslintConfig(tree, schema, projectConfig);
|
||||
|
||||
expect(
|
||||
readJson(tree, '/libs/shared/my-destination/.eslintrc.json')
|
||||
).toEqual(
|
||||
expect.objectContaining({
|
||||
extends: ['../../../.eslintrc.json'],
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('should update .eslintrc.json extends path when project is moved from subdirectory', async () => {
|
||||
await libraryGenerator(tree, {
|
||||
name: 'test',
|
||||
directory: 'api',
|
||||
linter: Linter.EsLint,
|
||||
});
|
||||
// This step is usually handled elsewhere
|
||||
tree.rename('libs/api/test/.eslintrc.json', 'libs/test/.eslintrc.json');
|
||||
const projectConfig = readProjectConfiguration(tree, 'api-test');
|
||||
|
||||
const newSchema = {
|
||||
projectName: 'api-test',
|
||||
destination: 'test',
|
||||
importPath: '@proj/test',
|
||||
updateImportPath: true,
|
||||
newProjectName: 'test',
|
||||
relativeToRootDestination: 'libs/test',
|
||||
};
|
||||
|
||||
updateEslintConfig(tree, newSchema, projectConfig);
|
||||
|
||||
expect(readJson(tree, '/libs/test/.eslintrc.json')).toEqual(
|
||||
expect.objectContaining({
|
||||
extends: ['../../.eslintrc.json'],
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('should preserve .eslintrc.json non-relative extends when project is moved to subdirectory', async () => {
|
||||
await libraryGenerator(tree, {
|
||||
name: 'my-lib',
|
||||
linter: Linter.EsLint,
|
||||
});
|
||||
updateJson(tree, 'libs/my-lib/.eslintrc.json', (eslintRcJson) => {
|
||||
eslintRcJson.extends = [
|
||||
'plugin:@nx/react',
|
||||
'../../.eslintrc.json',
|
||||
'./customrc.json',
|
||||
];
|
||||
return eslintRcJson;
|
||||
});
|
||||
// This step is usually handled elsewhere
|
||||
tree.rename(
|
||||
'libs/my-lib/.eslintrc.json',
|
||||
'libs/shared/my-destination/.eslintrc.json'
|
||||
);
|
||||
const projectConfig = readProjectConfiguration(tree, 'my-lib');
|
||||
|
||||
updateEslintConfig(tree, schema, projectConfig);
|
||||
|
||||
expect(
|
||||
readJson(tree, '/libs/shared/my-destination/.eslintrc.json')
|
||||
).toEqual(
|
||||
expect.objectContaining({
|
||||
extends: [
|
||||
'plugin:@nx/react',
|
||||
'../../../.eslintrc.json',
|
||||
'./customrc.json',
|
||||
],
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('should update .eslintrc.json overrides parser project when project is moved', async () => {
|
||||
await libraryGenerator(tree, {
|
||||
name: 'my-lib',
|
||||
linter: Linter.EsLint,
|
||||
setParserOptionsProject: true,
|
||||
});
|
||||
// This step is usually handled elsewhere
|
||||
tree.rename(
|
||||
'libs/my-lib/.eslintrc.json',
|
||||
'libs/shared/my-destination/.eslintrc.json'
|
||||
);
|
||||
const projectConfig = readProjectConfiguration(tree, 'my-lib');
|
||||
|
||||
updateEslintConfig(tree, schema, projectConfig);
|
||||
|
||||
expect(
|
||||
readJson(tree, '/libs/shared/my-destination/.eslintrc.json')
|
||||
).toEqual(
|
||||
expect.objectContaining({
|
||||
overrides: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
parserOptions: expect.objectContaining({
|
||||
project: ['libs/shared/my-destination/tsconfig.*?.json'],
|
||||
}),
|
||||
}),
|
||||
]),
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('should update multiple .eslintrc.json overrides parser project when project is moved', async () => {
|
||||
await libraryGenerator(tree, {
|
||||
name: 'my-lib',
|
||||
linter: Linter.EsLint,
|
||||
setParserOptionsProject: true,
|
||||
});
|
||||
|
||||
// Add another parser project to eslint.json
|
||||
const storybookProject = '.storybook/tsconfig.json';
|
||||
updateJson(tree, '/libs/my-lib/.eslintrc.json', (eslintRcJson) => {
|
||||
eslintRcJson.overrides[0].parserOptions.project.push(
|
||||
`libs/my-lib/${storybookProject}`
|
||||
);
|
||||
return eslintRcJson;
|
||||
});
|
||||
|
||||
// This step is usually handled elsewhere
|
||||
tree.rename(
|
||||
'libs/my-lib/.eslintrc.json',
|
||||
'libs/shared/my-destination/.eslintrc.json'
|
||||
);
|
||||
const projectConfig = readProjectConfiguration(tree, 'my-lib');
|
||||
|
||||
updateEslintConfig(tree, schema, projectConfig);
|
||||
|
||||
expect(
|
||||
readJson(tree, '/libs/shared/my-destination/.eslintrc.json')
|
||||
).toEqual(
|
||||
expect.objectContaining({
|
||||
overrides: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
parserOptions: expect.objectContaining({
|
||||
project: [
|
||||
'libs/shared/my-destination/tsconfig.*?.json',
|
||||
`libs/shared/my-destination/${storybookProject}`,
|
||||
],
|
||||
}),
|
||||
}),
|
||||
]),
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('should update .eslintrc.json parserOptions.project as a string', async () => {
|
||||
await libraryGenerator(tree, {
|
||||
name: 'my-lib',
|
||||
linter: Linter.EsLint,
|
||||
setParserOptionsProject: true,
|
||||
});
|
||||
|
||||
// Add another parser project to eslint.json
|
||||
const storybookProject = '.storybook/tsconfig.json';
|
||||
updateJson(tree, '/libs/my-lib/.eslintrc.json', (eslintRcJson) => {
|
||||
eslintRcJson.overrides[0].parserOptions.project = `libs/my-lib/${storybookProject}`;
|
||||
return eslintRcJson;
|
||||
});
|
||||
|
||||
// This step is usually handled elsewhere
|
||||
tree.rename(
|
||||
'libs/my-lib/.eslintrc.json',
|
||||
'libs/shared/my-destination/.eslintrc.json'
|
||||
);
|
||||
const projectConfig = readProjectConfiguration(tree, 'my-lib');
|
||||
|
||||
updateEslintConfig(tree, schema, projectConfig);
|
||||
|
||||
expect(
|
||||
readJson(tree, '/libs/shared/my-destination/.eslintrc.json').overrides[0]
|
||||
.parserOptions
|
||||
).toEqual({ project: `libs/shared/my-destination/${storybookProject}` });
|
||||
});
|
||||
});
|
||||
|
||||
describe('updateEslint (flat config)', () => {
|
||||
let tree: Tree;
|
||||
let schema: NormalizedSchema;
|
||||
|
||||
beforeEach(async () => {
|
||||
schema = {
|
||||
projectName: 'my-lib',
|
||||
destination: 'shared/my-destination',
|
||||
importPath: '@proj/shared-my-destination',
|
||||
updateImportPath: true,
|
||||
newProjectName: 'shared-my-destination',
|
||||
relativeToRootDestination: 'libs/shared/my-destination',
|
||||
};
|
||||
|
||||
tree = createTreeWithEmptyWorkspace({ layout: 'apps-libs' });
|
||||
tree.delete('.eslintrc.json');
|
||||
tree.write('eslint.config.js', `module.exports = [];`);
|
||||
});
|
||||
|
||||
it('should handle config not existing', async () => {
|
||||
await libraryGenerator(tree, {
|
||||
name: 'my-lib',
|
||||
linter: Linter.None,
|
||||
});
|
||||
|
||||
const projectConfig = readProjectConfiguration(tree, 'my-lib');
|
||||
|
||||
expect(() => {
|
||||
updateEslintConfig(tree, schema, projectConfig);
|
||||
}).not.toThrow();
|
||||
});
|
||||
|
||||
it('should update config extends path when project is moved to subdirectory', async () => {
|
||||
await libraryGenerator(tree, {
|
||||
name: 'my-lib',
|
||||
linter: Linter.EsLint,
|
||||
});
|
||||
convertToFlat(tree, 'libs/my-lib');
|
||||
// This step is usually handled elsewhere
|
||||
tree.rename(
|
||||
'libs/my-lib/eslint.config.js',
|
||||
'libs/shared/my-destination/eslint.config.js'
|
||||
);
|
||||
const projectConfig = readProjectConfiguration(tree, 'my-lib');
|
||||
|
||||
updateEslintConfig(tree, schema, projectConfig);
|
||||
|
||||
expect(
|
||||
tree.read('libs/shared/my-destination/eslint.config.js', 'utf-8')
|
||||
).toEqual(expect.stringContaining(`require('../../../eslint.config.js')`));
|
||||
});
|
||||
|
||||
it('should update config extends path when project is moved from subdirectory', async () => {
|
||||
await libraryGenerator(tree, {
|
||||
name: 'test',
|
||||
directory: 'api',
|
||||
linter: Linter.EsLint,
|
||||
});
|
||||
convertToFlat(tree, 'libs/api/test');
|
||||
// This step is usually handled elsewhere
|
||||
tree.rename('libs/api/test/eslint.config.js', 'libs/test/eslint.config.js');
|
||||
|
||||
const projectConfig = readProjectConfiguration(tree, 'api-test');
|
||||
|
||||
const newSchema = {
|
||||
projectName: 'api-test',
|
||||
destination: 'test',
|
||||
importPath: '@proj/test',
|
||||
updateImportPath: true,
|
||||
newProjectName: 'test',
|
||||
relativeToRootDestination: 'libs/test',
|
||||
};
|
||||
|
||||
updateEslintConfig(tree, newSchema, projectConfig);
|
||||
|
||||
expect(tree.read('libs/test/eslint.config.js', 'utf-8')).toEqual(
|
||||
expect.stringContaining(`require('../../eslint.config.js')`)
|
||||
);
|
||||
});
|
||||
|
||||
it('should update config overrides parser project when project is moved', async () => {
|
||||
await libraryGenerator(tree, {
|
||||
name: 'my-lib',
|
||||
linter: Linter.EsLint,
|
||||
setParserOptionsProject: true,
|
||||
});
|
||||
convertToFlat(tree, 'libs/my-lib', { hasParser: true });
|
||||
// This step is usually handled elsewhere
|
||||
tree.rename(
|
||||
'libs/my-lib/eslint.config.js',
|
||||
'libs/shared/my-destination/eslint.config.js'
|
||||
);
|
||||
const projectConfig = readProjectConfiguration(tree, 'my-lib');
|
||||
|
||||
updateEslintConfig(tree, schema, projectConfig);
|
||||
|
||||
expect(
|
||||
tree.read('libs/shared/my-destination/eslint.config.js', 'utf-8')
|
||||
).toEqual(
|
||||
expect.stringContaining(
|
||||
`project: ["libs/shared/my-destination/tsconfig.*?.json"]`
|
||||
)
|
||||
);
|
||||
});
|
||||
|
||||
it('should update multiple config overrides parser project when project is moved', async () => {
|
||||
await libraryGenerator(tree, {
|
||||
name: 'my-lib',
|
||||
linter: Linter.EsLint,
|
||||
setParserOptionsProject: true,
|
||||
});
|
||||
// Add another parser project to eslint.json
|
||||
const storybookProject = '.storybook/tsconfig.json';
|
||||
convertToFlat(tree, 'libs/my-lib', {
|
||||
hasParser: true,
|
||||
anotherProject: storybookProject,
|
||||
});
|
||||
// This step is usually handled elsewhere
|
||||
tree.rename(
|
||||
'libs/my-lib/eslint.config.js',
|
||||
'libs/shared/my-destination/eslint.config.js'
|
||||
);
|
||||
const projectConfig = readProjectConfiguration(tree, 'my-lib');
|
||||
|
||||
updateEslintConfig(tree, schema, projectConfig);
|
||||
|
||||
expect(
|
||||
tree.read('libs/shared/my-destination/eslint.config.js', 'utf-8')
|
||||
).toEqual(
|
||||
expect.stringContaining(
|
||||
`project: ["libs/shared/my-destination/tsconfig.*?.json", "libs/shared/my-destination/${storybookProject}"]`
|
||||
)
|
||||
);
|
||||
});
|
||||
|
||||
it('should update config parserOptions.project as a string', async () => {
|
||||
await libraryGenerator(tree, {
|
||||
name: 'my-lib',
|
||||
linter: Linter.EsLint,
|
||||
setParserOptionsProject: true,
|
||||
});
|
||||
|
||||
convertToFlat(tree, 'libs/my-lib', { hasParser: true, isString: true });
|
||||
// This step is usually handled elsewhere
|
||||
tree.rename(
|
||||
'libs/my-lib/eslint.config.js',
|
||||
'libs/shared/my-destination/eslint.config.js'
|
||||
);
|
||||
const projectConfig = readProjectConfiguration(tree, 'my-lib');
|
||||
|
||||
updateEslintConfig(tree, schema, projectConfig);
|
||||
|
||||
expect(
|
||||
tree.read('libs/shared/my-destination/eslint.config.js', 'utf-8')
|
||||
).toEqual(
|
||||
expect.stringContaining(
|
||||
`project: "libs/shared/my-destination/tsconfig.*?.json"`
|
||||
)
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
function convertToFlat(
|
||||
tree: Tree,
|
||||
path: string,
|
||||
options: {
|
||||
hasParser?: boolean;
|
||||
anotherProject?: string;
|
||||
isString?: boolean;
|
||||
} = {}
|
||||
) {
|
||||
const offset = offsetFromRoot(path);
|
||||
tree.delete(joinPathFragments(path, '.eslintrc.json'));
|
||||
|
||||
let parserOptions = '';
|
||||
if (options.hasParser) {
|
||||
const paths = options.anotherProject
|
||||
? `["${path}/tsconfig.*?.json", "${path}/${options.anotherProject}"]`
|
||||
: options.isString
|
||||
? `"${path}/tsconfig.*?.json"`
|
||||
: `["${path}/tsconfig.*?.json"]`;
|
||||
parserOptions = `languageOptions: {
|
||||
parserOptions: {
|
||||
project: ${paths}
|
||||
}
|
||||
},
|
||||
`;
|
||||
}
|
||||
|
||||
tree.write(
|
||||
joinPathFragments(path, 'eslint.config.js'),
|
||||
`const { FlatCompat } = require("@eslint/eslintrc");
|
||||
const baseConfig = require("${offset}eslint.config.js");
|
||||
const js = require("@eslint/js");
|
||||
const compat = new FlatCompat({
|
||||
baseDirectory: __dirname,
|
||||
recommendedConfig: js.configs.recommended,
|
||||
});
|
||||
module.exports = [
|
||||
...baseConfig,
|
||||
{
|
||||
files: ["${path}/**/*.ts", "${path}/**/*.tsx", "${path}/**/*.js", "${path}/**/*.jsx"],
|
||||
${parserOptions}rules: {}
|
||||
},
|
||||
{
|
||||
files: ["${path}/**/*.ts", "${path}/**/*.tsx"],
|
||||
rules: {}
|
||||
},
|
||||
{
|
||||
files: ["${path}/**/*.js", "${path}/**/*.jsx"],
|
||||
rules: {}
|
||||
},
|
||||
...compat.config({ parser: "jsonc-eslint-parser" }).map(config => ({
|
||||
...config,
|
||||
files: ["${path}/**/*.json"],
|
||||
rules: { "@nx/dependency-checks": "error" }
|
||||
}))
|
||||
];`
|
||||
);
|
||||
}
|
||||
@ -0,0 +1,36 @@
|
||||
import { output, ProjectConfiguration, Tree } from '@nx/devkit';
|
||||
import { NormalizedSchema } from '../schema';
|
||||
|
||||
/**
|
||||
* Update the .eslintrc file of the project if it exists.
|
||||
*
|
||||
* @param schema The options provided to the schematic
|
||||
*/
|
||||
export function updateEslintConfig(
|
||||
tree: Tree,
|
||||
schema: NormalizedSchema,
|
||||
project: ProjectConfiguration
|
||||
) {
|
||||
// if there is no suitable eslint config, we don't need to do anything
|
||||
if (!tree.exists('.eslintrc.json') && !tree.exists('eslint.config.js')) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const {
|
||||
updateRelativePathsInConfig,
|
||||
// nx-ignore-next-line
|
||||
} = require('@nx/linter/src/generators/utils/eslint-file');
|
||||
updateRelativePathsInConfig(
|
||||
tree,
|
||||
project.root,
|
||||
schema.relativeToRootDestination
|
||||
);
|
||||
} catch {
|
||||
output.warn({
|
||||
title: `Could not update the eslint config file.`,
|
||||
bodyLines: [
|
||||
'The @nx/linter package could not be loaded. Please update the paths in eslint config manually.',
|
||||
],
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -1,232 +0,0 @@
|
||||
import {
|
||||
readJson,
|
||||
readProjectConfiguration,
|
||||
Tree,
|
||||
updateJson,
|
||||
} from '@nx/devkit';
|
||||
import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing';
|
||||
import { Linter } from '../../../utils/lint';
|
||||
import { NormalizedSchema } from '../schema';
|
||||
import { updateEslintrcJson } from './update-eslintrc-json';
|
||||
|
||||
// nx-ignore-next-line
|
||||
const { libraryGenerator } = require('@nx/js');
|
||||
|
||||
describe('updateEslint', () => {
|
||||
let tree: Tree;
|
||||
let schema: NormalizedSchema;
|
||||
|
||||
beforeEach(async () => {
|
||||
schema = {
|
||||
projectName: 'my-lib',
|
||||
destination: 'shared/my-destination',
|
||||
importPath: '@proj/shared-my-destination',
|
||||
updateImportPath: true,
|
||||
newProjectName: 'shared-my-destination',
|
||||
relativeToRootDestination: 'libs/shared/my-destination',
|
||||
};
|
||||
|
||||
tree = createTreeWithEmptyWorkspace({ layout: 'apps-libs' });
|
||||
});
|
||||
|
||||
it('should handle .eslintrc.json not existing', async () => {
|
||||
await libraryGenerator(tree, {
|
||||
name: 'my-lib',
|
||||
linter: Linter.None,
|
||||
});
|
||||
|
||||
const projectConfig = readProjectConfiguration(tree, 'my-lib');
|
||||
|
||||
expect(() => {
|
||||
updateEslintrcJson(tree, schema, projectConfig);
|
||||
}).not.toThrow();
|
||||
});
|
||||
|
||||
it('should update .eslintrc.json extends path when project is moved to subdirectory', async () => {
|
||||
await libraryGenerator(tree, {
|
||||
name: 'my-lib',
|
||||
linter: Linter.EsLint,
|
||||
});
|
||||
// This step is usually handled elsewhere
|
||||
tree.rename(
|
||||
'libs/my-lib/.eslintrc.json',
|
||||
'libs/shared/my-destination/.eslintrc.json'
|
||||
);
|
||||
const projectConfig = readProjectConfiguration(tree, 'my-lib');
|
||||
|
||||
updateEslintrcJson(tree, schema, projectConfig);
|
||||
|
||||
expect(
|
||||
readJson(tree, '/libs/shared/my-destination/.eslintrc.json')
|
||||
).toEqual(
|
||||
expect.objectContaining({
|
||||
extends: ['../../../.eslintrc.json'],
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('should update .eslintrc.json extends path when project is moved from subdirectory', async () => {
|
||||
await libraryGenerator(tree, {
|
||||
name: 'test',
|
||||
directory: 'api',
|
||||
linter: Linter.EsLint,
|
||||
});
|
||||
// This step is usually handled elsewhere
|
||||
tree.rename('libs/api/test/.eslintrc.json', 'libs/test/.eslintrc.json');
|
||||
const projectConfig = readProjectConfiguration(tree, 'api-test');
|
||||
|
||||
const newSchema = {
|
||||
projectName: 'api-test',
|
||||
destination: 'test',
|
||||
importPath: '@proj/test',
|
||||
updateImportPath: true,
|
||||
newProjectName: 'test',
|
||||
relativeToRootDestination: 'libs/test',
|
||||
};
|
||||
|
||||
updateEslintrcJson(tree, newSchema, projectConfig);
|
||||
|
||||
expect(readJson(tree, '/libs/test/.eslintrc.json')).toEqual(
|
||||
expect.objectContaining({
|
||||
extends: ['../../.eslintrc.json'],
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('should preserve .eslintrc.json non-relative extends when project is moved to subdirectory', async () => {
|
||||
await libraryGenerator(tree, {
|
||||
name: 'my-lib',
|
||||
linter: Linter.EsLint,
|
||||
});
|
||||
updateJson(tree, 'libs/my-lib/.eslintrc.json', (eslintRcJson) => {
|
||||
eslintRcJson.extends = [
|
||||
'plugin:@nx/react',
|
||||
'../../.eslintrc.json',
|
||||
'./customrc.json',
|
||||
];
|
||||
return eslintRcJson;
|
||||
});
|
||||
// This step is usually handled elsewhere
|
||||
tree.rename(
|
||||
'libs/my-lib/.eslintrc.json',
|
||||
'libs/shared/my-destination/.eslintrc.json'
|
||||
);
|
||||
const projectConfig = readProjectConfiguration(tree, 'my-lib');
|
||||
|
||||
updateEslintrcJson(tree, schema, projectConfig);
|
||||
|
||||
expect(
|
||||
readJson(tree, '/libs/shared/my-destination/.eslintrc.json')
|
||||
).toEqual(
|
||||
expect.objectContaining({
|
||||
extends: [
|
||||
'plugin:@nx/react',
|
||||
'../../../.eslintrc.json',
|
||||
'./customrc.json',
|
||||
],
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('should update .eslintrc.json overrides parser project when project is moved', async () => {
|
||||
await libraryGenerator(tree, {
|
||||
name: 'my-lib',
|
||||
linter: Linter.EsLint,
|
||||
setParserOptionsProject: true,
|
||||
});
|
||||
// This step is usually handled elsewhere
|
||||
tree.rename(
|
||||
'libs/my-lib/.eslintrc.json',
|
||||
'libs/shared/my-destination/.eslintrc.json'
|
||||
);
|
||||
const projectConfig = readProjectConfiguration(tree, 'my-lib');
|
||||
|
||||
updateEslintrcJson(tree, schema, projectConfig);
|
||||
|
||||
expect(
|
||||
readJson(tree, '/libs/shared/my-destination/.eslintrc.json')
|
||||
).toEqual(
|
||||
expect.objectContaining({
|
||||
overrides: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
parserOptions: expect.objectContaining({
|
||||
project: ['libs/shared/my-destination/tsconfig.*?.json'],
|
||||
}),
|
||||
}),
|
||||
]),
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('should update multiple .eslintrc.json overrides parser project when project is moved', async () => {
|
||||
await libraryGenerator(tree, {
|
||||
name: 'my-lib',
|
||||
linter: Linter.EsLint,
|
||||
setParserOptionsProject: true,
|
||||
});
|
||||
|
||||
// Add another parser project to eslint.json
|
||||
const storybookProject = '.storybook/tsconfig.json';
|
||||
updateJson(tree, '/libs/my-lib/.eslintrc.json', (eslintRcJson) => {
|
||||
eslintRcJson.overrides[0].parserOptions.project.push(
|
||||
`libs/my-lib/${storybookProject}`
|
||||
);
|
||||
return eslintRcJson;
|
||||
});
|
||||
|
||||
// This step is usually handled elsewhere
|
||||
tree.rename(
|
||||
'libs/my-lib/.eslintrc.json',
|
||||
'libs/shared/my-destination/.eslintrc.json'
|
||||
);
|
||||
const projectConfig = readProjectConfiguration(tree, 'my-lib');
|
||||
|
||||
updateEslintrcJson(tree, schema, projectConfig);
|
||||
|
||||
expect(
|
||||
readJson(tree, '/libs/shared/my-destination/.eslintrc.json')
|
||||
).toEqual(
|
||||
expect.objectContaining({
|
||||
overrides: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
parserOptions: expect.objectContaining({
|
||||
project: [
|
||||
'libs/shared/my-destination/tsconfig.*?.json',
|
||||
`libs/shared/my-destination/${storybookProject}`,
|
||||
],
|
||||
}),
|
||||
}),
|
||||
]),
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('should update .eslintrc.json parserOptions.project as a string', async () => {
|
||||
await libraryGenerator(tree, {
|
||||
name: 'my-lib',
|
||||
linter: Linter.EsLint,
|
||||
setParserOptionsProject: true,
|
||||
});
|
||||
|
||||
// Add another parser project to eslint.json
|
||||
const storybookProject = '.storybook/tsconfig.json';
|
||||
updateJson(tree, '/libs/my-lib/.eslintrc.json', (eslintRcJson) => {
|
||||
eslintRcJson.overrides[0].parserOptions.project = `libs/my-lib/${storybookProject}`;
|
||||
return eslintRcJson;
|
||||
});
|
||||
|
||||
// This step is usually handled elsewhere
|
||||
tree.rename(
|
||||
'libs/my-lib/.eslintrc.json',
|
||||
'libs/shared/my-destination/.eslintrc.json'
|
||||
);
|
||||
const projectConfig = readProjectConfiguration(tree, 'my-lib');
|
||||
|
||||
updateEslintrcJson(tree, schema, projectConfig);
|
||||
|
||||
expect(
|
||||
readJson(tree, '/libs/shared/my-destination/.eslintrc.json').overrides[0]
|
||||
.parserOptions
|
||||
).toEqual({ project: `libs/shared/my-destination/${storybookProject}` });
|
||||
});
|
||||
});
|
||||
@ -1,83 +0,0 @@
|
||||
import {
|
||||
joinPathFragments,
|
||||
offsetFromRoot,
|
||||
ProjectConfiguration,
|
||||
Tree,
|
||||
updateJson,
|
||||
} from '@nx/devkit';
|
||||
import { join } from 'path';
|
||||
import { NormalizedSchema } from '../schema';
|
||||
|
||||
interface PartialEsLintrcOverride {
|
||||
parserOptions?: {
|
||||
project?: string[];
|
||||
};
|
||||
}
|
||||
|
||||
interface PartialEsLintRcJson {
|
||||
extends: string | string[];
|
||||
overrides?: PartialEsLintrcOverride[];
|
||||
}
|
||||
|
||||
function offsetFilePath(
|
||||
project: ProjectConfiguration,
|
||||
pathToFile: string,
|
||||
offset: string
|
||||
): string {
|
||||
if (!pathToFile.startsWith('..')) {
|
||||
// not a relative path
|
||||
return pathToFile;
|
||||
}
|
||||
const pathFromRoot = join(project.root, pathToFile);
|
||||
return joinPathFragments(offset, pathFromRoot);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the .eslintrc file of the project if it exists.
|
||||
*
|
||||
* @param schema The options provided to the schematic
|
||||
*/
|
||||
export function updateEslintrcJson(
|
||||
tree: Tree,
|
||||
schema: NormalizedSchema,
|
||||
project: ProjectConfiguration
|
||||
) {
|
||||
const eslintRcPath = join(schema.relativeToRootDestination, '.eslintrc.json');
|
||||
|
||||
if (!tree.exists(eslintRcPath)) {
|
||||
// no .eslintrc found. nothing to do
|
||||
return;
|
||||
}
|
||||
|
||||
const offset = offsetFromRoot(schema.relativeToRootDestination);
|
||||
|
||||
updateJson<PartialEsLintRcJson>(tree, eslintRcPath, (eslintRcJson) => {
|
||||
if (typeof eslintRcJson.extends === 'string') {
|
||||
eslintRcJson.extends = offsetFilePath(
|
||||
project,
|
||||
eslintRcJson.extends,
|
||||
offset
|
||||
);
|
||||
} else if (eslintRcJson.extends) {
|
||||
eslintRcJson.extends = eslintRcJson.extends.map((extend: string) =>
|
||||
offsetFilePath(project, extend, offset)
|
||||
);
|
||||
}
|
||||
|
||||
eslintRcJson.overrides?.forEach(
|
||||
(o: { parserOptions?: { project?: string | string[] } }) => {
|
||||
if (o.parserOptions?.project) {
|
||||
o.parserOptions.project = Array.isArray(o.parserOptions.project)
|
||||
? o.parserOptions.project.map((p) =>
|
||||
p.replace(project.root, schema.relativeToRootDestination)
|
||||
)
|
||||
: o.parserOptions.project.replace(
|
||||
project.root,
|
||||
schema.relativeToRootDestination
|
||||
);
|
||||
}
|
||||
}
|
||||
);
|
||||
return eslintRcJson;
|
||||
});
|
||||
}
|
||||
@ -56,7 +56,7 @@ export function updateFilesForRootProjects(
|
||||
if (!allowedExt.includes(ext)) {
|
||||
continue;
|
||||
}
|
||||
if (file === '.eslintrc.json') {
|
||||
if (file === '.eslintrc.json' || file === 'eslint.config.js') {
|
||||
continue;
|
||||
}
|
||||
|
||||
@ -108,7 +108,7 @@ export function updateFilesForNonRootProjects(
|
||||
if (!allowedExt.includes(ext)) {
|
||||
continue;
|
||||
}
|
||||
if (file === '.eslintrc.json') {
|
||||
if (file === '.eslintrc.json' || file === 'eslint.config.js') {
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
@ -12,7 +12,7 @@ import { normalizeSchema } from './lib/normalize-schema';
|
||||
import { updateBuildTargets } from './lib/update-build-targets';
|
||||
import { updateCypressConfig } from './lib/update-cypress-config';
|
||||
import { updateDefaultProject } from './lib/update-default-project';
|
||||
import { updateEslintrcJson } from './lib/update-eslintrc-json';
|
||||
import { updateEslintConfig } from './lib/update-eslint-config';
|
||||
import { updateImplicitDependencies } from './lib/update-implicit-dependencies';
|
||||
import { updateImports } from './lib/update-imports';
|
||||
import { updateJestConfig } from './lib/update-jest-config';
|
||||
@ -48,7 +48,7 @@ export async function moveGenerator(tree: Tree, rawSchema: Schema) {
|
||||
updateCypressConfig(tree, schema, projectConfig);
|
||||
updateJestConfig(tree, schema, projectConfig);
|
||||
updateStorybookConfig(tree, schema, projectConfig);
|
||||
updateEslintrcJson(tree, schema, projectConfig);
|
||||
updateEslintConfig(tree, schema, projectConfig);
|
||||
updateReadme(tree, schema);
|
||||
updatePackageJson(tree, schema);
|
||||
updateBuildTargets(tree, schema);
|
||||
|
||||
44
pnpm-lock.yaml
generated
44
pnpm-lock.yaml
generated
@ -394,9 +394,6 @@ devDependencies:
|
||||
'@testing-library/react':
|
||||
specifier: 13.4.0
|
||||
version: 13.4.0(react-dom@18.2.0)(react@18.2.0)
|
||||
'@types/css-minimizer-webpack-plugin':
|
||||
specifier: ^3.2.1
|
||||
version: 3.2.1(esbuild@0.17.18)(webpack@5.88.0)
|
||||
'@types/cytoscape':
|
||||
specifier: ^3.18.2
|
||||
version: 3.19.9
|
||||
@ -404,8 +401,8 @@ devDependencies:
|
||||
specifier: ^1.3.2
|
||||
version: 1.3.2
|
||||
'@types/eslint':
|
||||
specifier: ~8.4.1
|
||||
version: 8.4.8
|
||||
specifier: ~8.44.2
|
||||
version: 8.44.2
|
||||
'@types/express':
|
||||
specifier: 4.17.14
|
||||
version: 4.17.14
|
||||
@ -10055,21 +10052,6 @@ packages:
|
||||
'@types/node': 18.16.9
|
||||
dev: true
|
||||
|
||||
/@types/css-minimizer-webpack-plugin@3.2.1(esbuild@0.17.18)(webpack@5.88.0):
|
||||
resolution: {integrity: sha512-MIlnEVQDTX0Y1/ZBY0RyD+F6+ZHlG42qCeSoCVhxI5N1atm+RnmDLQWUCWrdNqebFozUTRLDZJ04v5aYzGG5CA==}
|
||||
deprecated: This is a stub types definition. css-minimizer-webpack-plugin provides its own type definitions, so you do not need this installed.
|
||||
dependencies:
|
||||
css-minimizer-webpack-plugin: 5.0.0(esbuild@0.17.18)(webpack@5.88.0)
|
||||
transitivePeerDependencies:
|
||||
- '@parcel/css'
|
||||
- '@swc/css'
|
||||
- clean-css
|
||||
- csso
|
||||
- esbuild
|
||||
- lightningcss
|
||||
- webpack
|
||||
dev: true
|
||||
|
||||
/@types/cytoscape@3.19.9:
|
||||
resolution: {integrity: sha512-oqCx0ZGiBO0UESbjgq052vjDAy2X53lZpMrWqiweMpvVwKw/2IiYDdzPFK6+f4tMfdv9YKEM9raO5bAZc3UYBg==}
|
||||
dev: true
|
||||
@ -10099,15 +10081,22 @@ packages:
|
||||
/@types/eslint-scope@3.7.4:
|
||||
resolution: {integrity: sha512-9K4zoImiZc3HlIp6AVUDE4CWYx22a+lhSZMYNpbjW04+YF0KWj4pJXnEMjdnFTiQibFFmElcsasJXDbdI/EPhA==}
|
||||
dependencies:
|
||||
'@types/eslint': 8.4.8
|
||||
'@types/eslint': 8.4.1
|
||||
'@types/estree': 1.0.1
|
||||
dev: true
|
||||
|
||||
/@types/eslint@8.4.8:
|
||||
resolution: {integrity: sha512-zUCKQI1bUCTi+0kQs5ZQzQ/XILWRLIlh15FXWNykJ+NG3TMKMVvwwC6GP3DR1Ylga15fB7iAExSzc4PNlR5i3w==}
|
||||
/@types/eslint@8.4.1:
|
||||
resolution: {integrity: sha512-GE44+DNEyxxh2Kc6ro/VkIj+9ma0pO0bwv9+uHSyBrikYOHr8zYcdPvnBOp1aw8s+CjRvuSx7CyWqRrNFQ59mA==}
|
||||
dependencies:
|
||||
'@types/estree': 1.0.0
|
||||
'@types/json-schema': 7.0.11
|
||||
'@types/estree': 1.0.1
|
||||
'@types/json-schema': 7.0.12
|
||||
dev: true
|
||||
|
||||
/@types/eslint@8.44.2:
|
||||
resolution: {integrity: sha512-sdPRb9K6iL5XZOmBubg8yiFp5yS/JdUDQsq5e6h95km91MCYMuvp7mh1fjPEYUhvHepKpZOjnEaMBR4PxjWDzg==}
|
||||
dependencies:
|
||||
'@types/estree': 1.0.1
|
||||
'@types/json-schema': 7.0.12
|
||||
dev: true
|
||||
|
||||
/@types/estree@0.0.39:
|
||||
@ -10118,10 +10107,6 @@ packages:
|
||||
resolution: {integrity: sha512-CuPgU6f3eT/XgKKPqKd/gLZV1Xmvf1a2R5POBOGQa6uv82xpls89HU5zKeVoyR8XzHd1RGNOlQlvUe3CFkjWNQ==}
|
||||
dev: true
|
||||
|
||||
/@types/estree@1.0.0:
|
||||
resolution: {integrity: sha512-WulqXMDUTYAXCjZnk6JtIHPigp55cVtDgDrO2gHRwhyJto21+1zbVCtOYB2L1F9w4qCQ0rOGWBnBe0FNTiEJIQ==}
|
||||
dev: true
|
||||
|
||||
/@types/estree@1.0.1:
|
||||
resolution: {integrity: sha512-LG4opVs2ANWZ1TJoKc937iMmNstM/d0ae1vNbnBvBhqCSezgVUOzcLCqbI5elV8Vy6WKwKjaqR+zO9VKirBBCA==}
|
||||
dev: true
|
||||
@ -10277,6 +10262,7 @@ packages:
|
||||
|
||||
/@types/json-schema@7.0.11:
|
||||
resolution: {integrity: sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ==}
|
||||
dev: false
|
||||
|
||||
/@types/json-schema@7.0.12:
|
||||
resolution: {integrity: sha512-Hr5Jfhc9eYOQNPYO5WLDq/n4jqijdHNlDXjuAQkkt+mWdQR+XJToOHrsD4cPaMXpn6KO7y2+wM8AZEs8VpBLVA==}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user