mirror of
https://github.com/n8n-io/n8n.git
synced 2026-08-30 18:01:23 +08:00
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:
@@ -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();
|
||||
}
|
||||
Reference in New Issue
Block a user