• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

boardgameio / boardgame.io / 15797955584

21 Jun 2025 05:20PM UTC coverage: 97.263% (-2.7%) from 100.0%
15797955584

Pull #1226

github

web-flow
Merge 8ac078c34 into 4f3c90df0
Pull Request #1226: Koa to express

1644 of 1714 branches covered (95.92%)

Branch coverage included in aggregate %.

347 of 349 new or added lines in 3 files covered. (99.43%)

228 existing lines in 10 files now uncovered.

9337 of 9576 relevant lines covered (97.5%)

6155.46 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

98.88
/src/plugins/main.ts
1
/*
4✔
2
 * Copyright 2018 The boardgame.io Authors
4✔
3
 *
4✔
4
 * Use of this source code is governed by a MIT-style
4✔
5
 * license that can be found in the LICENSE file or at
4✔
6
 * https://opensource.org/licenses/MIT.
4✔
7
 */
4✔
8

4✔
9
import PluginImmer from './plugin-immer';
4✔
10
import PluginRandom from './plugin-random';
4✔
11
import PluginEvents from './plugin-events';
4✔
12
import PluginLog from './plugin-log';
4✔
13
import PluginSerializable from './plugin-serializable';
4✔
14
import type {
4✔
15
  AnyFn,
4✔
16
  DefaultPluginAPIs,
4✔
17
  PartialGameState,
4✔
18
  State,
4✔
19
  Game,
4✔
20
  Plugin,
4✔
21
  ActionShape,
4✔
22
  PlayerID,
4✔
23
} from '../types';
4✔
24
import { error } from '../core/logger';
4✔
25
import type { GameMethod } from '../core/game-methods';
4✔
26

4✔
27
interface PluginOpts {
4✔
28
  game: Game;
4✔
29
  isClient?: boolean;
4✔
30
}
4✔
31

4✔
32
/**
4✔
33
 * List of plugins that are always added.
4✔
34
 */
4✔
35
const CORE_PLUGINS = [PluginImmer, PluginRandom, PluginLog, PluginSerializable];
4✔
36
const DEFAULT_PLUGINS = [...CORE_PLUGINS, PluginEvents];
4✔
37

4✔
38
/**
4✔
39
 * Allow plugins to intercept actions and process them.
4✔
40
 */
4✔
41
export const ProcessAction = (
4✔
42
  state: State,
8✔
43
  action: ActionShape.Plugin,
8✔
44
  opts: PluginOpts
8✔
45
): State => {
8✔
46
  // TODO(#723): Extend error handling to plugins.
8✔
47
  opts.game.plugins
8✔
48
    .filter((plugin) => plugin.action !== undefined)
8✔
49
    .filter((plugin) => plugin.name === action.payload.type)
8✔
50
    .forEach((plugin) => {
8✔
51
      const name = plugin.name;
8✔
52
      const pluginState = state.plugins[name] || { data: {} };
8✔
53
      const data = plugin.action(pluginState.data, action.payload);
8✔
54

8✔
55
      state = {
8✔
56
        ...state,
8✔
57
        plugins: {
8✔
58
          ...state.plugins,
8✔
59
          [name]: { ...pluginState, data },
8✔
60
        },
8✔
61
      };
8✔
62
    });
8✔
63
  return state;
8✔
64
};
8✔
65

4✔
66
/**
4✔
67
 * The APIs created by various plugins are stored in the plugins
4✔
68
 * section of the state object:
4✔
69
 *
4✔
70
 * {
4✔
71
 *   G: {},
4✔
72
 *   ctx: {},
4✔
73
 *   plugins: {
4✔
74
 *     plugin-a: {
4✔
75
 *       data: {},  // this is generated by the plugin at Setup / Flush.
4✔
76
 *       api: {},   // this is ephemeral and generated by Enhance.
4✔
77
 *     }
4✔
78
 *   }
4✔
79
 * }
4✔
80
 *
4✔
81
 * This function retrieves plugin APIs and returns them as an object
4✔
82
 * for consumption as used by move contexts.
4✔
83
 */
4✔
84
export const GetAPIs = ({ plugins }: PartialGameState) =>
4✔
85
  Object.entries(plugins || {}).reduce((apis, [name, { api }]) => {
60,564✔
86
    apis[name] = api;
169,508✔
87
    return apis;
169,508✔
88
  }, {} as DefaultPluginAPIs);
4✔
89

4✔
90
/**
4✔
91
 * Applies the provided plugins to the given move / flow function.
4✔
92
 *
4✔
93
 * @param methodToWrap - The move function or hook to apply the plugins to.
4✔
94
 * @param methodType - The type of the move or hook being wrapped.
4✔
95
 * @param plugins - The list of plugins.
4✔
96
 */
