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: {