Initial commit: n8n Strike API node

- Add Strike API credentials configuration
- Implement Strike node with 9 resources (Account, Balance, Currency Exchange, Deposit, Invoice, Payment, Payment Method, Payout, Rates)
- Add comprehensive operation descriptions for all resources
- Include CLAUDE.MD documentation
- Set up build configuration with TypeScript, ESLint, and Prettier

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-12-10 11:00:38 +01:00
commit 5605b9b49a
8925 changed files with 1417728 additions and 0 deletions

138
node_modules/@n8n/tournament/src/Analysis.ts generated vendored Normal file
View File

@ -0,0 +1,138 @@
import * as tmpl from '@n8n_io/riot-tmpl';
import { print, visit } from 'recast';
import { isDifferent } from './Differ';
import type { ExpressionAnalysis } from './ExpressionBuilder';
import { getExpressionCode, getParsedExpression } from './ExpressionBuilder';
import { joinExpression } from './ExpressionSplitter';
interface TmplOrTournament {
tmpl: boolean;
tournament: boolean;
}
interface TmplSame {
same: true;
expression?: SanitizedString;
}
interface TmplDiff {
same: false;
expression: SanitizedString | 'UNPARSEABLE';
has?: ExpressionAnalysis['has'];
parserError?: TmplOrTournament;
}
export type TmplDifference = TmplSame | TmplDiff;
export const getTmplDifference = (expr: string, dataNodeName: string): TmplDifference => {
if (!expr) {
return { same: true };
}
if (tmpl.brackets.settings.brackets !== '{{ }}') {
tmpl.brackets.set('{{ }}');
}
let tournParsed: string | null;
let tmplParsed: string | null;
let analysis: ExpressionAnalysis | null;
try {
[tournParsed, analysis] = getExpressionCode(expr, dataNodeName, { before: [], after: [] });
} catch (e) {
tournParsed = null;
analysis = null;
}
try {
tmplParsed = tmpl.tmpl.getStr(expr);
} catch (e) {
tmplParsed = null;
}
if (analysis?.has.function || analysis?.has.templateString) {
return {
same: false,
expression: stripIdentifyingInformation(expr),
has: analysis.has,
};
}
if (tournParsed === null && tmplParsed === null) {
// Bad expression
return { same: true };
} else if (tournParsed === null) {
// Unparsable expression for tournament
return {
same: false,
expression: 'UNPARSEABLE',
parserError: {
tmpl: false,
tournament: true,
},
};
} else if (tmplParsed === null) {
// Unparsable expression for tmpl
return {
same: false,
expression: stripIdentifyingInformation(expr),
parserError: {
tmpl: true,
tournament: false,
},
};
}
const different = isDifferent(tmplParsed, tournParsed);
if (different) {
// Different output
return {
same: false,
expression: stripIdentifyingInformation(expr),
};
}
// Same, nothing to report
return { same: true, expression: stripIdentifyingInformation(expr) };
};
const CHAR_REPLACE = /\S/gu;
// Replace all non-whitespace characters with the letter v.
// This should work with all characters since we're just
// matching on all non-whitespace characters, including
// anything outside the ASCII code points.
const replaceValue = (value: string): string => {
return value.replace(CHAR_REPLACE, 'v');
};
export interface SanitizedString {
value: string;
sanitized: 'ACTUALLY_SANITIZED_DO_NOT_MANUALLY_MAKE_THIS_OBJECT';
}
export const stripIdentifyingInformation = (expr: string): SanitizedString => {
const chunks = getParsedExpression(expr);
for (const chunk of chunks) {
if (chunk.type === 'text') {
chunk.text = replaceValue(chunk.text);
} else {
visit(chunk.parsed, {
visitLiteral(path) {
this.traverse(path);
if (typeof path.node.value === 'string') {
path.node.value = replaceValue(path.node.value);
}
},
visitStringLiteral(path) {
this.traverse(path);
path.node.value = replaceValue(path.node.value);
},
visitTemplateElement(path) {
this.traverse(path);
if (path.node.value.cooked !== null) {
path.node.value.cooked = replaceValue(path.node.value.cooked);
}
path.node.value.raw = replaceValue(path.node.value.raw);
},
});
chunk.text = print(chunk.parsed).code;
}
}
return {
value: joinExpression(chunks),
sanitized: 'ACTUALLY_SANITIZED_DO_NOT_MANUALLY_MAKE_THIS_OBJECT',
};
};

