diff --git a/.github/workflows/prod-test.yml b/.github/workflows/prod-test.yml index 40cce9719..9dfb7e996 100644 --- a/.github/workflows/prod-test.yml +++ b/.github/workflows/prod-test.yml @@ -94,6 +94,7 @@ jobs: if: ${{ matrix.node == '14' || matrix.node == '16' }} run: | yarn set version 3 + yarn config set httpRetry 10 cat .yarnrc.yml - name: Setup Node.js diff --git a/changelog_unreleased/api/17331.md b/changelog_unreleased/api/17331.md index 1c222e246..91bf28dc9 100644 --- a/changelog_unreleased/api/17331.md +++ b/changelog_unreleased/api/17331.md @@ -1,8 +1,15 @@ -#### Add `isSupported` function support for `languages` API (#17331 by @JounQin) +#### Add `isSupported` function support for `languages` API (#17331 by @JounQin, #17490 by @fisker) Previously, `languages` API for custom plugin only supported to infer parser based on the file basename or extension. -Prettier main added `isSupported: (file: string) => boolean` function to allow plugin check if file is supported based on the full path (eg: files in a specific directory), the `file` parameter could be a normal path or a url string like `file:///C:/test.txt`. +Prettier main added `isSupported: ({ filepath: string }) => boolean` function to allow plugin check if file is supported based on the full path (eg: files in a specific directory). + +:::note + +Prettier can not ensure that `filepath` exists on disk.\ +When using from APIs(eg: `prettier.format()`), Prettier can not ensure it's a valid path either. + +::: If no `isSupported` provided, it just behaviors the same way as before. @@ -11,7 +18,7 @@ export const languages = [ { name: "foo", parsers: ["foo"], - isSupported: (file) => file.includes(".foo"), + isSupported: ({ filepath }) => filepath.includes(".foo"), }, ]; ``` diff --git a/docs/api.md b/docs/api.md index fe7d29b9a..8d6f1443f 100644 --- a/docs/api.md +++ b/docs/api.md @@ -124,11 +124,18 @@ The support information looks like this: filenames?: string[]; linguistLanguageId?: number; vscodeLanguageIds?: string[]; - isSupported?(file: string): boolean; + isSupported?({ filepath: string }): boolean; }>; } ``` +:::note + +Prettier can not ensure that `filepath` exists on disk.\ +When using from APIs(eg: `prettier.format()`), Prettier can not ensure it's a valid path either. + +::: + ## Custom Parser API (removed) diff --git a/package.json b/package.json index f4314162c..6cf2eb795 100644 --- a/package.json +++ b/package.json @@ -52,6 +52,7 @@ "collapse-white-space": "1.0.6", "css-units-list": "2.1.0", "dashify": "2.0.0", + "deno-path-from-file-url": "0.0.2", "diff": "8.0.1", "editorconfig": "0.15.3", "emoji-regex": "10.4.0", diff --git a/src/index.d.ts b/src/index.d.ts index c8f10e1e7..b215fa42d 100644 --- a/src/index.d.ts +++ b/src/index.d.ts @@ -680,7 +680,7 @@ export interface SupportLanguage { linguistLanguageId?: number | undefined; vscodeLanguageIds?: string[] | undefined; interpreters?: string[] | undefined; - isSupported?: ((file: string) => boolean) | undefined; + isSupported?: (({ filepath: string }) => boolean) | undefined; } export interface SupportOptionRange { diff --git a/src/utils/infer-parser.js b/src/utils/infer-parser.js index a9bd5d185..b45220e4a 100644 --- a/src/utils/infer-parser.js +++ b/src/utils/infer-parser.js @@ -1,5 +1,6 @@ import getInterpreter from "./get-interpreter.js"; import isNonEmptyArray from "./is-non-empty-array.js"; +import toPath from "./universal-to-path.js"; /** @import {Options, SupportLanguage} from "../index.js" */ @@ -93,14 +94,22 @@ function getLanguageByIsSupported(languages, file) { return; } - /* - We can't use `url.fileURLToPath` here since this module should work in browsers too - `URL#pathname` won't work either since `new URL('file:///C:/path/to/file').pathname` - equals to `/C:/path/to/file`, try to improve this part in future - */ - file = String(file); + // Ideally, we should only allow `URL` with `file:` protocol and + // string starts with `file:`, but `URL` is missing in some environments + // eg: `node:vm` + if (String(file).startsWith("file:")) { + try { + file = toPath(file); + } catch { + return; + } + } - return languages.find(({ isSupported }) => isSupported?.(file)); + if (typeof file !== "string") { + return; + } + + return languages.find(({ isSupported }) => isSupported?.({ filepath: file })); } /** diff --git a/src/utils/universal-to-path.js b/src/utils/universal-to-path.js new file mode 100644 index 000000000..e7d97d27a --- /dev/null +++ b/src/utils/universal-to-path.js @@ -0,0 +1,6 @@ +import { fileURLToPath } from "node:url"; +import fromFileUrl from "deno-path-from-file-url"; + +export default process.env.PRETTIER_TARGET === "universal" + ? fromFileUrl + : fileURLToPath; diff --git a/tests/integration/__tests__/infer-parser.js b/tests/integration/__tests__/infer-parser.js index 2d331bc01..cbac9cfd5 100644 --- a/tests/integration/__tests__/infer-parser.js +++ b/tests/integration/__tests__/infer-parser.js @@ -1,3 +1,4 @@ +import url from "node:url"; import prettier from "../../config/prettier-entry.js"; import jestPathSerializer from "../path-serializer.js"; @@ -236,4 +237,92 @@ describe("isSupported", () => { stderr: "", write: [], }); + + test("API", async () => { + const fileUrl = new URL("foo.unknown", import.meta.url); + const filePath = url.fileURLToPath(fileUrl); + expect(await getIsSupportedReceivedFilepath({ filepath: fileUrl })).toBe( + filePath, + ); + expect( + await getIsSupportedReceivedFilepath({ filepath: fileUrl.href }), + ).toBe(filePath); + expect(await getIsSupportedReceivedFilepath({ filepath: filePath })).toBe( + filePath, + ); + + // Relative path + expect( + await getIsSupportedReceivedFilepath({ filepath: "./foo.unknown" }), + ).toBe("./foo.unknown"); + + expect( + await getIsSupportedReceivedFilepath({ + filepath: "", + }), + ).toBeUndefined(); + + expect( + await getIsSupportedReceivedFilepath({ + filepath: Buffer.from("foo.unknown"), + }), + ).toBeUndefined(); + + expect( + await getIsSupportedReceivedFilepath({ + filepath: { toString: () => "foo.unknown" }, + }), + ).toBeUndefined(); + + expect( + await getIsSupportedReceivedFilepath({ + filepath: new (class { + toString() { + return "foo.unknown"; + } + })(), + }), + ).toBeUndefined(); + + expect( + await getIsSupportedReceivedFilepath({ filepath: "file://%0" }), + ).toBeUndefined(); + + expect( + await getIsSupportedReceivedFilepath({ + filepath: new URL("https://example.com/foo.unknown"), + }), + ).toBeUndefined(); + + expect( + await getIsSupportedReceivedFilepath({ + filepath: "https://example.com/foo.unknown", + }), + ).toBe("https://example.com/foo.unknown"); + }); }); + +const getIsSupportedReceivedFilepath = async (options) => { + let received; + try { + await prettier.format("foo", { + plugins: [ + { + languages: [ + { + isSupported({ filepath }) { + received = filepath; + }, + }, + ], + }, + ], + ...options, + }); + } catch (error) { + if (error.name !== "UndefinedParserError") { + throw error; + } + } + return received; +}; diff --git a/tests/integration/plugins/languages/is-supported.js b/tests/integration/plugins/languages/is-supported.js index 6b8350406..60e847050 100644 --- a/tests/integration/plugins/languages/is-supported.js +++ b/tests/integration/plugins/languages/is-supported.js @@ -8,12 +8,13 @@ const languages = [ { name: "language-name-does-not-matter", parsers: [PARSER_NAME], - isSupported(file) { - if (!path.isAbsolute(file)) { + isSupported({filepath}) { + // This is only true when running from CLI + if (!path.isAbsolute(filepath)) { throw new Error("Unexpected non absolute path"); } - return /(?[\\/])\.husky\k[^\\/]+$/u.test(file); + return /(?[\\/])\.husky\k[^\\/]+$/u.test(filepath); }, }, ]; diff --git a/yarn.lock b/yarn.lock index 573c1f45d..d91fafa72 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3470,6 +3470,13 @@ __metadata: languageName: node linkType: hard +"deno-path-from-file-url@npm:0.0.2": + version: 0.0.2 + resolution: "deno-path-from-file-url@npm:0.0.2" + checksum: 10/d2204f36d0b3b88c22c4fa0f6d2000c837d2915cb6d7dc48e20a0ee20881646343fa0b580b646596a039d6c386124b14b3bc9534b42e62a01ec991a3d3a674b0 + languageName: node + linkType: hard + "depd@npm:2.0.0, depd@npm:^2.0.0": version: 2.0.0 resolution: "depd@npm:2.0.0" @@ -7428,6 +7435,7 @@ __metadata: cspell: "npm:9.0.0" css-units-list: "npm:2.1.0" dashify: "npm:2.0.0" + deno-path-from-file-url: "npm:0.0.2" diff: "npm:8.0.1" editorconfig: "npm:0.15.3" emoji-regex: "npm:10.4.0"