diff --git a/config/scripts/renderer-boot-graph.mjs b/config/scripts/renderer-boot-graph.mjs index a0168cb8f1b..d99c5468e78 100644 --- a/config/scripts/renderer-boot-graph.mjs +++ b/config/scripts/renderer-boot-graph.mjs @@ -84,6 +84,13 @@ export function findForbiddenBootPayloads(rendererDir, payloads) { return violations } +// V8 keeps a whole script as UTF-16 when any char is above U+00FF, doubling that boot chunk's memory. +export function findTwoByteBootChunks(rendererDir) { + return readRendererBootGraph(rendererDir) + .chunks.filter((chunk) => /[\u0100-\uffff]/.test(fs.readFileSync(chunk.file, 'utf8'))) + .map((chunk) => path.basename(chunk.file)) +} + export function verifyRendererBootGraph(root = process.cwd()) { const rendererDir = path.join(root, RENDERER_BUILD_DIR) const { chunks, totalBytes } = readRendererBootGraph(rendererDir) @@ -91,6 +98,15 @@ export function verifyRendererBootGraph(root = process.cwd()) { console.log( `Renderer boot graph: ${chunks.length} chunks, ${(totalBytes / 1024).toFixed(1)} KB minified.` ) + const twoByteChunks = findTwoByteBootChunks(rendererDir) + if (twoByteChunks.length > 0) { + console.error('Boot chunks contain chars above U+00FF that output.charset cannot escape:') + for (const chunk of twoByteChunks) { + console.error(` ${chunk}`) + } + console.error('Escape them in the source (e.g. \\u2014 inside String.raw regex sources).') + return 1 + } if (violations.length === 0) { return 0 } diff --git a/electron.vite.config.ts b/electron.vite.config.ts index 5c5f98f9322..98bb16bd983 100644 --- a/electron.vite.config.ts +++ b/electron.vite.config.ts @@ -227,6 +227,8 @@ export const electronViteConfig: UserConfig = { preserveEntrySignatures: 'exports-only', // Electron and sidecar launchers require stable CommonJS paths despite Rolldown’s SSR ESM default. output: { + // V8 stores a whole script as UTF-16 when any char is above U+00FF; escaping keeps sources one-byte (half the memory). + charset: 'ascii', format: 'cjs', entryFileNames: '[name].js', chunkFileNames: 'chunks/[name]-[hash].js' @@ -260,6 +262,10 @@ export const electronViteConfig: UserConfig = { build: { externalizeDeps: { exclude: ['zod'] + }, + rollupOptions: { + // Unminified preload keeps dependency JSDoc whose em dashes would force two-byte storage in every renderer. + output: { charset: 'ascii', comments: { legal: true, annotation: true, jsdoc: false } } } } }, @@ -284,7 +290,10 @@ export const electronViteConfig: UserConfig = { createViteCacheRetentionPlugin() ], worker: { - format: 'es' + format: 'es', + rollupOptions: { + output: { charset: 'ascii' } + } }, build: { manifest: true, @@ -296,6 +305,7 @@ export const electronViteConfig: UserConfig = { rollupOptions: { // Shared chunks must not import HTML entries that mount another React root. preserveEntrySignatures: 'strict', + output: { charset: 'ascii' }, input: { index: resolve('src/renderer/index.html'), popout: resolve('src/renderer/popout.html'), diff --git a/src/shared/prose-url-boundary.ts b/src/shared/prose-url-boundary.ts index 0f12c31bdf6..f7071462576 100644 --- a/src/shared/prose-url-boundary.ts +++ b/src/shared/prose-url-boundary.ts @@ -1,6 +1,7 @@ // Why not all non-ascii punctuation: titles keep `・`, `–` and `·` unencoded in a url // (`/wiki/ジョン・スミス`), so only brackets, quotes, spaces and sentence marks end one. -const PROSE_URL_TERMINATOR_SOURCE = String.raw`(?![\0-\x7f])[\p{Ps}\p{Pe}\p{Pi}\p{Pf}\p{Z}、。,.!?:;。、…‥—]` +// Escaped CJK sentence marks 、。,.!?:;。、…‥— keep this module's chunk one-byte in V8. +const PROSE_URL_TERMINATOR_SOURCE = String.raw`(?![\0-\x7f])[\p{Ps}\p{Pe}\p{Pi}\p{Pf}\p{Z}\u3001\u3002\uFF0C\uFF0E\uFF01\uFF1F\uFF1A\uFF1B\uFF61\uFF64\u2026\u2025\u2014]` const NOT_PROSE_URL_TERMINATOR = `(?!${PROSE_URL_TERMINATOR_SOURCE})`