@oh-my-pi/hashline 16.2.7 → 16.2.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,12 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [16.2.8] - 2026-06-30
6
+
7
+ ### Fixed
8
+
9
+ - Fixed hashline writes preserving UTF-8 BOM bytes when the host text decoder hides the leading `U+FEFF`. ([#3867](https://github.com/can1357/oh-my-pi/issues/3867))
10
+
5
11
  ## [16.2.6] - 2026-06-29
6
12
 
7
13
  ### Fixed
@@ -37,6 +37,8 @@ export declare function isNotFound(error: unknown): boolean;
37
37
  export declare abstract class Filesystem {
38
38
  /** Read the file's full text content. Throw on missing file. */
39
39
  abstract readText(path: string): Promise<string>;
40
+ /** Read raw bytes for backends whose text is a direct decode of persisted bytes. */
41
+ readBinary?(path: string): Promise<Uint8Array | undefined>;
40
42
  /** Validate that `path` is writable before a prepared batch starts committing. */
41
43
  preflightWrite(_path: string, _options?: PreflightWriteOptions): Promise<void>;
42
44
  /** Persist `content` at `path`. Returns the actual final text that was written. */
@@ -98,6 +100,7 @@ export declare class InMemoryFilesystem extends Filesystem {
98
100
  */
99
101
  export declare class NodeFilesystem extends Filesystem {
100
102
  readText(path: string): Promise<string>;
103
+ readBinary(path: string): Promise<Uint8Array>;
101
104
  writeText(path: string, content: string): Promise<WriteResult>;
102
105
  delete(path: string): Promise<void>;
103
106
  move(from: string, to: string, content?: string): Promise<void>;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "@oh-my-pi/hashline",
4
- "version": "16.2.7",
4
+ "version": "16.2.8",
5
5
  "description": "Hashline: a compact, line-anchored patch language and applier. Pluggable FS/IO so it works over disk, in-memory, or any custom backend.",
6
6
  "homepage": "https://omp.sh",
7
7
  "author": "Can Boluk",
package/src/fs.ts CHANGED
@@ -65,6 +65,9 @@ export abstract class Filesystem {
65
65
  /** Read the file's full text content. Throw on missing file. */
66
66
  abstract readText(path: string): Promise<string>;
67
67
 
68
+ /** Read raw bytes for backends whose text is a direct decode of persisted bytes. */
69
+ readBinary?(path: string): Promise<Uint8Array | undefined>;
70
+
68
71
  /** Validate that `path` is writable before a prepared batch starts committing. */
69
72
  async preflightWrite(_path: string, _options?: PreflightWriteOptions): Promise<void> {}
70
73
 
@@ -196,6 +199,15 @@ export class NodeFilesystem extends Filesystem {
196
199
  return file.text();
197
200
  }
198
201
 
202
+ async readBinary(path: string): Promise<Uint8Array> {
203
+ try {
204
+ return await fs.readFile(path);
205
+ } catch (error) {
206
+ if (isNotFound(error)) throw new NotFoundError(path, error);
207
+ throw error;
208
+ }
209
+ }
210
+
199
211
  async writeText(path: string, content: string): Promise<WriteResult> {
200
212
  await Bun.write(path, content);
201
213
  return { text: content };
package/src/patcher.ts CHANGED
@@ -148,6 +148,10 @@ function mergeWarnings(...sources: ReadonlyArray<readonly string[] | undefined>)
148
148
  return out;
149
149
  }
150
150
 
151
+ function hasUtf8Bom(bytes: Uint8Array | undefined): boolean {
152
+ return bytes !== undefined && bytes.length >= 3 && bytes[0] === 0xef && bytes[1] === 0xbb && bytes[2] === 0xbf;
153
+ }
154
+
151
155
  function assertUniqueCanonicalPaths(prepared: readonly PreparedSection[]): void {
152
156
  const seen = new Map<string, string>();
153
157
  for (const entry of prepared) {
@@ -295,7 +299,8 @@ export class Patcher {
295
299
  throw new Error(`MV destination is the same as ${target.path}.`);
296
300
  }
297
301
 
298
- const { bom, text } = stripBom(read.rawContent);
302
+ const { bom: bomFromText, text } = stripBom(read.rawContent);
303
+ const bom = bomFromText || (await this.#readBinaryBom(target.path));
299
304
  const lineEnding = detectLineEnding(text);
300
305
  const normalized = normalizeToLF(text);
301
306
 
@@ -453,6 +458,12 @@ export class Patcher {
453
458
  };
454
459
  }
455
460
 
461
+ async #readBinaryBom(path: string): Promise<string> {
462
+ if (!this.fs.readBinary) return "";
463
+ const bytes = await this.fs.readBinary(path);
464
+ return hasUtf8Bom(bytes) ? "\uFEFF" : "";
465
+ }
466
+
456
467
  async #tryRead(path: string): Promise<{ exists: boolean; rawContent: string }> {
457
468
  try {
458
469
  const content = await this.fs.readText(path);