Skip to content

Commit 271785b

Browse files
authored
feature: add code-to-composite command (#1232)
* feature: add code-to-composite command * fix some bugs on the code-to-composite cmd * add ~system mocks * add: asset migrator * update: bundle entrypoint + comment out only entrypoint files * add: name generator * add: warning * remove: --force option * add: --yes arg * add: comment explaining Name component filtering * add: AudioSource & VideoPlayer to name generator * add: suppot for .bmp on asset migrator * add: MeshCollider to name generator * add: filtering out unsopported components * remove: entities that have any react-ecs component * fix: wrong path when working from outside scene * remove: Transform from PlayerEntity * update: code-comenter to comment out all src files * remove: backup file
1 parent 6268935 commit 271785b

17 files changed

Lines changed: 1338 additions & 12 deletions

File tree

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

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

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
"dependencies": {
1010
"@dcl/crypto": "^3.4.4",
1111
"@dcl/ecs": "file:../ecs",
12+
"@dcl/gltf-validator-ts": "1.0.14",
1213
"@dcl/hashing": "1.1.3",
1314
"@dcl/inspector": "7.13.5",
1415
"@dcl/linker-dapp": "^0.14.2",
Lines changed: 309 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,309 @@
1+
import path from 'path'
2+
import { IEngine } from '@dcl/ecs/dist-cjs'
3+
import { validateBytes } from '@dcl/gltf-validator-ts'
4+
5+
import { CliComponents } from '../../components'
6+
import { SceneProject } from '../../logic/project-validations'
7+
8+
/**
9+
* Asset Migrator for code-to-composite command
10+
*
11+
* This module handles migrating asset files to the Creator Hub directory structure:
12+
* - Models: assets/scene/Models/{modelName}/*.{gltf,glb} (with dependencies)
13+
* - Images: assets/scene/Images/*.{png,jpg,jpeg,bmp}
14+
* - Audio: assets/scene/Audio/*.{mp3,wav,ogg}
15+
* - Video: assets/scene/Video/*.{mp4}
16+
*
17+
* Models are treated specially: each model gets its own folder with all dependencies
18+
* (textures, .bin files) copied into it, even if originally shared between models.
19+
*/
20+
21+
export type AssetType = 'Models' | 'Images' | 'Audio' | 'Video' | 'Other'
22+
23+
interface ModelDependencies {
24+
textures: string[]
25+
binaries: string[]
26+
}
27+
28+
/**
29+
* Determines the asset type based on file extension
30+
*/
31+
function getAssetType(extension: string): AssetType {
32+
const ext = extension.toLowerCase().replace(/^\./, '')
33+
34+
switch (ext) {
35+
case 'gltf':
36+
case 'glb':
37+
return 'Models'
38+
case 'png':
39+
case 'jpg':
40+
case 'jpeg':
41+
case 'bmp':
42+
return 'Images'
43+
case 'mp3':
44+
case 'wav':
45+
case 'ogg':
46+
return 'Audio'
47+
case 'mp4':
48+
return 'Video'
49+
default:
50+
return 'Other'
51+
}
52+
}
53+
54+
/**
55+
* Checks if a string value looks like an asset path
56+
*/
57+
function looksLikeAssetPath(value: string): boolean {
58+
if (typeof value !== 'string' || value.length === 0) {
59+
return false
60+
}
61+
62+
const ext = path.extname(value)
63+
if (!ext) {
64+
return false
65+
}
66+
67+
return getAssetType(ext) !== 'Other'
68+
}
69+
70+
/**
71+
* Extracts dependencies from a GLTF file using @dcl/gltf-validator-ts
72+
*/
73+
async function extractGltfDependencies(
74+
components: Pick<CliComponents, 'fs' | 'logger'>,
75+
gltfPath: string
76+
): Promise<ModelDependencies> {
77+
const { fs, logger } = components
78+
const dependencies: ModelDependencies = {
79+
textures: [],
80+
binaries: []
81+
}
82+
83+
try {
84+
const gltfBuffer = await fs.readFile(gltfPath)
85+
86+
const result = await validateBytes(new Uint8Array(gltfBuffer), {
87+
externalResourceFunction: () => Promise.resolve(new Uint8Array())
88+
})
89+
90+
if (result.info && result.info.resources) {
91+
for (const resource of result.info.resources) {
92+
if (resource.storage === 'external' && resource.uri) {
93+
const uri = resource.uri
94+
95+
if (resource.pointer && resource.pointer.includes('image')) {
96+
dependencies.textures.push(uri)
97+
} else if (resource.pointer && resource.pointer.includes('buffer')) {
98+
dependencies.binaries.push(uri)
99+
}
100+
}
101+
}
102+
}
103+
} catch (error) {
104+
logger.error(` ⚠ Failed to extract dependencies from GLTF "${gltfPath}": ${error}`)
105+
}
106+
107+
return dependencies
108+
}
109+
110+
/**
111+
* Builds the destination path following Creator Hub conventions
112+
*/
113+
function getDestinationPath(sceneRoot: string, originalPath: string): string {
114+
const fileName = path.basename(originalPath)
115+
const extension = path.extname(originalPath)
116+
const assetType = getAssetType(extension)
117+
118+
if (assetType === 'Models') {
119+
// Models get their own folder: assets/scene/Models/{modelName}/file.glb
120+
const modelName = path.basename(originalPath, extension)
121+
return path.join(sceneRoot, 'assets', 'scene', 'Models', modelName, fileName)
122+
}
123+
124+
// Other assets go directly in type folder: assets/scene/Images/file.png
125+
return path.join(sceneRoot, 'assets', 'scene', assetType, fileName)
126+
}
127+
128+
/**
129+
* Recursively finds all asset paths in component data
130+
*/
131+
function collectAssetPaths(data: any, assetPaths: Set<string>): void {
132+
if (data === null || data === undefined) {
133+
return
134+
}
135+
136+
if (typeof data === 'string') {
137+
if (looksLikeAssetPath(data)) {
138+
assetPaths.add(data)
139+
}
140+
return
141+
}
142+
143+
if (Array.isArray(data)) {
144+
for (const item of data) {
145+
collectAssetPaths(item, assetPaths)
146+
}
147+
return
148+
}
149+
150+
if (typeof data === 'object') {
151+
for (const value of Object.values(data)) {
152+
collectAssetPaths(value, assetPaths)
153+
}
154+
}
155+
}
156+
157+
/**
158+
* Recursively replaces asset paths in component data
159+
* Returns an object with the updated data and a flag indicating if changes were made
160+
*/
161+
function replaceAssetPaths(
162+
data: any,
163+
pathMapping: Map<string, string>
164+
): { data: any; hasChanges: boolean } {
165+
if (data === null || data === undefined) {
166+
return { data, hasChanges: false }
167+
}
168+
169+
if (typeof data === 'string') {
170+
const newPath = pathMapping.get(data)
171+
if (newPath !== undefined) {
172+
return { data: newPath, hasChanges: true }
173+
}
174+
return { data, hasChanges: false }
175+
}
176+
177+
if (Array.isArray(data)) {
178+
let hasChanges = false
179+
const result = data.map((item) => {
180+
const replaced = replaceAssetPaths(item, pathMapping)
181+
if (replaced.hasChanges) hasChanges = true
182+
return replaced.data
183+
})
184+
return { data: result, hasChanges }
185+
}
186+
187+
if (typeof data === 'object') {
188+
let hasChanges = false
189+
const result: any = {}
190+
for (const [key, value] of Object.entries(data)) {
191+
const replaced = replaceAssetPaths(value, pathMapping)
192+
if (replaced.hasChanges) hasChanges = true
193+
result[key] = replaced.data
194+
}
195+
return { data: result, hasChanges }
196+
}
197+
198+
return { data, hasChanges: false }
199+
}
200+
201+
/**
202+
* Main asset migration function
203+
*
204+
* Process:
205+
* 1. Scan all components to find asset references
206+
* 2. Copy asset files to new Creator Hub structure
207+
* 3. Update all components with new asset paths
208+
*/
209+
export async function migrateAssets(
210+
components: Pick<CliComponents, 'fs' | 'logger'>,
211+
project: SceneProject,
212+
engine: IEngine
213+
): Promise<number> {
214+
const { fs, logger } = components
215+
const sceneRoot = project.workingDirectory
216+
217+
// Step 1: find all asset paths in the engine
218+
const assetPaths = new Set<string>()
219+
for (const component of engine.componentsIter()) {
220+
for (const [entity] of engine.getEntitiesWith(component)) {
221+
const componentData = component.get(entity)
222+
if (componentData) {
223+
collectAssetPaths(componentData, assetPaths)
224+
}
225+
}
226+
}
227+
228+
if (assetPaths.size === 0) {
229+
logger.log('No assets found to migrate')
230+
return 0
231+
}
232+
233+
logger.log(`Found ${assetPaths.size} asset reference(s)`)
234+
235+
// Step 2: copy files to new locations
236+
const pathMapping = new Map<string, string>()
237+
238+
for (const originalPath of assetPaths) {
239+
const absoluteOriginalPath = path.isAbsolute(originalPath) ? originalPath : path.join(sceneRoot, originalPath)
240+
241+
if (!(await fs.fileExists(absoluteOriginalPath))) {
242+
logger.warn(` ⚠ Asset not found: ${originalPath}`)
243+
continue
244+
}
245+
246+
const assetType = getAssetType(path.extname(originalPath))
247+
const newAbsolutePath = getDestinationPath(sceneRoot, originalPath)
248+
const newRelativePath = path.relative(sceneRoot, newAbsolutePath)
249+
250+
await fs.mkdir(path.dirname(newAbsolutePath), { recursive: true })
251+
await fs.copyFile(absoluteOriginalPath, newAbsolutePath)
252+
253+
pathMapping.set(originalPath, newRelativePath)
254+
255+
logger.log(` ✓ ${originalPath}${newRelativePath}`)
256+
257+
// special handling for models: copy dependencies
258+
if (assetType === 'Models') {
259+
const dependencies = await extractGltfDependencies(components, absoluteOriginalPath)
260+
261+
if (dependencies.textures.length === 0 && dependencies.binaries.length === 0) {
262+
continue
263+
}
264+
265+
const allDependencies = [...dependencies.textures, ...dependencies.binaries]
266+
267+
for (const depFileName of allDependencies) {
268+
const depOriginalPath = path.join(path.dirname(absoluteOriginalPath), depFileName)
269+
270+
if (await fs.fileExists(depOriginalPath)) {
271+
const depNewPath = path.join(path.dirname(newAbsolutePath), depFileName)
272+
273+
await fs.copyFile(depOriginalPath, depNewPath)
274+
275+
const depOriginalRelative = path.join(path.dirname(originalPath), depFileName)
276+
const depNewRelative = path.relative(sceneRoot, depNewPath)
277+
pathMapping.set(depOriginalRelative, depNewRelative)
278+
279+
logger.log(` ↳ ${depFileName}`)
280+
}
281+
}
282+
}
283+
}
284+
285+
logger.log(`Migrated ${pathMapping.size} asset file(s)`)
286+
287+
// Step 3: update components with new paths
288+
let updatedCount = 0
289+
290+
for (const component of engine.componentsIter()) {
291+
for (const [entity] of engine.getEntitiesWith(component)) {
292+
const componentData = component.get(entity)
293+
294+
if (componentData) {
295+
const { data: updatedData, hasChanges } = replaceAssetPaths(componentData, pathMapping)
296+
297+
if (hasChanges) {
298+
// only supporting LastWriteWinElementSetComponentDefinition (inspector does the same thing)
299+
if ('createOrReplace' in component) {
300+
component.createOrReplace(entity, updatedData)
301+
updatedCount++
302+
}
303+
}
304+
}
305+
}
306+
}
307+
308+
return updatedCount
309+
}

0 commit comments

Comments
 (0)