26
node_modules/@n8n/tournament/src/Constants.ts generated vendored Normal file
View File

@ -0,0 +1,26 @@
import type {
CatchClauseKind,
ExpressionKind,
PatternKind,
PropertyKind,
StatementKind,
VariableDeclaratorKind,
} from 'ast-types/lib/gen/kinds';
export const EXEMPT_IDENTIFIER_LIST = [
'isFinite',
'isNaN',
'NaN',
'Date',
'RegExp',
'Math',
'undefined',
];
export type ParentKind =
| ExpressionKind
| StatementKind
| PropertyKind
| PatternKind
| VariableDeclaratorKind
| CatchClauseKind;

94
node_modules/@n8n/tournament/src/Differ.ts generated vendored Normal file
View File

@ -0,0 +1,94 @@
import type { StatementKind } from 'ast-types/lib/gen/kinds';
import { types, parse } from 'recast';
import { parseWithEsprimaNext } from './Parser';
const isWrapped = (node: types.namedTypes.File): boolean => {
return node.program.body[1]?.type === 'TryStatement';
};
const getWrapped = (node: types.namedTypes.File) => {
return (node.program.body[1] as types.namedTypes.TryStatement)?.block.body[0];
};
const isMultiPartExpression = (node: types.namedTypes.File): boolean => {
return (
node.program.body[1]?.type === 'ReturnStatement' &&
node.program.body[1].argument?.type === 'CallExpression' &&
node.program.body[1].argument?.arguments[0]?.type === 'Literal' &&
node.program.body[1].argument?.arguments[0]?.value === '' &&
node.program.body[1].argument?.callee.type === 'MemberExpression' &&
node.program.body[1].argument?.callee.object.type === 'ArrayExpression' &&
node.program.body[1].argument?.callee.property.type === 'Identifier' &&
node.program.body[1].argument?.callee.property.name === 'join'
);
};
const getMultiPartExpression = (node: types.namedTypes.File): StatementKind[] => {
if (
!(
node.program.body[1]?.type === 'ReturnStatement' &&
node.program.body[1].argument?.type === 'CallExpression' &&
node.program.body[1].argument?.arguments[0]?.type === 'Literal' &&
node.program.body[1].argument?.arguments[0]?.value === '' &&
node.program.body[1].argument?.callee.type === 'MemberExpression' &&
node.program.body[1].argument?.callee.object.type === 'ArrayExpression'
)
) {
return [];
}
return node.program.body[1].argument.callee.object.elements
.map((e) => {
if (
e?.type !== 'CallExpression' ||
e.callee.type !== 'MemberExpression' ||
e.callee.object.type !== 'FunctionExpression'
) {
return null;
}
const maybeExpr = e.callee.object.body.body[0];
if (maybeExpr.type !== 'TryStatement') {
return maybeExpr;
}
return maybeExpr.block.body[0];
})
.filter((e) => e !== null) as StatementKind[];
};
export const isDifferent = (tmpl: string, tourn: string): boolean => {
const tmplParsed = parse(tmpl, {
parser: { parse: parseWithEsprimaNext },
}) as types.namedTypes.File;
const tournParsed = parse(tourn, {
parser: { parse: parseWithEsprimaNext },
}) as types.namedTypes.File;
const problemPaths: any[] = [];
let same = types.astNodesAreEquivalent(tmplParsed, tournParsed, problemPaths);
// So we sometimes wrap single value expressions when tmpl doesn't.
// This causes our code to obviously be different to tmpl's. It's
// actually more of a bug on tmpl's side and it's incredibly hard
// to match exactly on our end. I think this is an acceptable difference
// in behaviours and shouldn't cause any issues since an exception will
// be raised either way.
if (!same) {
if (isWrapped(tournParsed) && !isWrapped(tmplParsed)) {
// This is a single part expression that might be wrapped
const tournWrapped = getWrapped(tournParsed);
same = types.astNodesAreEquivalent(tmplParsed.program.body[1], tournWrapped);
} else if (isMultiPartExpression(tournParsed) && isMultiPartExpression(tmplParsed)) {
// This is a multi part expression that may be wrapped internally
const tournExprs = getMultiPartExpression(tournParsed);
const tmplExprs = getMultiPartExpression(tmplParsed);
if (tournExprs.length === tmplExprs.length) {
for (let i = 0; i < tournExprs.length; i++) {
same = types.astNodesAreEquivalent(tmplExprs[i], tournExprs[i], problemPaths);
if (!same) {
break;
}
}
}
}
}
return !same;
};

