mirror of
https://github.com/n8n-io/n8n.git
synced 2026-10-12 02:19:43 +00:00
test: Centralize the zod CJS-pin alias in shared Vitest config (#33627)
Co-authored-by: Claude Opus 4.8 (1M context) <[email protected]>
This commit is contained in:
@@ -2,21 +2,11 @@ import { createVitestConfig } from '@n8n/vitest-config/node';
|
||||
import path from 'node:path';
|
||||
import { mergeConfig } from 'vite';
|
||||
|
||||
export default mergeConfig(createVitestConfig({}), {
|
||||
// `zod` is pinned to its CJS build (via `pinCjs`) so `schema instanceof ZodSchema` in
|
||||
// src/converters/tool.ts holds against zod objects created in externalized CJS workspace
|
||||
// deps. See `cjsPinAliases` in @n8n/vitest-config/node for the rationale.
|
||||
export default mergeConfig(createVitestConfig({}, { pinCjs: ['zod'] }), {
|
||||
resolve: {
|
||||
alias: [
|
||||
{ find: 'src', replacement: path.resolve(__dirname, './src') },
|
||||
// zod has dual ESM/CJS exports (two separate `ZodType` class identities).
|
||||
// Workspace deps CJS-require zod while tests ESM-import it, so
|
||||
// `schema instanceof ZodSchema` in src/converters/tool.ts fails across the
|
||||
// two. Pin the top-level `zod` import to the CJS file so both share one
|
||||
// module instance. `require.resolve` follows zod's `require` export condition,
|
||||
// so it tracks the installed (catalog) version automatically. Subpaths like
|
||||
// `zod/v4` keep their normal resolution.
|
||||
{
|
||||
find: /^zod$/,
|
||||
replacement: require.resolve('zod'),
|
||||
},
|
||||
],
|
||||
alias: [{ find: 'src', replacement: path.resolve(__dirname, './src') }],
|
||||
},
|
||||
});
|
||||
|
||||
@@ -5,22 +5,13 @@ import { createVitestConfigWithDecorators } from '@n8n/vitest-config/node-decora
|
||||
// Shared base config for both the unit (vite.config.ts) and integration
|
||||
// (vitest.config.integration.ts) runs. Test include/exclude is intentionally
|
||||
// left to the extending configs so they don't merge-concatenate each other's globs.
|
||||
export const baseConfig = mergeConfig(createVitestConfigWithDecorators({}), {
|
||||
resolve: {
|
||||
alias: [
|
||||
{ find: '@', replacement: path.resolve(__dirname, './src') },
|
||||
// zod has dual ESM/CJS exports (`./dist/esm/index.js` for `import`,
|
||||
// `./dist/cjs/index.js` for `require`) — two separate files with two separate
|
||||
// `ZodType` classes. Workspace deps CJS-require zod, test files ESM-import it, and
|
||||
// `instanceof z.ZodX` checks in source fail between them. Pin the top-level `zod`
|
||||
// import to the CJS file so both code paths share a single module instance.
|
||||
{
|
||||
find: /^zod$/,
|
||||
replacement: path.resolve(
|
||||
__dirname,
|
||||
'../../../node_modules/.pnpm/[email protected]/node_modules/zod/dist/cjs/index.js',
|
||||
),
|
||||
},
|
||||
],
|
||||
export const baseConfig = mergeConfig(
|
||||
// Pin `zod` to its CJS build so `instanceof z.ZodError` holds against the externalized
|
||||
// CJS dist. See `cjsPinAliases` in @n8n/vitest-config/node for the rationale.
|
||||
createVitestConfigWithDecorators({}, { pinCjs: ['zod'] }),
|
||||
{
|
||||
resolve: {
|
||||
alias: [{ find: '@', replacement: path.resolve(__dirname, './src') }],
|
||||
},
|
||||
},
|
||||
});
|
||||
);
|
||||
|
||||
@@ -84,22 +84,12 @@ function rewriteLazyRequireForTests(): Plugin {
|
||||
};
|
||||
}
|
||||
|
||||
export default mergeConfig(createVitestConfig({}), {
|
||||
// `zod` is pinned to its CJS build (via `pinCjs`) so `instanceof` checks (e.g. in
|
||||
// sanitize-mcp-schemas) hold against zod objects created in externalized CJS workspace
|
||||
// deps. See `cjsPinAliases` in @n8n/vitest-config/node for the rationale.
|
||||
export default mergeConfig(createVitestConfig({}, { pinCjs: ['zod'] }), {
|
||||
plugins: [rewriteLazyRequireForTests()],
|
||||
resolve: {
|
||||
alias: [
|
||||
{ find: '@', replacement: path.resolve(__dirname, './src') },
|
||||
// zod has dual ESM/CJS exports (two separate `ZodType` class identities).
|
||||
// Workspace deps CJS-require zod while test files ESM-import it, so
|
||||
// `instanceof` checks (e.g. in sanitize-mcp-schemas) fail across the two
|
||||
// module instances. Pin the top-level `zod` import to the CJS file so all
|
||||
// code paths share one instance. `require.resolve` follows zod's `require`
|
||||
// export condition, so it tracks the installed (catalog) version
|
||||
// automatically. Subpaths like `zod/v4` resolve normally.
|
||||
{
|
||||
find: /^zod$/,
|
||||
replacement: require.resolve('zod'),
|
||||
},
|
||||
],
|
||||
alias: [{ find: '@', replacement: path.resolve(__dirname, './src') }],
|
||||
},
|
||||
});
|
||||
|
||||
@@ -2,8 +2,16 @@ import { mergeConfig } from 'vitest/config';
|
||||
import type { InlineConfig } from 'vitest/node';
|
||||
import { createVitestConfig } from './node.js';
|
||||
|
||||
export const createVitestConfigWithDecorators = (options: InlineConfig = {}) => {
|
||||
const baseConfig = createVitestConfig(options);
|
||||
export const createVitestConfigWithDecorators = (
|
||||
options: InlineConfig = {},
|
||||
// `pinCjs` is opt-in per package (default none): pinning a dep to CJS also forces its
|
||||
// transitive dual-build deps to CJS (e.g. `n8n-workflow` drags luxon's `DateTime`
|
||||
// identity along), which can break unrelated `instanceof`/`expect.any` checks. Only
|
||||
// packages whose own tests need a specific dep unified should pass it. See
|
||||
// `cjsPinAliases` for the full rationale.
|
||||
{ pinCjs = [] }: { pinCjs?: string[] } = {},
|
||||
) => {
|
||||
const baseConfig = createVitestConfig(options, { pinCjs });
|
||||
return mergeConfig(baseConfig, {
|
||||
test: {
|
||||
server: {
|
||||
|
||||
@@ -1,8 +1,32 @@
|
||||
import { createRequire } from 'node:module';
|
||||
import { join } from 'node:path';
|
||||
import type { Alias } from 'vite';
|
||||
import { coverageConfigDefaults, defineConfig } from 'vitest/config';
|
||||
import type { InlineConfig } from 'vitest/node';
|
||||
|
||||
import { coverageExcludes } from './coverage-excludes.js';
|
||||
|
||||
/**
|
||||
* Pin dual-build (ESM+CJS) deps to their CJS entry so a single class identity is shared
|
||||
* across test/source (which ESM-import the dep) and externalized workspace dist (which
|
||||
* `require()`s the CJS build) — otherwise cross-boundary `instanceof` checks fail. Vite's
|
||||
* externalizer always prefers a dep's `import` export condition, so the two sides land on
|
||||
* different files; rewriting the bare specifier to a concrete CJS path is the only way to
|
||||
* unify them. Resolved from the consuming package (cwd) so it tracks the catalog version.
|
||||
* The exact-match regex leaves subpaths (e.g. `zod/v4`) on their normal resolution. Deps
|
||||
* not resolvable from the package are skipped (harmless — they can't be imported there).
|
||||
*/
|
||||
export const cjsPinAliases = (deps: string[], from: string = process.cwd()): Alias[] => {
|
||||
const req = createRequire(join(from, 'noop.js'));
|
||||
return deps.flatMap((dep) => {
|
||||
try {
|
||||
return [{ find: new RegExp(`^${dep}$`), replacement: req.resolve(dep) }];
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Per-suite fork-pool cap, applied only in CI.
|
||||
*
|
||||
@@ -50,7 +74,13 @@ export const createBaseInlineConfig = (options: InlineConfig = {}): InlineConfig
|
||||
...options,
|
||||
});
|
||||
|
||||
export const createVitestConfig = (options: InlineConfig = {}) =>
|
||||
defineConfig({ test: createBaseInlineConfig(options) });
|
||||
export const createVitestConfig = (
|
||||
options: InlineConfig = {},
|
||||
{ pinCjs = [] }: { pinCjs?: string[] } = {},
|
||||
) =>
|
||||
defineConfig({
|
||||
test: createBaseInlineConfig(options),
|
||||
...(pinCjs.length ? { resolve: { alias: cjsPinAliases(pinCjs) } } : {}),
|
||||
});
|
||||
|
||||
export const vitestConfig = createVitestConfig();
|
||||
|
||||
@@ -3,37 +3,21 @@ import { createVitestConfigWithDecorators } from '@n8n/vitest-config/node-decora
|
||||
import path from 'node:path';
|
||||
|
||||
export default mergeConfig(
|
||||
createVitestConfigWithDecorators({
|
||||
globalSetup: ['./test/setup.ts'],
|
||||
setupFiles: ['./test/setup-mocks.ts'],
|
||||
}),
|
||||
createVitestConfigWithDecorators(
|
||||
{
|
||||
globalSetup: ['./test/setup.ts'],
|
||||
setupFiles: ['./test/setup-mocks.ts'],
|
||||
},
|
||||
// Pin `zod` and `n8n-workflow` to their CJS build so cross-boundary `instanceof`
|
||||
// (`ZodType`, `UserError`) holds against the externalized CJS dist. See
|
||||
// `cjsPinAliases` in @n8n/vitest-config/node for the rationale.
|
||||
{ pinCjs: ['zod', 'n8n-workflow'] },
|
||||
),
|
||||
{
|
||||
resolve: {
|
||||
alias: [
|
||||
{ find: '@', replacement: path.resolve(__dirname, './src') },
|
||||
{ find: '@test', replacement: path.resolve(__dirname, './test') },
|
||||
// zod has dual ESM/CJS exports (`./dist/esm/index.js` for `import`,
|
||||
// `./dist/cjs/index.js` for `require`) — two separate files with two separate
|
||||
// `ZodType` classes. @n8n/config dist CJS-requires zod, test files ESM-import
|
||||
// it, and `instanceof` fails between them. Pin the top-level `zod` import to the
|
||||
// CJS file so both code paths share a single module instance. `require.resolve`
|
||||
// follows zod's `require` export condition, so it tracks the installed (catalog)
|
||||
// version automatically. Subpaths like `zod/v4` keep their normal resolution.
|
||||
{
|
||||
find: /^zod$/,
|
||||
replacement: require.resolve('zod'),
|
||||
},
|
||||
// `n8n-workflow` has dual ESM/CJS builds (`./dist/esm/index.js` for `import`,
|
||||
// `./dist/cjs/index.js` for `require`), each with its own `UserError` class.
|
||||
// `@n8n/backend-network` is loaded from its CJS dist and `require`s the CJS copy,
|
||||
// so `SsrfBlockedIpError extends UserError` (CJS), while test files ESM-import
|
||||
// `UserError` (ESM) — and `instanceof` fails between them. Pin the top-level
|
||||
// `n8n-workflow` import to the CJS file so both code paths share one module
|
||||
// instance. `require.resolve` follows the `require` export condition.
|
||||
{
|
||||
find: /^n8n-workflow$/,
|
||||
replacement: require.resolve('n8n-workflow'),
|
||||
},
|
||||
],
|
||||
},
|
||||
oxc: {
|
||||
|
||||
Reference in New Issue
Block a user