From 72cd1c15e6e0a8037888eb86ca620cb30c157613 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leosvel=20P=C3=A9rez=20Espinosa?= Date: Tue, 24 Sep 2024 15:24:09 +0200 Subject: [PATCH] feat(js): add the setup-prettier generator (#27996) ## Current Behavior ## Expected Behavior ## Related Issue(s) Fixes # --- docs/generated/manifests/menus.json | 8 ++ docs/generated/manifests/nx-api.json | 9 ++ docs/generated/packages-metadata.json | 9 ++ .../js/generators/setup-prettier.json | 33 +++++++ docs/shared/reference/sitemap.md | 1 + packages/js/generators.json | 5 ++ packages/js/src/generators/init/init.ts | 72 +++------------- .../setup-prettier/generator.spec.ts | 86 +++++++++++++++++++ .../generators/setup-prettier/generator.ts | 31 +++++++ .../src/generators/setup-prettier/schema.d.ts | 4 + .../src/generators/setup-prettier/schema.json | 22 +++++ packages/js/src/index.ts | 1 + packages/js/src/utils/prettier.ts | 72 ++++++++++++++-- .../bin/index.ts__tmpl__ | 15 ++-- 14 files changed, 298 insertions(+), 70 deletions(-) create mode 100644 docs/generated/packages/js/generators/setup-prettier.json create mode 100644 packages/js/src/generators/setup-prettier/generator.spec.ts create mode 100644 packages/js/src/generators/setup-prettier/generator.ts create mode 100644 packages/js/src/generators/setup-prettier/schema.d.ts create mode 100644 packages/js/src/generators/setup-prettier/schema.json diff --git a/docs/generated/manifests/menus.json b/docs/generated/manifests/menus.json index 6209a08abe..ee5a84d3fd 100644 --- a/docs/generated/manifests/menus.json +++ b/docs/generated/manifests/menus.json @@ -8185,6 +8185,14 @@ "children": [], "isExternal": false, "disableCollapsible": false + }, + { + "id": "setup-prettier", + "path": "/nx-api/js/generators/setup-prettier", + "name": "setup-prettier", + "children": [], + "isExternal": false, + "disableCollapsible": false } ], "isExternal": false, diff --git a/docs/generated/manifests/nx-api.json b/docs/generated/manifests/nx-api.json index ef366a0354..42ff96d6bd 100644 --- a/docs/generated/manifests/nx-api.json +++ b/docs/generated/manifests/nx-api.json @@ -1291,6 +1291,15 @@ "originalFilePath": "/packages/js/src/generators/typescript-sync/schema.json", "path": "/nx-api/js/generators/typescript-sync", "type": "generator" + }, + "/nx-api/js/generators/setup-prettier": { + "description": "Setup Prettier as the formatting tool.", + "file": "generated/packages/js/generators/setup-prettier.json", + "hidden": false, + "name": "setup-prettier", + "originalFilePath": "/packages/js/src/generators/setup-prettier/schema.json", + "path": "/nx-api/js/generators/setup-prettier", + "type": "generator" } }, "path": "/nx-api/js" diff --git a/docs/generated/packages-metadata.json b/docs/generated/packages-metadata.json index 06f3052829..3899cd9bbd 100644 --- a/docs/generated/packages-metadata.json +++ b/docs/generated/packages-metadata.json @@ -1273,6 +1273,15 @@ "originalFilePath": "/packages/js/src/generators/typescript-sync/schema.json", "path": "js/generators/typescript-sync", "type": "generator" + }, + { + "description": "Setup Prettier as the formatting tool.", + "file": "generated/packages/js/generators/setup-prettier.json", + "hidden": false, + "name": "setup-prettier", + "originalFilePath": "/packages/js/src/generators/setup-prettier/schema.json", + "path": "js/generators/setup-prettier", + "type": "generator" } ], "githubRoot": "https://github.com/nrwl/nx/blob/master", diff --git a/docs/generated/packages/js/generators/setup-prettier.json b/docs/generated/packages/js/generators/setup-prettier.json new file mode 100644 index 0000000000..c15a465946 --- /dev/null +++ b/docs/generated/packages/js/generators/setup-prettier.json @@ -0,0 +1,33 @@ +{ + "name": "setup-prettier", + "factory": "./src/generators/setup-prettier/generator", + "schema": { + "$schema": "https://json-schema.org/schema", + "$id": "NxJsSetupPrettier", + "title": "Setup Prettier", + "description": "Setup Prettier as the formatting tool.", + "type": "object", + "properties": { + "skipFormat": { + "description": "Skip formatting files.", + "type": "boolean", + "default": false, + "x-priority": "internal" + }, + "skipPackageJson": { + "description": "Do not add dependencies to `package.json`.", + "type": "boolean", + "default": false, + "x-priority": "internal" + } + }, + "required": [], + "presets": [] + }, + "description": "Setup Prettier as the formatting tool.", + "implementation": "/packages/js/src/generators/setup-prettier/generator.ts", + "aliases": [], + "hidden": false, + "path": "/packages/js/src/generators/setup-prettier/schema.json", + "type": "generator" +} diff --git a/docs/shared/reference/sitemap.md b/docs/shared/reference/sitemap.md index 2059beb0ec..e2820b7cc1 100644 --- a/docs/shared/reference/sitemap.md +++ b/docs/shared/reference/sitemap.md @@ -487,6 +487,7 @@ - [setup-verdaccio](/nx-api/js/generators/setup-verdaccio) - [setup-build](/nx-api/js/generators/setup-build) - [typescript-sync](/nx-api/js/generators/typescript-sync) + - [setup-prettier](/nx-api/js/generators/setup-prettier) - [nest](/nx-api/nest) - [documents](/nx-api/nest/documents) - [Overview](/nx-api/nest/documents/overview) diff --git a/packages/js/generators.json b/packages/js/generators.json index badcbfd554..b4068838ec 100644 --- a/packages/js/generators.json +++ b/packages/js/generators.json @@ -48,6 +48,11 @@ "description": "Synchronize TypeScript project references based on the project graph", "alias": ["sync"], "hidden": true + }, + "setup-prettier": { + "factory": "./src/generators/setup-prettier/generator", + "schema": "./src/generators/setup-prettier/schema.json", + "description": "Setup Prettier as the formatting tool." } } } diff --git a/packages/js/src/generators/init/init.ts b/packages/js/src/generators/init/init.ts index dca25dac34..17a89b7f60 100644 --- a/packages/js/src/generators/init/init.ts +++ b/packages/js/src/generators/init/init.ts @@ -5,14 +5,14 @@ import { generateFiles, GeneratorCallback, readJson, - stripIndents, + runTasksInSerial, Tree, - updateJson, - writeJson, } from '@nx/devkit'; import { checkAndCleanWithSemver } from '@nx/devkit/src/utils/semver'; import { readModulePackageJson } from 'nx/src/utils/package-json'; +import { join } from 'path'; import { satisfies, valid } from 'semver'; +import { generatePrettierSetup } from '../../utils/prettier'; import { getRootTsConfigFileName } from '../../utils/typescript/ts-config'; import { nxVersion, @@ -24,7 +24,6 @@ import { typescriptVersion, } from '../../utils/versions'; import { InitSchema } from './schema'; -import { join } from 'path'; async function getInstalledTypescriptVersion( tree: Tree @@ -105,53 +104,10 @@ export async function initGeneratorInternal( } if (schema.setUpPrettier) { - devDependencies['prettier'] = prettierVersion; - - // https://prettier.io/docs/en/configuration.html - const prettierrcNameOptions = [ - '.prettierrc', - '.prettierrc.json', - '.prettierrc.yml', - '.prettierrc.yaml', - '.prettierrc.json5', - '.prettierrc.js', - '.prettierrc.cjs', - '.prettierrc.mjs', - '.prettierrc.toml', - 'prettier.config.js', - 'prettier.config.cjs', - 'prettier.config.mjs', - ]; - - if (prettierrcNameOptions.every((name) => !tree.exists(name))) { - writeJson(tree, '.prettierrc', { - singleQuote: true, - }); - } - - if (!tree.exists(`.prettierignore`)) { - tree.write( - '.prettierignore', - stripIndents` - # Add files here to ignore them from prettier formatting - /dist - /coverage - /.nx/cache - /.nx/workspace-data - ` - ); - } - } - - if (tree.exists('.vscode/extensions.json')) { - updateJson(tree, '.vscode/extensions.json', (json) => { - json.recommendations ??= []; - const extension = 'esbenp.prettier-vscode'; - if (!json.recommendations.includes(extension)) { - json.recommendations.push(extension); - } - return json; + const prettierTask = generatePrettierSetup(tree, { + skipPackageJson: schema.skipPackageJson, }); + tasks.push(prettierTask); } const installTask = !schema.skipPackageJson @@ -165,16 +121,16 @@ export async function initGeneratorInternal( : () => {}; tasks.push(installTask); - if (schema.setUpPrettier) { - ensurePackage('prettier', prettierVersion); - if (!schema.skipFormat) await formatFiles(tree); + if (!schema.skipFormat) { + if (!schema.skipPackageJson) { + ensurePackage('prettier', prettierVersion); + } + // even if skipPackageJson === true, we can safely run formatFiles, prettier might + // have been installed earlier and if not, the formatFiles function still handles it + await formatFiles(tree); } - return async () => { - for (const task of tasks) { - await task(); - } - }; + return runTasksInSerial(...tasks); } export default initGenerator; diff --git a/packages/js/src/generators/setup-prettier/generator.spec.ts b/packages/js/src/generators/setup-prettier/generator.spec.ts new file mode 100644 index 0000000000..fdc6f4f2dc --- /dev/null +++ b/packages/js/src/generators/setup-prettier/generator.spec.ts @@ -0,0 +1,86 @@ +import { readJson, writeJson, type Tree } from '@nx/devkit'; +import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing'; +import { prettierVersion } from '../../utils/versions'; +import { setupPrettierGenerator } from './generator'; + +describe('setup-prettier generator', () => { + let tree: Tree; + + beforeEach(() => { + tree = createTreeWithEmptyWorkspace(); + // remove the default generated .prettierrc file + tree.delete('.prettierrc'); + }); + + it('should install prettier package', async () => { + await setupPrettierGenerator(tree, { skipFormat: true }); + + const packageJson = readJson(tree, 'package.json'); + expect(packageJson.devDependencies['prettier']).toBe(prettierVersion); + }); + + it('should create .prettierrc and .prettierignore files', async () => { + await setupPrettierGenerator(tree, { skipFormat: true }); + + const prettierrc = readJson(tree, '.prettierrc'); + expect(prettierrc).toEqual({ singleQuote: true }); + const prettierignore = tree.read('.prettierignore', 'utf-8'); + expect(prettierignore).toMatch(/\n\/coverage/); + expect(prettierignore).toMatch(/\n\/dist/); + expect(prettierignore).toMatch(/\n\/\.nx\/cache/); + }); + + it('should not overwrite existing .prettierrc and .prettierignore files', async () => { + writeJson(tree, '.prettierrc', { singleQuote: false }); + tree.write('.prettierignore', `# custom ignore file`); + + await setupPrettierGenerator(tree, { skipFormat: true }); + + const prettierrc = readJson(tree, '.prettierrc'); + expect(prettierrc).toEqual({ singleQuote: false }); + const prettierignore = tree.read('.prettierignore', 'utf-8'); + expect(prettierignore).toContain('# custom ignore file'); + }); + + it('should not overwrite prettier configuration specified in other formats', async () => { + tree.delete('.prettierrc'); + tree.delete('.prettierignore'); + tree.write('.prettierrc.js', `module.exports = { singleQuote: true };`); + + await setupPrettierGenerator(tree, { skipFormat: true }); + + expect(tree.exists('.prettierrc')).toBeFalsy(); + expect(tree.exists('.prettierignore')).toBeTruthy(); + expect(tree.read('.prettierrc.js', 'utf-8')).toContain( + `module.exports = { singleQuote: true };` + ); + }); + + it('should add prettier vscode extension if .vscode/extensions.json file exists', async () => { + // No existing recommendations + writeJson(tree, '.vscode/extensions.json', {}); + + await setupPrettierGenerator(tree, { skipFormat: true }); + + let json = readJson(tree, '.vscode/extensions.json'); + expect(json).toEqual({ + recommendations: ['esbenp.prettier-vscode'], + }); + + // Existing recommendations + writeJson(tree, '.vscode/extensions.json', { recommendations: ['foo'] }); + + await setupPrettierGenerator(tree, { skipFormat: true }); + + json = readJson(tree, '.vscode/extensions.json'); + expect(json).toEqual({ + recommendations: ['foo', 'esbenp.prettier-vscode'], + }); + }); + + it('should skip adding prettier extension if .vscode/extensions.json file does not exist', async () => { + await setupPrettierGenerator(tree, { skipFormat: true }); + + expect(tree.exists('.vscode/extensions.json')).toBeFalsy(); + }); +}); diff --git a/packages/js/src/generators/setup-prettier/generator.ts b/packages/js/src/generators/setup-prettier/generator.ts new file mode 100644 index 0000000000..f73b3faa96 --- /dev/null +++ b/packages/js/src/generators/setup-prettier/generator.ts @@ -0,0 +1,31 @@ +import { + ensurePackage, + formatFiles, + type GeneratorCallback, + type Tree, +} from '@nx/devkit'; +import { generatePrettierSetup } from '../../utils/prettier'; +import { prettierVersion } from '../../utils/versions'; +import type { GeneratorOptions } from './schema'; + +export async function setupPrettierGenerator( + tree: Tree, + options: GeneratorOptions +): Promise { + const prettierTask = generatePrettierSetup(tree, { + skipPackageJson: options.skipPackageJson, + }); + + if (!options.skipFormat) { + if (!options.skipPackageJson) { + ensurePackage('prettier', prettierVersion); + } + // even if skipPackageJson === true, we can safely run formatFiles, prettier might + // have been installed earlier and if not, the formatFiles function still handles it + await formatFiles(tree); + } + + return prettierTask; +} + +export default setupPrettierGenerator; diff --git a/packages/js/src/generators/setup-prettier/schema.d.ts b/packages/js/src/generators/setup-prettier/schema.d.ts new file mode 100644 index 0000000000..d434ae47cc --- /dev/null +++ b/packages/js/src/generators/setup-prettier/schema.d.ts @@ -0,0 +1,4 @@ +export interface GeneratorOptions { + skipFormat?: boolean; + skipPackageJson?: boolean; +} diff --git a/packages/js/src/generators/setup-prettier/schema.json b/packages/js/src/generators/setup-prettier/schema.json new file mode 100644 index 0000000000..ac108712c0 --- /dev/null +++ b/packages/js/src/generators/setup-prettier/schema.json @@ -0,0 +1,22 @@ +{ + "$schema": "https://json-schema.org/schema", + "$id": "NxJsSetupPrettier", + "title": "Setup Prettier", + "description": "Setup Prettier as the formatting tool.", + "type": "object", + "properties": { + "skipFormat": { + "description": "Skip formatting files.", + "type": "boolean", + "default": false, + "x-priority": "internal" + }, + "skipPackageJson": { + "description": "Do not add dependencies to `package.json`.", + "type": "boolean", + "default": false, + "x-priority": "internal" + } + }, + "required": [] +} diff --git a/packages/js/src/index.ts b/packages/js/src/index.ts index 795b048973..8e0a45c750 100644 --- a/packages/js/src/index.ts +++ b/packages/js/src/index.ts @@ -13,6 +13,7 @@ export * from './utils/package-json/update-package-json'; export * from './utils/package-json/create-entry-points'; export { libraryGenerator } from './generators/library/library'; export { initGenerator } from './generators/init/init'; +export { setupPrettierGenerator } from './generators/setup-prettier/generator'; export { setupVerdaccio } from './generators/setup-verdaccio/generator'; export { isValidVariable } from './utils/is-valid-variable'; diff --git a/packages/js/src/utils/prettier.ts b/packages/js/src/utils/prettier.ts index 6332922d52..8b5531f7a2 100644 --- a/packages/js/src/utils/prettier.ts +++ b/packages/js/src/utils/prettier.ts @@ -1,9 +1,13 @@ +import { + addDependenciesToPackageJson, + stripIndents, + updateJson, + writeJson, + type GeneratorCallback, + type Tree, +} from '@nx/devkit'; import type { Options } from 'prettier'; - -let prettier: typeof import('prettier'); -try { - prettier = require('prettier'); -} catch {} +import { prettierVersion } from './versions'; export interface ExistingPrettierConfig { sourceFilepath: string; @@ -11,9 +15,13 @@ export interface ExistingPrettierConfig { } export async function resolveUserExistingPrettierConfig(): Promise { - if (!prettier) { + let prettier: typeof import('prettier'); + try { + prettier = require('prettier'); + } catch { return null; } + try { const filepath = await prettier.resolveConfigFile(); if (!filepath) { @@ -36,3 +44,55 @@ export async function resolveUserExistingPrettierConfig(): Promise !tree.exists(name))) { + writeJson(tree, '.prettierrc', { singleQuote: true }); + } + + if (!tree.exists('.prettierignore')) { + tree.write( + '.prettierignore', + stripIndents`# Add files here to ignore them from prettier formatting + /dist + /coverage + /.nx/cache + /.nx/workspace-data + ` + ); + } + + if (tree.exists('.vscode/extensions.json')) { + updateJson(tree, '.vscode/extensions.json', (json) => { + json.recommendations ??= []; + const extension = 'esbenp.prettier-vscode'; + if (!json.recommendations.includes(extension)) { + json.recommendations.push(extension); + } + return json; + }); + } + + return options.skipPackageJson + ? () => {} + : addDependenciesToPackageJson(tree, {}, { prettier: prettierVersion }); +} diff --git a/packages/plugin/src/generators/create-package/files/create-framework-package/bin/index.ts__tmpl__ b/packages/plugin/src/generators/create-package/files/create-framework-package/bin/index.ts__tmpl__ index 79d0b8fbf4..998ba0634b 100644 --- a/packages/plugin/src/generators/create-package/files/create-framework-package/bin/index.ts__tmpl__ +++ b/packages/plugin/src/generators/create-package/files/create-framework-package/bin/index.ts__tmpl__ @@ -12,14 +12,17 @@ async function main() { // This assumes "<%= preset %>" and "<%= projectName %>" are at the same version // eslint-disable-next-line @typescript-eslint/no-var-requires - const presetVersion = require('../package.json').version, + const presetVersion = require('../package.json').version; // TODO: update below to customize the workspace - const { directory } = await createWorkspace(`<%= preset %>@${presetVersion}`, { - name, - nxCloud: 'skip', - packageManager: 'npm', - }); + const { directory } = await createWorkspace( + `<%= preset %>@${presetVersion}`, + { + name, + nxCloud: 'skip', + packageManager: 'npm', + } + ); console.log(`Successfully created the workspace: ${directory}.`); }