9
node_modules/@n8n/tournament/src/Evaluator.ts generated vendored Normal file
View File

@ -0,0 +1,9 @@
import type { ReturnValue, Tournament } from '.';
export interface ExpressionEvaluator {
evaluate(code: string, data: unknown): ReturnValue;
}
export interface ExpressionEvaluatorClass {
new (instance: Tournament): ExpressionEvaluator;
}

297
node_modules/@n8n/tournament/src/ExpressionBuilder.ts generated vendored Normal file
View File

@ -0,0 +1,297 @@
import type { namedTypes } from 'ast-types';
import type { types } from 'recast';
import { parse, visit, print } from 'recast';
import { builders as b } from 'ast-types';
import type { ExpressionKind, StatementKind } from 'ast-types/lib/gen/kinds';
import { globalIdentifier, jsVariablePolyfill } from './VariablePolyfill';
import type { DataNode } from './VariablePolyfill';
import { splitExpression } from './ExpressionSplitter';
import type { ExpressionCode, ExpressionText } from './ExpressionSplitter';
import { parseWithEsprimaNext } from './Parser';
import type { TournamentHooks } from './ast';
export interface ExpressionAnalysis {
has: {
function: boolean;
templateString: boolean;
};
}
const v = b.identifier('v');
const shouldAlwaysWrapList = ['window', 'global', 'this'];
const shouldWrapInTry = (node: namedTypes.ASTNode) => {
let shouldWrap = false;
visit(node, {
visitMemberExpression() {
shouldWrap = true;
return false;
},
visitCallExpression() {
shouldWrap = true;
return false;
},
visitIdentifier(path) {
if (shouldAlwaysWrapList.includes(path.node.name)) {
shouldWrap = true;
return false;
}
this.traverse(path);
return;
},
});
return shouldWrap;
};
const hasFunction = (node: types.namedTypes.ASTNode) => {
let hasFn = false;
visit(node, {
visitFunctionExpression() {
hasFn = true;
return false;
},
visitFunctionDeclaration() {
hasFn = true;
return false;
},
visitArrowFunctionExpression() {
hasFn = true;
return false;
},
});
return hasFn;
};
const hasTemplateString = (node: types.namedTypes.ASTNode) => {
let hasTemp = false;
visit(node, {
visitTemplateLiteral(path) {
if (path.node.expressions.length) {
hasTemp = true;
return false;
}
this.traverse(path);
return;
},
});
return hasTemp;
};
const wrapInErrorHandler = (node: StatementKind) => {
return b.tryStatement(
b.blockStatement([node]),
b.catchClause(
b.identifier('e'),
null,
b.blockStatement([
b.expressionStatement(
b.callExpression(b.identifier('E'), [b.identifier('e'), b.thisExpression()]),
),
]),
),
);
};
const maybeWrapExpr = (expr: string): string => {
if (expr.trimStart()[0] === '{') {
return '(' + expr + ')';
}
return expr;
};
const buildFunctionBody = (expr: ExpressionKind) => {
return b.blockStatement([
// v = (<actual expression>)
b.expressionStatement(b.assignmentExpression('=', v, expr)),
// Return value or empty string on some falsy values
// return v || v === 0 || v === false ? v : 0
// The ordering is important for AST nodes to match
// tmpl's output
b.returnStatement(
b.conditionalExpression(
b.logicalExpression(
'||',
b.logicalExpression('||', v, b.binaryExpression('===', v, b.literal(0))),
b.binaryExpression('===', v, b.literal(false)),
),
v,
b.literal(''),
),
),
]);
};
type ParsedCode = ExpressionCode & { parsed: types.namedTypes.File };
// This replaces any actual new lines with \n's. This only happens in
// template strings.
const fixStringNewLines = (node: types.namedTypes.File): types.namedTypes.File => {
const replace = (str: string): string => {
return str.replace(/\n/g, '\\n');
};
visit(node, {
visitTemplateElement(path) {
this.traverse(path);
const el = b.templateElement(
{
cooked: path.node.value.cooked === null ? null : replace(path.node.value.cooked),
raw: replace(path.node.value.raw),
},
path.node.tail,
);
path.replace(el);
},
});
return node;
};
export const getParsedExpression = (expr: string): Array<ExpressionText | ParsedCode> => {
return splitExpression(expr).map<ExpressionText | ParsedCode>((chunk) => {
if (chunk.type === 'code') {
const code = maybeWrapExpr(chunk.text);
const node = parse(code, {
parser: { parse: parseWithEsprimaNext },
}) as types.namedTypes.File;
return { ...chunk, parsed: node };
}
return chunk;
});
};
export const getExpressionCode = (
expr: string,
dataNodeName: string,
hooks: TournamentHooks,
): [string, ExpressionAnalysis] => {
const chunks = getParsedExpression(expr);
const newProg = b.program([
b.variableDeclaration('var', [b.variableDeclarator(globalIdentifier, b.objectExpression([]))]),
]);
// This is what's used to access that's passed in. For compatibility we us
// `this` unless the expression contains a function. If it contains an
// expression we instead assign a different variable to hold onto the contents
// of `this` (default: `___n8n_data`) since functions aren't compatibility
// anyway.
let dataNode: DataNode = b.thisExpression();
const hasFn = (chunks.filter((c) => c.type === 'code') as ParsedCode[]).some((c) =>
hasFunction(c.parsed),
);
if (hasFn) {
dataNode = b.identifier(dataNodeName);
newProg.body.push(
b.variableDeclaration('var', [b.variableDeclarator(dataNode, b.thisExpression())]),
);
}
const hasTempString = (chunks.filter((c) => c.type === 'code') as ParsedCode[]).some((c) =>
hasTemplateString(c.parsed),
);
// So for compatibility we parse expressions the same way that `tmpl` does.
// This means we always have an initial text chunk but if there's only a blank
// text chunk and a code chunk then we want to return the actual value of the
// expression, not turn it into a string.
if (
chunks.length > 2 ||
chunks[0].text !== '' ||
// This is a blank expression. It should just return an empty string
(chunks[0].text === '' && chunks.length === 1)
) {
let parts: ExpressionKind[] = [];
for (const chunk of chunks) {
// This is just a text chunks, push it up as a literal.
if (chunk.type === 'text') {
parts.push(b.literal(chunk.text));
// This is a code chunk so do some magic
} else {
const fixed = fixStringNewLines(chunk.parsed);
for (const hook of hooks.before) {
hook(fixed, dataNode);
}
const parsed = jsVariablePolyfill(fixed, dataNode)?.[0];
if (!parsed || parsed.type !== 'ExpressionStatement') {
throw new SyntaxError('Not a expression statement');
}
for (const hook of hooks.after) {
hook(parsed, dataNode);
}
const functionBody = buildFunctionBody(parsed.expression);
if (shouldWrapInTry(parsed)) {
// Wraps the body of our expression function in a try/catch
// to match tmpl
functionBody.body = [
wrapInErrorHandler(functionBody.body[0]),
// This is for tmpl compat. It puts a ; after the try/catch
// creating an empty statement. emptyStatement is just printed
// to nothing so we use an expression statement with a blank
// identifier.
b.expressionStatement(b.identifier('')),
functionBody.body[1],
];
}
// Turn our expression into a function call with bound this. The function
// it create has a parameter called `v` that we don't actually set. I think
// this is a hack around only being able to use `var` and not `let`/`const`.
parts.push(
b.callExpression(
b.memberExpression(b.functionExpression(null, [v], functionBody), b.identifier('call')),
[b.thisExpression()],
),
);
}
}
// Just return the raw string if it's just a single string
if (chunks.length < 2) {
newProg.body.push(b.returnStatement(parts[0]));
} else {
// Filter out empty string literals for compat
parts = parts.filter((i) => !(i.type === 'Literal' && i.value === ''));
newProg.body.push(
b.returnStatement(
b.callExpression(b.memberExpression(b.arrayExpression(parts), b.identifier('join')), [
b.literal(''),
]),
),
);
}
} else {
const fixed = fixStringNewLines((chunks[1] as ParsedCode).parsed);
for (const hook of hooks.before) {
hook(fixed, dataNode);
}
const parsed = jsVariablePolyfill(fixed, dataNode)?.[0];
if (!parsed || parsed.type !== 'ExpressionStatement') {
throw new SyntaxError('Not a expression statement');
}
for (const hook of hooks.after) {
hook(parsed, dataNode);
}
let retData: StatementKind = b.returnStatement(parsed.expression);
if (shouldWrapInTry(parsed)) {
retData = wrapInErrorHandler(retData);
}
newProg.body.push(retData);
}
return [print(newProg).code, { has: { function: hasFn, templateString: hasTempString } }];
};

