Revert "Revert "feat(linter): add generator for converting to flat co… (#18631)
This commit is contained in:
parent
e2ac4e38e7
commit
968bd38218
@ -6384,6 +6384,14 @@
|
|||||||
"children": [],
|
"children": [],
|
||||||
"isExternal": false,
|
"isExternal": false,
|
||||||
"disableCollapsible": false
|
"disableCollapsible": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "convert-to-flat-config",
|
||||||
|
"path": "/packages/linter/generators/convert-to-flat-config",
|
||||||
|
"name": "convert-to-flat-config",
|
||||||
|
"children": [],
|
||||||
|
"isExternal": false,
|
||||||
|
"disableCollapsible": false
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"isExternal": false,
|
"isExternal": false,
|
||||||
|
|||||||
@ -1140,6 +1140,15 @@
|
|||||||
"originalFilePath": "/packages/linter/src/generators/workspace-rule/schema.json",
|
"originalFilePath": "/packages/linter/src/generators/workspace-rule/schema.json",
|
||||||
"path": "/packages/linter/generators/workspace-rule",
|
"path": "/packages/linter/generators/workspace-rule",
|
||||||
"type": "generator"
|
"type": "generator"
|
||||||
|
},
|
||||||
|
"/packages/linter/generators/convert-to-flat-config": {
|
||||||
|
"description": "Convert an Nx workspace to a Flat ESLint config.",
|
||||||
|
"file": "generated/packages/linter/generators/convert-to-flat-config.json",
|
||||||
|
"hidden": false,
|
||||||
|
"name": "convert-to-flat-config",
|
||||||
|
"originalFilePath": "/packages/linter/src/generators/convert-to-flat-config/schema.json",
|
||||||
|
"path": "/packages/linter/generators/convert-to-flat-config",
|
||||||
|
"type": "generator"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"path": "/packages/linter"
|
"path": "/packages/linter"
|
||||||
|
|||||||
@ -1123,6 +1123,15 @@
|
|||||||
"originalFilePath": "/packages/linter/src/generators/workspace-rule/schema.json",
|
"originalFilePath": "/packages/linter/src/generators/workspace-rule/schema.json",
|
||||||
"path": "linter/generators/workspace-rule",
|
"path": "linter/generators/workspace-rule",
|
||||||
"type": "generator"
|
"type": "generator"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Convert an Nx workspace to a Flat ESLint config.",
|
||||||
|
"file": "generated/packages/linter/generators/convert-to-flat-config.json",
|
||||||
|
"hidden": false,
|
||||||
|
"name": "convert-to-flat-config",
|
||||||
|
"originalFilePath": "/packages/linter/src/generators/convert-to-flat-config/schema.json",
|
||||||
|
"path": "linter/generators/convert-to-flat-config",
|
||||||
|
"type": "generator"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"githubRoot": "https://github.com/nrwl/nx/blob/master",
|
"githubRoot": "https://github.com/nrwl/nx/blob/master",
|
||||||
|
|||||||
@ -0,0 +1,28 @@
|
|||||||
|
{
|
||||||
|
"name": "convert-to-flat-config",
|
||||||
|
"factory": "./src/generators/convert-to-flat-config/generator",
|
||||||
|
"schema": {
|
||||||
|
"$schema": "http://json-schema.org/schema",
|
||||||
|
"$id": "ConvertToFlatConfig",
|
||||||
|
"cli": "nx",
|
||||||
|
"description": "Convert an Nx workspace to a Flat ESLint config.",
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"skipFormat": {
|
||||||
|
"type": "boolean",
|
||||||
|
"description": "Skip formatting files.",
|
||||||
|
"default": false,
|
||||||
|
"x-priority": "internal"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"additionalProperties": false,
|
||||||
|
"required": [],
|
||||||
|
"presets": []
|
||||||
|
},
|
||||||
|
"description": "Convert an Nx workspace to a Flat ESLint config.",
|
||||||
|
"implementation": "/packages/linter/src/generators/convert-to-flat-config/generator.ts",
|
||||||
|
"aliases": [],
|
||||||
|
"hidden": false,
|
||||||
|
"path": "/packages/linter/src/generators/convert-to-flat-config/schema.json",
|
||||||
|
"type": "generator"
|
||||||
|
}
|
||||||
@ -413,6 +413,7 @@
|
|||||||
- [generators](/packages/linter/generators)
|
- [generators](/packages/linter/generators)
|
||||||
- [workspace-rules-project](/packages/linter/generators/workspace-rules-project)
|
- [workspace-rules-project](/packages/linter/generators/workspace-rules-project)
|
||||||
- [workspace-rule](/packages/linter/generators/workspace-rule)
|
- [workspace-rule](/packages/linter/generators/workspace-rule)
|
||||||
|
- [convert-to-flat-config](/packages/linter/generators/convert-to-flat-config)
|
||||||
- [nest](/packages/nest)
|
- [nest](/packages/nest)
|
||||||
- [documents](/packages/nest/documents)
|
- [documents](/packages/nest/documents)
|
||||||
- [Overview](/packages/nest/documents/overview)
|
- [Overview](/packages/nest/documents/overview)
|
||||||
|
|||||||
@ -1,12 +1,15 @@
|
|||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
import {
|
import {
|
||||||
|
checkFilesDoNotExist,
|
||||||
checkFilesExist,
|
checkFilesExist,
|
||||||
cleanupProject,
|
cleanupProject,
|
||||||
createFile,
|
createFile,
|
||||||
|
getSelectedPackageManager,
|
||||||
newProject,
|
newProject,
|
||||||
readFile,
|
readFile,
|
||||||
readJson,
|
readJson,
|
||||||
runCLI,
|
runCLI,
|
||||||
|
runCreateWorkspace,
|
||||||
uniq,
|
uniq,
|
||||||
updateFile,
|
updateFile,
|
||||||
updateJson,
|
updateJson,
|
||||||
@ -537,6 +540,78 @@ describe('Linter', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('Flat config', () => {
|
||||||
|
const packageManager = getSelectedPackageManager() || 'pnpm';
|
||||||
|
|
||||||
|
afterEach(() => cleanupProject());
|
||||||
|
|
||||||
|
it('should convert integrated to flat config', () => {
|
||||||
|
const myapp = uniq('myapp');
|
||||||
|
const mylib = uniq('mylib');
|
||||||
|
|
||||||
|
runCreateWorkspace(myapp, {
|
||||||
|
preset: 'react-monorepo',
|
||||||
|
appName: myapp,
|
||||||
|
style: 'css',
|
||||||
|
packageManager,
|
||||||
|
bundler: 'vite',
|
||||||
|
e2eTestRunner: 'none',
|
||||||
|
});
|
||||||
|
runCLI(`generate @nx/js:lib ${mylib}`);
|
||||||
|
|
||||||
|
// migrate to flat structure
|
||||||
|
runCLI(`generate @nx/linter:convert-to-flat-config`);
|
||||||
|
checkFilesExist(
|
||||||
|
'eslint.config.js',
|
||||||
|
`apps/${myapp}/eslint.config.js`,
|
||||||
|
`libs/${mylib}/eslint.config.js`
|
||||||
|
);
|
||||||
|
checkFilesDoNotExist(
|
||||||
|
'.eslintrc.json',
|
||||||
|
`apps/${myapp}/.eslintrc.json`,
|
||||||
|
`libs/${mylib}/.eslintrc.json`
|
||||||
|
);
|
||||||
|
|
||||||
|
const outFlat = runCLI(`affected -t lint`, {
|
||||||
|
silenceError: true,
|
||||||
|
});
|
||||||
|
expect(outFlat).toContain('All files pass linting');
|
||||||
|
}, 1000000);
|
||||||
|
|
||||||
|
it('should convert standalone to flat config', () => {
|
||||||
|
const myapp = uniq('myapp');
|
||||||
|
const mylib = uniq('mylib');
|
||||||
|
|
||||||
|
runCreateWorkspace(myapp, {
|
||||||
|
preset: 'react-standalone',
|
||||||
|
appName: myapp,
|
||||||
|
style: 'css',
|
||||||
|
packageManager,
|
||||||
|
bundler: 'vite',
|
||||||
|
e2eTestRunner: 'none',
|
||||||
|
});
|
||||||
|
runCLI(`generate @nx/js:lib ${mylib}`);
|
||||||
|
|
||||||
|
// migrate to flat structure
|
||||||
|
runCLI(`generate @nx/linter:convert-to-flat-config`);
|
||||||
|
checkFilesExist(
|
||||||
|
'eslint.config.js',
|
||||||
|
`${mylib}/eslint.config.js`,
|
||||||
|
'eslint.base.config.js'
|
||||||
|
);
|
||||||
|
checkFilesDoNotExist(
|
||||||
|
'.eslintrc.json',
|
||||||
|
`${mylib}/.eslintrc.json`,
|
||||||
|
'.eslintrc.base.json'
|
||||||
|
);
|
||||||
|
|
||||||
|
const outFlat = runCLI(`affected -t lint`, {
|
||||||
|
silenceError: true,
|
||||||
|
});
|
||||||
|
expect(outFlat).toContain('All files pass linting');
|
||||||
|
}, 1000000);
|
||||||
|
});
|
||||||
|
|
||||||
describe('Root projects migration', () => {
|
describe('Root projects migration', () => {
|
||||||
beforeEach(() => newProject());
|
beforeEach(() => newProject());
|
||||||
afterEach(() => cleanupProject());
|
afterEach(() => cleanupProject());
|
||||||
|
|||||||
@ -46,6 +46,8 @@
|
|||||||
"@babel/preset-react": "^7.22.5",
|
"@babel/preset-react": "^7.22.5",
|
||||||
"@babel/preset-typescript": "^7.22.5",
|
"@babel/preset-typescript": "^7.22.5",
|
||||||
"@babel/runtime": "^7.22.6",
|
"@babel/runtime": "^7.22.6",
|
||||||
|
"@eslint/eslintrc": "^2.1.1",
|
||||||
|
"@eslint/js": "^8.46.0",
|
||||||
"@floating-ui/react": "0.19.2",
|
"@floating-ui/react": "0.19.2",
|
||||||
"@jest/reporters": "^29.4.1",
|
"@jest/reporters": "^29.4.1",
|
||||||
"@jest/test-result": "^29.4.1",
|
"@jest/test-result": "^29.4.1",
|
||||||
|
|||||||
@ -95,6 +95,7 @@ exports[`workspace move to nx layout should create nx.json 1`] = `
|
|||||||
"!{projectRoot}/**/*.spec.[jt]s",
|
"!{projectRoot}/**/*.spec.[jt]s",
|
||||||
"!{projectRoot}/karma.conf.js",
|
"!{projectRoot}/karma.conf.js",
|
||||||
"!{projectRoot}/.eslintrc.json",
|
"!{projectRoot}/.eslintrc.json",
|
||||||
|
"!{projectRoot}/eslint.config.js",
|
||||||
],
|
],
|
||||||
"sharedGlobals": [],
|
"sharedGlobals": [],
|
||||||
},
|
},
|
||||||
@ -118,6 +119,7 @@ exports[`workspace move to nx layout should create nx.json 1`] = `
|
|||||||
"inputs": [
|
"inputs": [
|
||||||
"default",
|
"default",
|
||||||
"{workspaceRoot}/.eslintrc.json",
|
"{workspaceRoot}/.eslintrc.json",
|
||||||
|
"{workspaceRoot}/eslint.config.js",
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
"test": {
|
"test": {
|
||||||
|
|||||||
@ -76,7 +76,9 @@ export function createNxJson(
|
|||||||
'!{projectRoot}/karma.conf.js',
|
'!{projectRoot}/karma.conf.js',
|
||||||
]
|
]
|
||||||
: []),
|
: []),
|
||||||
targets.lint ? '!{projectRoot}/.eslintrc.json' : undefined,
|
...(targets.lint
|
||||||
|
? ['!{projectRoot}/.eslintrc.json', '!{projectRoot}/eslint.config.js']
|
||||||
|
: []),
|
||||||
].filter(Boolean),
|
].filter(Boolean),
|
||||||
},
|
},
|
||||||
targetDefaults: {
|
targetDefaults: {
|
||||||
@ -91,7 +93,11 @@ export function createNxJson(
|
|||||||
: undefined,
|
: undefined,
|
||||||
lint: targets.lint
|
lint: targets.lint
|
||||||
? {
|
? {
|
||||||
inputs: ['default', '{workspaceRoot}/.eslintrc.json'],
|
inputs: [
|
||||||
|
'default',
|
||||||
|
'{workspaceRoot}/.eslintrc.json',
|
||||||
|
'{workspaceRoot}/eslint.config.js',
|
||||||
|
],
|
||||||
}
|
}
|
||||||
: undefined,
|
: undefined,
|
||||||
e2e: targets.e2e
|
e2e: targets.e2e
|
||||||
|
|||||||
@ -25,6 +25,11 @@
|
|||||||
"factory": "./src/generators/workspace-rule/workspace-rule#lintWorkspaceRuleGenerator",
|
"factory": "./src/generators/workspace-rule/workspace-rule#lintWorkspaceRuleGenerator",
|
||||||
"schema": "./src/generators/workspace-rule/schema.json",
|
"schema": "./src/generators/workspace-rule/schema.json",
|
||||||
"description": "Create a new Workspace ESLint rule."
|
"description": "Create a new Workspace ESLint rule."
|
||||||
|
},
|
||||||
|
"convert-to-flat-config": {
|
||||||
|
"factory": "./src/generators/convert-to-flat-config/generator",
|
||||||
|
"schema": "./src/generators/convert-to-flat-config/schema.json",
|
||||||
|
"description": "Convert an Nx workspace to a Flat ESLint config."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -37,7 +37,8 @@
|
|||||||
"tmp": "~0.2.1",
|
"tmp": "~0.2.1",
|
||||||
"tslib": "^2.3.0",
|
"tslib": "^2.3.0",
|
||||||
"@nx/devkit": "file:../devkit",
|
"@nx/devkit": "file:../devkit",
|
||||||
"@nx/js": "file:../js"
|
"@nx/js": "file:../js",
|
||||||
|
"typescript": "~5.1.3"
|
||||||
},
|
},
|
||||||
"peerDependenciesMeta": {
|
"peerDependenciesMeta": {
|
||||||
"eslint": {
|
"eslint": {
|
||||||
|
|||||||
@ -130,10 +130,13 @@ 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
|
||||||
|
? `'ignores' configuration`
|
||||||
|
: `'.eslintignore' file`;
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`All files matching the following patterns are ignored:\n${ignoredPatterns.join(
|
`All files matching the following patterns are ignored:\n${ignoredPatterns.join(
|
||||||
'\n'
|
'\n'
|
||||||
)}\n\nPlease check your '.eslintignore' file.`
|
)}\n\nPlease check your ${ignoreSection}.`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
throw new Error(
|
throw new Error(
|
||||||
|
|||||||
@ -0,0 +1,385 @@
|
|||||||
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
|
exports[`convert-to-flat-config generator should add env configuration 1`] = `
|
||||||
|
"const { FlatCompat } = require('@eslint/eslintrc');
|
||||||
|
const nxEslintPlugin = require('@nx/eslint-plugin');
|
||||||
|
const globals = require('globals');
|
||||||
|
const js = require('@eslint/js');
|
||||||
|
const compat = new FlatCompat({
|
||||||
|
baseDirectory: __dirname,
|
||||||
|
recommendedConfig: js.configs.recommended,
|
||||||
|
});
|
||||||
|
module.exports = [
|
||||||
|
{ plugins: { '@nx': nxEslintPlugin } },
|
||||||
|
{ languageOptions: { globals: { ...globals.browser, ...globals.node } } },
|
||||||
|
{
|
||||||
|
files: ['**/*.ts', '**/*.tsx', '**/*.js', '**/*.jsx'],
|
||||||
|
rules: {
|
||||||
|
'@nx/enforce-module-boundaries': [
|
||||||
|
'error',
|
||||||
|
{
|
||||||
|
enforceBuildableLibDependency: true,
|
||||||
|
allow: [],
|
||||||
|
depConstraints: [
|
||||||
|
{
|
||||||
|
sourceTag: '*',
|
||||||
|
onlyDependOnLibsWithTags: ['*'],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
...compat.config({ extends: ['plugin:@nx/typescript'] }).map((config) => ({
|
||||||
|
...config,
|
||||||
|
files: ['**/*.ts', '**/*.tsx'],
|
||||||
|
rules: {},
|
||||||
|
})),
|
||||||
|
...compat.config({ extends: ['plugin:@nx/javascript'] }).map((config) => ({
|
||||||
|
...config,
|
||||||
|
files: ['**/*.js', '**/*.jsx'],
|
||||||
|
rules: {},
|
||||||
|
})),
|
||||||
|
];
|
||||||
|
"
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`convert-to-flat-config generator should add global and env configuration 1`] = `
|
||||||
|
"const { FlatCompat } = require('@eslint/eslintrc');
|
||||||
|
const nxEslintPlugin = require('@nx/eslint-plugin');
|
||||||
|
const globals = require('globals');
|
||||||
|
const js = require('@eslint/js');
|
||||||
|
const compat = new FlatCompat({
|
||||||
|
baseDirectory: __dirname,
|
||||||
|
recommendedConfig: js.configs.recommended,
|
||||||
|
});
|
||||||
|
module.exports = [
|
||||||
|
{ plugins: { '@nx': nxEslintPlugin } },
|
||||||
|
{
|
||||||
|
languageOptions: {
|
||||||
|
globals: { ...globals.browser, myCustomGlobal: 'readonly' },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
files: ['**/*.ts', '**/*.tsx', '**/*.js', '**/*.jsx'],
|
||||||
|
rules: {
|
||||||
|
'@nx/enforce-module-boundaries': [
|
||||||
|
'error',
|
||||||
|
{
|
||||||
|
enforceBuildableLibDependency: true,
|
||||||
|
allow: [],
|
||||||
|
depConstraints: [
|
||||||
|
{
|
||||||
|
sourceTag: '*',
|
||||||
|
onlyDependOnLibsWithTags: ['*'],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
...compat.config({ extends: ['plugin:@nx/typescript'] }).map((config) => ({
|
||||||
|
...config,
|
||||||
|
files: ['**/*.ts', '**/*.tsx'],
|
||||||
|
rules: {},
|
||||||
|
})),
|
||||||
|
...compat.config({ extends: ['plugin:@nx/javascript'] }).map((config) => ({
|
||||||
|
...config,
|
||||||
|
files: ['**/*.js', '**/*.jsx'],
|
||||||
|
rules: {},
|
||||||
|
})),
|
||||||
|
];
|
||||||
|
"
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`convert-to-flat-config generator should add global configuration 1`] = `
|
||||||
|
"const { FlatCompat } = require('@eslint/eslintrc');
|
||||||
|
const nxEslintPlugin = require('@nx/eslint-plugin');
|
||||||
|
const js = require('@eslint/js');
|
||||||
|
const compat = new FlatCompat({
|
||||||
|
baseDirectory: __dirname,
|
||||||
|
recommendedConfig: js.configs.recommended,
|
||||||
|
});
|
||||||
|
module.exports = [
|
||||||
|
{ plugins: { '@nx': nxEslintPlugin } },
|
||||||
|
{ languageOptions: { globals: { myCustomGlobal: 'readonly' } } },
|
||||||
|
{
|
||||||
|
files: ['**/*.ts', '**/*.tsx', '**/*.js', '**/*.jsx'],
|
||||||
|
rules: {
|
||||||
|
'@nx/enforce-module-boundaries': [
|
||||||
|
'error',
|
||||||
|
{
|
||||||
|
enforceBuildableLibDependency: true,
|
||||||
|
allow: [],
|
||||||
|
depConstraints: [
|
||||||
|
{
|
||||||
|
sourceTag: '*',
|
||||||
|
onlyDependOnLibsWithTags: ['*'],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
...compat.config({ extends: ['plugin:@nx/typescript'] }).map((config) => ({
|
||||||
|
...config,
|
||||||
|
files: ['**/*.ts', '**/*.tsx'],
|
||||||
|
rules: {},
|
||||||
|
})),
|
||||||
|
...compat.config({ extends: ['plugin:@nx/javascript'] }).map((config) => ({
|
||||||
|
...config,
|
||||||
|
files: ['**/*.js', '**/*.jsx'],
|
||||||
|
rules: {},
|
||||||
|
})),
|
||||||
|
];
|
||||||
|
"
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`convert-to-flat-config generator should add global gitignores 1`] = `
|
||||||
|
"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: {
|
||||||
|
'@nx/enforce-module-boundaries': [
|
||||||
|
'error',
|
||||||
|
{
|
||||||
|
enforceBuildableLibDependency: true,
|
||||||
|
allow: [],
|
||||||
|
depConstraints: [
|
||||||
|
{
|
||||||
|
sourceTag: '*',
|
||||||
|
onlyDependOnLibsWithTags: ['*'],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
...compat.config({ extends: ['plugin:@nx/typescript'] }).map((config) => ({
|
||||||
|
...config,
|
||||||
|
files: ['**/*.ts', '**/*.tsx'],
|
||||||
|
rules: {},
|
||||||
|
})),
|
||||||
|
...compat.config({ extends: ['plugin:@nx/javascript'] }).map((config) => ({
|
||||||
|
...config,
|
||||||
|
files: ['**/*.js', '**/*.jsx'],
|
||||||
|
rules: {},
|
||||||
|
})),
|
||||||
|
{ ignores: ['ignore/me'] },
|
||||||
|
];
|
||||||
|
"
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`convert-to-flat-config generator should add parser 1`] = `
|
||||||
|
"const { FlatCompat } = require('@eslint/eslintrc');
|
||||||
|
const nxEslintPlugin = require('@nx/eslint-plugin');
|
||||||
|
const typescriptEslintParser = require('@typescript-eslint/parser');
|
||||||
|
const js = require('@eslint/js');
|
||||||
|
const compat = new FlatCompat({
|
||||||
|
baseDirectory: __dirname,
|
||||||
|
recommendedConfig: js.configs.recommended,
|
||||||
|
});
|
||||||
|
module.exports = [
|
||||||
|
{ plugins: { '@nx': nxEslintPlugin } },
|
||||||
|
{ languageOptions: { parser: typescriptEslintParser } },
|
||||||
|
{
|
||||||
|
files: ['**/*.ts', '**/*.tsx', '**/*.js', '**/*.jsx'],
|
||||||
|
rules: {
|
||||||
|
'@nx/enforce-module-boundaries': [
|
||||||
|
'error',
|
||||||
|
{
|
||||||
|
enforceBuildableLibDependency: true,
|
||||||
|
allow: [],
|
||||||
|
depConstraints: [
|
||||||
|
{
|
||||||
|
sourceTag: '*',
|
||||||
|
onlyDependOnLibsWithTags: ['*'],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
...compat.config({ extends: ['plugin:@nx/typescript'] }).map((config) => ({
|
||||||
|
...config,
|
||||||
|
files: ['**/*.ts', '**/*.tsx'],
|
||||||
|
rules: {},
|
||||||
|
})),
|
||||||
|
...compat.config({ extends: ['plugin:@nx/javascript'] }).map((config) => ({
|
||||||
|
...config,
|
||||||
|
files: ['**/*.js', '**/*.jsx'],
|
||||||
|
rules: {},
|
||||||
|
})),
|
||||||
|
];
|
||||||
|
"
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`convert-to-flat-config generator should add plugins 1`] = `
|
||||||
|
"const { FlatCompat } = require('@eslint/eslintrc');
|
||||||
|
const eslintPluginImport = require('eslint-plugin-import');
|
||||||
|
const eslintPluginSingleName = require('eslint-plugin-single-name');
|
||||||
|
const scopeEslintPluginWithName = require('@scope/eslint-plugin-with-name');
|
||||||
|
const justScopeEslintPlugin = require('@just-scope/eslint-plugin');
|
||||||
|
const js = require('@eslint/js');
|
||||||
|
const compat = new FlatCompat({
|
||||||
|
baseDirectory: __dirname,
|
||||||
|
recommendedConfig: js.configs.recommended,
|
||||||
|
});
|
||||||
|
module.exports = [
|
||||||
|
{
|
||||||
|
plugins: {
|
||||||
|
'eslint-plugin-import': eslintPluginImport,
|
||||||
|
'single-name': eslintPluginSingleName,
|
||||||
|
'@scope/with-name': scopeEslintPluginWithName,
|
||||||
|
'@just-scope': justScopeEslintPlugin,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
files: ['**/*.ts', '**/*.tsx', '**/*.js', '**/*.jsx'],
|
||||||
|
rules: {
|
||||||
|
'@nx/enforce-module-boundaries': [
|
||||||
|
'error',
|
||||||
|
{
|
||||||
|
enforceBuildableLibDependency: true,
|
||||||
|
allow: [],
|
||||||
|
depConstraints: [
|
||||||
|
{
|
||||||
|
sourceTag: '*',
|
||||||
|
onlyDependOnLibsWithTags: ['*'],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
...compat.config({ extends: ['plugin:@nx/typescript'] }).map((config) => ({
|
||||||
|
...config,
|
||||||
|
files: ['**/*.ts', '**/*.tsx'],
|
||||||
|
rules: {},
|
||||||
|
})),
|
||||||
|
...compat.config({ extends: ['plugin:@nx/javascript'] }).map((config) => ({
|
||||||
|
...config,
|
||||||
|
files: ['**/*.js', '**/*.jsx'],
|
||||||
|
rules: {},
|
||||||
|
})),
|
||||||
|
];
|
||||||
|
"
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`convert-to-flat-config generator should add settings 1`] = `
|
||||||
|
"const { FlatCompat } = require('@eslint/eslintrc');
|
||||||
|
const nxEslintPlugin = require('@nx/eslint-plugin');
|
||||||
|
const js = require('@eslint/js');
|
||||||
|
const compat = new FlatCompat({
|
||||||
|
baseDirectory: __dirname,
|
||||||
|
recommendedConfig: js.configs.recommended,
|
||||||
|
});
|
||||||
|
module.exports = [
|
||||||
|
{ plugins: { '@nx': nxEslintPlugin } },
|
||||||
|
{ settings: { sharedData: 'Hello' } },
|
||||||
|
{
|
||||||
|
files: ['**/*.ts', '**/*.tsx', '**/*.js', '**/*.jsx'],
|
||||||
|
rules: {
|
||||||
|
'@nx/enforce-module-boundaries': [
|
||||||
|
'error',
|
||||||
|
{
|
||||||
|
enforceBuildableLibDependency: true,
|
||||||
|
allow: [],
|
||||||
|
depConstraints: [
|
||||||
|
{
|
||||||
|
sourceTag: '*',
|
||||||
|
onlyDependOnLibsWithTags: ['*'],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
...compat.config({ extends: ['plugin:@nx/typescript'] }).map((config) => ({
|
||||||
|
...config,
|
||||||
|
files: ['**/*.ts', '**/*.tsx'],
|
||||||
|
rules: {},
|
||||||
|
})),
|
||||||
|
...compat.config({ extends: ['plugin:@nx/javascript'] }).map((config) => ({
|
||||||
|
...config,
|
||||||
|
files: ['**/*.js', '**/*.jsx'],
|
||||||
|
rules: {},
|
||||||
|
})),
|
||||||
|
];
|
||||||
|
"
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`convert-to-flat-config generator should run successfully 1`] = `
|
||||||
|
"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: {
|
||||||
|
'@nx/enforce-module-boundaries': [
|
||||||
|
'error',
|
||||||
|
{
|
||||||
|
enforceBuildableLibDependency: true,
|
||||||
|
allow: [],
|
||||||
|
depConstraints: [
|
||||||
|
{
|
||||||
|
sourceTag: '*',
|
||||||
|
onlyDependOnLibsWithTags: ['*'],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
...compat.config({ extends: ['plugin:@nx/typescript'] }).map((config) => ({
|
||||||
|
...config,
|
||||||
|
files: ['**/*.ts', '**/*.tsx'],
|
||||||
|
rules: {},
|
||||||
|
})),
|
||||||
|
...compat.config({ extends: ['plugin:@nx/javascript'] }).map((config) => ({
|
||||||
|
...config,
|
||||||
|
files: ['**/*.js', '**/*.jsx'],
|
||||||
|
rules: {},
|
||||||
|
})),
|
||||||
|
];
|
||||||
|
"
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`convert-to-flat-config generator should run successfully 2`] = `
|
||||||
|
"const baseConfig = require('../../eslint.config.js');
|
||||||
|
module.exports = [
|
||||||
|
...baseConfig,
|
||||||
|
{
|
||||||
|
files: [
|
||||||
|
'libs/test-lib/**/*.ts',
|
||||||
|
'libs/test-lib/**/*.tsx',
|
||||||
|
'libs/test-lib/**/*.js',
|
||||||
|
'libs/test-lib/**/*.jsx',
|
||||||
|
],
|
||||||
|
rules: {},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
files: ['libs/test-lib/**/*.ts', 'libs/test-lib/**/*.tsx'],
|
||||||
|
rules: {},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
files: ['libs/test-lib/**/*.js', 'libs/test-lib/**/*.jsx'],
|
||||||
|
rules: {},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
"
|
||||||
|
`;
|
||||||
@ -0,0 +1,68 @@
|
|||||||
|
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);
|
||||||
|
}
|
||||||
@ -0,0 +1,234 @@
|
|||||||
|
import { Tree } from '@nx/devkit';
|
||||||
|
import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing';
|
||||||
|
import { convertEslintJsonToFlatConfig } from './json-converter';
|
||||||
|
|
||||||
|
describe('convertEslintJsonToFlatConfig', () => {
|
||||||
|
let tree: Tree;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
tree = createTreeWithEmptyWorkspace();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should convert root configs', async () => {
|
||||||
|
tree.write(
|
||||||
|
'.eslintrc.json',
|
||||||
|
JSON.stringify({
|
||||||
|
root: true,
|
||||||
|
ignorePatterns: ['**/*', 'src/ignore/to/keep.ts'],
|
||||||
|
plugins: ['@nx'],
|
||||||
|
overrides: [
|
||||||
|
{
|
||||||
|
files: ['*.ts', '*.tsx', '*.js', '*.jsx'],
|
||||||
|
rules: {
|
||||||
|
'@nx/enforce-module-boundaries': [
|
||||||
|
'error',
|
||||||
|
{
|
||||||
|
enforceBuildableLibDependency: true,
|
||||||
|
allow: [],
|
||||||
|
depConstraints: [
|
||||||
|
{
|
||||||
|
sourceTag: '*',
|
||||||
|
onlyDependOnLibsWithTags: ['*'],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
files: ['*.ts', '*.tsx'],
|
||||||
|
extends: ['plugin:@nx/typescript'],
|
||||||
|
rules: {},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
files: [
|
||||||
|
'**/*.spec.ts',
|
||||||
|
'**/*.spec.tsx',
|
||||||
|
'**/*.spec.js',
|
||||||
|
'**/*.spec.jsx',
|
||||||
|
],
|
||||||
|
env: {
|
||||||
|
jest: true,
|
||||||
|
},
|
||||||
|
rules: {},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
tree.write('.eslintignore', 'node_modules\nsomething/else');
|
||||||
|
|
||||||
|
convertEslintJsonToFlatConfig(
|
||||||
|
tree,
|
||||||
|
'',
|
||||||
|
'.eslintrc.json',
|
||||||
|
'eslint.config.js'
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(tree.read('eslint.config.js', 'utf-8')).toMatchInlineSnapshot(`
|
||||||
|
"const { FlatCompat } = require("@eslint/eslintrc");
|
||||||
|
const nxEslintPlugin = require("@nx/eslint-plugin");
|
||||||
|
const js = require("@eslint/js");
|
||||||
|
const compat = new FlatCompat({
|
||||||
|
baseDirectory: __dirname,
|
||||||
|
recommendedConfig: js.configs.recommended,
|
||||||
|
});
|
||||||
|
module.exports = [
|
||||||
|
{ plugins: { "@nx": nxEslintPlugin } },
|
||||||
|
{
|
||||||
|
files: [
|
||||||
|
"**/*.ts",
|
||||||
|
"**/*.tsx",
|
||||||
|
"**/*.js",
|
||||||
|
"**/*.jsx"
|
||||||
|
],
|
||||||
|
rules: { "@nx/enforce-module-boundaries": [
|
||||||
|
"error",
|
||||||
|
{
|
||||||
|
enforceBuildableLibDependency: true,
|
||||||
|
allow: [],
|
||||||
|
depConstraints: [{
|
||||||
|
sourceTag: "*",
|
||||||
|
onlyDependOnLibsWithTags: ["*"]
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
] }
|
||||||
|
},
|
||||||
|
...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: ["src/ignore/to/keep.ts"] },
|
||||||
|
{ ignores: ["something/else"] }
|
||||||
|
];
|
||||||
|
"
|
||||||
|
`);
|
||||||
|
|
||||||
|
expect(tree.exists('.eslintrc.json')).toBeFalsy();
|
||||||
|
expect(tree.exists('.eslintignore')).toBeFalsy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should convert project configs', async () => {
|
||||||
|
tree.write(
|
||||||
|
'mylib/.eslintrc.json',
|
||||||
|
JSON.stringify({
|
||||||
|
extends: [
|
||||||
|
'plugin:@nx/react-typescript',
|
||||||
|
'next',
|
||||||
|
'next/core-web-vitals',
|
||||||
|
'../../.eslintrc.json',
|
||||||
|
],
|
||||||
|
ignorePatterns: ['!**/*', '.next/**/*'],
|
||||||
|
overrides: [
|
||||||
|
{
|
||||||
|
files: ['*.ts', '*.tsx', '*.js', '*.jsx'],
|
||||||
|
rules: {
|
||||||
|
'@next/next/no-html-link-for-pages': [
|
||||||
|
'error',
|
||||||
|
'apps/test-next/pages',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
files: ['*.ts', '*.tsx'],
|
||||||
|
rules: {},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
files: ['*.js', '*.jsx'],
|
||||||
|
rules: {},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
files: ['*.json'],
|
||||||
|
parser: 'jsonc-eslint-parser',
|
||||||
|
rules: {
|
||||||
|
'@nx/dependency-checks': 'error',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
rules: {
|
||||||
|
'@next/next/no-html-link-for-pages': 'off',
|
||||||
|
},
|
||||||
|
env: {
|
||||||
|
jest: true,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
tree.write('mylib/.eslintignore', 'node_modules\nsomething/else');
|
||||||
|
|
||||||
|
convertEslintJsonToFlatConfig(
|
||||||
|
tree,
|
||||||
|
'mylib',
|
||||||
|
'.eslintrc.json',
|
||||||
|
'eslint.config.js'
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(tree.read('mylib/eslint.config.js', 'utf-8')).toMatchInlineSnapshot(`
|
||||||
|
"const { FlatCompat } = require("@eslint/eslintrc");
|
||||||
|
const baseConfig = require("../../eslint.config.js");
|
||||||
|
const globals = require("globals");
|
||||||
|
const js = require("@eslint/js");
|
||||||
|
const compat = new FlatCompat({
|
||||||
|
baseDirectory: __dirname,
|
||||||
|
recommendedConfig: js.configs.recommended,
|
||||||
|
});
|
||||||
|
module.exports = [
|
||||||
|
...baseConfig,
|
||||||
|
...compat.extends("plugin:@nx/react-typescript", "next", "next/core-web-vitals"),
|
||||||
|
{ languageOptions: { globals: { ...globals.jest } } },
|
||||||
|
{ rules: { "@next/next/no-html-link-for-pages": "off" } },
|
||||||
|
{
|
||||||
|
files: [
|
||||||
|
"mylib/**/*.ts",
|
||||||
|
"mylib/**/*.tsx",
|
||||||
|
"mylib/**/*.js",
|
||||||
|
"mylib/**/*.jsx"
|
||||||
|
],
|
||||||
|
rules: { "@next/next/no-html-link-for-pages": [
|
||||||
|
"error",
|
||||||
|
"apps/test-next/pages"
|
||||||
|
] }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
files: [
|
||||||
|
"mylib/**/*.ts",
|
||||||
|
"mylib/**/*.tsx"
|
||||||
|
],
|
||||||
|
rules: {}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
files: [
|
||||||
|
"mylib/**/*.js",
|
||||||
|
"mylib/**/*.jsx"
|
||||||
|
],
|
||||||
|
rules: {}
|
||||||
|
},
|
||||||
|
...compat.config({ parser: "jsonc-eslint-parser" }).map(config => ({
|
||||||
|
...config,
|
||||||
|
files: ["mylib/**/*.json"],
|
||||||
|
rules: { "@nx/dependency-checks": "error" }
|
||||||
|
})),
|
||||||
|
{ ignores: ["mylib/.next/**/*"] },
|
||||||
|
{ ignores: ["mylib/something/else"] }
|
||||||
|
];
|
||||||
|
"
|
||||||
|
`);
|
||||||
|
|
||||||
|
expect(tree.exists('mylib/.eslintrc.json')).toBeFalsy();
|
||||||
|
expect(tree.exists('mylib/.eslintignore')).toBeFalsy();
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -0,0 +1,524 @@
|
|||||||
|
import {
|
||||||
|
Tree,
|
||||||
|
addDependenciesToPackageJson,
|
||||||
|
names,
|
||||||
|
readJson,
|
||||||
|
} from '@nx/devkit';
|
||||||
|
import { join } from 'path';
|
||||||
|
import { ESLint, Linter } from 'eslint';
|
||||||
|
import * as ts from 'typescript';
|
||||||
|
import { generateAst, generateRequire } from './generate-ast';
|
||||||
|
import { eslintrcVersion } from '../../../utils/versions';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts an ESLint JSON config to a flat config.
|
||||||
|
* Deletes the original file along with .eslintignore if it exists.
|
||||||
|
*/
|
||||||
|
export function convertEslintJsonToFlatConfig(
|
||||||
|
tree: Tree,
|
||||||
|
root: string,
|
||||||
|
sourceFile: string,
|
||||||
|
destinationFile: string
|
||||||
|
) {
|
||||||
|
const importsMap = new Map<string, string>();
|
||||||
|
const exportElements: ts.Expression[] = [];
|
||||||
|
let isFlatCompatNeeded = false;
|
||||||
|
let combinedConfig: ts.PropertyAssignment[] = [];
|
||||||
|
let languageOptions: ts.PropertyAssignment[] = [];
|
||||||
|
|
||||||
|
// read original config
|
||||||
|
const config: ESLint.ConfigData = readJson(tree, `${root}/${sourceFile}`);
|
||||||
|
|
||||||
|
if (config.extends) {
|
||||||
|
isFlatCompatNeeded = addExtends(importsMap, exportElements, config, tree);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (config.plugins) {
|
||||||
|
addPlugins(importsMap, exportElements, config);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (config.parser) {
|
||||||
|
languageOptions.push(addParser(importsMap, config));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (config.parserOptions) {
|
||||||
|
languageOptions.push(
|
||||||
|
ts.factory.createPropertyAssignment(
|
||||||
|
'parserOptions',
|
||||||
|
generateAst(config.parserOptions)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (config.globals || config.env) {
|
||||||
|
if (config.env) {
|
||||||
|
importsMap.set('globals', 'globals');
|
||||||
|
}
|
||||||
|
|
||||||
|
languageOptions.push(
|
||||||
|
ts.factory.createPropertyAssignment(
|
||||||
|
'globals',
|
||||||
|
ts.factory.createObjectLiteralExpression([
|
||||||
|
...Object.keys(config.env || {}).map((env) =>
|
||||||
|
ts.factory.createSpreadAssignment(
|
||||||
|
ts.factory.createPropertyAccessExpression(
|
||||||
|
ts.factory.createIdentifier('globals'),
|
||||||
|
ts.factory.createIdentifier(env)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
),
|
||||||
|
...Object.keys(config.globals || {}).map((key) =>
|
||||||
|
ts.factory.createPropertyAssignment(
|
||||||
|
key,
|
||||||
|
generateAst(config.globals[key])
|
||||||
|
)
|
||||||
|
),
|
||||||
|
])
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (config.settings) {
|
||||||
|
combinedConfig.push(
|
||||||
|
ts.factory.createPropertyAssignment(
|
||||||
|
'settings',
|
||||||
|
generateAst(config.settings)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
config.noInlineConfig !== undefined ||
|
||||||
|
config.reportUnusedDisableDirectives !== undefined
|
||||||
|
) {
|
||||||
|
combinedConfig.push(
|
||||||
|
ts.factory.createPropertyAssignment(
|
||||||
|
'linterOptions',
|
||||||
|
generateAst({
|
||||||
|
noInlineConfig: config.noInlineConfig,
|
||||||
|
reportUnusedDisableDirectives: config.reportUnusedDisableDirectives,
|
||||||
|
})
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (languageOptions.length > 0) {
|
||||||
|
combinedConfig.push(
|
||||||
|
ts.factory.createPropertyAssignment(
|
||||||
|
'languageOptions',
|
||||||
|
ts.factory.createObjectLiteralExpression(
|
||||||
|
languageOptions,
|
||||||
|
languageOptions.length > 1
|
||||||
|
)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (combinedConfig.length > 0) {
|
||||||
|
exportElements.push(
|
||||||
|
ts.factory.createObjectLiteralExpression(
|
||||||
|
combinedConfig,
|
||||||
|
combinedConfig.length > 1
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (config.rules) {
|
||||||
|
exportElements.push(generateAst({ rules: config.rules }));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (config.overrides) {
|
||||||
|
config.overrides.forEach((override) => {
|
||||||
|
updateFiles(override, root);
|
||||||
|
if (
|
||||||
|
override.env ||
|
||||||
|
override.extends ||
|
||||||
|
override.plugins ||
|
||||||
|
override.parser
|
||||||
|
) {
|
||||||
|
isFlatCompatNeeded = true;
|
||||||
|
addFlattenedOverride(override, exportElements);
|
||||||
|
} else {
|
||||||
|
exportElements.push(generateAst(override));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (config.ignorePatterns) {
|
||||||
|
const patterns = (
|
||||||
|
Array.isArray(config.ignorePatterns)
|
||||||
|
? config.ignorePatterns
|
||||||
|
: [config.ignorePatterns]
|
||||||
|
).filter((pattern) => !['**/*', '!**/*', 'node_modules'].includes(pattern)); // these are useless in a flat config
|
||||||
|
if (patterns.length > 0) {
|
||||||
|
exportElements.push(
|
||||||
|
generateAst({
|
||||||
|
ignores: patterns.map((path) => mapFilePath(path, root)),
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tree.exists(`${root}/.eslintignore`)) {
|
||||||
|
const patterns = tree
|
||||||
|
.read(`${root}/.eslintignore`, 'utf-8')
|
||||||
|
.split('\n')
|
||||||
|
.filter((line) => line.length > 0 && line !== 'node_modules')
|
||||||
|
.map((path) => mapFilePath(path, root));
|
||||||
|
if (patterns.length > 0) {
|
||||||
|
exportElements.push(generateAst({ ignores: patterns }));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
tree.delete(join(root, sourceFile));
|
||||||
|
tree.delete(join(root, '.eslintignore'));
|
||||||
|
|
||||||
|
// create the node list and print it to new file
|
||||||
|
const nodeList = createNodeList(
|
||||||
|
importsMap,
|
||||||
|
exportElements,
|
||||||
|
isFlatCompatNeeded
|
||||||
|
);
|
||||||
|
const printer = ts.createPrinter({ newLine: ts.NewLineKind.LineFeed });
|
||||||
|
const resultFile = ts.createSourceFile(
|
||||||
|
join(root, destinationFile),
|
||||||
|
'',
|
||||||
|
ts.ScriptTarget.Latest,
|
||||||
|
true,
|
||||||
|
ts.ScriptKind.JS
|
||||||
|
);
|
||||||
|
const result = printer.printList(
|
||||||
|
ts.ListFormat.MultiLine,
|
||||||
|
nodeList,
|
||||||
|
resultFile
|
||||||
|
);
|
||||||
|
tree.write(join(root, destinationFile), result);
|
||||||
|
|
||||||
|
if (isFlatCompatNeeded) {
|
||||||
|
addDependenciesToPackageJson(
|
||||||
|
tree,
|
||||||
|
{},
|
||||||
|
{
|
||||||
|
'@eslint/eslintrc': eslintrcVersion,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateFiles(
|
||||||
|
override: Linter.ConfigOverride<Linter.RulesRecord>,
|
||||||
|
root: string
|
||||||
|
) {
|
||||||
|
if (override.files) {
|
||||||
|
override.files = Array.isArray(override.files)
|
||||||
|
? override.files
|
||||||
|
: [override.files];
|
||||||
|
override.files = override.files.map((file) => mapFilePath(file, root));
|
||||||
|
}
|
||||||
|
return override;
|
||||||
|
}
|
||||||
|
|
||||||
|
function mapFilePath(filePath: string, root: string) {
|
||||||
|
if (filePath.startsWith('!')) {
|
||||||
|
const fileWithoutBang = filePath.slice(1);
|
||||||
|
if (fileWithoutBang.startsWith('*.')) {
|
||||||
|
return `!${join(root, '**', fileWithoutBang)}`;
|
||||||
|
} else {
|
||||||
|
return `!${join(root, fileWithoutBang)}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (filePath.startsWith('*.')) {
|
||||||
|
return join(root, '**', filePath);
|
||||||
|
} else {
|
||||||
|
return join(root, filePath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// add parsed extends to export blocks and add import statements
|
||||||
|
function addExtends(
|
||||||
|
importsMap: Map<string, string | string[]>,
|
||||||
|
configBlocks: ts.Expression[],
|
||||||
|
config: ESLint.ConfigData,
|
||||||
|
tree: Tree
|
||||||
|
): boolean {
|
||||||
|
let isFlatCompatNeeded = false;
|
||||||
|
const extendsConfig = Array.isArray(config.extends)
|
||||||
|
? config.extends
|
||||||
|
: [config.extends];
|
||||||
|
|
||||||
|
const eslintrcConfigs = [];
|
||||||
|
|
||||||
|
// add base extends
|
||||||
|
extendsConfig
|
||||||
|
.filter((imp) => imp.match(/^\.?(\.\/)/))
|
||||||
|
.forEach((imp, index) => {
|
||||||
|
if (imp.match(/\.eslintrc(.base)?\.json$/)) {
|
||||||
|
const localName = index ? `baseConfig${index}` : 'baseConfig';
|
||||||
|
configBlocks.push(
|
||||||
|
ts.factory.createSpreadElement(ts.factory.createIdentifier(localName))
|
||||||
|
);
|
||||||
|
const newImport = imp.replace(
|
||||||
|
/^(.*)\.eslintrc(.base)?\.json$/,
|
||||||
|
'$1eslint$2.config.js'
|
||||||
|
);
|
||||||
|
importsMap.set(newImport, localName);
|
||||||
|
} else {
|
||||||
|
eslintrcConfigs.push(imp);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
// add plugin extends
|
||||||
|
const pluginExtends = extendsConfig.filter((imp) => !imp.match(/^\.?(\.\/)/));
|
||||||
|
if (pluginExtends.length) {
|
||||||
|
const eslintPluginExtends = pluginExtends.filter((imp) =>
|
||||||
|
imp.startsWith('eslint:')
|
||||||
|
);
|
||||||
|
pluginExtends.forEach((imp) => {
|
||||||
|
if (!imp.startsWith('eslint:')) {
|
||||||
|
eslintrcConfigs.push(imp);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (eslintPluginExtends.length) {
|
||||||
|
addDependenciesToPackageJson(
|
||||||
|
tree,
|
||||||
|
{},
|
||||||
|
{
|
||||||
|
'@eslint/js': eslintrcVersion,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
importsMap.set('@eslint/js', 'js');
|
||||||
|
eslintPluginExtends.forEach((plugin) => {
|
||||||
|
configBlocks.push(
|
||||||
|
ts.factory.createPropertyAccessExpression(
|
||||||
|
ts.factory.createPropertyAccessExpression(
|
||||||
|
ts.factory.createIdentifier('js'),
|
||||||
|
ts.factory.createIdentifier('configs')
|
||||||
|
),
|
||||||
|
ts.factory.createIdentifier(plugin.slice(7)) // strip 'eslint:' prefix
|
||||||
|
)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (eslintrcConfigs.length) {
|
||||||
|
isFlatCompatNeeded = true;
|
||||||
|
addDependenciesToPackageJson(
|
||||||
|
tree,
|
||||||
|
{},
|
||||||
|
{
|
||||||
|
'@eslint/js': eslintrcVersion,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const pluginExtendsSpread = ts.factory.createSpreadElement(
|
||||||
|
ts.factory.createCallExpression(
|
||||||
|
ts.factory.createPropertyAccessExpression(
|
||||||
|
ts.factory.createIdentifier('compat'),
|
||||||
|
ts.factory.createIdentifier('extends')
|
||||||
|
),
|
||||||
|
undefined,
|
||||||
|
eslintrcConfigs.map((plugin) => ts.factory.createStringLiteral(plugin))
|
||||||
|
)
|
||||||
|
);
|
||||||
|
configBlocks.push(pluginExtendsSpread);
|
||||||
|
}
|
||||||
|
|
||||||
|
return isFlatCompatNeeded;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getPluginImport(pluginName: string): string {
|
||||||
|
if (pluginName.includes('eslint-plugin-')) {
|
||||||
|
return pluginName;
|
||||||
|
}
|
||||||
|
if (!pluginName.startsWith('@')) {
|
||||||
|
return `eslint-plugin-${pluginName}`;
|
||||||
|
}
|
||||||
|
if (!pluginName.includes('/')) {
|
||||||
|
return `${pluginName}/eslint-plugin`;
|
||||||
|
}
|
||||||
|
const [scope, name] = pluginName.split('/');
|
||||||
|
return `${scope}/eslint-plugin-${name}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function addPlugins(
|
||||||
|
importsMap: Map<string, string | string[]>,
|
||||||
|
configBlocks: ts.Expression[],
|
||||||
|
config: ESLint.ConfigData
|
||||||
|
) {
|
||||||
|
const mappedPlugins: { name: string; varName: string; imp: string }[] = [];
|
||||||
|
config.plugins.forEach((name) => {
|
||||||
|
const imp = getPluginImport(name);
|
||||||
|
const varName = names(imp).propertyName;
|
||||||
|
mappedPlugins.push({ name, varName, imp });
|
||||||
|
});
|
||||||
|
mappedPlugins.forEach(({ varName, imp }) => {
|
||||||
|
importsMap.set(imp, varName);
|
||||||
|
});
|
||||||
|
const pluginsAst = ts.factory.createObjectLiteralExpression(
|
||||||
|
[
|
||||||
|
ts.factory.createPropertyAssignment(
|
||||||
|
'plugins',
|
||||||
|
ts.factory.createObjectLiteralExpression(
|
||||||
|
mappedPlugins.map(({ name, varName }) => {
|
||||||
|
return ts.factory.createPropertyAssignment(
|
||||||
|
ts.factory.createStringLiteral(name),
|
||||||
|
ts.factory.createIdentifier(varName)
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
mappedPlugins.length > 1
|
||||||
|
)
|
||||||
|
),
|
||||||
|
...(config.processor
|
||||||
|
? [
|
||||||
|
ts.factory.createPropertyAssignment(
|
||||||
|
'processor',
|
||||||
|
ts.factory.createStringLiteral(config.processor)
|
||||||
|
),
|
||||||
|
]
|
||||||
|
: []),
|
||||||
|
],
|
||||||
|
false
|
||||||
|
);
|
||||||
|
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)
|
||||||
|
)
|
||||||
|
),
|
||||||
|
]);
|
||||||
|
}
|
||||||
@ -0,0 +1,350 @@
|
|||||||
|
import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing';
|
||||||
|
import {
|
||||||
|
NxJsonConfiguration,
|
||||||
|
Tree,
|
||||||
|
addProjectConfiguration,
|
||||||
|
readJson,
|
||||||
|
updateJson,
|
||||||
|
} from '@nx/devkit';
|
||||||
|
|
||||||
|
import { convertToFlatConfigGenerator } from './generator';
|
||||||
|
import { ConvertToFlatConfigGeneratorSchema } from './schema';
|
||||||
|
import { lintProjectGenerator } from '../lint-project/lint-project';
|
||||||
|
import { Linter } from '../utils/linter';
|
||||||
|
import { eslintrcVersion } from '../../utils/versions';
|
||||||
|
|
||||||
|
describe('convert-to-flat-config generator', () => {
|
||||||
|
let tree: Tree;
|
||||||
|
const options: ConvertToFlatConfigGeneratorSchema = { skipFormat: false };
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
tree = createTreeWithEmptyWorkspace();
|
||||||
|
addProjectConfiguration(tree, 'test-lib', {
|
||||||
|
root: 'libs/test-lib',
|
||||||
|
targets: {},
|
||||||
|
});
|
||||||
|
updateJson(tree, 'nx.json', (json: NxJsonConfiguration) => {
|
||||||
|
json.targetDefaults = {
|
||||||
|
lint: {
|
||||||
|
inputs: ['default'],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
json.namedInputs = {
|
||||||
|
default: ['{projectRoot}/**/*', 'sharedGlobals'],
|
||||||
|
production: [
|
||||||
|
'default',
|
||||||
|
'!{projectRoot}/**/?(*.)+(spec|test).[jt]s?(x)?(.snap)',
|
||||||
|
],
|
||||||
|
sharedGlobals: [],
|
||||||
|
};
|
||||||
|
return json;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should run successfully', async () => {
|
||||||
|
await lintProjectGenerator(tree, {
|
||||||
|
skipFormat: false,
|
||||||
|
linter: Linter.EsLint,
|
||||||
|
eslintFilePatterns: ['**/*.ts'],
|
||||||
|
project: 'test-lib',
|
||||||
|
setParserOptionsProject: false,
|
||||||
|
});
|
||||||
|
await convertToFlatConfigGenerator(tree, options);
|
||||||
|
|
||||||
|
expect(tree.exists('eslint.config.js')).toBeTruthy();
|
||||||
|
expect(tree.read('eslint.config.js', 'utf-8')).toMatchSnapshot();
|
||||||
|
expect(tree.exists('libs/test-lib/eslint.config.js')).toBeTruthy();
|
||||||
|
expect(
|
||||||
|
tree.read('libs/test-lib/eslint.config.js', 'utf-8')
|
||||||
|
).toMatchSnapshot();
|
||||||
|
// check nx.json changes
|
||||||
|
const nxJson = readJson(tree, 'nx.json');
|
||||||
|
expect(nxJson.targetDefaults.lint.inputs).toContain(
|
||||||
|
'{workspaceRoot}/eslint.config.js'
|
||||||
|
);
|
||||||
|
expect(nxJson.namedInputs.production).toContain(
|
||||||
|
'!{projectRoot}/eslint.config.js'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should add plugin extends', async () => {
|
||||||
|
await lintProjectGenerator(tree, {
|
||||||
|
skipFormat: false,
|
||||||
|
linter: Linter.EsLint,
|
||||||
|
eslintFilePatterns: ['**/*.ts'],
|
||||||
|
project: 'test-lib',
|
||||||
|
setParserOptionsProject: false,
|
||||||
|
});
|
||||||
|
updateJson(tree, '.eslintrc.json', (json) => {
|
||||||
|
json.extends = ['plugin:storybook/recommended'];
|
||||||
|
return json;
|
||||||
|
});
|
||||||
|
await convertToFlatConfigGenerator(tree, options);
|
||||||
|
|
||||||
|
expect(tree.read('eslint.config.js', 'utf-8')).toMatchInlineSnapshot(`
|
||||||
|
"const { FlatCompat } = require('@eslint/eslintrc');
|
||||||
|
const nxEslintPlugin = require('@nx/eslint-plugin');
|
||||||
|
const js = require('@eslint/js');
|
||||||
|
const compat = new FlatCompat({
|
||||||
|
baseDirectory: __dirname,
|
||||||
|
recommendedConfig: js.configs.recommended,
|
||||||
|
});
|
||||||
|
module.exports = [
|
||||||
|
...compat.extends('plugin:storybook/recommended'),
|
||||||
|
{ plugins: { '@nx': nxEslintPlugin } },
|
||||||
|
{
|
||||||
|
files: ['**/*.ts', '**/*.tsx', '**/*.js', '**/*.jsx'],
|
||||||
|
rules: {
|
||||||
|
'@nx/enforce-module-boundaries': [
|
||||||
|
'error',
|
||||||
|
{
|
||||||
|
enforceBuildableLibDependency: true,
|
||||||
|
allow: [],
|
||||||
|
depConstraints: [
|
||||||
|
{
|
||||||
|
sourceTag: '*',
|
||||||
|
onlyDependOnLibsWithTags: ['*'],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
...compat.config({ extends: ['plugin:@nx/typescript'] }).map((config) => ({
|
||||||
|
...config,
|
||||||
|
files: ['**/*.ts', '**/*.tsx'],
|
||||||
|
rules: {},
|
||||||
|
})),
|
||||||
|
...compat.config({ extends: ['plugin:@nx/javascript'] }).map((config) => ({
|
||||||
|
...config,
|
||||||
|
files: ['**/*.js', '**/*.jsx'],
|
||||||
|
rules: {},
|
||||||
|
})),
|
||||||
|
];
|
||||||
|
"
|
||||||
|
`);
|
||||||
|
expect(tree.read('libs/test-lib/eslint.config.js', 'utf-8'))
|
||||||
|
.toMatchInlineSnapshot(`
|
||||||
|
"const baseConfig = require('../../eslint.config.js');
|
||||||
|
module.exports = [
|
||||||
|
...baseConfig,
|
||||||
|
{
|
||||||
|
files: [
|
||||||
|
'libs/test-lib/**/*.ts',
|
||||||
|
'libs/test-lib/**/*.tsx',
|
||||||
|
'libs/test-lib/**/*.js',
|
||||||
|
'libs/test-lib/**/*.jsx',
|
||||||
|
],
|
||||||
|
rules: {},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
files: ['libs/test-lib/**/*.ts', 'libs/test-lib/**/*.tsx'],
|
||||||
|
rules: {},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
files: ['libs/test-lib/**/*.js', 'libs/test-lib/**/*.jsx'],
|
||||||
|
rules: {},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
"
|
||||||
|
`);
|
||||||
|
expect(
|
||||||
|
readJson(tree, 'package.json').devDependencies['@eslint/eslintrc']
|
||||||
|
).toEqual(eslintrcVersion);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should add global gitignores', async () => {
|
||||||
|
await lintProjectGenerator(tree, {
|
||||||
|
skipFormat: false,
|
||||||
|
linter: Linter.EsLint,
|
||||||
|
eslintFilePatterns: ['**/*.ts'],
|
||||||
|
project: 'test-lib',
|
||||||
|
setParserOptionsProject: false,
|
||||||
|
});
|
||||||
|
tree.write('.eslintignore', 'ignore/me');
|
||||||
|
await convertToFlatConfigGenerator(tree, options);
|
||||||
|
|
||||||
|
expect(tree.read('eslint.config.js', 'utf-8')).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should add settings', async () => {
|
||||||
|
await lintProjectGenerator(tree, {
|
||||||
|
skipFormat: false,
|
||||||
|
linter: Linter.EsLint,
|
||||||
|
eslintFilePatterns: ['**/*.ts'],
|
||||||
|
project: 'test-lib',
|
||||||
|
setParserOptionsProject: false,
|
||||||
|
});
|
||||||
|
updateJson(tree, '.eslintrc.json', (json) => {
|
||||||
|
json.settings = {
|
||||||
|
sharedData: 'Hello',
|
||||||
|
};
|
||||||
|
return json;
|
||||||
|
});
|
||||||
|
await convertToFlatConfigGenerator(tree, options);
|
||||||
|
|
||||||
|
expect(tree.read('eslint.config.js', 'utf-8')).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should add env configuration', async () => {
|
||||||
|
await lintProjectGenerator(tree, {
|
||||||
|
skipFormat: false,
|
||||||
|
linter: Linter.EsLint,
|
||||||
|
eslintFilePatterns: ['**/*.ts'],
|
||||||
|
project: 'test-lib',
|
||||||
|
setParserOptionsProject: false,
|
||||||
|
});
|
||||||
|
updateJson(tree, '.eslintrc.json', (json) => {
|
||||||
|
json.env = {
|
||||||
|
browser: true,
|
||||||
|
node: true,
|
||||||
|
};
|
||||||
|
return json;
|
||||||
|
});
|
||||||
|
await convertToFlatConfigGenerator(tree, options);
|
||||||
|
|
||||||
|
expect(tree.read('eslint.config.js', 'utf-8')).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should add global configuration', async () => {
|
||||||
|
await lintProjectGenerator(tree, {
|
||||||
|
skipFormat: false,
|
||||||
|
linter: Linter.EsLint,
|
||||||
|
eslintFilePatterns: ['**/*.ts'],
|
||||||
|
project: 'test-lib',
|
||||||
|
setParserOptionsProject: false,
|
||||||
|
});
|
||||||
|
updateJson(tree, '.eslintrc.json', (json) => {
|
||||||
|
json.globals = {
|
||||||
|
myCustomGlobal: 'readonly',
|
||||||
|
};
|
||||||
|
return json;
|
||||||
|
});
|
||||||
|
await convertToFlatConfigGenerator(tree, options);
|
||||||
|
|
||||||
|
expect(tree.read('eslint.config.js', 'utf-8')).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should add global and env configuration', async () => {
|
||||||
|
await lintProjectGenerator(tree, {
|
||||||
|
skipFormat: false,
|
||||||
|
linter: Linter.EsLint,
|
||||||
|
eslintFilePatterns: ['**/*.ts'],
|
||||||
|
project: 'test-lib',
|
||||||
|
setParserOptionsProject: false,
|
||||||
|
});
|
||||||
|
updateJson(tree, '.eslintrc.json', (json) => {
|
||||||
|
json.globals = {
|
||||||
|
myCustomGlobal: 'readonly',
|
||||||
|
};
|
||||||
|
json.env = {
|
||||||
|
browser: true,
|
||||||
|
};
|
||||||
|
return json;
|
||||||
|
});
|
||||||
|
await convertToFlatConfigGenerator(tree, options);
|
||||||
|
|
||||||
|
expect(tree.read('eslint.config.js', 'utf-8')).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should add plugins', async () => {
|
||||||
|
await lintProjectGenerator(tree, {
|
||||||
|
skipFormat: false,
|
||||||
|
linter: Linter.EsLint,
|
||||||
|
eslintFilePatterns: ['**/*.ts'],
|
||||||
|
project: 'test-lib',
|
||||||
|
setParserOptionsProject: false,
|
||||||
|
});
|
||||||
|
updateJson(tree, '.eslintrc.json', (json) => {
|
||||||
|
json.plugins = [
|
||||||
|
'eslint-plugin-import',
|
||||||
|
'single-name',
|
||||||
|
'@scope/with-name',
|
||||||
|
'@just-scope',
|
||||||
|
];
|
||||||
|
return json;
|
||||||
|
});
|
||||||
|
await convertToFlatConfigGenerator(tree, options);
|
||||||
|
|
||||||
|
expect(tree.read('eslint.config.js', 'utf-8')).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should add parser', async () => {
|
||||||
|
await lintProjectGenerator(tree, {
|
||||||
|
skipFormat: false,
|
||||||
|
linter: Linter.EsLint,
|
||||||
|
eslintFilePatterns: ['**/*.ts'],
|
||||||
|
project: 'test-lib',
|
||||||
|
setParserOptionsProject: false,
|
||||||
|
});
|
||||||
|
updateJson(tree, '.eslintrc.json', (json) => {
|
||||||
|
json.parser = '@typescript-eslint/parser';
|
||||||
|
return json;
|
||||||
|
});
|
||||||
|
await convertToFlatConfigGenerator(tree, options);
|
||||||
|
|
||||||
|
expect(tree.read('eslint.config.js', 'utf-8')).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should add linter options', async () => {
|
||||||
|
await lintProjectGenerator(tree, {
|
||||||
|
skipFormat: false,
|
||||||
|
linter: Linter.EsLint,
|
||||||
|
eslintFilePatterns: ['**/*.ts'],
|
||||||
|
project: 'test-lib',
|
||||||
|
setParserOptionsProject: false,
|
||||||
|
});
|
||||||
|
updateJson(tree, '.eslintrc.json', (json) => {
|
||||||
|
json.noInlineConfig = true;
|
||||||
|
return json;
|
||||||
|
});
|
||||||
|
await convertToFlatConfigGenerator(tree, options);
|
||||||
|
|
||||||
|
expect(tree.read('eslint.config.js', 'utf-8')).toMatchInlineSnapshot(`
|
||||||
|
"const { FlatCompat } = require('@eslint/eslintrc');
|
||||||
|
const nxEslintPlugin = require('@nx/eslint-plugin');
|
||||||
|
const js = require('@eslint/js');
|
||||||
|
const compat = new FlatCompat({
|
||||||
|
baseDirectory: __dirname,
|
||||||
|
recommendedConfig: js.configs.recommended,
|
||||||
|
});
|
||||||
|
module.exports = [
|
||||||
|
{ plugins: { '@nx': nxEslintPlugin } },
|
||||||
|
{
|
||||||
|
linterOptions: {
|
||||||
|
noInlineConfig: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
files: ['**/*.ts', '**/*.tsx', '**/*.js', '**/*.jsx'],
|
||||||
|
rules: {
|
||||||
|
'@nx/enforce-module-boundaries': [
|
||||||
|
'error',
|
||||||
|
{
|
||||||
|
enforceBuildableLibDependency: true,
|
||||||
|
allow: [],
|
||||||
|
depConstraints: [
|
||||||
|
{
|
||||||
|
sourceTag: '*',
|
||||||
|
onlyDependOnLibsWithTags: ['*'],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
...compat.config({ extends: ['plugin:@nx/typescript'] }).map((config) => ({
|
||||||
|
...config,
|
||||||
|
files: ['**/*.ts', '**/*.tsx'],
|
||||||
|
rules: {},
|
||||||
|
})),
|
||||||
|
...compat.config({ extends: ['plugin:@nx/javascript'] }).map((config) => ({
|
||||||
|
...config,
|
||||||
|
files: ['**/*.js', '**/*.jsx'],
|
||||||
|
rules: {},
|
||||||
|
})),
|
||||||
|
];
|
||||||
|
"
|
||||||
|
`);
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -0,0 +1,129 @@
|
|||||||
|
import {
|
||||||
|
formatFiles,
|
||||||
|
getProjects,
|
||||||
|
NxJsonConfiguration,
|
||||||
|
ProjectConfiguration,
|
||||||
|
readNxJson,
|
||||||
|
Tree,
|
||||||
|
updateJson,
|
||||||
|
updateProjectConfiguration,
|
||||||
|
} from '@nx/devkit';
|
||||||
|
import { ConvertToFlatConfigGeneratorSchema } from './schema';
|
||||||
|
import { findEslintFile } from '../utils/eslint-file';
|
||||||
|
import { convertEslintJsonToFlatConfig } from './converters/json-converter';
|
||||||
|
|
||||||
|
export async function convertToFlatConfigGenerator(
|
||||||
|
tree: Tree,
|
||||||
|
options: ConvertToFlatConfigGeneratorSchema
|
||||||
|
) {
|
||||||
|
const eslintFile = findEslintFile(tree);
|
||||||
|
if (!eslintFile) {
|
||||||
|
throw new Error('Could not find root eslint file');
|
||||||
|
}
|
||||||
|
if (!eslintFile.endsWith('.json')) {
|
||||||
|
throw new Error(
|
||||||
|
'Only json eslint config files are supported for conversion'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// rename root eslint config to eslint.config.js
|
||||||
|
convertRootToFlatConfig(tree, eslintFile);
|
||||||
|
// rename and map files
|
||||||
|
const projects = getProjects(tree);
|
||||||
|
for (const [project, projectConfig] of projects) {
|
||||||
|
convertProjectToFlatConfig(tree, project, projectConfig, readNxJson(tree));
|
||||||
|
}
|
||||||
|
// replace references in nx.json
|
||||||
|
updateNxJsonConfig(tree);
|
||||||
|
// install missing packages
|
||||||
|
|
||||||
|
if (!options.skipFormat) {
|
||||||
|
await formatFiles(tree);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default convertToFlatConfigGenerator;
|
||||||
|
|
||||||
|
function convertRootToFlatConfig(tree: Tree, eslintFile: string) {
|
||||||
|
if (eslintFile.endsWith('.base.json')) {
|
||||||
|
convertConfigToFlatConfig(
|
||||||
|
tree,
|
||||||
|
'',
|
||||||
|
'.eslintrc.base.json',
|
||||||
|
'eslint.base.config.js'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
convertConfigToFlatConfig(tree, '', '.eslintrc.json', 'eslint.config.js');
|
||||||
|
}
|
||||||
|
|
||||||
|
function convertProjectToFlatConfig(
|
||||||
|
tree: Tree,
|
||||||
|
project: string,
|
||||||
|
projectConfig: ProjectConfiguration,
|
||||||
|
nxJson: NxJsonConfiguration
|
||||||
|
) {
|
||||||
|
if (tree.exists(`${projectConfig.root}/.eslintrc.json`)) {
|
||||||
|
if (projectConfig.targets) {
|
||||||
|
const eslintTargets = Object.keys(projectConfig.targets || {}).filter(
|
||||||
|
(t) => projectConfig.targets[t].executor === '@nx/linter:eslint'
|
||||||
|
);
|
||||||
|
for (const target of eslintTargets) {
|
||||||
|
// remove any obsolete `eslintConfig` options pointing to the old config file
|
||||||
|
if (projectConfig.targets[target].options?.eslintConfig) {
|
||||||
|
delete projectConfig.targets[target].options.eslintConfig;
|
||||||
|
}
|
||||||
|
updateProjectConfiguration(tree, project, projectConfig);
|
||||||
|
}
|
||||||
|
const nxHasLintTargets = Object.keys(nxJson.targetDefaults || {}).some(
|
||||||
|
(t) =>
|
||||||
|
(t === '@nx/linter:eslint' ||
|
||||||
|
nxJson.targetDefaults[t].executor === '@nx/linter:eslint') &&
|
||||||
|
projectConfig.targets?.[t]
|
||||||
|
);
|
||||||
|
if (nxHasLintTargets || eslintTargets.length > 0) {
|
||||||
|
convertConfigToFlatConfig(
|
||||||
|
tree,
|
||||||
|
projectConfig.root,
|
||||||
|
'.eslintrc.json',
|
||||||
|
'eslint.config.js'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// update names of eslint files in nx.json
|
||||||
|
// and remove eslintignore
|
||||||
|
function updateNxJsonConfig(tree: Tree) {
|
||||||
|
if (tree.exists('nx.json')) {
|
||||||
|
updateJson(tree, 'nx.json', (json: NxJsonConfiguration) => {
|
||||||
|
if (json.targetDefaults?.lint?.inputs) {
|
||||||
|
const inputSet = new Set(json.targetDefaults.lint.inputs);
|
||||||
|
inputSet.add('{workspaceRoot}/eslint.config.js');
|
||||||
|
json.targetDefaults.lint.inputs = Array.from(inputSet);
|
||||||
|
}
|
||||||
|
if (json.targetDefaults?.['@nx/linter:eslint']?.inputs) {
|
||||||
|
const inputSet = new Set(
|
||||||
|
json.targetDefaults['@nx/linter:eslint'].inputs
|
||||||
|
);
|
||||||
|
inputSet.add('{workspaceRoot}/eslint.config.js');
|
||||||
|
json.targetDefaults['@nx/linter:eslint'].inputs = Array.from(inputSet);
|
||||||
|
}
|
||||||
|
if (json.namedInputs?.production) {
|
||||||
|
const inputSet = new Set(json.namedInputs.production);
|
||||||
|
inputSet.add('!{projectRoot}/eslint.config.js');
|
||||||
|
json.namedInputs.production = Array.from(inputSet);
|
||||||
|
}
|
||||||
|
return json;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function convertConfigToFlatConfig(
|
||||||
|
tree: Tree,
|
||||||
|
root: string,
|
||||||
|
source: string,
|
||||||
|
target: string
|
||||||
|
) {
|
||||||
|
convertEslintJsonToFlatConfig(tree, root, source, target);
|
||||||
|
}
|
||||||
3
packages/linter/src/generators/convert-to-flat-config/schema.d.ts
vendored
Normal file
3
packages/linter/src/generators/convert-to-flat-config/schema.d.ts
vendored
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
export interface ConvertToFlatConfigGeneratorSchema {
|
||||||
|
skipFormat?: boolean;
|
||||||
|
}
|
||||||
@ -0,0 +1,17 @@
|
|||||||
|
{
|
||||||
|
"$schema": "http://json-schema.org/schema",
|
||||||
|
"$id": "ConvertToFlatConfig",
|
||||||
|
"cli": "nx",
|
||||||
|
"description": "Convert an Nx workspace to a Flat ESLint config.",
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"skipFormat": {
|
||||||
|
"type": "boolean",
|
||||||
|
"description": "Skip formatting files.",
|
||||||
|
"default": false,
|
||||||
|
"x-priority": "internal"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"additionalProperties": false,
|
||||||
|
"required": []
|
||||||
|
}
|
||||||
@ -34,6 +34,7 @@ describe('@nx/linter:init', () => {
|
|||||||
'default',
|
'default',
|
||||||
'{workspaceRoot}/.eslintrc.json',
|
'{workspaceRoot}/.eslintrc.json',
|
||||||
'{workspaceRoot}/.eslintignore',
|
'{workspaceRoot}/.eslintignore',
|
||||||
|
'{workspaceRoot}/eslint.config.js',
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@ -32,6 +32,7 @@ function addTargetDefaults(tree: Tree) {
|
|||||||
if (productionFileSet) {
|
if (productionFileSet) {
|
||||||
// Remove .eslintrc.json
|
// Remove .eslintrc.json
|
||||||
productionFileSet.push('!{projectRoot}/.eslintrc.json');
|
productionFileSet.push('!{projectRoot}/.eslintrc.json');
|
||||||
|
productionFileSet.push('!{projectRoot}/eslint.config.js');
|
||||||
// Dedupe and set
|
// Dedupe and set
|
||||||
nxJson.namedInputs.production = Array.from(new Set(productionFileSet));
|
nxJson.namedInputs.production = Array.from(new Set(productionFileSet));
|
||||||
}
|
}
|
||||||
@ -43,6 +44,7 @@ function addTargetDefaults(tree: Tree) {
|
|||||||
'default',
|
'default',
|
||||||
`{workspaceRoot}/.eslintrc.json`,
|
`{workspaceRoot}/.eslintrc.json`,
|
||||||
`{workspaceRoot}/.eslintignore`,
|
`{workspaceRoot}/.eslintignore`,
|
||||||
|
`{workspaceRoot}/eslint.config.js`,
|
||||||
];
|
];
|
||||||
updateNxJson(tree, nxJson);
|
updateNxJson(tree, nxJson);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
export const nxVersion = require('../../package.json').version;
|
export const nxVersion = require('../../package.json').version;
|
||||||
|
|
||||||
export const eslintVersion = '~8.46.0';
|
export const eslintVersion = '~8.46.0';
|
||||||
|
export const eslintrcVersion = '^2.1.1';
|
||||||
export const eslintConfigPrettierVersion = '8.1.0';
|
export const eslintConfigPrettierVersion = '8.1.0';
|
||||||
/** @deprecated This will be removed in v17 */
|
/** @deprecated This will be removed in v17 */
|
||||||
export const tslintToEslintConfigVersion = '^2.13.0';
|
export const tslintToEslintConfigVersion = '^2.13.0';
|
||||||
|
|||||||
@ -90,7 +90,9 @@ function createNxJson(
|
|||||||
karmaProjectConfigFile ? '!{projectRoot}/karma.conf.js' : undefined,
|
karmaProjectConfigFile ? '!{projectRoot}/karma.conf.js' : undefined,
|
||||||
].filter(Boolean)
|
].filter(Boolean)
|
||||||
: []),
|
: []),
|
||||||
eslintProjectConfigFile ? '!{projectRoot}/.eslintrc.json' : undefined,
|
...(eslintProjectConfigFile
|
||||||
|
? ['!{projectRoot}/.eslintrc.json', '!{projectRoot}/eslint.config.js']
|
||||||
|
: []),
|
||||||
].filter(Boolean),
|
].filter(Boolean),
|
||||||
};
|
};
|
||||||
nxJson.targetDefaults = {};
|
nxJson.targetDefaults = {};
|
||||||
@ -115,6 +117,9 @@ function createNxJson(
|
|||||||
if (fileExists(join(repoRoot, '.eslintrc.json'))) {
|
if (fileExists(join(repoRoot, '.eslintrc.json'))) {
|
||||||
inputs.push('{workspaceRoot}/.eslintrc.json');
|
inputs.push('{workspaceRoot}/.eslintrc.json');
|
||||||
}
|
}
|
||||||
|
if (fileExists(join(repoRoot, 'eslint.config.js'))) {
|
||||||
|
inputs.push('{workspaceRoot}/eslint.config.js');
|
||||||
|
}
|
||||||
nxJson.targetDefaults.lint = { inputs };
|
nxJson.targetDefaults.lint = { inputs };
|
||||||
}
|
}
|
||||||
if (workspaceTargets.includes('e2e')) {
|
if (workspaceTargets.includes('e2e')) {
|
||||||
|
|||||||
6
pnpm-lock.yaml
generated
6
pnpm-lock.yaml
generated
@ -217,6 +217,12 @@ devDependencies:
|
|||||||
'@babel/runtime':
|
'@babel/runtime':
|
||||||
specifier: ^7.22.6
|
specifier: ^7.22.6
|
||||||
version: 7.22.6
|
version: 7.22.6
|
||||||
|
'@eslint/eslintrc':
|
||||||
|
specifier: ^2.1.1
|
||||||
|
version: 2.1.1
|
||||||
|
'@eslint/js':
|
||||||
|
specifier: ^8.46.0
|
||||||
|
version: 8.46.0
|
||||||
'@floating-ui/react':
|
'@floating-ui/react':
|
||||||
specifier: 0.19.2
|
specifier: 0.19.2
|
||||||
version: 0.19.2(@types/react@18.2.14)(react-dom@18.2.0)(react@18.2.0)
|
version: 0.19.2(@types/react@18.2.14)(react-dom@18.2.0)(react@18.2.0)
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user