4✔
97
export const FnWrap = (
4✔
98
  methodToWrap: AnyFn,
7,572✔
99
  methodType: GameMethod,
7,572✔
100
  plugins: Plugin[]
7,572✔
101
) => {
7,572✔
102
  return [...CORE_PLUGINS, ...plugins, PluginEvents]
7,572✔
103
    .filter((plugin) => plugin.fnWrap !== undefined)
7,572✔
104
    .reduce(
7,572✔
105
      (method: AnyFn, { fnWrap }: Plugin) => fnWrap(method, methodType),
7,572✔
106
      methodToWrap
7,572✔
107
    );
7,572✔
108
};
7,572✔
109

4✔
110
/**
4✔
111
 * Allows the plugin to generate its initial state.
4✔
112
 */
4✔
113
export const Setup = (
4✔
114
  state: PartialGameState,
484✔
115
  opts: PluginOpts
484✔
116
): PartialGameState => {
484✔
117
  [...DEFAULT_PLUGINS, ...opts.game.plugins]
484✔
118
    .filter((plugin) => plugin.setup !== undefined)
484✔
119
    .forEach((plugin) => {
484✔
120
      const name = plugin.name;
968✔
121
      const data = plugin.setup({
968✔
122
        G: state.G,
968✔
123
        ctx: state.ctx,
968✔
124
        game: opts.game,
968✔
125
      });
968✔
126

968✔
127
      state = {
968✔
128
        ...state,
968✔
129
        plugins: {
968✔
130
          ...state.plugins,
968✔
131
          [name]: { data },
968✔
132
        },
968✔
133
      };
968✔
134
    });
484✔
135
  return state;
484✔
136
};
484✔
137

4✔
138
/**
4✔
139
 * Invokes the plugin before a move or event.
4✔
140
 * The API that the plugin generates is stored inside
4✔
141
 * the `plugins` section of the state (which is subsequently
4✔
142
 * merged into ctx).
4✔
143
 */
4✔
144
export const Enhance = <S extends State | PartialGameState>(
4✔
145
  state: S,
904✔
146
  opts: PluginOpts & { playerID: PlayerID }
904✔
147
): S => {
904✔
148
  [...DEFAULT_PLUGINS, ...opts.game.plugins]
904✔
149
    .filter((plugin) => plugin.api !== undefined)
904✔
150
    .forEach((plugin) => {
904✔
151
      const name = plugin.name;
2,816✔
152
      const pluginState = state.plugins[name] || { data: {} };
2,816✔
153

2,816✔
154
      const api = plugin.api({
2,816✔
155
        G: state.G,
2,816✔
156
        ctx: state.ctx,
2,816✔
157
        data: pluginState.data,
2,816✔
158
        game: opts.game,
2,816✔
159
        playerID: opts.playerID,
2,816✔
160
      });
2,816✔
161

2,816✔
162
      state = {
2,816✔
163
        ...state,
2,816✔
164
        plugins: {
2,816✔
165
          ...state.plugins,
2,816✔
166
          [name]: { ...pluginState, api },
2,816✔
167
        },
2,816✔
168
      };
2,816✔
169
    });
904✔
170
  return state;
904✔
171
};
904✔
172

4✔
173
/**
4✔
174
 * Allows plugins to update their state after a move / event.
4✔
175
 */
4✔
176
const Flush = (state: State, opts: PluginOpts): State => {
4✔
177
  // We flush the events plugin first, then custom plugins and the core plugins.
900✔
178
  // This means custom plugins cannot use the events API but will be available in event hooks.
900✔
179
  // Note that plugins are flushed in reverse, to allow custom plugins calling each other.
900✔
180
  [...CORE_PLUGINS, ...opts.game.plugins, PluginEvents]
900✔
181
    .reverse()
900✔
182
    .forEach((plugin) => {
900✔
183
      const name = plugin.name;
4,604✔
184
      const pluginState = state.plugins[name] || { data: {} };
4,604✔
185

4,604✔
186
      if (plugin.flush) {
4,604✔
187
        const newData = plugin.flush({
1,800✔
188
          G: state.G,
1,800✔
189
          ctx: state.ctx,
1,800✔
190
          game: opts.game,
1,800✔
191
          api: pluginState.api,
1,800✔
192
          data: pluginState.data,
1,800✔
193
        });
1,800✔
194

1,800✔
195
        state = {
1,800✔
196
          ...state,
1,800✔
197
          plugins: {
1,800✔
198
            ...state.plugins,
1,800✔
199
            [plugin.name]: { data: newData },
1,800✔
200
          },
1,800✔
201
        };
1,800✔
202
      } else if (plugin.dangerouslyFlushRawState) {
4,604✔
203
        state = plugin.dangerouslyFlushRawState({
900✔
204
          state,
900✔
205
          game: opts.game,
900✔
206
          api: pluginState.api,
900✔
207
          data: pluginState.data,
900✔
208
        });
900✔
209

900✔
210
        // Remove everything other than data.
900✔
211
        const data = state.plugins[name].data;
900✔
212
        state = {
900✔
213
          ...state,
900✔
214
          plugins: {
900✔
215
            ...state.plugins,
900✔
216
            [plugin.name]: { data },
900✔
217
          },
900✔
218
        };
900✔
219
      }
900✔
220
    });
900✔
221

900✔
222
  return state;
900✔
223
};
900✔
224