108
node_modules/@n8n/tournament/src/ExpressionSplitter.ts generated vendored Normal file
View File

@ -0,0 +1,108 @@
export interface ExpressionText {
type: 'text';
text: string;
}
export interface ExpressionCode {
type: 'code';
text: string;
// tmpl has different behaviours if the last expression
// doesn't close itself.
hasClosingBrackets: boolean;
}
export type ExpressionChunk = ExpressionCode | ExpressionText;
const OPEN_BRACKET = /(?<brackets>\{\{)/;
const CLOSE_BRACKET = /(?<brackets>\}\})/;
export const escapeCode = (text: string): string => {
return text.replace('\\}}', '}}');
};
const normalizeBackslashes = (text: string): string => {
return text.replace(/\\\\/g, '\\');
};
export const splitExpression = (expression: string): ExpressionChunk[] => {
const chunks: ExpressionChunk[] = [];
let searchingFor: 'open' | 'close' = 'open';
let activeRegex = OPEN_BRACKET;
let buffer = '';
let index = 0;
while (index < expression.length) {
const expr = expression.slice(index);
const res = activeRegex.exec(expr);
// No more brackets. If it's a closing bracket
// this is sort of valid so we accept it but mark
// that it has no closing bracket.
if (!res?.groups) {
buffer += expr;
if (searchingFor === 'open') {
chunks.push({
type: 'text',
text: buffer,
});
} else {
chunks.push({
type: 'code',
text: escapeCode(buffer),
hasClosingBrackets: false,
});
}
break;
}
const beforeMatch = expr.slice(0, res.index);
const backslashCount = beforeMatch.match(/\\*$/)?.[0]?.length ?? 0;
const isEscaped = backslashCount % 2 === 1;
if (isEscaped) {
buffer += expr.slice(0, res.index + '{{'.length);
index += res.index + '{{'.length;
} else {
buffer += expr.slice(0, res.index);
if (searchingFor === 'open') {
chunks.push({
type: 'text',
text: normalizeBackslashes(buffer),
});
searchingFor = 'close';
activeRegex = CLOSE_BRACKET;
} else {
chunks.push({
type: 'code',
text: escapeCode(buffer),
hasClosingBrackets: true,
});
searchingFor = 'open';
activeRegex = OPEN_BRACKET;
}
index += res.index + 2;
buffer = '';
}
}
return chunks;
};
// Expressions only have closing brackets escaped
const escapeTmplExpression = (part: string) => {
return part.replace('}}', '\\}}');
};
export const joinExpression = (parts: ExpressionChunk[]): string => {
return parts
.map((chunk) => {
if (chunk.type === 'code') {
return `{{${escapeTmplExpression(chunk.text)}${chunk.hasClosingBrackets ? '}}' : ''}`;
}
return chunk.text;
})
.join('');
};

