diff --git a/app/DevelopmentService.main.ts b/app/DevelopmentService.main.ts new file mode 100644 index 000000000..9fb7fb004 --- /dev/null +++ b/app/DevelopmentService.main.ts @@ -0,0 +1,37 @@ +// Copyright 2024 Signal Messenger, LLC +// SPDX-License-Identifier: AGPL-3.0-only + +import { join } from 'node:path'; +import { pathToFileURL } from 'node:url'; +import { protocol, net } from 'electron'; + +import { isPathInside } from '../ts/util/isPathInside.node.ts'; +import { getAppRootDir } from '../ts/util/appRootDir.main.ts'; + +type DevelopmentServiceOptions = Readonly<{ + isDevelopment: boolean; +}>; + +export function start({ isDevelopment }: DevelopmentServiceOptions): void { + if (!isDevelopment) { + protocol.handle('bundles', () => { + return new Response('Unavailable in production', { + status: 404, + }); + }); + return; + } + + const BUNDLES_DIR = join(getAppRootDir(), 'bundles'); + + // Serve source maps + protocol.handle('bundles', req => { + const url = new URL(req.url); + const path = join(BUNDLES_DIR, url.pathname.slice(1)); + if (!isPathInside(path, BUNDLES_DIR)) { + throw new Error(`Invalid source map request: ${path}`); + } + + return net.fetch(pathToFileURL(path).toString()); + }); +} diff --git a/app/main.main.ts b/app/main.main.ts index 14f745f95..1a59da22a 100644 --- a/app/main.main.ts +++ b/app/main.main.ts @@ -86,6 +86,7 @@ import { SystemTraySettingCache } from './SystemTraySettingCache.node.ts'; import { OptionalResourceService } from './OptionalResourceService.main.ts'; import { EmojiService } from './EmojiService.main.ts'; import { AssetService } from './AssetService.main.ts'; +import * as DevelopmentService from './DevelopmentService.main.ts'; import { SystemTraySetting, shouldMinimizeToSystemTray, @@ -2092,6 +2093,9 @@ app.on('ready', async () => { ); await EmojiService.create(resourceService); AssetService.create(resourceService); + DevelopmentService.start({ + isDevelopment: development && !app.isPackaged, + }); if (!resolvedTranslationsLocale) { preferredSystemLocales = resolveCanonicalLocales( diff --git a/background.html b/background.html index fc503312d..1f3925040 100644 --- a/background.html +++ b/background.html @@ -16,7 +16,7 @@ http-equiv="Content-Security-Policy" content="default-src 'none'; child-src 'self'; - connect-src 'self' https: wss: attachment:; + connect-src 'self' https: wss: attachment: bundles:; font-src 'self' asset:; form-action 'self'; frame-src 'none'; diff --git a/preload.wrapper.ts b/preload.wrapper.ts index d05df1d67..bc3e12f37 100644 --- a/preload.wrapper.ts +++ b/preload.wrapper.ts @@ -23,7 +23,7 @@ try { } } -const source = readFileSync(srcPath); +const source = readFileSync(srcPath, 'utf8'); window.preloadCompileStartTime = Date.now(); @@ -38,27 +38,22 @@ window.preloadCompileStartTime = Date.now(); // path-independent value for reproducibility, and otherwise use full absolute // path in the packaged app. const filename = process.env.GENERATE_PRELOAD_CACHE - ? 'preload.bundle.js' + ? 'bundles/preload/main.js' : srcPath; -const script = new Script( - `(function(require, __dirname, exports){${source.toString()}})`, - { - filename, - lineOffset: 0, - cachedData, - importModuleDynamically: constants.USE_MAIN_CONTEXT_DEFAULT_LOADER, - } -); +// Note: we wrap with (function(require, __dirname, exports){}) in +// `rolldown.config.ts` to preserve correct offsets in the sourcemap. +const script = new Script(source, { + filename, + lineOffset: 0, + cachedData, + importModuleDynamically: constants.USE_MAIN_CONTEXT_DEFAULT_LOADER, +}); const { cachedDataRejected } = script; const fn = script.runInThisContext({ - filename, - lineOffset: 0, - columnOffset: 0, displayErrors: true, - importModuleDynamically: constants.USE_MAIN_CONTEXT_DEFAULT_LOADER, }); // See `scripts/generate-preload-cache.mjs` diff --git a/rolldown.config.ts b/rolldown.config.ts index d77649e8e..9fc5fb268 100644 --- a/rolldown.config.ts +++ b/rolldown.config.ts @@ -72,7 +72,7 @@ const defaults = { plugins: [ { name: 'NODE_ENV', - async transform(code, id) { + transform(code, id) { if (id.endsWith('.json')) { return; } @@ -82,13 +82,12 @@ const defaults = { return; } - const { code: newCode } = await transform(id, code, { + return transform(id, code, { define: { 'process.env.NODE_ENV': isProd ? '"production"' : '"development"', }, + sourcemap: !isProd, }); - - return { code: newCode }; }, }, ], @@ -101,6 +100,28 @@ const defaults = { generatedCode: { symbols: false, }, + sourcemap: !isProd, + sourcemapBaseUrl: 'bundles:///', + postBanner: ({ fileName }) => { + // See preload.wrapper.ts + if (fileName === 'preload/main.js') { + return '(function(require, __dirname, exports){'; + } + + return ''; + }, + postFooter: ({ fileName }) => { + const lines = new Array(); + + // See preload.wrapper.ts + if (fileName === 'preload/main.js') { + lines.push('})'); + } + + lines.push(`//# sourceURL=bundles:///${fileName}`); + + return lines.join('\n'); + }, }, watch: { clearScreen: false,