Skip to content

Commit febf5a7

Browse files
felixtrzmeta-codesync[bot]
authored andcommitted
fix(core): break ecs/world ↔ in it/world-initializer import cycle
Summary: After D105972540 introduced a narrow `sideEffects` allowlist in `iwsdk/core`'s `package.json`, the bundler stopped eliding side-effect-only imports and started honoring the full module graph during code emission. That exposed a latent value-level import cycle: ecs/world.ts → init/index.js (barrel) → init/world-initializer.ts → ecs/index.js → ecs/world.ts Because `createSystem({queries: {required: [X]}})` captures the component reference `X` at class-body evaluation time, this cycle let bundlers (Vite/esbuild prebundle and the production Rollup bundle) emit `AudioSystem`'s class body before `AudioSource` was defined. The query then held `[undefined]`, and the failure surfaced later at `QueryManager.registerQuery` with `Cannot read properties of undefined (reading 'bitmask')`. Three changes break the cycle and prevent regressions: 1. `ecs/world.ts` no longer imports `initializeWorld` statically — it `await import()`s `init/world-initializer.js` from inside `World.create()`. `WorldOptions` is now a `type` import (erased at build time). `launchXR` and `XROptions` come directly from `init/xr.js`, bypassing the `init/index.js` barrel. 2. `audio/audio-system.ts` and `audio/audio.ts` import `Types`, `Entity`, `createSystem`, and `createComponent` directly from `ecs/{component,entity,system}.js` rather than the `ecs/index.js` barrel. Defense-in-depth: even if a future change reintroduces a `world.ts`-side cycle, these modules will no longer participate in it. 3. New `tests/ecs/no-import-cycles.test.ts` asserts that `AudioSystem.queries.audioEntities.required[0]` is `AudioSource` (not `undefined`) at import time. This is a fast TDZ regression guard — if a future bundler change reintroduces the load-order bug, this test fails before any consumer crashes. Reviewed By: cabanier Differential Revision: D106196204 fbshipit-source-id: 1e6b227213d5393161f9f4ce9d962353edcefbbd
1 parent 44a46ff commit febf5a7

46 files changed

Lines changed: 184 additions & 81 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

packages/core/src/asset/asset-manager.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
*/
77

88
import { GLTF } from 'three/examples/jsm/loaders/GLTFLoader.js';
9-
import type { World } from '../ecs/index.js';
9+
import type { World } from '../ecs/world.js';
1010
import { LoadingManager, Texture, WebGLRenderer } from '../runtime/index.js';
1111
import { CacheManager } from './cache-manager.js';
1212
import { AudioAssetLoader } from './loaders/audio-loader.js';

packages/core/src/audio/audio-system.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,14 @@
77

88
import { PositionalAudio, AudioListener, Audio as AmbientAudio } from 'three';
99
import { AssetManager } from '../asset/index.js';
10-
import { Types, Entity, createSystem } from '../ecs/index.js';
10+
// Import directly from submodules — not the `../ecs/index.js` barrel — to
11+
// avoid a value-level import cycle: ecs/index.js re-exports ecs/world.js,
12+
// which (via its transitive deps) loops back to audio/audio-system.js while
13+
// it is still being evaluated. That would leave the AudioSource reference
14+
// captured in `audioEntities.required` as `undefined` at class-body eval time.
15+
import { Types } from '../ecs/component.js';
16+
import { Entity } from '../ecs/entity.js';
17+
import { createSystem } from '../ecs/system.js';
1118
import { AudioInstance, AudioPool } from './audio-pool.js';
1219
import { AudioSource, InstanceStealPolicy, PlaybackMode } from './audio.js';
1320

packages/core/src/audio/audio-utils.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@
55
* LICENSE file in the root directory of this source tree.
66
*/
77

8-
import { Entity } from '../ecs/index.js';
9-
import type { World } from '../ecs/index.js';
8+
import { Entity } from '../ecs/entity.js';
9+
import type { World } from '../ecs/world.js';
1010
import { AudioSource as AudioComponent } from './audio.js';
1111

1212
/** Utility helpers to control {@link AudioSource} without touching Three audio.

packages/core/src/audio/audio.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,9 @@
55
* LICENSE file in the root directory of this source tree.
66
*/
77

8-
import { Types, createComponent } from '../ecs/index.js';
8+
// See audio-system.ts for rationale — import from the subfile, not the
9+
// `../ecs/index.js` barrel, to keep this module free of the world.js cycle.
10+
import { Types, createComponent } from '../ecs/component.js';
911

1012
/**
1113
* Playback behavior when a new play is requested.

packages/core/src/camera/camera-source.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
* LICENSE file in the root directory of this source tree.
66
*/
77

8-
import { Types, createComponent } from '../ecs/index.js';
8+
import { Types, createComponent } from '../ecs/component.js';
99
import { CameraFacing, CameraState } from './types.js';
1010

