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"