fix(core): Prevent duplicate zod instances that break npm installs (#33463)

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Matsu
2026-07-02 13:27:38 +00:00
committed by GitHub
co-authored by Claude Opus 4.8
parent 3286414ac0
commit 39a2093dac
9 changed files with 130 additions and 20 deletions
+6
View File
@@ -32,6 +32,12 @@ pre-commit:
skip:
- merge
- rebase
zod_peer_deps_check:
glob: 'packages/{@n8n/api-types,workflow,core,@n8n/agents}/package.json'
run: node scripts/check-zod-peer-deps.mjs
skip:
- merge
- rebase
skill_links_check:
glob: '.agents/skills/**'
run: node scripts/sync-agent-skill-links.mjs --check
+1
View File
@@ -41,6 +41,7 @@
"lint:ci": "turbo run lint lint:styles",
"sync:skill-links": "node scripts/sync-agent-skill-links.mjs",
"check:skill-links": "node scripts/sync-agent-skill-links.mjs --check",
"check:zod-peer-deps": "node scripts/check-zod-peer-deps.mjs",
"optimize-svg": "find ./packages -name '*.svg' ! -name 'pipedrive.svg' -print0 | xargs -0 -P16 -L20 npx svgo",
"setup-backend-module": "node scripts/ensure-zx.mjs && zx scripts/backend-module/setup.mjs",
"start": "node scripts/os-normalize.mjs --dir packages/cli/bin n8n",
+5 -2
View File
@@ -86,9 +86,11 @@
"langsmith": "catalog:",
"undici": "catalog:undici-v7",
"yaml": "catalog:",
"zod": "catalog:",
"zod-to-json-schema": "catalog:"
},
"peerDependencies": {
"zod": "catalog:"
},
"peerDependenciesMeta": {
"langsmith": {
"optional": true
@@ -113,7 +115,8 @@
"nock": "catalog:",
"vite": "catalog:",
"vitest": "catalog:",
"vitest-mock-extended": "catalog:"
"vitest-mock-extended": "catalog:",
"zod": "catalog:"
},
"license": "LicenseRef-n8n-sustainable-use"
}
+5 -2
View File
@@ -30,13 +30,16 @@
"vite": "catalog:",
"vitest": "catalog:",
"vitest-mock-extended": "catalog:",
"minifaker": "1.34.1"
"minifaker": "1.34.1",
"zod": "catalog:"
},
"dependencies": {
"@n8n_io/ai-assistant-sdk": "catalog:",
"n8n-workflow": "workspace:*",
"xss": "catalog:",
"@n8n/permissions": "workspace:*",
"@n8n/permissions": "workspace:*"
},
"peerDependencies": {
"zod": "catalog:"
},
"license": "LicenseRef-n8n-sustainable-use"
@@ -351,7 +351,6 @@ export const baseConfig = tseslint.config(
'**/*.stories.ts',
],
optionalDependencies: false,
peerDependencies: false,
},
],
+2 -2
View File
@@ -50,7 +50,8 @@
"reflect-metadata": "catalog:",
"vite": "catalog:",
"vitest": "catalog:",
"vitest-mock-extended": "catalog:"
"vitest-mock-extended": "catalog:",
"zod": "catalog:"
},
"peerDependencies": {
"zod": "catalog:"
@@ -96,7 +97,6 @@
"uuid": "catalog:",
"winston": "3.14.2",
"xml2js": "catalog:",
"zod": "catalog:",
"qs": "catalog:"
},
"license": "LicenseRef-n8n-sustainable-use"
+3 -1
View File
@@ -92,7 +92,9 @@
"transliteration": "2.3.5",
"uuid": "catalog:",
"xml2js": "catalog:",
"jsonrepair": "catalog:",
"jsonrepair": "catalog:"
},
"peerDependencies": {
"zod": "catalog:"
},
"license": "LicenseRef-n8n-sustainable-use"
+12 -12
View File
@@ -959,9 +959,6 @@ importers:
yaml:
specifier: 'catalog:'
version: 2.8.3
zod:
specifier: 3.25.67
version: 3.25.67
zod-to-json-schema:
specifier: 'catalog:'
version: 3.23.3(zod@3.25.67)
@@ -996,6 +993,9 @@ importers:
vitest-mock-extended:
specifier: 'catalog:'
version: 3.1.0(typescript@6.0.2)(vitest@4.1.9)
zod:
specifier: 3.25.67
version: 3.25.67
packages/@n8n/ai-node-sdk:
dependencies:
@@ -1257,9 +1257,6 @@ importers:
xss:
specifier: 'catalog:'
version: 1.0.15
zod:
specifier: 3.25.67
version: 3.25.67
devDependencies:
'@n8n/config':
specifier: workspace:*
@@ -1285,6 +1282,9 @@ importers:
vitest-mock-extended:
specifier: 'catalog:'
version: 3.1.0(typescript@6.0.2)(vitest@4.1.9)
zod:
specifier: 3.25.67
version: 3.25.67
packages/@n8n/backend-common:
dependencies:
@@ -4393,9 +4393,6 @@ importers:
xml2js:
specifier: 'catalog:'
version: 0.6.2
zod:
specifier: 3.25.67
version: 3.25.67
devDependencies:
'@n8n/playwright-janitor':
specifier: workspace:*
@@ -4457,6 +4454,9 @@ importers:
vitest-mock-extended:
specifier: 'catalog:'
version: 3.1.0(typescript@6.0.2)(vitest@4.1.9)
zod:
specifier: 3.25.67
version: 3.25.67
packages/extensions/insights:
dependencies:
@@ -6417,9 +6417,6 @@ importers:
xml2js:
specifier: 'catalog:'
version: 0.6.2
zod:
specifier: 3.25.67
version: 3.25.67
devDependencies:
'@langchain/core':
specifier: 'catalog:'
@@ -6478,6 +6475,9 @@ importers:
vitest-mock-extended:
specifier: 'catalog:'
version: 3.1.0(typescript@6.0.2)(vitest@4.1.9)
zod:
specifier: 3.25.67
version: 3.25.67
packages:
+96
View File
@@ -0,0 +1,96 @@
#!/usr/bin/env node
/**
* Guards against an npm-only startup break: workspace packages that compose zod
* schemas across package boundaries must declare `zod` as a **peerDependency**,
* not a regular dependency.
*
* `@n8n/api-types` builds a `z.discriminatedUnion` over schemas created in
* `n8n-workflow`. That only works when both packages resolve the *same* physical
* zod instance. In the pnpm monorepo the catalog forces one instance, so it always
* holds locally. But on `npm install n8n`, if these packages each list `zod` in
* `dependencies`, npm can install a separate nested copy per package (langchain
* pins `zod@4` at the root, pushing our `zod@3` down), so the cross-package schema
* composition resolves different instances and n8n crashes at boot. Declaring zod
* as a peerDependency makes the consuming app provide a single shared instance.
*
* This exact regression shipped in 2.28.0 (#32386 moved zod peer→dependencies),
* reverting the fix from #28604. This check keeps zod a peer so it can't recur.
*/
import { existsSync, readFileSync } from 'node:fs';
import { dirname, join, resolve } from 'node:path';
import { fileURLToPath } from 'node:url';
const root = resolve(dirname(fileURLToPath(import.meta.url)), '..');
// Packages that share zod schema objects across the workspace and are runtime
// dependencies of the `n8n` cli. zod must be a peer here, never a plain dependency.
export const PACKAGES_REQUIRING_ZOD_PEER = [
'packages/@n8n/api-types',
'packages/workflow',
'packages/core',
'packages/@n8n/agents',
];
/** Pure, testable core: return a problem string for a manifest, or null if OK. */
export function checkManifest(name, pkg) {
const inDeps = pkg.dependencies?.zod;
const inPeer = pkg.peerDependencies?.zod;
if (inDeps) {
return `${name}: "zod" is in "dependencies" (${inDeps}); it must be a "peerDependency" instead.`;
}
if (!inPeer) {
return `${name}: "zod" is missing from "peerDependencies".`;
}
return null;
}
function main() {
const problems = [];
for (const rel of PACKAGES_REQUIRING_ZOD_PEER) {
const file = join(root, rel, 'package.json');
if (!existsSync(file)) continue; // package moved/renamed — don't hard-fail here
const pkg = JSON.parse(readFileSync(file, 'utf8'));
const problem = checkManifest(pkg.name ?? rel, pkg);
if (problem) problems.push(problem);
}
if (problems.length > 0) {
console.error('');
console.error('ERROR: zod must stay a peerDependency in schema-composing packages.');
console.error('');
console.error('Moving zod into "dependencies" lets npm install duplicate zod copies, which');
console.error('breaks cross-package schema composition and crashes n8n at startup on');
console.error('`npm install n8n` (regressed once in 2.28.0 via #32386).');
console.error('');
for (const p of problems) console.error(` - ${p}`);
console.error('');
console.error(
'Move "zod" to "peerDependencies" (keep it in "devDependencies" for local builds).',
);
console.error('');
process.exit(1);
}
console.log(
`OK: zod is a peerDependency in all ${PACKAGES_REQUIRING_ZOD_PEER.length} schema-composing packages.`,
);
}
// `--self-test` exercises the core logic without touching the workspace.
if (process.argv.includes('--self-test')) {
const good = checkManifest('good', { peerDependencies: { zod: 'catalog:' } }) === null;
const badDep = checkManifest('bad', { dependencies: { zod: 'catalog:' } }) !== null;
const badMissing = checkManifest('missing', {}) !== null;
if (good && badDep && badMissing) {
console.log('self-test passed');
process.exit(0);
}
console.error(`self-test FAILED (good=${good}, badDep=${badDep}, badMissing=${badMissing})`);
process.exit(1);
}
if (import.meta.url === `file://${process.argv[1]}`) {
main();
}