feat(js): add the setup-prettier generator (#27996)

<!-- Please make sure you have read the submission guidelines before
posting an PR -->
<!--
https://github.com/nrwl/nx/blob/master/CONTRIBUTING.md#-submitting-a-pr
-->

<!-- Please make sure that your commit message follows our format -->
<!-- Example: `fix(nx): must begin with lowercase` -->

<!-- If this is a particularly complex change or feature addition, you
can request a dedicated Nx release for this pull request branch. Mention
someone from the Nx team or the `@nrwl/nx-pipelines-reviewers` and they
will confirm if the PR warrants its own release for testing purposes,
and generate it for you if appropriate. -->

## Current Behavior
<!-- This is the behavior we have today -->

## Expected Behavior
<!-- This is the behavior we should expect with the changes in this PR
-->

## Related Issue(s)
<!-- Please link the issue being fixed so it gets closed when this is
merged. -->

Fixes #
This commit is contained in:
Leosvel Pérez Espinosa 2024-09-24 15:24:09 +02:00 committed by GitHub
parent 3e1a87917f
commit 72cd1c15e6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
14 changed files with 298 additions and 70 deletions

View File

@ -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,

View File

@ -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"

View File

@ -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",

View File

@ -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"
}

View File

@ -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)

View File

@ -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."
}
}
}

View File

@ -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;

View File

@ -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();
});
});

View File

@ -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<GeneratorCallback> {
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;

View File

@ -0,0 +1,4 @@
export interface GeneratorOptions {
skipFormat?: boolean;
skipPackageJson?: boolean;
}

View File

@ -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": []
}

View File

@ -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';

View File

@ -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<ExistingPrettierConfig | null> {
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<ExistingPrett
return null;
}
}
export function generatePrettierSetup(
tree: Tree,
options: { skipPackageJson?: boolean }
): GeneratorCallback {
// 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;
});
}
return options.skipPackageJson
? () => {}
: addDependenciesToPackageJson(tree, {}, { prettier: prettierVersion });
}

View File

@ -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}.`);
}