feat: Modernize build and testing for workflow package (no-changelog) (#16771)

This commit is contained in:
Alex Grozav
2025-06-30 20:02:16 +03:00
committed by GitHub
parent d1d5412bfb
commit c76d94b364
70 changed files with 733 additions and 486 deletions

View File

@@ -139,7 +139,7 @@ jobs:
with: with:
projects: ${{ secrets.SENTRY_TASK_RUNNER_PROJECT }} projects: ${{ secrets.SENTRY_TASK_RUNNER_PROJECT }}
version: n8n@${{ needs.publish-to-npm.outputs.release }} version: n8n@${{ needs.publish-to-npm.outputs.release }}
sourcemaps: packages/core/dist packages/workflow/dist packages/@n8n/task-runner/dist sourcemaps: packages/core/dist packages/workflow/dist/esm packages/@n8n/task-runner/dist
trigger-release-note: trigger-release-note:
name: Trigger a release note name: Trigger a release note

View File

@@ -8,5 +8,5 @@
}, },
"include": ["**/*.ts"], "include": ["**/*.ts"],
"exclude": ["**/dist/**/*", "**/node_modules/**/*"], "exclude": ["**/dist/**/*", "**/node_modules/**/*"],
"references": [{ "path": "../packages/workflow/tsconfig.build.json" }] "references": [{ "path": "../packages/workflow/tsconfig.build.esm.json" }]
} }

View File

@@ -111,7 +111,8 @@
"@types/ws@8.18.1": "patches/@types__ws@8.18.1.patch", "@types/ws@8.18.1": "patches/@types__ws@8.18.1.patch",
"@types/uuencode@0.0.3": "patches/@types__uuencode@0.0.3.patch", "@types/uuencode@0.0.3": "patches/@types__uuencode@0.0.3.patch",
"vue-tsc@2.2.8": "patches/vue-tsc@2.2.8.patch", "vue-tsc@2.2.8": "patches/vue-tsc@2.2.8.patch",
"element-plus@2.4.3": "patches/element-plus@2.4.3.patch" "element-plus@2.4.3": "patches/element-plus@2.4.3.patch",
"js-base64": "patches/js-base64.patch"
} }
} }
} }

View File

@@ -8,7 +8,7 @@
}, },
"include": ["src/**/*.ts"], "include": ["src/**/*.ts"],
"references": [ "references": [
{ "path": "../../workflow/tsconfig.build.json" }, { "path": "../../workflow/tsconfig.build.esm.json" },
{ "path": "../config/tsconfig.build.json" }, { "path": "../config/tsconfig.build.json" },
{ "path": "../permissions/tsconfig.build.json" } { "path": "../permissions/tsconfig.build.json" }
] ]

View File

@@ -15,7 +15,7 @@
"include": ["src/**/*.ts"], "include": ["src/**/*.ts"],
"references": [ "references": [
{ "path": "../../core/tsconfig.build.json" }, { "path": "../../core/tsconfig.build.json" },
{ "path": "../../workflow/tsconfig.build.json" }, { "path": "../../workflow/tsconfig.build.esm.json" },
{ "path": "../config/tsconfig.build.json" }, { "path": "../config/tsconfig.build.json" },
{ "path": "../di/tsconfig.build.json" }, { "path": "../di/tsconfig.build.json" },
{ "path": "../permissions/tsconfig.build.json" } { "path": "../permissions/tsconfig.build.json" }

View File

@@ -10,7 +10,7 @@
}, },
"include": ["src/**/*.ts"], "include": ["src/**/*.ts"],
"references": [ "references": [
{ "path": "../../workflow/tsconfig.build.json" }, { "path": "../../workflow/tsconfig.build.esm.json" },
{ "path": "../constants/tsconfig.build.json" }, { "path": "../constants/tsconfig.build.json" },
{ "path": "../di/tsconfig.build.json" }, { "path": "../di/tsconfig.build.json" },
{ "path": "../permissions/tsconfig.build.json" } { "path": "../permissions/tsconfig.build.json" }

View File

@@ -26,6 +26,6 @@
"references": [ "references": [
{ "path": "../../core/tsconfig.build.json" }, { "path": "../../core/tsconfig.build.json" },
{ "path": "../../nodes-base/tsconfig.build.json" }, { "path": "../../nodes-base/tsconfig.build.json" },
{ "path": "../../workflow/tsconfig.build.json" } { "path": "../../workflow/tsconfig.build.esm.json" }
] ]
} }

View File

@@ -0,0 +1,9 @@
{
"compilerOptions": {
"composite": true,
"module": "umd",
"moduleResolution": "node",
"verbatimModuleSyntax": false,
"resolveJsonModule": false
}
}

View File

@@ -0,0 +1,27 @@
{
"compilerOptions": {
"esModuleInterop": true,
"skipLibCheck": true,
"target": "es2022",
"allowJs": true,
"resolveJsonModule": true,
"moduleDetection": "force",
"isolatedModules": true,
"verbatimModuleSyntax": true,
"strict": true,
"noUncheckedIndexedAccess": true,
"noImplicitOverride": true,
"module": "esnext",
"moduleResolution": "bundler",
"declaration": true,
"composite": true,
"declarationMap": true,
"sourceMap": true,
"lib": ["es2022", "dom", "dom.iterable"]
}
}

View File