22
node_modules/@n8n/tournament/src/FunctionEvaluator.ts generated vendored Normal file
View File

@ -0,0 +1,22 @@
import type { ExpressionEvaluator, ReturnValue, Tournament } from '.';
export class FunctionEvaluator implements ExpressionEvaluator {
private _codeCache: Record<string, Function> = {};
constructor(private instance: Tournament) {}
private getFunction(expr: string): Function {
if (expr in this._codeCache) {
return this._codeCache[expr];
}
const [code] = this.instance.getExpressionCode(expr);
const func = new Function('E', code + ';');
this._codeCache[expr] = func;
return func;
}
evaluate(expr: string, data: unknown): ReturnValue {
const fn = this.getFunction(expr);
return fn.call(data, this.instance.errorHandler);
}
}

23
node_modules/@n8n/tournament/src/Parser.ts generated vendored Normal file
View File

@ -0,0 +1,23 @@
import { parse as esprimaParse } from 'esprima-next';
import type { Config as EsprimaConfig } from 'esprima-next';
import { getOption } from 'recast/lib/util';
export function parseWithEsprimaNext(source: string, options?: any): any {
try {
const ast = esprimaParse(source, {
loc: true,
locations: true,
comment: true,
range: getOption(options, 'range', false) as boolean,
tolerant: getOption(options, 'tolerant', true) as boolean,
tokens: true,
jsx: getOption(options, 'jsx', false) as boolean,
sourceType: getOption(options, 'sourceType', 'module') as string,
} as EsprimaConfig);
return ast;
} catch (error) {
if (error instanceof Error) throw new SyntaxError(error.message);
throw error;
}
}

