diff --git a/changelog_unreleased/api/20127.md b/changelog_unreleased/api/20127.md new file mode 100644 index 000000000..f0d2b5368 --- /dev/null +++ b/changelog_unreleased/api/20127.md @@ -0,0 +1,19 @@ +#### Support configuration files with UTF-8 BOM (#20127 by @fisker) + + +```console +# Input +$ cat .prettierrc.json +{"semi":false} +$ prettier example.js --check + +# Prettier stable +Invalid configuration for file "example.js": +JSON Error in .prettierrc.json: +Unexpected token "" (\u{feff}) + +# Prettier main +Checking formatting... +[warn] example.js +[warn] Code style issues found in the above file. Run Prettier with --write to fix. +``` diff --git a/src/config/prettier-config/loaders.js b/src/config/prettier-config/loaders.js index a4fa4757f..404af1813 100644 --- a/src/config/prettier-config/loaders.js +++ b/src/config/prettier-config/loaders.js @@ -2,10 +2,12 @@ import { pathToFileURL } from "node:url"; import { parse as parseJson5 } from "@bybrave/json5"; import parseJson from "parse-json"; import { parse as parseToml } from "smol-toml"; +import { stripBom } from "../../utilities/bom.js"; import readFile from "../../utilities/read-file.js"; async function readJson(file) { - const content = await readFile(file); + let content = await readFile(file); + content = stripBom(content); try { return parseJson(content); } catch (/** @type {any} */ error) { @@ -70,7 +72,8 @@ async function loadYaml(file) { } async function loadToml(file) { - const content = await readFile(file); + let content = await readFile(file); + content = stripBom(content); try { return parseToml(content); } catch (/** @type {any} */ error) { diff --git a/src/main/core.js b/src/main/core.js index 04b0e20bd..c8352016e 100644 --- a/src/main/core.js +++ b/src/main/core.js @@ -12,6 +12,7 @@ import { printDocToDebug, printDocToString as printDocToStringWithoutNormalizeOptions, } from "../document/index.js"; +import { addBom, hasBom as stringHasBom } from "../utilities/bom.js"; import getAlignmentSize from "../utilities/get-alignment-size.js"; import { prepareToPrint, printAstToDoc } from "./ast-to-doc.js"; import getCursorLocation from "./get-cursor-node.js"; @@ -21,8 +22,6 @@ import parseText from "./parse.js"; import { resolveParser } from "./parser-and-printer.js"; import { calculateRange } from "./range.js"; -const BOM = "\uFEFF"; - const CURSOR = Symbol("cursor"); async function coreFormat(originalText, opts, addAlignmentSize = 0) { @@ -271,9 +270,9 @@ function normalizeInputAndOptions(text, options) { options, ); - const hasBOM = text.charAt(0) === BOM; + const hasBom = stringHasBom(text); - if (hasBOM) { + if (hasBom) { text = text.slice(1); cursorOffset--; rangeStart--; @@ -297,7 +296,7 @@ function normalizeInputAndOptions(text, options) { } return { - hasBOM, + hasBom, text, options: normalizeIndexes(text, { ...options, @@ -320,7 +319,7 @@ async function hasIgnorePragma(text, options) { } async function formatWithCursor(originalText, originalOptions) { - let { hasBOM, text, options } = normalizeInputAndOptions( + let { hasBom, text, options } = normalizeInputAndOptions( originalText, await normalizeFormatOptions(originalOptions), ); @@ -353,8 +352,8 @@ async function formatWithCursor(originalText, originalOptions) { result = await coreFormat(text, options); } - if (hasBOM) { - result.formatted = BOM + result.formatted; + if (hasBom) { + result.formatted = addBom(result.formatted); if (result.cursorOffset >= 0) { result.cursorOffset++; diff --git a/src/utilities/bom.js b/src/utilities/bom.js new file mode 100644 index 000000000..5e0c9b6e2 --- /dev/null +++ b/src/utilities/bom.js @@ -0,0 +1,12 @@ +const BOM = "\uFEFF"; + +/** @type {(string: string) => boolean} */ +const hasBom = (string) => string.charAt(0) === BOM; + +/** @type {(string: string) => string} */ +const stripBom = (string) => (hasBom(string) ? string.slice(1) : string); + +/** @type {(string: string) => string} */ +const addBom = (string) => BOM + string; + +export { addBom, hasBom, stripBom }; diff --git a/tests/unit/config-loader.js b/tests/unit/config-loader.js new file mode 100644 index 000000000..01e4175c3 --- /dev/null +++ b/tests/unit/config-loader.js @@ -0,0 +1,36 @@ +import fs from "node:fs/promises"; +import path from "node:path"; +import { temporaryDirectory as getTemporaryDirectory } from "tempy"; +import loadConfig from "../../src/config/prettier-config/load-config.js"; + +describe("Support BOM", () => { + const files = [ + { filename: "package.json", content: '{"prettier": {"tabWidth": 5}}' }, + { filename: ".prettierrc", content: "tabWidth: 5" }, + { filename: ".prettierrc.json", content: '{"tabWidth": 5}' }, + { filename: ".prettierrc.yml", content: "tabWidth: 5" }, + { filename: ".prettierrc.json5", content: "{tabWidth: 5}" }, + { filename: ".prettierrc.toml", content: "tabWidth = 5" }, + ]; + + const directory = getTemporaryDirectory(); + const expected = { tabWidth: 5 }; + beforeAll(async () => { + await Promise.all( + files.map(({ filename, content }) => + fs.writeFile(path.join(directory, filename), `\ufeff${content}`), + ), + ); + }); + afterAll(async () => { + await fs.rm(directory, { recursive: true, force: true }); + }); + + for (const { filename } of files) { + test(filename, async () => { + expect(await loadConfig(path.join(directory, filename))).toStrictEqual( + expected, + ); + }); + } +});