1111
/**

packages/core/src/camera/camera-system.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,9 @@
55
* LICENSE file in the root directory of this source tree.
66
*/
77

8-
import { createSystem, Entity, VisibilityState } from '../ecs/index.js';
8+
import { createSystem } from '../ecs/system.js';
9+
import { Entity } from '../ecs/entity.js';
10+
import { VisibilityState } from '../ecs/world.js';
911
import { LinearFilter, VideoTexture } from '../runtime/three.js';
1012
import { CameraSource } from './camera-source.js';
1113
import { CameraUtils } from './camera-utils.js';

packages/core/src/camera/camera-utils.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
* LICENSE file in the root directory of this source tree.
66
*/
77

8-
import { Entity } from '../ecs/index.js';
8+
import { Entity } from '../ecs/entity.js';
99
import { CameraSource } from './camera-source.js';
1010
import {
1111
CameraFacing,

packages/core/src/depth/depth-occludable.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,7 @@
55
* LICENSE file in the root directory of this source tree.
66
*/
77

8-
import { Types, createComponent } from '../ecs/index.js';
9-
8+
import { Types, createComponent } from '../ecs/component.js';
109
/** Occlusion shader mode for {@link DepthOccludable}. @category Depth Sensing */
1110
export const OcclusionShadersMode = {
1211
/** Soft occlusion with 13-tap blur sampling for smooth edges. */

packages/core/src/depth/depth-sensing-system.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,9 @@
55
* LICENSE file in the root directory of this source tree.
66
*/
77

8-
import { createSystem, Entity, Types } from '../ecs/index.js';
8+
import { createSystem } from '../ecs/system.js';
9+
import { Entity } from '../ecs/entity.js';
10+
import { Types } from '../ecs/component.js';
911
import { type IUniform, Mesh, Texture, Vector2 } from '../runtime/three.js';
1012
import { DepthOccludable, OcclusionShadersMode } from './depth-occludable.js';
1113
import { DepthTextures } from './depth-textures.js';

packages/core/src/ecs/world.ts

Lines changed: 21 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -11,14 +11,22 @@ import { Signal, signal } from '@preact/signals-core';
1111
import { AnyComponent, World as ElicsWorld } from 'elics';
1212
import { AssetManager } from '../asset/index.js';
1313
// Environment is driven by components/systems; no world helpers
14-
import {
15-
WorldOptions,
16-
initializeWorld,
17-
XROptions,
18-
launchXR,
19-
} from '../init/index.js';
14+
// NOTE: import `launchXR` and types directly from submodules — not the
15+
// `../init/index.js` barrel — so loading this file does not pull in
16+
// `init/world-initializer.js`. That would create a value-level cycle with
17+
// audio-system.ts (and other systems that import the ecs barrel), causing
18+
// the bundler to emit System classes before their referenced Component
19+
// constants, producing `undefined.bitmask` crashes at QueryManager.registerQuery.
20+
// `initializeWorld` is loaded lazily inside `World.create` for the same reason.
21+
import type { WorldOptions } from '../init/world-initializer.js';
22+
import { launchXR } from '../init/xr.js';
23+
import type { XROptions } from '../init/xr.js';
2024
import type { InputManager } from '../input/index.js';
21-
import { LevelTag } from '../level/index.js';
25+
// Import LevelTag from its leaf module — not '../level/index.js' — so that
26+
// loading ecs/world.ts does not transitively pull in level-system.ts (which
27+
// would re-enter '../ecs/index.js' and form a cycle, leading to TDZ
28+
// undefined.bitmask crashes for any component captured in System.queries).
29+
import { LevelTag } from '../level/level-tag.js';
2230
import type { MCPRuntime } from '../mcp/index.js';
2331
import type { Object3DEventMap } from '../runtime/index.js';
2432
import {
@@ -28,7 +36,10 @@ import {
2836
Scene,
2937
WebGLRenderer,
3038
} from '../runtime/index.js';
31-
import { Transform } from '../transform/index.js';
39+
// See note above on LevelTag — import Transform directly from its leaf
40+
// module to avoid cycling back through '../ecs/index.js' via the transform
41+
// barrel.
42+
import { Transform } from '../transform/transform.js';
3243
import { Entity } from './entity.js';
3344

3445
export enum VisibilityState {
@@ -264,10 +275,11 @@ export class World extends ElicsWorld {
264275
* - If {@link WorldOptions.level} is provided, the LevelSystem will load it after assets are preloaded.
265276
* @see /getting-started/01-hello-xr
266277
*/
267-
static create(
278+
static async create(
268279
container: HTMLDivElement,
269280
options?: WorldOptions,
270281
): Promise<World> {
282+
const { initializeWorld } = await import('../init/world-initializer.js');
271283
return initializeWorld(container, options);
272284
}
273285
}

0 commit comments

Comments
 (0)