fix(vite): ignore vite temp files in eslint config (#29909)

## Current Behavior

Vite config temp files can sometimes cause errors to be thrown by
ESLint.

## Expected Behavior

Vite config temp files should be ignored by ESLint.

## Related Issue(s)

Fixes #
This commit is contained in:
Leosvel Pérez Espinosa 2025-02-13 17:04:26 +01:00 committed by GitHub
parent 9e204f973c
commit eb0505b1ad
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
31 changed files with 1120 additions and 91 deletions

View File

@ -5530,6 +5530,16 @@
"path": "/nx-api/vite/migrations/update-20-5-0-update-resolve-conditions",
"type": "migration"
},
"/nx-api/vite/migrations/eslint-ignore-vite-temp-files": {
"description": "Add vite config temporary files to the ESLint configuration ignore patterns if ESLint is used.",
"file": "generated/packages/vite/migrations/eslint-ignore-vite-temp-files.json",
"hidden": false,
"name": "eslint-ignore-vite-temp-files",
"version": "20.5.0-beta.3",
"originalFilePath": "/packages/vite",
"path": "/nx-api/vite/migrations/eslint-ignore-vite-temp-files",
"type": "migration"
},
"/nx-api/vite/migrations/20.5.0-package-updates": {
"description": "",
"file": "generated/packages/vite/migrations/20.5.0-package-updates.json",

View File

@ -5496,6 +5496,16 @@
"path": "vite/migrations/update-20-5-0-update-resolve-conditions",
"type": "migration"
},
{
"description": "Add vite config temporary files to the ESLint configuration ignore patterns if ESLint is used.",
"file": "generated/packages/vite/migrations/eslint-ignore-vite-temp-files.json",
"hidden": false,
"name": "eslint-ignore-vite-temp-files",
"version": "20.5.0-beta.3",
"originalFilePath": "/packages/vite",
"path": "vite/migrations/eslint-ignore-vite-temp-files",
"type": "migration"
},
{
"description": "",
"file": "generated/packages/vite/migrations/20.5.0-package-updates.json",

View File

@ -0,0 +1,12 @@
{
"name": "eslint-ignore-vite-temp-files",
"version": "20.5.0-beta.3",
"description": "Add vite config temporary files to the ESLint configuration ignore patterns if ESLint is used.",
"implementation": "/packages/vite/src/migrations/update-20-5-0/eslint-ignore-vite-temp-files.ts",
"aliases": [],
"hidden": false,
"path": "/packages/vite",
"schema": null,
"type": "migration",
"examplesFile": "#### Sample Code Changes\n\nAdd `vite.config.*.timestamp*` and `vitest.config.*.timestamp*` to the root `eslint.config.mjs` file (using **ESLint Flat Config**).\n\n{% tabs %}\n{% tab label=\"Before\" %}\n\n```js {% fileName=\"eslint.config.mjs\" %}\nexport default [\n {\n ignores: ['dist'],\n },\n];\n```\n\n{% /tab %}\n{% tab label=\"After\" %}\n\n```js {% highlightLines=[3] fileName=\"eslint.config.mjs\" %}\nexport default [\n {\n ignores: ['dist', 'vite.config.*.timestamp*', 'vitest.config.*.timestamp*'],\n },\n];\n```\n\n{% /tab %}\n\n{% /tabs %}\n\nAdd `vite.config.*.timestamp*` and `vitest.config.*.timestamp*` to the project's `.eslintrc.json` file (using **eslintrc** format config).\n\n{% tabs %}\n{% tab label=\"Before\" %}\n\n```json {% fileName=\"apps/app1/eslintrc.json\" %}\n{\n \"ignorePatterns\": [\"!**/*\"]\n}\n```\n\n{% /tab %}\n{% tab label=\"After\" %}\n\n```json {% highlightLines=[4,5] fileName=\"apps/app1/eslintrc.json\" %}\n{\n \"ignorePatterns\": [\n \"!**/*\",\n \"vite.config.*.timestamp*\",\n \"vitest.config.*.timestamp*\"\n ]\n}\n```\n\n{% /tab %}\n\n{% /tabs %}\n"
}

View File

@ -7,6 +7,7 @@ import {
} from '../../utils/config-file';
import {
addExtendsToLintConfig,
addIgnoresToLintConfig,
findEslintFile,
lintConfigHasOverride,
replaceOverridesInLintConfig,
@ -526,4 +527,197 @@ module.exports = [
`);
});
});
describe('addIgnoresToLintConfig', () => {
it('should add a new block with ignores to esm flat config when there is none', () => {
tree.write('eslint.config.mjs', 'export default [];');
addIgnoresToLintConfig(tree, '', ['**/some-dir/**/*']);
expect(tree.read('eslint.config.mjs', 'utf-8')).toMatchInlineSnapshot(`
"
export default [
{
ignores: [
"**/some-dir/**/*"
]
}
];
"
`);
});
it('should update existing block with ignores in esm flat config', () => {
tree.write(
'eslint.config.mjs',
`export default [
{
ignores: ["dist"],
}
];
`
);
addIgnoresToLintConfig(tree, '', ['**/some-dir/**/*']);
expect(tree.read('eslint.config.mjs', 'utf-8')).toMatchInlineSnapshot(`
"export default [
{
"ignores": [
"dist",
"**/some-dir/**/*"
]
}
];
"
`);
});
it('should not duplicate existing patterns in a block with ignores in esm flat config', () => {
tree.write(
'eslint.config.mjs',
`export default [
{
ignores: ["dist"],
}
];
`
);
addIgnoresToLintConfig(tree, '', ['**/some-dir/**/*', 'dist']);
expect(tree.read('eslint.config.mjs', 'utf-8')).toMatchInlineSnapshot(`
"export default [
{
"ignores": [
"dist",
"**/some-dir/**/*"
]
}
];
"
`);
});
it('should add a new block with ignores to cjs flat config when there is none', () => {
tree.write('eslint.config.cjs', 'module.exports = [];');
addIgnoresToLintConfig(tree, '', ['**/some-dir/**/*']);
expect(tree.read('eslint.config.cjs', 'utf-8')).toMatchInlineSnapshot(`
"module.exports = [,
{
ignores: [
"**/some-dir/**/*"
]
}];"
`);
});
it('should update existing block with ignores in cjs flat config', () => {
tree.write(
'eslint.config.cjs',
`module.exports = [
{
ignores: ["dist"],
}
];
`
);
addIgnoresToLintConfig(tree, '', ['**/some-dir/**/*']);
expect(tree.read('eslint.config.cjs', 'utf-8')).toMatchInlineSnapshot(`
"module.exports = [
{
"ignores": [
"dist",
"**/some-dir/**/*"
]
}
];
"
`);
});
it('should not duplicate existing patterns in a block with ignores in cjs flat config', () => {
tree.write(
'eslint.config.cjs',
`module.exports = [
{
ignores: ["dist"],
}
];
`
);
addIgnoresToLintConfig(tree, '', ['**/some-dir/**/*', 'dist']);
expect(tree.read('eslint.config.cjs', 'utf-8')).toMatchInlineSnapshot(`
"module.exports = [
{
"ignores": [
"dist",
"**/some-dir/**/*"
]
}
];
"
`);
});
it('should add ignore patterns to eslintrc config when there is none', () => {
tree.write('.eslintrc.json', '{}');
addIgnoresToLintConfig(tree, '', ['**/some-dir/**/*']);
expect(readJson(tree, '.eslintrc.json')).toMatchInlineSnapshot(`
{
"ignorePatterns": [
"**/some-dir/**/*",
],
}
`);
});
it('should update existing ignore patterns in eslintrc config', () => {
tree.write(
'.eslintrc.json',
`{
"ignorePatterns": ["dist"]
}`
);
addIgnoresToLintConfig(tree, '', ['**/some-dir/**/*']);
expect(readJson(tree, '.eslintrc.json')).toMatchInlineSnapshot(`
{
"ignorePatterns": [
"dist",
"**/some-dir/**/*",
],
}
`);
});
it('should not duplicate existing ignore patterns in eslintrc config', () => {
tree.write(
'.eslintrc.json',
`{
"ignorePatterns": ["dist"]
}`
);
addIgnoresToLintConfig(tree, '', ['**/some-dir/**/*', 'dist']);
expect(readJson(tree, '.eslintrc.json')).toMatchInlineSnapshot(`
{
"ignorePatterns": [
"dist",
"**/some-dir/**/*",
],
}
`);
});
});
});

View File

@ -14,6 +14,7 @@ import {
baseEsLintConfigFile,
ESLINT_CONFIG_FILENAMES,
BASE_ESLINT_CONFIG_FILENAMES,
ESLINT_FLAT_CONFIG_FILENAMES,
} from '../../utils/config-file';
import {
eslintFlatConfigFilenames,
@ -29,12 +30,14 @@ import {
addBlockToFlatConfigExport,
addFlatCompatToFlatConfig,
addImportToFlatConfig,
addPatternsToFlatConfigIgnoresBlock,
addPluginsToExportsBlock,
generateAst,
generateFlatOverride,
generateFlatPredefinedConfig,
generatePluginExtendsElement,
generatePluginExtendsElementWithCompatFixup,
hasFlatConfigIgnoresBlock,
hasOverride,
overrideNeedsCompat,
removeOverridesFromLintConfig,
@ -609,15 +612,25 @@ export function addIgnoresToLintConfig(
}
}
const block = generateAst<ts.ObjectLiteralExpression>({
ignores: ignorePatterns.map((path) => mapFilePath(path)),
});
tree.write(
fileName,
addBlockToFlatConfigExport(tree.read(fileName, 'utf8'), block)
);
if (!fileName) {
return;
}
let content = tree.read(fileName, 'utf8');
if (hasFlatConfigIgnoresBlock(content)) {
content = addPatternsToFlatConfigIgnoresBlock(content, ignorePatterns);
tree.write(fileName, content);
} else {
const block = generateAst<ts.ObjectLiteralExpression>({
ignores: ignorePatterns.map((path) => mapFilePath(path)),
});
tree.write(fileName, addBlockToFlatConfigExport(content, block));
}
} else {
const fileName = joinPathFragments(root, '.eslintrc.json');
if (!tree.exists(fileName)) {
return;
}
updateJson(tree, fileName, (json) => {
const ignoreSet = new Set([
...(json.ignorePatterns ?? []),

View File

@ -75,6 +75,84 @@ function findModuleExports(source: ts.SourceFile): ts.NodeArray<ts.Node> {
});
}
export function addPatternsToFlatConfigIgnoresBlock(
content: string,
ignorePatterns: string[]
): string {
const source = ts.createSourceFile(
'',
content,
ts.ScriptTarget.Latest,
true,
ts.ScriptKind.JS
);
const format = content.includes('export default') ? 'mjs' : 'cjs';
const exportsArray =
format === 'mjs' ? findExportDefault(source) : findModuleExports(source);
if (!exportsArray) {
return content;
}
const changes: StringChange[] = [];
for (const node of exportsArray) {
if (!isFlatConfigIgnoresBlock(node)) {
continue;
}
const start = node.properties.pos + 1; // keep leading line break
const data = parseTextToJson(node.getFullText());
changes.push({
type: ChangeType.Delete,
start,
length: node.properties.end - start,
});
data.ignores = Array.from(
new Set([...(data.ignores ?? []), ...ignorePatterns])
);
changes.push({
type: ChangeType.Insert,
index: start,
text:
' ' +
JSON.stringify(data, null, 2)
.slice(2, -2) // Remove curly braces and start/end line breaks
.replaceAll(/\n/g, '\n '), // Maintain indentation
});
break;
}
return applyChangesToString(content, changes);
}
export function hasFlatConfigIgnoresBlock(content: string): boolean {
const source = ts.createSourceFile(
'',
content,
ts.ScriptTarget.Latest,
true,
ts.ScriptKind.JS
);
const format = content.includes('export default') ? 'mjs' : 'cjs';
const exportsArray =
format === 'mjs' ? findExportDefault(source) : findModuleExports(source);
if (!exportsArray) {
return false;
}
return exportsArray.some(isFlatConfigIgnoresBlock);
}
function isFlatConfigIgnoresBlock(
node: ts.Node
): node is ts.ObjectLiteralExpression {
return (
ts.isObjectLiteralExpression(node) &&
node.properties.length === 1 &&
(node.properties[0].name.getText() === 'ignores' ||
node.properties[0].name.getText() === '"ignores"') &&
ts.isPropertyAssignment(node.properties[0]) &&
ts.isArrayLiteralExpression(node.properties[0].initializer)
);
}
function isOverride(node: ts.Node): boolean {
return (
(ts.isObjectLiteralExpression(node) &&

View File

@ -22,7 +22,14 @@ exports[`app generated files content - as-provided - my-app general application
exports[`app generated files content - as-provided - my-app general application should configure eslint correctly (eslintrc) 1`] = `
"{
"extends": ["@nuxt/eslint-config", "../.eslintrc.json"],
"ignorePatterns": ["!**/*", ".nuxt/**", ".output/**", "node_modules"],
"ignorePatterns": [
"!**/*",
".nuxt/**",
".output/**",
"node_modules",
"**/vite.config.*.timestamp*",
"**/vitest.config.*.timestamp*"
],
"overrides": [
{
"files": ["*.ts", "*.tsx", "*.js", "*.jsx", "*.vue"],
@ -416,7 +423,14 @@ exports[`app generated files content - as-provided - myApp general application s
exports[`app generated files content - as-provided - myApp general application should configure eslint correctly (eslintrc) 1`] = `
"{
"extends": ["@nuxt/eslint-config", "../.eslintrc.json"],
"ignorePatterns": ["!**/*", ".nuxt/**", ".output/**", "node_modules"],
"ignorePatterns": [
"!**/*",
".nuxt/**",
".output/**",
"node_modules",
"**/vite.config.*.timestamp*",
"**/vitest.config.*.timestamp*"
],
"overrides": [
{
"files": ["*.ts", "*.tsx", "*.js", "*.jsx", "*.vue"],

View File

@ -122,6 +122,9 @@ export async function applicationGeneratorInternal(
tasks.push(twTask);
}
const lintTask = await addLinting(tree, options);
tasks.push(lintTask);
if (options.bundler === 'vite') {
await setupViteConfiguration(tree, options, tasks);
} else if (options.bundler === 'rsbuild') {
@ -144,9 +147,6 @@ export async function applicationGeneratorInternal(
);
}
const lintTask = await addLinting(tree, options);
tasks.push(lintTask);
const e2eTask = await addE2e(tree, options);
tasks.push(e2eTask);

View File

@ -659,7 +659,13 @@ export default defineConfig({
exports[`Remix Application Standalone Project Repo should create the application correctly 5`] = `
"{
"root": true,
"ignorePatterns": ["!**/*", "build", "public/build"],
"ignorePatterns": [
"!**/*",
"build",
"public/build",
"**/vite.config.*.timestamp*",
"**/vitest.config.*.timestamp*"
],
"plugins": ["@nx"],
"overrides": [
{

View File

@ -47,6 +47,66 @@ describe('Remix Application', () => {
expect(tree.read('.eslintrc.json', 'utf-8')).toMatchSnapshot();
});
it('should ignore vite temp files', async () => {
const tree = createTreeWithEmptyWorkspace({ layout: 'apps-libs' });
await applicationGenerator(tree, {
name: 'test',
directory: '.',
addPlugin: true,
skipFormat: true,
});
expect(tree.read('.gitignore', 'utf-8')).toMatchInlineSnapshot(`
"null
.cache
build
public/build
.env
vite.config.*.timestamp*
vitest.config.*.timestamp*"
`);
expect(tree.read('.eslintrc.json', 'utf-8')).toMatchInlineSnapshot(`
"{
"root": true,
"ignorePatterns": [
"!**/*",
"build",
"public/build",
"**/vite.config.*.timestamp*",
"**/vitest.config.*.timestamp*"
],
"plugins": [
"@nx"
],
"overrides": [
{
"files": [
"*.ts",
"*.tsx"
],
"extends": [
"plugin:@nx/typescript"
],
"rules": {}
},
{
"files": [
"*.js",
"*.jsx"
],
"extends": [
"plugin:@nx/javascript"
],
"rules": {}
}
]
}
"
`);
});
describe('--unitTestRunner', () => {
it('should generate the correct files for testing using vitest', async () => {
// ARRANGE
@ -177,6 +237,62 @@ describe('Remix Application', () => {
).toMatchSnapshot();
});
it('should ignore vite temp files', async () => {
const tree = createTreeWithEmptyWorkspace({ layout: 'apps-libs' });
await applicationGenerator(tree, {
directory: 'test',
addPlugin: true,
skipFormat: true,
});
expect(tree.read('.gitignore', 'utf-8')).toMatchInlineSnapshot(`
"vite.config.*.timestamp*
vitest.config.*.timestamp*"
`);
expect(tree.read(`${appDir}/.eslintrc.json`, 'utf-8'))
.toMatchInlineSnapshot(`
"{
"extends": [
"../.eslintrc.json"
],
"ignorePatterns": [
"!**/*",
"build",
"public/build",
"**/vite.config.*.timestamp*",
"**/vitest.config.*.timestamp*"
],
"overrides": [
{
"files": [
"*.ts",
"*.tsx",
"*.js",
"*.jsx"
],
"rules": {}
},
{
"files": [
"*.ts",
"*.tsx"
],
"rules": {}
},
{
"files": [
"*.js",
"*.jsx"
],
"rules": {}
}
]
}
"
`);
});
describe('--directory', () => {
it('should create the application correctly', async () => {
// ARRANGE

View File

@ -37,7 +37,7 @@ import initGenerator from '../init/init';
import { updateDependencies } from '../utils/update-dependencies';
import {
addE2E,
addViteTempFilesToGitIgnore,
ignoreViteTempFiles,
normalizeOptions,
updateUnitTestConfig,
} from './lib';
@ -312,7 +312,7 @@ export default {...nxPreset};
tasks.push(await addE2E(tree, options));
addViteTempFilesToGitIgnore(tree);
await ignoreViteTempFiles(tree, options.projectRoot);
updateTsconfigFiles(
tree,

View File

@ -1,16 +0,0 @@
import { stripIndents, Tree } from '@nx/devkit';
export function addViteTempFilesToGitIgnore(tree: Tree) {
let newGitIgnoreContents = `vite.config.*.timestamp*`;
if (tree.exists('.gitignore')) {
const gitIgnoreContents = tree.read('.gitignore', 'utf-8');
if (!gitIgnoreContents.includes(newGitIgnoreContents)) {
newGitIgnoreContents = stripIndents`${gitIgnoreContents}
${newGitIgnoreContents}`;
tree.write('.gitignore', newGitIgnoreContents);
}
} else {
tree.write('.gitignore', newGitIgnoreContents);
}
}

View File

@ -0,0 +1,75 @@
import { ensurePackage, readJson, stripIndents, type Tree } from '@nx/devkit';
import { getPackageVersion } from '../../../utils/versions';
export async function ignoreViteTempFiles(
tree: Tree,
projectRoot?: string | undefined
): Promise<void> {
addViteTempFilesToGitIgnore(tree);
await ignoreViteTempFilesInEslintConfig(tree, projectRoot);
}
function addViteTempFilesToGitIgnore(tree: Tree): void {
let gitIgnoreContents = tree.exists('.gitignore')
? tree.read('.gitignore', 'utf-8')
: '';
if (!/^vite\.config\.\*\.timestamp\*$/m.test(gitIgnoreContents)) {
gitIgnoreContents = stripIndents`${gitIgnoreContents}
vite.config.*.timestamp*`;
}
if (!/^vitest\.config\.\*\.timestamp\*$/m.test(gitIgnoreContents)) {
gitIgnoreContents = stripIndents`${gitIgnoreContents}
vitest.config.*.timestamp*`;
}
tree.write('.gitignore', gitIgnoreContents);
}
async function ignoreViteTempFilesInEslintConfig(
tree: Tree,
projectRoot: string | undefined
): Promise<void> {
if (!isEslintInstalled(tree)) {
return;
}
ensurePackage('@nx/eslint', getPackageVersion(tree, 'nx'));
const { addIgnoresToLintConfig, isEslintConfigSupported } = await import(
'@nx/eslint/src/generators/utils/eslint-file'
);
if (!isEslintConfigSupported(tree)) {
return;
}
const { useFlatConfig } = await import('@nx/eslint/src/utils/flat-config');
const isUsingFlatConfig = useFlatConfig(tree);
if (!projectRoot && !isUsingFlatConfig) {
// root eslintrc files ignore all files and the root eslintrc files add
// back all the project files, so we only add the ignores to the project
// eslintrc files
return;
}
// for flat config, we update the root config file
const directory = isUsingFlatConfig ? '' : projectRoot ?? '';
addIgnoresToLintConfig(tree, directory, [
'**/vite.config.*.timestamp*',
'**/vitest.config.*.timestamp*',
]);
}
export function isEslintInstalled(tree: Tree): boolean {
try {
require('eslint');
return true;
} catch {}
// it might not be installed yet, but it might be in the tree pending install
const { devDependencies, dependencies } = tree.exists('package.json')
? readJson(tree, 'package.json')
: {};
return !!devDependencies?.['eslint'] || !!dependencies?.['eslint'];
}

View File

@ -1,4 +1,4 @@
export * from './normalize-options';
export * from './update-unit-test-config';
export * from './add-e2e';
export * from './add-vite-temp-files-to-gitignore';
export * from './ignore-vite-temp-files';

View File

@ -29,7 +29,15 @@
"error",
{
"buildTargets": ["build-base"],
"ignoredDependencies": ["nx", "typescript", "vite"]
"ignoredDependencies": [
"nx",
"typescript",
"vite",
// we only check if the package is installed
"eslint",
// we ensure it is installed and only use it when eslint is installed
"@nx/eslint"
]
}
]
}

View File

@ -44,6 +44,11 @@
"version": "20.5.0-beta.3",
"description": "Update resolve.conditions to include defaults that are no longer provided by Vite.",
"implementation": "./src/migrations/update-20-5-0/update-resolve-conditions"
},
"eslint-ignore-vite-temp-files": {
"version": "20.5.0-beta.3",
"description": "Add vite config temporary files to the ESLint configuration ignore patterns if ESLint is used.",
"implementation": "./src/migrations/update-20-5-0/eslint-ignore-vite-temp-files"
}
},
"packageJsonUpdates": {

View File

@ -81,7 +81,11 @@ export async function viteConfigurationGeneratorInternal(
tsConfigName: projectRoot === '.' ? 'tsconfig.json' : 'tsconfig.base.json',
});
tasks.push(jsInitTask);
const initTask = await initGenerator(tree, { ...schema, skipFormat: true });
const initTask = await initGenerator(tree, {
...schema,
projectRoot,
skipFormat: true,
});
tasks.push(initTask);
tasks.push(ensureDependencies(tree, schema));

View File

@ -132,17 +132,200 @@ describe('@nx/vite:init', () => {
`);
});
it('should ignore vite temp files in gitignore', async () => {
await initGenerator(tree, {});
expect(tree.read('.gitignore', 'utf-8')).toMatchInlineSnapshot(`
"vite.config.*.timestamp*
vitest.config.*.timestamp*"
`);
});
it(`should not add multiple instances of the same vite temp file glob to gitignore`, async () => {
// ARRANGE
tree.write('.gitignore', 'vite.config.*.timestamp*');
tree.write(
'.gitignore',
`vitest.config.*.timestamp*
vite.config.*.timestamp*`
);
// ACT
await initGenerator(tree, {});
// ASSERT
expect(tree.read('.gitignore', 'utf-8')).toMatchInlineSnapshot(`
"vite.config.*.timestamp*
vitest.config.*.timestamp*"
"vitest.config.*.timestamp*
vite.config.*.timestamp*"
`);
});
it('should ignore vite temp files in eslint flat config without a block with ignores', async () => {
updateJson(tree, 'package.json', (json) => {
json.devDependencies = { eslint: '9.0.0' };
return json;
});
tree.write('eslint.config.mjs', `export default [];`);
await initGenerator(tree, {});
expect(tree.read('eslint.config.mjs', 'utf-8')).toMatchInlineSnapshot(`
"export default [
{
ignores: ['**/vite.config.*.timestamp*', '**/vitest.config.*.timestamp*'],
},
];
"
`);
});
it('should ignore vite temp files in eslint flat config with a block with ignores', async () => {
updateJson(tree, 'package.json', (json) => {
json.devDependencies = { eslint: '9.0.0' };
return json;
});
tree.write(
'eslint.config.mjs',
`export default [
{
ignores: ['dist'],
},
];`
);
await initGenerator(tree, {});
expect(tree.read('eslint.config.mjs', 'utf-8')).toMatchInlineSnapshot(`
"export default [
{
ignores: [
'dist',
'**/vite.config.*.timestamp*',
'**/vitest.config.*.timestamp*',
],
},
];
"
`);
});
it('should not duplicate vite temp files in eslint flat config', async () => {
updateJson(tree, 'package.json', (json) => {
json.devDependencies = { eslint: '9.0.0' };
return json;
});
tree.write(
'eslint.config.mjs',
`export default [
{
ignores: ['**/vitest.config.*.timestamp*', '**/vite.config.*.timestamp*'],
},
];`
);
await initGenerator(tree, {});
expect(tree.read('eslint.config.mjs', 'utf-8')).toMatchInlineSnapshot(`
"export default [
{
ignores: ['**/vitest.config.*.timestamp*', '**/vite.config.*.timestamp*'],
},
];
"
`);
});
it('should ignore vite temp files in project eslintrc config without ignorePatterns', async () => {
updateJson(tree, 'package.json', (json) => {
json.devDependencies = { eslint: '9.0.0' };
return json;
});
tree.write('.eslintrc.json', JSON.stringify({ ignorePatterns: ['**/*'] }));
tree.write('apps/my-app/.eslintrc.json', `{}`);
await initGenerator(tree, { projectRoot: 'apps/my-app' });
expect(readJson(tree, '.eslintrc.json')).toMatchInlineSnapshot(`
{
"ignorePatterns": [
"**/*",
],
}
`);
expect(readJson(tree, 'apps/my-app/.eslintrc.json')).toMatchInlineSnapshot(`
{
"ignorePatterns": [
"**/vite.config.*.timestamp*",
"**/vitest.config.*.timestamp*",
],
}
`);
});
it('should ignore vite temp files in project eslintrc config with ignorePatterns config', async () => {
updateJson(tree, 'package.json', (json) => {
json.devDependencies = { eslint: '9.0.0' };
return json;
});
tree.write('.eslintrc.json', JSON.stringify({ ignorePatterns: ['**/*'] }));
tree.write(
'apps/my-app/.eslintrc.json',
JSON.stringify({ ignorePatterns: ['!**/*'] })
);
await initGenerator(tree, { projectRoot: 'apps/my-app' });
expect(readJson(tree, '.eslintrc.json')).toMatchInlineSnapshot(`
{
"ignorePatterns": [
"**/*",
],
}
`);
expect(readJson(tree, 'apps/my-app/.eslintrc.json')).toMatchInlineSnapshot(`
{
"ignorePatterns": [
"!**/*",
"**/vite.config.*.timestamp*",
"**/vitest.config.*.timestamp*",
],
}
`);
});
it('should not duplicate vite temp files in project eslintrc config', async () => {
updateJson(tree, 'package.json', (json) => {
json.devDependencies = { eslint: '9.0.0' };
return json;
});
tree.write('.eslintrc.json', JSON.stringify({ ignorePatterns: ['**/*'] }));
tree.write(
'apps/my-app/.eslintrc.json',
JSON.stringify({
ignorePatterns: [
'!**/*',
'**/vitest.config.*.timestamp*',
'**/vite.config.*.timestamp*',
],
})
);
await initGenerator(tree, { projectRoot: 'apps/my-app' });
expect(readJson(tree, '.eslintrc.json')).toMatchInlineSnapshot(`
{
"ignorePatterns": [
"**/*",
],
}
`);
expect(readJson(tree, 'apps/my-app/.eslintrc.json')).toMatchInlineSnapshot(`
{
"ignorePatterns": [
"!**/*",
"**/vitest.config.*.timestamp*",
"**/vite.config.*.timestamp*",
],
}
`);
});
});

View File

@ -13,7 +13,7 @@ import { setupPathsPlugin } from '../setup-paths-plugin/setup-paths-plugin';
import { createNodesV2 } from '../../plugins/plugin';
import { InitGeneratorSchema } from './schema';
import { checkDependenciesInstalled, moveToDevDependencies } from './lib/utils';
import { addViteTempFilesToGitIgnore } from '../../utils/add-vite-temp-files-to-gitignore';
import { ignoreViteTempFiles } from '../../utils/ignore-vite-temp-files';
export function updateNxJsonSettings(tree: Tree) {
const nxJson = readNxJson(tree);
@ -96,7 +96,7 @@ export async function initGeneratorInternal(
}
updateNxJsonSettings(tree);
addViteTempFilesToGitIgnore(tree);
await ignoreViteTempFiles(tree, schema.projectRoot);
if (schema.setupPathsPlugin) {
await setupPathsPlugin(tree, { skipFormat: true });

View File

@ -7,4 +7,5 @@ export interface InitGeneratorSchema {
addPlugin?: boolean;
vitestOnly?: boolean;
useViteV5?: boolean;
projectRoot?: string;
}

View File

@ -73,6 +73,7 @@ export async function vitestGeneratorInternal(
const useVite5 =
major(coerce(pkgJson.devDependencies['vite']) ?? '6.0.0') === 5;
const initTask = await initGenerator(tree, {
projectRoot: root,
skipFormat: true,
addPlugin: schema.addPlugin,
useViteV5: useVite5,

View File

@ -1,5 +1,5 @@
import { Tree } from '@nx/devkit';
import { addViteTempFilesToGitIgnore as _addViteTempFilesToGitIgnore } from '../../utils/add-vite-temp-files-to-gitignore';
import { addViteTempFilesToGitIgnore as _addViteTempFilesToGitIgnore } from '../../utils/ignore-vite-temp-files';
export default function addViteTempFilesToGitIgnore(tree: Tree) {
// need to check if .gitignore exists before adding to it

View File

@ -1,5 +1,5 @@
import { Tree } from '@nx/devkit';
import { addViteTempFilesToGitIgnore as _addViteTempFilesToGitIgnore } from '../../utils/add-vite-temp-files-to-gitignore';
import { addViteTempFilesToGitIgnore as _addViteTempFilesToGitIgnore } from '../../utils/ignore-vite-temp-files';
export default function addViteTempFilesToGitIgnore(tree: Tree) {
// need to check if .gitignore exists before adding to it

View File

@ -0,0 +1,57 @@
#### Sample Code Changes
Add `vite.config.*.timestamp*` and `vitest.config.*.timestamp*` to the root `eslint.config.mjs` file (using **ESLint Flat Config**).
{% tabs %}
{% tab label="Before" %}
```js {% fileName="eslint.config.mjs" %}
export default [
{
ignores: ['dist'],
},
];
```
{% /tab %}
{% tab label="After" %}
```js {% highlightLines=[3] fileName="eslint.config.mjs" %}
export default [
{
ignores: ['dist', 'vite.config.*.timestamp*', 'vitest.config.*.timestamp*'],
},
];
```
{% /tab %}
{% /tabs %}
Add `vite.config.*.timestamp*` and `vitest.config.*.timestamp*` to the project's `.eslintrc.json` file (using **eslintrc** format config).
{% tabs %}
{% tab label="Before" %}
```json {% fileName="apps/app1/eslintrc.json" %}
{
"ignorePatterns": ["!**/*"]
}
```
{% /tab %}
{% tab label="After" %}
```json {% highlightLines=[4,5] fileName="apps/app1/eslintrc.json" %}
{
"ignorePatterns": [
"!**/*",
"vite.config.*.timestamp*",
"vitest.config.*.timestamp*"
]
}
```
{% /tab %}
{% /tabs %}

View File

@ -0,0 +1,148 @@
import { addProjectConfiguration, type Tree } from '@nx/devkit';
import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing';
import { isEslintInstalled } from '../../utils/ignore-vite-temp-files';
import migration from './eslint-ignore-vite-temp-files';
jest.mock('../../utils/ignore-vite-temp-files', () => ({
...jest.requireActual('../../utils/ignore-vite-temp-files'),
isEslintInstalled: jest.fn(),
}));
describe('eslint-ignore-vite-temp-files migration', () => {
let tree: Tree;
let isEslintInstalledMock: jest.Mock;
beforeEach(() => {
tree = createTreeWithEmptyWorkspace();
isEslintInstalledMock = (isEslintInstalled as jest.Mock).mockReturnValue(
true
);
});
it('should not throw an error if eslint is not installed', async () => {
isEslintInstalledMock.mockReturnValue(false);
await expect(migration(tree)).resolves.not.toThrow();
});
it('should not throw an error if there are no eslint config files', async () => {
await expect(migration(tree)).resolves.not.toThrow();
});
it('should only update the root eslint config when using flat config', async () => {
tree.write('eslint.config.mjs', 'export default [];');
addProjectConfiguration(tree, 'app1', {
root: 'apps/app1',
projectType: 'application',
sourceRoot: 'apps/app1/src',
targets: {},
});
tree.write('apps/app1/eslint.config.mjs', 'export default [];');
await migration(tree);
expect(tree.read('eslint.config.mjs', 'utf-8')).toMatchInlineSnapshot(`
"export default [
{
ignores: ['**/vite.config.*.timestamp*', '**/vitest.config.*.timestamp*'],
},
];
"
`);
expect(tree.read('apps/app1/eslint.config.mjs', 'utf-8'))
.toMatchInlineSnapshot(`
"export default [];
"
`);
});
it('should update the project eslint config when using eslintrc config and it is using vite', async () => {
tree.write(
'.eslintrc.json',
`{
"ignorePatterns": ["**/*"]
}
`
);
addProjectConfiguration(tree, 'app1', {
root: 'apps/app1',
projectType: 'application',
sourceRoot: 'apps/app1/src',
targets: {},
});
tree.write(
'apps/app1/.eslintrc.json',
`{
"ignorePatterns": ["!**/*"]
}
`
);
tree.write('apps/app1/vite.config.ts', 'export default {};');
addProjectConfiguration(tree, 'app2', {
root: 'apps/app2',
projectType: 'application',
sourceRoot: 'apps/app2/src',
targets: {},
});
tree.write(
'apps/app2/.eslintrc.json',
`{
"ignorePatterns": ["!**/*"]
}
`
);
tree.write('apps/app2/vitest.config.ts', 'export default {};');
// app not using vite, it should not be updated
addProjectConfiguration(tree, 'app3', {
root: 'apps/app3',
projectType: 'application',
sourceRoot: 'apps/app3/src',
targets: {},
});
tree.write(
'apps/app3/.eslintrc.json',
`{
"ignorePatterns": ["!**/*"]
}
`
);
await migration(tree);
expect(tree.read('.eslintrc.json', 'utf-8')).toMatchInlineSnapshot(`
"{
"ignorePatterns": ["**/*"]
}
"
`);
expect(tree.read('apps/app1/.eslintrc.json', 'utf-8'))
.toMatchInlineSnapshot(`
"{
"ignorePatterns": [
"!**/*",
"**/vite.config.*.timestamp*",
"**/vitest.config.*.timestamp*"
]
}
"
`);
expect(tree.read('apps/app2/.eslintrc.json', 'utf-8'))
.toMatchInlineSnapshot(`
"{
"ignorePatterns": [
"!**/*",
"**/vite.config.*.timestamp*",
"**/vitest.config.*.timestamp*"
]
}
"
`);
expect(tree.read('apps/app3/.eslintrc.json', 'utf-8'))
.toMatchInlineSnapshot(`
"{
"ignorePatterns": ["!**/*"]
}
"
`);
});
});

View File

@ -0,0 +1,54 @@
import {
ensurePackage,
formatFiles,
getProjects,
globAsync,
type Tree,
} from '@nx/devkit';
import { isEslintInstalled } from '../../utils/ignore-vite-temp-files';
import { nxVersion } from '../../utils/versions';
export default async function (tree: Tree) {
if (!isEslintInstalled(tree)) {
return;
}
ensurePackage('@nx/eslint', nxVersion);
const { addIgnoresToLintConfig, isEslintConfigSupported } = await import(
'@nx/eslint/src/generators/utils/eslint-file'
);
if (!isEslintConfigSupported(tree)) {
return;
}
const { useFlatConfig } = await import('@nx/eslint/src/utils/flat-config');
const isUsingFlatConfig = useFlatConfig(tree);
if (isUsingFlatConfig) {
// using flat config, so we update the root eslint config
addIgnoresToLintConfig(tree, '', [
'**/vite.config.*.timestamp*',
'**/vitest.config.*.timestamp*',
]);
} else {
// not using flat config, so we update each project's eslint config
const projects = getProjects(tree);
for (const [, { root: projectRoot }] of projects) {
const viteConfigFiles = await globAsync(tree, [
`${projectRoot}/**/{vite,vitest}.config.{js,ts,mjs,mts,cjs,cts}`,
]);
if (!viteConfigFiles.length) {
// the project doesn't use vite or vitest, so we skip it
continue;
}
addIgnoresToLintConfig(tree, projectRoot, [
'**/vite.config.*.timestamp*',
'**/vitest.config.*.timestamp*',
]);
}
}
await formatFiles(tree);
}

View File

@ -1,29 +0,0 @@
import { stripIndents, Tree } from '@nx/devkit';
export function addViteTempFilesToGitIgnore(tree: Tree) {
let newGitIgnoreContents = `vite.config.*.timestamp*`;
if (tree.exists('.gitignore')) {
const gitIgnoreContents = tree.read('.gitignore', 'utf-8');
if (!gitIgnoreContents.includes(newGitIgnoreContents)) {
newGitIgnoreContents = stripIndents`${gitIgnoreContents}
${newGitIgnoreContents}`;
tree.write('.gitignore', newGitIgnoreContents);
}
} else {
tree.write('.gitignore', newGitIgnoreContents);
}
newGitIgnoreContents = `vitest.config.*.timestamp*`;
if (tree.exists('.gitignore')) {
const gitIgnoreContents = tree.read('.gitignore', 'utf-8');
if (!gitIgnoreContents.includes(newGitIgnoreContents)) {
newGitIgnoreContents = stripIndents`${gitIgnoreContents}
${newGitIgnoreContents}`;
tree.write('.gitignore', newGitIgnoreContents);
}
} else {
tree.write('.gitignore', newGitIgnoreContents);
}
}

View File

@ -0,0 +1,75 @@
import { ensurePackage, readJson, stripIndents, type Tree } from '@nx/devkit';
import { nxVersion } from './versions';
export async function ignoreViteTempFiles(
tree: Tree,
projectRoot?: string | undefined
): Promise<void> {
addViteTempFilesToGitIgnore(tree);
await ignoreViteTempFilesInEslintConfig(tree, projectRoot);
}
export function addViteTempFilesToGitIgnore(tree: Tree): void {
let gitIgnoreContents = tree.exists('.gitignore')
? tree.read('.gitignore', 'utf-8')
: '';
if (!/^vite\.config\.\*\.timestamp\*$/m.test(gitIgnoreContents)) {
gitIgnoreContents = stripIndents`${gitIgnoreContents}
vite.config.*.timestamp*`;
}
if (!/^vitest\.config\.\*\.timestamp\*$/m.test(gitIgnoreContents)) {
gitIgnoreContents = stripIndents`${gitIgnoreContents}
vitest.config.*.timestamp*`;
}
tree.write('.gitignore', gitIgnoreContents);
}
async function ignoreViteTempFilesInEslintConfig(
tree: Tree,
projectRoot: string | undefined
): Promise<void> {
if (!isEslintInstalled(tree)) {
return;
}
ensurePackage('@nx/eslint', nxVersion);
const { addIgnoresToLintConfig, isEslintConfigSupported } = await import(
'@nx/eslint/src/generators/utils/eslint-file'
);
if (!isEslintConfigSupported(tree)) {
return;
}
const { useFlatConfig } = await import('@nx/eslint/src/utils/flat-config');
const isUsingFlatConfig = useFlatConfig(tree);
if (!projectRoot && !isUsingFlatConfig) {
// root eslintrc files ignore all files and the root eslintrc files add
// back all the project files, so we only add the ignores to the project
// eslintrc files
return;
}
// for flat config, we update the root config file
const directory = isUsingFlatConfig ? '' : projectRoot ?? '';
addIgnoresToLintConfig(tree, directory, [
'**/vite.config.*.timestamp*',
'**/vitest.config.*.timestamp*',
]);
}
export function isEslintInstalled(tree: Tree): boolean {
try {
require('eslint');
return true;
} catch {}
// it might not be installed yet, but it might be in the tree pending install
const { devDependencies, dependencies } = tree.exists('package.json')
? readJson(tree, 'package.json')
: {};
return !!devDependencies?.['eslint'] || !!dependencies?.['eslint'];
}

View File

@ -94,7 +94,11 @@ exports[`application generator should set up project correctly for cypress 3`] =
"@vue/eslint-config-prettier/skip-formatting",
"../.eslintrc.json"
],
"ignorePatterns": ["!**/*"],
"ignorePatterns": [
"!**/*",
"**/vite.config.*.timestamp*",
"**/vitest.config.*.timestamp*"
],
"overrides": [
{
"files": ["*.ts", "*.tsx", "*.js", "*.jsx", "*.vue"],
@ -300,7 +304,11 @@ exports[`application generator should set up project correctly with given option
"@vue/eslint-config-prettier/skip-formatting",
"../.eslintrc.json"
],
"ignorePatterns": ["!**/*"],
"ignorePatterns": [
"!**/*",
"**/vite.config.*.timestamp*",
"**/vitest.config.*.timestamp*"
],
"overrides": [
{
"files": ["*.ts", "*.tsx", "*.js", "*.jsx", "*.vue"],

View File

@ -240,6 +240,8 @@ exports[`library should generate files 1`] = `
],
"ignorePatterns": [
"!**/*",
"**/vite.config.*.timestamp*",
"**/vitest.config.*.timestamp*",
],
"overrides": [
{

View File

@ -317,6 +317,25 @@ export async function applicationGeneratorInternal(host: Tree, schema: Schema) {
createApplicationFiles(host, options);
if (options.linter === 'eslint') {
const { lintProjectGenerator } = ensurePackage<typeof import('@nx/eslint')>(
'@nx/eslint',
nxVersion
);
const lintTask = await lintProjectGenerator(host, {
linter: options.linter,
project: options.projectName,
tsConfigPaths: [
joinPathFragments(options.appProjectRoot, 'tsconfig.app.json'),
],
unitTestRunner: options.unitTestRunner,
skipFormat: true,
setParserOptionsProject: options.setParserOptionsProject,
addPlugin: options.addPlugin,
});
tasks.push(lintTask);
}
if (options.bundler === 'vite') {
const { viteConfigurationGenerator, createOrEditViteConfig } =
ensurePackage<typeof import('@nx/vite')>('@nx/vite', nxVersion);
@ -387,25 +406,6 @@ export async function applicationGeneratorInternal(host: Tree, schema: Schema) {
);
}
if (options.linter === 'eslint') {
const { lintProjectGenerator } = ensurePackage<typeof import('@nx/eslint')>(
'@nx/eslint',
nxVersion
);
const lintTask = await lintProjectGenerator(host, {
linter: options.linter,
project: options.projectName,
tsConfigPaths: [
joinPathFragments(options.appProjectRoot, 'tsconfig.app.json'),
],
unitTestRunner: options.unitTestRunner,
skipFormat: true,
setParserOptionsProject: options.setParserOptionsProject,
addPlugin: options.addPlugin,
});
tasks.push(lintTask);
}
const nxJson = readNxJson(host);
let hasPlugin: PluginConfiguration | undefined;
let buildPlugin: string;