feat(integrations, project): prettier
ADD
internal/core/worker/integrations/prettier.ts
+import IntegrationLoader from "@internal/core/common/IntegrationLoader";
+import Worker from "../Worker";
+import {FileReference, WorkerProject} from "@internal/core";
+import {Duration, DurationMeasurer} from "@internal/numbers";
+import {Diagnostic} from "@internal/diagnostics";
+
+const prettierLoader = new IntegrationLoader({
+ name: "prettier",
+ range: "^2.0.0",
+ normalize: (consumer) => {
+ return consumer;
+ },
+});
+
+export interface MaybeRunPrettier {
+ formatted: string;
+ timing: Duration;
+ diagnostics: Diagnostic[];
+}
+
+export async function maybeRunPrettier(
+ {ref, project, worker}: {
+ worker: Worker;
+ ref: FileReference;
+ project: WorkerProject;
+ },
+): Promise<undefined | MaybeRunPrettier> {
+ const options = project.config.integrations.prettier;
+ if (!options.enabled) {
+ return undefined;
+ }
+
+ const loader = await prettierLoader.load(
+ project.configPath,
+ project.directory,
+ );
+
+ const timer = new DurationMeasurer();
+
+ const content = await worker.readFileText(ref);
+
+ // NOTE: check if we need diagnostics
+ const diagnostics: Diagnostic[] = [];
+
+ const formatted = await prettierLoader.wrap(async () => {
+ const format = loader.module.get("format").asWrappedFunction();
+ return await format(
+ content,
+ {
+ ...options,
+ filepath: ref.real.join(),
+ },
+ ).asPromise();
+ });
+
+ return {
+ formatted: formatted.asString(),
+ timing: timer.since(),
+ diagnostics,
+ };
+}

