From e10f6d2e2ce9e20cff5780fa05e90de6f0f2213b Mon Sep 17 00:00:00 2001 From: "paseo-bot[bot]" <266920839+paseo-bot[bot]@users.noreply.github.com> Date: Thu, 1 Oct 2026 14:02:49 +0200 Subject: [PATCH] Let plugin client code import packages that declare only main (#5838) The plugin compiler builds client code with esbuild's neutral platform, which reads no package.json entry fields, so any dependency without an exports map failed with Could not resolve. Read module and main for the client target, as the server target already does through the node platform. Co-authored-by: paseo-bot[bot] <266920839+paseo-bot[bot]@users.noreply.github.com> --- .../src/server/plugins/compiler.test.ts | 27 +++++++++++++++++++ .../server/src/server/plugins/compiler.ts | 4 +++ 2 files changed, 31 insertions(+) diff --git a/packages/server/src/server/plugins/compiler.test.ts b/packages/server/src/server/plugins/compiler.test.ts index dca7d3e3e2..facba48fdb 100644 --- a/packages/server/src/server/plugins/compiler.test.ts +++ b/packages/server/src/server/plugins/compiler.test.ts @@ -455,6 +455,33 @@ export type Value = string;`, expect(serverBundle).not.toContain("Client contribution"); }); + it.each(["main", "module"])( + "builds a dependency that declares only %s into both runtimes", + async (field) => { + const entries = await createSplitPlugin(); + const dependency = path.join(entries.directory, "node_modules/legacy-entry"); + await mkdir(dependency, { recursive: true }); + await writeFile( + path.join(dependency, "package.json"), + JSON.stringify({ name: "legacy-entry", [field]: "lib/entry.js" }), + ); + await mkdir(path.join(dependency, "lib")); + await writeFile( + path.join(dependency, "lib/entry.js"), + 'exports.label = "legacy entry contribution";', + ); + await writeFile( + path.join(entries.directory, "shared/labels.ts"), + 'import { label } from "legacy-entry"; export const clientLabel = label; export const serverLabel = label;', + ); + + const { clientBundle, serverBundle } = await compilePlugin(entries); + + expect(clientBundle).toContain("legacy entry contribution"); + expect(serverBundle).toContain("legacy entry contribution"); + }, + ); + it("uses the automatic JSX runtime without a React import", async () => { const entries = await createSplitPlugin(); const { clientBundle } = await compilePlugin(entries); diff --git a/packages/server/src/server/plugins/compiler.ts b/packages/server/src/server/plugins/compiler.ts index 80dbb1b7b0..15df395d06 100644 --- a/packages/server/src/server/plugins/compiler.ts +++ b/packages/server/src/server/plugins/compiler.ts @@ -391,6 +391,10 @@ async function compileTarget(entryPath: string, target: PluginBuildTarget): Prom jsx: "automatic", platform: target === "server" ? "node" : "neutral", target: target === "server" ? "node20" : "es2020", + // The neutral platform reads no package.json entry fields, so packages without + // `exports` would not resolve. Read the same fields as esbuild's browser platform + // minus `browser`, since the bundle also runs in React Native. + mainFields: target === "client" ? ["module", "main"] : undefined, // Metro lowers async syntax before Hermes sees app code. Plugin client bundles bypass Metro, // so apply the same compatibility transform before the app evaluates them from source. supported: target === "client" ? { "async-await": false } : undefined,