Files
nrwl--nx/packages/devkit/src/generators/format-files.ts
T
2026-07-13 12:38:36 +08:00

155 lines
4.1 KiB
TypeScript

import { readJson, Tree, writeJson } from 'nx/src/devkit-exports';
import {
isUsingPrettierInTree,
sortObjectByKeys,
} from 'nx/src/devkit-internals';
import * as path from 'path';
import type * as Prettier from 'prettier';
// Prettier v3 (ESM) exposes its API as named exports; v2 (CJS) exposes it under
// `.default` when loaded via `import()`. Return whichever carries the API, or
// null if prettier isn't installed.
async function importPrettier(): Promise<typeof Prettier | null> {
try {
const imported = await import('prettier');
return (
(imported as any).resolveConfig ? imported : (imported as any).default
) as typeof Prettier;
} catch {
return null;
}
}
/**
* Formats all the created or updated files using Prettier
* @param tree - the file system tree
* @param options - options for the formatFiles function
*
* @remarks
* Set the environment variable `NX_SKIP_FORMAT` to `true` to skip Prettier
* formatting. This is useful for repositories that use alternative formatters
* like Biome, dprint, or have custom formatting requirements.
*
* Note: `NX_SKIP_FORMAT` only skips Prettier formatting. TSConfig path sorting
* (controlled by `sortRootTsconfigPaths` option or `NX_FORMAT_SORT_TSCONFIG_PATHS`)
* will still occur.
*/
export async function formatFiles(
tree: Tree,
options: {
sortRootTsconfigPaths?: boolean;
} = {}
): Promise<void> {
options.sortRootTsconfigPaths ??=
process.env.NX_FORMAT_SORT_TSCONFIG_PATHS === 'true';
if (options.sortRootTsconfigPaths) {
sortTsConfig(tree);
}
// Skip Prettier formatting if NX_SKIP_FORMAT is set
// This is checked after tsconfig sorting since sorting is a separate concern
if (process.env.NX_SKIP_FORMAT === 'true') {
return;
}
let prettier: typeof Prettier | null;
try {
prettier = await importPrettier();
/**
* Even after we discovered prettier in node_modules, we need to be sure that the user is intentionally using prettier
* before proceeding to format with it.
*/
if (!isUsingPrettierInTree(tree)) {
return;
}
} catch {}
if (!prettier) return;
const files = new Set(
tree.listChanges().filter((file) => file.type !== 'DELETE')
);
const changedPrettierInTree = getChangedPrettierConfigInTree(tree);
await Promise.all(
Array.from(files).map(async (file) => {
try {
const systemPath = path.join(tree.root, file.path);
const resolvedOptions = await prettier.resolveConfig(systemPath, {
editorconfig: true,
});
const options: Prettier.Options = {
...resolvedOptions,
...changedPrettierInTree,
filepath: systemPath,
};
if (file.path.endsWith('.swcrc')) {
options.parser = 'json';
}
const support = await prettier.getFileInfo(systemPath, options as any);
if (support.ignored || !support.inferredParser) {
return;
}
tree.write(
file.path,
await prettier.format(file.content.toString('utf-8'), options)
);
} catch (e) {
console.warn(`Could not format ${file.path}. Error: "${e.message}"`);
}
})
);
}
function sortTsConfig(tree: Tree) {
try {
const tsConfigPath = getRootTsConfigPath(tree);
if (!tsConfigPath) {
return;
}
const tsconfig = readJson(tree, tsConfigPath);
if (!tsconfig.compilerOptions?.paths) {
// no paths to sort
return;
}
writeJson(tree, tsConfigPath, {
...tsconfig,
compilerOptions: {
...tsconfig.compilerOptions,
paths: sortObjectByKeys(tsconfig.compilerOptions.paths),
},
});
} catch (e) {
// catch noop
}
}
function getRootTsConfigPath(tree: Tree): string | null {
for (const path of ['tsconfig.base.json', 'tsconfig.json']) {
if (tree.exists(path)) {
return path;
}
}
return null;
}
function getChangedPrettierConfigInTree(tree: Tree): Prettier.Options | null {
if (tree.listChanges().find((file) => file.path === '.prettierrc')) {
try {
return readJson(tree, '.prettierrc');
} catch {
return null;
}
} else {
return null;
}
}