chore(core): Add VM expression-engine API to Expression class (no-changelog) (#31384)

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Mike Repeć 2026-06-02 10:44:02 +02:00 committed by GitHub
parent dbfd513cd0
commit a9338d0c79
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 144 additions and 1 deletions

View File

@ -26,6 +26,11 @@ const packagesDir = resolve(__dirname, '..', '..');
const alias = [
{ find: '@', replacement: resolve(__dirname, 'src') },
{ find: 'stream', replacement: 'stream-browserify' },
// Stub out @n8n/expression-runtime for browser build (it pulls in isolated-vm, a Node.js-only native module)
{
find: '@n8n/expression-runtime',
replacement: resolve(__dirname, 'vite/expression-runtime-stub.ts'),
},
// Ensure bare imports resolve to sources (not dist)
{ find: '@n8n/i18n', replacement: resolve(packagesDir, 'frontend', '@n8n', 'i18n', 'src') },
{

View File

@ -0,0 +1,54 @@
/**
* Browser stub for @n8n/expression-runtime.
* The real implementation uses isolated-vm (a Node.js-only native module).
* IS_FRONTEND guards in expression.ts prevent these from ever being instantiated.
*/
export class ExpressionEvaluator {
constructor(_config?: unknown) {
throw new Error('ExpressionEvaluator is not available in browser environments');
}
}
export class IsolatedVmBridge {
constructor(_config?: unknown) {
throw new Error('IsolatedVmBridge is not available in browser environments');
}
}
export class ExpressionError extends Error {
constructor(
message: string,
public context: Record<string, unknown> = {},
) {
super(message);
}
}
export class MemoryLimitError extends Error {}
export class TimeoutError extends Error {}
export class SecurityViolationError extends Error {}
// Note: SyntaxError not re-exported to avoid shadowing built-in
export class RuntimeError extends Error {}
export function extend() {}
export function extendOptional() {}
export const EXTENSION_OBJECTS: unknown[] = [];
export class ExpressionExtensionError extends Error {}
export class IsolateError extends Error {}
export const DEFAULT_BRIDGE_CONFIG = {};
// Type-only exports (resolved by TypeScript, erased at runtime)
export type IExpressionEvaluator = never;
export type EvaluatorConfig = never;
export type WorkflowData = Record<string, unknown>;
export type EvaluateOptions = never;
export type RuntimeBridge = never;
export type BridgeConfig = never;
export type ObservabilityProvider = never;
export type MetricsAPI = never;
export type TracesAPI = never;
export type Span = never;
export type LogsAPI = never;
export type ExecuteOptions = never;

View File

@ -52,6 +52,7 @@
},
"dependencies": {
"@n8n/errors": "workspace:*",
"@n8n/expression-runtime": "workspace:*",
"@n8n/tournament": "1.0.6",
"ast-types": "0.16.1",
"callsites": "catalog:",

View File

@ -1,11 +1,19 @@
import { DateTime, Duration, Interval } from 'luxon';
import { ApplicationError } from '@n8n/errors';
import type { IExpressionEvaluator, ObservabilityProvider } from '@n8n/expression-runtime';
import { ExpressionExtensionError } from './errors/expression-extension.error';
import { ExpressionError } from './errors/expression.error';
import { evaluateExpression, setErrorHandler } from './expression-evaluator-proxy';
import { sanitizer, sanitizerName } from './expression-sandboxing';
import {
DollarSignValidator,
PrototypeSanitizer,
ThisSanitizer,
sanitizer,
sanitizerName,
} from './expression-sandboxing';
import { isExpression } from './expressions/expression-helpers';
import * as LoggerProxy from './logger-proxy';
import { extend, extendOptional } from './extensions';
import { extendSyntax } from './extensions/expression-extension';
import { extendedFunctions } from './extensions/extended-functions';
@ -180,6 +188,78 @@ const createSafeErrorSubclass = <T extends ErrorConstructor>(ErrorClass: T): T =
export class Expression {
constructor(private readonly workflow: Workflow) {}
private static expressionEngine: 'legacy' | 'vm' = 'legacy';
private static vmEvaluator?: IExpressionEvaluator;
/**
* Check if VM evaluator should be used for evaluation.
* @private
*/
private static shouldUseVm(): boolean {
return this.expressionEngine === 'vm' && !IS_FRONTEND && !!this.vmEvaluator;
}
/**
* Initialize the VM evaluator (if feature flag is enabled).
* Should be called once during application startup.
* Only available in Node.js environments (not in browser).
*/
static async initExpressionEngine(options: {
engine: 'legacy' | 'vm';
bridgeTimeout: number;
bridgeMemoryLimit: number;
poolSize: number;
maxCodeCacheSize: number;
observability?: ObservabilityProvider;
idleTimeoutMs?: number;
}): Promise<void> {
this.expressionEngine = options.engine;
if (options.engine !== 'vm' || IS_FRONTEND) return;
if (!this.vmEvaluator) {
// Dynamic import to avoid loading expression-runtime in browser environments
const { ExpressionEvaluator, IsolatedVmBridge } = await import('@n8n/expression-runtime');
this.vmEvaluator = new ExpressionEvaluator({
createBridge: () =>
new IsolatedVmBridge({
timeout: options.bridgeTimeout,
memoryLimit: options.bridgeMemoryLimit,
logger: LoggerProxy,
}),
maxCodeCacheSize: options.maxCodeCacheSize,
poolSize: options.poolSize,
idleTimeoutMs: options.idleTimeoutMs,
hooks: {
before: [ThisSanitizer],
after: [PrototypeSanitizer, DollarSignValidator],
},
logger: LoggerProxy,
observability: options.observability,
});
await this.vmEvaluator.initialize();
}
}
async acquireIsolate(): Promise<void> {
if (Expression.vmEvaluator) await Expression.vmEvaluator.acquire(this);
}
async releaseIsolate(): Promise<void> {
if (Expression.vmEvaluator) await Expression.vmEvaluator.release(this);
}
/**
* Dispose the VM evaluator and release resources.
* Should be called during application shutdown or test teardown.
*/
static async disposeExpressionEngine(): Promise<void> {
if (this.vmEvaluator) {
await this.vmEvaluator.dispose();
this.vmEvaluator = undefined;
}
}
static initializeGlobalContext(data: IDataObject) {
/**
* Denylist

View File

@ -3386,6 +3386,9 @@ importers:
'@n8n/errors':
specifier: workspace:*
version: link:../@n8n/errors
'@n8n/expression-runtime':
specifier: workspace:*
version: link:../@n8n/expression-runtime
'@n8n/tournament':
specifier: 1.0.6
version: 1.0.6