From f023393a183f0c2a9743ed9887f79bc107a2813f Mon Sep 17 00:00:00 2001 From: luke Date: Thu, 22 Oct 2026 14:37:09 +0800 Subject: [PATCH] perf(server): minify plugin client bundles Plugin client bundles are sent and evaluated as the esbuild text. On a mobile link that text is the catalog download. Minify whitespace and syntax after the Hermes interop getters are made eager. Identifier minify stays off so that replacement still matches, and server bundles stay readable. Co-authored-by: RoundRedDot --- .../src/server/plugins/compiler.test.ts | 36 +++++++++++++++++++ .../server/src/server/plugins/compiler.ts | 20 ++++++++++- 2 files changed, 55 insertions(+), 1 deletion(-) diff --git a/packages/server/src/server/plugins/compiler.test.ts b/packages/server/src/server/plugins/compiler.test.ts index 6f694fbaa3..8604d39b8c 100644 --- a/packages/server/src/server/plugins/compiler.test.ts +++ b/packages/server/src/server/plugins/compiler.test.ts @@ -567,6 +567,42 @@ export default function contribute(client) { expect(clientBundle).toContain("__async"); }); + it("minifies the client bundle after Hermes interop getters are eager", async () => { + const entries = await createSplitPlugin(); + await writeFile( + entries.client, + `import { useState } from "react"; +const label = "\\u2013"; +export default function contribute(client) { + void useState; + void label; + client.addCommandCenterItem({ id: "open", title: "Open", icon: "Scan", context: "global", onSelect() {} }); + return () => undefined; +}`, + ); + await writeFile( + entries.server, + `import { z } from "zod"; +export default function contribute() { + void z.object({}); + return () => undefined; +}`, + ); + + const { clientBundle, serverBundle } = await compilePlugin(entries); + if (clientBundle === null || serverBundle === null) { + throw new Error("compilePlugin returned no bundle"); + } + + expect(clientBundle).not.toContain("get: () => from[key]"); + expect(clientBundle).not.toContain("get:()=>from[key]"); + expect(clientBundle).toContain("value:from[key]"); + expect(clientBundle.split("").every((char) => char.charCodeAt(0) <= 0x7f)).toBe(true); + expect(serverBundle).toContain("value: from[key]"); + expect(serverBundle).not.toContain("value:from[key]"); + expect(clientBundle.split("\n").length).toBeLessThan(serverBundle.split("\n").length); + }); + it("rejects node imports from the client entry", async () => { const entries = await createSplitPlugin(); await writeFile( diff --git a/packages/server/src/server/plugins/compiler.ts b/packages/server/src/server/plugins/compiler.ts index 4cf89a902c..e163bb0f20 100644 --- a/packages/server/src/server/plugins/compiler.ts +++ b/packages/server/src/server/plugins/compiler.ts @@ -313,6 +313,22 @@ function makeHermesInteropEager(code: string): string { return code.replaceAll("get: () => from[key]", "value: from[key]"); } +async function minifyClientBundle(code: string): Promise { + const { transform } = loadEsbuild(); + // Minify after the eager replace. Whitespace minify rewrites the getter this + // replace matches, and identifier minify can delete the helper entirely. + const result = await transform(code, { + minifyWhitespace: true, + minifySyntax: true, + minifyIdentifiers: false, + target: "es2020", + supported: { "async-await": false }, + charset: "ascii", + legalComments: "none", + }); + return result.code; +} + function runtimeSpecifierError( specifier: string, target: PluginBuildTarget | "shared", @@ -424,7 +440,9 @@ async function compileTarget(entryPath: string, target: PluginBuildTarget): Prom checkSharedDependencies(result.metafile.inputs, pluginDirectory); const output = result.outputFiles[0]?.text; if (!output) throw new Error(`Plugin ${target} compilation produced no output`); - return wrapCommonJsBundle(makeHermesInteropEager(output)); + const eager = makeHermesInteropEager(output); + const prepared = target === "client" ? await minifyClientBundle(eager) : eager; + return wrapCommonJsBundle(prepared); } export async function compilePlugin(entryPaths: {