The signed release showed only its background colour and never started the sidecar. Tauri's freezePrototype freezes Object.prototype before the bundle runs; d3-color and d3-format (under the visx charts) assign `constructor` / `toString` on plain objects while loading, which throws on a frozen prototype, so the bundle stopped before React drew anything. The saved state was not the cause. - build/freeze-safe.ts: Vite plugin that rewrites those assignments in dependencies into a helper that defines the property when the frozen prototype would refuse it - error boundaries for the window, each screen and each app tab (Copy details, Reload, Reset saved state with a backup); public/boot.js shows the same panel when the bundle fails or the window stays empty - logs/host.log (start, saved state, sidecar start/stop, panics) and logs/webview.log (log_webview), rotated, home as ~, keys, tokens and e-mail addresses replaced - state.json `version` 2 with an explicit 1 -> 2 migration (original kept as state.json.v1.bak), per-entry tolerant parsing, partial settings keep their fields, unreadable files kept aside - folders that are gone: "Folder not found" with Locate... (project_locate) / Remove; portfolio reads skip them - tools/desktop-boot-check.ts: the built window with the release CSP and the frozen prototype, a mocked host from the state fixtures (empty, D1 with folders gone, D3) and the real sidecar, through every screen; in bun run ci, the Forgejo CI workflow and the macOS release
68 lines
4.4 KiB
TypeScript
68 lines
4.4 KiB
TypeScript
// 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 };
|
|
},
|
|
};
|
|
}
|