diff --git a/changelog_unreleased/api/15332.md b/changelog_unreleased/api/15332.md index 7fdca0c08..79bf48983 100644 --- a/changelog_unreleased/api/15332.md +++ b/changelog_unreleased/api/15332.md @@ -1,13 +1,15 @@ -#### Accept `URL` in `prettier.{resolveConfig,resolveConfigFile}()` (#15332, #15354 by @fisker) +#### Accept `URL` in `prettier.{resolveConfig,resolveConfigFile,getFileInfo}()` (#15332, #15354, #15360 by @fisker) -[`prettier.resolveConfig()`](https://prettier.io/docs/en/api#prettierresolveconfigfileurlorpath--options) and [`prettier.resolveConfigFile()`](https://prettier.io/docs/en/api#prettierresolveconfigfilefileurlorpath) now accepts an [URL](https://developer.mozilla.org/en-US/docs/Web/API/URL) with `file:` protocol or a url string starts with `file://`. +[`prettier.resolveConfig()`](https://prettier.io/docs/en/api#prettierresolveconfigfileurlorpath--options), [`prettier.resolveConfigFile()`](https://prettier.io/docs/en/api#prettierresolveconfigfilefileurlorpath), and [`prettier.getFileInfo()`](https://prettier.io/docs/en/api#prettiergetfileinfofileurlorpath--options) now accepts an [URL](https://developer.mozilla.org/en-US/docs/Web/API/URL) with `file:` protocol or a url string starts with `file://`. ```js // `URL` await prettier.resolveConfig(new URL("./path/to/file", import.meta.url)); await prettier.resolveConfigFile(new URL("./path/to/file", import.meta.url)); +await prettier.getFileInfo(new URL("./path/to/file", import.meta.url)); // URL string await prettier.resolveConfig("file:///path/to/file"); await prettier.resolveConfigFile("file:///path/to/file"); +await prettier.getFileInfo("file:///path/to/file"); ``` diff --git a/docs/api.md b/docs/api.md index e55234f6a..aac954776 100644 --- a/docs/api.md +++ b/docs/api.md @@ -77,7 +77,7 @@ const configFile = await prettier.resolveConfigFile(filePath); When Prettier loads configuration files and plugins, the file system structure is cached for performance. This function will clear the cache. Generally this is only needed for editor integrations that know that the file system has changed since the last format took place. -## `prettier.getFileInfo(filePath [, options])` +## `prettier.getFileInfo(fileUrlOrPath [, options])` `getFileInfo` can be used by editor extensions to decide if a particular file needs to be formatted. This method returns a promise, which resolves to an object with the following properties: @@ -88,11 +88,11 @@ When Prettier loads configuration files and plugins, the file system structure i } ``` -The promise will be rejected if the type of `filePath` is not `string`. +The promise will be rejected if the type of `fileUrlOrPath` is not `string` or `URL`. Setting `options.ignorePath` (`string | string[]`) and `options.withNodeModules` (`boolean`) influence the value of `ignored` (`false` by default). -If the given `filePath` is ignored, the `inferredParser` is always `null`. +If the given `fileUrlOrPath` is ignored, the `inferredParser` is always `null`. Providing [plugin](plugins.md) paths in `options.plugins` (`string[]`) helps extract `inferredParser` for files that are not supported by Prettier core. diff --git a/src/common/get-file-info.js b/src/common/get-file-info.js index 344763ba2..13821bcdb 100644 --- a/src/common/get-file-info.js +++ b/src/common/get-file-info.js @@ -1,3 +1,4 @@ +import { toPath } from "url-or-path"; import inferParser from "../utils/infer-parser.js"; import { resolveConfig } from "../config/resolve-config.js"; import { isIgnored } from "../utils/ignore.js"; @@ -8,7 +9,7 @@ import { isIgnored } from "../utils/ignore.js"; */ /** - * @param {string} filePath + * @param {string | URL} fileUrlOrPath * @param {FileInfoOptions} options * @returns {Promise} * @@ -16,10 +17,10 @@ import { isIgnored } from "../utils/ignore.js"; * not an object. A transformation from this array to an object is automatically done * internally by the method wrapper. See withPlugins() in index.js. */ -async function getFileInfo(filePath, options) { - if (typeof filePath !== "string") { +async function getFileInfo(fileUrlOrPath, options) { + if (typeof fileUrlOrPath !== "string" && !(fileUrlOrPath instanceof URL)) { throw new TypeError( - `expect \`filePath\` to be a string, got \`${typeof filePath}\``, + `expect \`fileUrlOrPath\` to be a string or URL, got \`${typeof fileUrlOrPath}\``, ); } @@ -29,6 +30,7 @@ async function getFileInfo(filePath, options) { ignorePath = [ignorePath]; } + const filePath = toPath(fileUrlOrPath); const ignored = await isIgnored(filePath, { ignorePath, withNodeModules }); let inferredParser; diff --git a/src/index.d.ts b/src/index.d.ts index bfd46b9e5..39f42fc8c 100644 --- a/src/index.d.ts +++ b/src/index.d.ts @@ -799,7 +799,7 @@ export interface FileInfoResult { } export function getFileInfo( - filePath: string, + fileUrlOrPath: string, options?: FileInfoOptions, ): Promise; diff --git a/tests/integration/__tests__/file-info.js b/tests/integration/__tests__/file-info.js index 3cf7face4..8ddc474f6 100644 --- a/tests/integration/__tests__/file-info.js +++ b/tests/integration/__tests__/file-info.js @@ -1,5 +1,6 @@ import path from "node:path"; import fs from "node:fs"; +import url from "node:url"; import { temporaryDirectory as getTemporaryDirectory } from "tempy"; import createEsmUtils from "esm-utils"; import prettier from "../../config/prettier-entry.js"; @@ -118,12 +119,14 @@ describe("extracts file-info with inferredParser=foo when a plugin is hand-picke test("API getFileInfo with no args", async () => { await expect(prettier.getFileInfo()).rejects.toThrow( - new TypeError("expect `filePath` to be a string, got `undefined`"), + new TypeError( + "expect `fileUrlOrPath` to be a string or URL, got `undefined`", + ), ); }); test("API getFileInfo with filepath only", async () => { - await expect(prettier.getFileInfo("README")).resolves.toMatchObject({ + await expect(prettier.getFileInfo("README")).resolves.toEqual({ ignored: false, inferredParser: "markdown", }); @@ -133,25 +136,23 @@ describe("API getFileInfo resolveConfig", () => { const files = Object.fromEntries( ["foo", "js", "bar", "css"].map((ext) => [ ext, - path.resolve( - path.join(__dirname, `../cli/with-resolve-config/file.${ext}`), - ), + new URL(`../cli/with-resolve-config/file.${ext}`, import.meta.url), ]), ); test("{resolveConfig: undefined}", async () => { - await expect(prettier.getFileInfo(files.foo)).resolves.toMatchObject({ + await expect(prettier.getFileInfo(files.foo)).resolves.toEqual({ ignored: false, inferredParser: "foo-parser", }); - await expect(prettier.getFileInfo(files.js)).resolves.toMatchObject({ + await expect(prettier.getFileInfo(files.js)).resolves.toEqual({ ignored: false, inferredParser: "override-js-parser", }); - await expect(prettier.getFileInfo(files.bar)).resolves.toMatchObject({ + await expect(prettier.getFileInfo(files.bar)).resolves.toEqual({ ignored: false, inferredParser: null, }); - await expect(prettier.getFileInfo(files.css)).resolves.toMatchObject({ + await expect(prettier.getFileInfo(files.css)).resolves.toEqual({ ignored: false, inferredParser: "css", }); @@ -159,25 +160,25 @@ describe("API getFileInfo resolveConfig", () => { test("{resolveConfig: true}", async () => { await expect( prettier.getFileInfo(files.foo, { resolveConfig: true }), - ).resolves.toMatchObject({ + ).resolves.toEqual({ ignored: false, inferredParser: "foo-parser", }); await expect( prettier.getFileInfo(files.js, { resolveConfig: true }), - ).resolves.toMatchObject({ + ).resolves.toEqual({ ignored: false, inferredParser: "override-js-parser", }); await expect( prettier.getFileInfo(files.bar, { resolveConfig: true }), - ).resolves.toMatchObject({ + ).resolves.toEqual({ ignored: false, inferredParser: null, }); await expect( prettier.getFileInfo(files.css, { resolveConfig: true }), - ).resolves.toMatchObject({ + ).resolves.toEqual({ ignored: false, inferredParser: "css", }); @@ -188,15 +189,15 @@ describe("API getFileInfo resolveConfig when no config is present", () => { const files = Object.fromEntries( ["foo", "js"].map((ext) => [ ext, - path.resolve(path.join(__dirname, `../cli/non-exists-dir/file.${ext}`)), + new URL(`../cli/non-exists-dir/file.${ext}`, import.meta.url), ]), ); test("{resolveConfig: undefined}", async () => { - await expect(prettier.getFileInfo(files.foo)).resolves.toMatchObject({ + await expect(prettier.getFileInfo(files.foo)).resolves.toEqual({ ignored: false, inferredParser: null, }); - await expect(prettier.getFileInfo(files.js)).resolves.toMatchObject({ + await expect(prettier.getFileInfo(files.js)).resolves.toEqual({ ignored: false, inferredParser: "babel", }); @@ -204,13 +205,13 @@ describe("API getFileInfo resolveConfig when no config is present", () => { test("{resolveConfig: true}", async () => { await expect( prettier.getFileInfo(files.foo, { resolveConfig: true }), - ).resolves.toMatchObject({ + ).resolves.toEqual({ ignored: false, inferredParser: null, }); await expect( prettier.getFileInfo(files.js, { resolveConfig: true }), - ).resolves.toMatchObject({ + ).resolves.toEqual({ ignored: false, inferredParser: "babel", }); @@ -218,48 +219,40 @@ describe("API getFileInfo resolveConfig when no config is present", () => { }); test("API getFileInfo with ignorePath", async () => { - const file = path.resolve( - path.join( - __dirname, - "../cli/ignore-path/file-info-test/ignored-by-customignore.js", - ), + const file = new URL( + "../cli/ignore-path/file-info-test/ignored-by-customignore.js", + import.meta.url, ); const ignorePath = path.resolve( path.join(__dirname, "../cli/ignore-path/file-info-test/.customignore"), ); - await expect(prettier.getFileInfo(file)).resolves.toMatchObject({ + await expect(prettier.getFileInfo(file)).resolves.toEqual({ ignored: false, inferredParser: "babel", }); - await expect( - prettier.getFileInfo(file, { ignorePath }), - ).resolves.toMatchObject({ + await expect(prettier.getFileInfo(file, { ignorePath })).resolves.toEqual({ ignored: true, inferredParser: null, }); }); test("API getFileInfo with ignorePath containing relative paths", async () => { - const file = path.resolve( - path.join( - __dirname, - "../cli/ignore-relative-path/level1-glob/level2-glob/level3-glob/shouldNotBeFormat.js", - ), + const file = new URL( + "../cli/ignore-relative-path/level1-glob/level2-glob/level3-glob/shouldNotBeFormat.js", + import.meta.url, ); const ignorePath = path.resolve( path.join(__dirname, "../cli/ignore-relative-path/.prettierignore"), ); - await expect(prettier.getFileInfo(file)).resolves.toMatchObject({ + await expect(prettier.getFileInfo(file)).resolves.toEqual({ ignored: false, inferredParser: "babel", }); - await expect( - prettier.getFileInfo(file, { ignorePath }), - ).resolves.toMatchObject({ + await expect(prettier.getFileInfo(file, { ignorePath })).resolves.toEqual({ ignored: true, inferredParser: null, }); @@ -300,10 +293,11 @@ describe("API getFileInfo with ignorePath", () => { }); test("API getFileInfo with withNodeModules", async () => { - const file = path.resolve( - path.join(__dirname, "../cli/with-node-modules/node_modules/file.js"), + const file = new URL( + "../cli/with-node-modules/node_modules/file.js", + import.meta.url, ); - await expect(prettier.getFileInfo(file)).resolves.toMatchObject({ + await expect(prettier.getFileInfo(file)).resolves.toEqual({ ignored: true, inferredParser: null, }); @@ -311,7 +305,7 @@ test("API getFileInfo with withNodeModules", async () => { prettier.getFileInfo(file, { withNodeModules: true, }), - ).resolves.toMatchObject({ + ).resolves.toEqual({ ignored: false, inferredParser: "babel", }); @@ -320,7 +314,7 @@ test("API getFileInfo with withNodeModules", async () => { test("extracts file-info for a JS file with no extension but a standard shebang", async () => { await expect( prettier.getFileInfo("tests/integration/cli/shebang/node-shebang"), - ).resolves.toMatchObject({ + ).resolves.toEqual({ ignored: false, inferredParser: "babel", }); @@ -329,7 +323,7 @@ test("extracts file-info for a JS file with no extension but a standard shebang" test("extracts file-info for a JS file with no extension but an env-based shebang", async () => { await expect( prettier.getFileInfo("tests/integration/cli/shebang/env-node-shebang"), - ).resolves.toMatchObject({ + ).resolves.toEqual({ ignored: false, inferredParser: "babel", }); @@ -338,7 +332,7 @@ test("extracts file-info for a JS file with no extension but an env-based sheban test("returns null parser for unknown shebang", async () => { await expect( prettier.getFileInfo("tests/integration/cli/shebang/nonsense-shebang"), - ).resolves.toMatchObject({ + ).resolves.toEqual({ ignored: false, inferredParser: null, }); @@ -352,7 +346,7 @@ test("API getFileInfo with hand-picked plugins", async () => { "../plugins/automatic/node_modules/@prettier/plugin-foo/index.js", ), ); - await expect(prettier.getFileInfo(file)).resolves.toMatchObject({ + await expect(prettier.getFileInfo(file)).resolves.toEqual({ ignored: false, inferredParser: null, }); @@ -360,7 +354,7 @@ test("API getFileInfo with hand-picked plugins", async () => { prettier.getFileInfo(file, { plugins: [pluginPath], }), - ).resolves.toMatchObject({ + ).resolves.toEqual({ ignored: false, inferredParser: "foo", }); @@ -375,8 +369,24 @@ test("API getFileInfo with ignorePath and resolveConfig should infer parser with ignorePath, }; - await expect(prettier.getFileInfo(filePath, options)).resolves.toMatchObject({ + await expect(prettier.getFileInfo(filePath, options)).resolves.toEqual({ ignored: false, inferredParser: "parser-for-config-dir", }); }); + +test("API getFileInfo accepts path or URL", async () => { + const fileUrl = new URL("../../../README.md", import.meta.url); + const expectedResult = { ignored: false, inferredParser: "markdown" }; + + const resultByUrl = await prettier.getFileInfo(fileUrl); + const resultByUrlHref = await prettier.getFileInfo(fileUrl.href); + const resultByPath = await prettier.getFileInfo(url.fileURLToPath(fileUrl)); + const resultByRelativePath = await prettier.getFileInfo( + path.relative(process.cwd(), url.fileURLToPath(fileUrl)), + ); + expect(resultByUrl).toEqual(expectedResult); + expect(resultByUrlHref).toEqual(expectedResult); + expect(resultByPath).toEqual(expectedResult); + expect(resultByRelativePath).toEqual(expectedResult); +});