// Tauri's `freezePrototype: true` (tauri.conf.json) runs Object.freeze(Object.prototype) before any page script. In // strict code, assigning a property that an object only inherits from the frozen Object.prototype (`constructor`, // `toString`, ...) then throws "Cannot assign to read only property": the JavaScript "override mistake". d3-color and // d3-format (under @visx/scale) do exactly that while their modules load, so the bundle stopped before React rendered // anything and the release window showed only its background colour. // // This plugin rewrites every `x.constructor = v`, `x.toString = v`, ... in dependencies into a call that does the same // assignment and, only when that assignment hits the frozen prototype, defines the property on `x` itself instead // (Object.defineProperty is allowed there). The release boot check (tools/desktop-boot-check.ts) loads the built // bundle with the prototype frozen, as Tauri does, and fails on any uncaught error. import type { Plugin } from 'vite'; /** The writable data properties of Object.prototype that freezing turns read-only for every plain object. */ export const FROZEN_NAMES = ['constructor', 'toString', 'toLocaleString', 'valueOf', 'hasOwnProperty', 'isPrototypeOf', 'propertyIsEnumerable'] as const; const QUICK = new RegExp(`\\.(?:${FROZEN_NAMES.join('|')})\\s*=(?!=)`); export const HELPER = '__storeboundFreezeSafeSet'; // Appended (function declarations hoist), so a leading "use strict" directive stays first. Works in strict and sloppy // code: when `k` is only inherited from a frozen (read-only) data property, define it on `o`; otherwise assign. const HELPER_SRC = `\nfunction ${HELPER}(o, k, v) { if (o !== null && (typeof o === 'object' || typeof o === 'function') && !Object.prototype.hasOwnProperty.call(o, k)) { for (var p = Object.getPrototypeOf(o); p !== null; p = Object.getPrototypeOf(p)) { var d = Object.getOwnPropertyDescriptor(p, k); if (!d) continue; if ('value' in d && !d.writable) { Object.defineProperty(o, k, { value: v, writable: true, enumerable: true, configurable: true }); return v; } break; } } o[k] = v; return v; }\n`; interface Node { type: string; start: number; end: number; [k: string]: unknown } function walk(node: unknown, visit: (n: Node) => void): void { if (!node || typeof node !== 'object') return; if (Array.isArray(node)) { for (const n of node) walk(n, visit); return; } const n = node as Node; if (typeof n.type === 'string') visit(n); for (const k of Object.keys(n)) if (k !== 'type' && k !== 'start' && k !== 'end') walk(n[k], visit); } /** Rewrites the assignments in one module's code; null when there is nothing to change. */ export function rewrite(code: string, parse: (code: string) => unknown): string | null { if (!QUICK.test(code)) return null; const edits: { start: number; end: number; text: (inner: (s: number, e: number) => string) => string }[] = []; walk(parse(code), n => { if (n.type !== 'AssignmentExpression' || n.operator !== '=') return; const left = n.left as Node & { object: Node; property: Node & { name?: string }; computed: boolean; optional?: boolean }; if (left.type !== 'MemberExpression' || left.computed || left.object.type === 'Super') return; if (!(FROZEN_NAMES as readonly string[]).includes(left.property.name ?? '')) return; const right = n.right as Node; edits.push({ start: n.start, end: n.end, text: inner => `${HELPER}(${inner(left.object.start, left.object.end)}, ${JSON.stringify(left.property.name)}, ${inner(right.start, right.end)})` }); }); if (!edits.length) return null; // outermost edits only carry their inner text through `inner`, which applies nested edits recursively edits.sort((a, b) => a.start - b.start || b.end - a.end); const render = (s: number, e: number): string => { let out = '', at = s; for (const ed of edits) { if (ed.start < at || ed.end > e || ed.start < s) continue; out += code.slice(at, ed.start) + ed.text(render); at = ed.end; } return out + code.slice(at, e); }; return render(0, code.length) + HELPER_SRC; } export function freezeSafe(): Plugin { return { name: 'storebound-freeze-safe', transform(code, id) { const file = id.split('?')[0]!; if (!/node_modules/.test(file) || !/\.(?:m?js|cjs)$/.test(file)) return null; const out = rewrite(code, c => this.parse(c)); return out === null ? null : { code: out, map: null }; }, }; }