Skip to content

Commit

Permalink
feat: Add AST transform hooks
Browse files Browse the repository at this point in the history
  • Loading branch information
valya authored and netroy committed Aug 13, 2024
1 parent c134729 commit 9b6c605
Show file tree
Hide file tree
Showing 5 changed files with 72 additions and 7 deletions.
2 changes: 1 addition & 1 deletion src/Analysis.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
25 changes: 20 additions & 5 deletions src/ExpressionBuilder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 './';

export interface ExpressionAnalysis {
has: {
Expand Down Expand Up @@ -171,6 +172,7 @@ export const getParsedExpression = (expr: string): Array<ExpressionText | Parsed
export const getExpressionCode = (
expr: string,
dataNodeName: string,
hooks: TournamentHooks,
): [string, ExpressionAnalysis] => {
const chunks = getParsedExpression(expr);

Expand Down Expand Up @@ -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)) {
Expand Down Expand Up @@ -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);
Expand Down
16 changes: 16 additions & 0 deletions src/ast.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
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 type ASTAfterHook = (
ast: namedTypes.ExpressionStatement,
dataNode: namedTypes.ThisExpression | namedTypes.Identifier,
) => void;

export type ASTBeforeHook = (
ast: types.namedTypes.File,
dataNode: namedTypes.ThisExpression | namedTypes.Identifier,
) => void;
11 changes: 10 additions & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,28 @@ import type { ExpressionAnalysis } from './ExpressionBuilder';
import { getTmplDifference } from './Analysis';
import type { ExpressionEvaluator, ExpressionEvaluatorClass } from './Evaluator';
import { FunctionEvaluator } from './FunctionEvaluator';
import type { ASTBeforeHook, ASTAfterHook } 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 interface TournamentHooks {
before: ASTBeforeHook[];
after: ASTAfterHook[];
}

export class Tournament {
private evaluator!: ExpressionEvaluator;

constructor(
public errorHandler: (error: Error) => void = () => {},
private _dataNodeName: string = DATA_NODE_NAME,
Evaluator: ExpressionEvaluatorClass = FunctionEvaluator,
private astHooks: TournamentHooks = { before: [], after: [] },
) {
this.setEvaluator(Evaluator);
}
Expand All @@ -25,7 +34,7 @@ export class Tournament {
}

getExpressionCode(expr: string): [string, ExpressionAnalysis] {
return getExpressionCode(expr, this._dataNodeName);
return getExpressionCode(expr, this._dataNodeName, this.astHooks);
}

tmplDiff(expr: string) {
Expand Down
25 changes: 25 additions & 0 deletions test/ASTHooks.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import type { ExpressionStatement } from 'esprima-next';
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');
expect((ast.program.body[0] as ExpressionStatement).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');
expect((ast as ExpressionStatement).expression.type).toBe('MemberExpression');
};

const t = new Tournament(() => {}, undefined, undefined, { before: [], after: [hook] });
expect(t.execute('{{ test }}', { test: 1 })).toBe(1);
});
});

0 comments on commit 9b6c605

Please sign in to comment.