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

excaliburjs / Excalibur / 20112442888

10 Dec 2025 08:32PM UTC coverage: 86.689% (-1.9%) from 88.636%
20112442888

Pull #3617

github

web-flow
Merge 05feefa61 into 548f5e4e7
Pull Request #3617: [feat] extend the components class to add json/serialize/deserialize to component properties

5316 of 7546 branches covered (70.45%)

1 of 374 new or added lines in 6 files covered. (0.27%)

200 existing lines in 5 files now uncovered.

14719 of 16979 relevant lines covered (86.69%)

24145.37 hits per line

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

19.3
/src/engine/EntityComponentSystem/Component.ts
1
import type { Entity } from './Entity';
2

3
/**
4
 * Component Constructor Types
5
 */
6
export declare type ComponentCtor<TComponent extends Component = Component> = new (...args: any[]) => TComponent;
7

8
/**
9
 *
10
 */
11
export function isComponentCtor(value: any): value is ComponentCtor<Component> {
12
  return !!value && !!value.prototype && !!value.prototype.constructor;
55✔
13
}
14

15
/**
16
 * Type guard to check if a component implements clone
17
 * @param x
18
 */
19
function hasClone(x: any): x is { clone(): any } {
20
  return !!x?.clone;
175✔
21
}
22

23
/**
24
 * Components are containers for state in Excalibur, the are meant to convey capabilities that an Entity possesses
25
 *
26
 * Implementations of Component must have a zero-arg constructor to support dependencies
27
 *
28
 * ```typescript
29
 * class MyComponent extends ex.Component {
30
 *   // zero arg support required if you want to use component dependencies
31
 *   constructor(public optionalPos?: ex.Vector) {}
32
 * }
33
 * ```
34
 */
