@rohal12/spindle 0.42.0 → 0.43.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rohal12/spindle",
3
- "version": "0.42.0",
3
+ "version": "0.43.1",
4
4
  "type": "module",
5
5
  "description": "A Preact-based story format for Twine 2.",
6
6
  "license": "Unlicense",
@@ -61,11 +61,13 @@ function computeAndApply(
61
61
  variables: Record<string, unknown>,
62
62
  temporary: Record<string, unknown>,
63
63
  locals: Record<string, unknown>,
64
+ transient: Record<string, unknown>,
64
65
  rawArgs: string,
66
+ prevRef: { current: unknown },
65
67
  ): void {
66
68
  let newValue: unknown;
67
69
  try {
68
- newValue = evaluate(expr, variables, temporary, locals);
70
+ newValue = evaluate(expr, variables, temporary, locals, transient);
69
71
  } catch (err) {
70
72
  console.error(
71
73
  `spindle: Error in {computed ${rawArgs}}${currentSourceLocation()}:`,
@@ -74,8 +76,8 @@ function computeAndApply(
74
76
  return;
75
77
  }
76
78
 
77
- const current = isTemp ? temporary[name] : variables[name];
78
- if (!valuesEqual(current, newValue)) {
79
+ if (!valuesEqual(prevRef.current, newValue)) {
80
+ prevRef.current = newValue;
79
81
  const state = useStoryStore.getState();
80
82
  if (isTemp) state.setTemporary(name, newValue);
81
83
  else state.setVariable(name, newValue);
@@ -86,7 +88,7 @@ defineMacro({
86
88
  name: 'computed',
87
89
  merged: true,
88
90
  render({ rawArgs }, ctx) {
89
- const [mergedVars, mergedTemps, mergedLocals] = ctx.merged!;
91
+ const [mergedVars, mergedTemps, mergedLocals, mergedTrans] = ctx.merged!;
90
92
 
91
93
  let target: string;
92
94
  let expr: string;
@@ -103,6 +105,8 @@ defineMacro({
103
105
  const isTemp = target.startsWith('_');
104
106
  const name = target.slice(1);
105
107
 
108
+ const prevOutput = ctx.hooks.useRef<unknown>(undefined);
109
+
106
110
  const ran = ctx.hooks.useRef(false);
107
111
  if (!ran.current) {
108
112
  ran.current = true;
@@ -113,7 +117,9 @@ defineMacro({
113
117
  mergedVars,
114
118
  mergedTemps,
115
119
  mergedLocals,
120
+ mergedTrans,
116
121
  rawArgs,
122
+ prevOutput,
117
123
  );
118
124
  }
119
125
 
@@ -125,9 +131,11 @@ defineMacro({
125
131
  mergedVars,
126
132
  mergedTemps,
127
133
  mergedLocals,
134
+ mergedTrans,
128
135
  rawArgs,
136
+ prevOutput,
129
137
  );
130
- }, [mergedVars, mergedTemps, mergedLocals]);
138
+ }, [mergedVars, mergedTemps, mergedLocals, mergedTrans]);
131
139
 
132
140
  return null;
133
141
  },
@@ -17,10 +17,17 @@ export function ExprDisplay({ expression, className, id }: ExprDisplayProps) {
17
17
  const localsValues = useContext(LocalsValuesContext);
18
18
  const variables = useStoryStore((s) => s.variables);
19
19
  const temporary = useStoryStore((s) => s.temporary);
20
+ const transient = useStoryStore((s) => s.transient);
20
21
 
21
22
  let display: string;
22
23
  try {
23
- const value = evaluate(expression, variables, temporary, localsValues);
24
+ const value = evaluate(
25
+ expression,
26
+ variables,
27
+ temporary,
28
+ localsValues,
29
+ transient,
30
+ );
24
31
  display = value == null ? '' : String(value);
25
32
  } catch {
26
33
  display = `{error: ${expression}}`;
@@ -53,21 +53,35 @@ function parseForArgs(rawArgs: string): {
53
53
 
54
54
  function ForIteration({
55
55
  parentValues,
56
- ownKeys,
57
- initialValues,
56
+ itemVar,
57
+ itemValue,
58
+ indexVar,
59
+ indexValue,
58
60
  children,
59
61
  }: {
60
62
  parentValues: Record<string, unknown>;
61
- ownKeys: Record<string, unknown>;
62
- initialValues: Record<string, unknown>;
63
+ itemVar: string;
64
+ itemValue: unknown;
65
+ indexVar: string | null;
66
+ indexValue: number;
63
67
  children: ASTNode[];
64
68
  }) {
65
69
  const [localMutations, setLocalMutations] = useState<Record<string, unknown>>(
66
- () => ({ ...initialValues }),
70
+ () => ({}),
67
71
  );
68
72
 
69
- // Recomputed every render — picks up new parentValues/ownKeys from parent
70
- const localState = { ...parentValues, ...ownKeys, ...localMutations };
73
+ const ownKeys = useMemo(
74
+ () => ({
75
+ [itemVar]: itemValue,
76
+ ...(indexVar ? { [indexVar]: indexValue } : undefined),
77
+ }),
78
+ [itemVar, itemValue, indexVar, indexValue],
79
+ );
80
+
81
+ const localState = useMemo(
82
+ () => ({ ...parentValues, ...ownKeys, ...localMutations }),
83
+ [parentValues, ownKeys, localMutations],
84
+ );
71
85
 
72
86
  const valuesRef = useRef(localState);
73
87
  valuesRef.current = localState;
@@ -131,22 +145,17 @@ defineMacro({
131
145
  );
132
146
  }
133
147
 
134
- const content = list.map((item, i) => {
135
- const ownKeys: Record<string, unknown> = {
136
- [itemVar]: item,
137
- ...(indexVar ? { [indexVar]: i } : undefined),
138
- };
139
-
140
- return (
141
- <ForIteration
142
- key={`${i}-${JSON.stringify(item)}`}
143
- parentValues={parentValues}
144
- ownKeys={ownKeys}
145
- initialValues={{}}
146
- children={children}
147
- />
148
- );
149
- });
148
+ const content = list.map((item, i) => (
149
+ <ForIteration
150
+ key={`${i}-${JSON.stringify(item)}`}
151
+ parentValues={parentValues}
152
+ itemVar={itemVar}
153
+ itemValue={item}
154
+ indexVar={indexVar}
155
+ indexValue={i}
156
+ children={children}
157
+ />
158
+ ));
150
159
 
151
160
  return ctx.wrap(content);
152
161
  },
@@ -15,11 +15,13 @@ defineMacro({
15
15
  state.deleteVariable(name.slice(1));
16
16
  } else if (name.startsWith('_')) {
17
17
  state.deleteTemporary(name.slice(1));
18
+ } else if (name.startsWith('%')) {
19
+ state.deleteTransient(name.slice(1));
18
20
  } else if (name.startsWith('@')) {
19
21
  ctx.update(name.slice(1), undefined);
20
22
  } else {
21
23
  console.error(
22
- `spindle: {unset} expects a variable ($name, _name, or @name), got "${name}"`,
24
+ `spindle: {unset} expects a variable ($name, _name, %name, or @name), got "${name}"`,
23
25
  );
24
26
  }
25
27
  }
@@ -5,7 +5,7 @@ import { useInterpolate } from '../../hooks/use-interpolate';
5
5
 
6
6
  interface VarDisplayProps {
7
7
  name: string;
8
- scope: 'variable' | 'temporary' | 'local';
8
+ scope: 'variable' | 'temporary' | 'local' | 'transient';
9
9
  className?: string;
10
10
  id?: string;
11
11
  }
@@ -22,7 +22,9 @@ export function VarDisplay({ name, scope, className, id }: VarDisplayProps) {
22
22
  ? s.variables[root]
23
23
  : scope === 'temporary'
24
24
  ? s.temporary[root]
25
- : undefined,
25
+ : scope === 'transient'
26
+ ? s.transient[root]
27
+ : undefined,
26
28
  );
27
29
 
28
30
  let value: unknown;
@@ -32,7 +32,8 @@ function isStandaloneValue(token: string): boolean {
32
32
  // Quoted string
33
33
  if (first === '"' || first === "'" || first === '`') return true;
34
34
  // Variable ($var, _var, @var)
35
- if (first === '$' || first === '_' || first === '@') return true;
35
+ if (first === '$' || first === '_' || first === '@' || first === '%')
36
+ return true;
36
37
  // Number literal
37
38
  if (/\d/.test(first)) return true;
38
39
  // Signed number (-1, +2)
@@ -189,8 +190,10 @@ function WidgetBody({
189
190
  {},
190
191
  );
191
192
 
192
- // Recomputed every render — picks up new ownKeys from parent
193
- const localState = { ...parentValues, ...ownKeys, ...localMutations };
193
+ const localState = useMemo(
194
+ () => ({ ...parentValues, ...ownKeys, ...localMutations }),
195
+ [parentValues, ownKeys, localMutations],
196
+ );
194
197
 
195
198
  const valuesRef = useRef(localState);
196
199
  valuesRef.current = localState;
@@ -218,7 +221,8 @@ export function WidgetInvocation({
218
221
  }: WidgetInvocationProps) {
219
222
  const parentValues = useContext(LocalsValuesContext);
220
223
  const nobr = useContext(NobrContext);
221
- const [mergedVars, mergedTemps, mergedLocals] = useMergedLocals();
224
+ const [mergedVars, mergedTemps, mergedLocals, mergedTrans] =
225
+ useMergedLocals();
222
226
 
223
227
  const childrenValue = invocationChildren?.length ? invocationChildren : null;
224
228
 
@@ -231,22 +235,38 @@ export function WidgetInvocation({
231
235
  }
232
236
 
233
237
  const argExprs = splitArgs(rawArgs);
234
- const ownKeys: Record<string, unknown> = {};
238
+ const values: unknown[] = [];
235
239
 
236
240
  for (let i = 0; i < params.length; i++) {
237
- const param = params[i]!;
238
241
  const expr = argExprs[i];
239
242
  let value: unknown;
240
243
  if (expr !== undefined) {
241
244
  try {
242
- value = evaluate(expr, mergedVars, mergedTemps, mergedLocals);
245
+ value = evaluate(
246
+ expr,
247
+ mergedVars,
248
+ mergedTemps,
249
+ mergedLocals,
250
+ mergedTrans,
251
+ );
243
252
  } catch {
244
253
  value = undefined;
245
254
  }
246
255
  }
247
- ownKeys[param.startsWith('@') ? param.slice(1) : param] = value;
256
+ values.push(value);
248
257
  }
249
258
 
259
+ const ownKeys = useMemo(() => {
260
+ const keys: Record<string, unknown> = {};
261
+ for (let i = 0; i < params.length; i++) {
262
+ keys[params[i]!.startsWith('@') ? params[i]!.slice(1) : params[i]!] =
263
+ values[i];
264
+ }
265
+ return keys;
266
+ // params is stable per widget instance; values tracks evaluated args
267
+ // eslint-disable-next-line react-hooks/exhaustive-deps
268
+ }, values);
269
+
250
270
  return (
251
271
  <WidgetChildrenContext.Provider value={childrenValue}>
252
272
  <WidgetBody
@@ -52,6 +52,7 @@ export interface MacroContext {
52
52
  Record<string, unknown>,
53
53
  Record<string, unknown>,
54
54
  Record<string, unknown>,
55
+ Record<string, unknown>,
55
56
  ];
56
57
  varName?: string;
57
58
  value?: unknown;
@@ -178,12 +179,21 @@ export function defineMacro(
178
179
  ctx.merged = useMergedLocals();
179
180
  const merged = ctx.merged;
180
181
  ctx.evaluate = (expr: string) =>
181
- evaluate(expr, merged[0], merged[1], merged[2]);
182
+ evaluate(expr, merged[0], merged[1], merged[2], merged[3]);
182
183
  }
183
184
 
184
185
  if (config.storeVar) {
185
186
  const firstToken =
186
187
  props.rawArgs.trim().split(/\s+/)[0]?.replace(/["']/g, '') ?? '';
188
+
189
+ if (firstToken.startsWith('%')) {
190
+ return h(
191
+ 'span',
192
+ { class: 'error' },
193
+ `{${config.name}}: transient variables (%${firstToken.slice(1)}) cannot be bound to input macros`,
194
+ );
195
+ }
196
+
187
197
  const varExpr = firstToken.replace(/["']/g, '').replace(/^\$/, '');
188
198
  const segments = varExpr.split('.');
189
199
  ctx.varName = varExpr;
@@ -10,9 +10,10 @@ export function executeMutation(
10
10
  const state = useStoryStore.getState();
11
11
  const vars = deepClone(state.variables);
12
12
  const temps = deepClone(state.temporary);
13
+ const trans = deepClone(state.transient);
13
14
  const localsClone = { ...mergedLocals };
14
15
 
15
- execute(code, vars, temps, localsClone);
16
+ execute(code, vars, temps, localsClone, trans);
16
17
 
17
18
  for (const key of Object.keys(vars)) {
18
19
  if (vars[key] !== state.variables[key]) {
@@ -24,6 +25,11 @@ export function executeMutation(
24
25
  state.setTemporary(key, temps[key]);
25
26
  }
26
27
  }
28
+ for (const key of Object.keys(trans)) {
29
+ if (trans[key] !== state.transient[key]) {
30
+ state.setTransient(key, trans[key]);
31
+ }
32
+ }
27
33
  for (const key of Object.keys(localsClone)) {
28
34
  if (localsClone[key] !== mergedLocals[key]) {
29
35
  scopeUpdate(key, localsClone[key]);
@@ -41,6 +47,11 @@ export function executeMutation(
41
47
  state.deleteTemporary(key);
42
48
  }
43
49
  }
50
+ for (const key of Object.keys(state.transient)) {
51
+ if (!(key in trans)) {
52
+ state.deleteTransient(key);
53
+ }
54
+ }
44
55
  for (const key of Object.keys(mergedLocals)) {
45
56
  if (!(key in localsClone)) {
46
57
  scopeUpdate(key, undefined);
package/src/expression.ts CHANGED
@@ -23,30 +23,34 @@ type CompiledExpression = (
23
23
  temporary: Record<string, unknown>,
24
24
  locals: Record<string, unknown>,
25
25
  __fns: ExpressionFns,
26
+ transient: Record<string, unknown>,
26
27
  ) => unknown;
27
28
 
28
29
  const FN_CACHE_MAX = 500;
29
30
  const fnCache = new Map<string, CompiledExpression>();
30
31
 
31
32
  /**
32
- * Transform expression: $var → variables["var"], _var → temporary["var"]
33
- * Only transforms when $ or _ appears as a word boundary (not inside strings naively,
33
+ * Transform expression: $var → variables["var"], _var → temporary["var"],
34
+ * @var locals["var"], %var transient["var"]
35
+ * Only transforms when sigils appear as a word boundary (not inside strings naively,
34
36
  * but authors already have full JS access so this is acceptable).
35
37
  */
36
38
  const VAR_RE = /\$(\w+)/g;
37
39
  const TEMP_RE = /(?<![.\w])_(\w+)/g;
38
40
  const LOCAL_RE = /@(\w+)/g;
41
+ const TRANS_RE = /(?<!\w)%(\w+)/g;
39
42
 
40
43
  function transformSegment(segment: string): string {
41
44
  return segment
42
45
  .replace(VAR_RE, 'variables["$1"]')
43
46
  .replace(TEMP_RE, 'temporary["$1"]')
44
- .replace(LOCAL_RE, 'locals["$1"]');
47
+ .replace(LOCAL_RE, 'locals["$1"]')
48
+ .replace(TRANS_RE, 'transient["$1"]');
45
49
  }
46
50
 
47
51
  /**
48
52
  * String-aware expression transformer. Walks the expression character by
49
- * character so that variable sigils ($, _, @) inside string literals are
53
+ * character so that variable sigils ($, _, @, %) inside string literals are
50
54
  * left untouched while code — including expressions inside template-literal
51
55
  * `${…}` interpolations — is transformed.
52
56
  */
@@ -215,6 +219,7 @@ function getOrCompile(key: string, body: string): CompiledExpression {
215
219
  'temporary',
216
220
  'locals',
217
221
  '__fns',
222
+ 'transient',
218
223
  preamble + body,
219
224
  ) as CompiledExpression;
220
225
  fnCache.set(key, fn);
@@ -298,11 +303,12 @@ export function evaluate(
298
303
  variables: Record<string, unknown>,
299
304
  temporary: Record<string, unknown>,
300
305
  locals: Record<string, unknown> = {},
306
+ transient: Record<string, unknown> = {},
301
307
  ): unknown {
302
308
  const transformed = transform(expr);
303
309
  const body = `return (${transformed});`;
304
310
  const fn = getOrCompile(body, body);
305
- return fn(variables, temporary, locals, buildExpressionFns());
311
+ return fn(variables, temporary, locals, buildExpressionFns(), transient);
306
312
  }
307
313
 
308
314
  /**
@@ -314,10 +320,11 @@ export function execute(
314
320
  variables: Record<string, unknown>,
315
321
  temporary: Record<string, unknown>,
316
322
  locals: Record<string, unknown> = {},
323
+ transient: Record<string, unknown> = {},
317
324
  ): void {
318
325
  const transformed = transform(code);
319
326
  const fn = getOrCompile('exec:' + transformed, transformed);
320
- fn(variables, temporary, locals, buildExpressionFns());
327
+ fn(variables, temporary, locals, buildExpressionFns(), transient);
321
328
  }
322
329
 
323
330
  /**
@@ -332,5 +339,5 @@ export function clearExpressionCache(): void {
332
339
  }
333
340
 
334
341
  export function evaluateWithState(expr: string, state: StoryState): unknown {
335
- return evaluate(expr, state.variables, state.temporary, {});
342
+ return evaluate(expr, state.variables, state.temporary, {}, state.transient);
336
343
  }
@@ -5,13 +5,13 @@ import { hasInterpolation, interpolate } from '../interpolation';
5
5
  export function useInterpolate(): (
6
6
  s: string | undefined,
7
7
  ) => string | undefined {
8
- const [variables, temporary, locals] = useMergedLocals();
8
+ const [variables, temporary, locals, transient] = useMergedLocals();
9
9
 
10
10
  return useCallback(
11
11
  (s: string | undefined): string | undefined => {
12
12
  if (s === undefined || !hasInterpolation(s)) return s;
13
- return interpolate(s, variables, temporary, locals);
13
+ return interpolate(s, variables, temporary, locals, transient);
14
14
  },
15
- [variables, temporary, locals],
15
+ [variables, temporary, locals, transient],
16
16
  );
17
17
  }
@@ -3,19 +3,21 @@ import { useStoryStore } from '../store';
3
3
  import { LocalsValuesContext } from '../markup/render';
4
4
 
5
5
  /**
6
- * Return store variables, temporary, and locals from context.
7
- * All three dicts use unprefixed keys suitable for evaluate/execute.
6
+ * Return store variables, temporary, locals from context, and transient.
7
+ * All four dicts use unprefixed keys suitable for evaluate/execute.
8
8
  */
9
9
  export function useMergedLocals(): readonly [
10
10
  Record<string, unknown>,
11
11
  Record<string, unknown>,
12
12
  Record<string, unknown>,
13
+ Record<string, unknown>,
13
14
  ] {
14
15
  const variables = useStoryStore((s) => s.variables);
15
16
  const temporary = useStoryStore((s) => s.temporary);
17
+ const transient = useStoryStore((s) => s.transient);
16
18
  const localsValues = useContext(LocalsValuesContext);
17
19
 
18
20
  return useMemo(() => {
19
- return [variables, temporary, localsValues] as const;
20
- }, [variables, temporary, localsValues]);
21
+ return [variables, temporary, localsValues, transient] as const;
22
+ }, [variables, temporary, localsValues, transient]);
21
23
  }
package/src/index.tsx CHANGED
@@ -88,6 +88,27 @@ function boot() {
88
88
  const schema = parseStoryVariables(storyVarsPassage.content);
89
89
  const errors = validatePassages(storyData.passages, schema);
90
90
 
91
+ // Parse StoryTransients (optional — no error if missing)
92
+ let transientDefaults: Record<string, unknown> = {};
93
+ const storyTransientsPassage = storyData.passages.get('StoryTransients');
94
+ if (storyTransientsPassage) {
95
+ const transientSchema = parseStoryVariables(
96
+ storyTransientsPassage.content,
97
+ '%',
98
+ );
99
+
100
+ // Check for cross-scope name collisions
101
+ for (const name of transientSchema.keys()) {
102
+ if (schema.has(name)) {
103
+ errors.push(
104
+ `StoryTransients: Variable "${name}" is already declared in StoryVariables. Names must be unique across scopes.`,
105
+ );
106
+ }
107
+ }
108
+
109
+ transientDefaults = extractDefaults(transientSchema);
110
+ }
111
+
91
112
  if (errors.length > 0) {
92
113
  const root = document.getElementById('root');
93
114
  if (root) renderErrors(root, errors);
@@ -98,7 +119,7 @@ function boot() {
98
119
 
99
120
  defaults = extractDefaults(schema);
100
121
 
101
- useStoryStore.getState().init(storyData, defaults);
122
+ useStoryStore.getState().init(storyData, defaults, transientDefaults);
102
123
 
103
124
  // Enter runtime phase — handlers registered from here on are cleaned on restart
104
125
  enterRuntimePhase();
@@ -1,7 +1,7 @@
1
1
  import { evaluate } from './expression';
2
2
 
3
- /** Detects any {…} block that starts with a sigil ($, _, @). */
4
- const INTERP_TEST = /\{[\$_@]\w/;
3
+ /** Detects any {…} block that starts with a sigil ($, _, @, %). */
4
+ const INTERP_TEST = /\{[\$_@%]\w/;
5
5
 
6
6
  export function hasInterpolation(s: string): boolean {
7
7
  return INTERP_TEST.test(s);
@@ -25,8 +25,9 @@ export function interpolateExpression(
25
25
  variables: Record<string, unknown>,
26
26
  temporary: Record<string, unknown>,
27
27
  locals: Record<string, unknown>,
28
+ transient: Record<string, unknown> = {},
28
29
  ): string {
29
- const value = evaluate(expr, variables, temporary, locals);
30
+ const value = evaluate(expr, variables, temporary, locals, transient);
30
31
  return value == null ? '' : String(value);
31
32
  }
32
33
 
@@ -35,6 +36,7 @@ function resolveSimple(
35
36
  variables: Record<string, unknown>,
36
37
  temporary: Record<string, unknown>,
37
38
  locals: Record<string, unknown>,
39
+ transient: Record<string, unknown>,
38
40
  ): string {
39
41
  const prefix = ref[0]!;
40
42
  const path = ref.slice(1);
@@ -46,6 +48,8 @@ function resolveSimple(
46
48
  value = variables[root];
47
49
  } else if (prefix === '_') {
48
50
  value = temporary[root];
51
+ } else if (prefix === '%') {
52
+ value = transient[root];
49
53
  } else {
50
54
  value = locals[root];
51
55
  }
@@ -62,6 +66,7 @@ export function interpolate(
62
66
  variables: Record<string, unknown>,
63
67
  temporary: Record<string, unknown>,
64
68
  locals: Record<string, unknown>,
69
+ transient: Record<string, unknown> = {},
65
70
  ): string {
66
71
  // Manual scan: process {…} blocks containing sigils.
67
72
  // Simple dot-path refs use the fast resolver; everything else falls back
@@ -86,7 +91,7 @@ export function interpolate(
86
91
  i++; // skip {
87
92
 
88
93
  const sigil = template[i];
89
- if (sigil !== '$' && sigil !== '_' && sigil !== '@') {
94
+ if (sigil !== '$' && sigil !== '_' && sigil !== '@' && sigil !== '%') {
90
95
  // Not an interpolation — emit the { as text
91
96
  result += '{';
92
97
  continue;
@@ -111,10 +116,16 @@ export function interpolate(
111
116
  i = j + 1; // skip past closing }
112
117
 
113
118
  // Try simple dot-path match first
114
- if (/^[\$_@][\w.]+$/.test(inner)) {
115
- result += resolveSimple(inner, variables, temporary, locals);
119
+ if (/^[\$_@%][\w.]+$/.test(inner)) {
120
+ result += resolveSimple(inner, variables, temporary, locals, transient);
116
121
  } else {
117
- result += interpolateExpression(inner, variables, temporary, locals);
122
+ result += interpolateExpression(
123
+ inner,
124
+ variables,
125
+ temporary,
126
+ locals,
127
+ transient,
128
+ );
118
129
  }
119
130
  }
120
131
 
package/src/markup/ast.ts CHANGED
@@ -8,7 +8,7 @@ export interface TextNode {
8
8
  export interface VariableNode {
9
9
  type: 'variable';
10
10
  name: string;
11
- scope: 'variable' | 'temporary' | 'local';
11
+ scope: 'variable' | 'temporary' | 'local' | 'transient';
12
12
  className?: string;
13
13
  id?: string;
14
14
  }
@@ -243,6 +243,7 @@ function getVariableTextValue(
243
243
  let value: unknown;
244
244
  if (node.scope === 'variable') value = state.variables[root];
245
245
  else if (node.scope === 'temporary') value = state.temporary[root];
246
+ else if (node.scope === 'transient') value = state.transient[root];
246
247
  else value = locals[root];
247
248
 
248
249
  for (let i = 1; i < parts.length; i++) {