From 7e45a92707fc578c7d00d86a8b6fe4618229c56b Mon Sep 17 00:00:00 2001 From: fisker Cheung Date: Wed, 6 Sep 2023 14:54:36 +0800 Subject: [PATCH] Accept `URL` in `resolveConfig` (#15354) --- changelog_unreleased/api/15332.md | 14 +- docs/api.md | 4 +- src/config/resolve-config.js | 3 +- src/index.d.ts | 6 +- .../__tests__/config-resolution.js | 134 ++++++++++-------- .../__tests__/resolve-config-file.js | 8 +- 6 files changed, 94 insertions(+), 75 deletions(-) diff --git a/changelog_unreleased/api/15332.md b/changelog_unreleased/api/15332.md index f366d4d17..7fdca0c08 100644 --- a/changelog_unreleased/api/15332.md +++ b/changelog_unreleased/api/15332.md @@ -1,11 +1,13 @@ -#### Accept `URL` in `prettier.resolveConfigFile()` (#15332 by @fisker) +#### Accept `URL` in `prettier.{resolveConfig,resolveConfigFile}()` (#15332, #15354 by @fisker) -[`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://`. - -```js -await prettier.resolveConfigFile(import.meta.url); -``` +[`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://`. ```js +// `URL` +await prettier.resolveConfig(new URL("./path/to/file", import.meta.url)); await prettier.resolveConfigFile(new URL("./path/to/file", import.meta.url)); + +// URL string +await prettier.resolveConfig("file:///path/to/file"); +await prettier.resolveConfigFile("file:///path/to/file"); ``` diff --git a/docs/api.md b/docs/api.md index 7cf2a0f2a..e55234f6a 100644 --- a/docs/api.md +++ b/docs/api.md @@ -33,9 +33,9 @@ await prettier.formatWithCursor(" 1", { cursorOffset: 2, parser: "babel" }); // -> { formatted: '1;\n', cursorOffset: 1 } ``` -## `prettier.resolveConfig(filePath [, options])` +## `prettier.resolveConfig(fileUrlOrPath [, options])` -`resolveConfig` can be used to resolve configuration for a given source file, passing its path as the first argument. The config search will start at the file path and continue to search up the directory (you can use `process.cwd()` to start searching from the current directory). Or you can pass directly the path of the config file as `options.config` if you don’t wish to search for it. A promise is returned which will resolve to: +`resolveConfig` can be used to resolve configuration for a given source file, passing its path or url as the first argument. The config search will start at the file location and continue to search up the directory (you can use `process.cwd()` to start searching from the current directory). Or you can pass directly the path of the config file as `options.config` if you don’t wish to search for it. A promise is returned which will resolve to: - An options object, providing a [config file](configuration.md) was found. - `null`, if no file was found. diff --git a/src/config/resolve-config.js b/src/config/resolve-config.js index e21fcbf3e..54e3d9806 100644 --- a/src/config/resolve-config.js +++ b/src/config/resolve-config.js @@ -35,8 +35,9 @@ function loadPrettierConfig(filePath, options) { : search(filePath ? path.resolve(filePath) : undefined); } -async function resolveConfig(filePath, options) { +async function resolveConfig(fileUrlOrPath, options) { options = { useCache: true, ...options }; + const filePath = toPath(fileUrlOrPath); const [result, editorConfigured] = await Promise.all([ loadPrettierConfig(filePath, options), diff --git a/src/index.d.ts b/src/index.d.ts index cdc0fd599..bfd46b9e5 100644 --- a/src/index.d.ts +++ b/src/index.d.ts @@ -609,8 +609,8 @@ export interface ResolveConfigOptions { /** * `resolveConfig` can be used to resolve configuration for a given source file, - * passing its path as the first argument. The config search will start at the - * file path and continue to search up the directory. + * passing its path or url as the first argument. The config search will start at + * the file location and continue to search up the directory. * (You can use `process.cwd()` to start searching from the current directory). * * A promise is returned which will resolve to: @@ -621,7 +621,7 @@ export interface ResolveConfigOptions { * The promise will be rejected if there was an error parsing the configuration file. */ export function resolveConfig( - filePath: string, + fileUrlOrPath: string | URL, options?: ResolveConfigOptions, ): Promise; diff --git a/tests/integration/__tests__/config-resolution.js b/tests/integration/__tests__/config-resolution.js index dfd518295..a29a4247e 100644 --- a/tests/integration/__tests__/config-resolution.js +++ b/tests/integration/__tests__/config-resolution.js @@ -1,18 +1,16 @@ import path from "node:path"; -import createEsmUtils from "esm-utils"; +import url from "node:url"; import prettier from "../../config/prettier-entry.js"; -const { __dirname } = createEsmUtils(import.meta); - test("resolves configuration from external files and overrides by extname", async () => { await expect( prettier.resolveConfig( - path.join(__dirname, "../cli/config/external-overrides/file.js"), + new URL("../cli/config/external-overrides/file.js", import.meta.url), ), ).resolves.toEqual({ tabWidth: 3, semi: false }); await expect( prettier.resolveConfig( - path.join(__dirname, "../cli/config/external-overrides/file.ts"), + new URL("../cli/config/external-overrides/file.ts", import.meta.url), ), ).resolves.toEqual({ tabWidth: 3, semi: true }); }); @@ -88,26 +86,21 @@ test("API resolveConfig with no args", async () => { }); test("API resolveConfig with file arg", async () => { - const file = path.resolve(path.join(__dirname, "../cli/config/js/file.js")); + const file = new URL("../cli/config/js/file.js", import.meta.url); await expect(prettier.resolveConfig(file)).resolves.toMatchObject({ tabWidth: 8, }); }); test("API resolveConfig with file arg and extension override", async () => { - const file = path.resolve( - path.join(__dirname, "../cli/config/no-config/file.ts"), - ); + const file = new URL("../cli/config/no-config/file.ts", import.meta.url); await expect(prettier.resolveConfig(file)).resolves.toMatchObject({ semi: true, }); }); test("API resolveConfig with file arg and .editorconfig", async () => { - const file = path.resolve( - path.join(__dirname, "../cli/config/editorconfig/file.js"), - ); - + const file = new URL("../cli/config/editorconfig/file.js", import.meta.url); await expect(prettier.resolveConfig(file)).resolves.toMatchObject({ semi: false, }); @@ -122,18 +115,19 @@ test("API resolveConfig with file arg and .editorconfig", async () => { }); test("API resolveConfig with file arg and .editorconfig (key = unset)", async () => { - const file = path.resolve( - path.join(__dirname, "../cli/config/editorconfig/tab_width=unset.js"), + const file = new URL( + "../cli/config/editorconfig/tab_width=unset.js", + import.meta.url, ); - await expect( prettier.resolveConfig(file, { editorconfig: true }), ).resolves.not.toMatchObject({ tabWidth: "unset" }); }); test("API resolveConfig with nested file arg and .editorconfig", async () => { - const file = path.resolve( - path.join(__dirname, "../cli/config/editorconfig/lib/file.js"), + const file = new URL( + "../cli/config/editorconfig/lib/file.js", + import.meta.url, ); await expect(prettier.resolveConfig(file)).resolves.toMatchObject({ @@ -150,8 +144,9 @@ test("API resolveConfig with nested file arg and .editorconfig", async () => { }); test("API resolveConfig with nested file arg and .editorconfig and indent_size = tab", async () => { - const file = path.resolve( - path.join(__dirname, "../cli/config/editorconfig/lib/indent_size=tab.js"), + const file = new URL( + "../cli/config/editorconfig/lib/indent_size=tab.js", + import.meta.url, ); await expect(prettier.resolveConfig(file)).resolves.toMatchObject({ @@ -172,44 +167,47 @@ test("API clearConfigCache", () => { }); test("API resolveConfig overrides work with dotfiles", async () => { - const folder = path.join(__dirname, "../cli/config/dot-overrides"); - await expect( - prettier.resolveConfig(path.join(folder, ".foo.json")), - ).resolves.toMatchObject({ + const file = new URL( + "../cli/config/dot-overrides/.foo.json", + import.meta.url, + ); + await expect(prettier.resolveConfig(file)).resolves.toMatchObject({ tabWidth: 4, }); }); test("API resolveConfig overrides work with absolute paths", async () => { // Absolute path - const file = path.join(__dirname, "../cli/config/filepath/subfolder/file.js"); + const file = url.fileURLToPath( + new URL("../cli/config/filepath/subfolder/file.js", import.meta.url), + ); await expect(prettier.resolveConfig(file)).resolves.toMatchObject({ tabWidth: 6, }); }); test("API resolveConfig overrides excludeFiles", async () => { - const notOverride = path.join( - __dirname, + const notOverride = new URL( "../cli/config/overrides-exclude-files/foo", + import.meta.url, ); await expect(prettier.resolveConfig(notOverride)).resolves.toMatchObject({ singleQuote: true, trailingComma: "all", }); - const singleQuote = path.join( - __dirname, + const singleQuote = new URL( "../cli/config/overrides-exclude-files/single-quote.js", + import.meta.url, ); await expect(prettier.resolveConfig(singleQuote)).resolves.toMatchObject({ singleQuote: true, trailingComma: "es5", }); - const doubleQuote = path.join( - __dirname, + const doubleQuote = new URL( "../cli/config/overrides-exclude-files/double-quote.js", + import.meta.url, ); await expect(prettier.resolveConfig(doubleQuote)).resolves.toMatchObject({ singleQuote: false, @@ -218,19 +216,20 @@ test("API resolveConfig overrides excludeFiles", async () => { }); test("API resolveConfig removes $schema option", async () => { - const file = path.resolve( - path.join(__dirname, "../cli/config/$schema/index.js"), - ); + const file = new URL("../cli/config/$schema/index.js", import.meta.url); await expect(prettier.resolveConfig(file)).resolves.toEqual({ tabWidth: 42, }); }); test("API resolveConfig resolves relative path values based on config filepath", async () => { - const currentDir = path.join(__dirname, "../cli/config/resolve-relative"); - const parentDir = path.resolve(currentDir, ".."); + const currentDir = new URL( + "../cli/config/resolve-relative/", + import.meta.url, + ); + const parentDir = path.resolve(url.fileURLToPath(currentDir), ".."); await expect( - prettier.resolveConfig(`${currentDir}/index.js`), + prettier.resolveConfig(new URL("index.js", currentDir)), ).resolves.toMatchObject({ plugins: [path.join(parentDir, "path-to-plugin")], }); @@ -240,29 +239,32 @@ test("API resolveConfig de-references to an external module", async () => { const config = { printWidth: 77, semi: false }; await expect( prettier.resolveConfig( - path.join( - __dirname, + new URL( "../cli/config/external-config/cjs-package/index.js", + import.meta.url, ), ), ).resolves.toEqual(config); await expect( prettier.resolveConfig( - path.join( - __dirname, + new URL( "../cli/config/external-config/esm-package/index.js", + import.meta.url, ), ), ).resolves.toEqual(config); await expect( prettier.resolveConfig( - path.join(__dirname, "../cli/config/external-config/esm-file/index.js"), + new URL( + "../cli/config/external-config/esm-file/index.js", + import.meta.url, + ), ), ).resolves.toEqual(config); }); test(".js config file", async () => { - const parentDirectory = path.join(__dirname, "../cli/config/rc-js"); + const parentDirectory = new URL("../cli/config/rc-js/", import.meta.url); const config = { trailingComma: "all", @@ -277,7 +279,7 @@ test(".js config file", async () => { "mjs-prettier-config-js-in-type-module", "mjs-prettierrc-js-in-type-module", ]) { - const file = path.join(parentDirectory, directoryName, "foo.js"); + const file = new URL(`${directoryName}/foo.js`, parentDirectory); await expect(prettier.resolveConfig(file)).resolves.toMatchObject(config); } @@ -286,7 +288,7 @@ test(".js config file", async () => { "cjs-prettier-config-js-in-type-module", "cjs-prettierrc-js-in-type-module", ]) { - const file = path.join(parentDirectory, directoryName, "foo.js"); + const file = new URL(`./${directoryName}/foo.js`, parentDirectory); await expect(prettier.resolveConfig(file)).rejects.toThrow(cjsError); } @@ -297,13 +299,13 @@ test(".js config file", async () => { "mjs-prettierrc-js-in-type-commonjs", "mjs-prettierrc-js-in-type-none", ]) { - const file = path.join(parentDirectory, directoryName, "foo.js"); + const file = new URL(`./${directoryName}/foo.js`, parentDirectory); await expect(prettier.resolveConfig(file)).rejects.toThrow(mjsError); } }); test(".cjs config file", async () => { - const parentDirectory = path.join(__dirname, "../cli/config/rc-cjs"); + const parentDirectory = new URL("../cli/config/rc-cjs/", import.meta.url); const config = { trailingComma: "all", @@ -318,13 +320,13 @@ test(".cjs config file", async () => { "prettier-config-cjs-in-type-none", "prettier-config-cjs-in-type-module", ]) { - const file = path.join(parentDirectory, directoryName, "foo.js"); + const file = new URL(`./${directoryName}/foo.js`, parentDirectory); await expect(prettier.resolveConfig(file)).resolves.toMatchObject(config); } }); test(".mjs config file", async () => { - const parentDirectory = path.join(__dirname, "../cli/config/rc-mjs"); + const parentDirectory = new URL("../cli/config/rc-mjs/", import.meta.url); const config = { trailingComma: "all", @@ -339,37 +341,51 @@ test(".mjs config file", async () => { "prettier-config-mjs-in-type-none", "prettier-config-mjs-in-type-module", ]) { - const file = path.join(parentDirectory, directoryName, "foo.js"); + const file = new URL(`./${directoryName}/foo.js`, parentDirectory); await expect(prettier.resolveConfig(file)).resolves.toMatchObject(config); } }); test(".json5 config file", async () => { - const parentDirectory = path.join(__dirname, "../cli/config/rc-json5"); + const parentDirectory = new URL("../cli/config/rc-json5/", import.meta.url); const config = { trailingComma: "all", printWidth: 81, tabWidth: 3, }; - const file = path.join(parentDirectory, "json5/foo.js"); + const file = new URL("./json5/foo.js", parentDirectory); await expect(prettier.resolveConfig(file)).resolves.toMatchObject(config); }); test(".json5 config file(invalid)", async () => { - const parentDirectory = path.join(__dirname, "../cli/config/rc-json5"); - const file = path.join(parentDirectory, "invalid/foo.js"); + const parentDirectory = new URL("../cli/config/rc-json5/", import.meta.url); + const file = new URL("./invalid/foo.js", parentDirectory); const error = /JSON5: invalid end of input at 2:1/; await expect(prettier.resolveConfig(file)).rejects.toThrow(error); }); test("support external module with `module` only `exports`", async () => { - const directory = path.join( - __dirname, - "../cli/config/external-config/esm-package-forbids-require", + const file = new URL( + "../cli/config/external-config/esm-package-forbids-require/index.js", + import.meta.url, ); - const file = path.join(directory, "index.js"); - await expect(prettier.resolveConfig(file)).resolves.toMatchObject({ printWidth: 79, }); }); + +test("API resolveConfig accepts path or URL", async () => { + const fileUrl = new URL("../cli/config/js/file.js", import.meta.url); + const expectedResult = { tabWidth: 8 }; + + const resultByUrl = await prettier.resolveConfig(fileUrl); + const resultByUrlHref = await prettier.resolveConfig(fileUrl.href); + const resultByPath = await prettier.resolveConfig(url.fileURLToPath(fileUrl)); + const resultByRelativePath = await prettier.resolveConfig( + path.relative(process.cwd(), url.fileURLToPath(fileUrl)), + ); + expect(resultByUrl).toMatchObject(expectedResult); + expect(resultByUrlHref).toMatchObject(expectedResult); + expect(resultByPath).toMatchObject(expectedResult); + expect(resultByRelativePath).toMatchObject(expectedResult); +}); diff --git a/tests/integration/__tests__/resolve-config-file.js b/tests/integration/__tests__/resolve-config-file.js index bf1537315..aa3d922ca 100644 --- a/tests/integration/__tests__/resolve-config-file.js +++ b/tests/integration/__tests__/resolve-config-file.js @@ -1,23 +1,23 @@ -import * as urlOrPath from "url-or-path"; +import url from "node:url"; import prettier from "../../config/prettier-entry.js"; test("API resolveConfigFile", async () => { const result = await prettier.resolveConfigFile(); expect(result).toEqual( - urlOrPath.toPath(new URL("../../../.prettierrc", import.meta.url)), + url.fileURLToPath(new URL("../../../.prettierrc", import.meta.url)), ); }); test("API resolveConfigFile accepts path or URL", async () => { const fileUrl = new URL("../cli/config/filepath/foo.js", import.meta.url); - const expectedConfigFilePath = urlOrPath.toPath( + const expectedConfigFilePath = url.fileURLToPath( new URL("./.prettierrc", fileUrl), ); const resultByUrl = await prettier.resolveConfigFile(fileUrl); const resultByUrlHref = await prettier.resolveConfigFile(fileUrl.href); const resultByPath = await prettier.resolveConfigFile( - urlOrPath.toPath(fileUrl), + url.fileURLToPath(fileUrl), ); expect(resultByUrl).toEqual(expectedConfigFilePath); expect(resultByUrlHref).toEqual(expectedConfigFilePath);