diff --git a/src/Analysis.ts b/src/Analysis.ts index 00b0d1b..cde85a9 100644 --- a/src/Analysis.ts +++ b/src/Analysis.ts @@ -33,7 +33,7 @@ export const getTmplDifference = (expr: string, dataNodeName: string): TmplDiffe let tmplParsed: string | null; let analysis: ExpressionAnalysis | null; try { - [tournParsed, analysis] = getExpressionCode(expr, dataNodeName); + [tournParsed, analysis] = getExpressionCode(expr, dataNodeName, { before: [], after: [] }); } catch (e) { tournParsed = null; analysis = null; diff --git a/src/ExpressionBuilder.ts b/src/ExpressionBuilder.ts index 137d553..8f7c394 100644 --- a/src/ExpressionBuilder.ts +++ b/src/ExpressionBuilder.ts @@ -10,6 +10,7 @@ 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: { @@ -171,6 +172,7 @@ export const getParsedExpression = (expr: string): Array { const chunks = getParsedExpression(expr); @@ -215,11 +217,19 @@ export const getExpressionCode = ( parts.push(b.literal(chunk.text)); // This is a code chunk so do some magic } else { - const parsed = jsVariablePolyfill(fixStringNewLines(chunk.parsed), dataNode)?.[0]; + 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)) { @@ -263,14 +273,19 @@ export const getExpressionCode = ( ); } } else { - const parsed = jsVariablePolyfill( - fixStringNewLines((chunks[1] as ParsedCode).parsed), - dataNode, - )?.[0]; + 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); diff --git a/src/ast.ts b/src/ast.ts new file mode 100644 index 0000000..db3c1d5 --- /dev/null +++ b/src/ast.ts @@ -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; diff --git a/src/index.ts b/src/index.ts index d25abb5..09f037d 100644 --- a/src/index.ts +++ b/src/index.ts @@ -3,8 +3,11 @@ 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); @@ -16,6 +19,7 @@ export class Tournament { public errorHandler: (error: Error) => void = () => {}, private _dataNodeName: string = DATA_NODE_NAME, Evaluator: ExpressionEvaluatorClass = FunctionEvaluator, + private readonly astHooks: TournamentHooks = { before: [], after: [] }, ) { this.setEvaluator(Evaluator); } @@ -25,7 +29,7 @@ export class Tournament { } getExpressionCode(expr: string): [string, ExpressionAnalysis] { - return getExpressionCode(expr, this._dataNodeName); + return getExpressionCode(expr, this._dataNodeName, this.astHooks); } tmplDiff(expr: string) { diff --git a/test/ASTHooks.test.ts b/test/ASTHooks.test.ts new file mode 100644 index 0000000..d4ced35 --- /dev/null +++ b/test/ASTHooks.test.ts @@ -0,0 +1,30 @@ +import { type ASTAfterHook, type ASTBeforeHook, Tournament } from '../src'; + +describe('AST Hooks', () => { + test('Before hooks are called before variable expansion', () => { + const hook: ASTBeforeHook = (ast) => { + expect(ast.program.body).toHaveLength(1); + expect(ast.program.body[0].type).toBe('ExpressionStatement'); + if (ast.program.body[0].type !== 'ExpressionStatement') { + fail('Expected ExpressionStatement'); + } + expect(ast.program.body[0].expression.type).toBe('Identifier'); + }; + + const t = new Tournament(() => {}, undefined, undefined, { before: [hook], after: [] }); + expect(t.execute('{{ test }}', { test: 1 })).toBe(1); + }); + + test('After hooks are called after variable expansion', () => { + const hook: ASTAfterHook = (ast) => { + expect(ast.type).toBe('ExpressionStatement'); + if (ast.type !== 'ExpressionStatement') { + fail('Expected ExpressionStatement'); + } + expect(ast.expression.type).toBe('MemberExpression'); + }; + + const t = new Tournament(() => {}, undefined, undefined, { before: [], after: [hook] }); + expect(t.execute('{{ test }}', { test: 1 })).toBe(1); + }); +});