@rohal12/spindle 0.43.0 → 0.43.2

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.43.0",
3
+ "version": "0.43.2",
4
4
  "type": "module",
5
5
  "description": "A Preact-based story format for Twine 2.",
6
6
  "license": "Unlicense",
@@ -22,9 +22,9 @@ function parseComputedArgs(rawArgs: string): { target: string; expr: string } {
22
22
  const target = trimmed.slice(0, i).trim();
23
23
  const expr = trimmed.slice(i + 1).trim();
24
24
 
25
- if (!target.match(/^[$_]\w+$/)) {
25
+ if (!target.match(/^[$_@]\w+$/)) {
26
26
  throw new Error(
27
- `{computed}: target must be $name or _name, got "${target}"`,
27
+ `{computed}: target must be $name, _name, or @name, got "${target}"`,
28
28
  );
29
29
  }
30
30
 
@@ -58,11 +58,14 @@ function computeAndApply(
58
58
  expr: string,
59
59
  name: string,
60
60
  isTemp: boolean,
61
+ isLocal: boolean,
61
62
  variables: Record<string, unknown>,
62
63
  temporary: Record<string, unknown>,
63
64
  locals: Record<string, unknown>,
64
65
  transient: Record<string, unknown>,
65
66
  rawArgs: string,
67
+ prevRef: { current: unknown },
68
+ localsUpdate: ((key: string, value: unknown) => void) | null,
66
69
  ): void {
67
70
  let newValue: unknown;
68
71
  try {
@@ -75,11 +78,22 @@ function computeAndApply(
75
78
  return;
76
79
  }
77
80
 
78
- const current = isTemp ? temporary[name] : variables[name];
79
- if (!valuesEqual(current, newValue)) {
80
- const state = useStoryStore.getState();
81
- if (isTemp) state.setTemporary(name, newValue);
82
- else state.setVariable(name, newValue);
81
+ if (!valuesEqual(prevRef.current, newValue)) {
82
+ prevRef.current = newValue;
83
+ if (isLocal) {
84
+ try {
85
+ localsUpdate!(name, newValue);
86
+ } catch (err) {
87
+ console.error(
88
+ `spindle: Error in {computed ${rawArgs}}${currentSourceLocation()}:`,
89
+ err,
90
+ );
91
+ }
92
+ } else {
93
+ const state = useStoryStore.getState();
94
+ if (isTemp) state.setTemporary(name, newValue);
95
+ else state.setVariable(name, newValue);
96
+ }
83
97
  }
84
98
  }
85
99
 
@@ -101,8 +115,12 @@ defineMacro({
101
115
  />
102
116
  );
103
117
  }
118
+ const isLocal = target.startsWith('@');
104
119
  const isTemp = target.startsWith('_');
105
120
  const name = target.slice(1);
121
+ const localsUpdate = isLocal ? ctx.update : null;
122
+
123
+ const prevOutput = ctx.hooks.useRef<unknown>(undefined);
106
124
 
107
125
  const ran = ctx.hooks.useRef(false);
108
126
  if (!ran.current) {
@@ -111,11 +129,14 @@ defineMacro({
111
129
  expr,
112
130
  name,
113
131
  isTemp,
132
+ isLocal,
114
133
  mergedVars,
115
134
  mergedTemps,
116
135
  mergedLocals,
117
136
  mergedTrans,
118
137
  rawArgs,
138
+ prevOutput,
139
+ localsUpdate,
119
140
  );
120
141
  }
121
142
 
@@ -124,11 +145,14 @@ defineMacro({
124
145
  expr,
125
146
  name,
126
147
  isTemp,
148
+ isLocal,
127
149
  mergedVars,
128
150
  mergedTemps,
129
151
  mergedLocals,
130
152
  mergedTrans,
131
153
  rawArgs,
154
+ prevOutput,
155
+ localsUpdate,
132
156
  );
133
157
  }, [mergedVars, mergedTemps, mergedLocals, mergedTrans]);
134
158
 
@@ -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
  },
@@ -190,8 +190,10 @@ function WidgetBody({
190
190
  {},
191
191
  );
192
192
 
193
- // Recomputed every render — picks up new ownKeys from parent
194
- const localState = { ...parentValues, ...ownKeys, ...localMutations };
193
+ const localState = useMemo(
194
+ () => ({ ...parentValues, ...ownKeys, ...localMutations }),
195
+ [parentValues, ownKeys, localMutations],
196
+ );
195
197
 
196
198
  const valuesRef = useRef(localState);
197
199
  valuesRef.current = localState;
@@ -233,10 +235,9 @@ export function WidgetInvocation({
233
235
  }
234
236
 
235
237
  const argExprs = splitArgs(rawArgs);
236
- const ownKeys: Record<string, unknown> = {};
238
+ const values: unknown[] = [];
237
239
 
238
240
  for (let i = 0; i < params.length; i++) {
239
- const param = params[i]!;
240
241
  const expr = argExprs[i];
241
242
  let value: unknown;
242
243
  if (expr !== undefined) {
@@ -252,9 +253,20 @@ export function WidgetInvocation({
252
253
  value = undefined;
253
254
  }
254
255
  }
255
- ownKeys[param.startsWith('@') ? param.slice(1) : param] = value;
256
+ values.push(value);
256
257
  }
257
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
+
258
270
  return (
259
271
  <WidgetChildrenContext.Provider value={childrenValue}>
260
272
  <WidgetBody