4✔
225
/**
4✔
226
 * Allows plugins to indicate if they should not be materialized on the client.
4✔
227
 * This will cause the client to discard the state update and wait for the
4✔
228
 * master instead.
4✔
229
 */
4✔
230
export const NoClient = (state: State, opts: PluginOpts): boolean => {
4✔
231
  return [...DEFAULT_PLUGINS, ...opts.game.plugins]
24✔
232
    .filter((plugin) => plugin.noClient !== undefined)
24✔
233
    .map((plugin) => {
24✔
234
      const name = plugin.name;
48✔
235
      const pluginState = state.plugins[name];
48✔
236

48✔
237
      if (pluginState) {
48✔
238
        return plugin.noClient({
48✔
239
          G: state.G,
48✔
240
          ctx: state.ctx,
48✔
241
          game: opts.game,
48✔
242
          api: pluginState.api,
48✔
243
          data: pluginState.data,
48✔
244
        });
48✔
245
      }
48✔
UNCOV
246

×
UNCOV
247
      return false;
×
248
    })
24✔
249
    .includes(true);
24✔
250
};
24✔
251

4✔
252
/**
4✔
253
 * Allows plugins to indicate if the entire action should be thrown out
4✔
254
 * as invalid. This will cancel the entire state update.
4✔
255
 */
4✔
256
const IsInvalid = (
4✔
257
  state: State,
900✔
258
  opts: PluginOpts
900✔
259
): false | { plugin: string; message: string } => {
900✔
260
  const firstInvalidReturn = [...DEFAULT_PLUGINS, ...opts.game.plugins]
900✔
261
    .filter((plugin) => plugin.isInvalid !== undefined)
900✔
262
    .map((plugin) => {
900✔
263
      const { name } = plugin;
900✔
264
      const pluginState = state.plugins[name];
900✔
265

900✔
266
      const message = plugin.isInvalid({
900✔
267
        G: state.G,
900✔
268
        ctx: state.ctx,
900✔
269
        game: opts.game,
900✔
270
        data: pluginState && pluginState.data,
900✔
271
      });
900✔
272

900✔
273
      return message ? { plugin: name, message } : false;
900✔
274
    })
900✔
275
    .find((value) => value);
900✔
276
  return firstInvalidReturn || false;
900✔
277
};
900✔
278

4✔
279
/**
4✔
280
 * Update plugin state after move/event & check if plugins consider the update to be valid.
4✔
281
 * @returns Tuple of `[updatedState]` or `[originalState, invalidError]`.
4✔
282
 */
4✔
283
export const FlushAndValidate = (state: State, opts: PluginOpts) => {
4✔
284
  const updatedState = Flush(state, opts);
900✔
285
  const isInvalid = IsInvalid(updatedState, opts);
900✔
286
  if (!isInvalid) return [updatedState] as const;
900✔
287
  const { plugin, message } = isInvalid;
76✔
288
  error(`${plugin} plugin declared action invalid:\n${message}`);
76✔
289
  return [state, isInvalid] as const;
76✔
290
};
76✔
291

4✔
292
/**
4✔
293
 * Allows plugins to customize their data for specific players.
4✔
294
 * For example, a plugin may want to share no data with the client, or
4✔
295
 * want to keep some player data secret from opponents.
4✔
296
 */
4✔
297
export const PlayerView = (
4✔
298
  { G, ctx, plugins = {} }: State,
352✔
299
  { game, playerID }: PluginOpts & { playerID: PlayerID }
352✔
300
) => {
352✔
301
  [...DEFAULT_PLUGINS, ...game.plugins].forEach(({ name, playerView }) => {
352✔
302
    if (!playerView) return;
1,760✔
303

352✔
304
    const { data } = plugins[name] || { data: {} };
1,760!
305
    const newData = playerView({ G, ctx, game, data, playerID });
1,760✔
306

1,760✔
307
    plugins = {
1,760✔
308
      ...plugins,
1,760✔
309
      [name]: { data: newData },
1,760✔
310
    };
1,760✔
311
  });
352✔
312

352✔
313
  return plugins;
352✔
314
};
352✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc