mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-17 10:02:05 +00:00
feat: Only send needed data to task runner (no-changelog) (#11487)
This commit is contained in:
@@ -0,0 +1,142 @@
|
||||
import type { CallExpression, Identifier, Node, Program } from 'acorn';
|
||||
import { parse } from 'acorn';
|
||||
import { ancestor } from 'acorn-walk';
|
||||
import type { Result } from 'n8n-workflow';
|
||||
import { toResult } from 'n8n-workflow';
|
||||
|
||||
import {
|
||||
isAssignmentExpression,
|
||||
isIdentifier,
|
||||
isLiteral,
|
||||
isMemberExpression,
|
||||
isVariableDeclarator,
|
||||
} from './acorn-helpers';
|
||||
import { BuiltInsParserState } from './built-ins-parser-state';
|
||||
|
||||
/**
|
||||
* Class for parsing Code Node code to identify which built-in variables
|
||||
* are accessed
|
||||
*/
|
||||
export class BuiltInsParser {
|
||||
/**
|
||||
* Parses which built-in variables are accessed in the given code
|
||||
*/
|
||||
public parseUsedBuiltIns(code: string): Result<BuiltInsParserState, Error> {
|
||||
return toResult(() => {
|
||||
const wrappedCode = `async function VmCodeWrapper() { ${code} }`;
|
||||
const ast = parse(wrappedCode, { ecmaVersion: 2025, sourceType: 'module' });
|
||||
|
||||
return this.identifyBuiltInsByWalkingAst(ast);
|
||||
});
|
||||
}
|
||||
|
||||
/** Traverse the AST of the script and mark any data needed for it to run. */
|
||||
private identifyBuiltInsByWalkingAst(ast: Program) {
|
||||
const accessedBuiltIns = new BuiltInsParserState();
|
||||
|
||||
ancestor(
|
||||
ast,
|
||||
{
|
||||
CallExpression: this.visitCallExpression,
|
||||
Identifier: this.visitIdentifier,
|
||||
},
|
||||
undefined,
|
||||
accessedBuiltIns,
|
||||
);
|
||||
|
||||
return accessedBuiltIns;
|
||||
}
|
||||
|
||||
private visitCallExpression = (
|
||||
node: CallExpression,
|
||||
state: BuiltInsParserState,
|
||||
ancestors: Node[],
|
||||
) => {
|
||||
// $(...)
|
||||
const isDollar = node.callee.type === 'Identifier' && node.callee.name === '$';
|
||||
if (!isDollar) return;
|
||||
|
||||
// $(): This is not valid, ignore
|
||||
if (node.arguments.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const firstArg = node.arguments[0];
|
||||
if (!isLiteral(firstArg)) {
|
||||
// $(variable): Can't easily determine statically, mark all nodes as needed
|
||||
state.markNeedsAllNodes();
|
||||
return;
|
||||
}
|
||||
|
||||
if (typeof firstArg.value !== 'string') {
|
||||
// $(123): Static value, but not a string --> invalid code --> ignore
|
||||
return;
|
||||
}
|
||||
|
||||
// $("node"): Static value, mark 'nodeName' as needed
|
||||
state.markNodeAsNeeded(firstArg.value);
|
||||
|
||||
// Determine how $("node") is used
|
||||
this.handlePrevNodeCall(node, state, ancestors);
|
||||
};
|
||||
|
||||
private handlePrevNodeCall(_node: CallExpression, state: BuiltInsParserState, ancestors: Node[]) {
|
||||
// $("node").item, .pairedItem or .itemMatching: In a case like this, the execution
|
||||
// engine will traverse back from current node (i.e. the Code Node) to
|
||||
// the "node" node and use `pairedItem`s to find which item is linked
|
||||
// to the current item. So, we need to mark all nodes as needed.
|
||||
// TODO: We could also mark all the nodes between the current node and
|
||||
// the "node" node as needed, but that would require more complex logic.
|
||||
const directParent = ancestors[ancestors.length - 2];
|
||||
if (isMemberExpression(directParent)) {
|
||||
const accessedProperty = directParent.property;
|
||||
|
||||
if (directParent.computed) {
|
||||
// $("node")["item"], ["pairedItem"] or ["itemMatching"]
|
||||
if (isLiteral(accessedProperty)) {
|
||||
if (this.isPairedItemProperty(accessedProperty.value)) {
|
||||
state.markNeedsAllNodes();
|
||||
}
|
||||
// Else: $("node")[123]: Static value, but not any of the ones above --> ignore
|
||||
}
|
||||
// $("node")[variable]
|
||||
else if (isIdentifier(accessedProperty)) {
|
||||
state.markNeedsAllNodes();
|
||||
}
|
||||
}
|
||||
// $("node").item, .pairedItem or .itemMatching
|
||||
else if (isIdentifier(accessedProperty) && this.isPairedItemProperty(accessedProperty.name)) {
|
||||
state.markNeedsAllNodes();
|
||||
}
|
||||
} else if (isVariableDeclarator(directParent) || isAssignmentExpression(directParent)) {
|
||||
// const variable = $("node") or variable = $("node"):
|
||||
// In this case we would need to track down all the possible use sites
|
||||
// of 'variable' and determine if `.item` is accessed on it. This is
|
||||
// more complex and skipped for now.
|
||||
// TODO: Optimize for this case
|
||||
state.markNeedsAllNodes();
|
||||
} else {
|
||||
// Something else than the cases above. Mark all nodes as needed as it
|
||||
// could be a dynamic access.
|
||||
state.markNeedsAllNodes();
|
||||
}
|
||||
}
|
||||
|
||||
private visitIdentifier = (node: Identifier, state: BuiltInsParserState) => {
|
||||
if (node.name === '$env') {
|
||||
state.markEnvAsNeeded();
|
||||
} else if (node.name === '$input' || node.name === '$json') {
|
||||
state.markInputAsNeeded();
|
||||
} else if (node.name === '$execution') {
|
||||
state.markExecutionAsNeeded();
|
||||
} else if (node.name === '$prevNode') {
|
||||
state.markPrevNodeAsNeeded();
|
||||
}
|
||||
};
|
||||
|
||||
private isPairedItemProperty(
|
||||
property?: string | boolean | null | number | RegExp | bigint,
|
||||
): boolean {
|
||||
return property === 'item' || property === 'pairedItem' || property === 'itemMatching';
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user