35
export abstract class Component {
36
  // TODO maybe generate a unique id?
37

38
  /**
39
   * Optionally list any component types this component depends on
40
   * If the owner entity does not have these components, new components will be added to the entity
41
   *
42
   * Only components with zero-arg constructors are supported as automatic component dependencies
43
   */
44
  readonly dependencies?: ComponentCtor[];
45

46
  /**
47
   * Current owning {@apilink Entity}, if any, of this component. Null if not added to any {@apilink Entity}
48
   */
49
  owner?: Entity = undefined;
29,623✔
50

51
  /**
52
   * Clones any properties on this component, if that property value has a `clone()` method it will be called
53
   */
54
  clone(): Component {
55
    const newComponent = new (this.constructor as any)();
27✔
56
    for (const prop in this) {
27✔
57
      if (this.hasOwnProperty(prop)) {
175!
58
        const val = this[prop];
175✔
59
        if (hasClone(val) && prop !== 'owner' && prop !== 'clone') {
175✔
60
          newComponent[prop] = val.clone();
53✔
61
        } else {
62
          newComponent[prop] = val;
122✔
63
        }
64
      }
65
    }
66
    return newComponent;
27✔
67
  }
68

69
  /**
70
   * Optional callback called when a component is added to an entity
71
   */
72
  onAdd?(owner: Entity): void;
73

74
  /**
75
   * Optional callback called when a component is removed from an entity
76
   */
77
  onRemove?(previousOwner: Entity): void;
78

79
  /*
80
   * Serialization, Deserialization, and toJSONhandled
81
   */
82

83
  toJSON(pretty: boolean = false): string {
×
NEW
UNCOV
84
    return JSON.stringify(this.serialize(), null, pretty ? 2 : 0);
×
85
  }
86

87
  serialize(): Record<string, any> {
NEW
UNCOV
88
    const data: Record<string, any> = {
×
89
      type: this.constructor.name
90
    };
91

NEW
UNCOV
92
    for (const prop in this) {
×
NEW
UNCOV
93
      if (!this.hasOwnProperty(prop)) {
×
NEW
UNCOV
94
        continue;
×
95
      }
96

97
      // Skip owner, private fields, functions, and non-serializable objects
NEW
UNCOV
98
      if (
×
99
        prop === 'owner' ||
×
100
        prop === 'dependencies' ||
101
        prop.startsWith('_') ||
102
        typeof this[prop] === 'function' ||
103
        !this._isSerializable(this[prop])
104
      ) {
NEW
UNCOV
105
        continue;
×
106
      }
107

NEW
UNCOV
108
      data[prop] = this._serializeValue(this[prop]);
×
109
    }
110

NEW
UNCOV
111
    return data;
×
112
  }
113

114
  deserialize(data: Record<string, any>): void {
NEW
UNCOV
115
    for (const key in data) {
×
NEW
UNCOV
116
      if (key === 'type' || key === 'owner' || key === 'dependencies') {
×
NEW
UNCOV
117
        continue;
×
118
      }
119

NEW
UNCOV
120
      if (this.hasOwnProperty(key)) {
×
NEW
UNCOV
121
        (this as any)[key] = this._deserializeValue(data[key], (this as any)[key]);
×
122
      }
123
    }
124
  }
125

126
  protected _serializeValue(value: any): any {
NEW
UNCOV
127
    if (value === null || value === undefined) {
×
NEW
UNCOV
128
      return value;
×
129
    }
130

131
    // Handle Vector-like objects (any object with x/y numeric properties)
NEW
UNCOV
132
    if (value && typeof value === 'object' && 'x' in value && 'y' in value && typeof value.x === 'number' && typeof value.y === 'number') {
×
NEW
UNCOV
133
      return { x: value.x, y: value.y };
×
134
    }
135

136
    // Handle arrays
NEW
UNCOV
137
    if (Array.isArray(value)) {
×
NEW
UNCOV
138
      return value.map((v) => this._serializeValue(v));
×
139
    }
140

141
    // Handle plain objects
NEW
UNCOV
142
    if (value && typeof value === 'object' && value.constructor === Object) {
×
NEW
UNCOV
143
      const obj: Record<string, any> = {};
×
NEW
UNCOV
144
      for (const k in value) {
×
NEW
UNCOV
145
        if (value.hasOwnProperty(k)) {
×
NEW
UNCOV
146
          obj[k] = this._serializeValue(value[k]);
×
147
        }
148
      }
NEW
UNCOV
149
      return obj;
×
150
    }
151

152
    // Primitives (string, number, boolean) and other types
NEW
UNCOV
153
    if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') {
×
NEW
UNCOV
154
      return value;
×
155
    }
156

157
    // Skip non-serializable objects
NEW
UNCOV
158
    return undefined;
×
159
  }
160

161
  protected _deserializeValue(data: any, existingValue?: any): any {
NEW
UNCOV
162
    if (data === null || data === undefined) {
×
NEW
UNCOV
163
      return data;
×
164
    }
165

166
    // If existing value has x/y (Vector-like), update it in place
NEW
UNCOV
167
    if (
×
168
      existingValue &&
×
169
      typeof existingValue === 'object' &&
170
      'x' in existingValue &&
171
      'y' in existingValue &&
172
      data &&
173
      typeof data === 'object' &&
174
      'x' in data &&
175
      'y' in data
176
    ) {
NEW
UNCOV
177
      existingValue.x = data.x;
×
NEW
UNCOV
178
      existingValue.y = data.y;
×
NEW
UNCOV
179
      return existingValue;
×
180
    }
181

182
    // Otherwise return the deserialized data as-is
NEW
UNCOV
183
    return data;
×
184
  }
185

186
  private _isSerializable(value: any): boolean {
NEW
UNCOV
187
    if (value === null || value === undefined) {
×
NEW
UNCOV
188
      return true;
×
189
    }
190

NEW
UNCOV
191
    const type = typeof value;
×
NEW
UNCOV
192
    if (type === 'string' || type === 'number' || type === 'boolean') {
×
NEW
UNCOV
193
      return true;
×
194
    }
195

196
    // Check for Observable or other non-serializable objects
NEW
UNCOV
197
    if (type === 'object' && value.subscribe) {
×
NEW
UNCOV
198
      return false;
×
199
    }
NEW
UNCOV
200
    if (type === 'function') {
×
NEW
UNCOV
201
      return false;
×
202
    }
203

NEW
UNCOV
204
    return true;
×
205
  }
206
}
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