Skip to content

Commit a5e11cb

Browse files
authored
feat: add type safe entity names file (#1107)
* feat: add type safe entity names file * fix build: * simplify enum types * update tests
1 parent 27ceddb commit a5e11cb

File tree

19 files changed

+152
-42
lines changed

19 files changed

+152
-42
lines changed

packages/@dcl/ecs/src/engine/index.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -189,14 +189,19 @@ function preEngine(options?: IEngineOptions): PreEngine {
189189
}
190190
}
191191

192-
function getEntityOrNullByName(value: string) {
192+
function getEntityOrNullByName<T = string>(value: T) {
193193
const NameComponent = components.Name({ defineComponent })
194194
for (const [entity, name] of getEntitiesWith(NameComponent)) {
195195
if (name.value === value) return entity
196196
}
197197
return null
198198
}
199199

200+
function getEntityByName<T = never, K = T>(value: K & (T extends never ? never : string)): Entity {
201+
const entity = getEntityOrNullByName(value)
202+
return entity!
203+
}
204+
200205
function* getComponentDefGroup<T extends ComponentDefinition<any>[]>(...args: T): Iterable<[Entity, ...T]> {
201206
const [firstComponentDef, ...componentDefinitions] = args
202207
for (const [entity] of firstComponentDef.iterator()) {
@@ -252,6 +257,7 @@ function preEngine(options?: IEngineOptions): PreEngine {
252257
getComponent,
253258
getComponentOrNull: getComponentOrNull as IEngine['getComponentOrNull'],
254259
getEntityOrNullByName,
260+
getEntityByName,
255261
removeComponentDefinition,
256262
registerComponentDefinition,
257263
entityContainer,
@@ -313,6 +319,7 @@ export function Engine(options?: IEngineOptions): IEngine {
313319
componentsIter: partialEngine.componentsIter,
314320
seal: partialEngine.seal,
315321
getEntityOrNullByName: partialEngine.getEntityOrNullByName,
322+
getEntityByName: partialEngine.getEntityByName,
316323

317324
update,
318325

packages/@dcl/ecs/src/engine/types.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ export type PreEngine = Pick<
6262
| 'seal'
6363
| 'entityContainer'
6464
| 'getEntityOrNullByName'
65+
| 'getEntityByName'
6566
> & {
6667
getSystems: () => SystemItem[]
6768
}
@@ -245,7 +246,15 @@ export interface IEngine {
245246
* Search for the entity that matches de label string defined in the editor.
246247
* @param value - Name value string
247248
*/
248-
getEntityOrNullByName(label: string): Entity | null
249+
getEntityOrNullByName<T = string>(label: T): Entity | null
250+
251+
/**
252+
* @public
253+
* Search for the entity that matches de label string defined in the editor.
254+
* @param value - Name value string
255+
* @typeParam T - The type of the entity name value
256+
*/
257+
getEntityByName<T = never, K = T>(value: K & (T extends never ? never : string)): Entity
249258

250259
/**
251260
* @public

packages/@dcl/inspector/src/lib/data-layer/host/utils/composite-dirty.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import {
99
} from '@dcl/ecs'
1010
import { initComponents } from '@dcl/asset-packs'
1111
import { EditorComponentNames, EditorComponents } from '../../../sdk/components'
12-
import { dumpEngineToComposite, dumpEngineToCrdtCommands } from './engine-to-composite'
12+
import { dumpEngineToComposite, dumpEngineToCrdtCommands, generateEntityNamesType } from './engine-to-composite'
1313
import { FileSystemInterface } from '../../types'
1414
import { CompositeManager, createFsCompositeProvider } from './fs-composite-provider'
1515
import { getMinimalComposite } from '../../client/feeded-local-fs'
@@ -21,6 +21,7 @@ import { toSceneComponent } from './component'
2121
import { addNodesComponentsToPlayerAndCamera } from './migrations/add-nodes-to-player-and-camera'
2222
import { fixNetworkEntityValues } from './migrations/fix-network-entity-values'
2323
import { selectSceneEntity } from './migrations/select-scene-entity'
24+
import { DIRECTORY, withAssetDir } from '../fs-utils'
2425

2526
enum DirtyEnum {
2627
// No changes
@@ -108,6 +109,9 @@ export async function compositeAndDirty(
108109
await fs.writeFile('main.crdt', Buffer.from(mainCrdt))
109110
await compositeProvider.save({ src: compositePath, composite }, 'json')
110111

112+
// Generate entity names type file
113+
await generateEntityNamesType(engine, withAssetDir(DIRECTORY.SCENE + '/entity-names.ts'), 'EntityNames', fs)
114+
111115
return composite
112116
} catch (e) {
113117
console.log('Failed saving composite')

packages/@dcl/inspector/src/lib/data-layer/host/utils/engine-to-composite.ts

Lines changed: 87 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,11 @@ import {
88
IEngine,
99
LastWriteWinElementSetComponentDefinition,
1010
PutComponentOperation,
11-
getCompositeRootComponent
11+
getCompositeRootComponent,
12+
Name
1213
} from '@dcl/ecs'
1314
import { ReadWriteByteBuffer } from '@dcl/ecs/dist/serialization/ByteBuffer'
15+
import { FileSystemInterface } from '../../types'
1416

1517
function componentToCompositeComponentData<T>(
1618
$case: 'json' | 'binary',
@@ -128,3 +130,87 @@ export function dumpEngineToCrdtCommands(engine: IEngine): Uint8Array {
128130

129131
return crdtBuffer.toBinary()
130132
}
133+
134+
/**
135+
* Generate a TypeScript declaration file with a string literal union type containing all entity names in the scene.
136+
* This allows for type-safe references to entities by name in scene scripts.
137+
*
138+
* @param engine The ECS engine instance
139+
* @param outputPath The path where the .d.ts file should be written
140+
* @param typeName The name for the generated type (default: "SceneEntityNames")
141+
* @param fs FileSystem interface for writing the file
142+
* @returns Promise that resolves when the file has been written
143+
*/
144+
export async function generateEntityNamesType(
145+
engine: IEngine,
146+
outputPath: string = 'scene-entity-names.d.ts',
147+
typeName: string = 'SceneEntityNames',
148+
fs: FileSystemInterface
149+
): Promise<void> {
150+
try {
151+
// Find the Name component definition
152+
153+
const NameComponent: typeof Name = engine.getComponentOrNull(Name.componentId) as typeof Name
154+
155+
if (!NameComponent) {
156+
throw new Error('Name component not found in engine')
157+
}
158+
159+
// Collect all names from entities
160+
const names: string[] = []
161+
for (const [_, nameValue] of engine.getEntitiesWith(NameComponent)) {
162+
if (nameValue.value) {
163+
names.push(nameValue.value)
164+
}
165+
}
166+
167+
// Sort names for consistency
168+
names.sort()
169+
170+
// Remove duplicates
171+
const uniqueNames = Array.from(new Set(names))
172+
173+
// Generate valid TypeScript identifiers and handle duplicates in a single pass
174+
const validNameMap = new Map<string, string>()
175+
const finalNames: Array<{ original: string; valid: string }> = []
176+
177+
for (const name of uniqueNames) {
178+
// Create a valid TypeScript identifier
179+
let validName = name
180+
.replace(/[^a-zA-Z0-9_]/g, '_') // Replace non-alphanumeric chars with underscore
181+
.replace(/^[0-9]/, '_$&') // Prepend underscore if starts with number
182+
183+
// Handle collision if this valid name already exists
184+
if (validNameMap.has(validName)) {
185+
// Add numeric suffix for uniqueness
186+
let suffix = 1
187+
while (validNameMap.has(`${validName}_${suffix}`)) {
188+
suffix++
189+
}
190+
validName = `${validName}_${suffix}`
191+
}
192+
193+
// Store the mapping and add to result
194+
validNameMap.set(validName, name)
195+
finalNames.push({ original: name, valid: validName })
196+
}
197+
198+
// Generate the .d.ts file content
199+
let fileContent = `// Auto-generated entity names from the scene\n\n`
200+
201+
// Add a constant object with name keys for IDE autocompletion
202+
fileContent += `\n/**\n * Object containing all entity names in the scene for autocomplete support.\n */\n`
203+
fileContent += `export enum ${typeName} {\n`
204+
205+
for (const { original, valid } of finalNames) {
206+
fileContent += ` ${valid} = "${original}",\n`
207+
}
208+
209+
fileContent += `} \n`
210+
211+
// Write to file
212+
await fs.writeFile(outputPath, Buffer.from(fileContent, 'utf-8'))
213+
} catch (e) {
214+
console.error('Fail to generate entity names types', e)
215+
}
216+
}

packages/@dcl/playground-assets/etc/playground-assets.api.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1247,8 +1247,9 @@ export interface IEngine {
12471247
getComponent<T>(componentId: number | string): ComponentDefinition<T>;
12481248
getComponentOrNull<T>(componentId: number | string): ComponentDefinition<T> | null;
12491249
getEntitiesWith<T extends [ComponentDefinition<any>, ...ComponentDefinition<any>[]]>(...components: T): Iterable<[Entity, ...ReadonlyComponentSchema<T>]>;
1250+
getEntityByName<T = never, K = T>(value: K & (T extends never ? never : string)): Entity;
12501251
// @alpha
1251-
getEntityOrNullByName(label: string): Entity | null;
1252+
getEntityOrNullByName<T = string>(label: T): Entity | null;
12521253
getEntityState(entity: Entity): EntityState;
12531254
// (undocumented)
12541255
_id: number;

packages/@dcl/sdk-commands/package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

test/ecs/components/Name.spec.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,5 +12,8 @@ describe('Generated Raycast ProtoBuf', () => {
1212

1313
expect(newEngine.getEntityOrNullByName('CASLA')).toBeDefined()
1414
expect(newEngine.getEntityOrNullByName('Boedo')).toBe(null)
15+
16+
expect(newEngine.getEntityByName<string>('CASLA')).toBeDefined()
17+
expect(newEngine.getEntityByName<string>('Boedo')).toBe(null)
1518
})
1619
})

test/snapshots/development-bundles/static-scene.test.ts.crdt

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
SCENE_COMPILED_JS_SIZE_PROD=462.7k bytes
1+
SCENE_COMPILED_JS_SIZE_PROD=462.9k bytes
22
THE BUNDLE HAS SOURCEMAPS
33
(start empty vm 0.21.0-3680274614.commit-1808aa1)
44
OPCODES ~= 0k
@@ -11,7 +11,7 @@ EVAL test/snapshots/development-bundles/static-scene.test.js
1111
REQUIRE: ~system/EngineApi
1212
REQUIRE: ~system/EngineApi
1313
OPCODES ~= 54k
14-
MALLOC_COUNT = 13727
14+
MALLOC_COUNT = 13735
1515
ALIVE_OBJS_DELTA ~= 2.70k
1616
CALL onStart()
1717
main.crdt: PUT_COMPONENT e=0x200 c=1 t=0 data={"position":{"x":5.880000114440918,"y":2.7916901111602783,"z":7.380000114440918},"rotation":{"x":0,"y":0,"z":0,"w":1},"scale":{"x":1,"y":1,"z":1},"parent":0}
@@ -56,4 +56,4 @@ CALL onUpdate(0.1)
5656
OPCODES ~= 3k
5757
MALLOC_COUNT = -5
5858
ALIVE_OBJS_DELTA ~= 0.00k
59-
MEMORY_USAGE_COUNT ~= 1209.35k bytes
59+
MEMORY_USAGE_COUNT ~= 1209.94k bytes

test/snapshots/development-bundles/testing-fw.test.ts.crdt

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
SCENE_COMPILED_JS_SIZE_PROD=463.2k bytes
1+
SCENE_COMPILED_JS_SIZE_PROD=463.4k bytes
22
THE BUNDLE HAS SOURCEMAPS
33
(start empty vm 0.21.0-3680274614.commit-1808aa1)
44
OPCODES ~= 0k
@@ -11,7 +11,7 @@ EVAL test/snapshots/development-bundles/testing-fw.test.js
1111
REQUIRE: ~system/EngineApi
1212
REQUIRE: ~system/EngineApi
1313
OPCODES ~= 63k
14-
MALLOC_COUNT = 14248
14+
MALLOC_COUNT = 14260
1515
ALIVE_OBJS_DELTA ~= 2.85k
1616
CALL onStart()
1717
LOG: ["Adding one to position.y=0"]
@@ -64,4 +64,4 @@ CALL onUpdate(0.1)
6464
OPCODES ~= 5k
6565
MALLOC_COUNT = -40
6666
ALIVE_OBJS_DELTA ~= -0.01k
67-
MEMORY_USAGE_COUNT ~= 1217.97k bytes
67+
MEMORY_USAGE_COUNT ~= 1218.62k bytes

test/snapshots/development-bundles/two-way-crdt.test.ts.crdt

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
SCENE_COMPILED_JS_SIZE_PROD=463.3k bytes
1+
SCENE_COMPILED_JS_SIZE_PROD=463.4k bytes
22
THE BUNDLE HAS SOURCEMAPS
33
(start empty vm 0.21.0-3680274614.commit-1808aa1)
44
OPCODES ~= 0k
@@ -11,7 +11,7 @@ EVAL test/snapshots/development-bundles/two-way-crdt.test.js
1111
REQUIRE: ~system/EngineApi
1212
REQUIRE: ~system/EngineApi
1313
OPCODES ~= 63k
14-
MALLOC_COUNT = 14248
14+
MALLOC_COUNT = 14260
1515
ALIVE_OBJS_DELTA ~= 2.85k
1616
CALL onStart()
1717
LOG: ["Adding one to position.y=0"]
@@ -64,4 +64,4 @@ CALL onUpdate(0.1)
6464
OPCODES ~= 5k
6565
MALLOC_COUNT = -40
6666
ALIVE_OBJS_DELTA ~= -0.01k
67-
MEMORY_USAGE_COUNT ~= 1217.97k bytes
67+
MEMORY_USAGE_COUNT ~= 1218.63k bytes

test/snapshots/production-bundles/append-value-crdt.ts.crdt

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
SCENE_COMPILED_JS_SIZE_PROD=203k bytes
1+
SCENE_COMPILED_JS_SIZE_PROD=203.1k bytes
22
(start empty vm 0.21.0-3680274614.commit-1808aa1)
33
OPCODES ~= 0k
44
MALLOC_COUNT = 1005
@@ -9,7 +9,7 @@ EVAL test/snapshots/production-bundles/append-value-crdt.js
99
REQUIRE: ~system/EngineApi
1010
REQUIRE: ~system/EngineApi
1111
OPCODES ~= 65k
12-
MALLOC_COUNT = 12773
12+
MALLOC_COUNT = 12784
1313
ALIVE_OBJS_DELTA ~= 2.85k
1414
CALL onStart()
1515
Renderer: APPEND_VALUE e=0x200 c=1063 t=0 data={"button":0,"hit":{"position":{"x":1,"y":2,"z":3},"globalOrigin":{"x":1,"y":2,"z":3},"direction":{"x":1,"y":2,"z":3},"normalHit":{"x":1,"y":2,"z":3},"length":10,"meshName":"mesh","entityId":512},"state":1,"timestamp":1,"analog":5,"tickNumber":0}
@@ -55,4 +55,4 @@ CALL onUpdate(0.1)
5555
OPCODES ~= 14k
5656
MALLOC_COUNT = 31
5757
ALIVE_OBJS_DELTA ~= 0.01k
58-
MEMORY_USAGE_COUNT ~= 909.46k bytes
58+
MEMORY_USAGE_COUNT ~= 909.93k bytes

test/snapshots/production-bundles/billboard.ts.crdt

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
SCENE_COMPILED_JS_SIZE_PROD=236k bytes
1+
SCENE_COMPILED_JS_SIZE_PROD=236.1k bytes
22
(start empty vm 0.21.0-3680274614.commit-1808aa1)
33
OPCODES ~= 0k
44
MALLOC_COUNT = 1005
@@ -9,7 +9,7 @@ EVAL test/snapshots/production-bundles/billboard.js
99
REQUIRE: ~system/EngineApi
1010
REQUIRE: ~system/EngineApi
1111
OPCODES ~= 66k
12-
MALLOC_COUNT = 14885
12+
MALLOC_COUNT = 14892
1313
ALIVE_OBJS_DELTA ~= 3.25k
1414
CALL onStart()
1515
OPCODES ~= 0k
@@ -77,4 +77,4 @@ CALL onUpdate(0.1)
7777
OPCODES ~= 10k
7878
MALLOC_COUNT = 0
7979
ALIVE_OBJS_DELTA ~= 0.00k
80-
MEMORY_USAGE_COUNT ~= 1051.90k bytes
80+
MEMORY_USAGE_COUNT ~= 1052.22k bytes

test/snapshots/production-bundles/cube-deleted.ts.crdt

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
SCENE_COMPILED_JS_SIZE_PROD=199.2k bytes
1+
SCENE_COMPILED_JS_SIZE_PROD=199.3k bytes
22
(start empty vm 0.21.0-3680274614.commit-1808aa1)
33
OPCODES ~= 0k
44
MALLOC_COUNT = 1005
@@ -9,7 +9,7 @@ EVAL test/snapshots/production-bundles/cube-deleted.js
99
REQUIRE: ~system/EngineApi
1010
REQUIRE: ~system/EngineApi
1111
OPCODES ~= 55k
12-
MALLOC_COUNT = 11927
12+
MALLOC_COUNT = 11935
1313
ALIVE_OBJS_DELTA ~= 2.63k
1414
CALL onStart()
1515
OPCODES ~= 0k
@@ -42,4 +42,4 @@ CALL onUpdate(0.1)
4242
OPCODES ~= 5k
4343
MALLOC_COUNT = 1
4444
ALIVE_OBJS_DELTA ~= 0.00k
45-
MEMORY_USAGE_COUNT ~= 872.22k bytes
45+
MEMORY_USAGE_COUNT ~= 872.62k bytes

test/snapshots/production-bundles/cube.ts.crdt

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
SCENE_COMPILED_JS_SIZE_PROD=199.1k bytes
1+
SCENE_COMPILED_JS_SIZE_PROD=199.2k bytes
22
(start empty vm 0.21.0-3680274614.commit-1808aa1)
33
OPCODES ~= 0k
44
MALLOC_COUNT = 1005
@@ -9,7 +9,7 @@ EVAL test/snapshots/production-bundles/cube.js
99
REQUIRE: ~system/EngineApi
1010
REQUIRE: ~system/EngineApi
1111
OPCODES ~= 55k
12-
MALLOC_COUNT = 11900
12+
MALLOC_COUNT = 11908
1313
ALIVE_OBJS_DELTA ~= 2.62k
1414
CALL onStart()
1515
OPCODES ~= 0k
@@ -32,4 +32,4 @@ CALL onUpdate(0.1)
3232
OPCODES ~= 1k
3333
MALLOC_COUNT = 0
3434
ALIVE_OBJS_DELTA ~= 0.00k
35-
MEMORY_USAGE_COUNT ~= 862.13k bytes
35+
MEMORY_USAGE_COUNT ~= 862.53k bytes

0 commit comments

Comments
 (0)