MODIFY
internal/core/worker/workerLint.ts
internal/core/worker/workerLint.ts
@@ -24,6 +24,7 @@ import * as jsAnalysis from "@internal/js-analysis";
import Worker from "./Worker";
import {formatAST} from "@internal/formatter";
import {maybeRunESLint} from "./integrations/eslint";
+import {maybeRunPrettier} from "@internal/core/worker/integrations/prettier";
const EMPTY_LINT_RESULT: WorkerLintResult = {
timings: new Map(),
@@ -129,7 +130,9 @@ export async function uncachedLint(
}: ExtensionLintResult = res.value;
// If the file has pending fixes
- const needsSave = project.config.format.enabled && formatted !== sourceText;
+ const needsSave =
+ (project.config.format.enabled && formatted !== sourceText) ||
+ (project.config.integrations.prettier.enabled && formatted !== sourceText);
// Autofix if necessary
if (options.save && needsSave) {
@@ -256,6 +259,21 @@ export async function compilerLint(
diagnostics = [...diagnostics, ...eslintResult.diagnostics];
}
+ const prettierResult = await maybeRunPrettier({worker, ref, project});
+
+ if (prettierResult) {
+ formatted = prettierResult.formatted;
+ timings.set(
+ "prettier",
+ {
+ type: "official",
+ displayName: "Prettier",
+ took: prettierResult.timing,
+ },
+ );
+
+ diagnostics = [...diagnostics, ...prettierResult.diagnostics];
+ }
return worker.api.interceptDiagnostics(
{
timings,

MODIFY
internal/diagnostics/categories.ts
internal/diagnostics/categories.ts
@@ -28,6 +28,7 @@ export type DiagnosticCategory =
| ["compile", "jsx"]
| ["compile", "nonnumeric-enum-values"]
| ["eslint"]
+ | ["prettier"]
| ["files", "missingHandler"]
| ["files", "tooBig"]
| ["flags", "invalid"]
@@ -431,6 +432,7 @@ export const DIAGNOSTIC_CATEGORIES: {
"compile/jsx": ["compile", "jsx"],
"compile/nonnumeric-enum-values": ["compile", "nonnumeric-enum-values"],
eslint: ["eslint"],
+ prettier: ["prettier"],
"files/missingHandler": ["files", "missingHandler"],
"files/tooBig": ["files", "tooBig"],
"flags/invalid": ["flags", "invalid"],
@@ -514,6 +516,7 @@ export const categoryPrefixMap: {[name in DiagnosticCategoryPrefix]: true} = {
commands: true,
compile: true,
eslint: true,
+ prettier: true,
files: true,
integration: true,
format: true,

MODIFY
internal/project/constants.ts
internal/project/constants.ts
@@ -16,6 +16,13 @@ export const ESLINT_CONFIG_FILENAMES: string[] = [
".eslintignore",
];
+// NOTE: only supporting JSON for now
+export const PRETTIER_CONFIG_FILESNAMES: string[] = [
+ ".prettierrc",
+ ".prettierr.json",
+ ".prettierr.json5",
+];
+
export const PROJECT_CONFIG_PACKAGE_JSON_FIELD = "rome";
export const PROJECT_CONFIG_DIRECTORY = ".config";
export const PROJECT_CONFIG_FILENAMES: string[] = [];

ADD
internal/project/integrations/loadPrettier.ts
+import {consumeUnknown} from "@internal/consume";
+import {DIAGNOSTIC_CATEGORIES} from "@internal/diagnostics";
+import {PrettierConfig} from "@internal/project";
+import {json, json5} from "@internal/codec-config";
+
+export function loadPrettier(
+ file: string,
+ extension: string,
+): Partial<PrettierConfig> {
+ const config: Partial<PrettierConfig> = {};
+ let data: unknown;
+ // NOTE: we only support json for now
+ if (extension === "json5") {
+ data = json5.parse({input: file});
+ } else {
+ data = json.parse({input: file});
+ }
+ const prettier = consumeUnknown(data, DIAGNOSTIC_CATEGORIES.prettier, "json");
+
+ if (prettier.has("printWidth")) {
+ config.printWidth = prettier.get("printWidth").asNumber();
+ }
+ if (prettier.has("tabWidth")) {
+ config.tabWidth = prettier.get("tabWidth").asNumber();
+ }
+ if (prettier.has("useTabs")) {
+ config.useTabs = prettier.get("useTabs").asBoolean();
+ }
+ if (prettier.has("semi")) {
+ config.semi = prettier.get("semi").asBoolean();
+ }
+ if (prettier.has("singleQuote")) {
+ config.singleQuote = prettier.get("singleQuote").asBoolean();
+ }
+
+ return config;
+}

MODIFY
internal/project/load.ts
internal/project/load.ts
@@ -31,6 +31,7 @@ import {CachedFileReader} from "@internal/fs";
import {parseSemverRange} from "@internal/codec-semver";
import {descriptions} from "@internal/diagnostics";
import {
+ PRETTIER_CONFIG_FILESNAMES,
PROJECT_CONFIG_PACKAGE_JSON_FIELD,
VCS_IGNORE_FILENAMES,
} from "./constants";
@@ -38,6 +39,7 @@ import {lintRuleNames} from "@internal/compiler";
import {sha256} from "@internal/string-utils";
import {resolveBrowsers} from "@internal/codec-browsers";
import {ParserOptions} from "@internal/parser-core";
+import {loadPrettier} from "@internal/project/integrations/loadPrettier";
type NormalizedPartial = {
partial: PartialProjectConfig;
@@ -114,6 +116,19 @@ export async function loadCompleteProjectConfig(
}
}
+ // Load extensions configuration of prettier
+ for (const prettierFile of PRETTIER_CONFIG_FILESNAMES) {
+ const possiblePath = projectDirectory.append(prettierFile);
+ if (await possiblePath.exists()) {
+ const file = await reader.readFileText(possiblePath);
+ const prettierConfig = loadPrettier(file, possiblePath.getExtensions());
+ config.integrations.prettier = {
+ ...config.integrations.prettier,
+ ...prettierConfig,
+ };
+ }
+ }
+
// Calculate config hash keys
await Promise.all(
Array.from(
@@ -466,6 +481,29 @@ export async function normalizeProjectConfig(
}
}
eslint.enforceUsedProperties("eslint config property");
+
+ const prettier = integrations.get("prettier");
+ if (categoryExists(prettier)) {
+ if (prettier.has("enabled")) {
+ config.integrations.prettier.enabled = prettier.get("enabled").asBoolean();
+ if (prettier.has("printWidth")) {
+ config.integrations.prettier.printWidth = prettier.get("printWidth").asNumber();
+ }
+ if (prettier.has("tabWidth")) {
+ config.integrations.prettier.tabWidth = prettier.get("tabWidth").asNumber();
+ }
+ if (prettier.has("useTabs")) {
+ config.integrations.prettier.useTabs = prettier.get("useTabs").asBoolean();
+ }
+ if (prettier.has("semi")) {
+ config.integrations.prettier.semi = prettier.get("semi").asBoolean();
+ }
+ if (prettier.has("singleQuote")) {
+ config.integrations.prettier.singleQuote = prettier.get("singleQuote").asBoolean();
+ }
+ }
+ }
+ prettier.enforceUsedProperties("prettier config property");
}
meta.configDependencies = new AbsoluteFilePathSet([

MODIFY
internal/project/types.ts
internal/project/types.ts
@@ -103,10 +103,18 @@ export type ProjectConfigObjects = {
targets: Map<string, GetBrowserProps[]>;
};
+export type PrettierConfig = {
+ printWidth: number;
+ tabWidth: number;
+ useTabs: boolean;
+ semi: boolean;
+ singleQuote: boolean;
+};
+
export type ProjectConfigIntegrations = {
eslint: Enableable;
typescriptChecker: Enableable;
- prettier: Enableable;
+ prettier: Enableable & Partial<PrettierConfig>;
};
export type ProjectConfigCategoriesWithIgnore = "tests" | "lint";
@@ -215,7 +223,7 @@ export type RawUserProjectConfig = DeepPartial<{
integrations: {
eslint: Enableable;
typescriptChecker: Enableable;
- prettier: Enableable;
+ prettier: Enableable & Partial<PrettierConfig>;
};
}>;

MODIFY
internal/project/utils.ts
internal/project/utils.ts
@@ -11,6 +11,7 @@ import {AbsoluteFilePath, AbsoluteFilePathSet} from "@internal/path";
import {PartialProjectConfig} from "./types";
import {
ESLINT_CONFIG_FILENAMES,
+ PRETTIER_CONFIG_FILESNAMES,
PROJECT_CONFIG_FILENAMES,
VCS_IGNORE_FILENAMES,
} from "./constants";
@@ -89,6 +90,10 @@ export function getParentConfigDependencies(
basenames = basenames.concat(ESLINT_CONFIG_FILENAMES);
}
+ if (partialConfig.integrations.prettier.enabled) {
+ basenames = basenames.concat(PRETTIER_CONFIG_FILESNAMES);
+ }
+
if (partialConfig.vcs.root?.equal(directory)) {
basenames = basenames.concat(VCS_IGNORE_FILENAMES);
}

MODIFY
website/src/_includes/docs/project-config.md
website/src/_includes/docs/project-config.md
@@ -107,15 +107,79 @@ rome config push dependencies.exceptions.invalidLicenses.invalid-license-name "t
If you are unsure about the license name of your library, rome will suggest the command for
you when you try to run a command.
+### `integrations`
+
+Rome is more than linting or formatting. Rome has the concept of integrations, where
+it's possible to run a limited number of third party libraries using Rome itself.
+
+The main advantage of using Rome to run these integrations is to take advantage of Rome's
+infrastructure: caching and parallelism.
+
+### `integrations.prettier`
+
+You can use Rome to format your code using prettier. In order to integrate it,
+you have to install `prettier` in your project and enabled the integration via Rome:
+
+```bash
+rome config enable integrations.prettier
+```
+
+> The minimum `prettier`'s version supported is `2.0.0`;
+
+Now, when you run `./rome check --apply`, Rome will use `prettier` to format your code.
+
+If your project uses `.prettierrc` already, Rome will load the configuration and pass it to
+`prettier`.
+
+Alternatively, you can configure `prettier` inside Rome's configuration. Rome supports only a
+subset of options:
+
+- [printWidth](https://prettier.io/docs/en/options.html#print-width);
+- [tabWidth](https://prettier.io/docs/en/options.html#tab-width);
+- [useTabs](https://prettier.io/docs/en/options.html#tabs);
+- [semi](https://prettier.io/docs/en/options.html#semicolons);
+- [singleQuote](https://prettier.io/docs/en/options.html#quotes);
+
+The rest of options will be ignored and won't be passed to `prettier`.
+
+Ultimately, the configuration will look like this:
+
+```json
+{
+ "rome": {
+ "root": true,
+ "name": "fancy-project",
+ "lint": {
+ "enabled": false
+ },
+ "format": {
+ "enabled": false
+ },
+ "integrations": {
+ "prettier": {
+ "enabled": true,
+ "tabWidth": 2,
+ "useTabs": true,
+ "printWidth": 140
+ }
+ }
+ }
+}
+```
+
+### `integrations.eslint`
+
+MISSING DOCUMENTATION
+
### Supported Locations
You can specify your project config in a few different places.
##### `.config/rome.rjson` (recommended)
-This is the recommend location. It's the file we create when running `rome init`.
+This is the recommended location. It's the file we create when running `rome init`.
-It can contains Rome's flavor of JSON, [RJSON](#rome-json), that allows comments and simpler syntax.
+It can contain Rome's flavor of JSON, [RJSON](#rome-json), that allows comments and simpler syntax.
##### `.config/rome.json`

feat(integrations, project): prettier (#1575)