Skip to content
Merged
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
38 changes: 38 additions & 0 deletions apps/desktop/forge.config.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { execFileSync } from "node:child_process";
import { cpSync, existsSync } from "node:fs";
import { resolve } from "node:path";
import { FuseV1Options, FuseVersion } from "@electron/fuses";
import { MakerDMG } from "@electron-forge/maker-dmg";
Expand Down Expand Up @@ -69,7 +71,43 @@ const githubPublisher = process.env.GITHUB_TOKEN
})
: null;

// Inject sentry debug-ids into the staged Vite output AFTER electron-packager
// has copied it to its temp build path. We can't use prePackage because
// Forge runs user hooks before plugin-vite's build, so .vite/ doesn't exist
// yet. We can't use any earlier post-Vite hook because plugin-vite doesn't
// expose one. packageAfterCopy fires once per platform/arch with `buildPath`
// pointing at the staging dir whose contents will be sealed into the asar;
// inject there, then mirror the modified files back to the source `.vite/`
// so `scripts/upload-sourcemaps.mjs` uploads sourcemaps with debug-ids that
// match what got packed.
function injectSentryDebugIds(buildPath: string, sourceRoot: string): void {
if (!process.env.SENTRY_AUTH_TOKEN) {
return;
}
const targets = [".vite/build", ".vite/renderer/main_window"];
for (const t of targets) {
const stagingDir = resolve(buildPath, t);
const sourceDir = resolve(sourceRoot, t);
if (!existsSync(stagingDir)) {
continue;
}
execFileSync(
"pnpm",
["exec", "sentry-cli", "sourcemaps", "inject", stagingDir],
{ cwd: sourceRoot, stdio: "inherit" }
);
if (existsSync(sourceDir)) {
cpSync(stagingDir, sourceDir, { recursive: true, force: true });
}
}
}

const config: ForgeConfig = {
hooks: {
packageAfterCopy: async (_forgeConfig, buildPath) => {
injectSentryDebugIds(buildPath, import.meta.dirname);
},
},
packagerConfig: {
name: "Lightfast",
executableName: "lightfast",
Expand Down
36 changes: 8 additions & 28 deletions apps/desktop/scripts/upload-sourcemaps.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@ for (const name of required) {
// `apps/desktop/src/main/sentry.ts`; keep both in sync.
const releaseName = pkg.name.replace(/^@/, "").replace("/", "-");
const release = `${releaseName}@${pkg.version}+${pkg.buildNumber}`;
const urlPrefix = "app:///";
const buildDir = resolve(desktopRoot, ".vite/build");
const rendererDir = resolve(desktopRoot, ".vite/renderer/main_window");

Expand All @@ -38,33 +37,14 @@ function sentry(args) {
});
}

sentry(["releases", "new", release]);
sentry([
"releases",
"files",
release,
"upload-sourcemaps",
"--url-prefix",
urlPrefix,
"--ext",
"js",
"--ext",
"map",
buildDir,
]);
sentry([
"releases",
"files",
release,
"upload-sourcemaps",
"--url-prefix",
urlPrefix,
"--ext",
"js",
"--ext",
"map",
rendererDir,
]);
// Modern artifact-bundle flow with debug-id matching. `sourcemaps inject`
// runs in `forge.config.ts`'s prePackage hook so the injected //# debugId=
// comments land in the asar; here we only `upload`. Stack frames in Sentry
// resolve via debug-id, which avoids URL-prefix mismatches between the
// uploaded path (`assets/index-*.js`) and the runtime frame
// (`app:///.vite/renderer/main_window/assets/index-*.js`).
sentry(["sourcemaps", "upload", "--release", release, buildDir]);
sentry(["sourcemaps", "upload", "--release", release, rendererDir]);
sentry(["releases", "finalize", release]);

console.log(`Uploaded sourcemaps for release ${release}`);
11 changes: 8 additions & 3 deletions apps/desktop/src/main/windows/factory.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { dirname, join } from "node:path";
import { fileURLToPath } from "node:url";
import { join } from "node:path";
import {
BrowserWindow,
type BrowserWindowConstructorOptions,
Expand All @@ -12,7 +11,13 @@ import { loadWindowState, trackWindowState } from "../window-state";
declare const MAIN_WINDOW_VITE_DEV_SERVER_URL: string | undefined;
declare const MAIN_WINDOW_VITE_NAME: string;

const factoryDir = dirname(fileURLToPath(import.meta.url));
// Vite emits the main bundle as CJS and Rollup does not polyfill
// `import.meta` — both `.url` and `.dirname` get stripped to `{}.<prop>` =
// undefined, which crashes downstream `fileURLToPath(undefined)`. `__dirname`
// is CJS-native and the only reliable way to get the bundle directory inside
// the asar at runtime.
// biome-ignore lint/correctness/noGlobalDirnameFilename: Vite CJS output strips import.meta.*; __dirname is the only working option here.
const factoryDir = __dirname;
const PRELOAD_PATH = join(factoryDir, "preload.js");
const RENDERER_DIST = join(factoryDir, `../renderer/${MAIN_WINDOW_VITE_NAME}`);

Expand Down
5 changes: 5 additions & 0 deletions apps/desktop/src/preload/preload.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
// Installs the @sentry/electron IPC bridge on the contextBridge so the
// renderer-side SDK routes events through main via `sentry-ipc:` instead of
// fetching the ingest URL directly (which the renderer CSP blocks). Pair:
// src/main/sentry.ts (main init) and src/renderer/src/main.ts (renderer init).
import "@sentry/electron/preload";
import { contextBridge, type IpcRendererEvent, ipcRenderer } from "electron";
import type { AcceleratorName } from "../shared/accelerators";
import {
Expand Down
7 changes: 6 additions & 1 deletion apps/desktop/src/renderer/src/main.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
import * as Sentry from "@sentry/browser";
// @sentry/electron/renderer routes events through `sentry-ipc:` (CSP-bypass
// scheme registered by @sentry/electron/main + bridged by the
// `@sentry/electron/preload` import in src/preload/preload.ts). The plain
// @sentry/browser SDK fetches the ingest URL directly, which the renderer
// CSP blocks — events silently drop.
import * as Sentry from "@sentry/electron/renderer";
import "./react/entry";
import {
ACCELERATORS,
Expand Down
Loading