Skip to content

Commit

Permalink
feat(linter): add migration for updating eslint flat config with new …
Browse files Browse the repository at this point in the history
…extensions
  • Loading branch information
jaysoo committed Dec 18, 2024
1 parent a8f3523 commit 7713c79
Show file tree
Hide file tree
Showing 5 changed files with 295 additions and 0 deletions.
5 changes: 5 additions & 0 deletions packages/eslint/migrations.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,11 @@
"version": "20.2.0-beta.5",
"description": "Update TypeScript ESLint packages to v8.13.0 if they are already on v8",
"implementation": "./src/migrations/update-20-2-0/update-typescript-eslint-v8-13-0"
},
"add-file-extensions-to-overrides": {
"version": "20.3.0-beta.1",
"description": "Update ESLint flat config to include .cjs, .mjs, .cts, and .mts files in overrides (if needed)",
"implementation": "./src/migrations/update-20-3-0/add-file-extensions-to-overrides"
}
},
"packageJsonUpdates": {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,8 @@ describe('ast-utils', () => {
}).map(config => ({
...config,
files: [
"**/*.ts",
"**/*.tsx",
"**/*.cts",
"**/*.mts"
],
Expand Down
4 changes: 4 additions & 0 deletions packages/eslint/src/generators/utils/flat-config/ast-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1016,6 +1016,8 @@ export function generateFlatOverride(
rest.extends === 'plugin:@nx/javascript'
) {
const newFiles = new Set(files);
newFiles.add('**/*.js');
newFiles.add('**/*.jsx');
newFiles.add('**/*.cjs');
newFiles.add('**/*.mjs');
files = Array.from(newFiles);
Expand All @@ -1027,6 +1029,8 @@ export function generateFlatOverride(
rest.extends === 'plugin:@nx/typescript'
) {
const newFiles = new Set(files);
newFiles.add('**/*.ts');
newFiles.add('**/*.tsx');
newFiles.add('**/*.cts');
newFiles.add('**/*.mts');
files = Array.from(newFiles);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
import { type Tree } from '@nx/devkit';
import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing';
import migration from './add-file-extensions-to-overrides';

describe('add-file-extensions-to-overrides', () => {
let tree: Tree;

beforeEach(() => {
tree = createTreeWithEmptyWorkspace();
});

it('should add .cjs, .mjs, .cts, .mts file extensions to overrides converted using convert-to-flat-config', async () => {
tree.write(
'eslint.config.js',
`const { FlatCompat } = require('@eslint/eslintrc');
const js = require('@eslint/js');
const nxEslintPlugin = require('@nx/eslint-plugin');
const compat = new FlatCompat({
baseDirectory: __dirname,
recommendedConfig: js.configs.recommended,
});
module.exports = [
...compat
.config({
extends: ['plugin:@nx/typescript'],
})
.map((config) => ({
...config,
files: ['**/*.ts', '**/*.tsx'],
rules: {
...config.rules,
},
})),
...compat
.config({
extends: ['plugin:@nx/javascript'],
})
.map((config) => ({
...config,
files: ['**/*.js', '**/*.jsx'],
rules: {
...config.rules,
},
})),
];`
);

await migration(tree);

const updated = tree.read('eslint.config.js', 'utf-8');
expect(updated).toMatchInlineSnapshot(`
"const { FlatCompat } = require('@eslint/eslintrc');
const js = require('@eslint/js');
const nxEslintPlugin = require('@nx/eslint-plugin');
const compat = new FlatCompat({
baseDirectory: __dirname,
recommendedConfig: js.configs.recommended,
});
module.exports = [
...compat
.config({
extends: ['plugin:@nx/typescript'],
})
.map((config) => ({
...config,
files: ['**/*.ts', '**/*.tsx', '**/*.cts', '**/*.mts'],
rules: {
...config.rules,
},
})),
...compat
.config({
extends: ['plugin:@nx/javascript'],
})
.map((config) => ({
...config,
files: ['**/*.js', '**/*.jsx', '**/*.cjs', '**/*.mjs'],
rules: {
...config.rules,
},
})),
];"
`);
});

it('should handle duplicates', async () => {
tree.write(
'eslint.config.js',
`const { FlatCompat } = require('@eslint/eslintrc');
const js = require('@eslint/js');
const nxEslintPlugin = require('@nx/eslint-plugin');
const compat = new FlatCompat({
baseDirectory: __dirname,
recommendedConfig: js.configs.recommended,
});
module.exports = [
...compat
.config({
extends: ['plugin:@nx/javascript'],
})
.map((config) => ({
...config,
files: ['**/*.js', '**/*.jsx', '**/*.mjs'],
rules: {
...config.rules,
},
})),
];`
);

await migration(tree);

const updated = tree.read('eslint.config.js', 'utf-8');
expect(updated).toMatchInlineSnapshot(`
"const { FlatCompat } = require('@eslint/eslintrc');
const js = require('@eslint/js');
const nxEslintPlugin = require('@nx/eslint-plugin');
const compat = new FlatCompat({
baseDirectory: __dirname,
recommendedConfig: js.configs.recommended,
});
module.exports = [
...compat
.config({
extends: ['plugin:@nx/javascript'],
})
.map((config) => ({
...config,
files: ['**/*.js', '**/*.jsx', '**/*.mjs', '**/*.cjs'],
rules: {
...config.rules,
},
})),
];"
`);
});

it('should not update if plugin:@nx/javascript and plugin:@nx/typescript are not used', async () => {
const original = `const { FlatCompat } = require('@eslint/eslintrc');
const js = require('@eslint/js');
const nxEslintPlugin = require('@nx/eslint-plugin');
const compat = new FlatCompat({
baseDirectory: __dirname,
recommendedConfig: js.configs.recommended,
});
module.exports = [
...compat
.config({
extends: ['plugin:@acme/foo'],
})
.map((config) => ({
...config,
files: ['**/*.ts', '**/*.tsx'],
rules: {
...config.rules,
},
})),
...compat
.config({
extends: ['plugin:@acme/bar'],
})
.map((config) => ({
...config,
files: ['**/*.js', '**/*.jsx'],
rules: {
...config.rules,
},
})),
];`;
tree.write('eslint.config.js', original);

await migration(tree);

const updated = tree.read('eslint.config.js', 'utf-8');
expect(updated).toEqual(original);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import { type Tree } from '@nx/devkit';
import * as ts from 'typescript';
import { findNodes, replaceChange } from '@nx/js';

export default async function (tree: Tree): Promise<void> {
let rootConfig: string;

// NOTE: we don't support generating ESM base config currently so they are not handled.
for (const candidate of ['eslint.config.js', 'eslint.config.cjs']) {
if (tree.exists(candidate)) {
rootConfig = candidate;
break;
}
}

if (!rootConfig) return;

updateOverrideFileExtensions(
tree,
rootConfig,
'plugin:@nx/typescript',
[`'**/*.ts'`, `'**/*.tsx'`],
[`'**/*.cts'`, `'**/*.mts'`]
);

updateOverrideFileExtensions(
tree,
rootConfig,
'plugin:@nx/javascript',
[`'**/*.js'`, `'**/*.jsx'`],
[`'**/*.cjs'`, `'**/*.mjs'`]
);
}

function updateOverrideFileExtensions(
tree: Tree,
configFile: string,
plugin: string,
matchingExts: string[],
newExts: string[]
): void {
const content = tree.read(configFile, 'utf-8');
const source = ts.createSourceFile(
'',
content,
ts.ScriptTarget.Latest,
true,
ts.ScriptKind.JS
);
let compatNode: ts.SpreadElement;

const spreadElementNodes = findNodes(
source,
ts.SyntaxKind.SpreadElement
) as ts.SpreadElement[];
for (const a of spreadElementNodes) {
const assignmentNodes = findNodes(
a,
ts.SyntaxKind.PropertyAssignment
) as ts.PropertyAssignment[];
if (assignmentNodes.length === 0) continue;
for (const b of assignmentNodes) {
if (
b.name.getText() === 'extends' &&
b.initializer.getText().includes(plugin)
) {
compatNode = a;
break;
}
}
}

if (compatNode) {
const arrayNodes = findNodes(
compatNode,
ts.SyntaxKind.ArrayLiteralExpression
) as ts.ArrayLiteralExpression[];
for (const a of arrayNodes) {
if (
matchingExts.every((ext) => a.elements.some((e) => e.getText() === ext))
) {
const exts = new Set(a.elements.map((e) => e.getText()));
for (const ext of newExts) {
exts.add(ext);
}
replaceChange(
tree,
source,
configFile,
a.getStart(a.getSourceFile()),
`[${Array.from(exts).join(', ')}]`,
a.getText()
).getText();
}
}
}
}

0 comments on commit 7713c79

Please sign in to comment.