mirror of
https://github.com/logancyang/obsidian-copilot.git
synced 2026-07-22 07:50:24 +00:00
fix(mobile): import Buffer from the buffer polyfill so it works in WebView (#2443)
Mobile Obsidian runs on Capacitor + WebView (no Node.js runtime), so the bare `Buffer` global is undefined. On iOS WebKit this surfaces as the runtime error "Can't find variable: Buffer" the moment the image processing path fires. Desktop is unaffected because Electron exposes Node globals. Two PRs in 3.3.0 introduced direct `Buffer.from(...)` calls in image-handling paths assuming "Buffer is global": - #2403 rewrote src/utils/base64.ts from btoa/atob to bare Buffer.from - #2401 did the same in src/utils.ts safeFetch arrayBuffer branch and src/LLMProviders/BedrockChatModel.ts decodeBase64ToUint8Array Result: any image attachment on mobile fails with "failed to send image, please try again". Confirmed via user log: ERROR Error sending message: Can't find variable: Buffer The fix is an explicit `import { Buffer } from "buffer"` in each affected file. The `buffer` npm package (already in dependencies) is a browser-compatible polyfill that esbuild bundles into main.js, so the same Buffer code path works on desktop (where it's effectively the Electron-provided Buffer) and on mobile (where it's the polyfill). Two lint rules complicate this: - Obsidian's official scorecard flags btoa/atob as deprecated and asks for Buffer.from. That rules out reverting to btoa/atob. - The repo's own `import/no-nodejs-modules` rule blocks `import from "buffer"`. Suppressed inline at each import site with justification, because `buffer` here is the npm polyfill (in package.json dependencies), not Node's built-in being used at runtime. Files touched: - src/utils/base64.ts (arrayBufferToBase64 / base64ToArrayBuffer) This is the call site the image processor hits and the path encryptionService.ts uses for WEBCRYPTO_PREFIX (mobile encryption), so mobile encryption is also fixed transitively. - src/utils.ts (safeFetch arrayBuffer fallback) - src/LLMProviders/BedrockChatModel.ts (decodeBase64ToUint8Array) Regression test: - src/utils/base64.test.ts deletes globalThis.Buffer before exercising the helpers to confirm the imported polyfill binding is what gets used (not a bare global reference). Catches any future regression where someone replaces the import with a bare global usage. Out of scope: - src/encryptionService.ts has Buffer.from references but they sit inside desktop-only branches (`getSafeStorage().isEncryptionAvailable()` / DESKTOP_PREFIX) that mobile never enters. Touching them from a mobile hotfix would expand blast radius unnecessarily. - src/LLMProviders/BedrockChatModel.ts:835 is already guarded with `typeof Buffer !== "undefined"` so mobile skips it; left alone. Bundle size unchanged at 3.3 MB (buffer polyfill is small). Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
280d95ebde
commit
7587b4fa4d
4 changed files with 61 additions and 1 deletions
|
|
@ -1,3 +1,8 @@
|
|||
// Reason: `buffer` is the npm polyfill (browser-compatible), bundled by esbuild
|
||||
// so the same Buffer code path works on desktop (Electron) and mobile (WebView).
|
||||
// eslint-disable-next-line import/no-nodejs-modules
|
||||
import { Buffer } from "buffer";
|
||||
|
||||
import {
|
||||
BaseChatModel,
|
||||
type BaseChatModelCallOptions,
|
||||
|
|
@ -793,6 +798,8 @@ export class BedrockChatModel extends BaseChatModel<BedrockChatModelCallOptions>
|
|||
|
||||
private decodeBase64ToUint8Array(encoded: string): Uint8Array | null {
|
||||
try {
|
||||
// Reason: Buffer (from the `buffer` polyfill imported above) is the
|
||||
// cross-platform path — bare global Buffer is undefined in mobile WebView.
|
||||
return new Uint8Array(Buffer.from(encoded, "base64"));
|
||||
} catch {
|
||||
return null;
|
||||
|
|
|
|||
|
|
@ -1,3 +1,8 @@
|
|||
// Reason: `buffer` is the npm polyfill (browser-compatible), bundled by esbuild
|
||||
// so the same Buffer code path works on desktop (Electron) and mobile (WebView).
|
||||
// eslint-disable-next-line import/no-nodejs-modules
|
||||
import { Buffer } from "buffer";
|
||||
|
||||
import { Document } from "@/chainFactory";
|
||||
import { ChainType } from "@/chainType";
|
||||
import {
|
||||
|
|
@ -883,6 +888,8 @@ export async function safeFetch(
|
|||
return response.arrayBuffer;
|
||||
}
|
||||
const base64 = response.text.replace(/^data:.*;base64,/, "");
|
||||
// Reason: Buffer (from the `buffer` polyfill imported above) is the
|
||||
// cross-platform path — bare global Buffer is undefined in mobile WebView.
|
||||
const buf = Buffer.from(base64, "base64");
|
||||
return buf.buffer.slice(buf.byteOffset, buf.byteOffset + buf.byteLength) as ArrayBuffer;
|
||||
},
|
||||
|
|
|
|||
37
src/utils/base64.test.ts
Normal file
37
src/utils/base64.test.ts
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
import { arrayBufferToBase64, base64ToArrayBuffer } from "./base64";
|
||||
|
||||
describe("base64 utils", () => {
|
||||
it("round-trips a small ArrayBuffer", () => {
|
||||
const original = new Uint8Array([0, 1, 2, 3, 127, 128, 255]).buffer;
|
||||
const encoded = arrayBufferToBase64(original);
|
||||
const decoded = base64ToArrayBuffer(encoded);
|
||||
expect(new Uint8Array(decoded)).toEqual(new Uint8Array(original));
|
||||
});
|
||||
|
||||
it("matches a known base64 fixture", () => {
|
||||
const bytes = new TextEncoder().encode("Copilot 🚀");
|
||||
expect(arrayBufferToBase64(bytes.buffer as ArrayBuffer)).toBe("Q29waWxvdCDwn5qA");
|
||||
});
|
||||
|
||||
// Reason: regression guard for the 3.3.0 mobile bug. Mobile Obsidian's
|
||||
// WebView runtime has no Node.js globals — bare `Buffer` is undefined.
|
||||
// The helpers must import Buffer from the `buffer` npm polyfill (bundled
|
||||
// by esbuild) and not rely on `globalThis.Buffer`. This test deletes the
|
||||
// global to make sure the imported binding is what's actually being used.
|
||||
it("works without relying on globalThis.Buffer (mobile WebView simulation)", () => {
|
||||
// eslint-disable-next-line obsidianmd/no-global-this -- jsdom test needs to mutate the actual global runtime, not a per-window scope
|
||||
const g = globalThis as { Buffer?: unknown };
|
||||
const originalBuffer = g.Buffer;
|
||||
try {
|
||||
delete g.Buffer;
|
||||
const bytes = new Uint8Array([10, 20, 30, 40]).buffer;
|
||||
const encoded = arrayBufferToBase64(bytes);
|
||||
const decoded = base64ToArrayBuffer(encoded);
|
||||
expect(new Uint8Array(decoded)).toEqual(new Uint8Array(bytes));
|
||||
} finally {
|
||||
if (originalBuffer !== undefined) {
|
||||
g.Buffer = originalBuffer;
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
|
@ -1,7 +1,16 @@
|
|||
// Reason: explicit import from the `buffer` npm polyfill (a browser-compatible
|
||||
// drop-in, not a runtime use of Node's built-in). Mobile Obsidian's WebView has
|
||||
// no Node globals, so bare `Buffer` throws "Can't find variable: Buffer" on iOS
|
||||
// WebKit. esbuild bundles this polyfill into main.js so the same code path
|
||||
// works on both desktop and mobile.
|
||||
// eslint-disable-next-line import/no-nodejs-modules
|
||||
import { Buffer } from "buffer";
|
||||
|
||||
export function arrayBufferToBase64(buffer: ArrayBuffer): string {
|
||||
return Buffer.from(buffer).toString("base64");
|
||||
}
|
||||
|
||||
export function base64ToArrayBuffer(base64: string): ArrayBuffer {
|
||||
return new Uint8Array(Buffer.from(base64, "base64")).buffer;
|
||||
const buf = Buffer.from(base64, "base64");
|
||||
return buf.buffer.slice(buf.byteOffset, buf.byteOffset + buf.byteLength) as ArrayBuffer;
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue