mirror of
https://github.com/n8n-io/n8n.git
synced 2026-06-03 18:27:09 +02:00
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:
parent
dbfd513cd0
commit
a9338d0c79
|
|
@ -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') },
|
||||
{
|
||||
|
|
|
|||
54
packages/frontend/editor-ui/vite/expression-runtime-stub.ts
Normal file
54
packages/frontend/editor-ui/vite/expression-runtime-stub.ts
Normal 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;
|
||||
|
|
@ -52,6 +52,7 @@
|
|||
},
|
||||
"dependencies": {
|
||||
"@n8n/errors": "workspace:*",
|
||||
"@n8n/expression-runtime": "workspace:*",
|
||||
"@n8n/tournament": "1.0.6",
|
||||
"ast-types": "0.16.1",
|
||||
"callsites": "catalog:",
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user