diff --git a/docs/generated/manifests/nx-api.json b/docs/generated/manifests/nx-api.json index 9d2dae924e..16b4bdcbf6 100644 --- a/docs/generated/manifests/nx-api.json +++ b/docs/generated/manifests/nx-api.json @@ -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", diff --git a/docs/generated/packages-metadata.json b/docs/generated/packages-metadata.json index 36ac1e1dbe..9bd66549ba 100644 --- a/docs/generated/packages-metadata.json +++ b/docs/generated/packages-metadata.json @@ -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", diff --git a/docs/generated/packages/vite/migrations/eslint-ignore-vite-temp-files.json b/docs/generated/packages/vite/migrations/eslint-ignore-vite-temp-files.json new file mode 100644 index 0000000000..5e93c0fdb4 --- /dev/null +++ b/docs/generated/packages/vite/migrations/eslint-ignore-vite-temp-files.json @@ -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" +} diff --git a/packages/eslint/src/generators/utils/eslint-file.spec.ts b/packages/eslint/src/generators/utils/eslint-file.spec.ts index dce747af9d..4e147af1fa 100644 --- a/packages/eslint/src/generators/utils/eslint-file.spec.ts +++ b/packages/eslint/src/generators/utils/eslint-file.spec.ts @@ -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/**/*", + ], + } + `); + }); + }); }); diff --git a/packages/eslint/src/generators/utils/eslint-file.ts b/packages/eslint/src/generators/utils/eslint-file.ts index 6a458695dd..1cee49ac35 100644 --- a/packages/eslint/src/generators/utils/eslint-file.ts +++ b/packages/eslint/src/generators/utils/eslint-file.ts @@ -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({ - 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({ + 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 ?? []), diff --git a/packages/eslint/src/generators/utils/flat-config/ast-utils.ts b/packages/eslint/src/generators/utils/flat-config/ast-utils.ts index 146f637fd0..743578e4cd 100644 --- a/packages/eslint/src/generators/utils/flat-config/ast-utils.ts +++ b/packages/eslint/src/generators/utils/flat-config/ast-utils.ts @@ -75,6 +75,84 @@ function findModuleExports(source: ts.SourceFile): ts.NodeArray { }); } +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) && diff --git a/packages/nuxt/src/generators/application/__snapshots__/application.spec.ts.snap b/packages/nuxt/src/generators/application/__snapshots__/application.spec.ts.snap index 629200e500..8997954f20 100644 --- a/packages/nuxt/src/generators/application/__snapshots__/application.spec.ts.snap +++ b/packages/nuxt/src/generators/application/__snapshots__/application.spec.ts.snap @@ -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"], diff --git a/packages/react/src/generators/application/application.ts b/packages/react/src/generators/application/application.ts index 363d624dc8..8e86a46429 100644 --- a/packages/react/src/generators/application/application.ts +++ b/packages/react/src/generators/application/application.ts @@ -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); diff --git a/packages/remix/src/generators/application/__snapshots__/application.impl.spec.ts.snap b/packages/remix/src/generators/application/__snapshots__/application.impl.spec.ts.snap index d69fd03e13..5dda4330e7 100644 --- a/packages/remix/src/generators/application/__snapshots__/application.impl.spec.ts.snap +++ b/packages/remix/src/generators/application/__snapshots__/application.impl.spec.ts.snap @@ -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": [ { diff --git a/packages/remix/src/generators/application/application.impl.spec.ts b/packages/remix/src/generators/application/application.impl.spec.ts index 7f8fff6001..309c8cfd53 100644 --- a/packages/remix/src/generators/application/application.impl.spec.ts +++ b/packages/remix/src/generators/application/application.impl.spec.ts @@ -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 diff --git a/packages/remix/src/generators/application/application.impl.ts b/packages/remix/src/generators/application/application.impl.ts index 97cc6bd785..b1613d716b 100644 --- a/packages/remix/src/generators/application/application.impl.ts +++ b/packages/remix/src/generators/application/application.impl.ts @@ -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, diff --git a/packages/remix/src/generators/application/lib/add-vite-temp-files-to-gitignore.ts b/packages/remix/src/generators/application/lib/add-vite-temp-files-to-gitignore.ts deleted file mode 100644 index e3d603042f..0000000000 --- a/packages/remix/src/generators/application/lib/add-vite-temp-files-to-gitignore.ts +++ /dev/null @@ -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); - } -} diff --git a/packages/remix/src/generators/application/lib/ignore-vite-temp-files.ts b/packages/remix/src/generators/application/lib/ignore-vite-temp-files.ts new file mode 100644 index 0000000000..767a380efa --- /dev/null +++ b/packages/remix/src/generators/application/lib/ignore-vite-temp-files.ts @@ -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 { + 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 { + 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']; +} diff --git a/packages/remix/src/generators/application/lib/index.ts b/packages/remix/src/generators/application/lib/index.ts index b1769741cc..e2a4be9bd1 100644 --- a/packages/remix/src/generators/application/lib/index.ts +++ b/packages/remix/src/generators/application/lib/index.ts @@ -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'; diff --git a/packages/vite/.eslintrc.json b/packages/vite/.eslintrc.json index 4a5d6fc8e2..c0c5d826ba 100644 --- a/packages/vite/.eslintrc.json +++ b/packages/vite/.eslintrc.json @@ -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" + ] } ] } diff --git a/packages/vite/migrations.json b/packages/vite/migrations.json index ca85137f0a..4b1b5cd104 100644 --- a/packages/vite/migrations.json +++ b/packages/vite/migrations.json @@ -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": { diff --git a/packages/vite/src/generators/configuration/configuration.ts b/packages/vite/src/generators/configuration/configuration.ts index 01e558c35b..45eb53e9e5 100644 --- a/packages/vite/src/generators/configuration/configuration.ts +++ b/packages/vite/src/generators/configuration/configuration.ts @@ -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)); diff --git a/packages/vite/src/generators/init/init.spec.ts b/packages/vite/src/generators/init/init.spec.ts index 654aef8590..0c0c4ab36e 100644 --- a/packages/vite/src/generators/init/init.spec.ts +++ b/packages/vite/src/generators/init/init.spec.ts @@ -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*", + ], + } `); }); }); diff --git a/packages/vite/src/generators/init/init.ts b/packages/vite/src/generators/init/init.ts index 35566d0853..720ccfe5e7 100644 --- a/packages/vite/src/generators/init/init.ts +++ b/packages/vite/src/generators/init/init.ts @@ -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 }); diff --git a/packages/vite/src/generators/init/schema.d.ts b/packages/vite/src/generators/init/schema.d.ts index 3c52a303c5..c1e666432b 100644 --- a/packages/vite/src/generators/init/schema.d.ts +++ b/packages/vite/src/generators/init/schema.d.ts @@ -7,4 +7,5 @@ export interface InitGeneratorSchema { addPlugin?: boolean; vitestOnly?: boolean; useViteV5?: boolean; + projectRoot?: string; } diff --git a/packages/vite/src/generators/vitest/vitest-generator.ts b/packages/vite/src/generators/vitest/vitest-generator.ts index 069d55611d..a2f773d29f 100644 --- a/packages/vite/src/generators/vitest/vitest-generator.ts +++ b/packages/vite/src/generators/vitest/vitest-generator.ts @@ -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, diff --git a/packages/vite/src/migrations/update-20-0-4/add-vite-temp-files-to-git-ignore.ts b/packages/vite/src/migrations/update-20-0-4/add-vite-temp-files-to-git-ignore.ts index 205c83d11a..129108c405 100644 --- a/packages/vite/src/migrations/update-20-0-4/add-vite-temp-files-to-git-ignore.ts +++ b/packages/vite/src/migrations/update-20-0-4/add-vite-temp-files-to-git-ignore.ts @@ -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 diff --git a/packages/vite/src/migrations/update-20-3-0/add-vitest-temp-files-to-git-ignore.ts b/packages/vite/src/migrations/update-20-3-0/add-vitest-temp-files-to-git-ignore.ts index 0a6d6def75..09790ce453 100644 --- a/packages/vite/src/migrations/update-20-3-0/add-vitest-temp-files-to-git-ignore.ts +++ b/packages/vite/src/migrations/update-20-3-0/add-vitest-temp-files-to-git-ignore.ts @@ -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 diff --git a/packages/vite/src/migrations/update-20-5-0/eslint-ignore-vite-temp-files.md b/packages/vite/src/migrations/update-20-5-0/eslint-ignore-vite-temp-files.md new file mode 100644 index 0000000000..dbbfeeb285 --- /dev/null +++ b/packages/vite/src/migrations/update-20-5-0/eslint-ignore-vite-temp-files.md @@ -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 %} diff --git a/packages/vite/src/migrations/update-20-5-0/eslint-ignore-vite-temp-files.spec.ts b/packages/vite/src/migrations/update-20-5-0/eslint-ignore-vite-temp-files.spec.ts new file mode 100644 index 0000000000..d5b6ec8e92 --- /dev/null +++ b/packages/vite/src/migrations/update-20-5-0/eslint-ignore-vite-temp-files.spec.ts @@ -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": ["!**/*"] + } + " + `); + }); +}); diff --git a/packages/vite/src/migrations/update-20-5-0/eslint-ignore-vite-temp-files.ts b/packages/vite/src/migrations/update-20-5-0/eslint-ignore-vite-temp-files.ts new file mode 100644 index 0000000000..76a55214d1 --- /dev/null +++ b/packages/vite/src/migrations/update-20-5-0/eslint-ignore-vite-temp-files.ts @@ -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); +} diff --git a/packages/vite/src/utils/add-vite-temp-files-to-gitignore.ts b/packages/vite/src/utils/add-vite-temp-files-to-gitignore.ts deleted file mode 100644 index fd49446f45..0000000000 --- a/packages/vite/src/utils/add-vite-temp-files-to-gitignore.ts +++ /dev/null @@ -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); - } -} diff --git a/packages/vite/src/utils/ignore-vite-temp-files.ts b/packages/vite/src/utils/ignore-vite-temp-files.ts new file mode 100644 index 0000000000..c9289932b2 --- /dev/null +++ b/packages/vite/src/utils/ignore-vite-temp-files.ts @@ -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 { + 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 { + 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']; +} diff --git a/packages/vue/src/generators/application/__snapshots__/application.spec.ts.snap b/packages/vue/src/generators/application/__snapshots__/application.spec.ts.snap index 0ab2b213c6..2cc7cc0d5c 100644 --- a/packages/vue/src/generators/application/__snapshots__/application.spec.ts.snap +++ b/packages/vue/src/generators/application/__snapshots__/application.spec.ts.snap @@ -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"], diff --git a/packages/vue/src/generators/library/__snapshots__/library.spec.ts.snap b/packages/vue/src/generators/library/__snapshots__/library.spec.ts.snap index 2a49000925..d279032ad3 100644 --- a/packages/vue/src/generators/library/__snapshots__/library.spec.ts.snap +++ b/packages/vue/src/generators/library/__snapshots__/library.spec.ts.snap @@ -240,6 +240,8 @@ exports[`library should generate files 1`] = ` ], "ignorePatterns": [ "!**/*", + "**/vite.config.*.timestamp*", + "**/vitest.config.*.timestamp*", ], "overrides": [ { diff --git a/packages/web/src/generators/application/application.ts b/packages/web/src/generators/application/application.ts index 6fae37f034..65351b9853 100644 --- a/packages/web/src/generators/application/application.ts +++ b/packages/web/src/generators/application/application.ts @@ -317,6 +317,25 @@ export async function applicationGeneratorInternal(host: Tree, schema: Schema) { createApplicationFiles(host, options); + if (options.linter === 'eslint') { + const { lintProjectGenerator } = ensurePackage( + '@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('@nx/vite', nxVersion); @@ -387,25 +406,6 @@ export async function applicationGeneratorInternal(host: Tree, schema: Schema) { ); } - if (options.linter === 'eslint') { - const { lintProjectGenerator } = ensurePackage( - '@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;