@@ -1,6 +1,7 @@
import { defineConfig } from 'vitest/config'; import { defineConfig } from 'vitest/config';
import type { InlineConfig } from 'vitest/node';
export const createVitestConfig = (options = {}) => { export const createVitestConfig = (options: InlineConfig = {}) => {
const vitestConfig = defineConfig({ const vitestConfig = defineConfig({
test: { test: {
silent: true, silent: true,

View File

@@ -1,19 +1,27 @@
import { defineConfig } from 'vitest/config'; import { defineConfig } from 'vitest/config';
import type { InlineConfig } from 'vitest/node';
export const vitestConfig = defineConfig({ export const createVitestConfig = (options: InlineConfig = {}) => {
test: { const vitestConfig = defineConfig({
silent: true, test: {
globals: true, silent: true,
environment: 'node', globals: true,
...(process.env.COVERAGE_ENABLED === 'true' environment: 'node',
? { ...(process.env.COVERAGE_ENABLED === 'true'
coverage: { ? {
enabled: true, coverage: {
provider: 'v8', enabled: true,
reporter: process.env.CI === 'true' ? 'cobertura' : 'text-summary', provider: 'v8',
all: true, reporter: process.env.CI === 'true' ? 'cobertura' : 'text-summary',
}, all: true,
} },
: {}), }
}, : {}),
}); ...options,
},
});
return vitestConfig;
};
export const vitestConfig = createVitestConfig();

View File

@@ -12,9 +12,15 @@
"vitest": "catalog:" "vitest": "catalog:"
}, },
"files": [ "files": [
"backend.mjs",
"frontend.mjs" "frontend.mjs"
], ],
"exports": { "exports": {
"./backend": {
"import": "./backend.mjs",
"require": "./backend.mjs",
"types": "./backend.d.ts"
},
"./frontend": { "./frontend": {
"import": "./dist/frontend.js", "import": "./dist/frontend.js",
"require": "./dist/frontend.js", "require": "./dist/frontend.js",

View File

@@ -22,7 +22,7 @@
"references": [ "references": [
{ "path": "../core/tsconfig.build.json" }, { "path": "../core/tsconfig.build.json" },
{ "path": "../nodes-base/tsconfig.build.json" }, { "path": "../nodes-base/tsconfig.build.json" },
{ "path": "../workflow/tsconfig.build.json" }, { "path": "../workflow/tsconfig.build.esm.json" },
{ "path": "../@n8n/api-types/tsconfig.build.json" }, { "path": "../@n8n/api-types/tsconfig.build.json" },
{ "path": "../@n8n/client-oauth2/tsconfig.build.json" }, { "path": "../@n8n/client-oauth2/tsconfig.build.json" },
{ "path": "../@n8n/config/tsconfig.build.json" }, { "path": "../@n8n/config/tsconfig.build.json" },

View File

@@ -18,7 +18,7 @@
}, },
"include": ["src/**/*.ts", "test/**/*.ts"], "include": ["src/**/*.ts", "test/**/*.ts"],
"references": [ "references": [
{ "path": "../workflow/tsconfig.build.json" }, { "path": "../workflow/tsconfig.build.esm.json" },
{ "path": "../@n8n/decorators/tsconfig.build.json" }, { "path": "../@n8n/decorators/tsconfig.build.json" },
{ "path": "../@n8n/backend-common/tsconfig.build.json" }, { "path": "../@n8n/backend-common/tsconfig.build.json" },
{ "path": "../@n8n/config/tsconfig.build.json" }, { "path": "../@n8n/config/tsconfig.build.json" },

View File

@@ -27,7 +27,7 @@
], ],
"references": [ "references": [
{ "path": "../@n8n/imap/tsconfig.build.json" }, { "path": "../@n8n/imap/tsconfig.build.json" },
{ "path": "../workflow/tsconfig.build.json" }, { "path": "../workflow/tsconfig.build.esm.json" },
{ "path": "../core/tsconfig.build.json" } { "path": "../core/tsconfig.build.json" }
] ]
} }

View File

@@ -2,14 +2,14 @@
"name": "n8n-workflow", "name": "n8n-workflow",
"version": "1.98.0", "version": "1.98.0",
"description": "Workflow base code of n8n", "description": "Workflow base code of n8n",
"main": "dist/index.js", "types": "dist/esm/index.d.ts",
"module": "src/index.ts", "module": "dist/esm/index.js",
"types": "dist/index.d.ts", "main": "dist/cjs/index.js",
"exports": { "exports": {
".": { ".": {
"require": "./dist/index.js", "types": "./dist/esm/index.d.ts",
"import": "./src/index.ts", "import": "./dist/esm/index.js",
"types": "./dist/index.d.ts" "require": "./dist/cjs/index.js"
}, },
"./*": "./*" "./*": "./*"
}, },
@@ -17,14 +17,15 @@
"clean": "rimraf dist .turbo", "clean": "rimraf dist .turbo",
"dev": "pnpm watch", "dev": "pnpm watch",
"typecheck": "tsc --noEmit", "typecheck": "tsc --noEmit",
"build": "tsc -p tsconfig.build.json", "build:vite": "vite build",
"build": "tsc --build tsconfig.build.esm.json tsconfig.build.cjs.json",
"format": "biome format --write .", "format": "biome format --write .",
"format:check": "biome ci .", "format:check": "biome ci .",
"lint": "eslint src --quiet", "lint": "eslint src --quiet",
"lintfix": "eslint src --fix", "lintfix": "eslint src --fix",
"watch": "tsc -p tsconfig.build.json --watch", "watch": "tsc --build tsconfig.build.esm.json tsconfig.build.cjs.json --watch",
"test": "jest", "test": "vitest run",
"test:dev": "jest --watch" "test:dev": "vitest --watch"
}, },
"files": [ "files": [
"dist/**/*" "dist/**/*"
@@ -32,13 +33,16 @@
"devDependencies": { "devDependencies": {
"@langchain/core": "catalog:", "@langchain/core": "catalog:",
"@n8n/config": "workspace:*", "@n8n/config": "workspace:*",
"@n8n/vitest-config": "workspace:*",
"@n8n/typescript-config": "workspace:*", "@n8n/typescript-config": "workspace:*",
"@types/express": "catalog:", "@types/express": "catalog:",
"@types/jmespath": "^0.15.0", "@types/jmespath": "^0.15.0",
"@types/lodash": "catalog:", "@types/lodash": "catalog:",
"@types/luxon": "3.2.0", "@types/luxon": "3.2.0",
"@types/md5": "^2.3.5", "@types/md5": "^2.3.5",
"@types/xml2js": "catalog:" "@types/xml2js": "catalog:",
"vitest": "catalog:",
"vitest-mock-extended": "catalog:"
}, },
"dependencies": { "dependencies": {
"@n8n/tournament": "1.0.6", "@n8n/tournament": "1.0.6",

View File

@@ -10,7 +10,7 @@ interface ExecutionBaseErrorOptions extends ReportingOptions {
export abstract class ExecutionBaseError extends ApplicationError { export abstract class ExecutionBaseError extends ApplicationError {
description: string | null | undefined; description: string | null | undefined;
cause?: Error; override cause?: Error;
errorResponse?: JsonObject; errorResponse?: JsonObject;

View File

@@ -1,7 +1,7 @@
import type { Event } from '@sentry/node'; import type { Event } from '@sentry/node';
import callsites from 'callsites'; import callsites from 'callsites';
import type { ErrorLevel, ReportingOptions } from '@/errors/error.types'; import type { ErrorLevel, ReportingOptions } from './error.types';
/** /**
* @deprecated Use `UserError`, `OperationalError` or `UnexpectedError` instead. * @deprecated Use `UserError`, `OperationalError` or `UnexpectedError` instead.
@@ -17,7 +17,7 @@ export class ApplicationError extends Error {
constructor( constructor(
message: string, message: string,
{ level, tags = {}, extra, ...rest }: Partial<ErrorOptions> & ReportingOptions = {}, { level, tags = {}, extra, ...rest }: ErrorOptions & ReportingOptions = {},
) { ) {
super(message, rest); super(message, rest);
this.level = level ?? 'error'; this.level = level ?? 'error';

View File

@@ -14,7 +14,7 @@ export type UserErrorOptions = Omit<BaseErrorOptions, 'level'> & {
* Default level: info * Default level: info
*/ */
export class UserError extends BaseError { export class UserError extends BaseError {
readonly description: string | null | undefined; declare readonly description: string | null | undefined;
constructor(message: string, opts: UserErrorOptions = {}) { constructor(message: string, opts: UserErrorOptions = {}) {
opts.level = opts.level ?? 'info'; opts.level = opts.level ?? 'info';

View File

@@ -1,9 +1,9 @@
import { WorkflowOperationError } from './workflow-operation.error'; import { WorkflowOperationError } from './workflow-operation.error';
export class SubworkflowOperationError extends WorkflowOperationError { export class SubworkflowOperationError extends WorkflowOperationError {
description = ''; override description = '';
cause: Error; override cause: Error;
constructor(message: string, description: string) { constructor(message: string, description: string) {
super(message); super(message);

View File

@@ -7,7 +7,7 @@ import type { INode } from '../interfaces';
export class WorkflowOperationError extends ExecutionBaseError { export class WorkflowOperationError extends ExecutionBaseError {
node: INode | undefined; node: INode | undefined;
timestamp: number; override timestamp: number;
constructor(message: string, node?: INode, description?: string) { constructor(message: string, node?: INode, description?: string) {
super(message, { cause: undefined }); super(message, { cause: undefined });

View File

@@ -1,6 +1,4 @@
/** // @vitest-environment jsdom
* @jest-environment jsdom
*/
import { DateTime } from 'luxon'; import { DateTime } from 'luxon';
import type { ExtensionMap } from './extensions'; import type { ExtensionMap } from './extensions';

View File

@@ -1,4 +1,4 @@
import type { INode, NodeParameterValueType } from '@/interfaces'; import type { INode, NodeParameterValueType } from '../interfaces';
export function renameFormFields( export function renameFormFields(
node: INode, node: INode,

View File

@@ -1,3 +1,5 @@
/// <reference lib="es2022.error" />
declare module '@n8n_io/riot-tmpl' { declare module '@n8n_io/riot-tmpl' {
interface Brackets { interface Brackets {
set(token: string): void; set(token: string): void;

View File

@@ -1,9 +1,5 @@
import { import { parse as esprimaParse, Syntax } from 'esprima-next';
parse as esprimaParse, import type { Node as SyntaxNode, ExpressionStatement } from 'esprima-next';
Syntax,
type Node as SyntaxNode,
type ExpressionStatement,
} from 'esprima-next';
import FormData from 'form-data'; import FormData from 'form-data';
import merge from 'lodash/merge'; import merge from 'lodash/merge';

View File

@@ -1,10 +1,7 @@
/** // @vitest-environment jsdom
* @jest-environment jsdom
*/
import { arrayExtensions } from '@/extensions/array-extensions';
import { evaluate } from './helpers'; import { evaluate } from './helpers';
import { arrayExtensions } from '../../src/extensions/array-extensions';
describe('Data Transformation Functions', () => { describe('Data Transformation Functions', () => {
describe('Array Data Transformation Functions', () => { describe('Array Data Transformation Functions', () => {

View File

@@ -1,10 +1,7 @@
/** // @vitest-environment jsdom
* @jest-environment jsdom
*/
import { booleanExtensions } from '@/extensions/boolean-extensions';
import { evaluate } from './helpers'; import { evaluate } from './helpers';
import { booleanExtensions } from '../../src/extensions/boolean-extensions';
describe('Data Transformation Functions', () => { describe('Data Transformation Functions', () => {
describe('Boolean Data Transformation Functions', () => { describe('Boolean Data Transformation Functions', () => {

View File

@@ -1,13 +1,10 @@
/** // @vitest-environment jsdom
* @jest-environment jsdom
*/
import { DateTime } from 'luxon'; import { DateTime } from 'luxon';
import { dateExtensions } from '@/extensions/date-extensions';
import { getGlobalState } from '@/global-state';
import { evaluate, getLocalISOString } from './helpers'; import { evaluate, getLocalISOString } from './helpers';
import { dateExtensions } from '../../src/extensions/date-extensions';
import { getGlobalState } from '../../src/global-state';
const { defaultTimezone } = getGlobalState(); const { defaultTimezone } = getGlobalState();

View File

@@ -1,14 +1,11 @@
/** // @vitest-environment jsdom
* @jest-environment jsdom
*/
/* eslint-disable n8n-local-rules/no-interpolation-in-regular-string */ /* eslint-disable n8n-local-rules/no-interpolation-in-regular-string */
import { ExpressionExtensionError } from '@/errors/expression-extension.error';
import { extendTransform, extend } from '@/extensions';
import { joinExpression, splitExpression } from '@/extensions/expression-parser';
import { evaluate } from './helpers'; import { evaluate } from './helpers';
import { ExpressionExtensionError } from '../../src/errors/expression-extension.error';
import { extendTransform, extend } from '../../src/extensions';
import { joinExpression, splitExpression } from '../../src/extensions/expression-parser';
describe('Expression Extension Transforms', () => { describe('Expression Extension Transforms', () => {
describe('extend() transform', () => { describe('extend() transform', () => {
@@ -210,7 +207,7 @@ describe('Expression Parser', () => {
// This will likely break when sandboxing is implemented but it works for now. // This will likely break when sandboxing is implemented but it works for now.
// If you're implementing sandboxing maybe provide a way to add functions to // If you're implementing sandboxing maybe provide a way to add functions to
// sandbox we can check instead? // sandbox we can check instead?
const mockCallback = jest.fn(() => false); const mockCallback = vi.fn(() => false);
evaluate('={{ $if("a"==="a", true, $data.cb()) }}', [{ cb: mockCallback }]); evaluate('={{ $if("a"==="a", true, $data.cb()) }}', [{ cb: mockCallback }]);
expect(mockCallback.mock.calls.length).toEqual(0); expect(mockCallback.mock.calls.length).toEqual(0);

View File

@@ -1,6 +1,5 @@
import type { IDataObject } from '@/interfaces'; import type { IDataObject } from '../../src/interfaces';
import { Workflow } from '@/workflow'; import { Workflow } from '../../src/workflow';
import * as Helpers from '../helpers'; import * as Helpers from '../helpers';
export const nodeTypes = Helpers.NodeTypes(); export const nodeTypes = Helpers.NodeTypes();

View File

@@ -1,10 +1,7 @@
/** // @vitest-environment jsdom
* @jest-environment jsdom
*/
import { numberExtensions } from '@/extensions/number-extensions';
import { evaluate } from './helpers'; import { evaluate } from './helpers';
import { numberExtensions } from '../../src/extensions/number-extensions';
describe('Data Transformation Functions', () => { describe('Data Transformation Functions', () => {
describe('Number Data Transformation Functions', () => { describe('Number Data Transformation Functions', () => {

View File

@@ -1,7 +1,6 @@
import { ApplicationError } from '@/errors';
import { objectExtensions } from '@/extensions/object-extensions';
import { evaluate } from './helpers'; import { evaluate } from './helpers';
import { ApplicationError } from '../../src/errors';
import { objectExtensions } from '../../src/extensions/object-extensions';
describe('Data Transformation Functions', () => { describe('Data Transformation Functions', () => {
describe('Object Data Transformation Functions', () => { describe('Object Data Transformation Functions', () => {

View File

@@ -1,11 +1,8 @@
/** // @vitest-environment jsdom
* @jest-environment jsdom
*/
import { DateTime } from 'luxon'; import { DateTime } from 'luxon';
import { ExpressionExtensionError } from '@/errors';
import { evaluate } from './helpers'; import { evaluate } from './helpers';
import { ExpressionExtensionError } from '../../src/errors';
describe('Data Transformation Functions', () => { describe('Data Transformation Functions', () => {
describe('String Data Transformation Functions', () => { describe('String Data Transformation Functions', () => {
@@ -287,9 +284,11 @@ describe('Data Transformation Functions', () => {
expect(evaluate('={{ "1713976144063".toDateTime("ms") }}')).toBeInstanceOf(DateTime); expect(evaluate('={{ "1713976144063".toDateTime("ms") }}')).toBeInstanceOf(DateTime);
expect(evaluate('={{ "31-01-2024".toDateTime("dd-MM-yyyy") }}')).toBeInstanceOf(DateTime); expect(evaluate('={{ "31-01-2024".toDateTime("dd-MM-yyyy") }}')).toBeInstanceOf(DateTime);
expect(() => evaluate('={{ "hi".toDateTime() }}')).toThrowError( vi.useFakeTimers({ now: new Date() });
expect(() => evaluate('={{ "hi".toDateTime() }}')).toThrow(
new ExpressionExtensionError('cannot convert to Luxon DateTime'), new ExpressionExtensionError('cannot convert to Luxon DateTime'),
); );
vi.useRealTimers();
}); });
test('.extractUrlPath should work on a string', () => { test('.extractUrlPath should work on a string', () => {

View File

@@ -1,5 +1,5 @@
import { ExpressionError } from '@/errors/expression.error'; import { ExpressionError } from '../../src/errors/expression.error';
import type { GenericValue, IDataObject } from '@/interfaces'; import type { GenericValue, IDataObject } from '../../src/interfaces';
interface ExpressionTestBase { interface ExpressionTestBase {
type: 'evaluation' | 'transform'; type: 'evaluation' | 'transform';
@@ -275,7 +275,7 @@ export const baseFixtures: ExpressionTestFixture[] = [
input: [], input: [],
error: new ExpressionError('No execution data available', { error: new ExpressionError('No execution data available', {
runIndex: 0, runIndex: 0,
itemIndex: 0, itemIndex: -1,
type: 'no_execution_data', type: 'no_execution_data',
}), }),
}, },

View File

@@ -1,6 +1,6 @@
import { augmentArray, augmentObject } from '@/augment-object'; import { augmentArray, augmentObject } from '../src/augment-object';
import type { IDataObject } from '@/interfaces'; import type { IDataObject } from '../src/interfaces';
import { deepCopy } from '@/utils'; import { deepCopy } from '../src/utils';
describe('AugmentObject', () => { describe('AugmentObject', () => {
describe('augmentArray', () => { describe('augmentArray', () => {
@@ -485,7 +485,9 @@ describe('AugmentObject', () => {
expect(originalObject).toEqual(copyOriginal); expect(originalObject).toEqual(copyOriginal);
}); });
test('should be faster than doing a deepCopy', () => { // Skipping this test since it is no longer true in vitest, to be investigated
// eslint-disable-next-line n8n-local-rules/no-skipped-tests
test.skip('should be faster than doing a deepCopy', () => {
const iterations = 100; const iterations = 100;
const originalObject: any = { const originalObject: any = {
a: { a: {

View File

@@ -1,5 +1,5 @@
import { toCronExpression } from '@/cron'; import { toCronExpression } from '../src/cron';
import type { CronExpression } from '@/interfaces'; import type { CronExpression } from '../src/interfaces';
describe('Cron', () => { describe('Cron', () => {
describe('toCronExpression', () => { describe('toCronExpression', () => {

View File

@@ -1,4 +1,4 @@
import { createDeferredPromise } from '@/deferred-promise'; import { createDeferredPromise } from '../src/deferred-promise';
describe('DeferredPromise', () => { describe('DeferredPromise', () => {
it('should resolve the promise with the correct value', async () => { it('should resolve the promise with the correct value', async () => {

View File

@@ -1,5 +1,5 @@
import { BaseError } from '@/errors/base/base.error'; import { BaseError } from '../../../src/errors/base/base.error';
import { OperationalError } from '@/errors/base/operational.error'; import { OperationalError } from '../../../src/errors/base/operational.error';
describe('OperationalError', () => { describe('OperationalError', () => {
it('should be an instance of OperationalError', () => { it('should be an instance of OperationalError', () => {

View File

@@ -1,5 +1,5 @@
import { BaseError } from '@/errors/base/base.error'; import { BaseError } from '../../../src/errors/base/base.error';
import { UnexpectedError } from '@/errors/base/unexpected.error'; import { UnexpectedError } from '../../../src/errors/base/unexpected.error';
describe('UnexpectedError', () => { describe('UnexpectedError', () => {
it('should be an instance of UnexpectedError', () => { it('should be an instance of UnexpectedError', () => {

View File

@@ -1,5 +1,5 @@
import { BaseError } from '@/errors/base/base.error'; import { BaseError } from '../../../src/errors/base/base.error';
import { UserError } from '@/errors/base/user.error'; import { UserError } from '../../../src/errors/base/user.error';
describe('UserError', () => { describe('UserError', () => {
it('should be an instance of UserError', () => { it('should be an instance of UserError', () => {

View File

@@ -1,19 +1,24 @@
import { mock } from 'jest-mock-extended'; import { mock } from 'vitest-mock-extended';
import { NodeApiError } from '@/errors/node-api.error'; import { NodeApiError } from '../../src/errors/node-api.error';
import { NodeOperationError } from '@/errors/node-operation.error'; import { NodeOperationError } from '../../src/errors/node-operation.error';
import type { INode } from '@/interfaces'; import type { INode } from '../../src/interfaces';
describe('NodeError', () => { describe('NodeError', () => {
const node = mock<INode>(); const node = mock<INode>();
it('should update re-wrapped error level and message', () => { it('should update re-wrapped error level and message', () => {
vi.useFakeTimers({ now: new Date() });
const apiError = new NodeApiError(node, { message: 'Some error happened', code: 500 }); const apiError = new NodeApiError(node, { message: 'Some error happened', code: 500 });
const opsError = new NodeOperationError(node, mock(), { message: 'Some operation failed' }); const opsError = new NodeOperationError(node, mock(), { message: 'Some operation failed' });
const wrapped1 = new NodeOperationError(node, apiError); const wrapped1 = new NodeOperationError(node, apiError);
const wrapped2 = new NodeOperationError(node, opsError); const wrapped2 = new NodeOperationError(node, opsError);
expect(wrapped1).toEqual(apiError); expect(wrapped1.level).toEqual(apiError.level);
expect(wrapped1.message).toEqual(apiError.message);
expect(wrapped2).toEqual(opsError); expect(wrapped2).toEqual(opsError);
vi.useRealTimers();
}); });
}); });

View File

@@ -1,4 +1,4 @@
import { WorkflowActivationError } from '@/errors'; import { WorkflowActivationError } from '../../src/errors';
describe('WorkflowActivationError', () => { describe('WorkflowActivationError', () => {
it('should default to `error` level', () => { it('should default to `error` level', () => {

View File

@@ -1,6 +1,6 @@
import { Tournament } from '@n8n/tournament'; import { Tournament } from '@n8n/tournament';
import { PrototypeSanitizer, sanitizer } from '@/expression-sandboxing'; import { PrototypeSanitizer, sanitizer } from '../src/expression-sandboxing';
const tournament = new Tournament( const tournament = new Tournament(
(e) => { (e) => {

View File

@@ -1,18 +1,15 @@
/** // @vitest-environment jsdom
* @jest-environment jsdom
*/
import { DateTime, Duration, Interval } from 'luxon'; import { DateTime, Duration, Interval } from 'luxon';
import { ExpressionError } from '@/errors/expression.error';
import { extendSyntax } from '@/extensions/expression-extension';
import type { INodeExecutionData } from '@/interfaces';
import { Workflow } from '@/workflow';
import { workflow } from './ExpressionExtensions/helpers'; import { workflow } from './ExpressionExtensions/helpers';
import { baseFixtures } from './ExpressionFixtures/base'; import { baseFixtures } from './ExpressionFixtures/base';
import type { ExpressionTestEvaluation, ExpressionTestTransform } from './ExpressionFixtures/base'; import type { ExpressionTestEvaluation, ExpressionTestTransform } from './ExpressionFixtures/base';
import * as Helpers from './helpers'; import * as Helpers from './helpers';
import { ExpressionError } from '../src/errors/expression.error';
import { extendSyntax } from '../src/extensions/expression-extension';
import type { INodeExecutionData } from '../src/interfaces';
import { Workflow } from '../src/workflow';
describe('Expression', () => { describe('Expression', () => {
describe('getParameterValue()', () => { describe('getParameterValue()', () => {
@@ -71,9 +68,11 @@ describe('Expression', () => {
expect(evaluate('={{Reflect}}')).toEqual({}); expect(evaluate('={{Reflect}}')).toEqual({});
expect(evaluate('={{Proxy}}')).toEqual({}); expect(evaluate('={{Proxy}}')).toEqual({});
vi.useFakeTimers({ now: new Date() });
expect(() => evaluate('={{constructor}}')).toThrowError( expect(() => evaluate('={{constructor}}')).toThrowError(
new ExpressionError('Cannot access "constructor" due to security concerns'), new ExpressionError('Cannot access "constructor" due to security concerns'),
); );
vi.useRealTimers();
expect(evaluate('={{escape}}')).toEqual({}); expect(evaluate('={{escape}}')).toEqual({});
expect(evaluate('={{unescape}}')).toEqual({}); expect(evaluate('={{unescape}}')).toEqual({});
@@ -85,11 +84,11 @@ describe('Expression', () => {
DateTime.now().toLocaleString(), DateTime.now().toLocaleString(),
); );
jest.useFakeTimers({ now: new Date() }); vi.useFakeTimers({ now: new Date() });
expect(evaluate('={{Interval.after(new Date(), 100)}}')).toEqual( expect(evaluate('={{Interval.after(new Date(), 100)}}')).toEqual(
Interval.after(new Date(), 100), Interval.after(new Date(), 100),
); );
jest.useRealTimers(); vi.useRealTimers();
expect(evaluate('={{Duration.fromMillis(100)}}')).toEqual(Duration.fromMillis(100)); expect(evaluate('={{Duration.fromMillis(100)}}')).toEqual(Duration.fromMillis(100));
@@ -162,11 +161,15 @@ describe('Expression', () => {
}); });
it('should not able to do arbitrary code execution', () => { it('should not able to do arbitrary code execution', () => {
const testFn = jest.fn(); const testFn = vi.fn();
Object.assign(global, { testFn }); Object.assign(global, { testFn });
vi.useFakeTimers({ now: new Date() });
expect(() => evaluate("={{ Date['constructor']('testFn()')()}}")).toThrowError( expect(() => evaluate("={{ Date['constructor']('testFn()')()}}")).toThrowError(
new ExpressionError('Cannot access "constructor" due to security concerns'), new ExpressionError('Cannot access "constructor" due to security concerns'),
); );
vi.useRealTimers();
expect(testFn).not.toHaveBeenCalled(); expect(testFn).not.toHaveBeenCalled();
}); });
}); });
@@ -184,6 +187,8 @@ describe('Expression', () => {
continue; continue;
} }
test(t.expression, () => { test(t.expression, () => {
vi.spyOn(workflow, 'getParentNodes').mockReturnValue(['Parent']);
const evaluationTests = t.tests.filter( const evaluationTests = t.tests.filter(
(test): test is ExpressionTestEvaluation => test.type === 'evaluation', (test): test is ExpressionTestEvaluation => test.type === 'evaluation',
); );
@@ -192,7 +197,11 @@ describe('Expression', () => {
const input = test.input.map((d) => ({ json: d })) as any; const input = test.input.map((d) => ({ json: d })) as any;
if ('error' in test) { if ('error' in test) {
vi.useFakeTimers({ now: test.error.timestamp });
expect(() => evaluate(t.expression, input)).toThrowError(test.error); expect(() => evaluate(t.expression, input)).toThrowError(test.error);
vi.useRealTimers();
} else { } else {
expect(evaluate(t.expression, input)).toStrictEqual(test.output); expect(evaluate(t.expression, input)).toStrictEqual(test.output);
} }
@@ -207,12 +216,16 @@ describe('Expression', () => {
continue; continue;
} }
test(t.expression, () => { test(t.expression, () => {
vi.useFakeTimers({ now: new Date() });
for (const test of t.tests.filter( for (const test of t.tests.filter(
(test): test is ExpressionTestTransform => test.type === 'transform', (test): test is ExpressionTestTransform => test.type === 'transform',
)) { )) {
const expr = t.expression; const expr = t.expression;
expect(extendSyntax(expr, test.forceTransform)).toEqual(test.result ?? expr); expect(extendSyntax(expr, test.forceTransform)).toEqual(test.result ?? expr);
} }
vi.useRealTimers();
}); });
} }
}); });

View File

@@ -1,8 +1,8 @@
import merge from 'lodash/merge'; import merge from 'lodash/merge';
import { DateTime } from 'luxon'; import { DateTime } from 'luxon';
import type { FilterConditionValue, FilterValue } from '@/interfaces'; import type { FilterConditionValue, FilterValue } from '../src/interfaces';
import { arrayContainsValue, executeFilter } from '@/node-parameters/filter-parameter'; import { arrayContainsValue, executeFilter } from '../src/node-parameters/filter-parameter';
type DeepPartial<T> = { type DeepPartial<T> = {
[P in keyof T]?: T[P] extends object ? DeepPartial<T[P]> : T[P]; [P in keyof T]?: T[P] extends object ? DeepPartial<T[P]> : T[P];

View File

@@ -3,7 +3,7 @@ import {
traverseNodeParameters, traverseNodeParameters,
type FromAIArgument, type FromAIArgument,
generateZodSchema, generateZodSchema,
} from '@/from-ai-parse-utils'; } from '../src/from-ai-parse-utils';
// Note that for historic reasons a lot of testing of this file happens indirectly in `packages/core/test/CreateNodeAsTool.test.ts` // Note that for historic reasons a lot of testing of this file happens indirectly in `packages/core/test/CreateNodeAsTool.test.ts`

View File

@@ -6,8 +6,8 @@ import {
parseExtractableSubgraphSelection, parseExtractableSubgraphSelection,
hasPath, hasPath,
buildAdjacencyList, buildAdjacencyList,
} from '@/graph/graph-utils'; } from '../../src/graph/graph-utils';
import type { IConnection, IConnections, NodeConnectionType } from '@/index'; import type { IConnection, IConnections, NodeConnectionType } from '../../src/index';
function makeConnection( function makeConnection(
node: string, node: string,

View File

@@ -1,9 +1,8 @@
import { readFileSync } from 'fs'; import { readFileSync } from 'fs';
import path from 'path'; import path from 'path';
import type { INodeTypes } from '@/interfaces';
import { NodeTypes as NodeTypesClass } from './node-types'; import { NodeTypes as NodeTypesClass } from './node-types';
import type { INodeTypes } from '../src/interfaces';
let nodeTypesInstance: NodeTypesClass | undefined; let nodeTypesInstance: NodeTypesClass | undefined;

View File

@@ -1,4 +1,4 @@
import { parseErrorMetadata } from '@/metadata-utils'; import { parseErrorMetadata } from '../src/metadata-utils';
describe('MetadataUtils', () => { describe('MetadataUtils', () => {
describe('parseMetadataFromError', () => { describe('parseMetadataFromError', () => {

View File

@@ -1,7 +1,7 @@
import { UNKNOWN_ERROR_DESCRIPTION, UNKNOWN_ERROR_MESSAGE } from '@/constants'; import { UNKNOWN_ERROR_DESCRIPTION, UNKNOWN_ERROR_MESSAGE } from '../src/constants';
import { NodeOperationError } from '@/errors'; import { NodeOperationError } from '../src/errors';
import { NodeApiError } from '@/errors/node-api.error'; import { NodeApiError } from '../src/errors/node-api.error';
import type { INode, JsonObject } from '@/interfaces'; import type { INode, JsonObject } from '../src/interfaces';
const node: INode = { const node: INode = {
id: '1', id: '1',

View File

@@ -1,5 +1,5 @@
import type { INodeParameters, INodeProperties } from '@/interfaces'; import type { INodeParameters, INodeProperties } from '../src/interfaces';
import { getNodeParameters } from '@/node-helpers'; import { getNodeParameters } from '../src/node-helpers';
describe('NodeHelpers', () => { describe('NodeHelpers', () => {
describe('getNodeParameters, displayOptions set using DisplayCondition', () => { describe('getNodeParameters, displayOptions set using DisplayCondition', () => {

View File

@@ -7,7 +7,7 @@ import {
type INodeParameters, type INodeParameters,
type INodeProperties, type INodeProperties,
type INodeTypeDescription, type INodeTypeDescription,
} from '@/interfaces'; } from '../src/interfaces';
import { import {
getNodeParameters, getNodeParameters,
isSubNodeType, isSubNodeType,
@@ -21,8 +21,8 @@ import {
isDefaultNodeName, isDefaultNodeName,
makeNodeName, makeNodeName,
isTool, isTool,
} from '@/node-helpers'; } from '../src/node-helpers';
import type { Workflow } from '@/workflow'; import type { Workflow } from '../src/workflow';
describe('NodeHelpers', () => { describe('NodeHelpers', () => {
describe('getNodeParameters', () => { describe('getNodeParameters', () => {
@@ -4226,7 +4226,7 @@ describe('NodeHelpers', () => {
describe('isExecutable', () => { describe('isExecutable', () => {
const workflowMock = { const workflowMock = {
expression: { expression: {
getSimpleParameterValue: jest.fn().mockReturnValue([NodeConnectionTypes.Main]), getSimpleParameterValue: vi.fn().mockReturnValue([NodeConnectionTypes.Main]),
}, },
} as unknown as Workflow; } as unknown as Workflow;
@@ -4382,7 +4382,7 @@ describe('NodeHelpers', () => {
test(testData.description, () => { test(testData.description, () => {
// If this test has a custom mock return value, configure it // If this test has a custom mock return value, configure it
if (testData.mockReturnValue) { if (testData.mockReturnValue) {
(workflowMock.expression.getSimpleParameterValue as jest.Mock).mockReturnValueOnce( vi.mocked(workflowMock.expression.getSimpleParameterValue).mockReturnValueOnce(
testData.mockReturnValue, testData.mockReturnValue,
); );
} }

View File

@@ -1,11 +1,11 @@
import type { INode } from '@/interfaces'; import type { INode } from '../src/interfaces';
import { import {
hasDotNotationBannedChar, hasDotNotationBannedChar,
backslashEscape, backslashEscape,
dollarEscape, dollarEscape,
applyAccessPatterns, applyAccessPatterns,
extractReferencesInNodeExpressions, extractReferencesInNodeExpressions,
} from '@/node-reference-parser-utils'; } from '../src/node-reference-parser-utils';
const makeNode = (name: string, expressions?: string[]) => const makeNode = (name: string, expressions?: string[]) =>
({ ({

View File

@@ -1,4 +1,4 @@
import { mock } from 'jest-mock-extended'; import { mock } from 'vitest-mock-extended';
import { import {
NodeConnectionTypes, NodeConnectionTypes,
@@ -8,8 +8,8 @@ import {
type INodeTypes, type INodeTypes,
type IVersionedNodeType, type IVersionedNodeType,
type LoadedClass, type LoadedClass,
} from '@/interfaces'; } from '../src/interfaces';
import * as NodeHelpers from '@/node-helpers'; import * as NodeHelpers from '../src/node-helpers';
const stickyNode: LoadedClass<INodeType> = { const stickyNode: LoadedClass<INodeType> = {
type: { type: {

View File

@@ -1,5 +1,5 @@
import type { IDataObject } from '@/interfaces'; import type { IDataObject } from '../src/interfaces';
import * as ObservableObject from '@/observable-object'; import * as ObservableObject from '../src/observable-object';
describe('ObservableObject', () => { describe('ObservableObject', () => {
test('should recognize that item on parent level got added (init empty)', () => { test('should recognize that item on parent level got added (init empty)', () => {

View File

@@ -1,7 +1,7 @@
import { mockFn } from 'jest-mock-extended'; import { mockFn } from 'vitest-mock-extended';
import type { INode } from '@/index'; import type { INode } from '../src/index';
import { renameFormFields } from '@/node-parameters/rename-node-utils'; import { renameFormFields } from '../src/node-parameters/rename-node-utils';
const makeNode = (formFieldValues: Array<Record<string, unknown>>) => const makeNode = (formFieldValues: Array<Record<string, unknown>>) =>
({ ({

View File

@@ -1,8 +1,10 @@
import { mock } from 'jest-mock-extended';
import { v5 as uuidv5, v3 as uuidv3, v4 as uuidv4, v1 as uuidv1 } from 'uuid'; import { v5 as uuidv5, v3 as uuidv3, v4 as uuidv4, v1 as uuidv1 } from 'uuid';
import { mock } from 'vitest-mock-extended';
import { STICKY_NODE_TYPE } from '@/constants'; import { nodeTypes } from './ExpressionExtensions/helpers';
import { ApplicationError, ExpressionError, NodeApiError } from '@/errors'; import type { NodeTypes } from './node-types';
import { STICKY_NODE_TYPE } from '../src/constants';
import { ApplicationError, ExpressionError, NodeApiError } from '../src/errors';
import type { import type {
INode, INode,
INodeTypeDescription, INodeTypeDescription,
@@ -11,9 +13,9 @@ import type {
NodeConnectionType, NodeConnectionType,
IWorkflowBase, IWorkflowBase,
INodeParameters, INodeParameters,
} from '@/interfaces'; } from '../src/interfaces';
import { NodeConnectionTypes } from '@/interfaces'; import { NodeConnectionTypes } from '../src/interfaces';
import * as nodeHelpers from '@/node-helpers'; import * as nodeHelpers from '../src/node-helpers';
import { import {
ANONYMIZATION_CHARACTER as CHAR, ANONYMIZATION_CHARACTER as CHAR,
extractLastExecutedNodeCredentialData, extractLastExecutedNodeCredentialData,
@@ -24,11 +26,8 @@ import {
resolveAIMetrics, resolveAIMetrics,
resolveVectorStoreMetrics, resolveVectorStoreMetrics,
userInInstanceRanOutOfFreeAiCredits, userInInstanceRanOutOfFreeAiCredits,
} from '@/telemetry-helpers'; } from '../src/telemetry-helpers';
import { randomInt } from '@/utils'; import { randomInt } from '../src/utils';
import { nodeTypes } from './ExpressionExtensions/helpers';
import type { NodeTypes } from './node-types';
describe('getDomainBase should return protocol plus domain', () => { describe('getDomainBase should return protocol plus domain', () => {
test('in valid URLs', () => { test('in valid URLs', () => {
@@ -932,7 +931,7 @@ describe('generateNodesGraph', () => {
test('should not fail on error to resolve a node parameter for sticky node type', () => { test('should not fail on error to resolve a node parameter for sticky node type', () => {
const workflow = mock<IWorkflowBase>({ nodes: [{ type: STICKY_NODE_TYPE }] }); const workflow = mock<IWorkflowBase>({ nodes: [{ type: STICKY_NODE_TYPE }] });
jest.spyOn(nodeHelpers, 'getNodeParameters').mockImplementationOnce(() => { vi.spyOn(nodeHelpers, 'getNodeParameters').mockImplementationOnce(() => {
throw new ApplicationError('Could not find property option'); throw new ApplicationError('Could not find property option');
}); });
@@ -2206,9 +2205,9 @@ describe('extractLastExecutedNodeStructuredOutputErrorInfo', () => {
}, },
}); });
const runData = mockRunData('Agent', new Error('Some error')); const runData = mockRunData('Agent', new Error('Some error'));
jest vi.spyOn(nodeHelpers, 'getNodeParameters').mockReturnValueOnce(
.spyOn(nodeHelpers, 'getNodeParameters') mock<INodeParameters>({ model: { value: 'gpt-4-turbo' } }),
.mockReturnValueOnce(mock<INodeParameters>({ model: { value: 'gpt-4-turbo' } })); );
const result = extractLastExecutedNodeStructuredOutputErrorInfo(workflow, nodeTypes, runData); const result = extractLastExecutedNodeStructuredOutputErrorInfo(workflow, nodeTypes, runData);
expect(result).toEqual({ expect(result).toEqual({
@@ -2260,9 +2259,9 @@ describe('extractLastExecutedNodeStructuredOutputErrorInfo', () => {
], ],
}); });
jest vi.spyOn(nodeHelpers, 'getNodeParameters').mockReturnValueOnce(
.spyOn(nodeHelpers, 'getNodeParameters') mock<INodeParameters>({ model: { value: 'gpt-4.1-mini' } }),
.mockReturnValueOnce(mock<INodeParameters>({ model: { value: 'gpt-4.1-mini' } })); );
const result = extractLastExecutedNodeStructuredOutputErrorInfo(workflow, nodeTypes, runData); const result = extractLastExecutedNodeStructuredOutputErrorInfo(workflow, nodeTypes, runData);
expect(result).toEqual({ expect(result).toEqual({
@@ -2288,9 +2287,9 @@ describe('extractLastExecutedNodeStructuredOutputErrorInfo', () => {
const runData = mockRunData('Agent', new Error('Some error')); const runData = mockRunData('Agent', new Error('Some error'));
jest vi.spyOn(nodeHelpers, 'getNodeParameters').mockReturnValueOnce(
.spyOn(nodeHelpers, 'getNodeParameters') mock<INodeParameters>({ model: 'gpt-4' }),
.mockReturnValueOnce(mock<INodeParameters>({ model: 'gpt-4' })); );
const result = extractLastExecutedNodeStructuredOutputErrorInfo(workflow, nodeTypes, runData); const result = extractLastExecutedNodeStructuredOutputErrorInfo(workflow, nodeTypes, runData);
expect(result).toEqual({ expect(result).toEqual({
@@ -2378,9 +2377,9 @@ describe('extractLastExecutedNodeStructuredOutputErrorInfo', () => {
}); });
const runData = mockRunData('Agent', new Error('Some error')); const runData = mockRunData('Agent', new Error('Some error'));
jest vi.spyOn(nodeHelpers, 'getNodeParameters').mockReturnValueOnce(
.spyOn(nodeHelpers, 'getNodeParameters') mock<INodeParameters>({ modelName: 'gemini-1.5-pro' }),
.mockReturnValueOnce(mock<INodeParameters>({ modelName: 'gemini-1.5-pro' })); );
const result = extractLastExecutedNodeStructuredOutputErrorInfo(workflow, nodeTypes, runData); const result = extractLastExecutedNodeStructuredOutputErrorInfo(workflow, nodeTypes, runData);
expect(result).toEqual({ expect(result).toEqual({

View File

@@ -1,6 +1,6 @@
import { DateTime, Settings } from 'luxon'; import { DateTime, Settings } from 'luxon';
import { getValueDescription, tryToParseDateTime, validateFieldType } from '@/type-validation'; import { getValueDescription, tryToParseDateTime, validateFieldType } from '../src/type-validation';
describe('Type Validation', () => { describe('Type Validation', () => {
describe('string-alphanumeric', () => { describe('string-alphanumeric', () => {

View File

@@ -1,6 +1,6 @@
import { ALPHABET } from '@/constants'; import { ALPHABET } from '../src/constants';
import { ApplicationError } from '@/errors/application.error'; import { ApplicationError } from '../src/errors/application.error';
import { ExecutionCancelledError } from '@/errors/execution-cancelled.error'; import { ExecutionCancelledError } from '../src/errors/execution-cancelled.error';
import { import {
jsonParse, jsonParse,
jsonStringify, jsonStringify,
@@ -13,7 +13,7 @@ import {
isSafeObjectProperty, isSafeObjectProperty,
setSafeObjectProperty, setSafeObjectProperty,
sleepWithAbort, sleepWithAbort,
} from '@/utils'; } from '../src/utils';
describe('isObjectEmpty', () => { describe('isObjectEmpty', () => {
it('should handle null and undefined', () => { it('should handle null and undefined', () => {
@@ -69,7 +69,7 @@ describe('isObjectEmpty', () => {
}); });
it('should not call Object.keys unless a plain object', () => { it('should not call Object.keys unless a plain object', () => {
const keySpy = jest.spyOn(Object, 'keys'); const keySpy = vi.spyOn(Object, 'keys');
const { calls } = keySpy.mock; const { calls } = keySpy.mock;
const assertCalls = (count: number) => { const assertCalls = (count: number) => {
@@ -447,7 +447,7 @@ describe('sleepWithAbort', () => {
it('should clean up timeout when aborted during sleep', async () => { it('should clean up timeout when aborted during sleep', async () => {
const abortController = new AbortController(); const abortController = new AbortController();
const clearTimeoutSpy = jest.spyOn(global, 'clearTimeout'); const clearTimeoutSpy = vi.spyOn(global, 'clearTimeout');
// Start the sleep and abort after 50ms // Start the sleep and abort after 50ms
const sleepPromise = sleepWithAbort(1000, abortController.signal); const sleepPromise = sleepWithAbort(1000, abortController.signal);

View File

@@ -1,5 +1,5 @@
import { ExpressionError } from '@/errors/expression.error'; import { ExpressionError } from '../src/errors/expression.error';
import { createEnvProvider, createEnvProviderState } from '@/workflow-data-proxy-env-provider'; import { createEnvProvider, createEnvProviderState } from '../src/workflow-data-proxy-env-provider';
describe('createEnvProviderState', () => { describe('createEnvProviderState', () => {
afterEach(() => { afterEach(() => {
@@ -54,6 +54,8 @@ describe('createEnvProvider', () => {
}); });
it('should throw ExpressionError when process is unavailable', () => { it('should throw ExpressionError when process is unavailable', () => {
vi.useFakeTimers({ now: new Date() });
const originalProcess = global.process; const originalProcess = global.process;
// @ts-expect-error process is read-only // @ts-expect-error process is read-only
global.process = undefined; global.process = undefined;
@@ -69,6 +71,8 @@ describe('createEnvProvider', () => {
} finally { } finally {
global.process = originalProcess; global.process = originalProcess;
} }
vi.useRealTimers();
}); });
it('should throw ExpressionError when env access is blocked', () => { it('should throw ExpressionError when env access is blocked', () => {

View File

@@ -1,7 +1,8 @@
import { DateTime, Duration, Interval } from 'luxon'; import { DateTime, Duration, Interval } from 'luxon';
import { ensureError } from '@/errors/ensure-error'; import * as Helpers from './helpers';
import { ExpressionError } from '@/errors/expression.error'; import { ensureError } from '../src/errors/ensure-error';
import { ExpressionError } from '../src/errors/expression.error';
import { import {
NodeConnectionTypes, NodeConnectionTypes,
type NodeConnectionType, type NodeConnectionType,
@@ -11,11 +12,9 @@ import {
type IRun, type IRun,
type IWorkflowBase, type IWorkflowBase,
type WorkflowExecuteMode, type WorkflowExecuteMode,
} from '@/interfaces'; } from '../src/interfaces';
import { Workflow } from '@/workflow'; import { Workflow } from '../src/workflow';
import { WorkflowDataProxy } from '@/workflow-data-proxy'; import { WorkflowDataProxy } from '../src/workflow-data-proxy';
import * as Helpers from './helpers';
const loadFixture = (fixture: string) => { const loadFixture = (fixture: string) => {
const workflow = Helpers.readJsonFileSync<IWorkflowBase>( const workflow = Helpers.readJsonFileSync<IWorkflowBase>(
@@ -225,7 +224,7 @@ describe('WorkflowDataProxy', () => {
describe('Errors', () => { describe('Errors', () => {
const fixture = loadFixture('errors'); const fixture = loadFixture('errors');
test('$("NodeName").item, Node does not exist', (done) => { test('$("NodeName").item, Node does not exist', () => {
const proxy = getProxyFromFixture( const proxy = getProxyFromFixture(
fixture.workflow, fixture.workflow,
fixture.run, fixture.run,
@@ -233,30 +232,26 @@ describe('WorkflowDataProxy', () => {
); );
try { try {
proxy.$('does not exist').item; proxy.$('does not exist').item;
done('should throw');
} catch (error) { } catch (error) {
expect(error).toBeInstanceOf(ExpressionError); expect(error).toBeInstanceOf(ExpressionError);
const exprError = error as ExpressionError; const exprError = error as ExpressionError;
expect(exprError.message).toEqual("Referenced node doesn't exist"); expect(exprError.message).toEqual("Referenced node doesn't exist");
done();
} }
}); });
test('$("NodeName").item, node has no connection to referenced node', (done) => { test('$("NodeName").item, node has no connection to referenced node', () => {
const proxy = getProxyFromFixture(fixture.workflow, fixture.run, 'NoPathBack'); const proxy = getProxyFromFixture(fixture.workflow, fixture.run, 'NoPathBack');
try { try {
proxy.$('Customer Datastore (n8n training)').item; proxy.$('Customer Datastore (n8n training)').item;
done('should throw');
} catch (error) { } catch (error) {
expect(error).toBeInstanceOf(ExpressionError); expect(error).toBeInstanceOf(ExpressionError);
const exprError = error as ExpressionError; const exprError = error as ExpressionError;
expect(exprError.message).toEqual('Invalid expression'); expect(exprError.message).toEqual('Invalid expression');
expect(exprError.context.type).toEqual('paired_item_no_connection'); expect(exprError.context.type).toEqual('paired_item_no_connection');
done();
} }
}); });
test('$("NodeName").first(), node has no connection to referenced node', (done) => { test('$("NodeName").first(), node has no connection to referenced node', () => {
const proxy = getProxyFromFixture( const proxy = getProxyFromFixture(
fixture.workflow, fixture.workflow,
fixture.run, fixture.run,
@@ -264,77 +259,66 @@ describe('WorkflowDataProxy', () => {
); );
try { try {
proxy.$('Impossible').first().json.name; proxy.$('Impossible').first().json.name;
done('should throw');
} catch (error) { } catch (error) {
expect(error).toBeInstanceOf(ExpressionError); expect(error).toBeInstanceOf(ExpressionError);
const exprError = error as ExpressionError; const exprError = error as ExpressionError;
expect(exprError.message).toEqual('Referenced node is unexecuted'); expect(exprError.message).toEqual('Referenced node is unexecuted');
expect(exprError.context.type).toEqual('no_node_execution_data'); expect(exprError.context.type).toEqual('no_node_execution_data');
done();
} }
}); });
test('$json, Node has no connections', (done) => { test('$json, Node has no connections', () => {
const proxy = getProxyFromFixture(fixture.workflow, fixture.run, 'NoInputConnection'); const proxy = getProxyFromFixture(fixture.workflow, fixture.run, 'NoInputConnection');
try { try {
proxy.$json.email; proxy.$json.email;
done('should throw');
} catch (error) { } catch (error) {
expect(error).toBeInstanceOf(ExpressionError); expect(error).toBeInstanceOf(ExpressionError);
const exprError = error as ExpressionError; const exprError = error as ExpressionError;
expect(exprError.message).toEqual('No execution data available'); expect(exprError.message).toEqual('No execution data available');
expect(exprError.context.type).toEqual('no_input_connection'); expect(exprError.context.type).toEqual('no_input_connection');
done();
} }
}); });
test('$("NodeName").item, Node has not run', (done) => { test('$("NodeName").item, Node has not run', () => {
const proxy = getProxyFromFixture(fixture.workflow, fixture.run, 'Impossible'); const proxy = getProxyFromFixture(fixture.workflow, fixture.run, 'Impossible');
try { try {
proxy.$('Impossible if').item; proxy.$('Impossible if').item;
done('should throw');
} catch (error) { } catch (error) {
expect(error).toBeInstanceOf(ExpressionError); expect(error).toBeInstanceOf(ExpressionError);
const exprError = error as ExpressionError; const exprError = error as ExpressionError;
expect(exprError.message).toEqual('Referenced node is unexecuted'); expect(exprError.message).toEqual('Referenced node is unexecuted');
expect(exprError.context.type).toEqual('no_node_execution_data'); expect(exprError.context.type).toEqual('no_node_execution_data');
done();
} }
}); });
test('$json, Node has not run', (done) => { test('$json, Node has not run', () => {
const proxy = getProxyFromFixture(fixture.workflow, fixture.run, 'Impossible'); const proxy = getProxyFromFixture(fixture.workflow, fixture.run, 'Impossible');
try { try {
proxy.$json.email; proxy.$json.email;
done('should throw');
} catch (error) { } catch (error) {
expect(error).toBeInstanceOf(ExpressionError); expect(error).toBeInstanceOf(ExpressionError);
const exprError = error as ExpressionError; const exprError = error as ExpressionError;
expect(exprError.message).toEqual('No execution data available'); expect(exprError.message).toEqual('No execution data available');
expect(exprError.context.type).toEqual('no_execution_data'); expect(exprError.context.type).toEqual('no_execution_data');
done();
} }
}); });
test('$("NodeName").item, paired item error: more than 1 matching item', (done) => { test('$("NodeName").item, paired item error: more than 1 matching item', () => {
const proxy = getProxyFromFixture(fixture.workflow, fixture.run, 'PairedItemMultipleMatches'); const proxy = getProxyFromFixture(fixture.workflow, fixture.run, 'PairedItemMultipleMatches');
try { try {
proxy.$('Edit Fields').item; proxy.$('Edit Fields').item;
done('should throw');
} catch (error) { } catch (error) {
expect(error).toBeInstanceOf(ExpressionError); expect(error).toBeInstanceOf(ExpressionError);
const exprError = error as ExpressionError; const exprError = error as ExpressionError;
expect(exprError.message).toEqual('Multiple matches found'); expect(exprError.message).toEqual('Multiple matches found');
expect(exprError.context.type).toEqual('paired_item_multiple_matches'); expect(exprError.context.type).toEqual('paired_item_multiple_matches');
done();
} }
}); });
test('$("NodeName").item, paired item error: missing paired item', (done) => { test('$("NodeName").item, paired item error: missing paired item', () => {
const proxy = getProxyFromFixture(fixture.workflow, fixture.run, 'PairedItemInfoMissing'); const proxy = getProxyFromFixture(fixture.workflow, fixture.run, 'PairedItemInfoMissing');
try { try {
proxy.$('Edit Fields').item; proxy.$('Edit Fields').item;
done('should throw');
} catch (error) { } catch (error) {
expect(error).toBeInstanceOf(ExpressionError); expect(error).toBeInstanceOf(ExpressionError);
const exprError = error as ExpressionError; const exprError = error as ExpressionError;
@@ -342,21 +326,18 @@ describe('WorkflowDataProxy', () => {
"Paired item data for item from node 'Break pairedItem chain' is unavailable. Ensure 'Break pairedItem chain' is providing the required output.", "Paired item data for item from node 'Break pairedItem chain' is unavailable. Ensure 'Break pairedItem chain' is providing the required output.",
); );
expect(exprError.context.type).toEqual('paired_item_no_info'); expect(exprError.context.type).toEqual('paired_item_no_info');
done();
} }
}); });
test('$("NodeName").item, paired item error: invalid paired item', (done) => { test('$("NodeName").item, paired item error: invalid paired item', () => {
const proxy = getProxyFromFixture(fixture.workflow, fixture.run, 'IncorrectPairedItem'); const proxy = getProxyFromFixture(fixture.workflow, fixture.run, 'IncorrectPairedItem');
try { try {
proxy.$('Edit Fields').item; proxy.$('Edit Fields').item;
done('should throw');
} catch (error) { } catch (error) {
expect(error).toBeInstanceOf(ExpressionError); expect(error).toBeInstanceOf(ExpressionError);
const exprError = error as ExpressionError; const exprError = error as ExpressionError;
expect(exprError.message).toEqual("Can't get data for expression"); expect(exprError.message).toEqual("Can't get data for expression");
expect(exprError.context.type).toEqual('paired_item_invalid_info'); expect(exprError.context.type).toEqual('paired_item_invalid_info');
done();
} }
}); });
}); });
@@ -430,7 +411,7 @@ describe('WorkflowDataProxy', () => {
async ({ methodName }) => { async ({ methodName }) => {
try { try {
proxy.$('DebugHelper')[methodName](0); proxy.$('DebugHelper')[methodName](0);
fail('should throw'); throw new Error('should throw');
} catch (e) { } catch (e) {
const error = ensureError(e); const error = ensureError(e);
expect(error.message).toEqual( expect(error.message).toEqual(
@@ -456,7 +437,7 @@ describe('WorkflowDataProxy', () => {
test('item should throw when it cannot find a paired item', async () => { test('item should throw when it cannot find a paired item', async () => {
try { try {
proxy.$('DebugHelper').item; proxy.$('DebugHelper').item;
fail('should throw'); throw new Error('should throw');
} catch (e) { } catch (e) {
const error = ensureError(e); const error = ensureError(e);
expect(error.message).toEqual( expect(error.message).toEqual(

View File

@@ -1,7 +1,8 @@
import { mock } from 'jest-mock-extended'; /* eslint-disable import/order */
import { mock } from 'vitest-mock-extended';
import { UserError } from '@/errors'; import { UserError } from '../src/errors';
import { NodeConnectionTypes } from '@/interfaces'; import { NodeConnectionTypes } from '../src/interfaces';
import type { import type {
IBinaryKeyData, IBinaryKeyData,
IConnection, IConnection,
@@ -12,11 +13,12 @@ import type {
INodeParameters, INodeParameters,
IRunExecutionData, IRunExecutionData,
NodeParameterValueType, NodeParameterValueType,
} from '@/interfaces'; } from '../src/interfaces';
import { Workflow } from '@/workflow'; import { Workflow } from '../src/workflow';
process.env.TEST_VARIABLE_1 = 'valueEnvVariable1'; process.env.TEST_VARIABLE_1 = 'valueEnvVariable1';
// eslint-disable-next-line import/order
import * as Helpers from './helpers'; import * as Helpers from './helpers';
interface StubNode { interface StubNode {
@@ -347,7 +349,7 @@ describe('Workflow', () => {
}); });
beforeEach(() => { beforeEach(() => {
jest.restoreAllMocks(); vi.restoreAllMocks();
}); });
describe('renameNodeInParameterValue', () => { describe('renameNodeInParameterValue', () => {
@@ -2621,7 +2623,7 @@ describe('Workflow', () => {
test('should skip nodes that do not exist and log a warning', () => { test('should skip nodes that do not exist and log a warning', () => {
// Spy on console.warn // Spy on console.warn
const consoleWarnSpy = jest.spyOn(console, 'warn').mockImplementation(); const consoleWarnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
const nodes = SIMPLE_WORKFLOW.getNodes(['Start', 'NonExistentNode', 'Set1']); const nodes = SIMPLE_WORKFLOW.getNodes(['Start', 'NonExistentNode', 'Set1']);
expect(nodes).toHaveLength(2); expect(nodes).toHaveLength(2);
@@ -2634,7 +2636,7 @@ describe('Workflow', () => {
test('should return an empty array if none of the requested nodes exist', () => { test('should return an empty array if none of the requested nodes exist', () => {
// Spy on console.warn // Spy on console.warn
const consoleWarnSpy = jest.spyOn(console, 'warn').mockImplementation(); const consoleWarnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
const nodes = SIMPLE_WORKFLOW.getNodes(['NonExistentNode1', 'NonExistentNode2']); const nodes = SIMPLE_WORKFLOW.getNodes(['NonExistentNode1', 'NonExistentNode2']);
expect(nodes).toHaveLength(0); expect(nodes).toHaveLength(0);

View File

@@ -0,0 +1,10 @@
{
"extends": ["./tsconfig.json", "@n8n/typescript-config/modern/tsconfig.cjs.json"],
"compilerOptions": {
"rootDir": "src",
"outDir": "dist/cjs",
"tsBuildInfoFile": "dist/cjs/typecheck.tsbuildinfo"
},
"include": ["src/**/*.ts"],
"exclude": ["node_modules"]
}

View File

@@ -0,0 +1,10 @@
{
"extends": ["./tsconfig.json"],
"compilerOptions": {
"rootDir": "src",
"outDir": "dist/esm",
"tsBuildInfoFile": "dist/esm/typecheck.tsbuildinfo"
},
"include": ["src/**/*.ts"],
"exclude": ["node_modules"]
}

View File

@@ -1,11 +0,0 @@
{
"extends": ["./tsconfig.json", "@n8n/typescript-config/tsconfig.build.json"],
"compilerOptions": {
"composite": true,
"rootDir": "src",
"outDir": "dist",
"tsBuildInfoFile": "dist/build.tsbuildinfo"
},
"include": ["src/**/*.ts"],
"exclude": ["test/**", "src/**/__tests__/**"]
}

View File

@@ -1,12 +1,10 @@
{ {
"extends": "@n8n/typescript-config/tsconfig.common.json", "extends": "@n8n/typescript-config/modern/tsconfig.json",
"compilerOptions": { "compilerOptions": {
"rootDir": ".", "rootDir": ".",
"baseUrl": "src", "noUncheckedIndexedAccess": false,
"paths": { "types": ["vite/client", "vitest/globals"]
"@/*": ["./*"]
},
"tsBuildInfoFile": "dist/typecheck.tsbuildinfo"
}, },
"include": ["src/**/*.ts", "test/**/*.ts"] "include": ["src/**/*.ts", "test/**/*.ts", "vitest.config.ts"],
"exclude": ["node_modules"]
} }

View File

@@ -0,0 +1,8 @@
/* eslint-disable import-x/no-default-export */
export default async () => {
const { createVitestConfig } = await import('@n8n/vitest-config/node');
return createVitestConfig({
include: ['test/**/*.test.ts'],
});
};

12
patches/js-base64.patch Normal file
View File

@@ -0,0 +1,12 @@
diff --git a/package.json b/package.json
index 5c6ed32cd20c7cb2635bbd43d2b24e5e6771e229..dc1b417593915de2069f55d6afd9f6950fff6c84 100644
--- a/package.json
+++ b/package.json
@@ -12,6 +12,7 @@
],
"exports": {
".": {
+ "types": "./base64.d.ts",
"import": "./base64.mjs",
"require": "./base64.js"
},

636
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -41,14 +41,15 @@ catalog:
tsup: ^8.5.0 tsup: ^8.5.0
tsx: ^4.19.3 tsx: ^4.19.3
uuid: 10.0.0 uuid: 10.0.0
vite: ^6.3.5
vite-plugin-dts: ^4.5.4
vitest: ^3.1.3
vitest-mock-extended: ^3.1.0
xml2js: 0.6.2 xml2js: 0.6.2
xss: 1.0.15 xss: 1.0.15
zod: 3.25.67 zod: 3.25.67
zod-to-json-schema: 3.23.3 zod-to-json-schema: 3.23.3
typescript: 5.8.3 typescript: 5.8.3
vite: 6.3.5
vitest: 3.1.3
vitest-mock-extended: 3.1.0
eslint: 9.29.0 eslint: 9.29.0
catalogs: catalogs: