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/core": "^1.3.51",
|
||||||
"@swc/jest": "^0.2.20",
|
"@swc/jest": "^0.2.20",
|
||||||
"@testing-library/react": "13.4.0",
|
"@testing-library/react": "13.4.0",
|
||||||
"@types/css-minimizer-webpack-plugin": "^3.2.1",
|
|
||||||
"@types/cytoscape": "^3.18.2",
|
"@types/cytoscape": "^3.18.2",
|
||||||
"@types/detect-port": "^1.3.2",
|
"@types/detect-port": "^1.3.2",
|
||||||
"@types/eslint": "~8.4.1",
|
"@types/eslint": "~8.44.2",
|
||||||
"@types/express": "4.17.14",
|
"@types/express": "4.17.14",
|
||||||
"@types/flat": "^5.0.1",
|
"@types/flat": "^5.0.1",
|
||||||
"@types/fs-extra": "^11.0.0",
|
"@types/fs-extra": "^11.0.0",
|
||||||
|
|||||||
@ -4,13 +4,17 @@ import {
|
|||||||
joinPathFragments,
|
joinPathFragments,
|
||||||
runTasksInSerial,
|
runTasksInSerial,
|
||||||
Tree,
|
Tree,
|
||||||
updateJson,
|
|
||||||
} from '@nx/devkit';
|
} from '@nx/devkit';
|
||||||
import { Linter, lintProjectGenerator } from '@nx/linter';
|
import { Linter, lintProjectGenerator } from '@nx/linter';
|
||||||
import { mapLintPattern } from '@nx/linter/src/generators/lint-project/lint-project';
|
import { mapLintPattern } from '@nx/linter/src/generators/lint-project/lint-project';
|
||||||
import { addAngularEsLintDependencies } from './lib/add-angular-eslint-dependencies';
|
import { addAngularEsLintDependencies } from './lib/add-angular-eslint-dependencies';
|
||||||
import { extendAngularEslintJson } from './lib/create-eslint-configuration';
|
|
||||||
import type { AddLintingGeneratorSchema } from './schema';
|
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(
|
export async function addLintingGenerator(
|
||||||
tree: Tree,
|
tree: Tree,
|
||||||
@ -35,11 +39,57 @@ export async function addLintingGenerator(
|
|||||||
});
|
});
|
||||||
tasks.push(lintTask);
|
tasks.push(lintTask);
|
||||||
|
|
||||||
updateJson(
|
if (isEslintConfigSupported(tree)) {
|
||||||
tree,
|
const eslintFile = findEslintFile(tree, options.projectRoot);
|
||||||
joinPathFragments(options.projectRoot, '.eslintrc.json'),
|
// keep parser options if they exist
|
||||||
(json) => extendAngularEslintJson(json, options)
|
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) {
|
if (!options.skipPackageJson) {
|
||||||
const installTask = addAngularEsLintDependencies(tree);
|
const installTask = addAngularEsLintDependencies(tree);
|
||||||
|
|||||||
@ -8,6 +8,9 @@ type EslintExtensionSchema = {
|
|||||||
prefix: string;
|
prefix: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @deprecated Use tools from `@nx/linter/src/generators/utils/eslint-file` instead
|
||||||
|
*/
|
||||||
export const extendAngularEslintJson = (
|
export const extendAngularEslintJson = (
|
||||||
json: Linter.Config,
|
json: Linter.Config,
|
||||||
options: EslintExtensionSchema
|
options: EslintExtensionSchema
|
||||||
|
|||||||
@ -21,6 +21,7 @@ describe('Cypress e2e configuration', () => {
|
|||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
tree = createTreeWithEmptyWorkspace({ layout: 'apps-libs' });
|
tree = createTreeWithEmptyWorkspace({ layout: 'apps-libs' });
|
||||||
|
tree.write('.eslintrc.json', '{}'); // we are explicitly checking for existance of config type
|
||||||
});
|
});
|
||||||
|
|
||||||
afterAll(() => {
|
afterAll(() => {
|
||||||
|
|||||||
@ -5,12 +5,19 @@ import {
|
|||||||
readProjectConfiguration,
|
readProjectConfiguration,
|
||||||
runTasksInSerial,
|
runTasksInSerial,
|
||||||
Tree,
|
Tree,
|
||||||
updateJson,
|
|
||||||
} from '@nx/devkit';
|
} from '@nx/devkit';
|
||||||
import { Linter, lintProjectGenerator } from '@nx/linter';
|
import { Linter, lintProjectGenerator } from '@nx/linter';
|
||||||
import { globalJavaScriptOverrides } from '@nx/linter/src/generators/init/global-eslint-config';
|
|
||||||
import { installedCypressVersion } from './cypress-version';
|
import { installedCypressVersion } from './cypress-version';
|
||||||
import { eslintPluginCypressVersion } from './versions';
|
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 {
|
export interface CyLinterOptions {
|
||||||
project: string;
|
project: string;
|
||||||
@ -42,7 +49,8 @@ export async function addLinterToCyProject(
|
|||||||
const tasks: GeneratorCallback[] = [];
|
const tasks: GeneratorCallback[] = [];
|
||||||
const projectConfig = readProjectConfiguration(tree, options.project);
|
const projectConfig = readProjectConfiguration(tree, options.project);
|
||||||
|
|
||||||
if (!tree.exists(joinPathFragments(projectConfig.root, '.eslintrc.json'))) {
|
const eslintFile = findEslintFile(tree, projectConfig.root);
|
||||||
|
if (!eslintFile) {
|
||||||
tasks.push(
|
tasks.push(
|
||||||
await lintProjectGenerator(tree, {
|
await lintProjectGenerator(tree, {
|
||||||
project: options.project,
|
project: options.project,
|
||||||
@ -73,19 +81,33 @@ export async function addLinterToCyProject(
|
|||||||
: () => {}
|
: () => {}
|
||||||
);
|
);
|
||||||
|
|
||||||
updateJson(
|
if (isEslintConfigSupported(tree)) {
|
||||||
tree,
|
const overrides = [];
|
||||||
joinPathFragments(projectConfig.root, '.eslintrc.json'),
|
if (options.rootProject) {
|
||||||
(json) => {
|
addPluginsToLintConfig(tree, projectConfig.root, '@nx');
|
||||||
if (options.rootProject) {
|
overrides.push(javaScriptOverride);
|
||||||
json.plugins = ['@nx'];
|
}
|
||||||
json.extends = ['plugin:cypress/recommended'];
|
addExtendsToLintConfig(
|
||||||
} else {
|
tree,
|
||||||
json.extends = ['plugin:cypress/recommended', ...json.extends];
|
projectConfig.root,
|
||||||
}
|
'plugin:cypress/recommended'
|
||||||
json.overrides ??= [];
|
);
|
||||||
const globals = options.rootProject ? [globalJavaScriptOverrides] : [];
|
const cyVersion = installedCypressVersion();
|
||||||
const override = {
|
/**
|
||||||
|
* 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'],
|
files: ['*.ts', '*.tsx', '*.js', '*.jsx'],
|
||||||
parserOptions: !options.setParserOptionsProject
|
parserOptions: !options.setParserOptionsProject
|
||||||
? undefined
|
? undefined
|
||||||
@ -93,40 +115,32 @@ export async function addLinterToCyProject(
|
|||||||
project: `${projectConfig.root}/tsconfig.*?.json`,
|
project: `${projectConfig.root}/tsconfig.*?.json`,
|
||||||
},
|
},
|
||||||
rules: {},
|
rules: {},
|
||||||
};
|
});
|
||||||
const cyFiles = [
|
if (addCy6Override) {
|
||||||
{
|
overrides.push(cy6Override);
|
||||||
...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);
|
|
||||||
}
|
}
|
||||||
|
replaceOverridesInLintConfig(tree, projectConfig.root, overrides);
|
||||||
const cyVersion = installedCypressVersion();
|
} else {
|
||||||
if (cyVersion && cyVersion < 7) {
|
overrides.unshift({
|
||||||
/**
|
files: [
|
||||||
* We need this override because we enabled allowJS in the tsconfig to allow for JS based Cypress tests.
|
'*.cy.{ts,js,tsx,jsx}',
|
||||||
* That however leads to issues with the CommonJS Cypress plugin file.
|
`${options.cypressDir}/**/*.{ts,js,tsx,jsx}`,
|
||||||
*/
|
],
|
||||||
json.overrides.push({
|
parserOptions: !options.setParserOptionsProject
|
||||||
files: [`${options.cypressDir}/plugins/index.js`],
|
? undefined
|
||||||
rules: {
|
: {
|
||||||
'@typescript-eslint/no-var-requires': 'off',
|
project: `${projectConfig.root}/tsconfig.*?.json`,
|
||||||
'no-undef': 'off',
|
},
|
||||||
},
|
rules: {},
|
||||||
});
|
});
|
||||||
|
if (addCy6Override) {
|
||||||
|
overrides.push(cy6Override);
|
||||||
}
|
}
|
||||||
return json;
|
overrides.forEach((override) =>
|
||||||
|
addOverrideToLintConfig(tree, projectConfig.root, override)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
);
|
}
|
||||||
|
|
||||||
return runTasksInSerial(...tasks);
|
return runTasksInSerial(...tasks);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,10 +4,13 @@ import {
|
|||||||
joinPathFragments,
|
joinPathFragments,
|
||||||
runTasksInSerial,
|
runTasksInSerial,
|
||||||
Tree,
|
Tree,
|
||||||
updateJson,
|
|
||||||
} from '@nx/devkit';
|
} from '@nx/devkit';
|
||||||
import { extendReactEslintJson, extraEslintDependencies } from '@nx/react';
|
import { extraEslintDependencies } from '@nx/react';
|
||||||
import { NormalizedSchema } from './normalize-options';
|
import { NormalizedSchema } from './normalize-options';
|
||||||
|
import {
|
||||||
|
addExtendsToLintConfig,
|
||||||
|
isEslintConfigSupported,
|
||||||
|
} from '@nx/linter/src/generators/utils/eslint-file';
|
||||||
|
|
||||||
export async function addLinting(host: Tree, options: NormalizedSchema) {
|
export async function addLinting(host: Tree, options: NormalizedSchema) {
|
||||||
if (options.linter === Linter.None) {
|
if (options.linter === Linter.None) {
|
||||||
@ -24,11 +27,9 @@ export async function addLinting(host: Tree, options: NormalizedSchema) {
|
|||||||
skipFormat: true,
|
skipFormat: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
updateJson(
|
if (isEslintConfigSupported(host)) {
|
||||||
host,
|
addExtendsToLintConfig(host, options.e2eProjectRoot, 'plugin:@nx/react');
|
||||||
joinPathFragments(options.e2eProjectRoot, '.eslintrc.json'),
|
}
|
||||||
extendReactEslintJson
|
|
||||||
);
|
|
||||||
|
|
||||||
const installTask = addDependenciesToPackageJson(
|
const installTask = addDependenciesToPackageJson(
|
||||||
host,
|
host,
|
||||||
|
|||||||
@ -2,16 +2,15 @@ import { Linter, lintProjectGenerator } from '@nx/linter';
|
|||||||
import {
|
import {
|
||||||
addDependenciesToPackageJson,
|
addDependenciesToPackageJson,
|
||||||
GeneratorCallback,
|
GeneratorCallback,
|
||||||
joinPathFragments,
|
|
||||||
runTasksInSerial,
|
runTasksInSerial,
|
||||||
Tree,
|
Tree,
|
||||||
updateJson,
|
|
||||||
} from '@nx/devkit';
|
} from '@nx/devkit';
|
||||||
|
import { extraEslintDependencies } from '@nx/react/src/utils/lint';
|
||||||
import {
|
import {
|
||||||
extendReactEslintJson,
|
addExtendsToLintConfig,
|
||||||
extraEslintDependencies,
|
addIgnoresToLintConfig,
|
||||||
} from '@nx/react/src/utils/lint';
|
isEslintConfigSupported,
|
||||||
import type { Linter as ESLintLinter } from 'eslint';
|
} from '@nx/linter/src/generators/utils/eslint-file';
|
||||||
|
|
||||||
interface NormalizedSchema {
|
interface NormalizedSchema {
|
||||||
linter?: Linter;
|
linter?: Linter;
|
||||||
@ -39,24 +38,15 @@ export async function addLinting(host: Tree, options: NormalizedSchema) {
|
|||||||
|
|
||||||
tasks.push(lintTask);
|
tasks.push(lintTask);
|
||||||
|
|
||||||
updateJson(
|
if (isEslintConfigSupported(host)) {
|
||||||
host,
|
addExtendsToLintConfig(host, options.projectRoot, 'plugin:@nx/react');
|
||||||
joinPathFragments(options.projectRoot, '.eslintrc.json'),
|
addIgnoresToLintConfig(host, options.projectRoot, [
|
||||||
(json: ESLintLinter.Config) => {
|
'.expo',
|
||||||
json = extendReactEslintJson(json);
|
'web-build',
|
||||||
|
'cache',
|
||||||
json.ignorePatterns = [
|
'dist',
|
||||||
...json.ignorePatterns,
|
]);
|
||||||
'.expo',
|
}
|
||||||
'node_modules',
|
|
||||||
'web-build',
|
|
||||||
'cache',
|
|
||||||
'dist',
|
|
||||||
];
|
|
||||||
|
|
||||||
return json;
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!options.skipPackageJson) {
|
if (!options.skipPackageJson) {
|
||||||
const installTask = await addDependenciesToPackageJson(
|
const installTask = await addDependenciesToPackageJson(
|
||||||
|
|||||||
@ -256,20 +256,24 @@ export async function addLint(
|
|||||||
setParserOptionsProject: options.setParserOptionsProject,
|
setParserOptionsProject: options.setParserOptionsProject,
|
||||||
rootProject: options.rootProject,
|
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.
|
// But we need to set the package.json checks.
|
||||||
if (options.rootProject) {
|
if (options.rootProject) {
|
||||||
updateJson(tree, '.eslintrc.json', (json) => {
|
const {
|
||||||
json.overrides ??= [];
|
addOverrideToLintConfig,
|
||||||
json.overrides.push({
|
isEslintConfigSupported,
|
||||||
|
// nx-ignore-next-line
|
||||||
|
} = require('@nx/linter/src/generators/utils/eslint-file');
|
||||||
|
|
||||||
|
if (isEslintConfigSupported(tree)) {
|
||||||
|
addOverrideToLintConfig(tree, '', {
|
||||||
files: ['*.json'],
|
files: ['*.json'],
|
||||||
parser: 'jsonc-eslint-parser',
|
parser: 'jsonc-eslint-parser',
|
||||||
rules: {
|
rules: {
|
||||||
'@nx/dependency-checks': 'error',
|
'@nx/dependency-checks': 'error',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
return json;
|
}
|
||||||
});
|
|
||||||
}
|
}
|
||||||
return task;
|
return task;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { ExecutorContext, joinPathFragments } from '@nx/devkit';
|
import { ExecutorContext, joinPathFragments, workspaceRoot } from '@nx/devkit';
|
||||||
import { ESLint } from 'eslint';
|
import { ESLint } from 'eslint';
|
||||||
import { existsSync, mkdirSync, writeFileSync } from 'fs';
|
import { existsSync, mkdirSync, writeFileSync } from 'fs';
|
||||||
import { dirname, resolve } from 'path';
|
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
|
* 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
|
* their root ESLint config to use eslint.config.js
|
||||||
*/
|
*/
|
||||||
const useFlatConfig = existsSync(
|
const hasFlatConfig = existsSync(
|
||||||
joinPathFragments(systemRoot, 'eslint.config.js')
|
joinPathFragments(workspaceRoot, 'eslint.config.js')
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!eslintConfigPath && useFlatConfig) {
|
if (!eslintConfigPath && hasFlatConfig) {
|
||||||
const projectRoot =
|
const projectRoot =
|
||||||
context.projectsConfigurations.projects[context.projectName].root;
|
context.projectsConfigurations.projects[context.projectName].root;
|
||||||
eslintConfigPath = joinPathFragments(projectRoot, 'eslint.config.js');
|
eslintConfigPath = joinPathFragments(projectRoot, 'eslint.config.js');
|
||||||
@ -59,7 +59,7 @@ export default async function run(
|
|||||||
const { eslint, ESLint } = await resolveAndInstantiateESLint(
|
const { eslint, ESLint } = await resolveAndInstantiateESLint(
|
||||||
eslintConfigPath,
|
eslintConfigPath,
|
||||||
normalizedOptions,
|
normalizedOptions,
|
||||||
useFlatConfig
|
hasFlatConfig
|
||||||
);
|
);
|
||||||
|
|
||||||
const version = ESLint.version?.split('.');
|
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)
|
.filter((pattern) => !!pattern)
|
||||||
.map((pattern) => `- '${pattern}'`);
|
.map((pattern) => `- '${pattern}'`);
|
||||||
if (ignoredPatterns.length) {
|
if (ignoredPatterns.length) {
|
||||||
const ignoreSection = useFlatConfig
|
const ignoreSection = hasFlatConfig
|
||||||
? `'ignores' configuration`
|
? `'ignores' configuration`
|
||||||
: `'.eslintignore' file`;
|
: `'.eslintignore' file`;
|
||||||
throw new Error(
|
throw new Error(
|
||||||
|
|||||||
@ -19,7 +19,7 @@ export interface Schema extends JsonObject {
|
|||||||
cacheStrategy: 'content' | 'metadata' | null;
|
cacheStrategy: 'content' | 'metadata' | null;
|
||||||
rulesdir: string[];
|
rulesdir: string[];
|
||||||
resolvePluginsRelativeTo: string | null;
|
resolvePluginsRelativeTo: string | null;
|
||||||
reportUnusedDisableDirectives: Linter.RuleLevel | null;
|
reportUnusedDisableDirectives: Linter.StringSeverity | null;
|
||||||
printConfig?: string | 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,
|
readJson,
|
||||||
} from '@nx/devkit';
|
} from '@nx/devkit';
|
||||||
import { join } from 'path';
|
import { join } from 'path';
|
||||||
import { ESLint, Linter } from 'eslint';
|
import { ESLint } from 'eslint';
|
||||||
import * as ts from 'typescript';
|
import * as ts from 'typescript';
|
||||||
import { generateAst, generateRequire } from './generate-ast';
|
|
||||||
import { eslintrcVersion } from '../../../utils/versions';
|
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.
|
* Converts an ESLint JSON config to a flat config.
|
||||||
@ -38,7 +47,16 @@ export function convertEslintJsonToFlatConfig(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (config.parser) {
|
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) {
|
if (config.parserOptions) {
|
||||||
@ -129,7 +147,6 @@ export function convertEslintJsonToFlatConfig(
|
|||||||
|
|
||||||
if (config.overrides) {
|
if (config.overrides) {
|
||||||
config.overrides.forEach((override) => {
|
config.overrides.forEach((override) => {
|
||||||
updateFiles(override, root);
|
|
||||||
if (
|
if (
|
||||||
override.env ||
|
override.env ||
|
||||||
override.extends ||
|
override.extends ||
|
||||||
@ -137,10 +154,8 @@ export function convertEslintJsonToFlatConfig(
|
|||||||
override.parser
|
override.parser
|
||||||
) {
|
) {
|
||||||
isFlatCompatNeeded = true;
|
isFlatCompatNeeded = true;
|
||||||
addFlattenedOverride(override, exportElements);
|
|
||||||
} else {
|
|
||||||
exportElements.push(generateAst(override));
|
|
||||||
}
|
}
|
||||||
|
exportElements.push(generateFlatOverride(override, root));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -179,20 +194,8 @@ export function convertEslintJsonToFlatConfig(
|
|||||||
exportElements,
|
exportElements,
|
||||||
isFlatCompatNeeded
|
isFlatCompatNeeded
|
||||||
);
|
);
|
||||||
const printer = ts.createPrinter({ newLine: ts.NewLineKind.LineFeed });
|
const content = stringifyNodeList(nodeList, root, destinationFile);
|
||||||
const resultFile = ts.createSourceFile(
|
tree.write(join(root, destinationFile), content);
|
||||||
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);
|
|
||||||
|
|
||||||
if (isFlatCompatNeeded) {
|
if (isFlatCompatNeeded) {
|
||||||
addDependenciesToPackageJson(
|
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
|
// add parsed extends to export blocks and add import statements
|
||||||
function addExtends(
|
function addExtends(
|
||||||
importsMap: Map<string, string | string[]>,
|
importsMap: Map<string, string | string[]>,
|
||||||
@ -254,9 +228,7 @@ function addExtends(
|
|||||||
.forEach((imp, index) => {
|
.forEach((imp, index) => {
|
||||||
if (imp.match(/\.eslintrc(.base)?\.json$/)) {
|
if (imp.match(/\.eslintrc(.base)?\.json$/)) {
|
||||||
const localName = index ? `baseConfig${index}` : 'baseConfig';
|
const localName = index ? `baseConfig${index}` : 'baseConfig';
|
||||||
configBlocks.push(
|
configBlocks.push(generateSpreadElement(localName));
|
||||||
ts.factory.createSpreadElement(ts.factory.createIdentifier(localName))
|
|
||||||
);
|
|
||||||
const newImport = imp.replace(
|
const newImport = imp.replace(
|
||||||
/^(.*)\.eslintrc(.base)?\.json$/,
|
/^(.*)\.eslintrc(.base)?\.json$/,
|
||||||
'$1eslint$2.config.js'
|
'$1eslint$2.config.js'
|
||||||
@ -311,36 +283,12 @@ function addExtends(
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
const pluginExtendsSpread = ts.factory.createSpreadElement(
|
configBlocks.push(generatePluginExtendsElement(eslintrcConfigs));
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return isFlatCompatNeeded;
|
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(
|
function addPlugins(
|
||||||
importsMap: Map<string, string | string[]>,
|
importsMap: Map<string, string | string[]>,
|
||||||
configBlocks: ts.Expression[],
|
configBlocks: ts.Expression[],
|
||||||
@ -382,143 +330,3 @@ function addPlugins(
|
|||||||
);
|
);
|
||||||
configBlocks.push(pluginsAst);
|
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.
|
* This configuration is intended to apply to all TypeScript source files.
|
||||||
* See the eslint-plugin package for what is in the referenced shareable config.
|
* See the eslint-plugin package for what is in the referenced shareable config.
|
||||||
*/
|
*/
|
||||||
export const globalTypeScriptOverrides = {
|
export const typeScriptOverride = {
|
||||||
files: ['*.ts', '*.tsx'],
|
files: ['*.ts', '*.tsx'],
|
||||||
extends: ['plugin:@nx/typescript'],
|
extends: ['plugin:@nx/typescript'],
|
||||||
/**
|
/**
|
||||||
@ -18,7 +27,7 @@ export const globalTypeScriptOverrides = {
|
|||||||
* This configuration is intended to apply to all JavaScript source files.
|
* This configuration is intended to apply to all JavaScript source files.
|
||||||
* See the eslint-plugin package for what is in the referenced shareable config.
|
* See the eslint-plugin package for what is in the referenced shareable config.
|
||||||
*/
|
*/
|
||||||
export const globalJavaScriptOverrides = {
|
export const javaScriptOverride = {
|
||||||
files: ['*.js', '*.jsx'],
|
files: ['*.js', '*.jsx'],
|
||||||
extends: ['plugin:@nx/javascript'],
|
extends: ['plugin:@nx/javascript'],
|
||||||
/**
|
/**
|
||||||
@ -28,25 +37,11 @@ export const globalJavaScriptOverrides = {
|
|||||||
rules: {},
|
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
|
* This configuration is intended to apply to all "source code" (but not
|
||||||
* markup like HTML, or other custom file types like GraphQL)
|
* markup like HTML, or other custom file types like GraphQL)
|
||||||
*/
|
*/
|
||||||
export const moduleBoundariesOverride = {
|
const moduleBoundariesOverride = {
|
||||||
files: ['*.ts', '*.tsx', '*.js', '*.jsx'],
|
files: ['*.ts', '*.tsx', '*.js', '*.jsx'],
|
||||||
rules: {
|
rules: {
|
||||||
'@nx/enforce-module-boundaries': [
|
'@nx/enforce-module-boundaries': [
|
||||||
@ -57,14 +52,26 @@ export const moduleBoundariesOverride = {
|
|||||||
depConstraints: [{ sourceTag: '*', onlyDependOnLibsWithTags: ['*'] }],
|
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 = (
|
export const getGlobalEsLintConfiguration = (
|
||||||
unitTestRunner?: string,
|
unitTestRunner?: string,
|
||||||
rootProject?: boolean
|
rootProject?: boolean
|
||||||
) => {
|
): Linter.Config => {
|
||||||
const config: LinterType.Config = {
|
const config: Linter.Config = {
|
||||||
root: true,
|
root: true,
|
||||||
ignorePatterns: rootProject ? ['!**/*'] : ['**/*'],
|
ignorePatterns: rootProject ? ['!**/*'] : ['**/*'],
|
||||||
plugins: ['@nx'],
|
plugins: ['@nx'],
|
||||||
@ -77,18 +84,44 @@ export const getGlobalEsLintConfiguration = (
|
|||||||
*/
|
*/
|
||||||
overrides: [
|
overrides: [
|
||||||
...(rootProject ? [] : [moduleBoundariesOverride]),
|
...(rootProject ? [] : [moduleBoundariesOverride]),
|
||||||
globalTypeScriptOverrides,
|
typeScriptOverride,
|
||||||
globalJavaScriptOverrides,
|
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;
|
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 {
|
import {
|
||||||
|
addDependenciesToPackageJson,
|
||||||
joinPathFragments,
|
joinPathFragments,
|
||||||
offsetFromRoot,
|
offsetFromRoot,
|
||||||
ProjectConfiguration,
|
ProjectConfiguration,
|
||||||
@ -8,19 +9,46 @@ import {
|
|||||||
writeJson,
|
writeJson,
|
||||||
} from '@nx/devkit';
|
} from '@nx/devkit';
|
||||||
import { dirname } from 'path';
|
import { dirname } from 'path';
|
||||||
import { findEslintFile } from '../utils/eslint-file';
|
import { findEslintFile, isEslintConfigSupported } from '../utils/eslint-file';
|
||||||
import { getGlobalEsLintConfiguration } from './global-eslint-config';
|
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(
|
export function migrateConfigToMonorepoStyle(
|
||||||
projects: ProjectConfiguration[],
|
projects: ProjectConfiguration[],
|
||||||
tree: Tree,
|
tree: Tree,
|
||||||
unitTestRunner: string
|
unitTestRunner: string
|
||||||
): void {
|
): void {
|
||||||
writeJson(
|
if (useFlatConfig(tree)) {
|
||||||
tree,
|
// we need this for the compat
|
||||||
'.eslintrc.base.json',
|
addDependenciesToPackageJson(
|
||||||
getGlobalEsLintConfiguration(unitTestRunner)
|
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
|
// update extens in all projects' eslint configs
|
||||||
projects.forEach((project) => {
|
projects.forEach((project) => {
|
||||||
@ -47,49 +75,72 @@ export function findLintTarget(
|
|||||||
}
|
}
|
||||||
|
|
||||||
function migrateEslintFile(projectEslintPath: string, tree: Tree) {
|
function migrateEslintFile(projectEslintPath: string, tree: Tree) {
|
||||||
if (
|
if (isEslintConfigSupported(tree)) {
|
||||||
projectEslintPath.endsWith('.json') ||
|
if (useFlatConfig(tree)) {
|
||||||
projectEslintPath.endsWith('.eslintrc')
|
let config = tree.read(projectEslintPath, 'utf-8');
|
||||||
) {
|
// remove @nx plugin
|
||||||
updateJson(tree, projectEslintPath, (json) => {
|
config = removePlugin(config, '@nx', '@nx/eslint-plugin-nx');
|
||||||
// we have a new root now
|
// extend eslint.base.config.js
|
||||||
delete json.root;
|
config = addImportToFlatConfig(
|
||||||
// remove nrwl/nx plugins
|
config,
|
||||||
if (json.plugins) {
|
'baseConfig',
|
||||||
json.plugins = json.plugins.filter(
|
`${offsetFromRoot(dirname(projectEslintPath))}eslint.base.config.js`
|
||||||
(p) => p !== '@nx' && p !== '@nrwl/nx'
|
);
|
||||||
);
|
config = addBlockToFlatConfigExport(
|
||||||
if (json.plugins.length === 0) {
|
config,
|
||||||
delete json.plugins;
|
generateSpreadElement('baseConfig'),
|
||||||
}
|
{ insertAtTheEnd: false }
|
||||||
}
|
);
|
||||||
// add extends
|
// cleanup file extends
|
||||||
json.extends = json.extends || [];
|
config = removeCompatExtends(config, [
|
||||||
const pathToRootConfig = `${offsetFromRoot(
|
'plugin:@nx/typescript',
|
||||||
dirname(projectEslintPath)
|
'plugin:@nx/javascript',
|
||||||
)}.eslintrc.base.json`;
|
'plugin:@nrwl/typescript',
|
||||||
if (json.extends.indexOf(pathToRootConfig) === -1) {
|
'plugin:@nrwl/javascript',
|
||||||
json.extends.push(pathToRootConfig);
|
]);
|
||||||
}
|
console.warn('Flat eslint config is not supported yet for migration');
|
||||||
// cleanup overrides
|
tree.write(projectEslintPath, config);
|
||||||
if (json.overrides) {
|
} else {
|
||||||
json.overrides.forEach((override) => {
|
updateJson(tree, projectEslintPath, (json) => {
|
||||||
if (override.extends) {
|
// we have a new root now
|
||||||
override.extends = override.extends.filter(
|
delete json.root;
|
||||||
(ext) =>
|
// remove nrwl/nx plugins
|
||||||
ext !== 'plugin:@nx/typescript' &&
|
if (json.plugins) {
|
||||||
ext !== 'plugin:@nrwl/nx/typescript' &&
|
json.plugins = json.plugins.filter(
|
||||||
ext !== 'plugin:@nx/javascript' &&
|
(p) => p !== '@nx' && p !== '@nrwl/nx'
|
||||||
ext !== 'plugin:@nrwl/nx/javascript'
|
);
|
||||||
);
|
if (json.plugins.length === 0) {
|
||||||
if (override.extends.length === 0) {
|
delete json.plugins;
|
||||||
delete override.extends;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
}
|
// add extends
|
||||||
return json;
|
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;
|
return;
|
||||||
}
|
}
|
||||||
if (
|
if (
|
||||||
@ -99,6 +150,6 @@ function migrateEslintFile(projectEslintPath: string, tree: Tree) {
|
|||||||
console.warn('YAML eslint config is not supported yet for migration');
|
console.warn('YAML eslint config is not supported yet for migration');
|
||||||
}
|
}
|
||||||
if (projectEslintPath.endsWith('.js') || projectEslintPath.endsWith('.cjs')) {
|
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,
|
writeJson,
|
||||||
} from '@nx/devkit';
|
} from '@nx/devkit';
|
||||||
|
|
||||||
import { Linter } from '../utils/linter';
|
import { Linter as LinterEnum } from '../utils/linter';
|
||||||
import { findEslintFile } from '../utils/eslint-file';
|
import {
|
||||||
|
baseEsLintConfigFile,
|
||||||
|
baseEsLintFlatConfigFile,
|
||||||
|
findEslintFile,
|
||||||
|
} from '../utils/eslint-file';
|
||||||
import { join } from 'path';
|
import { join } from 'path';
|
||||||
import { lintInitGenerator } from '../init/init';
|
import { lintInitGenerator } from '../init/init';
|
||||||
|
import type { Linter } from 'eslint';
|
||||||
import {
|
import {
|
||||||
findLintTarget,
|
findLintTarget,
|
||||||
migrateConfigToMonorepoStyle,
|
migrateConfigToMonorepoStyle,
|
||||||
} from '../init/init-migration';
|
} from '../init/init-migration';
|
||||||
import { getProjects } from 'nx/src/generators/utils/project-configuration';
|
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 {
|
interface LintProjectOptions {
|
||||||
project: string;
|
project: string;
|
||||||
linter?: Linter;
|
linter?: LinterEnum;
|
||||||
eslintFilePatterns?: string[];
|
eslintFilePatterns?: string[];
|
||||||
tsConfigPaths?: string[];
|
tsConfigPaths?: string[];
|
||||||
skipFormat: boolean;
|
skipFormat: boolean;
|
||||||
@ -111,60 +123,85 @@ function createEsLintConfiguration(
|
|||||||
setParserOptionsProject: boolean
|
setParserOptionsProject: boolean
|
||||||
) {
|
) {
|
||||||
const eslintConfig = findEslintFile(tree);
|
const eslintConfig = findEslintFile(tree);
|
||||||
writeJson(tree, join(projectConfig.root, `.eslintrc.json`), {
|
const pathToRootConfig = eslintConfig
|
||||||
extends: eslintConfig
|
? `${offsetFromRoot(projectConfig.root)}${eslintConfig}`
|
||||||
? [`${offsetFromRoot(projectConfig.root)}${eslintConfig}`]
|
: undefined;
|
||||||
: undefined,
|
const addDependencyChecks = isBuildableLibraryProject(projectConfig);
|
||||||
// Include project files to be linted since the global one excludes all files.
|
|
||||||
ignorePatterns: ['!**/*'],
|
const overrides: Linter.ConfigOverride<Linter.RulesRecord>[] = [
|
||||||
overrides: [
|
{
|
||||||
{
|
files: ['*.ts', '*.tsx', '*.js', '*.jsx'],
|
||||||
files: ['*.ts', '*.tsx', '*.js', '*.jsx'],
|
/**
|
||||||
/**
|
* NOTE: We no longer set parserOptions.project by default when creating new projects.
|
||||||
* 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
|
||||||
* 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,
|
||||||
* 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
|
||||||
* 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
|
||||||
* parse (and AST tranformation) of the source files it encounters during a lint run, which is much faster and much
|
* less memory intensive.
|
||||||
* 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
|
||||||
* 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
|
||||||
* parserOptions.project), the executor will attempt to look for the particular error typescript-eslint gives you
|
* and provide feedback to the user.
|
||||||
* and provide feedback to the user.
|
*/
|
||||||
*/
|
parserOptions: !setParserOptionsProject
|
||||||
parserOptions: !setParserOptionsProject
|
? undefined
|
||||||
? undefined
|
: {
|
||||||
: {
|
project: [`${projectConfig.root}/tsconfig.*?.json`],
|
||||||
project: [`${projectConfig.root}/tsconfig.*?.json`],
|
},
|
||||||
},
|
/**
|
||||||
/**
|
* Having an empty rules object present makes it more obvious to the user where they would
|
||||||
* Having an empty rules object present makes it more obvious to the user where they would
|
* extend things from if they needed to
|
||||||
* extend things from if they needed to
|
*/
|
||||||
*/
|
rules: {},
|
||||||
rules: {},
|
},
|
||||||
},
|
{
|
||||||
{
|
files: ['*.ts', '*.tsx'],
|
||||||
files: ['*.ts', '*.tsx'],
|
rules: {},
|
||||||
rules: {},
|
},
|
||||||
},
|
{
|
||||||
{
|
files: ['*.js', '*.jsx'],
|
||||||
files: ['*.js', '*.jsx'],
|
rules: {},
|
||||||
rules: {},
|
},
|
||||||
},
|
...(isBuildableLibraryProject(projectConfig)
|
||||||
...(isBuildableLibraryProject(projectConfig)
|
? [
|
||||||
? [
|
{
|
||||||
{
|
files: ['*.json'],
|
||||||
files: ['*.json'],
|
parser: 'jsonc-eslint-parser',
|
||||||
parser: 'jsonc-eslint-parser',
|
rules: {
|
||||||
rules: {
|
'@nx/dependency-checks': 'error',
|
||||||
'@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(
|
function isBuildableLibraryProject(
|
||||||
@ -186,7 +223,10 @@ function isMigrationToMonorepoNeeded(
|
|||||||
tree: Tree
|
tree: Tree
|
||||||
): boolean {
|
): boolean {
|
||||||
// the base config is already created, migration has been done
|
// 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;
|
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 = [
|
export const eslintConfigFileWhitelist = [
|
||||||
'.eslintrc',
|
'.eslintrc',
|
||||||
@ -7,15 +30,19 @@ export const eslintConfigFileWhitelist = [
|
|||||||
'.eslintrc.yaml',
|
'.eslintrc.yaml',
|
||||||
'.eslintrc.yml',
|
'.eslintrc.yml',
|
||||||
'.eslintrc.json',
|
'.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 baseEsLintConfigFile = '.eslintrc.base.json';
|
||||||
|
export const baseEsLintFlatConfigFile = 'eslint.base.config.js';
|
||||||
|
|
||||||
export function findEslintFile(tree: Tree, projectRoot = ''): string | null {
|
export function findEslintFile(tree: Tree, projectRoot = ''): string | null {
|
||||||
if (projectRoot === '' && tree.exists(baseEsLintConfigFile)) {
|
if (projectRoot === '' && tree.exists(baseEsLintConfigFile)) {
|
||||||
return baseEsLintConfigFile;
|
return baseEsLintConfigFile;
|
||||||
}
|
}
|
||||||
|
if (projectRoot === '' && tree.exists(baseEsLintFlatConfigFile)) {
|
||||||
|
return baseEsLintFlatConfigFile;
|
||||||
|
}
|
||||||
for (const file of eslintConfigFileWhitelist) {
|
for (const file of eslintConfigFileWhitelist) {
|
||||||
if (tree.exists(joinPathFragments(projectRoot, file))) {
|
if (tree.exists(joinPathFragments(projectRoot, file))) {
|
||||||
return file;
|
return file;
|
||||||
@ -24,3 +51,322 @@ export function findEslintFile(tree: Tree, projectRoot = ''): string | null {
|
|||||||
|
|
||||||
return 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
|
* 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 pluginIndexPath = joinPathFragments(workspaceLintPluginDir, 'index.ts');
|
||||||
const existingPluginIndexContents = tree.read(pluginIndexPath, 'utf-8');
|
const existingPluginIndexContents = tree.read(pluginIndexPath, 'utf-8');
|
||||||
@ -106,7 +106,7 @@ export async function lintWorkspaceRuleGenerator(
|
|||||||
|
|
||||||
await formatFiles(tree);
|
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": {
|
"rules": {
|
||||||
"@nx/workspace/${options.name}": "error"
|
"@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');
|
const eslintJson = readJson(tree, '/apps/my-app/.eslintrc.json');
|
||||||
expect(eslintJson).toMatchInlineSnapshot(`
|
expect(eslintJson).toMatchInlineSnapshot(`
|
||||||
{
|
{
|
||||||
"env": {
|
|
||||||
"jest": true,
|
|
||||||
},
|
|
||||||
"extends": [
|
"extends": [
|
||||||
"plugin:@nx/react-typescript",
|
"plugin:@nx/react-typescript",
|
||||||
"next",
|
"next",
|
||||||
@ -587,6 +584,14 @@ describe('app', () => {
|
|||||||
".next/**/*",
|
".next/**/*",
|
||||||
],
|
],
|
||||||
"overrides": [
|
"overrides": [
|
||||||
|
{
|
||||||
|
"files": [
|
||||||
|
"*.*",
|
||||||
|
],
|
||||||
|
"rules": {
|
||||||
|
"@next/next/no-html-link-for-pages": "off",
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"files": [
|
"files": [
|
||||||
"*.ts",
|
"*.ts",
|
||||||
@ -615,10 +620,18 @@ describe('app', () => {
|
|||||||
],
|
],
|
||||||
"rules": {},
|
"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,
|
joinPathFragments,
|
||||||
runTasksInSerial,
|
runTasksInSerial,
|
||||||
Tree,
|
Tree,
|
||||||
updateJson,
|
|
||||||
} from '@nx/devkit';
|
} from '@nx/devkit';
|
||||||
import {
|
import { extraEslintDependencies } from '@nx/react/src/utils/lint';
|
||||||
extendReactEslintJson,
|
|
||||||
extraEslintDependencies,
|
|
||||||
} from '@nx/react/src/utils/lint';
|
|
||||||
import { NormalizedSchema } from './normalize-options';
|
import { NormalizedSchema } from './normalize-options';
|
||||||
|
import {
|
||||||
|
addExtendsToLintConfig,
|
||||||
|
addIgnoresToLintConfig,
|
||||||
|
addOverrideToLintConfig,
|
||||||
|
isEslintConfigSupported,
|
||||||
|
updateOverrideInLintConfig,
|
||||||
|
} from '@nx/linter/src/generators/utils/eslint-file';
|
||||||
|
|
||||||
export async function addLinting(
|
export async function addLinting(
|
||||||
host: Tree,
|
host: Tree,
|
||||||
@ -28,70 +31,56 @@ export async function addLinting(
|
|||||||
skipFormat: true,
|
skipFormat: true,
|
||||||
rootProject: options.rootProject,
|
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) {
|
// Turn off @next/next/no-html-link-for-pages since there is an issue with nextjs throwing linting errors
|
||||||
updateJson(
|
// TODO(nicholas): remove after Vercel updates nextjs linter to only lint ["*.ts", "*.tsx", "*.js", "*.jsx"]
|
||||||
|
addOverrideToLintConfig(
|
||||||
host,
|
host,
|
||||||
joinPathFragments(options.appProjectRoot, '.eslintrc.json'),
|
options.appProjectRoot,
|
||||||
(json) => {
|
{
|
||||||
json = extendReactEslintJson(json);
|
files: ['*.*'],
|
||||||
|
rules: {
|
||||||
// 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 = {
|
|
||||||
'@next/next/no-html-link-for-pages': 'off',
|
'@next/next/no-html-link-for-pages': 'off',
|
||||||
...json.rules,
|
},
|
||||||
};
|
},
|
||||||
|
{ insertAtTheEnd: false }
|
||||||
// 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;
|
|
||||||
}
|
|
||||||
);
|
);
|
||||||
|
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(
|
const installTask = addDependenciesToPackageJson(
|
||||||
|
|||||||
@ -11,18 +11,21 @@ import {
|
|||||||
readProjectConfiguration,
|
readProjectConfiguration,
|
||||||
runTasksInSerial,
|
runTasksInSerial,
|
||||||
Tree,
|
Tree,
|
||||||
updateJson,
|
|
||||||
} from '@nx/devkit';
|
} from '@nx/devkit';
|
||||||
import { determineProjectNameAndRootOptions } from '@nx/devkit/src/generators/project-name-and-root-utils';
|
import { determineProjectNameAndRootOptions } from '@nx/devkit/src/generators/project-name-and-root-utils';
|
||||||
import { Linter, lintProjectGenerator } from '@nx/linter';
|
import { Linter, lintProjectGenerator } from '@nx/linter';
|
||||||
import {
|
import {
|
||||||
globalJavaScriptOverrides,
|
javaScriptOverride,
|
||||||
globalTypeScriptOverrides,
|
typeScriptOverride,
|
||||||
} from '@nx/linter/src/generators/init/global-eslint-config';
|
} from '@nx/linter/src/generators/init/global-eslint-config';
|
||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
import { join } from 'path';
|
|
||||||
import { axiosVersion } from '../../utils/versions';
|
import { axiosVersion } from '../../utils/versions';
|
||||||
import { Schema } from './schema';
|
import { Schema } from './schema';
|
||||||
|
import {
|
||||||
|
addPluginsToLintConfig,
|
||||||
|
isEslintConfigSupported,
|
||||||
|
replaceOverridesInLintConfig,
|
||||||
|
} from '@nx/linter/src/generators/utils/eslint-file';
|
||||||
|
|
||||||
export async function e2eProjectGenerator(host: Tree, options: Schema) {
|
export async function e2eProjectGenerator(host: Tree, options: Schema) {
|
||||||
return await e2eProjectGeneratorInternal(host, {
|
return await e2eProjectGeneratorInternal(host, {
|
||||||
@ -119,32 +122,13 @@ export async function e2eProjectGeneratorInternal(
|
|||||||
});
|
});
|
||||||
tasks.push(linterTask);
|
tasks.push(linterTask);
|
||||||
|
|
||||||
updateJson(host, join(options.e2eProjectRoot, '.eslintrc.json'), (json) => {
|
if (options.rootProject && isEslintConfigSupported(host)) {
|
||||||
if (options.rootProject) {
|
addPluginsToLintConfig(host, options.e2eProjectRoot, '@nx');
|
||||||
json.plugins = ['@nx'];
|
replaceOverridesInLintConfig(host, options.e2eProjectRoot, [
|
||||||
json.extends = [];
|
typeScriptOverride,
|
||||||
}
|
javaScriptOverride,
|
||||||
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.skipFormat) {
|
if (!options.skipFormat) {
|
||||||
|
|||||||
@ -217,7 +217,10 @@ function projectHasKarmaConfig(
|
|||||||
function projectHasEslintConfig(
|
function projectHasEslintConfig(
|
||||||
project: AngularJsonProjectConfiguration
|
project: AngularJsonProjectConfiguration
|
||||||
): boolean {
|
): 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 {
|
function replaceNgWithNxInPackageJsonScripts(repoRoot: string): void {
|
||||||
|
|||||||
@ -5,11 +5,17 @@ import {
|
|||||||
readProjectConfiguration,
|
readProjectConfiguration,
|
||||||
runTasksInSerial,
|
runTasksInSerial,
|
||||||
Tree,
|
Tree,
|
||||||
updateJson,
|
|
||||||
} from '@nx/devkit';
|
} from '@nx/devkit';
|
||||||
import { Linter, lintProjectGenerator } from '@nx/linter';
|
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 { eslintPluginPlaywrightVersion } from './versions';
|
||||||
|
import {
|
||||||
|
addExtendsToLintConfig,
|
||||||
|
addOverrideToLintConfig,
|
||||||
|
addPluginsToLintConfig,
|
||||||
|
findEslintFile,
|
||||||
|
isEslintConfigSupported,
|
||||||
|
} from '@nx/linter/src/generators/utils/eslint-file';
|
||||||
|
|
||||||
export interface PlaywrightLinterOptions {
|
export interface PlaywrightLinterOptions {
|
||||||
project: string;
|
project: string;
|
||||||
@ -35,7 +41,8 @@ export async function addLinterToPlaywrightProject(
|
|||||||
const tasks: GeneratorCallback[] = [];
|
const tasks: GeneratorCallback[] = [];
|
||||||
const projectConfig = readProjectConfiguration(tree, options.project);
|
const projectConfig = readProjectConfiguration(tree, options.project);
|
||||||
|
|
||||||
if (!tree.exists(joinPathFragments(projectConfig.root, '.eslintrc.json'))) {
|
const eslintFile = findEslintFile(tree, projectConfig.root);
|
||||||
|
if (!eslintFile) {
|
||||||
tasks.push(
|
tasks.push(
|
||||||
await lintProjectGenerator(tree, {
|
await lintProjectGenerator(tree, {
|
||||||
project: options.project,
|
project: options.project,
|
||||||
@ -66,38 +73,26 @@ export async function addLinterToPlaywrightProject(
|
|||||||
: () => {}
|
: () => {}
|
||||||
);
|
);
|
||||||
|
|
||||||
updateJson(
|
if (isEslintConfigSupported(tree)) {
|
||||||
tree,
|
addExtendsToLintConfig(
|
||||||
joinPathFragments(projectConfig.root, '.eslintrc.json'),
|
tree,
|
||||||
(json) => {
|
projectConfig.root,
|
||||||
if (options.rootProject) {
|
'plugin:playwright/recommended'
|
||||||
json.plugins = ['@nx'];
|
);
|
||||||
json.extends = ['plugin:playwright/recommended'];
|
if (options.rootProject) {
|
||||||
} else {
|
addPluginsToLintConfig(tree, projectConfig.root, '@nx');
|
||||||
json.extends = ['plugin:playwright/recommended', ...json.extends];
|
addOverrideToLintConfig(tree, projectConfig.root, javaScriptOverride);
|
||||||
}
|
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
);
|
addOverrideToLintConfig(tree, projectConfig.root, {
|
||||||
|
files: [`${options.directory}/**/*.{ts,js,tsx,jsx}`],
|
||||||
|
parserOptions: !options.setParserOptionsProject
|
||||||
|
? undefined
|
||||||
|
: {
|
||||||
|
project: `${projectConfig.root}/tsconfig.*?.json`,
|
||||||
|
},
|
||||||
|
rules: {},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return runTasksInSerial(...tasks);
|
return runTasksInSerial(...tasks);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -8,18 +8,22 @@ import {
|
|||||||
readProjectConfiguration,
|
readProjectConfiguration,
|
||||||
TargetConfiguration,
|
TargetConfiguration,
|
||||||
Tree,
|
Tree,
|
||||||
updateJson,
|
|
||||||
updateProjectConfiguration,
|
updateProjectConfiguration,
|
||||||
writeJson,
|
writeJson,
|
||||||
} from '@nx/devkit';
|
} from '@nx/devkit';
|
||||||
|
|
||||||
import type { Linter as ESLint } from 'eslint';
|
|
||||||
|
|
||||||
import type { Schema as EsLintExecutorOptions } from '@nx/linter/src/executors/eslint/schema';
|
import type { Schema as EsLintExecutorOptions } from '@nx/linter/src/executors/eslint/schema';
|
||||||
|
|
||||||
import { PluginLintChecksGeneratorSchema } from './schema';
|
import { PluginLintChecksGeneratorSchema } from './schema';
|
||||||
import { NX_PREFIX } from 'nx/src/utils/logger';
|
import { NX_PREFIX } from 'nx/src/utils/logger';
|
||||||
import { PackageJson, readNxMigrateConfig } from 'nx/src/utils/package-json';
|
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(
|
export default async function pluginLintCheckGenerator(
|
||||||
host: Tree,
|
host: Tree,
|
||||||
@ -101,22 +105,19 @@ export function addMigrationJsonChecks(
|
|||||||
updateProjectConfiguration(host, options.projectName, projectConfiguration);
|
updateProjectConfiguration(host, options.projectName, projectConfiguration);
|
||||||
|
|
||||||
// Update project level eslintrc
|
// Update project level eslintrc
|
||||||
updateJson<ESLint.Config>(
|
updateOverrideInLintConfig(
|
||||||
host,
|
host,
|
||||||
`${projectConfiguration.root}/.eslintrc.json`,
|
projectConfiguration.root,
|
||||||
(c) => {
|
(o) =>
|
||||||
const override = c.overrides.find(
|
Object.keys(o.rules ?? {})?.includes('@nx/nx-plugin-checks') ||
|
||||||
(o) =>
|
Object.keys(o.rules ?? {})?.includes('@nrwl/nx/nx-plugin-checks'),
|
||||||
Object.keys(o.rules ?? {})?.includes('@nx/nx-plugin-checks') ||
|
(o) => {
|
||||||
Object.keys(o.rules ?? {})?.includes('@nrwl/nx/nx-plugin-checks')
|
const fileSet = new Set(Array.isArray(o.files) ? o.files : [o.files]);
|
||||||
);
|
fileSet.add(relativeMigrationsJsonPath);
|
||||||
if (
|
return {
|
||||||
Array.isArray(override?.files) &&
|
...o,
|
||||||
!override.files.includes(relativeMigrationsJsonPath)
|
files: Array.from(fileSet),
|
||||||
) {
|
};
|
||||||
override.files.push(relativeMigrationsJsonPath);
|
|
||||||
}
|
|
||||||
return c;
|
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -179,42 +180,49 @@ function updateProjectEslintConfig(
|
|||||||
options: ProjectConfiguration,
|
options: ProjectConfiguration,
|
||||||
packageJson: PackageJson
|
packageJson: PackageJson
|
||||||
) {
|
) {
|
||||||
// Update the project level lint configuration to specify
|
if (isEslintConfigSupported(host, options.root)) {
|
||||||
// the plugin schema rule for generated files
|
const lookup = (o) =>
|
||||||
const eslintPath = `${options.root}/.eslintrc.json`;
|
Object.keys(o.rules ?? {}).includes('@nx/nx-plugin-checks') ||
|
||||||
if (host.exists(eslintPath)) {
|
Object.keys(o.rules ?? {}).includes('@nrwl/nx/nx-plugin-checks');
|
||||||
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 (newentry) {
|
const files = [
|
||||||
eslintConfig.overrides.push(entry);
|
'./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
|
// This is required, otherwise every json file that is not overriden
|
||||||
// will display false errors in the IDE
|
// will display false errors in the IDE
|
||||||
function updateRootEslintConfig(host: Tree) {
|
function updateRootEslintConfig(host: Tree) {
|
||||||
if (host.exists('.eslintrc.json')) {
|
if (isEslintConfigSupported(host)) {
|
||||||
const rootESLint = readJson<ESLint.Config>(host, '.eslintrc.json');
|
if (
|
||||||
rootESLint.overrides ??= [];
|
!lintConfigHasOverride(
|
||||||
if (!eslintConfigContainsJsonOverride(rootESLint)) {
|
host,
|
||||||
rootESLint.overrides.push({
|
'',
|
||||||
files: '*.json',
|
(o) =>
|
||||||
parser: 'jsonc-eslint-parser',
|
Array.isArray(o.files)
|
||||||
rules: {},
|
? o.files.some((f) => f.match(/\.json$/))
|
||||||
});
|
: !!o.files?.match(/\.json$/),
|
||||||
writeJson(host, '.eslintrc.json', rootESLint);
|
true
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
addOverrideToLintConfig(
|
||||||
|
host,
|
||||||
|
'',
|
||||||
|
{
|
||||||
|
files: '*.json',
|
||||||
|
parser: 'jsonc-eslint-parser',
|
||||||
|
rules: {},
|
||||||
|
},
|
||||||
|
{ checkBaseConfig: true }
|
||||||
|
);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
output.note({
|
output.note({
|
||||||
title: 'Unable to update root eslint config.',
|
title: 'Unable to update root eslint config.',
|
||||||
bodyLines: [
|
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.',
|
'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.',
|
'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);
|
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) {
|
function projectIsEsLintEnabled(project: ProjectConfiguration) {
|
||||||
return !!getEsLintOptions(project);
|
return !!getEsLintOptions(project);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,16 +2,15 @@ import { Linter, lintProjectGenerator } from '@nx/linter';
|
|||||||
import {
|
import {
|
||||||
addDependenciesToPackageJson,
|
addDependenciesToPackageJson,
|
||||||
GeneratorCallback,
|
GeneratorCallback,
|
||||||
joinPathFragments,
|
|
||||||
runTasksInSerial,
|
runTasksInSerial,
|
||||||
Tree,
|
Tree,
|
||||||
updateJson,
|
|
||||||
} from '@nx/devkit';
|
} from '@nx/devkit';
|
||||||
|
import { extraEslintDependencies } from '@nx/react/src/utils/lint';
|
||||||
import {
|
import {
|
||||||
extendReactEslintJson,
|
addExtendsToLintConfig,
|
||||||
extraEslintDependencies,
|
addIgnoresToLintConfig,
|
||||||
} from '@nx/react/src/utils/lint';
|
isEslintConfigSupported,
|
||||||
import type { Linter as ESLintLinter } from 'eslint';
|
} from '@nx/linter/src/generators/utils/eslint-file';
|
||||||
|
|
||||||
interface NormalizedSchema {
|
interface NormalizedSchema {
|
||||||
linter?: Linter;
|
linter?: Linter;
|
||||||
@ -39,22 +38,14 @@ export async function addLinting(host: Tree, options: NormalizedSchema) {
|
|||||||
|
|
||||||
tasks.push(lintTask);
|
tasks.push(lintTask);
|
||||||
|
|
||||||
updateJson(
|
if (isEslintConfigSupported(host)) {
|
||||||
host,
|
addExtendsToLintConfig(host, options.projectRoot, 'plugin:@nx/react');
|
||||||
joinPathFragments(options.projectRoot, '.eslintrc.json'),
|
addIgnoresToLintConfig(host, options.projectRoot, [
|
||||||
(json: ESLintLinter.Config) => {
|
'public',
|
||||||
json = extendReactEslintJson(json);
|
'.cache',
|
||||||
|
'node_modules',
|
||||||
json.ignorePatterns = [
|
]);
|
||||||
...json.ignorePatterns,
|
}
|
||||||
'public',
|
|
||||||
'.cache',
|
|
||||||
'node_modules',
|
|
||||||
];
|
|
||||||
|
|
||||||
return json;
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!options.skipPackageJson) {
|
if (!options.skipPackageJson) {
|
||||||
const installTask = await addDependenciesToPackageJson(
|
const installTask = await addDependenciesToPackageJson(
|
||||||
|
|||||||
@ -1,7 +1,4 @@
|
|||||||
import {
|
import { extraEslintDependencies } from '../../utils/lint';
|
||||||
extendReactEslintJson,
|
|
||||||
extraEslintDependencies,
|
|
||||||
} from '../../utils/lint';
|
|
||||||
import { NormalizedSchema, Schema } from './schema';
|
import { NormalizedSchema, Schema } from './schema';
|
||||||
import { createApplicationFiles } from './lib/create-application-files';
|
import { createApplicationFiles } from './lib/create-application-files';
|
||||||
import { updateSpecConfig } from './lib/update-jest-config';
|
import { updateSpecConfig } from './lib/update-jest-config';
|
||||||
@ -22,7 +19,6 @@ import {
|
|||||||
runTasksInSerial,
|
runTasksInSerial,
|
||||||
stripIndents,
|
stripIndents,
|
||||||
Tree,
|
Tree,
|
||||||
updateJson,
|
|
||||||
} from '@nx/devkit';
|
} from '@nx/devkit';
|
||||||
|
|
||||||
import reactInitGenerator from '../init/init';
|
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 * as chalk from 'chalk';
|
||||||
import { showPossibleWarnings } from './lib/show-possible-warnings';
|
import { showPossibleWarnings } from './lib/show-possible-warnings';
|
||||||
import { addE2e } from './lib/add-e2e';
|
import { addE2e } from './lib/add-e2e';
|
||||||
|
import {
|
||||||
|
addExtendsToLintConfig,
|
||||||
|
isEslintConfigSupported,
|
||||||
|
} from '@nx/linter/src/generators/utils/eslint-file';
|
||||||
|
|
||||||
async function addLinting(host: Tree, options: NormalizedSchema) {
|
async function addLinting(host: Tree, options: NormalizedSchema) {
|
||||||
const tasks: GeneratorCallback[] = [];
|
const tasks: GeneratorCallback[] = [];
|
||||||
@ -63,11 +63,9 @@ async function addLinting(host: Tree, options: NormalizedSchema) {
|
|||||||
});
|
});
|
||||||
tasks.push(lintTask);
|
tasks.push(lintTask);
|
||||||
|
|
||||||
updateJson(
|
if (isEslintConfigSupported(host)) {
|
||||||
host,
|
addExtendsToLintConfig(host, options.appProjectRoot, 'plugin:@nx/react');
|
||||||
joinPathFragments(options.appProjectRoot, '.eslintrc.json'),
|
}
|
||||||
extendReactEslintJson
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!options.skipPackageJson) {
|
if (!options.skipPackageJson) {
|
||||||
const installTask = addDependenciesToPackageJson(
|
const installTask = addDependenciesToPackageJson(
|
||||||
|
|||||||
@ -4,7 +4,6 @@ import {
|
|||||||
ensurePackage,
|
ensurePackage,
|
||||||
getPackageManagerCommand,
|
getPackageManagerCommand,
|
||||||
joinPathFragments,
|
joinPathFragments,
|
||||||
readProjectConfiguration,
|
|
||||||
} from '@nx/devkit';
|
} from '@nx/devkit';
|
||||||
import { webStaticServeGenerator } from '@nx/web';
|
import { webStaticServeGenerator } from '@nx/web';
|
||||||
|
|
||||||
|
|||||||
@ -1,14 +1,14 @@
|
|||||||
import { Tree } from 'nx/src/generators/tree';
|
import { Tree } from 'nx/src/generators/tree';
|
||||||
import { Linter, lintProjectGenerator } from '@nx/linter';
|
import { Linter, lintProjectGenerator } from '@nx/linter';
|
||||||
import { joinPathFragments } from 'nx/src/utils/path';
|
import { joinPathFragments } from 'nx/src/utils/path';
|
||||||
import { updateJson } from 'nx/src/generators/utils/json';
|
|
||||||
import { addDependenciesToPackageJson, runTasksInSerial } from '@nx/devkit';
|
import { addDependenciesToPackageJson, runTasksInSerial } from '@nx/devkit';
|
||||||
|
|
||||||
import { NormalizedSchema } from '../schema';
|
import { NormalizedSchema } from '../schema';
|
||||||
|
import { extraEslintDependencies } from '../../../utils/lint';
|
||||||
import {
|
import {
|
||||||
extendReactEslintJson,
|
addExtendsToLintConfig,
|
||||||
extraEslintDependencies,
|
isEslintConfigSupported,
|
||||||
} from '../../../utils/lint';
|
} from '@nx/linter/src/generators/utils/eslint-file';
|
||||||
|
|
||||||
export async function addLinting(host: Tree, options: NormalizedSchema) {
|
export async function addLinting(host: Tree, options: NormalizedSchema) {
|
||||||
if (options.linter === Linter.EsLint) {
|
if (options.linter === Linter.EsLint) {
|
||||||
@ -25,11 +25,9 @@ export async function addLinting(host: Tree, options: NormalizedSchema) {
|
|||||||
setParserOptionsProject: options.setParserOptionsProject,
|
setParserOptionsProject: options.setParserOptionsProject,
|
||||||
});
|
});
|
||||||
|
|
||||||
updateJson(
|
if (isEslintConfigSupported(host)) {
|
||||||
host,
|
addExtendsToLintConfig(host, options.projectRoot, 'plugin:@nx/react');
|
||||||
joinPathFragments(options.projectRoot, '.eslintrc.json'),
|
}
|
||||||
extendReactEslintJson
|
|
||||||
);
|
|
||||||
|
|
||||||
let installTask = () => {};
|
let installTask = () => {};
|
||||||
if (!options.skipPackageJson) {
|
if (!options.skipPackageJson) {
|
||||||
|
|||||||
@ -1,5 +1,4 @@
|
|||||||
import { offsetFromRoot } from '@nx/devkit';
|
import { Linter } from 'eslint';
|
||||||
import type { Linter } from 'eslint';
|
|
||||||
import {
|
import {
|
||||||
eslintPluginImportVersion,
|
eslintPluginImportVersion,
|
||||||
eslintPluginReactVersion,
|
eslintPluginReactVersion,
|
||||||
@ -17,6 +16,9 @@ export const extraEslintDependencies = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @deprecated Use `addExtendsToLintConfig` from `@nx/linter` instead.
|
||||||
|
*/
|
||||||
export const extendReactEslintJson = (json: Linter.Config) => {
|
export const extendReactEslintJson = (json: Linter.Config) => {
|
||||||
const { extends: pluginExtends, ...config } = json;
|
const { extends: pluginExtends, ...config } = json;
|
||||||
|
|
||||||
|
|||||||
@ -28,7 +28,8 @@ import {
|
|||||||
import { StorybookConfigureSchema } from '../schema';
|
import { StorybookConfigureSchema } from '../schema';
|
||||||
import { UiFramework7 } from '../../../utils/models';
|
import { UiFramework7 } from '../../../utils/models';
|
||||||
import { nxVersion } from '../../../utils/versions';
|
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;
|
const DEFAULT_PORT = 4400;
|
||||||
|
|
||||||
@ -173,7 +174,7 @@ export function createStorybookTsconfigFile(
|
|||||||
if (tree.exists(oldStorybookTsConfigPath)) {
|
if (tree.exists(oldStorybookTsConfigPath)) {
|
||||||
logger.warn(`.storybook/tsconfig.json already exists for this project`);
|
logger.warn(`.storybook/tsconfig.json already exists for this project`);
|
||||||
logger.warn(
|
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.
|
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.
|
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.
|
* which includes *.stories files.
|
||||||
*
|
*
|
||||||
* For TSLint this is done via the builder config, for ESLint this is
|
* 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) {
|
export function updateLintConfig(tree: Tree, schema: StorybookConfigureSchema) {
|
||||||
const { name: projectName } = schema;
|
const { name: projectName } = schema;
|
||||||
@ -382,18 +383,46 @@ export function updateLintConfig(tree: Tree, schema: StorybookConfigureSchema) {
|
|||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
if (tree.exists(join(root, '.eslintrc.json'))) {
|
const eslintFile = findEslintFile(tree, root);
|
||||||
updateJson(tree, join(root, '.eslintrc.json'), (json) => {
|
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') {
|
if (typeof json.parserOptions?.project === 'string') {
|
||||||
json.parserOptions.project = [json.parserOptions.project];
|
json.parserOptions.project = [json.parserOptions.project];
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Array.isArray(json.parserOptions?.project)) {
|
if (json.parserOptions?.project) {
|
||||||
json.parserOptions.project = dedupe([
|
json.parserOptions.project = dedupe([
|
||||||
...json.parserOptions.project,
|
...json.parserOptions.project,
|
||||||
schema.uiFramework === '@storybook/angular'
|
parserConfigPath,
|
||||||
? join(root, '.storybook/tsconfig.json')
|
|
||||||
: join(root, 'tsconfig.storybook.json'),
|
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -402,12 +431,10 @@ export function updateLintConfig(tree: Tree, schema: StorybookConfigureSchema) {
|
|||||||
if (typeof o.parserOptions?.project === 'string') {
|
if (typeof o.parserOptions?.project === 'string') {
|
||||||
o.parserOptions.project = [o.parserOptions.project];
|
o.parserOptions.project = [o.parserOptions.project];
|
||||||
}
|
}
|
||||||
if (Array.isArray(o.parserOptions?.project)) {
|
if (o.parserOptions?.project) {
|
||||||
o.parserOptions.project = dedupe([
|
o.parserOptions.project = dedupe([
|
||||||
...o.parserOptions.project,
|
...o.parserOptions.project,
|
||||||
schema.uiFramework === '@storybook/angular'
|
parserConfigPath,
|
||||||
? join(root, '.storybook/tsconfig.json')
|
|
||||||
: join(root, 'tsconfig.storybook.json'),
|
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -629,15 +656,15 @@ export function rootFileIsTs(
|
|||||||
): boolean {
|
): boolean {
|
||||||
if (tree.exists(`.storybook/${rootFileName}.ts`) && !tsConfiguration) {
|
if (tree.exists(`.storybook/${rootFileName}.ts`) && !tsConfiguration) {
|
||||||
logger.info(
|
logger.info(
|
||||||
`The root Storybook configuration is in TypeScript,
|
`The root Storybook configuration is in TypeScript,
|
||||||
so Nx will generate TypeScript Storybook configuration files
|
so Nx will generate TypeScript Storybook configuration files
|
||||||
in this project's .storybook folder as well.`
|
in this project's .storybook folder as well.`
|
||||||
);
|
);
|
||||||
return true;
|
return true;
|
||||||
} else if (tree.exists(`.storybook/${rootFileName}.js`) && tsConfiguration) {
|
} else if (tree.exists(`.storybook/${rootFileName}.js`) && tsConfiguration) {
|
||||||
logger.info(
|
logger.info(
|
||||||
`The root Storybook configuration is in JavaScript,
|
`The root Storybook configuration is in JavaScript,
|
||||||
so Nx will generate JavaScript Storybook configuration files
|
so Nx will generate JavaScript Storybook configuration files
|
||||||
in this project's .storybook folder as well.`
|
in this project's .storybook folder as well.`
|
||||||
);
|
);
|
||||||
return false;
|
return false;
|
||||||
@ -751,17 +778,13 @@ export function renameAndMoveOldTsConfig(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const projectEsLintFile = joinPathFragments(projectRoot, '.eslintrc.json');
|
const eslintFile = findEslintFile(tree, projectRoot);
|
||||||
|
if (eslintFile) {
|
||||||
if (tree.exists(projectEsLintFile)) {
|
const fileName = joinPathFragments(projectRoot, eslintFile);
|
||||||
updateJson(tree, projectEsLintFile, (json) => {
|
const config = tree.read(fileName, 'utf-8');
|
||||||
const jsonString = JSON.stringify(json);
|
tree.write(
|
||||||
const newJsonString = jsonString.replace(
|
fileName,
|
||||||
/\.storybook\/tsconfig\.json/g,
|
config.replace(/\.storybook\/tsconfig\.json/g, 'tsconfig.storybook.json')
|
||||||
'tsconfig.storybook.json'
|
);
|
||||||
);
|
|
||||||
json = JSON.parse(newJsonString);
|
|
||||||
return json;
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -24,6 +24,7 @@ export function moveProjectFiles(
|
|||||||
'tsconfig.spec.json',
|
'tsconfig.spec.json',
|
||||||
'.babelrc',
|
'.babelrc',
|
||||||
'.eslintrc.json',
|
'.eslintrc.json',
|
||||||
|
'eslint.config.js',
|
||||||
/^jest\.config\.(app|lib)\.[jt]s$/,
|
/^jest\.config\.(app|lib)\.[jt]s$/,
|
||||||
'vite.config.ts',
|
'vite.config.ts',
|
||||||
/^webpack.*\.js$/,
|
/^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)) {
|
if (!allowedExt.includes(ext)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (file === '.eslintrc.json') {
|
if (file === '.eslintrc.json' || file === 'eslint.config.js') {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -108,7 +108,7 @@ export function updateFilesForNonRootProjects(
|
|||||||
if (!allowedExt.includes(ext)) {
|
if (!allowedExt.includes(ext)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (file === '.eslintrc.json') {
|
if (file === '.eslintrc.json' || file === 'eslint.config.js') {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -12,7 +12,7 @@ import { normalizeSchema } from './lib/normalize-schema';
|
|||||||
import { updateBuildTargets } from './lib/update-build-targets';
|
import { updateBuildTargets } from './lib/update-build-targets';
|
||||||
import { updateCypressConfig } from './lib/update-cypress-config';
|
import { updateCypressConfig } from './lib/update-cypress-config';
|
||||||
import { updateDefaultProject } from './lib/update-default-project';
|
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 { updateImplicitDependencies } from './lib/update-implicit-dependencies';
|
||||||
import { updateImports } from './lib/update-imports';
|
import { updateImports } from './lib/update-imports';
|
||||||
import { updateJestConfig } from './lib/update-jest-config';
|
import { updateJestConfig } from './lib/update-jest-config';
|
||||||
@ -48,7 +48,7 @@ export async function moveGenerator(tree: Tree, rawSchema: Schema) {
|
|||||||
updateCypressConfig(tree, schema, projectConfig);
|
updateCypressConfig(tree, schema, projectConfig);
|
||||||
updateJestConfig(tree, schema, projectConfig);
|
updateJestConfig(tree, schema, projectConfig);
|
||||||
updateStorybookConfig(tree, schema, projectConfig);
|
updateStorybookConfig(tree, schema, projectConfig);
|
||||||
updateEslintrcJson(tree, schema, projectConfig);
|
updateEslintConfig(tree, schema, projectConfig);
|
||||||
updateReadme(tree, schema);
|
updateReadme(tree, schema);
|
||||||
updatePackageJson(tree, schema);
|
updatePackageJson(tree, schema);
|
||||||
updateBuildTargets(tree, schema);
|
updateBuildTargets(tree, schema);
|
||||||
|
|||||||
44
pnpm-lock.yaml
generated
44
pnpm-lock.yaml
generated
@ -394,9 +394,6 @@ devDependencies:
|
|||||||
'@testing-library/react':
|
'@testing-library/react':
|
||||||
specifier: 13.4.0
|
specifier: 13.4.0
|
||||||
version: 13.4.0(react-dom@18.2.0)(react@18.2.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':
|
'@types/cytoscape':
|
||||||
specifier: ^3.18.2
|
specifier: ^3.18.2
|
||||||
version: 3.19.9
|
version: 3.19.9
|
||||||
@ -404,8 +401,8 @@ devDependencies:
|
|||||||
specifier: ^1.3.2
|
specifier: ^1.3.2
|
||||||
version: 1.3.2
|
version: 1.3.2
|
||||||
'@types/eslint':
|
'@types/eslint':
|
||||||
specifier: ~8.4.1
|
specifier: ~8.44.2
|
||||||
version: 8.4.8
|
version: 8.44.2
|
||||||
'@types/express':
|
'@types/express':
|
||||||
specifier: 4.17.14
|
specifier: 4.17.14
|
||||||
version: 4.17.14
|
version: 4.17.14
|
||||||
@ -10055,21 +10052,6 @@ packages:
|
|||||||
'@types/node': 18.16.9
|
'@types/node': 18.16.9
|
||||||
dev: true
|
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:
|
/@types/cytoscape@3.19.9:
|
||||||
resolution: {integrity: sha512-oqCx0ZGiBO0UESbjgq052vjDAy2X53lZpMrWqiweMpvVwKw/2IiYDdzPFK6+f4tMfdv9YKEM9raO5bAZc3UYBg==}
|
resolution: {integrity: sha512-oqCx0ZGiBO0UESbjgq052vjDAy2X53lZpMrWqiweMpvVwKw/2IiYDdzPFK6+f4tMfdv9YKEM9raO5bAZc3UYBg==}
|
||||||
dev: true
|
dev: true
|
||||||
@ -10099,15 +10081,22 @@ packages:
|
|||||||
/@types/eslint-scope@3.7.4:
|
/@types/eslint-scope@3.7.4:
|
||||||
resolution: {integrity: sha512-9K4zoImiZc3HlIp6AVUDE4CWYx22a+lhSZMYNpbjW04+YF0KWj4pJXnEMjdnFTiQibFFmElcsasJXDbdI/EPhA==}
|
resolution: {integrity: sha512-9K4zoImiZc3HlIp6AVUDE4CWYx22a+lhSZMYNpbjW04+YF0KWj4pJXnEMjdnFTiQibFFmElcsasJXDbdI/EPhA==}
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/eslint': 8.4.8
|
'@types/eslint': 8.4.1
|
||||||
'@types/estree': 1.0.1
|
'@types/estree': 1.0.1
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
/@types/eslint@8.4.8:
|
/@types/eslint@8.4.1:
|
||||||
resolution: {integrity: sha512-zUCKQI1bUCTi+0kQs5ZQzQ/XILWRLIlh15FXWNykJ+NG3TMKMVvwwC6GP3DR1Ylga15fB7iAExSzc4PNlR5i3w==}
|
resolution: {integrity: sha512-GE44+DNEyxxh2Kc6ro/VkIj+9ma0pO0bwv9+uHSyBrikYOHr8zYcdPvnBOp1aw8s+CjRvuSx7CyWqRrNFQ59mA==}
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/estree': 1.0.0
|
'@types/estree': 1.0.1
|
||||||
'@types/json-schema': 7.0.11
|
'@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
|
dev: true
|
||||||
|
|
||||||
/@types/estree@0.0.39:
|
/@types/estree@0.0.39:
|
||||||
@ -10118,10 +10107,6 @@ packages:
|
|||||||
resolution: {integrity: sha512-CuPgU6f3eT/XgKKPqKd/gLZV1Xmvf1a2R5POBOGQa6uv82xpls89HU5zKeVoyR8XzHd1RGNOlQlvUe3CFkjWNQ==}
|
resolution: {integrity: sha512-CuPgU6f3eT/XgKKPqKd/gLZV1Xmvf1a2R5POBOGQa6uv82xpls89HU5zKeVoyR8XzHd1RGNOlQlvUe3CFkjWNQ==}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
/@types/estree@1.0.0:
|
|
||||||
resolution: {integrity: sha512-WulqXMDUTYAXCjZnk6JtIHPigp55cVtDgDrO2gHRwhyJto21+1zbVCtOYB2L1F9w4qCQ0rOGWBnBe0FNTiEJIQ==}
|
|
||||||
dev: true
|
|
||||||
|
|
||||||
/@types/estree@1.0.1:
|
/@types/estree@1.0.1:
|
||||||
resolution: {integrity: sha512-LG4opVs2ANWZ1TJoKc937iMmNstM/d0ae1vNbnBvBhqCSezgVUOzcLCqbI5elV8Vy6WKwKjaqR+zO9VKirBBCA==}
|
resolution: {integrity: sha512-LG4opVs2ANWZ1TJoKc937iMmNstM/d0ae1vNbnBvBhqCSezgVUOzcLCqbI5elV8Vy6WKwKjaqR+zO9VKirBBCA==}
|
||||||
dev: true
|
dev: true
|
||||||
@ -10277,6 +10262,7 @@ packages:
|
|||||||
|
|
||||||
/@types/json-schema@7.0.11:
|
/@types/json-schema@7.0.11:
|
||||||
resolution: {integrity: sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ==}
|
resolution: {integrity: sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ==}
|
||||||
|
dev: false
|
||||||
|
|
||||||
/@types/json-schema@7.0.12:
|
/@types/json-schema@7.0.12:
|
||||||
resolution: {integrity: sha512-Hr5Jfhc9eYOQNPYO5WLDq/n4jqijdHNlDXjuAQkkt+mWdQR+XJToOHrsD4cPaMXpn6KO7y2+wM8AZEs8VpBLVA==}
|
resolution: {integrity: sha512-Hr5Jfhc9eYOQNPYO5WLDq/n4jqijdHNlDXjuAQkkt+mWdQR+XJToOHrsD4cPaMXpn6KO7y2+wM8AZEs8VpBLVA==}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user