313
node_modules/@n8n/tournament/src/VariablePolyfill.ts generated vendored Normal file
View File

@ -0,0 +1,313 @@
import type { types } from 'recast';
import { visit } from 'recast';
import type { StatementKind, VariableDeclaratorKind } from 'ast-types/lib/gen/kinds';
import type { NodePath } from 'ast-types/lib/node-path';
import type { namedTypes } from 'ast-types';
import { builders as b } from 'ast-types';
import type { ParentKind } from './Constants';
import { EXEMPT_IDENTIFIER_LIST } from './Constants';
function assertNever(value: never): value is never {
return true;
}
export const globalIdentifier = b.identifier(
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
typeof window !== 'object' ? 'global' : 'window',
);
const buildGlobalSwitch = (node: types.namedTypes.Identifier, dataNode: DataNode) => {
return b.memberExpression(
b.conditionalExpression(
b.binaryExpression('in', b.literal(node.name), dataNode),
dataNode,
globalIdentifier,
),
b.identifier(node.name),
);
};
const isInScope = (path: NodePath<types.namedTypes.Identifier>) => {
let scope = path.scope;
while (scope !== null) {
if (scope.declares(path.node.name)) {
return true;
}
scope = scope.parent;
}
return false;
};
const polyfillExceptions = ['this', 'window', 'global'];
const polyfillVar = (
path: NodePath<types.namedTypes.Identifier>,
dataNode: DataNode,
force: boolean = false,
) => {
if (!force) {
if (isInScope(path)) {
// console.log('In scope', path.node.name);
return;
}
}
// For tmpl compat we ignore these identifiers
if (polyfillExceptions.includes(path.node.name)) {
return;
}
path.replace(buildGlobalSwitch(path.node, dataNode));
};
export type DataNode = namedTypes.ThisExpression | namedTypes.Identifier;
type CustomPatcher = (
path: NodePath<types.namedTypes.Identifier>,
parent: any,
dataNode: DataNode,
) => void;
const customPatches: Partial<Record<ParentKind['type'], CustomPatcher>> = {
MemberExpression(path, parent: namedTypes.MemberExpression, dataNode) {
if (parent.object === path.node || parent.computed) {
polyfillVar(path, dataNode);
}
},
OptionalMemberExpression(path, parent: namedTypes.OptionalMemberExpression, dataNode) {
if (parent.object === path.node) {
polyfillVar(path, dataNode);
}
},
Property(path, parent: namedTypes.Property, dataNode) {
if (path.node !== parent.value) {
return;
}
const objPattern: namedTypes.ObjectPattern = path.parent?.parent?.node;
if (!objPattern) {
return;
}
const objParent: VariableDeclaratorKind = path.parent.parent.parent?.node;
if (!objParent) {
return;
}
if (objParent.type === 'VariableDeclarator' && objParent.id === objPattern) {
return;
}
parent.shorthand = false;
polyfillVar(path, dataNode);
},
AssignmentPattern(path, parent: namedTypes.AssignmentPattern, dataNode) {
if (parent.right === path.node) {
polyfillVar(path, dataNode);
}
},
VariableDeclarator(path, parent: namedTypes.VariableDeclarator, dataNode) {
if (parent.init === path.node) {
polyfillVar(path, dataNode);
}
},
};
export const jsVariablePolyfill = (
ast: types.namedTypes.File,
dataNode: DataNode,
): StatementKind[] | undefined => {
visit(ast, {
visitImportExpression(_path) {
throw new Error('Imports are not supported');
},
visitIdentifier(path) {
this.traverse(path);
const parent: ParentKind = path.parent.node;
// This is for tmpl compat
if (EXEMPT_IDENTIFIER_LIST.includes(path.node.name)) {
return;
}
switch (parent.type) {
case 'AssignmentPattern':
case 'Property':
case 'MemberExpression':
case 'OptionalMemberExpression':
case 'VariableDeclarator':
if (!customPatches[parent.type]) {
throw new Error(`Couldn\'t find custom patcher for parent type: ${parent.type}`);
}
customPatches[parent.type]!(path, parent, dataNode);
break;
case 'BinaryExpression':
case 'UnaryExpression':
case 'ArrayExpression':
case 'AssignmentExpression':
case 'SequenceExpression':
case 'YieldExpression':
case 'UpdateExpression':
case 'LogicalExpression':
case 'ConditionalExpression':
case 'NewExpression':
case 'CallExpression':
case 'OptionalCallExpression':
case 'TaggedTemplateExpression':
case 'TemplateLiteral':
case 'AwaitExpression':
case 'ImportExpression':
case 'ForStatement':
case 'IfStatement':
case 'WhileStatement':
case 'ForInStatement':
case 'ForOfStatement':
case 'SwitchStatement':
case 'ReturnStatement':
case 'DoWhileStatement':
case 'ExpressionStatement':
case 'ForAwaitStatement':
case 'ThrowStatement':
case 'WithStatement':
case 'TupleExpression':
polyfillVar(path, dataNode);
break;
// Do nothing
case 'Super':
case 'Identifier':
case 'ArrowFunctionExpression':
case 'FunctionDeclaration':
case 'FunctionExpression':
case 'ThisExpression':
case 'ObjectExpression':
case 'MetaProperty':
case 'ChainExpression':
case 'PrivateName':
case 'ParenthesizedExpression':
case 'Import':
case 'VariableDeclaration':
case 'CatchClause':
case 'BlockStatement':
case 'TryStatement':
case 'EmptyStatement':
case 'LabeledStatement':
case 'BreakStatement':
case 'ContinueStatement':
case 'DebuggerStatement':
case 'ImportDeclaration':
case 'ExportDeclaration':
case 'ExportAllDeclaration':
case 'ExportDefaultDeclaration':
case 'Noop':
case 'ClassMethod':
case 'ClassPrivateMethod':
case 'RestElement':
case 'ArrayPattern':
case 'ObjectPattern':
case 'ClassExpression':
case 'RecordExpression':
case 'V8IntrinsicIdentifier':
case 'TopicReference':
case 'MethodDefinition':
case 'ClassDeclaration':
case 'ClassProperty':
case 'StaticBlock':
case 'ClassBody':
case 'ExportNamedDeclaration':
case 'ClassPrivateProperty':
case 'ClassAccessorProperty':
case 'PropertyPattern':
break;
// I can't seem to figure out what causes these
case 'SpreadElementPattern':
case 'SpreadPropertyPattern':
case 'ClassPropertyDefinition':
break;
// Flow types
case 'DeclareClass':
case 'DeclareModule':
case 'DeclareVariable':
case 'DeclareFunction':
case 'DeclareInterface':
case 'DeclareTypeAlias':
case 'DeclareOpaqueType':
case 'DeclareModuleExports':
case 'DeclareExportDeclaration':
case 'DeclareExportAllDeclaration':
case 'InterfaceDeclaration':
case 'TypeAlias':
case 'OpaqueType':
case 'EnumDeclaration':
case 'TypeCastExpression':
break;
// Typescript types
case 'TSAsExpression':
case 'TSTypeParameter':
case 'TSTypeAssertion':
case 'TSDeclareMethod':
case 'TSIndexSignature':
case 'TSDeclareFunction':
case 'TSMethodSignature':
case 'TSEnumDeclaration':
case 'TSExportAssignment':
case 'TSNonNullExpression':
case 'TSPropertySignature':
case 'TSModuleDeclaration':
case 'TSParameterProperty':
case 'TSTypeCastExpression':
case 'TSSatisfiesExpression':
case 'TSTypeAliasDeclaration':
case 'TSInterfaceDeclaration':
case 'TSImportEqualsDeclaration':
case 'TSExternalModuleReference':
case 'TSInstantiationExpression':
case 'TSTypeParameterDeclaration':
case 'TSCallSignatureDeclaration':
case 'TSNamespaceExportDeclaration':
case 'TSConstructSignatureDeclaration':
break;
// Literals that can't contain an identifier
case 'DirectiveLiteral':
case 'StringLiteral':
case 'NumericLiteral':
case 'BigIntLiteral':
case 'NullLiteral':
case 'Literal':
case 'RegExpLiteral':
case 'BooleanLiteral':
case 'DecimalLiteral':
break;
// Proposals that are stage 0 or 1
case 'DoExpression':
case 'BindExpression':
break;
// JSX stuff. We don't support this so just do nothing.
case 'JSXIdentifier':
case 'JSXText':
case 'JSXElement':
case 'JSXFragment':
case 'JSXMemberExpression':
case 'JSXExpressionContainer':
break;
// I _think_ these are obsolete features proposed as part of ECMAScript 7.
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Deprecated_and_obsolete_features#legacy_generator_and_iterator
case 'ComprehensionExpression':
case 'GeneratorExpression':
polyfillVar(path, dataNode);
break;
default:
// This is a simple type guard that guarantees we haven't missed
// a case. It'll result in a type error at compile time.
assertNever(parent);
break;
}
},
});
return ast.program.body as StatementKind[];
};

