@codehz/ecs 0.10.0 → 0.10.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": "@codehz/ecs",
3
- "version": "0.10.0",
3
+ "version": "0.10.1",
4
4
  "license": "MIT",
5
5
  "module": "src/index.ts",
6
6
  "type": "module",
@@ -3,6 +3,8 @@ import { ComponentEntityStore } from "../../component/entity-store";
3
3
  import { component, createEntityId, relation, type EntityId } from "../../entity";
4
4
  import { World } from "../../world/world";
5
5
 
6
+ function expectType<T>(_value: T): void {}
7
+
6
8
  describe("World - Singleton Component", () => {
7
9
  type GlobalConfig = { debug: boolean; version: string };
8
10
  type GameState = { score: number; level: number };
@@ -26,21 +28,64 @@ describe("World - Singleton Component", () => {
26
28
  const world = new World();
27
29
  const singleton = world.singleton(GlobalConfigId);
28
30
  const Marker = component<void>();
31
+ const originalWarn = console.warn;
32
+ const warnings: string[] = [];
33
+
34
+ console.warn = (...args: unknown[]) => {
35
+ warnings.push(args.join(" "));
36
+ };
37
+
38
+ try {
39
+ world.set(GlobalConfigId, Marker);
40
+ } finally {
41
+ console.warn = originalWarn;
42
+ }
29
43
 
30
- world.set(GlobalConfigId, Marker);
31
44
  world.sync();
32
45
 
33
46
  expect(world.has(GlobalConfigId, Marker)).toBe(true);
34
47
  expect(singleton.has()).toBe(false);
48
+ expect(warnings).toHaveLength(0);
35
49
  });
36
50
 
37
- it("should reject the removed singleton data shorthand at runtime", () => {
51
+ it("should support the deprecated singleton data shorthand for non-number values", () => {
38
52
  const world = new World();
39
53
  const config: GlobalConfig = { debug: true, version: "1.0.0" };
54
+ const originalWarn = console.warn;
55
+ const warnings: string[] = [];
40
56
 
41
- expect(() => {
42
- world.set(GlobalConfigId as any, config as any);
43
- }).toThrow("Invalid component type");
57
+ if (false) {
58
+ expectType<void>(world.set(GlobalConfigId, config));
59
+ }
60
+
61
+ console.warn = (...args: unknown[]) => {
62
+ warnings.push(args.join(" "));
63
+ };
64
+
65
+ try {
66
+ world.set(GlobalConfigId, config);
67
+ } finally {
68
+ console.warn = originalWarn;
69
+ }
70
+
71
+ world.sync();
72
+
73
+ expect(world.get(GlobalConfigId)).toEqual(config);
74
+ expect(warnings).toHaveLength(1);
75
+ expect(warnings[0]).toContain("deprecated");
76
+ expect(warnings[0]).toContain("world.singleton(componentId).set(value)");
77
+ });
78
+
79
+ it("should not expose the deprecated shorthand for numeric singleton types at the type level", () => {
80
+ const world = new World();
81
+ const Score = component<number>();
82
+
83
+ if (false) {
84
+ // @ts-expect-error Numeric singleton shorthand is intentionally unsupported.
85
+ expectType<void>(world.set(Score, 123));
86
+ }
87
+
88
+ expect(true).toBe(true);
44
89
  });
45
90
 
46
91
  it("should manage singleton data through an explicit handle", () => {
@@ -1,5 +1,5 @@
1
1
  import type { ComponentId, EntityId } from "../entity";
2
- import { getDetailedIdType } from "../entity";
2
+ import { getDetailedIdType, isComponentId } from "../entity";
3
3
 
4
4
  /**
5
5
  * Validation and overload-resolution helpers extracted from World.
@@ -51,14 +51,26 @@ export function resolveSetOperation(
51
51
  entityId: EntityId | ComponentId,
52
52
  componentTypeOrComponent?: EntityId | any,
53
53
  maybeComponent?: any,
54
+ argCount = 3,
54
55
  exists: (id: EntityId) => boolean = () => true, // default permissive for tests / internal
55
- ): { entityId: EntityId; componentType: EntityId; component: any } {
56
+ ): { entityId: EntityId; componentType: EntityId; component: any; deprecatedSingletonShorthand: boolean } {
56
57
  const targetEntityId = entityId as EntityId;
58
+
59
+ if (argCount === 2 && isComponentId(targetEntityId) && typeof componentTypeOrComponent !== "number") {
60
+ assertEntityExists(targetEntityId, "Component entity", exists);
61
+ return {
62
+ entityId: targetEntityId,
63
+ componentType: targetEntityId,
64
+ component: componentTypeOrComponent,
65
+ deprecatedSingletonShorthand: true,
66
+ };
67
+ }
68
+
57
69
  const componentType = componentTypeOrComponent as EntityId;
58
70
  assertEntityExists(targetEntityId, "Entity", exists);
59
71
  assertSetComponentTypeValid(componentType);
60
72
 
61
- return { entityId: targetEntityId, componentType, component: maybeComponent };
73
+ return { entityId: targetEntityId, componentType, component: maybeComponent, deprecatedSingletonShorthand: false };
62
74
  }
63
75
 
64
76
  /**
@@ -11,8 +11,9 @@ export interface SingletonHandleOps<T> {
11
11
  /**
12
12
  * Explicit handle for a singleton component (component-as-entity).
13
13
  *
14
- * This provides an explicit and concise API for singleton components without
15
- * overloading `world.set()` semantics.
14
+ * This is the preferred API for singleton components.
15
+ * `world.set(componentId, value)` remains available only as a deprecated
16
+ * compatibility shorthand.
16
17
  *
17
18
  * @example
18
19
  * const config = world.singleton(Config);
@@ -52,6 +52,9 @@ import { SingletonHandle } from "./singleton";
52
52
  * Manages entities and components
53
53
  */
54
54
  export class World {
55
+ private static readonly DEPRECATED_SINGLETON_SET_SHORTHAND_WARNING =
56
+ "world.set(componentId, value) for singleton components is deprecated; use world.singleton(componentId).set(value) or world.set(componentId, componentId, value) instead.";
57
+
55
58
  // Core data structures for entity and archetype management
56
59
  private entityIdManager = new EntityIdManager();
57
60
  private entityReferences: EntityReferencesMap = new Map();
@@ -277,6 +280,10 @@ export class World {
277
280
  * @overload set(entityId: EntityId, componentType: EntityId<void>): void
278
281
  * Marks a void component as present on the entity
279
282
  *
283
+ * @overload set<T>(componentId: ComponentId<T>, component: Exclude<NoInfer<T>, number>): void
284
+ * @deprecated Use `world.singleton(componentId).set(value)` or `world.set(componentId, componentId, value)` instead.
285
+ * Compatibility shorthand for singleton component data when the second argument is not a number
286
+ *
280
287
  * @overload set<T>(entityId: EntityId, componentType: EntityId<T>, component: NoInfer<T>): void
281
288
  * Adds or updates a component with data on the entity
282
289
  *
@@ -287,16 +294,25 @@ export class World {
287
294
  * world.set(entity, Position, { x: 10, y: 20 });
288
295
  * world.set(entity, Marker); // void component
289
296
  * world.singleton(GlobalConfig).set({ debug: true }); // singleton component
297
+ * world.set(GlobalConfig, { debug: true }); // deprecated singleton compatibility shorthand
290
298
  * world.sync(); // Apply changes
291
299
  */
292
300
  set(entityId: EntityId, componentType: EntityId<void>): void;
301
+ /** @deprecated Use `world.singleton(componentId).set(value)` or `world.set(componentId, componentId, value)` instead. */
302
+ set<T>(componentId: ComponentId<T>, component: Exclude<NoInfer<T>, number>): void;
293
303
  set<T>(entityId: EntityId, componentType: EntityId<T>, component: NoInfer<T>): void;
294
304
  set(entityId: EntityId | ComponentId, componentTypeOrComponent?: EntityId | any, maybeComponent?: any): void {
295
305
  const {
296
306
  entityId: targetEntityId,
297
307
  componentType,
298
308
  component,
299
- } = resolveSetOperation(entityId, componentTypeOrComponent, maybeComponent, (id) => this.exists(id));
309
+ deprecatedSingletonShorthand,
310
+ } = resolveSetOperation(entityId, componentTypeOrComponent, maybeComponent, arguments.length, (id) =>
311
+ this.exists(id),
312
+ );
313
+ if (deprecatedSingletonShorthand) {
314
+ console.warn(World.DEPRECATED_SINGLETON_SET_SHORTHAND_WARNING);
315
+ }
300
316
  this.commandBuffer.set(targetEntityId, componentType, component);
301
317
  }
302
318