Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions docs/ai-agent-context.md
Original file line number Diff line number Diff line change
Expand Up @@ -110,14 +110,14 @@ Uses esbuild with `platform: 'browser'`, `format: 'cjs'`, `target: 'es2020'`, an
| `-p, --port <number>` | HTTP port (auto-detected if omitted) |
| `--dclenv <env>` | Explorer environment: `org` (mainnet production, default), `zone` (staging), `today` |
| `--realm <name>` | Realm name shown in Explorer (default: `Localhost`) |
| `--web3` | Enable Web3 wallet integration in the preview |
| `--web3` | (deprecated) No effect; kept for backwards compatibility |
| `--skip-build` | Serve pre-built files without rebuilding |
| `--no-watch` | Disable file watching / hot reload |
| `--no-browser` | Don't auto-open Explorer |
| `--ci` | CI mode: disable browser and debug panel |
| `--debug` | Enable scene debug panel (on by default with `--explorer-alpha`) |
| `--explorer-alpha` | Use the new Alpha Explorer deeplink (default) |
| `--web-explorer` | Use legacy web-based Explorer |
| `--web, --bevy-web` | Open the preview in Bevy Web (`https://decentraland.org/bevy-web/`) instead of the desktop Explorer. Chrome 142+ requires the Local Network Access permission ("Apps on device" in 145+) for the page to reach the localhost preview server; the CLI prints instructions |
| `--mobile` | Print ASCII QR code for mobile preview |
| `--position <x,y>` | Initial spawn position (default: from `scene.json`) |
| `--skip-auth-screen` | Skip Explorer's authentication screen |
Expand Down
63 changes: 21 additions & 42 deletions packages/@dcl/sdk-commands/src/commands/start/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,6 @@ export const args = declareArgs({
'--skip-build': Boolean,
'--data-layer': Boolean,
'--explorer-alpha': Boolean,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P1] --web-explorer was removed from declareArgs, but decentraland/godot-explorer (scripts/bench/launch_devices.sh) and decentraland/docs still reference it. The arg library throws on unknown flags, so this will crash those scripts on the next SDK release.

Keep it as a deprecated no-op (same pattern as --web3 and --no-debug):

Suggested change
'--explorer-alpha': Boolean,
'--explorer-alpha': Boolean,
'--web-explorer': Boolean, // deprecated no-op, kept for backwards compatibility

'--web-explorer': Boolean,
'--hub': Boolean,
'--mobile': Boolean,
'-m': '--mobile',
Expand All @@ -67,6 +66,7 @@ export const args = declareArgs({
'--landscape-terrain-enabled': Boolean,
'-n': Boolean,
'--bevy-web': Boolean,
'--web': '--bevy-web',
'--multi-instance': Boolean,
'--no-client': Boolean,
'--mcp': Boolean,
Expand All @@ -81,13 +81,12 @@ export async function help(options: Options) {

-h, --help Displays complete help
-p, --port Select a custom port for the development server
-d, --no-debug Disable debugging panel
-d, --no-debug (deprecated) No effect. Kept for backwards compatibility
-b, --no-browser Do not open a new browser window
-w, --no-watch Do not open watch for filesystem changes
-c, --ci Run the parcel previewer on a remote unix server
--web3 Connects preview to browser wallet to use the associated avatar and account
--web3 (deprecated) No effect. Kept for backwards compatibility
--skip-build Skip build and only serve the files in preview mode
--web-explorer Launch the scene in the Web Explorer
--debug Enables Debug panel mode inside DCL Explorer (default=true)
--dclenv Decentraland Environment. Which environment to use for the content. This determines the catalyst server used, asset-bundles, etc. Possible values: org, zone, today. (default=org)
--realm Realm used to serve the content. (default=Localhost)
Expand All @@ -96,7 +95,7 @@ export async function help(options: Options) {
--skip-auth-screen Skip the auth screen (accepts 'true' or 'false').
--landscape-terrain-enabled Enable landscape terrain.
-n Open a new instance of the Client even if one is already running.
--bevy-web Opens preview using the Bevy Web browser window.
--web, --bevy-web Opens preview using the Bevy Web browser window.
--mobile Show QR code for mobile preview on the same network.
--multi-instance Allow running multiple Explorer instances simultaneously.
--no-client Suppress every auto-launch (desktop Explorer deeplink, browser open, mobile QR). The file watcher still notifies a desktop Explorer if it connects on its own — useful when an external tool owns the Explorer process.
Expand Down Expand Up @@ -127,27 +126,23 @@ export async function main(options: Options) {
let baseCoords = { x: 0, y: 0 }
const workingDirectory = path.resolve(process.cwd(), options.args['--dir'] || '.')
const isCi = options.args['--ci'] || process.env.CI || false
const debug = !options.args['--no-debug'] && !isCi
const openBrowser = !options.args['--no-browser'] && !isCi
const build = !options.args['--skip-build']
const watch = !options.args['--no-watch']
const withDataLayer = options.args['--data-layer']
const enableWeb3 = options.args['--web3']
const isHub = !!options.args['--hub']
const skipClient = !!options.args['--no-client']
const bevyWeb = !!options.args['--bevy-web']
const isMobile = !!options.args['--mobile']
const explorerAlpha = !options.args['--web-explorer'] && !bevyWeb
const explorerAlpha = !bevyWeb

let hasSmartWearable = false
const workspace = await getValidWorkspace(options.components, workingDirectory)

/* istanbul ignore if */
if (workspace.projects.length > 1)
printWarning(options.components.logger, 'Support for multiple projects is still experimental.')

for (const project of workspace.projects) {
if (project.kind === 'smart-wearable') hasSmartWearable = true
if (project.kind === 'scene' || project.kind === 'smart-wearable') {
printCurrentProjectStarting(options.components.logger, project, workspace)

Expand Down Expand Up @@ -218,12 +213,7 @@ export async function main(options: Options) {
await wireRouter(components, workspace, dataLayer)
if (watch) {
for (const project of workspace.projects) {
await wireFileWatcherToWebSockets(
components,
project.workingDirectory,
project.kind,
!!explorerAlpha || !!bevyWeb
)
await wireFileWatcherToWebSockets(components, project.workingDirectory, project.kind)
}
}
await startComponents()
Expand All @@ -232,23 +222,11 @@ export async function main(options: Options) {
const availableURLs: string[] = []

printProgressInfo(options.components.logger, 'Preview server is now running!')
if (!explorerAlpha) {
components.logger.log('Available on:\n')
}

Object.keys(networkInterfaces).forEach((dev) => {
;(networkInterfaces[dev] || []).forEach((details) => {
if (details.family === 'IPv4') {
const oldBackpack = 'DISABLE_backpack_editor_v2=&ENABLE_backpack_editor_v1'
let addr = `http://${details.address}:${port}?position=${baseCoords.x}%2C${baseCoords.y}&${oldBackpack}`
if (debug) {
addr = `${addr}&SCENE_DEBUG_PANEL`
}
if (enableWeb3 || hasSmartWearable) {
addr = `${addr}&ENABLE_WEB3`
}

availableURLs.push(addr)
availableURLs.push(`http://${details.address}:${port}`)
}
})
})
Expand All @@ -260,17 +238,20 @@ export async function main(options: Options) {
const sortedURLs = availableURLs.sort((a, _b) => {
return a.toLowerCase().includes('localhost') || a.includes('127.0.0.1') || a.includes('0.0.0.0') ? -1 : 1
})
const bevyUrl = `https://decentraland.zone/bevy-web/?preview=true&realm=${
const bevyUrl = `https://decentraland.org/bevy-web/?preview=true&realm=${
new URL(sortedURLs[0]).origin
}&position=${baseCoords.x},${baseCoords.y}`
if (!explorerAlpha) {
if (bevyWeb) {
components.logger.log(` ${bevyUrl}`)
} else {
for (const addr of sortedURLs) {
components.logger.log(` ${addr}`)
}
}
if (bevyWeb) {
components.logger.log('Available on:\n')
components.logger.log(` ${bevyUrl}`)
printWarning(
components.logger,
'Chromium-based browsers require permission for websites to reach localhost (Local Network Access).\n' +
'When the browser asks to access apps on your device, click "Allow".\n' +
'If the scene never loads and no prompt appears, enable it manually and reload:\n' +
' chrome://settings/content/siteDetails?site=https%3A%2F%2Fdecentraland.org\n' +
' → "Apps on device" (Chrome 145+) or "Local network access" (Chrome 142-144) → Allow'
)
}
components.logger.log('\nPress CTRL+C to exit\n')

Expand All @@ -290,11 +271,9 @@ export async function main(options: Options) {
})
}

// Open preferably localhost/127.0.0.1
if ((!explorerAlpha || bevyWeb) && openBrowser && !skipClient && sortedURLs.length) {
if (bevyWeb && openBrowser && !skipClient && sortedURLs.length) {
try {
const url = bevyWeb ? bevyUrl : sortedURLs[0]
await open(url)
await open(bevyUrl)
} catch (_) {
components.logger.warn('Unable to open browser automatically.')
}
Expand Down
109 changes: 10 additions & 99 deletions packages/@dcl/sdk-commands/src/commands/start/server/endpoints.ts
Original file line number Diff line number Diff line change
Expand Up @@ -174,7 +174,16 @@ export async function setupEcs6Endpoints(
}
})

serveStatic(components, workspace, router)
router.get('/feature-flags/:file', async (ctx) => {
const res = await components.fetch.fetch(`https://feature-flags.decentraland.zone/${ctx.params.file}`, {
headers: {
connection: 'close'
}
})
return {
body: await res.arrayBuffer()
}
})

// TODO: get workspace scenes & wearables...

Expand Down Expand Up @@ -421,104 +430,6 @@ async function getSceneJson(
return resultEntities
}

function serveStatic(
components: Pick<CliComponents, 'fs' | 'fetch'>,
workspace: Workspace,
router: Router<PreviewComponents>
) {
const sdkPath = path.dirname(
require.resolve('@dcl/sdk/package.json', {
paths: [workspace.rootWorkingDirectory, ...workspace.projects.map(($) => $.workingDirectory)]
})
)
const dclExplorerJsonPath = path.dirname(
require.resolve('@dcl/explorer/package.json', {
paths: [workspace.rootWorkingDirectory, ...workspace.projects.map(($) => $.workingDirectory), sdkPath]
})
)

const dclKernelDefaultProfilePath = path.resolve(dclExplorerJsonPath, 'default-profile')
const dclKernelImagesDecentralandConnect = path.resolve(dclExplorerJsonPath, 'images', 'decentraland-connect')

const routes = [
{
route: '/',
path: path.resolve(dclExplorerJsonPath, 'preview.html'),
type: 'text/html'
},
{
route: '/favicon.ico',
path: path.resolve(dclExplorerJsonPath, 'favicon.ico'),
type: 'text/html'
},
{
route: '/@/explorer/index.js',
path: path.resolve(dclExplorerJsonPath, 'index.js'),
type: 'text/javascript'
}
]

for (const route of routes) {
router.get(route.route, async (_ctx) => {
return {
headers: { 'Content-Type': route.type },
body: components.fs.createReadStream(route.path)
}
})
}

function createStaticRoutes(
components: Pick<CliComponents, 'fs'>,
route: string,
folder: string,
transform = (str: string) => str
) {
router.get(route, async (ctx, next) => {
const file = ctx.params.path
const fullPath = path.resolve(folder, transform(file))

// only return files IF the file is within a baseFolder
if (!(await components.fs.fileExists(fullPath))) {
return next()
}

if (await components.fs.directoryExists(fullPath)) {
return { status: 404 }
}

const headers: Record<string, any> = {
'x-timestamp': Date.now(),
'x-sent': true,
'cache-control': 'no-cache,private,max-age=1'
}

if (fullPath.endsWith('.wasm')) {
headers['content-type'] = 'application/wasm'
}

return {
headers,
body: components.fs.createReadStream(fullPath)
}
})
}

createStaticRoutes(components, '/images/decentraland-connect/:path+', dclKernelImagesDecentralandConnect)
createStaticRoutes(components, '/default-profile/:path+', dclKernelDefaultProfilePath)
createStaticRoutes(components, '/@/explorer/:path+', dclExplorerJsonPath, (filePath) => filePath.replace(/.br+$/, ''))

router.get('/feature-flags/:file', async (ctx) => {
const res = await components.fetch.fetch(`https://feature-flags.decentraland.zone/${ctx.params.file}`, {
headers: {
connection: 'close'
}
})
return {
body: await res.arrayBuffer()
}
})
}

async function fakeEntityV3FromProject(
components: Pick<CliComponents, 'fs' | 'logger'>,
project: ProjectUnion,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,11 @@ import { debounce } from '../../../logic/debounce'
/**
* This function gets file modification events and sends them to all the connected
* websockets, it is used to hot-reload assets of the scene.
*
* IMPORTANT: this is a legacy protocol and needs to be revisited for SDK7
*/
export async function wireFileWatcherToWebSockets(
components: Pick<PreviewComponents, 'fs' | 'ws' | 'logger'>,
projectRoot: string,
projectKind: ProjectUnion['kind'],
desktopClient: boolean
projectKind: ProjectUnion['kind']
) {
const ignored = await getDCLIgnorePatterns(components, projectRoot)
const sceneId = b64HashingFunction(projectRoot)
Expand All @@ -36,17 +33,15 @@ export async function wireFileWatcherToWebSockets(
cwd: projectRoot
})
.on('unlink', (_: unknown, file: string) => {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] Pre-existing bug: chokidar unlink fires with a single (path) argument, not (event, path). Here _ captures the path and file is always undefined, making removeModel a no-op.

Suggested fix:

Suggested change
.on('unlink', (_: unknown, file: string) => {
.on('unlink', (file: string) => {
removeModel(sceneId, file)
})

if (desktopClient) {
return removeModel(sceneId, file)
}
removeModel(sceneId, file)
})
.on(
'all',
debounce(async (a, file) => {
if (desktopClient) {
updateScene(sceneId, file)
}
return __LEGACY__updateScene(projectRoot, sceneUpdateClients, projectKind)
debounce(async (_, file) => {
updateScene(sceneId, file)
// Legacy JSON protocol: still the only one Bevy and Godot explorers understand.
// Remove once both consume the protobuf WsSceneMessage.
__LEGACY__updateScene(projectRoot, sceneUpdateClients, projectKind)
}, 800)
)
}
Expand Down Expand Up @@ -95,7 +90,7 @@ function sendSceneMessage(sceneMessage: WsSceneMessage) {
}

/**
* @deprecated old explorer (kernel)
* @deprecated legacy JSON protocol, consumed by Bevy and Godot explorers
*/
export function __LEGACY__updateScene(dir: string, clients: Set<WebSocket>, projectKind: ProjectUnion['kind']): void {
for (const client of clients) {
Expand All @@ -105,7 +100,6 @@ export function __LEGACY__updateScene(dir: string, clients: Set<WebSocket>, proj
payload: { sceneId: b64HashingFunction(dir), sceneType: projectKind }
}

// Old explorer
client.send(sdk.UPDATE, { binary: false })
client.send(JSON.stringify(message), { binary: false })
}
Expand Down
1 change: 0 additions & 1 deletion packages/@dcl/sdk/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
"dependencies": {
"@dcl/ecs": "file:../ecs",
"@dcl/ecs-math": "2.1.0",
"@dcl/explorer": "1.0.164509-20240802172549.commit-fb95b9b",
"@dcl/js-runtime": "file:../js-runtime",
"@dcl/react-ecs": "file:../react-ecs",
"@dcl/sdk-commands": "file:../sdk-commands"
Expand Down
9 changes: 1 addition & 8 deletions test/kernel-and-renderer-version.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,14 +27,7 @@ describe('Check there is fixed version', () => {

it('should @dcl/sdk has fixed version dependencies', async () => {
const sdkDeps = checkDeps(SDK_PATH)
const requiredDependencies = [
'@dcl/sdk-commands',
'@dcl/ecs-math',
'@dcl/ecs',
'@dcl/js-runtime',
'@dcl/explorer',
'@dcl/react-ecs'
]
const requiredDependencies = ['@dcl/sdk-commands', '@dcl/ecs-math', '@dcl/ecs', '@dcl/js-runtime', '@dcl/react-ecs']

const dependencies = Object.keys(sdkDeps)

Expand Down
1 change: 0 additions & 1 deletion test/snapshots/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading