Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions packages/cli/script/build.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ import { gzip } from "node:zlib";
import { build as esbuild } from "esbuild";
import { uploadSourcemaps } from "../src/lib/api/sourcemaps.js";
import { injectDebugId, PLACEHOLDER_DEBUG_ID } from "./debug-id.js";
import { zodEnglishLocalesPlugin } from "./lib/zod-locales-plugin.js";
import { textImportPlugin } from "./text-import-plugin.js";

const gzipAsync = promisify(gzip);
Expand Down Expand Up @@ -160,6 +161,7 @@ async function bundleJs(): Promise<boolean> {
},
plugins: [
textImportPlugin,
zodEnglishLocalesPlugin,
// Transform _require() → require() so esbuild resolves lazy relative
// requires at bundle time. In tsx dev mode, _require is a file-local
// createRequire(import.meta.url) that resolves relative to the file.
Expand Down
2 changes: 2 additions & 0 deletions packages/cli/script/bundle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { build, type Plugin } from "esbuild";
import pkg from "../package.json";
import { uploadSourcemaps } from "../src/lib/api/sourcemaps.js";
import { injectDebugId, PLACEHOLDER_DEBUG_ID } from "./debug-id.js";
import { zodEnglishLocalesPlugin } from "./lib/zod-locales-plugin.js";
import { buildCoreDeclarations, SDK_TYPES_PATH } from "./sdk-declarations.js";
import { textImportPlugin } from "./text-import-plugin.js";

Expand Down Expand Up @@ -253,6 +254,7 @@ const zlibNamespaceShimPlugin: Plugin = {

const plugins: Plugin[] = [
sentrySourcemapPlugin,
zodEnglishLocalesPlugin,
textImportPlugin,
requireAliasPlugin,
];
Expand Down
56 changes: 56 additions & 0 deletions packages/cli/script/lib/zod-locales-plugin.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
/**
* esbuild plugin that bundles only Zod's English error messages.
*
* Zod 4's `core`, `classic`, and `mini` entry points all re-export
* `locales/index.js`, which statically imports every locale (about 260 KB
* minified). Zod only uses English unless code calls
* `z.config(z.locales.xx())`, and nothing we bundle does. This plugin swaps
* that index for one that exports only `en`, so `z.locales.en` still works
* and error messages are unchanged. `classic/schemas.js` imports `en.js`
* directly to set the default locale, so that path is not touched.
*
* Shared by the CLI bundles (`script/bundle.ts`, `script/build.ts`), which
* inline `@sentry/mcp-core` for `sentry mcp`.
*/

import { existsSync } from "node:fs";
import { dirname, join } from "node:path";
import type { Plugin } from "esbuild";

const ZOD_LOCALES_NS = "zod-en-locales";
const LOCALES_INDEX_FILTER = /[\\/]locales[\\/]index\.c?js$/;
const ANY_FILTER = /.*/;
/** Matches a file inside an installed `zod` package (npm or pnpm layout). */
const ZOD_PACKAGE_RE = /[\\/]node_modules[\\/]zod[\\/]/;

export const zodEnglishLocalesPlugin: Plugin = {
name: "zod-english-locales",
setup(build) {
build.onResolve({ filter: LOCALES_INDEX_FILTER }, (args) => {
if (!ZOD_PACKAGE_RE.test(`${args.resolveDir}/`)) {
return;
}
// `zod/locales/index.js` re-exports `../v4/locales/index.js`, so both
// resolve here. Keep the importer's module format: `.cjs` for require.
const indexPath = join(args.resolveDir, args.path);
const enPath = join(
dirname(indexPath),
indexPath.endsWith(".cjs") ? "en.cjs" : "en.js",
);
if (!existsSync(enPath)) {
// Fail loudly if Zod changes its layout, rather than shipping a
// locales namespace without English.
return {
errors: [{ text: `Zod English locale not found at ${enPath}` }],
};
}
return { path: enPath, namespace: ZOD_LOCALES_NS };
});

build.onLoad({ filter: ANY_FILTER, namespace: ZOD_LOCALES_NS }, (args) => ({
contents: `export { default as en } from ${JSON.stringify(args.path)};`,
resolveDir: dirname(args.path),
loader: "js",
}));
},
};
119 changes: 119 additions & 0 deletions packages/cli/test/script/zod-locales-plugin.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
/**
* Tests for the esbuild plugin that bundles only Zod's English locale.
*
* Builds a small entry against the real `zod` package (resolved from
* `@sentry/mcp-core`, which depends on it) with and without the plugin, then
* runs both bundles and compares their error messages.
*/

import { createRequire } from "node:module";
import { dirname, join } from "node:path";
import { fileURLToPath } from "node:url";
import { runInNewContext } from "node:vm";
import { build } from "esbuild";
import { describe, expect, test } from "vitest";
import { zodEnglishLocalesPlugin } from "../../script/lib/zod-locales-plugin.js";

const MCP_CORE_DIR = join(
dirname(fileURLToPath(import.meta.url)),
"../../../mcp-core",
);

/** `import` resolves Zod's ESM build and `require` its CJS build. */
const IMPORTS = {
esm: `import { z } from "zod";`,
cjs: `const { z } = require("zod");`,
};

const BODY = `
const issues = [
z.string().safeParse(1),
z.object({ n: z.number().min(5) }).safeParse({ n: 1 }),
z.enum(["a", "b"]).safeParse("c"),
].flatMap((r) => r.error.issues.map((i) => i.message));
module.exports = { issues, locales: Object.keys(z.locales) };
`;

type BundleResult = {
bytes: number;
issues: string[];
locales: string[];
};

async function bundleZod(
format: "cjs" | "esm",
withPlugin: boolean,
): Promise<BundleResult> {
const result = await build({
stdin: {
contents: IMPORTS[format] + BODY,
resolveDir: MCP_CORE_DIR,
loader: "js",
},
bundle: true,
write: false,
platform: "node",
format: "cjs",
logLevel: "silent",
plugins: withPlugin ? [zodEnglishLocalesPlugin] : [],
});
const code = result.outputFiles[0]?.text ?? "";
const sandbox = {
module: { exports: {} },
require: createRequire(import.meta.url),
};
runInNewContext(code, sandbox);
const { issues, locales } = sandbox.module.exports as Omit<
BundleResult,
"bytes"
>;
return { bytes: code.length, issues, locales };
}

describe("zodEnglishLocalesPlugin", () => {
test.each(["esm", "cjs"] as const)(
"keeps English messages and drops other locales (%s)",
async (format) => {
const full = await bundleZod(format, false);
const english = await bundleZod(format, true);

expect(english.issues).toEqual(full.issues);
expect(english.issues[0]).toBe(
"Invalid input: expected string, received number",
);
expect(full.locales).toContain("fr");
expect(english.locales).toEqual(["en"]);
// All other locales together are well over 100 KB unminified.
expect(full.bytes - english.bytes).toBeGreaterThan(100_000);
},
);

test("leaves non-Zod locales/index.js imports alone", async () => {
const result = await build({
stdin: {
contents: `import { en } from "./locales/index.js"; module.exports = en;`,
resolveDir: "/nonexistent",
loader: "js",
},
bundle: true,
write: false,
logLevel: "silent",
plugins: [
zodEnglishLocalesPlugin,
{
name: "fake-locales",
setup(b) {
b.onResolve({ filter: /locales/ }, () => ({
path: "fake",
namespace: "fake",
}));
b.onLoad({ filter: /.*/, namespace: "fake" }, () => ({
contents: `export const en = "fake";`,
}));
},
},
],
});
expect(result.outputFiles[0]?.text).toContain('"fake"');
});
});
Loading