diff --git a/changelog_unreleased/api/16828.md b/changelog_unreleased/api/16828.md new file mode 100644 index 000000000..c737f1a24 --- /dev/null +++ b/changelog_unreleased/api/16828.md @@ -0,0 +1,42 @@ +#### Add support for TypeScript config files (#16828 by @itsyoboieltr & @fisker) + +Added new format of configuration files: + +- `.prettierrc.ts` +- `.prettierrc.mts` +- `.prettierrc.cts` +- `prettier.config.ts` +- `prettier.config.mts` +- `prettier.config.cts` + +Note: + +Currently TypeScript support in Node.js is experimental. + +To make TypeScript config files work, Node.js>=22.6.0 is required and `--experimental-strip-types` is required to run Node.js. + +You can run prettier with + +```sh +node --experimental-strip-types node_modules/prettier/bin/prettier.cjs . --write +``` + +or + +```sh +NODE_OPTIONS="--experimental-strip-types" prettier . --write +``` + +Other TS loaders should also work, but not tested, use at your own risk. + +For example, with [`tsx`](https://tsx.is/), you can + +```sh +node --import tsx node_modules/prettier/bin/prettier.cjs . --write +``` + +or + +```sh +tsx node_modules/prettier/bin/prettier.cjs . --write +``` diff --git a/docs/configuration.md b/docs/configuration.md index 7afd43007..c09ace3c2 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -8,17 +8,33 @@ You can configure Prettier via (in order of precedence): - A `"prettier"` key in your `package.json`, or [`package.yaml`](https://github.com/pnpm/pnpm/pull/1799) file. - A `.prettierrc` file written in JSON or YAML. - A `.prettierrc.json`, `.prettierrc.yml`, `.prettierrc.yaml`, or `.prettierrc.json5` file. -- A `.prettierrc.js`, or `prettier.config.js` file that exports an object using `export default` or `module.exports` (depends on the [`type`](https://nodejs.org/api/packages.html#type) value in your `package.json`). -- A `.prettierrc.mjs`, or `prettier.config.mjs` file that exports an object using `export default`. -- A `.prettierrc.cjs`, or `prettier.config.cjs` file that exports an object using `module.exports`. +- A `.prettierrc.js`, `prettier.config.js`, `.prettierrc.ts`, or `prettier.config.ts` file that exports an object using `export default` or `module.exports` (depends on the [`type`](https://nodejs.org/api/packages.html#type) value in your `package.json`). +- A `.prettierrc.mjs`, `prettier.config.mjs`, `.prettierrc.mts`, or `prettier.config.mts` file that exports an object using `export default`. +- A `.prettierrc.cjs`, `prettier.config.cjs`, `.prettierrc.cts`, or `prettier.config.cts` file that exports an object using `module.exports`. - A `.prettierrc.toml` file. +> TypeScript configuration files support requires [additional setup](#typescript-configuration-files) + The configuration file will be resolved starting from the location of the file being formatted, and searching up the file tree until a config file is (or isn’t) found. Prettier intentionally doesn’t support any kind of global configuration. This is to make sure that when a project is copied to another computer, Prettier’s behavior stays the same. Otherwise, Prettier wouldn’t be able to guarantee that everybody in a team gets the same consistent results. The options you can use in the configuration file are the same as the [API options](options.md). +### TypeScript Configuration Files + +TypeScript support for Node.js is currently experimental, Node.js>=22.6.0 is required and `--experimental-strip-types` is required to run Node.js. + +```sh +node --experimental-strip-types node_modules/prettier/bin/prettier.cjs . --write +``` + +or + +```sh +NODE_OPTIONS="--experimental-strip-types" prettier . --write +``` + ## Basic Configuration JSON: @@ -70,6 +86,34 @@ const config = { module.exports = config; ``` +TypeScript (ES Modules): + +```ts +// prettier.config.ts, .prettierrc.ts, prettier.config.mts, or .prettierrc.mts + +import { type Config } from "prettier"; + +const config: Config = { + trailingComma: "none", +}; + +export default config; +``` + +TypeScript (CommonJS): + +```ts +// prettier.config.ts, .prettierrc.ts, prettier.config.cts, or .prettierrc.cts + +import { type Config } from "prettier"; + +const config: Config = { + trailingComma: "none", +}; + +module.exports = config; +``` + YAML: ```yaml diff --git a/src/config/prettier-config/config-searcher.js b/src/config/prettier-config/config-searcher.js index b1c607a2d..49f0a560d 100644 --- a/src/config/prettier-config/config-searcher.js +++ b/src/config/prettier-config/config-searcher.js @@ -14,11 +14,17 @@ const CONFIG_FILE_NAMES = [ ".prettierrc.yml", ".prettierrc.json5", ".prettierrc.js", + ".prettierrc.ts", ".prettierrc.mjs", + ".prettierrc.mts", ".prettierrc.cjs", + ".prettierrc.cts", "prettier.config.js", + "prettier.config.ts", "prettier.config.mjs", + "prettier.config.mts", "prettier.config.cjs", + "prettier.config.cts", ".prettierrc.toml", ]; diff --git a/src/config/prettier-config/loaders.js b/src/config/prettier-config/loaders.js index bf6d1c4e5..b7d3923d7 100644 --- a/src/config/prettier-config/loaders.js +++ b/src/config/prettier-config/loaders.js @@ -63,6 +63,9 @@ const loaders = { ".js": loadJs, ".mjs": loadJs, ".cjs": loadJs, + ".ts": loadJs, + ".mts": loadJs, + ".cts": loadJs, ".yaml": loadYaml, ".yml": loadYaml, // No extension diff --git a/src/index.d.ts b/src/index.d.ts index 8e4df1647..b7117cda6 100644 --- a/src/index.d.ts +++ b/src/index.d.ts @@ -303,7 +303,7 @@ export type BuiltInParserName = export type BuiltInParsers = Record; /** - * For use in `.prettierrc.js`, `.prettierrc.cjs`, `prettierrc.mjs`, `prettier.config.js`, `prettier.config.cjs`, `prettier.config.mjs` + * For use in `.prettierrc.js`, `.prettierrc.ts`, `.prettierrc.cjs`, `.prettierrc.cts`, `prettierrc.mjs`, `prettierrc.mts`, `prettier.config.js`, `prettier.config.ts`, `prettier.config.cjs`, `prettier.config.cts`, `prettier.config.mjs`, `prettier.config.mts` */ export interface Config extends Options { overrides?: Array<{ diff --git a/tests/integration/__tests__/config-file-typescript.js b/tests/integration/__tests__/config-file-typescript.js new file mode 100644 index 000000000..29c76bfe3 --- /dev/null +++ b/tests/integration/__tests__/config-file-typescript.js @@ -0,0 +1,92 @@ +import { outdent } from "outdent"; +import jestPathSerializer from "../path-serializer.js"; + +expect.addSnapshotSerializer(jestPathSerializer); + +const NODE_TS_SUPPORT_FLAGS = ["--experimental-strip-types"]; +const IS_SUPPORTED_NODE_JS = Number(process.versions.node.split(".")[0]) >= 22; + +const getOutputTabWidth = (code) => + code.match(/\n(?\s+)return/u).groups.indention.length; +const code = "function foo() {return bar}"; + +const TAB_WIDTH_3_OUTPUT = outdent` + function foo() { + ${" ".repeat(3)}return bar; + } +`; + +if (IS_SUPPORTED_NODE_JS) { + test("Should support typescript config files", async () => { + const output = await runCli( + "cli/config/ts/auto-discovery/", + ["--stdin-filepath", "foo.js"], + { + input: code, + nodeOptions: NODE_TS_SUPPORT_FLAGS, + }, + ).stdout; + + expect(output).toBe(TAB_WIDTH_3_OUTPUT); + }); + + test("Should throw errors when flags are missing", async () => { + await runCli( + "cli/config/ts/auto-discovery/", + ["--stdin-filepath", "foo.js"], + { input: code }, + ).test({ + status: "non-zero", + stdout: "", + write: [], + stderr: expect.stringMatching(/Unknown file extension ".ts" for/u), + }); + }); + + describe("Config file names", () => { + for (const { configFileName, expectedTabWidth } of [ + { configFileName: ".prettierrc.cts", expectedTabWidth: 8 }, + { configFileName: ".prettierrc.mts", expectedTabWidth: 3 }, + { configFileName: ".prettierrc.ts", expectedTabWidth: 4 }, + { configFileName: "prettier.config.cts", expectedTabWidth: 7 }, + { configFileName: "prettier.config.mts", expectedTabWidth: 6 }, + { configFileName: "prettier.config.ts", expectedTabWidth: 5 }, + ]) { + test(`Should format file with tabWidth: ${expectedTabWidth} with config file '${configFileName}'.`, async () => { + const output = await runCli( + "cli/config/ts/config-file-names/", + ["--stdin-filepath", "foo.js", "--config", configFileName], + { + input: code, + nodeOptions: NODE_TS_SUPPORT_FLAGS, + }, + ).stdout; + + expect(getOutputTabWidth(output)).toBe(expectedTabWidth); + }); + } + }); +} else { + test("Should throw errors when Node.js < 22", async () => { + await runCli( + "cli/config/ts/auto-discovery/", + ["--stdin-filepath", "foo.js"], + { input: code }, + ).test({ + status: "non-zero", + stdout: "", + write: [], + stderr: expect.stringMatching(/Unknown file extension ".ts" for/u), + }); + + // https://github.com/nodejs/node/issues/41103 + await expect(() => + runCli("cli/config/ts/auto-discovery/", ["--stdin-filepath", "foo.js"], { + input: code, + nodeOptions: NODE_TS_SUPPORT_FLAGS, + }), + ).rejects.toThrow( + /Initiated Worker with invalid execArgv flags: --experimental-strip-types/u, + ); + }); +} diff --git a/tests/integration/cli/config/ts/auto-discovery/.prettierrc.ts b/tests/integration/cli/config/ts/auto-discovery/.prettierrc.ts new file mode 100644 index 000000000..497bc5d5c --- /dev/null +++ b/tests/integration/cli/config/ts/auto-discovery/.prettierrc.ts @@ -0,0 +1,7 @@ +type PrettierConfig = { + tabWidth: number +}; + +const config: PrettierConfig = {tabWidth: 3}; + +export default config; diff --git a/tests/integration/cli/config/ts/config-file-names/.prettierrc.cts b/tests/integration/cli/config/ts/config-file-names/.prettierrc.cts new file mode 100644 index 000000000..103becdf1 --- /dev/null +++ b/tests/integration/cli/config/ts/config-file-names/.prettierrc.cts @@ -0,0 +1,7 @@ +type PrettierConfig = { + tabWidth: number +}; + +const config: PrettierConfig = {tabWidth: 8}; + +module.exports = config; diff --git a/tests/integration/cli/config/ts/config-file-names/.prettierrc.mts b/tests/integration/cli/config/ts/config-file-names/.prettierrc.mts new file mode 100644 index 000000000..497bc5d5c --- /dev/null +++ b/tests/integration/cli/config/ts/config-file-names/.prettierrc.mts @@ -0,0 +1,7 @@ +type PrettierConfig = { + tabWidth: number +}; + +const config: PrettierConfig = {tabWidth: 3}; + +export default config; diff --git a/tests/integration/cli/config/ts/config-file-names/.prettierrc.ts b/tests/integration/cli/config/ts/config-file-names/.prettierrc.ts new file mode 100644 index 000000000..547d1aaa6 --- /dev/null +++ b/tests/integration/cli/config/ts/config-file-names/.prettierrc.ts @@ -0,0 +1,7 @@ +type PrettierConfig = { + tabWidth: number +}; + +const config: PrettierConfig = {tabWidth: 4}; + +export default config; diff --git a/tests/integration/cli/config/ts/config-file-names/file-to-format.js b/tests/integration/cli/config/ts/config-file-names/file-to-format.js new file mode 100644 index 000000000..83ee44af7 --- /dev/null +++ b/tests/integration/cli/config/ts/config-file-names/file-to-format.js @@ -0,0 +1,3 @@ +function foo() { + return bar; +} diff --git a/tests/integration/cli/config/ts/config-file-names/prettier.config.cts b/tests/integration/cli/config/ts/config-file-names/prettier.config.cts new file mode 100644 index 000000000..c222a151e --- /dev/null +++ b/tests/integration/cli/config/ts/config-file-names/prettier.config.cts @@ -0,0 +1,7 @@ +type PrettierConfig = { + tabWidth: number +}; + +const config: PrettierConfig = {tabWidth: 7}; + +module.exports = config; diff --git a/tests/integration/cli/config/ts/config-file-names/prettier.config.mts b/tests/integration/cli/config/ts/config-file-names/prettier.config.mts new file mode 100644 index 000000000..16e89d302 --- /dev/null +++ b/tests/integration/cli/config/ts/config-file-names/prettier.config.mts @@ -0,0 +1,7 @@ +type PrettierConfig = { + tabWidth: number +}; + +const config: PrettierConfig = {tabWidth: 6}; + +export default config; diff --git a/tests/integration/cli/config/ts/config-file-names/prettier.config.ts b/tests/integration/cli/config/ts/config-file-names/prettier.config.ts new file mode 100644 index 000000000..a420cff4a --- /dev/null +++ b/tests/integration/cli/config/ts/config-file-names/prettier.config.ts @@ -0,0 +1,7 @@ +type PrettierConfig = { + tabWidth: number +}; + +const config: PrettierConfig = {tabWidth: 5}; + +export default config; diff --git a/tests/integration/run-cli.js b/tests/integration/run-cli.js index 35af7385a..c42cca016 100644 --- a/tests/integration/run-cli.js +++ b/tests/integration/run-cli.js @@ -38,6 +38,8 @@ function runCliWorker(dir, args, options) { write: [], }; + const nodeOptions = options?.nodeOptions ?? []; + const worker = new Worker(CLI_WORKER_FILE, { argv: args, execArgv: [ @@ -45,6 +47,7 @@ function runCliWorker(dir, args, options) { ...(SUPPORTS_DISABLE_WARNING_FLAG ? ["--disable-warning=ExperimentalWarning"] : []), + ...nodeOptions, ], stdout: true, stderr: true,