21
node_modules/@n8n/tournament/src/ast.ts generated vendored Normal file
View File

@ -0,0 +1,21 @@
export type { types as astTypes } from 'recast';
export { visit as astVisit } from 'recast';
export { builders as astBuilders, type namedTypes as astNamedTypes } from 'ast-types';
import type { types } from 'recast';
import type { namedTypes } from 'ast-types';
export interface TournamentHooks {
before: ASTBeforeHook[];
after: ASTAfterHook[];
}
export type ASTAfterHook = (
ast: namedTypes.ExpressionStatement,
dataNode: namedTypes.ThisExpression | namedTypes.Identifier,
) => void;
export type ASTBeforeHook = (
ast: types.namedTypes.File,
dataNode: namedTypes.ThisExpression | namedTypes.Identifier,
) => void;

47
node_modules/@n8n/tournament/src/index.ts generated vendored Normal file
View File

@ -0,0 +1,47 @@
import { getExpressionCode } from './ExpressionBuilder';
import type { ExpressionAnalysis } from './ExpressionBuilder';
import { getTmplDifference } from './Analysis';
import type { ExpressionEvaluator, ExpressionEvaluatorClass } from './Evaluator';
import { FunctionEvaluator } from './FunctionEvaluator';
import type { TournamentHooks } from './ast';
export type { TmplDifference } from './Analysis';
export type { ExpressionEvaluator, ExpressionEvaluatorClass } from './Evaluator';
export * from './ast';
const DATA_NODE_NAME = '___n8n_data';
export type ReturnValue = string | null | (() => unknown);
export class Tournament {
private evaluator!: ExpressionEvaluator;
constructor(
public errorHandler: (error: Error) => void = () => {},
private _dataNodeName: string = DATA_NODE_NAME,
Evaluator: ExpressionEvaluatorClass = FunctionEvaluator,
private readonly astHooks: TournamentHooks = { before: [], after: [] },
) {
this.setEvaluator(Evaluator);
}
setEvaluator(Evaluator: ExpressionEvaluatorClass) {
this.evaluator = new Evaluator(this);
}
getExpressionCode(expr: string): [string, ExpressionAnalysis] {
return getExpressionCode(expr, this._dataNodeName, this.astHooks);
}
tmplDiff(expr: string) {
return getTmplDifference(expr, this._dataNodeName);
}
execute(expr: string, data: unknown): ReturnValue {
// This is to match tmpl. This will only really happen if
// an empty expression is passed in.
if (!expr) {
return expr;
}
return this.evaluator.evaluate(expr, data);
}
}