JS / TS Dialect Template Reference
Template Hierarchy
The it object passed to every JS/TS template (built in buildTemplateModel()):
it = {
className,
hasTypes, // boolean - true for TS output
imports, // TImports - { [pkg]: string[] }
importNamespaces, // TNullable<TImports>
diagram, // TStateDiagramMatrixIncludeNotes
stateDictionary, // BasicStateDictionary
actionDictionary, // BasicActionDictionary
eventDictionary, // BasicEventDictionary
expressions, // TExpressionRecord
defines, // DefineStatement[]
injects, // InjectStatement[]
injectedPath, // TNullable<string>
functions: {
userFunctionsCheck, // { injectIdentifiers: string[] }
customRegistrations, // TCustomRegistration[]
userFunctionsNamespace, // string | null
},
initialStateId, // string
initialStateValue, // number
initialContext, // Record<string, unknown>
startStateKey, // StartState
byPassAction, // ByPassAction
context: {
reducer, // { entries: { stateValue, transition }[] }
defaultContext, // { startStateValue, transition }
},
forks: {
predicates, // Record<stateId, Record<actionId, { transitions }>>
},
events: {
eventAdapter, // { emitters, listeners }
createEventBus, // { resultVarName }
},
dictionaries: {
actionToStateFromState, // Record<stateId, Record<actionId, { state, withPredicate }>>
byPassedList, // number[]
actionsMap, // Record<string, number>
statesMap, // Record<string, number>
},
}Full template hierarchy diagram
Include order
js/module.eta (7 steps):
js/shared/imports/module-it.imports,it.importNamespacesjs/shared/forks/module-it.forks.predicatesjs/shared/dictionaries/module-it.stateDictionary,it.actionDictionary,it.dictionaries.*js/shared/functions/module-it.functions.*js/shared/events/module-it.events.*js/context/module-it.context.*js/class/module-it.className,it.initialStateValue,it.initialContext,it.byPassAction
ts/module.eta (8 steps):
js/shared/imports/modulejs/shared/forks/modulejs/shared/dictionaries/modulets/types/module-TContext,TPayload,TRootReducertype exportsjs/shared/functions/modulejs/shared/events/modulejs/context/module(hasTypes=true - adds TS type annotations)ts/class/module
Cross-directory includes
| From | To | Data |
|---|---|---|
js/context/reducers/item | js/shared/expressions/context/defaultPropertyContext | { path, identifier, expression } |
js/context/defaultContext | js/context/reducers/item | { transition } |
js/class/module | js/shared/dictionaries/runtime | it |
ts/class/module | js/shared/dictionaries/runtime | it |
js/shared/events/adapter/source | js/shared/expressions/context/defaultPropertyContext | { path, identifier, expression } |
js/shared/functions/registrations | js/shared/expressions/module | { model: registration.bodyModel } |
js/shared/expressions/calls | js/shared/expressions/module (recursive) | { model: arg } |
Reducer Compilation
This section explains how reducer functions for each state are compiled from Yantrix notes into JavaScript/TypeScript code. All the logic described here corresponds to the implementation in packages/codegen/src/core/modules/JavaScript/JavaScriptCompiler/context/serializer.ts.
1. Overall goal
The contextSerializer is responsible for generating two main pieces of runtime code:
const reducer = { [stateId]: (prevContext, _payload, _functionDictionary, _automata) => newContext }const getDefaultContext = (prevContext, _payload) => { ... }
These are later embedded into the generated automata class and are used at runtime to compute the next context for each state transition.
The key exported helpers are:
getStateReducerCode- builds thereducerobject.getStateToContext- generates per-state reducer functions.getContextTransition- computes the context expression for a given state.getContextItem- converts a singlecontextDescriptionblock intokey: expressionpairs.mapReducerItems- converts reducer rows into right-hand-side expressions.getBoundValues- binds intermediate values to final context properties, with fallbacks.getDefaultContext- generates the default context constructor based onStartState.
2. From getStateReducerCode to getContextTransition
At the top level, getStateReducerCode produces the reducer definition:
ts
function getStateReducerCode(props) {
return `const reducer = {
${getStateToContext(props).join(',\n\t')}
}`;
}getStateToContext walks all states in the diagram and creates one reducer function per state:
ts
function getStateToContext(props) {
return props.diagram.states.map((state) => {
const stateValue = props.stateDictionary.getStateValues({ keys: [state.id] })[0];
if (!stateValue) {
throw new Error('Invalid state');
}
return `${stateValue}: (prevContext, payload, functionDictionary, automata) => {
return ${getContextTransition({
value: stateValue,
stateDictionary: props.stateDictionary,
diagram: props.diagram,
expressions: props.expressions,
})}
}`;
});
}For each logical state:
- it looks up the numeric
stateValuefromBasicStateDictionarybystate.id; - it then delegates to
getContextTransition(...), which returns a string expression representing the new context for that state:- either
"prevContext"(identity), - or an object literal string like
{ foo: ..., bar: ... }.
- either
The result is a reducer object of the form:
ts
const reducer = {
// expression reducer: prevContext used; other params unused (body uses module-level functionDictionary)
1: (prevContext, _payload, _functionDictionary, _automata) => {
return { /* compiled context for state 1 */ };
},
// identity reducer: all non-context params unused
2: (prevContext, _payload, _functionDictionary, _automata) => {
return prevContext;
},
// ...
};getContextTransition is the entry point for computing the per-state context expression:
ts
function getContextTransition(props) {
const stateFromDict = props.stateDictionary.getStateKeys({ states: [props.value] })[0];
if (stateFromDict === null) {
throw new Error(`Invalid state - ${props.value}`);
}
const diagramState = props.diagram.states.find((diagramState) => {
return diagramState.id === stateFromDict;
});
if (!diagramState) {
throw new Error(`Invalid state - ${props.value}`);
}
const ctxRes: string[] = [];
diagramState.notes?.contextDescription.forEach((ctx) => {
const newContext = getContextItem({
ctx,
expressions: props.expressions,
});
ctxRes.push(...newContext);
});
if (ctxRes.length === 0) return 'prevContext';
return `{${ctxRes.join(',\n\t')}}`;
};Steps:
- Reverse-lookup
stateFromDictfrom the numericvalueusingstateDictionary.getStateKeys. - Find the corresponding
diagramStateindiagram.states. - For each
ctxindiagramState.notes?.contextDescription, callgetContextItemto obtain a list of"key: expression"strings and accumulate them inctxRes. - If there is no context description (
ctxRes.length === 0), return'prevContext'so the reducer becomes an identity function. - Otherwise, wrap all context entries into an object literal:
`{${ctxRes.join(',\n\t')}}`.
The final reducer function for a state then effectively looks like:
ts
stateValue: (prevContext, _payload, _functionDictionary, _automata) => {
return {
foo: (function(){ ... }()),
bar: (function(){ ... }()),
};
}3. getContextItem: with and without reducer blocks
getContextItem is responsible for transforming a single TContextItem (one block from notes.contextDescription) into an array of "key: expression" strings:
ts
function getContextItem(props: { ctx: TContextItem; expressions: TExpressionRecord; }) {
if (isContextWithReducer(props.ctx)) {
const { context, reducer } = props.ctx;
return getBoundValues({
expressions: props.expressions,
arr: mapReducerItems({ reducer, expressions: props.expressions }),
context,
});
} else {
const { context } = props.ctx;
return context.map(({ keyItem }) => {
const { identifier } = keyItem;
if (isKeyItemWithExpression(keyItem)) {
const expressionValue = expressions.functions.getExpressionValue({
expression: keyItem.expression,
expressionRecord: props.expressions,
});
return `${identifier}: ${expressions.serializer.getDefaultPropertyContext('prevContext', identifier, expressionValue)}`;
} else {
return `${identifier}: ${expressions.serializer.getDefaultPropertyContext('prevContext', identifier)}`;
}
});
}
};There are two major branches:
isContextWithReducer(ctx)is true:- The context block declares an explicit
reducersection in Yantrix notes. - Flow:
mapReducerItemsconverts each reducer row into a right-hand-side expression string, thengetBoundValueszips these with thecontextdefinitions to produce final"targetProperty: expression"pairs. - Output example:ts
[ "foo: (function(){ const boundValue = ...; return boundValue; }())", "bar: (function(){ const boundValue = ...; if(boundValue !== null) return boundValue; else return <fallback>; }())", ]
- The context block declares an explicit
No reducer in
ctx:- For each
keyItemincontext:- If it has an expression,
getExpressionValueturns it into a JS snippet andgetDefaultPropertyContextgenerates code that prefersprevContext[identifier]but falls back to the expression value. - If there is no expression, the value is fully taken from
prevContext[identifier].
- If it has an expression,
- For each
4. mapReducerItems: compiling reducer rows
mapReducerItems takes a reducer: TKeyItems<'reducer'> and produces an array of raw expressions (arr: string[]), which are later bound to target context properties by getBoundValues:
ts
function mapReducerItems(props: {
reducer: TKeyItems<'reducer'>;
sourcePath?: string;
expressions: TExpressionRecord;
}) {
return props.reducer
.map(({ keyItem }) => {
if (isKeyItemReference(keyItem)) {
const { expressionType, identifier: boundIdentifier } = keyItem;
const path = props.sourcePath ?? pathRecord[expressionType];
if (keyItem.expressionType === ExpressionTypes.Constant) {
const expressionValueRight = expressions.functions.getExpressionValue({
expression: keyItem,
expressionRecord: props.expressions,
});
return `(function(){
return ${expressionValueRight}
}())`;
}
if (isKeyItemWithExpression(keyItem)) {
const { expression } = keyItem;
const expressionValueRight = expressions.functions.getExpressionValue({
expression,
expressionRecord: props.expressions,
});
return expressions.serializer.getDefaultPropertyContext(path, boundIdentifier, expressionValueRight);
}
return expressions.serializer.getDefaultPropertyContext(path, boundIdentifier);
} else {
const { expression } = keyItem;
const expressionValueRight = expressions.functions.getExpressionValue({
expression,
expressionRecord: props.expressions,
});
return `(function(){
return ${expressionValueRight}
}())`;
}
});
}Key branches:
isKeyItemReference(keyItem)is true: Row refers to a source path (context, payload, constants, etc.) and binds it to an identifier.expressionType(fromExpressionTypes) determines which path to use viapathRecord[expressionType].expressionType === Constant: wraps resolved constant in an IIFE.isKeyItemWithExpression: buildsgetDefaultPropertyContext(path, id, fallback).- No expression: just
getDefaultPropertyContext(path, id).
isKeyItemReferenceis false: Plain expression wrapped in IIFE.
The result is arr: string[] -- right-hand-side expressions not yet associated with context properties. That association happens in getBoundValues.
5. getBoundValues: zipping values to context properties
getBoundValues takes arr (expressions from mapReducerItems) and context (target context description) and produces final "targetProperty: expression" strings:
ts
function getBoundValues(props: {
expressions: TExpressionRecord;
arr: string[];
context: any;
}) {
return props.arr
.map((el, index) => {
const item = props.context[index];
if (!item) {
throw new Error('Unexpected index bound property');
}
const { keyItem } = item;
const { identifier: targetProperty } = keyItem;
if (isKeyItemWithExpression(keyItem)) {
const { expression } = keyItem;
const expressionValueRight = expressions.functions.getExpressionValue({
expression,
expressionRecord: props.expressions,
});
return `${targetProperty}: (function(){
const boundValue = ${el}
if(boundValue !== null){
return boundValue
}
else {
return ${expressionValueRight}
}
}())`;
} else {
return `${targetProperty}: (function(){
const boundValue = ${el}
return boundValue
}())`;
}
});
}Logic:
- Iterate over
arrwith index, find matchingcontext[index]. - If
keyItemhas its own expression: preferboundValue, fall back to expression if null. - If
keyItemhas no expression: returnboundValuedirectly.
6. getDefaultContext: initial context from StartState
ts
function getDefaultContext(props) {
const state = props.stateDictionary.getStateValues({ keys: [StartState] })[0];
if (state) {
const ctx = getContextTransition({
diagram: props.diagram,
expressions: props.expressions,
stateDictionary: props.stateDictionary,
value: state,
});
return `const getDefaultContext = (prevContext, _payload) => {
const ctx = ${ctx}
return Object.assign({}, prevContext, ctx);
}
`;
}
return `const getDefaultContext = (prevContext, payload) => {
return prevContext
}`;
}Reuses getContextTransition for the start state. If no start state, falls back to identity.
7. Role of TExpressionRecord and getExpressionValue
TExpressionRecord (provided by ../expressions) is a registry that:
- interprets different kinds of Yantrix expressions (context refs, payload refs, constants, function calls, etc.),
- serializes these expressions into JavaScript code snippets,
- provides helpers such as
getDefaultPropertyContext.
ts
expressions.functions.getExpressionValue({
expression,
expressionRecord: props.expressions,
});Takes a high-level Yantrix expression node, dispatches to the correct handler, returns a string of JavaScript code to be inserted into the generated output.
expressions.serializer.getDefaultPropertyContext(...) complements this by generating higher-level patterns that combine access to a source object with optional fallback expressions.
Together these allow the reducer compiler to stay declarative: it does not hardcode the shape of every expression; instead it relies on TExpressionRecord to generate the exact JavaScript code for each case.