Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion packages/core/package-manager/src/JSONParseStream.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import type {JSONObject} from '@parcel/types';

import logger from '@parcel/logger';
import {Transform} from 'stream';
import {stripBOM} from '@parcel/utils';

// Transforms chunks of json strings to parsed objects.
// Pair with split2 to parse stream of newline-delimited text.
Expand All @@ -21,7 +22,7 @@ export default class JSONParseStream extends Transform {
try {
let parsed;
try {
parsed = JSON.parse(chunk.toString());
parsed = JSON.parse(stripBOM(chunk.toString()));
} catch (e) {
// Be permissive and ignoreJSON parse errors in case there was
// a non-JSON line in the package manager's stdout.
Expand Down
3 changes: 2 additions & 1 deletion packages/core/utils/src/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import clone from 'clone';
import json5 from 'json5';
import {parse as toml} from '@iarna/toml';
import {LRUCache} from 'lru-cache';
import stripBOM from './stripBOM';

export type ConfigOutput = {|
config: ConfigResult,
Expand Down Expand Up @@ -112,7 +113,7 @@ export async function readConfig(
}

try {
let configContent = await fs.readFile(configFile, 'utf8');
let configContent = stripBOM(await fs.readFile(configFile, 'utf8'));
let config;
if (parse === false) {
config = configContent;
Expand Down
1 change: 1 addition & 0 deletions packages/core/utils/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ export {default as TapStream} from './TapStream';
export {default as urlJoin} from './urlJoin';
export {default as relativeUrl} from './relativeUrl';
export {default as createDependencyLocation} from './dependency-location';
export {default as stripBOM} from './stripBOM';
export {default as debounce} from './debounce';
export {default as throttle} from './throttle';
export {default as openInBrowser} from './openInBrowser';
Expand Down
13 changes: 13 additions & 0 deletions packages/core/utils/src/stripBOM.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
// @flow strict-local

// UTF-8 BOM (Byte Order Mark) is the character \uFEFF at the start of a file.
// Some editors on Windows (like Visual Studio) add this to UTF-8 files.
// This function strips it if present to prevent JSON parse errors.

export default function stripBOM(content: string): string {
// 0xFEFF is the UTF-8 BOM character code
if (content.charCodeAt(0) === 0xfeff) {

Copilot AI Feb 2, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The BOM constant is defined but never used in the code. The implementation directly uses 0xfeff in the comparison on line 9. Consider either using the constant in the comparison (content.charCodeAt(0) === BOM.charCodeAt(0)) or removing the constant definition if it's not needed.

Suggested change
if (content.charCodeAt(0) === 0xfeff) {
if (content.charCodeAt(0) === BOM.charCodeAt(0)) {

Copilot uses AI. Check for mistakes.
return content.slice(1);
}
return content;
}
36 changes: 36 additions & 0 deletions packages/core/utils/test/stripBOM.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
// @flow strict-local

import assert from 'assert';
import stripBOM from '../src/stripBOM';

describe('stripBOM', () => {
it('should strip UTF-8 BOM from the start of a string', () => {
const withBOM = '\uFEFF{"name": "test"}';
const result = stripBOM(withBOM);
assert.strictEqual(result, '{"name": "test"}');
});

it('should not modify strings without BOM', () => {
const withoutBOM = '{"name": "test"}';
const result = stripBOM(withoutBOM);
assert.strictEqual(result, '{"name": "test"}');
});

it('should handle empty strings', () => {
const empty = '';
const result = stripBOM(empty);
assert.strictEqual(result, '');
});

it('should only strip BOM at the beginning', () => {
const bomInMiddle = '{"name": "\uFEFFtest"}';
const result = stripBOM(bomInMiddle);
assert.strictEqual(result, '{"name": "\uFEFFtest"}');
});

it('should allow JSON.parse to work after stripping BOM', () => {
const withBOM = '\uFEFF{"name": "test", "version": "1.0.0"}';
const result = JSON.parse(stripBOM(withBOM));
assert.deepStrictEqual(result, {name: 'test', version: '1.0.0'});
});
});